Files
cve/tests/test_homepage_lists.py
T

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 &#x27; where logic.js writes &#39;."""
return markup.replace("&#x27;", "&#39;")
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&#39;s &lt;b&gt;file read&lt;/b&gt;", 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()