mirror of
https://github.com/0xMarcio/cve.git
synced 2026-10-23 16:35:19 +00:00
263 lines
15 KiB
Python
263 lines
15 KiB
Python
"""The front-page lists read the same before and after the feed arrives.
|
|
|
|
build_pages pre-renders Just landed; logic.js renders both lists from
|
|
trending_poc.json, run in node the way tests/test_search.js runs it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import html
|
|
import json
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT / "scripts"))
|
|
|
|
import build_pages
|
|
|
|
KERNELCTF = "https://github.com/google/security-research/tree/master/pocs/linux/kernelctf/CVE-2024-26642_"
|
|
SHARED = "https://github.com/owner/CVE-2026-21589"
|
|
# Published order. Sorting either list by stars or by date would change it.
|
|
LANDED = [
|
|
{"cve": "CVE-2024-26642", "url": KERNELCTF + "mitigation", "name": "CVE-2024-26642_mitigation", "stars": 5000,
|
|
"desc": "nf_tables", "released": "2026-10-06T23:30:00-02:00", "basis": "commit", "kev": False,
|
|
"page": "/CVE-2024-26642", "landed": True, "source": "github.com", "artifact": True},
|
|
{"cve": "CVE-2026-21589", "url": SHARED, "name": "CVE-2026-21589", "stars": 5,
|
|
"desc": "Atlassian's <b>file read</b>", "released": "2026-10-06T15:51:43Z", "basis": "commit", "kev": True,
|
|
"page": "/CVE-2026-21589", "landed": True, "source": "github.com", "artifact": False},
|
|
{"cve": "CVE-2021-44228", "url": "https://www.exploit-db.com/exploits/50592", "name": "50592", "stars": 4682,
|
|
"desc": "log4j", "released": "2026-10-05", "basis": "source", "kev": True,
|
|
"page": "/CVE-2021-44228", "landed": True, "source": "www.exploit-db.com", "artifact": True},
|
|
]
|
|
TRENDING = [
|
|
{"cve": "CVE-2026-21589", "url": SHARED, "name": "CVE-2026-21589", "stars": 5, "desc": "Atlassian",
|
|
"pushed": "2026-10-06T15:51:43Z", "released": "2026-10-06T15:51:43Z", "basis": "commit", "score": 0.4,
|
|
"trending": True, "kev": True, "page": "/CVE-2026-21589"},
|
|
{"cve": "CVE-2026-24061", "url": "https://github.com/owner/CVE-2026-24061", "name": "CVE-2026-24061",
|
|
"stars": 1250, "desc": "telnetd", "pushed": "2026-10-03T16:24:22Z", "released": "2026-03-08T11:25:39Z",
|
|
"basis": "commit", "score": 0.3, "trending": True, "kev": False, "page": "/CVE-2026-24061"},
|
|
{"cve": "CVE-2026-99999", "url": "https://github.com/owner/unpublished", "name": "unpublished", "stars": 40,
|
|
"desc": "", "pushed": None, "released": None, "basis": "unknown", "score": 0.2, "trending": True,
|
|
"kev": False, "page": None},
|
|
]
|
|
FEED = {"generated": "2026-10-07T09:00:00+00:00", "total_cves": 5, "with_pocs": 4,
|
|
"items": TRENDING, "landed": LANDED, "ranking": "stars_weighted_by_artifact_release_age"}
|
|
NOW = datetime.fromisoformat(FEED["generated"])
|
|
INDEXED = [{"cve": cve} for cve in ("CVE-2024-26642", "CVE-2026-21589", "CVE-2021-44228", "CVE-2026-24061")]
|
|
|
|
HARNESS = r"""
|
|
const fs = require('node:fs');
|
|
const vm = require('node:vm');
|
|
const { MessageChannel } = require('node:worker_threads');
|
|
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
const elements = new Map();
|
|
const buttons = ['LANDED', 'TRENDING'].map(mode => ({
|
|
dataset: { mode }, disabled: mode !== 'LANDED', pressed: null,
|
|
setAttribute(name, value) { if (name === 'aria-pressed') this.pressed = value; }
|
|
}));
|
|
const document = {
|
|
addEventListener() {},
|
|
querySelectorAll: selector => selector === '.trend-controls .switch button' ? buttons : [],
|
|
querySelector(selector) {
|
|
if (!elements.has(selector)) elements.set(selector, {
|
|
value: '', innerHTML: '', textContent: '', addEventListener() {}, setAttribute() {}, querySelectorAll: () => []
|
|
});
|
|
return elements.get(selector);
|
|
}
|
|
};
|
|
const context = vm.createContext({
|
|
Date: class extends Date { static now() { return Date.parse(input.now); } },
|
|
console: { warn() {} }, document, performance, MessageChannel, URLSearchParams, URL,
|
|
location: { pathname: '/', search: '' },
|
|
window: { matchMedia: () => ({ matches: false }) },
|
|
setTimeout() {}, clearTimeout() {},
|
|
// With archives, an empty index lets the archive download that follows it run.
|
|
fetch: url => url === '/trending_poc.json' ? Promise.resolve({ ok: true, json: async () => input.feed })
|
|
: input.archives && url === '/CVE_list.json' ? Promise.resolve({ ok: true, json: async () => [] })
|
|
: input.archives && url === '/archives.json' ? Promise.resolve({ ok: true, json: async () => input.archives })
|
|
: new Promise(() => {})
|
|
});
|
|
vm.runInContext(fs.readFileSync(input.script, 'utf8'), context);
|
|
const view = () => ({
|
|
rows: elements.get('[data-trend-rows]').innerHTML,
|
|
date: elements.get('[data-trend-date]').textContent,
|
|
pressed: buttons.map(button => button.pressed),
|
|
disabled: buttons.map(button => button.disabled)
|
|
});
|
|
setImmediate(() => {
|
|
const views = { initial: view() };
|
|
for (const mode of ['TRENDING', 'RECENT', 'LANDED']) {
|
|
vm.runInContext(`state.mode = ${JSON.stringify(mode)}; renderTrending();`, context);
|
|
views[mode] = view();
|
|
}
|
|
vm.runInContext('yieldPort.port1.close(); yieldPort.port2.close();', context);
|
|
process.stdout.write(JSON.stringify(views));
|
|
});
|
|
"""
|
|
|
|
|
|
def browser(feed: dict, archives: dict | None = None) -> dict:
|
|
"""What logic.js shows once the feed, and any archive index, arrives, then
|
|
for each switch value."""
|
|
node = shutil.which("node")
|
|
if node is None:
|
|
raise unittest.SkipTest("node renders the browser lists")
|
|
payload = {"script": str(ROOT / "docs" / "logic.js"), "feed": feed, "now": NOW.isoformat(), "archives": archives}
|
|
done = subprocess.run([node, "-e", HARNESS], input=json.dumps(payload), capture_output=True,
|
|
text=True, timeout=60)
|
|
if done.returncode:
|
|
raise AssertionError(done.stderr)
|
|
return json.loads(done.stdout)
|
|
|
|
|
|
def as_browser(markup: str) -> str:
|
|
"""html.escape writes an apostrophe as ' where logic.js writes '."""
|
|
return markup.replace("'", "'")
|
|
|
|
|
|
def cells(markup: str) -> list[dict]:
|
|
rows = []
|
|
for row in re.findall(r'<div class="trend-row">(.*?)</div>', markup):
|
|
stars = re.search(r'<span class="trend-stars[^"]*"[^>]*>(.*?)</span><(?:span|time) class="trend-age"', row).group(1)
|
|
href, name = re.search(r'<a class="trend-name" href="([^"]*)"[^>]*>([^<]*)</a>', row).groups()
|
|
rows.append({"href": html.unescape(href), "name": html.unescape(name),
|
|
"stars": re.sub(r"<[^>]+>", "", stars), "kev": 'class="trend-kev"' in row,
|
|
"date": re.search(r'<(?:span|time) class="trend-age"[^>]*>([^<]*)</(?:span|time)>', row).group(1)})
|
|
return rows
|
|
|
|
|
|
class HomepageListTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
cls.views = browser(FEED)
|
|
|
|
def test_browser_and_prerendered_homepage_render_the_same_rows(self) -> None:
|
|
landed, trending = build_pages.trend_rows(LANDED, True, NOW), build_pages.trend_rows(TRENDING, False, NOW)
|
|
for mode in ("initial", "LANDED", "RECENT"):
|
|
with self.subTest(mode=mode):
|
|
self.assertEqual(self.views[mode]["rows"], as_browser(landed))
|
|
self.assertEqual(self.views["TRENDING"]["rows"], as_browser(trending))
|
|
page = build_pages.homepage(INDEXED, {}, FEED, NOW)
|
|
self.assertIn(landed, page)
|
|
self.assertNotIn("telnetd", page)
|
|
|
|
def test_each_list_keeps_its_published_order_and_may_share_a_poc(self) -> None:
|
|
landed, trending = cells(self.views["LANDED"]["rows"]), cells(self.views["TRENDING"]["rows"])
|
|
self.assertEqual([row["href"] for row in landed], [item["url"] for item in LANDED])
|
|
self.assertEqual([row["href"] for row in trending], [item["url"] for item in TRENDING])
|
|
self.assertIn(SHARED, [row["href"] for row in landed])
|
|
self.assertIn(SHARED, [row["href"] for row in trending])
|
|
|
|
def test_relative_dates_preserve_release_update_and_timestamp_precision(self) -> None:
|
|
landed, trending = cells(self.views["LANDED"]["rows"]), cells(self.views["TRENDING"]["rows"])
|
|
self.assertEqual([row["date"] for row in landed], ["7 hours ago", "17 hours ago", "2 days ago"])
|
|
self.assertEqual([row["date"] for row in trending], ["17 hours ago", "3 days ago", "N/A"])
|
|
self.assertEqual([self.views[mode]["date"] for mode in ("initial", "TRENDING", "RECENT")],
|
|
["RELEASED", "UPDATED", "RELEASED"])
|
|
self.assertIn('title="2026-10-07 01:30:00.000 UTC"', self.views["LANDED"]["rows"])
|
|
self.assertIn('title="2026-10-05 UTC (time unavailable)"', self.views["LANDED"]["rows"])
|
|
|
|
def test_relative_age_boundaries_match_in_browser_and_prerender(self) -> None:
|
|
cases = [(None, "N/A"), ("invalid", "N/A"), ("2026-02-30", "N/A"), ("2026-10-08T00:00:00Z", "just now"),
|
|
("2026-10-07T08:59:01Z", "just now"), ("2026-10-07T08:59:00Z", "1 minute ago"),
|
|
("2026-10-07T08:01:00", "59 minutes ago"), ("2026-10-07T08:00:00Z", "1 hour ago"),
|
|
("2026-10-07 08:00:00", "1 hour ago"),
|
|
("2026-10-06T09:00:00Z", "1 day ago"), ("2026-10-07", "today"),
|
|
("2026-10-06", "1 day ago")]
|
|
items = [{**LANDED[0], "released": value} for value, _ in cases]
|
|
markup = browser({**FEED, "landed": items})["initial"]["rows"]
|
|
self.assertEqual(markup, as_browser(build_pages.trend_rows(items, True, NOW)))
|
|
self.assertEqual([row["date"] for row in cells(markup)], [label for _, label in cases])
|
|
|
|
def test_artifacts_show_host_repository_stars_and_keep_their_variant_or_host(self) -> None:
|
|
landed = cells(self.views["LANDED"]["rows"])
|
|
self.assertEqual([(row["name"], row["stars"], row["kev"]) for row in landed], [
|
|
("CVE-2024-26642_mitigation", "5.0k ★", False), ("CVE-2026-21589", "5 ★", True),
|
|
("exploit-db.com/50592", "N/A", True)])
|
|
self.assertIn('title="Repository stars: google/security-research"', self.views["LANDED"]["rows"])
|
|
self.assertEqual(cells(self.views["TRENDING"]["rows"])[1]["stars"], "1.3k ★")
|
|
self.assertIn("Atlassian's <b>file read</b>", self.views["LANDED"]["rows"])
|
|
|
|
def test_switch_starts_on_just_landed_and_trending_waits_for_the_feed(self) -> None:
|
|
page = build_pages.homepage(INDEXED, {}, FEED)
|
|
landed = re.search(r'<button [^>]*data-mode="LANDED"[^>]*>', page).group(0)
|
|
trending = re.search(r'<button [^>]*data-mode="TRENDING"[^>]*>', page).group(0)
|
|
self.assertIn('aria-pressed="true"', landed)
|
|
self.assertIn(" disabled", trending)
|
|
self.assertEqual(self.views["initial"]["disabled"], [False, False])
|
|
self.assertEqual([self.views[mode]["pressed"] for mode in ("initial", "TRENDING", "RECENT")],
|
|
[["true", "false"], ["false", "true"], ["true", "false"]])
|
|
|
|
def test_wayback_snapshots_and_lookups_match_in_browser_and_prerender(self) -> None:
|
|
stamp = "20261006160000"
|
|
archives = {
|
|
SHARED: {"url": f"https://web.archive.org/web/{stamp}/{SHARED}", "timestamp": stamp},
|
|
# Recorded with another scheme, www. and port: still this page.
|
|
LANDED[2]["url"]: {"url": f"https://web.archive.org/web/{stamp}/http://exploit-db.com:80/exploits/50592",
|
|
"timestamp": stamp},
|
|
# Another site's capture is not this link's snapshot.
|
|
KERNELCTF + "mitigation": {"url": f"https://web.archive.org/web/{stamp}/https://example.com/", "timestamp": stamp},
|
|
}
|
|
views = browser(FEED, archives)
|
|
landed = build_pages.trend_rows(LANDED, True, NOW, archives)
|
|
self.assertEqual(views["initial"]["rows"], as_browser(landed))
|
|
self.assertEqual(views["TRENDING"]["rows"], as_browser(build_pages.trend_rows(TRENDING, False, NOW, archives)))
|
|
self.assertIn(landed, build_pages.homepage(INDEXED, {}, FEED, NOW, archives))
|
|
self.assertEqual(landed.count('class="wayback is-snapshot"'), 2)
|
|
self.assertIn(f'<a class="wayback is-lookup" href="https://web.archive.org/web/*/{KERNELCTF}mitigation"', landed)
|
|
self.assertIn('aria-label="snapshot of CVE-2026-21589 on the Wayback Machine, captured 2026-10-06 16:00 UTC"', landed)
|
|
# Everything else in the rows reads as it does without archives.
|
|
self.assertEqual(cells(views["initial"]["rows"]), cells(self.views["LANDED"]["rows"]))
|
|
|
|
def test_saved_files_match_in_browser_and_prerender(self) -> None:
|
|
stamp, commit = "20261006160000", "0123456789abcdef0123456789abcdef01234567"
|
|
exploit = (f"https://raw.githubusercontent.com/google/security-research/{commit}/"
|
|
"pocs/linux/kernelctf/CVE-2024-26642_mitigation/exploit.c")
|
|
poc = f"https://raw.githubusercontent.com/owner/cve-2026-21589/{commit}/poc.py"
|
|
|
|
def saved(original: str) -> dict:
|
|
return {"original": original, "url": f"https://web.archive.org/web/{stamp}/{original}", "timestamp": stamp}
|
|
|
|
archives = {
|
|
SHARED: {"url": f"https://web.archive.org/web/{stamp}/{SHARED}", "timestamp": stamp, "files": [saved(poc)]},
|
|
# Saved files while the page snapshot is pending.
|
|
KERNELCTF + "mitigation": {"files": [saved(exploit)]},
|
|
# Not this page's file.
|
|
LANDED[2]["url"]: {"files": [saved(poc)]},
|
|
}
|
|
views = browser(FEED, archives)
|
|
landed = build_pages.trend_rows(LANDED, True, NOW, archives)
|
|
trending = build_pages.trend_rows(TRENDING, False, NOW, archives)
|
|
self.assertEqual(views["initial"]["rows"], as_browser(landed))
|
|
self.assertEqual(views["TRENDING"]["rows"], as_browser(trending))
|
|
self.assertIn(landed, build_pages.homepage(INDEXED, {}, FEED, NOW, archives))
|
|
self.assertEqual((landed.count("<details "), trending.count("<details ")), (2, 1))
|
|
self.assertIn(
|
|
f'find archive</a><details class="sources" style="--files:1" data-sources="{KERNELCTF}mitigation">'
|
|
'<summary title="Wayback Machine captures of individual files at pinned commits, not a copy of the whole '
|
|
'repository" aria-label="1 file saved from CVE-2024-26642_mitigation">1 file</summary>'
|
|
f'<div class="sources-list"><a href="https://web.archive.org/web/{stamp}/{exploit}" target="_blank" '
|
|
'rel="nofollow noopener" title="Wayback Machine capture of pocs/linux/kernelctf/CVE-2024-26642_mitigation/'
|
|
'exploit.c at commit 0123456, 2026-10-06 16:00 UTC" aria-label="pocs/linux/kernelctf/CVE-2024-26642_mitigation/'
|
|
'exploit.c at commit 0123456 of CVE-2024-26642_mitigation, Wayback Machine capture 2026-10-06 16:00 UTC">'
|
|
'pocs/linux/kernelctf/CVE-2024-26642_mitigation/exploit.c</a></div></details></div></div>', landed)
|
|
self.assertIn(f'snapshot</a><details class="sources" style="--files:1" data-sources="{SHARED}">', trending)
|
|
# Everything else in the rows reads as it does without archives.
|
|
self.assertEqual(cells(views["initial"]["rows"]), cells(self.views["LANDED"]["rows"]))
|
|
self.assertEqual(cells(views["TRENDING"]["rows"]), cells(self.views["TRENDING"]["rows"]))
|
|
|
|
def test_an_empty_list_shows_the_empty_state_in_both(self) -> None:
|
|
empty = build_pages.trend_rows([], True)
|
|
self.assertIn("No recent PoCs.", empty)
|
|
self.assertEqual(browser({**FEED, "landed": []})["initial"]["rows"], empty)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|