mirror of
https://github.com/0xMarcio/cve.git
synced 2026-10-23 16:35:19 +00:00
Fix PoC discovery, freshness and source validation
This commit is contained in:
1 parent
d474e60c83
commit
82ab6a1c19
493 files changed
+1939
-3528
No files matched your search
@@ -1,7 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
@@ -85,5 +87,56 @@ class ReferenceAuditTests(unittest.TestCase):
|
||||
self.assertEqual([call.args[0].method for call in fetch.call_args_list], ["HEAD", "GET"])
|
||||
|
||||
|
||||
class KevRefreshTests(unittest.TestCase):
|
||||
def row(self, cve: str = "CVE-2026-12345") -> dict:
|
||||
return {"cveID": cve, "dateAdded": "2026-10-06", "knownRansomwareCampaignUse": "Known"}
|
||||
|
||||
def test_incomplete_or_invalid_catalogue_preserves_stored_flags(self) -> None:
|
||||
row = self.row()
|
||||
invalid = [
|
||||
{"count": 2, "vulnerabilities": [row]},
|
||||
{"count": 2, "vulnerabilities": [row, row]},
|
||||
{"count": 1, "vulnerabilities": [self.row("NOT-A-CVE")]},
|
||||
{"count": 1, "vulnerabilities": [{"cveID": "CVE-2026-12345"}]},
|
||||
{"vulnerabilities": [row]},
|
||||
{"count": True, "vulnerabilities": [row]},
|
||||
]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "kev.json"
|
||||
old = '{"CVE-2026-10000":["2026-01-01",0]}\n'
|
||||
for payload in invalid:
|
||||
path.write_text(old, encoding="utf-8")
|
||||
with self.subTest(payload=payload), patch.object(audit_poc_links, "KEV_FILE", path), patch.object(
|
||||
audit_poc_links, "http_json", return_value=payload
|
||||
):
|
||||
audit_poc_links.refresh_kev(dry_run=False)
|
||||
self.assertEqual(path.read_text(), old)
|
||||
|
||||
def test_complete_catalogue_can_remove_entries(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "kev.json"
|
||||
for rows in ([self.row()], []):
|
||||
path.write_text('{"CVE-2026-10000":["2026-01-01",0]}\n', encoding="utf-8")
|
||||
with self.subTest(rows=rows), patch.object(audit_poc_links, "KEV_FILE", path), patch.object(
|
||||
audit_poc_links, "http_json", return_value={"count": len(rows), "vulnerabilities": rows}
|
||||
):
|
||||
self.assertEqual(audit_poc_links.refresh_kev(dry_run=False), len(rows))
|
||||
expected = {"CVE-2026-12345": ["2026-10-06", 1]} if rows else {}
|
||||
self.assertEqual(json.loads(path.read_text()), expected)
|
||||
|
||||
def test_failed_atomic_replace_preserves_catalogue(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = Path(directory) / "kev.json"
|
||||
old = '{"CVE-2026-10000":["2026-01-01",0]}\n'
|
||||
path.write_text(old, encoding="utf-8")
|
||||
with patch.object(audit_poc_links, "KEV_FILE", path), patch.object(
|
||||
audit_poc_links, "http_json", return_value={"count": 1, "vulnerabilities": [self.row()]}
|
||||
), patch.object(Path, "replace", side_effect=OSError("Replace unavailable")):
|
||||
with self.assertRaisesRegex(OSError, "Replace unavailable"):
|
||||
audit_poc_links.refresh_kev(dry_run=False)
|
||||
self.assertEqual(path.read_text(), old)
|
||||
self.assertEqual(list(Path(directory).iterdir()), [path])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,179 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import tempfile
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location("getTrending", str(ROOT / ".github" / "getTrending.py"))
|
||||
trending = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(trending)
|
||||
|
||||
NOW = datetime.now(timezone.utc)
|
||||
CVE = f"CVE-{NOW.year}-21589"
|
||||
|
||||
|
||||
def stamp(days: float) -> str:
|
||||
return (NOW - timedelta(days=days)).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
|
||||
def repo(full_name: str, created: object, stars: int = 0, description: str = "Proof of concept") -> dict:
|
||||
return {
|
||||
"full_name": full_name,
|
||||
"name": full_name.split("/", 1)[1],
|
||||
"html_url": f"https://github.com/{full_name}",
|
||||
"description": description,
|
||||
"stargazers_count": stars,
|
||||
"created_at": created,
|
||||
"pushed_at": stamp(0.1),
|
||||
}
|
||||
|
||||
|
||||
def gate(query: str, token: str) -> dict:
|
||||
"""GraphQL content for the real PoC gate: a README and an exploit at the root."""
|
||||
return {
|
||||
alias: {
|
||||
"readmeMd": {"text": f"Proof of concept for {name}"},
|
||||
"root": {"entries": [{"name": "exploit.py", "type": "blob"}]},
|
||||
}
|
||||
for alias, name in re.findall(r'(r\d+): repository\(owner: "[^"]*", name: "([^"]*)"\)', query)
|
||||
}
|
||||
|
||||
|
||||
def shipped(names: list[str], token: str, paths: dict | None = None) -> dict[str, str]:
|
||||
return {name: stamp(0.2) for name in names}
|
||||
|
||||
|
||||
class JustLandedTests(unittest.TestCase):
|
||||
def test_lists_new_repositories_for_any_cve_year_once(self) -> None:
|
||||
rows = [
|
||||
repo("example/CVE-2019-0708-poc", stamp(1)),
|
||||
repo("example/CVE-2021-44228-rce", stamp(400)),
|
||||
repo("example/cve-exploits", stamp(1), description="Exploit for CVE-2021-44228"),
|
||||
repo("example/CVE-2024-1111", None),
|
||||
repo("example/CVE-2024-2222", "last week"),
|
||||
repo(f"Example/{CVE}", stamp(1), stars=5),
|
||||
repo(f"example/{CVE.lower()}", stamp(1)),
|
||||
]
|
||||
queries: list[str] = []
|
||||
|
||||
def search(query: str) -> list[dict]:
|
||||
queries.append(query)
|
||||
return rows
|
||||
|
||||
with patch.multiple(trending, search=search, graphql=gate, code_pushed=shipped), \
|
||||
redirect_stdout(io.StringIO()):
|
||||
landed = trending.just_landed("")
|
||||
|
||||
self.assertCountEqual(
|
||||
[found["full_name"] for found in landed],
|
||||
["example/CVE-2019-0708-poc", f"Example/{CVE}"],
|
||||
)
|
||||
self.assertEqual(len(queries), 1)
|
||||
self.assertIn("created:>=", queries[0])
|
||||
self.assertNotRegex(queries[0], r"pushed:|CVE-\d{4}")
|
||||
|
||||
|
||||
class MainTests(unittest.TestCase):
|
||||
def test_new_trending_repository_is_in_both_sections_and_written_once(self) -> None:
|
||||
both = repo(f"MarcusProgram/{CVE}", stamp(0.3), stars=5)
|
||||
older = repo(f"example/CVE-{NOW.year}-1000-poc", stamp(300), stars=40)
|
||||
fresh = repo("example/CVE-2019-0708-poc", stamp(1))
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
readme, output = Path(tmp, "README.md"), Path(tmp, "trending.json")
|
||||
with patch.multiple(
|
||||
trending,
|
||||
README=str(readme),
|
||||
TRENDING=str(output),
|
||||
figures=lambda: {"total_cves": 3, "with_pocs": 2, "kev": 1},
|
||||
known_exploited=lambda: {CVE},
|
||||
search_year=lambda year, since: (2, [dict(both), dict(older)]) if year == NOW.year else (0, []),
|
||||
search=lambda query: [dict(fresh), dict(both), dict(older)],
|
||||
graphql=gate,
|
||||
code_pushed=shipped,
|
||||
), redirect_stdout(io.StringIO()) as log:
|
||||
self.assertEqual(trending.main(), 0)
|
||||
text = readme.read_text(encoding="utf-8")
|
||||
items = json.loads(output.read_text(encoding="utf-8"))
|
||||
|
||||
landed, trend = text.split("## Just landed", 1)[1].split(f"## Trending in {NOW.year}", 1)
|
||||
self.assertIn(f"]({both['html_url']})", landed)
|
||||
self.assertIn(f"]({both['html_url']})", trend)
|
||||
self.assertNotIn(older["html_url"], landed)
|
||||
self.assertCountEqual(
|
||||
[(item["url"], item.get("landed", False), item["kev"]) for item in items],
|
||||
[
|
||||
(both["html_url"], True, True),
|
||||
(older["html_url"], False, False),
|
||||
(fresh["html_url"], True, False),
|
||||
],
|
||||
)
|
||||
self.assertIn("(3 repositories, 1 known-exploited)", log.getvalue())
|
||||
|
||||
|
||||
class ArtifactDateTests(unittest.TestCase):
|
||||
def test_commit_clock_cannot_postdate_the_repository_push(self) -> None:
|
||||
pushed = stamp(0.2)
|
||||
payload = {"r0": {
|
||||
"pushedAt": pushed,
|
||||
"defaultBranchRef": {"target": {
|
||||
"p0": {"nodes": [{"committedDate": stamp(-1)}]},
|
||||
"p1": {"nodes": [{"committedDate": stamp(1)}]},
|
||||
}},
|
||||
}}
|
||||
with patch.object(trending, "graphql", return_value=payload) as query:
|
||||
result = trending.code_pushed(["owner/repo"], "test", {"owner/repo": ["exploit.py", "poc.c"]})
|
||||
self.assertEqual(result, {"owner/repo": pushed})
|
||||
self.assertIn("pushedAt", query.call_args.args[0])
|
||||
|
||||
def test_future_timestamp_does_not_wrap_into_yesterdays_age(self) -> None:
|
||||
self.assertEqual(trending.time_ago(stamp(-0.1)), "just now")
|
||||
|
||||
|
||||
class SearchTests(unittest.TestCase):
|
||||
def replies(self, *payloads: dict) -> tuple[list[str], object]:
|
||||
urls: list[str] = []
|
||||
queue = iter(payloads)
|
||||
|
||||
def urlopen(req, timeout):
|
||||
urls.append(req.full_url)
|
||||
return io.BytesIO(json.dumps(next(queue)).encode())
|
||||
|
||||
return urls, urlopen
|
||||
|
||||
def test_reads_every_page_in_rest_sort_order(self) -> None:
|
||||
urls, urlopen = self.replies(
|
||||
{"total_count": 101, "incomplete_results": False,
|
||||
"items": [{"full_name": f"a/{n}"} for n in range(100)]},
|
||||
{"total_count": 101, "incomplete_results": False, "items": [{"full_name": "a/100"}]},
|
||||
)
|
||||
with patch.object(trending.request, "urlopen", urlopen):
|
||||
self.assertEqual(len(trending.search("CVE in:name")), 101)
|
||||
params = [parse_qs(urlparse(url).query) for url in urls]
|
||||
self.assertEqual(
|
||||
[(p["sort"], p["order"], p["page"]) for p in params],
|
||||
[(["updated"], ["desc"], ["1"]), (["updated"], ["desc"], ["2"])],
|
||||
)
|
||||
|
||||
def test_incomplete_results_are_retried_and_never_returned(self) -> None:
|
||||
partial = {"total_count": 9, "incomplete_results": True, "items": [{"full_name": "a/b"}]}
|
||||
complete = {"total_count": 1, "incomplete_results": False, "items": [{"full_name": "a/b"}]}
|
||||
with patch.object(trending.time, "sleep"):
|
||||
_, urlopen = self.replies(partial, complete)
|
||||
with patch.object(trending.request, "urlopen", urlopen):
|
||||
self.assertEqual(trending.search("CVE in:name"), [{"full_name": "a/b"}])
|
||||
_, urlopen = self.replies(partial, partial, partial)
|
||||
with patch.object(trending.request, "urlopen", urlopen), self.assertRaises(RuntimeError):
|
||||
trending.search("CVE in:name")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,100 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
|
||||
import sync_collections
|
||||
|
||||
|
||||
class ResearchCollectionTests(unittest.TestCase):
|
||||
def tree(self, *paths: str) -> dict:
|
||||
return {"truncated": False, "tree": [
|
||||
{"path": path, "type": "blob", "mode": "100644", "size": 100}
|
||||
for path in paths
|
||||
]}
|
||||
|
||||
def test_cve_directory_requires_its_own_artifact(self) -> None:
|
||||
payload = self.tree(
|
||||
"pocs/linux/CVE-2024-1001/exploit/lts/exploit.c",
|
||||
"pocs/linux/CVE-2024-1001/docs/exploit.md",
|
||||
"pocs/linux/CVE-2024-1002/README.md",
|
||||
"pocs/linux/CVE-2024-1002/Makefile",
|
||||
"pocs/linux/CVE-2024-1002/helpers.c",
|
||||
"pocs/linux/CVE-2024-1002/tests/poc.py",
|
||||
"pocs/linux/CVE-2024-1002/docs/poc.py",
|
||||
"pocs/linux/CVE-2024-1003-and-CVE-2024-1004/exploit.c",
|
||||
"pocs/linux/CVE-2024-1005/CVE-2024-1006/exploit.c",
|
||||
"advisories/CVE-2024-1007/exploit.c",
|
||||
)
|
||||
self.assertEqual(sync_collections.collect_research_tree(
|
||||
"google/security-research", "master", "pocs/", payload,
|
||||
), {"CVE-2024-1001": [
|
||||
"https://github.com/google/security-research/tree/master/pocs/linux/CVE-2024-1001",
|
||||
]})
|
||||
|
||||
def test_security_lab_reproduction_files_and_code_qualify(self) -> None:
|
||||
paths = (
|
||||
"SecurityExploits/libcue/track_set_index_CVE-2023-43641/CVE-2023-43641-poc-simple.cue",
|
||||
"SecurityExploits/poppler/CVE-2025-52885/bug.pdf",
|
||||
"SecurityExploits/poppler/CVE-2025-52886/pdfgen.cpp",
|
||||
"SecurityExploits/DjVuLibre/CVE-2025-53367/fuzzer-poc.djvu",
|
||||
"SecurityExploits/libssh2/CVE-2019-17498/server/home/poc.bin",
|
||||
"SecurityExploits/unknown/CVE-2024-1001/writeup.pdf",
|
||||
)
|
||||
found = sync_collections.collect_research_tree(
|
||||
"github/securitylab", "main", "SecurityExploits/", self.tree(*paths),
|
||||
)
|
||||
self.assertEqual(set(found), {
|
||||
"CVE-2023-43641", "CVE-2025-52885", "CVE-2025-52886", "CVE-2025-53367", "CVE-2019-17498",
|
||||
})
|
||||
|
||||
def test_truncated_or_missing_tree_is_not_a_complete_source(self) -> None:
|
||||
for payload in ({"truncated": True, "tree": []}, {"tree": []}, {"truncated": False}):
|
||||
with self.subTest(payload=payload), self.assertRaises(ValueError):
|
||||
sync_collections.collect_research_tree("google/security-research", "master", "pocs/", payload)
|
||||
|
||||
def test_reviewed_exploit_patches_qualify_but_generic_text_does_not(self) -> None:
|
||||
payload = self.tree(
|
||||
"SecurityExploits/libssh/pubkey-auth-bypass-CVE-2023-2283/attacker/home/diff.txt",
|
||||
"SecurityExploits/libssh2/out_of_bounds_read_kex_CVE-2019-13115/server/home/diff.txt",
|
||||
"SecurityExploits/strongSwan/CVE-2018-5388/stroke_patch.txt",
|
||||
"SecurityExploits/unknown/CVE-2024-1001/diff.txt",
|
||||
"SecurityExploits/unknown/CVE-2024-1002/exploit.patch",
|
||||
"SecurityExploits/unknown/CVE-2024-1003/stroke_patch.txt",
|
||||
)
|
||||
found = sync_collections.collect_research_tree("github/securitylab", "main", "SecurityExploits/", payload)
|
||||
self.assertEqual(set(found), {"CVE-2023-2283", "CVE-2019-13115", "CVE-2018-5388"})
|
||||
|
||||
def test_failed_research_source_stops_before_any_index_mutation(self) -> None:
|
||||
with patch.object(sys, "argv", ["sync_collections.py"]), \
|
||||
patch.object(sync_collections, "SOURCES", ()), \
|
||||
patch.object(sync_collections, "download_tree", return_value={"truncated": True, "tree": []}), \
|
||||
patch.object(sync_collections, "ensure_cve_entries") as ensure:
|
||||
self.assertEqual(sync_collections.main(), 1)
|
||||
ensure.assert_not_called()
|
||||
|
||||
def test_research_links_merge_with_existing_collection_sources(self) -> None:
|
||||
cid = "CVE-2024-1001"
|
||||
payload = self.tree(f"pocs/linux/{cid}/exploit.c")
|
||||
old_url = "https://github.com/chaitin/xray/blob/master/pocs/example.yml"
|
||||
new_url = f"https://github.com/google/security-research/tree/master/pocs/linux/{cid}"
|
||||
with patch.object(sys, "argv", ["sync_collections.py"]), \
|
||||
patch.object(sync_collections, "SOURCES", (("xray", "chaitin/xray", "master"),)), \
|
||||
patch.object(sync_collections, "TREE_SOURCES", (("google", "google/security-research", "master", "pocs/"),)), \
|
||||
patch.object(sync_collections, "download", return_value=b"archive"), \
|
||||
patch.object(sync_collections, "collect", return_value={cid: [old_url]}), \
|
||||
patch.object(sync_collections, "download_tree", return_value=payload), \
|
||||
patch.object(sync_collections, "ensure_cve_entries", return_value=(set(), set())), \
|
||||
patch.object(sync_collections, "apply_links", return_value="unchanged") as apply, \
|
||||
patch.object(sync_collections, "CVES") as cves:
|
||||
cves.glob.return_value = []
|
||||
self.assertEqual(sync_collections.main(), 0)
|
||||
apply.assert_called_once_with(cid, sorted([old_url, new_url]), dry_run=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,163 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import io
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
|
||||
import build_site
|
||||
import sync_cve_metadata as metadata
|
||||
import update_cves
|
||||
|
||||
|
||||
CVE = "CVE-2026-12345"
|
||||
URL = "https://github.com/advisories/GHSA-abcd-efgh-2345"
|
||||
SCORE = ["3.1", 8.1, "HIGH", "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N", "CNA", "CVE Program CNA"]
|
||||
|
||||
|
||||
class AdvisoryLifecycleTests(unittest.TestCase):
|
||||
def merge(self, entries: dict, cache: dict) -> None:
|
||||
with patch.object(metadata, "enrich_fallback_metrics"), patch.object(
|
||||
metadata, "load_vendor_entries", return_value={}
|
||||
):
|
||||
metadata.merge_enrichment(entries, set(entries), cache)
|
||||
|
||||
def test_revocation_removes_only_github_owned_exploit_evidence(self) -> None:
|
||||
for independent in (False, True):
|
||||
for withdrawn in (False, True):
|
||||
with self.subTest(independent=independent, withdrawn=withdrawn):
|
||||
tags = ["Vendor Advisory", *(["Exploit"] if independent else [])]
|
||||
entries = {CVE: {"advisories": [[URL, tags]]}}
|
||||
self.merge(entries, {"GHSA": [CVE, URL, [], 1]})
|
||||
self.assertTrue(build_site.reference_is_verified_poc(CVE, URL, entries, set()))
|
||||
self.merge(entries, {} if withdrawn else {"GHSA": [CVE, URL, [], 0]})
|
||||
self.assertEqual(
|
||||
build_site.reference_is_verified_poc(CVE, URL, entries, set()), independent
|
||||
)
|
||||
self.assertIn("Vendor Advisory", entries[CVE]["advisories"][0][1])
|
||||
|
||||
def test_legacy_cache_only_advisory_is_removed(self) -> None:
|
||||
entries = {CVE: {"advisories": [[URL, ["GitHub Advisory", "Exploit"]]]}}
|
||||
self.merge(entries, {})
|
||||
self.assertFalse(build_site.reference_is_verified_poc(CVE, URL, entries, set()))
|
||||
self.assertFalse(entries[CVE].get("advisories"))
|
||||
|
||||
def test_legacy_mixed_source_evidence_is_preserved(self) -> None:
|
||||
entries = {CVE: {"advisories": [[URL, ["Vendor Advisory", "GitHub Advisory", "Exploit"]]]}}
|
||||
self.merge(entries, {})
|
||||
self.assertEqual(entries[CVE]["advisories"], [[URL, ["Vendor Advisory", "Exploit"]]])
|
||||
|
||||
def test_withdrawal_prunes_only_unverified_github_owned_references(self) -> None:
|
||||
cves = [CVE, "CVE-2026-12346", "CVE-2026-12347"]
|
||||
entries = {cve: {"advisories": [[URL, ["GitHub Advisory", "Exploit"]]]} for cve in cves}
|
||||
entries[cves[2]]["advisories"][0][1].append("Vendor Advisory")
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "2026").mkdir()
|
||||
for cve in cves:
|
||||
(root / "2026" / f"{cve}.md").write_text(
|
||||
"### Description\n\nKeep this description.\n\n### POC\n\n"
|
||||
f"#### Reference\n- {URL}\n- https://example.com/independent\n\n"
|
||||
"#### Github\nNo PoCs found on GitHub currently.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
inventory = root / "references.txt"
|
||||
inventory.write_text("".join(f"{cve} - {URL}\n" for cve in cves), encoding="utf-8")
|
||||
verified = root / "reference_pocs.txt"
|
||||
verified.write_text(f"{cves[1]} - {URL}\n", encoding="utf-8")
|
||||
with patch.object(metadata, "CVES", root), patch.object(
|
||||
update_cves, "REFERENCE_LIST", inventory
|
||||
), patch.object(update_cves, "VERIFIED_REFERENCE_LIST", verified):
|
||||
refreshed = run_sync(entries, [], dry_run=False, fallback=False)
|
||||
for index, cve in enumerate(cves):
|
||||
text = (root / "2026" / f"{cve}.md").read_text(encoding="utf-8")
|
||||
self.assertEqual(URL in text, index != 0)
|
||||
self.assertIn("Keep this description.", text)
|
||||
self.assertIn("https://example.com/independent", text)
|
||||
self.assertEqual(f"{cve} - {URL}" in inventory.read_text(), index != 0)
|
||||
self.assertEqual(verified.read_text(), f"{cves[1]} - {URL}\n")
|
||||
with patch.object(metadata, "CVES", root), patch.object(
|
||||
update_cves, "REFERENCE_LIST", inventory
|
||||
), patch.object(update_cves, "VERIFIED_REFERENCE_LIST", verified):
|
||||
run_sync(refreshed, [{"cve": {"id": cves[2], "references": [
|
||||
{"url": URL, "tags": ["Vendor Advisory"]}
|
||||
]}}], dry_run=False, fallback=False)
|
||||
self.assertNotIn(URL, (root / "2026" / f"{cves[2]}.md").read_text())
|
||||
self.assertNotIn(f"{cves[2]} - {URL}", inventory.read_text())
|
||||
|
||||
|
||||
def run_sync(entries: dict, feed: list, *, full: bool = False, dry_run: bool = True, fallback: bool = True) -> dict:
|
||||
with patch.object(metadata, "parse_args", return_value=Namespace(
|
||||
full=full, github_full=False, dry_run=dry_run, year=[] if full else [2026]
|
||||
)), patch.object(metadata, "sync_github_cache", return_value={}), patch.object(
|
||||
metadata, "sync_github_pocs"
|
||||
), patch.object(metadata, "local_cves", return_value=set(entries)), patch.object(
|
||||
metadata, "load_advisory_rules", return_value=[]
|
||||
), patch.object(metadata, "load_shards", return_value=copy.deepcopy(entries)), patch.object(
|
||||
metadata, "load_feed", return_value=feed
|
||||
), patch.object(metadata, "load_vendor_entries", return_value={}), patch.object(
|
||||
metadata, "write_shards", return_value=(1, 0)
|
||||
) as write, patch.object(
|
||||
metadata, "enrich_fallback_metrics",
|
||||
wraps=metadata.enrich_fallback_metrics if fallback else lambda *args, **kwargs: None,
|
||||
), redirect_stdout(io.StringIO()):
|
||||
metadata.main()
|
||||
return write.call_args.args[0]
|
||||
|
||||
|
||||
class FallbackLifecycleTests(unittest.TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.old = {CVE: {"cvss": [SCORE], "advisories": [["https://example.com/advisory", ["Vendor Advisory"]]]}}
|
||||
self.feed = [{"cve": {"id": CVE, "references": [
|
||||
{"url": "https://example.com/advisory", "tags": ["Vendor Advisory"]}
|
||||
]}}]
|
||||
|
||||
def test_failed_or_incomplete_refresh_preserves_scores_in_incremental_and_full_runs(self) -> None:
|
||||
for full in (False, True):
|
||||
for response in (TimeoutError("Source unavailable"), {}, {"cveMetadata": {"cveId": "CVE-2026-99999"}}):
|
||||
with self.subTest(full=full, response=response), patch.object(
|
||||
metadata, "load_json_url",
|
||||
side_effect=response if isinstance(response, Exception) else lambda *args, **kwargs: response,
|
||||
):
|
||||
result = run_sync(self.old, self.feed, full=full)
|
||||
self.assertEqual(result[CVE].get("cvss"), [SCORE])
|
||||
|
||||
def test_confirmed_removal_clears_stale_scores(self) -> None:
|
||||
empty_record = {"cveMetadata": {"cveId": CVE, "state": "PUBLISHED"}, "containers": {"cna": {}}}
|
||||
for full in (False, True):
|
||||
for response in (None, empty_record):
|
||||
with self.subTest(full=full, response=response), patch.object(metadata, "load_json_url", return_value=response):
|
||||
result = run_sync(self.old, self.feed, full=full)
|
||||
self.assertNotIn("cvss", result[CVE])
|
||||
|
||||
def test_one_failed_source_keeps_prior_score(self) -> None:
|
||||
def fetch(url: str, **kwargs: object) -> None:
|
||||
if url.startswith(metadata.VULNRICHMENT_ROOT):
|
||||
raise TimeoutError("CISA unavailable")
|
||||
return None
|
||||
|
||||
with patch.object(metadata, "load_json_url", side_effect=fetch):
|
||||
result = run_sync(self.old, self.feed, full=True)
|
||||
self.assertEqual(result[CVE]["cvss"], [SCORE])
|
||||
|
||||
def test_successful_refresh_replaces_prior_score(self) -> None:
|
||||
record = {
|
||||
"cveMetadata": {"cveId": CVE, "state": "PUBLISHED"},
|
||||
"containers": {"cna": {"providerMetadata": {"shortName": "CNA"}, "metrics": [{
|
||||
"cvssV3_1": {"baseScore": 9.8, "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"},
|
||||
}]}},
|
||||
}
|
||||
with patch.object(metadata, "load_json_url", return_value=record):
|
||||
result = run_sync(self.old, self.feed, full=True)
|
||||
self.assertEqual([row[1] for row in result[CVE]["cvss"]], [9.8])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,55 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
|
||||
import sync_nuclei
|
||||
|
||||
|
||||
CORE_VERSION_PROBE = '''id: CVE-2003-1598
|
||||
info:
|
||||
name: WordPress Core SQL Injection
|
||||
http:
|
||||
- method: GET
|
||||
path:
|
||||
- "{{BaseURL}}"
|
||||
- "{{BaseURL}}/wp-admin/install.php"
|
||||
- "{{BaseURL}}/feed/"
|
||||
- "{{BaseURL}}/?feed=rss2"
|
||||
matchers:
|
||||
- type: dsl
|
||||
dsl:
|
||||
- compare_versions(version_by_generator, '< 0.72')
|
||||
- compare_versions(version_by_js, '< 0.72')
|
||||
- compare_versions(version_by_css, '< 0.72')
|
||||
extractors:
|
||||
- type: regex
|
||||
name: version_by_generator
|
||||
regex:
|
||||
- 'wordpress.org/\\?v=([0-9.]+)'
|
||||
'''
|
||||
|
||||
|
||||
class VersionProbeTests(unittest.TestCase):
|
||||
def test_wordpress_core_version_probe_is_not_a_poc(self) -> None:
|
||||
self.assertTrue(sync_nuclei.version_probe_only(CORE_VERSION_PROBE))
|
||||
|
||||
def test_actual_exploit_is_kept_even_with_a_version_check(self) -> None:
|
||||
for change in (
|
||||
CORE_VERSION_PROBE.replace('"{{BaseURL}}/feed/"', '"{{BaseURL}}/?posts=1+UNION+SELECT+1"'),
|
||||
CORE_VERSION_PROBE.replace('"{{BaseURL}}/feed/"', "'{{BaseURL}}/?posts=1+UNION+SELECT+1'"),
|
||||
CORE_VERSION_PROBE.replace('"{{BaseURL}}/feed/"', '"{{RootURL}}/exploit"'),
|
||||
CORE_VERSION_PROBE.replace('"{{BaseURL}}/feed/"', '"https://example.com/exploit"'),
|
||||
CORE_VERSION_PROBE.replace("method: GET", "method: POST"),
|
||||
CORE_VERSION_PROBE + '\n raw: ["GET /exploit HTTP/1.1"]\n',
|
||||
CORE_VERSION_PROBE + '\ncode:\n - engine: [python3]\n source: exploit_code\n',
|
||||
):
|
||||
with self.subTest(template=change):
|
||||
self.assertFalse(sync_nuclei.version_probe_only(change))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,97 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
|
||||
import sync_pocingithub
|
||||
import update_cves
|
||||
|
||||
|
||||
class GitHubResponseTests(unittest.TestCase):
|
||||
def readers(self):
|
||||
client = update_cves.GitHubClient("test")
|
||||
return (
|
||||
("repo0", update_cves, lambda: client.fetch_readmes(["owner/repo"])),
|
||||
("r0", sync_pocingithub, lambda: sync_pocingithub.describe(client, ["owner/repo"])),
|
||||
)
|
||||
|
||||
def test_operational_graphql_errors_are_not_missing_repositories(self) -> None:
|
||||
for alias, module, read in self.readers():
|
||||
for error in (
|
||||
{"type": "RATE_LIMITED", "message": "Rate limited"},
|
||||
{"type": "INTERNAL", "message": "Internal error", "path": [alias]},
|
||||
{"type": "NOT_FOUND", "message": "Nested lookup failed", "path": [alias, "root"]},
|
||||
):
|
||||
with self.subTest(reader=alias, error=error["message"]):
|
||||
payload = {"data": {alias: None, "rateLimit": {"remaining": 100}}, "errors": [error]}
|
||||
with patch.object(module, "http_json", return_value=payload):
|
||||
with self.assertRaises(RuntimeError):
|
||||
read()
|
||||
|
||||
def test_missing_repository_is_tolerated(self) -> None:
|
||||
for alias, module, read in self.readers():
|
||||
with self.subTest(reader=alias):
|
||||
payload = {
|
||||
"data": {alias: None, "rateLimit": {"remaining": 100}},
|
||||
"errors": [{"type": "NOT_FOUND", "path": [alias], "message": "Repository missing"}],
|
||||
}
|
||||
with patch.object(module, "http_json", return_value=payload):
|
||||
result = read()
|
||||
self.assertFalse(result[0] if isinstance(result, tuple) else result)
|
||||
|
||||
def test_incomplete_response_is_not_a_missing_repository(self) -> None:
|
||||
for alias, module, read in self.readers():
|
||||
with self.subTest(reader=alias), patch.object(
|
||||
module, "http_json", return_value={"data": {"rateLimit": {"remaining": 100}}}
|
||||
):
|
||||
with self.assertRaises(RuntimeError):
|
||||
read()
|
||||
|
||||
def test_deleted_repository_does_not_discard_other_batch_results(self) -> None:
|
||||
client = update_cves.GitHubClient("test")
|
||||
names = ["owner/deleted", "owner/repo"]
|
||||
for prefix, module, read in (
|
||||
("repo", update_cves, lambda: client.fetch_readmes(names)),
|
||||
("r", sync_pocingithub, lambda: sync_pocingithub.describe(client, names)),
|
||||
):
|
||||
payload = {
|
||||
"data": {f"{prefix}0": None, f"{prefix}1": {"readmeMd": {"text": "PoC"}},
|
||||
"rateLimit": {"remaining": 100}},
|
||||
"errors": [{"type": "NOT_FOUND", "path": [f"{prefix}0"]}],
|
||||
}
|
||||
with self.subTest(reader=prefix), patch.object(module, "http_json", return_value=payload):
|
||||
result = read()
|
||||
self.assertEqual(set(result[0] if isinstance(result, tuple) else result), {"owner/repo"})
|
||||
|
||||
def test_near_exhausted_quota_aborts(self) -> None:
|
||||
for alias, module, read in self.readers():
|
||||
with self.subTest(reader=alias), patch.object(
|
||||
module, "http_json", return_value={"data": {alias: {}, "rateLimit": {"remaining": 0}}}
|
||||
):
|
||||
with self.assertRaises(RuntimeError):
|
||||
read()
|
||||
|
||||
def test_batch_transport_failure_aborts_before_writes(self) -> None:
|
||||
with patch.object(sys, "argv", ["sync_pocingithub.py"]), patch.dict(
|
||||
os.environ, {"GITHUB_TOKEN": "test"}
|
||||
), patch.object(
|
||||
sync_pocingithub, "candidates",
|
||||
return_value={"CVE-2026-1234": {"https://github.com/owner/repo"}},
|
||||
), patch.object(sync_pocingithub, "already_linked", return_value={}), patch.object(
|
||||
sync_pocingithub, "describe", side_effect=RuntimeError("API unavailable")
|
||||
), patch.object(sync_pocingithub, "ensure_cve_entries", return_value=(set(), set())) as ensure, patch.object(
|
||||
sync_pocingithub, "reconcile_inventory", return_value=(0, 0)
|
||||
) as reconcile:
|
||||
with self.assertRaisesRegex(RuntimeError, "API unavailable"):
|
||||
sync_pocingithub.main()
|
||||
ensure.assert_not_called()
|
||||
reconcile.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -4,6 +4,7 @@ import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
|
||||
|
||||
@@ -326,6 +327,48 @@ No PoCs found on GitHub currently.
|
||||
self.assertTrue(updated.endswith("\n\n"))
|
||||
|
||||
|
||||
class DiscoveryFailureTests(unittest.TestCase):
|
||||
def test_search_does_not_accept_partial_graphql_results(self) -> None:
|
||||
payload = {
|
||||
"data": {"search": {"repositoryCount": 1, "nodes": []}, "rateLimit": {"remaining": 100}},
|
||||
"errors": [{"type": "INTERNAL", "message": "Search unavailable"}],
|
||||
}
|
||||
with patch.object(update_cves, "http_json", return_value=payload):
|
||||
with self.assertRaisesRegex(RuntimeError, "Search unavailable"):
|
||||
update_cves.GitHubClient("test").search_page("CVE-2026")
|
||||
|
||||
def test_readme_batch_failure_aborts_discovery(self) -> None:
|
||||
repo = {
|
||||
"nameWithOwner": "owner/CVE-2026-1234-poc",
|
||||
"url": "https://github.com/owner/CVE-2026-1234-poc",
|
||||
}
|
||||
with patch.object(update_cves, "search_range", return_value=[repo]), patch.object(
|
||||
update_cves.GitHubClient, "fetch_readmes", side_effect=RuntimeError("API unavailable")
|
||||
):
|
||||
with self.assertRaisesRegex(RuntimeError, "API unavailable"):
|
||||
update_cves.discover_github_pocs(
|
||||
"test", years=[2026], lookback_days=3, backfill=False, cve_filter=set()
|
||||
)
|
||||
|
||||
def test_qualified_repository_survives_an_unverified_reference(self) -> None:
|
||||
cve = "CVE-2026-1234"
|
||||
url = "https://github.com/owner/CVE-2026-1234-poc"
|
||||
details = update_cves.CVEDetails("A vulnerability.", [], [], [])
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
path = root / "2026" / f"{cve}.md"
|
||||
path.parent.mkdir()
|
||||
path.write_text(update_cves.build_markdown(cve, details, [], [url]), encoding="utf-8")
|
||||
update_cves.update_existing_markdown(path, [url], [], dry_run=False)
|
||||
with patch.object(build_site, "CVES", root), patch.object(
|
||||
build_site, "load_metadata", return_value={}
|
||||
), patch.object(build_site, "load_dates", return_value={}), patch.object(
|
||||
build_site, "load_verified_references", return_value=set()
|
||||
):
|
||||
entries, _ = build_site.build_cve_list(set())
|
||||
self.assertEqual([(entry["cve"], entry["poc"]) for entry in entries], [(cve, [url])])
|
||||
|
||||
|
||||
class MetadataSourceTests(unittest.TestCase):
|
||||
def test_uses_vector_prefix_when_source_field_is_mislabeled(self) -> None:
|
||||
row = sync_cve_metadata.published_metric(
|
||||
|
||||
Reference in new issue
Block a user