Improve crawlable SEO and search reliability

This commit is contained in:
0xMarcio committed 2026-10-06 21:51:21 +02:00
1 parent 665f671cf9
commit 02e8fadcd6
17 files changed
+444 -186

No files matched your search

+3 -1
View File
@@ -45,7 +45,7 @@ USER_AGENT = "0xMarcio-cve-trending"
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
sys.path.insert(0, os.path.join(ROOT, "scripts"))
from brand import BRAND, SITE, SLUG
from brand import BRAND, DESCRIPTION, SEARCH_GUIDE, SITE, SLUG
from update_cves import load_blacklist, qualifying_repo_cves
README = os.path.join(ROOT, "README.md")
@@ -559,6 +559,8 @@ def header(stamp: str, synced: str) -> list[str]:
"",
"</div>",
"",
f"**[{BRAND}]({home})** — {DESCRIPTION} {SEARCH_GUIDE}",
"",
]
+1 -1
View File
@@ -38,7 +38,7 @@ jobs:
- name: Build site data
run: python3 scripts/build_site.py
- name: Build per-CVE pages
- name: Build homepage and CVE pages
run: python3 scripts/build_pages.py
- name: Build robots.txt and sitemaps
+1
View File
@@ -1,6 +1,7 @@
data/
.venv/
docs/CVE_list.json
docs/index.html
docs/kev.json
docs/repo_meta.json
docs/trending_poc.json
+2
View File
@@ -8,6 +8,8 @@
</div>
**[PoC Index](https://pocindex.io/)** — Search public CVE proof-of-concept exploits from GitHub, Nuclei, ExploitDB, Metasploit and Vulhub, with CVSS, EPSS and CISA KEV context. Find exploits by CVE, vendor, product or keyword. Compare CVSS severity, EPSS exploitation scores and CISA known-exploited status.
## Just landed
| Stars | Updated | Repository | Description |
-2
View File
@@ -2,8 +2,6 @@
**PAN-OS: Arbitrary File Creation Leads to OS Command Injection Vulnerability in GlobalProtect**
![](https://img.shields.io/static/v1?label=Vendor&message=Palo%20Alto%20Networks&color=blue)
![](https://img.shields.io/static/v1?label=Product&message=PAN-OS&color=blue)
![](https://img.shields.io/static/v1?label=Product&message=Cloud%20NGFW&color=blue)
![](https://img.shields.io/static/v1?label=Product&message=Prisma%20Access&color=blue)
![](https://img.shields.io/static/v1?label=Version&message=10.2.0&color=brightgreen)
![](https://img.shields.io/static/v1?label=Version&message=11.0.0&color=brightgreen)
![](https://img.shields.io/static/v1?label=Version&message=11.1.0&color=brightgreen)
-138
View File
@@ -1,138 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Absolute URLs all point at the canonical host. Keep them in step with
SITE in scripts/brand.py, which drives every generated surface. -->
<title>PoC Index: Search CVE Proof-of-Concept Exploits</title>
<meta name="description" content="Search 82,000+ public CVE proof-of-concept exploits from GitHub, Nuclei, ExploitDB, Metasploit and Vulhub." />
<meta name="robots" content="index, follow, max-image-preview:large" />
<link rel="icon" href="/favicon.ico" />
<link rel="canonical" href="https://pocindex.io/" />
<link rel="search" type="application/opensearchdescription+xml" title="PoC Index" href="/opensearch.xml" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="PoC Index" />
<meta property="og:title" content="PoC Index: Search CVE Proof-of-Concept Exploits" />
<meta property="og:description" content="Public proof-of-concept exploits, indexed by CVE." />
<meta property="og:url" content="https://pocindex.io/" />
<meta property="og:image" content="https://pocindex.io/social-card.png" />
<meta property="og:image:width" content="1280" />
<meta property="og:image:height" content="640" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="PoC Index: Search CVE Proof-of-Concept Exploits" />
<meta name="twitter:description" content="Public proof-of-concept exploits, indexed by CVE." />
<meta name="twitter:image" content="https://pocindex.io/social-card.png" />
<!-- WebSite.name is how Google decides what to print as the site name in a
result. Without it the title, the og tags and the h1 were three different
names and it picked one on its own. SearchAction points at the ?q= route
logic.js now keeps in the URL. -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "PoC Index",
"url": "https://pocindex.io/",
"description": "Search 82,000+ public CVE proof-of-concept exploits from GitHub, Nuclei, ExploitDB, Metasploit and Vulhub.",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://pocindex.io/?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
</script>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<header class="container hero-bar">
<div class="hero">
<svg class="hero-radar" viewBox="0 0 400 400" aria-hidden="true">
<circle cx="200" cy="200" r="70" />
<circle cx="200" cy="200" r="124" />
<circle cx="200" cy="200" r="170" />
<circle cx="200" cy="200" r="200" />
<line class="hero-sweep" x1="200" y1="200" x2="200" y2="0" />
</svg>
<div class="hero-id">
<h1>PoC Index</h1>
<p>Public proof-of-concept exploits, indexed by CVE.</p>
</div>
<dl class="hero-stats">
<div><dt data-stat-total>&nbsp;</dt><dd>CVES INDEXED</dd></div>
<div><dt class="is-accent" data-stat-pocs>&nbsp;</dt><dd>WITH POCS</dd></div>
<div><dt class="is-danger" data-stat-kev>&nbsp;</dt><dd>KNOWN EXPLOITED</dd></div>
</dl>
</div>
</header>
<div class="container search-bar">
<form class="search" role="search" autocomplete="off">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<circle cx="7.6" cy="7.6" r="5.1" stroke="#8b949e" stroke-width="1.7" />
<path d="M11.5 11.5 L15.6 15.6" stroke="#8b949e" stroke-width="1.7" stroke-linecap="round" />
</svg>
<input type="text" spellcheck="false" data-search
placeholder="Search CVE, vendor, product or keyword &middot; exclude with -windows"
aria-label="Search CVE, vendor, product or keyword" />
<span class="search-status" data-status aria-live="polite"></span>
</form>
<div class="filters" data-filters role="group" aria-label="CVE filters">
<div class="filter-group" role="group" aria-label="Severity">
<span class="filter-caption">SEVERITY</span>
<div class="filter-chips" data-filter-chips="severity"></div>
</div>
<span class="filter-sep" aria-hidden="true"></span>
<button type="button" class="filter-chip is-kev" data-filter-kev aria-pressed="false" disabled>KEV</button>
<span class="filter-sep" aria-hidden="true"></span>
<div class="filter-group" role="group" aria-label="Source">
<span class="filter-caption">SOURCE</span>
<div class="filter-chips" data-filter-chips="source"></div>
</div>
<button type="button" class="filter-clear" data-clear-filters hidden>CLEAR</button>
</div>
</div>
<main class="container content">
<section class="panel results" data-results hidden></section>
<section data-trending>
<div class="trend-controls">
<div class="switch" role="group" aria-label="Ranking">
<button type="button" data-mode="TRENDING" aria-pressed="true">TRENDING</button>
<button type="button" data-mode="RECENT" aria-pressed="false">RECENT</button>
</div>
<span class="trend-note" data-trend-note></span>
</div>
<div class="panel trending">
<div class="col-head">
<span class="col-stars">STARS</span>
<span class="col-updated">UPDATED</span>
<span>NAME</span>
<span class="col-desc">DESCRIPTION</span>
</div>
<div data-trend-rows></div>
</div>
</section>
</main>
<nav class="container years" aria-label="Browse by year">
<span>browse by year</span>
<a href="/2026">2026</a> <a href="/2025">2025</a> <a href="/2024">2024</a> <a href="/2023">2023</a> <a href="/2022">2022</a> <a href="/2021">2021</a> <a href="/2020">2020</a> <a href="/2019">2019</a> <a href="/2018">2018</a> <a href="/2017">2017</a> <a href="/2016">2016</a> <a href="/2015">2015</a> <a href="/2014">2014</a> <a href="/2013">2013</a> <a href="/2012">2012</a> <a href="/2011">2011</a> <a href="/2010">2010</a> <a href="/2009">2009</a> <a href="/2008">2008</a> <a href="/2007">2007</a> <a href="/2006">2006</a> <a href="/2005">2005</a> <a href="/2004">2004</a> <a href="/2003">2003</a> <a href="/2002">2002</a> <a href="/2001">2001</a> <a href="/2000">2000</a> <a href="/1999">1999</a>
</nav>
<footer class="site-footer">
<div class="container">
<span>sources: nvd &middot; cve program &middot; github &middot; github advisories &middot; cisa vulnrichment &middot; cisa kev &middot; first epss</span>
<span data-refreshed></span>
</div>
</footer>
<script src="/logic.js"></script>
</body>
</html>
+26 -18
View File
@@ -33,6 +33,7 @@ const state = {
source: new Set(FILTER_OPTIONS.source.map(([value]) => value))
},
ready: false,
loadFailed: false,
results: []
};
@@ -72,7 +73,7 @@ function hoursSince(iso) {
if (!iso) return null;
const then = Date.parse(iso.length === 10 ? iso + 'T00:00:00Z' : iso);
if (Number.isNaN(then)) return null;
return (Date.now() - then) / 36e5;
return Math.max(0, (Date.now() - then) / 36e5);
}
/** Long form for the trending table: "3 hours ago", "4 months ago". */
@@ -990,10 +991,13 @@ function trendRow(item) {
const flagged = item.cve && kev[item.cve]
? `<span class="trend-kev" title="Listed in CISA's Known Exploited Vulnerabilities catalogue">KEV</span>`
: '';
const detail = item.page
? `<a class="trend-detail" href="${escapeHTML(item.page)}">${escapeHTML(item.cve)} details</a>`
: '';
return `<div class="trend-row">
<span class="trend-stars${popular}">${formatStars(item.stars)} <span class="star">★</span></span>
<span class="trend-age">${escapeHTML(longAge(item._pushed))}</span>
<span class="trend-name-cell">${flagged}<a class="trend-name" href="${escapeHTML(item.url)}" target="_blank" rel="noopener">${escapeHTML(item.name)}</a></span>
<span class="trend-name-cell">${flagged}<a class="trend-name" href="${escapeHTML(item.url)}" target="_blank" rel="nofollow noopener">${escapeHTML(item.name)}</a>${detail}</span>
<span class="trend-desc">${escapeHTML(item.desc || '')}</span>
</div>`;
}
@@ -1001,6 +1005,8 @@ function trendRow(item) {
function renderTrending() {
el.results.hidden = true;
el.trending.hidden = false;
// Keep the build's rows if the optional feed is still loading or unavailable.
if (!indexMeta) return;
const ranked = trending.slice();
if (state.mode === 'TRENDING') {
@@ -1046,7 +1052,7 @@ function idleStatus() {
// A result was unlinkable: every search lived in memory, so nobody could cite
// one and no crawler ever saw a second page. The query rides in ?q= now, which
// is also the route the SearchAction in index.html declares.
// is also the route the OpenSearch descriptor declares.
function readURLState() {
const params = new URLSearchParams(location.search);
const query = (params.get('q') || '').trim();
@@ -1090,15 +1096,17 @@ function syncURL() {
function render() {
syncURL();
if (state.query.length < MIN_QUERY && !hasActiveFilters()) {
el.status.textContent = state.ready ? idleStatus() : 'loading index…';
el.status.textContent = state.ready ? idleStatus() : state.loadFailed ? 'index unavailable' : 'loading index…';
renderTrending();
return;
}
if (!state.ready) {
el.results.hidden = false;
el.trending.hidden = true;
el.status.textContent = 'loading index…';
el.results.innerHTML = '<div class="empty">Loading the CVE index…</div>';
el.status.textContent = state.loadFailed ? 'index unavailable' : 'loading index…';
el.results.innerHTML = state.loadFailed
? '<div class="empty">The CVE index could not be loaded. Reload the page to try again, or browse by year below.</div>'
: '<div class="empty">Loading the CVE index…</div>';
return;
}
const started = performance.now();
@@ -1275,8 +1283,12 @@ async function loadJSON(url, options) {
}
(async () => {
try {
const meta = await loadJSON('/trending_poc.json', { cache: 'no-store' });
readURLState();
syncFilterControls();
render();
// Trending is optional. A slow feed must not hold up a bookmarked search.
loadJSON('/trending_poc.json', { cache: 'no-store' }).then(meta => {
indexMeta = meta;
trending = (meta.items || []).map(item => Object.assign({}, item, {
_pushed: hoursSince(item.pushed),
@@ -1286,13 +1298,9 @@ async function loadJSON(url, options) {
el.refreshed.textContent = minutes < 60
? `index refreshed ${minutes} minute${minutes === 1 ? '' : 's'} ago`
: `index refreshed ${Math.round(minutes / 60)} hour${Math.round(minutes / 60) === 1 ? '' : 's'} ago`;
} catch (err) {
console.warn(err.message);
}
readURLState();
syncFilterControls();
paintHeroStats();
render();
paintHeroStats();
render();
}).catch(err => console.warn(err.message));
// The repository metadata is optional: without it the PoC rows simply lose
// their star and age columns, which is the documented fallback.
@@ -1300,8 +1308,7 @@ async function loadJSON(url, options) {
kev = data || {};
el.kevOnly.disabled = false;
paintHeroStats();
if (state.ready && (state.query || hasActiveFilters())) render();
else renderTrending();
render();
}).catch(err => console.warn(err.message));
// The metadata comes in two halves. The CVSS rows are small and gate the
@@ -1345,7 +1352,8 @@ async function loadJSON(url, options) {
buildWordIndex();
} catch (err) {
console.warn(err.message);
el.status.textContent = 'index unavailable';
state.loadFailed = true;
render();
}
loadAdvisories();
})();
+2
View File
@@ -424,6 +424,8 @@ a.chip:hover { color: var(--fg); border-color: var(--fg-muted); }
white-space: nowrap;
}
.trend-name { font: 500 12.5px/1 var(--mono); }
.trend-detail { display: block; width: fit-content; margin-top: 5px; font: 400 10px/1.4 var(--mono); color: var(--fg-muted); }
.index-intro { margin: 20px 0 0; max-width: 960px; font: 400 13px/1.7 var(--sans); color: var(--fg-muted); }
.trend-desc {
font: 400 13px/1.4 var(--sans);
color: var(--fg-subtle);
+10 -5
View File
@@ -8,15 +8,20 @@ here and every generated surface follows.
BRAND = "PoC Index"
TITLE = f"{BRAND}: Search CVE Proof-of-Concept Exploits"
# Short line under the wordmark, on the site and on both cards. Also carries
# og:description and twitter:description.
# Short line under the wordmark, on the site and on both cards.
SUBTITLE = "Public proof-of-concept exploits, indexed by CVE."
# Meta description, the JSON-LD description and the GitHub About box, which are
# three places a reader meets the same sentence and so should be one string.
# States what the thing is and stops. No trailing feature list.
# Keep this evergreen: search engines cache snippets independently of the
# corpus updates. Exact counts belong in the generated page, not this promise.
DESCRIPTION = (
"Search 82,000+ public CVE proof-of-concept exploits from GitHub, Nuclei, "
"ExploitDB, Metasploit and Vulhub."
"Search public CVE proof-of-concept exploits from GitHub, Nuclei, ExploitDB, "
"Metasploit and Vulhub, with CVSS, EPSS and CISA KEV context."
)
SEARCH_GUIDE = (
"Find exploits by CVE, vendor, product or keyword. Compare CVSS severity, "
"EPSS exploitation scores and CISA known-exploited status."
)
SITE = "https://pocindex.io"
+60 -17
View File
@@ -21,11 +21,12 @@ import os
import sys
from collections import defaultdict
from datetime import datetime, timezone
from string import Template
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
sys.path.insert(0, os.path.join(ROOT, "scripts"))
from brand import BRAND, FONTS, SITE, SOURCES_LINE
from brand import BRAND, DESCRIPTION, FONTS, SEARCH_GUIDE, SITE, SLUG, SOURCES_LINE, SUBTITLE, TITLE, host
DOCS = os.path.join(ROOT, "docs")
RELATED = 6
@@ -565,22 +566,63 @@ def hub_pages(cves: list, data: dict) -> dict:
return {"pages": pages, "lastmod": lastmod}
def check_static_drift() -> None:
"""Fail the build if the hand-maintained homepage drifts from the constants.
def homepage(cves: list, kev: dict, trending: dict) -> str:
"""Publish the same content before JavaScript or a crawler render runs."""
indexed = {entry["cve"] for entry in cves}
if trending["with_pocs"] != len(indexed) or trending["total_cves"] < len(indexed):
raise ValueError("Homepage counts disagree with the searchable corpus")
generated = datetime.fromisoformat(trending["generated"].replace("Z", "+00:00"))
index.html is written by hand and cannot be generated without losing what
is in it, so the next best thing is refusing to publish a homepage whose
fonts or source credit no longer match what every generated page states.
"""
with open(os.path.join(DOCS, "index.html"), encoding="utf-8") as handle:
home = handle.read()
problems = []
if "fonts.googleapis.com/css2?family=Space+Grotesk" not in home:
problems.append("index.html no longer loads the shared fonts")
if SOURCES_LINE not in home:
problems.append("index.html footer disagrees with brand.SOURCES_LINE")
if problems:
raise SystemExit("static drift: " + "; ".join(problems))
def rank(item: dict) -> float:
try:
pushed = datetime.fromisoformat(item["pushed"].replace("Z", "+00:00"))
if pushed.tzinfo is None:
pushed = pushed.replace(tzinfo=timezone.utc)
age = max(0, (generated - pushed).total_seconds() / 3600)
except (KeyError, TypeError, ValueError):
age = 8760
return item["stars"] / (age + 6) ** 0.45
rows = []
for item in sorted(trending["items"], key=rank, reverse=True)[:20]:
cid = item.get("cve")
stars = item["stars"]
star_label = f"{stars / 1000:.1f}k" if stars >= 1000 else str(stars)
popular = " is-popular" if stars >= 500 else ""
flagged = '<span class="trend-kev" title="CISA known exploited">KEV</span>' if cid in kev else ""
detail = (f'<a class="trend-detail" href="/{esc(cid)}">{esc(cid)} details</a>'
if cid in indexed else "")
rows.append(
f'<div class="trend-row"><span class="trend-stars{popular}">{star_label} <span class="star">★</span></span>'
f'<span class="trend-age">{esc((item.get("pushed") or "")[:10])}</span>'
f'<span class="trend-name-cell">{flagged}<a class="trend-name" href="{esc(item["url"])}" '
f'target="_blank" rel="nofollow noopener">{esc(item["name"])}</a>{detail}</span>'
f'<span class="trend-desc">{esc(item.get("desc"))}</span></div>'
)
if not rows:
rows.append('<div class="trend-row"><span class="trend-desc">No recent PoCs.</span></div>')
website = {
"@context": "https://schema.org",
"@type": "WebSite",
"@id": f"{SITE}/#website",
"name": BRAND,
"alternateName": ["pocindex", host()],
"url": f"{SITE}/",
"description": DESCRIPTION,
"sameAs": f"https://github.com/{SLUG}",
}
years = sorted({cid.split("-")[1] for cid in indexed}, reverse=True)
with open(os.path.join(ROOT, "templates", "index.html"), encoding="utf-8") as handle:
template = Template(handle.read())
return template.substitute(
brand=esc(BRAND), title=esc(TITLE), subtitle=esc(SUBTITLE),
description=esc(DESCRIPTION), search_guide=esc(SEARCH_GUIDE), site=esc(SITE),
fonts=FONTS, sources_line=SOURCES_LINE, website_schema=json_ld(website),
total_cves=f"{trending['total_cves']:,}", with_pocs=f"{len(indexed):,}", kev=f"{len(kev):,}",
trending_rows="\n".join(rows), generated=esc(generated.strftime("%Y-%m-%d %H:%M UTC")),
year_links=" ".join(f'<a href="/{esc(year)}">{esc(year)}</a>' for year in years),
)
def not_found() -> str:
@@ -652,7 +694,8 @@ def main() -> int:
with open(os.path.join(DOCS, "404.html"), "w", encoding="utf-8") as handle:
handle.write(not_found())
check_static_drift()
with open(os.path.join(DOCS, "index.html"), "w", encoding="utf-8") as handle:
handle.write(homepage(cves, data["kev"], load("trending_poc.json")))
related = sum(1 for cid in lastmod if data["related"].get(cid))
print(f"Wrote {written:,} CVE pages and {len(hubs['pages']):,} hub pages into {DOCS} "
+1 -3
View File
@@ -22,9 +22,7 @@ DOCS = os.path.join(ROOT, "docs")
ROBOTS = os.path.join(DOCS, "robots.txt")
SITEMAP = os.path.join(DOCS, "sitemap.xml")
# Googlebot renders the page before it indexes it, so anything the first paint
# needs has to stay crawlable. These three are small and carry the only server
# rendered content on the page, the trending table and the header counts.
# These small feeds enhance the prerendered homepage and remain crawlable.
RENDER_CRITICAL = ("trending_poc.json", "kev.json", "stats.json")
# These only feed the search box, which a crawler never types into. Together they
+4
View File
@@ -552,6 +552,10 @@ def main() -> int:
write_json(EPSS_OUTPUT, epss)
trending_items = build_trending(blacklist)
indexed = {entry["cve"] for entry in cve_payload}
for item in trending_items:
# Only link to detail pages that this build will actually publish.
item["page"] = f"/{item['cve']}" if item.get("cve") in indexed else None
write_json(
TRENDING_OUTPUT,
{
+14 -1
View File
@@ -755,13 +755,26 @@ def details_from_record(record: dict[str, Any]) -> CVEDetails | None:
versions: list[str] = []
vendors: list[str] = []
for affected in cna.get("affected") or []:
# CNA product entries also describe unaffected products. Exclude one
# only when the default and every version/change explicitly agree;
# missing or unknown statuses are not evidence that it is unaffected.
version_rows = affected.get("versions") or []
statuses = [str(affected.get("defaultStatus") or "").strip().lower()]
for version in version_rows:
statuses.append(str(version.get("status") or "").strip().lower())
statuses.extend(
str(change.get("status") or "").strip().lower()
for change in version.get("changes") or []
)
if all(status == "unaffected" for status in statuses):
continue
vendor = str(affected.get("vendor") or "").strip()
if vendor and vendor.lower() not in ("n/a", "unknown"):
vendors.append(vendor)
product = str(affected.get("product") or "").strip()
if product:
products.append(product)
for version in affected.get("versions") or []:
for version in version_rows:
status = str(version.get("status") or "affected").lower()
value = str(version.get("version") or "").strip()
if status == "affected" and value:
+117
View File
@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>${title}</title>
<meta name="description" content="${description}" />
<meta name="robots" content="index, follow, max-image-preview:large" />
<link rel="icon" href="/favicon.ico" />
<link rel="canonical" href="${site}/" />
<link rel="search" type="application/opensearchdescription+xml" title="${brand}" href="/opensearch.xml" />
<meta property="og:type" content="website" />
<meta property="og:site_name" content="${brand}" />
<meta property="og:title" content="${title}" />
<meta property="og:description" content="${description}" />
<meta property="og:url" content="${site}/" />
<meta property="og:image" content="${site}/social-card.png" />
<meta property="og:image:width" content="1280" />
<meta property="og:image:height" content="640" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="${title}" />
<meta name="twitter:description" content="${description}" />
<meta name="twitter:image" content="${site}/social-card.png" />
<script type="application/ld+json">${website_schema}</script>
${fonts}
<link rel="stylesheet" href="/style.css" />
</head>
<body>
<header class="container hero-bar">
<div class="hero">
<svg class="hero-radar" viewBox="0 0 400 400" aria-hidden="true">
<circle cx="200" cy="200" r="70" />
<circle cx="200" cy="200" r="124" />
<circle cx="200" cy="200" r="170" />
<circle cx="200" cy="200" r="200" />
<line class="hero-sweep" x1="200" y1="200" x2="200" y2="0" />
</svg>
<div class="hero-id">
<h1>${brand}</h1>
<p>${subtitle}</p>
</div>
<dl class="hero-stats">
<div data-nosnippet><dt data-stat-total>${total_cves}</dt><dd>CVES INDEXED</dd></div>
<div data-nosnippet><dt class="is-accent" data-stat-pocs>${with_pocs}</dt><dd>WITH POCS</dd></div>
<div data-nosnippet><dt class="is-danger" data-stat-kev>${kev}</dt><dd>KNOWN EXPLOITED</dd></div>
</dl>
</div>
</header>
<div class="container search-bar">
<form class="search" role="search" action="/" method="get" autocomplete="off">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
<circle cx="7.6" cy="7.6" r="5.1" stroke="#8b949e" stroke-width="1.7" />
<path d="M11.5 11.5 L15.6 15.6" stroke="#8b949e" stroke-width="1.7" stroke-linecap="round" />
</svg>
<input type="search" name="q" spellcheck="false" data-search
placeholder="Search CVE, vendor, product or keyword &middot; exclude with -windows"
aria-label="Search CVE, vendor, product or keyword" />
<span class="search-status" data-status aria-live="polite"></span>
<button type="submit" class="sr-only">Search</button>
</form>
<div class="filters" data-filters role="group" aria-label="CVE filters">
<div class="filter-group" role="group" aria-label="Severity">
<span class="filter-caption">SEVERITY</span>
<div class="filter-chips" data-filter-chips="severity"></div>
</div>
<span class="filter-sep" aria-hidden="true"></span>
<button type="button" class="filter-chip is-kev" data-filter-kev aria-pressed="false" disabled>KEV</button>
<span class="filter-sep" aria-hidden="true"></span>
<div class="filter-group" role="group" aria-label="Source">
<span class="filter-caption">SOURCE</span>
<div class="filter-chips" data-filter-chips="source"></div>
</div>
<button type="button" class="filter-clear" data-clear-filters hidden>CLEAR</button>
</div>
</div>
<main class="container content">
<noscript><p class="index-intro">Live search needs JavaScript. You can still browse CVEs by year below or open the CVE details linked in the table.</p></noscript>
<section class="panel results" data-results hidden></section>
<section data-trending data-nosnippet aria-label="Trending proof-of-concept repositories">
<div class="trend-controls">
<div class="switch" role="group" aria-label="Ranking">
<button type="button" data-mode="TRENDING" aria-pressed="true">TRENDING</button>
<button type="button" data-mode="RECENT" aria-pressed="false">RECENT</button>
</div>
<span class="trend-note" data-trend-note>stars weighted against time since the last commit</span>
</div>
<div class="panel trending">
<div class="col-head">
<span class="col-stars">STARS</span>
<span class="col-updated">UPDATED</span>
<span>NAME</span>
<span class="col-desc">DESCRIPTION</span>
</div>
<div data-trend-rows>${trending_rows}</div>
</div>
</section>
<p class="index-intro">${brand} is a free index of public CVE proof-of-concept exploits from GitHub, Nuclei, ExploitDB, Metasploit and Vulhub. ${search_guide}</p>
</main>
<nav class="container years" aria-label="Browse by year">
<span>browse by year</span>
${year_links}
</nav>
<footer class="site-footer">
<div class="container">
<span>${sources_line}</span>
<span data-refreshed data-nosnippet>index built ${generated}</span>
</div>
</footer>
<script src="/logic.js"></script>
</body>
</html>
+72
View File
@@ -1,14 +1,86 @@
from __future__ import annotations
import json
import sys
import unittest
from html.parser import HTMLParser
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
import build_pages
class ParsedPage(HTMLParser):
def __init__(self, markup: str) -> None:
super().__init__()
self.tags = []
self.text = []
self.feed(markup)
def handle_starttag(self, tag, attrs) -> None:
self.tags.append((tag, dict(attrs)))
def handle_data(self, data) -> None:
self.text.append(data)
class HomepageTests(unittest.TestCase):
def render(self, total: int = 123456) -> str:
return build_pages.homepage(
[{"cve": "CVE-2030-1234"}, {"cve": "CVE-2024-3400"}],
{"CVE-2024-3400": ["2024-04-12", False]},
{"total_cves": total, "with_pocs": 2, "generated": "2030-01-02T12:00:00Z", "items": [
{"cve": "CVE-2030-1234", "stars": 5, "pushed": "2030-01-02T09:00:00Z",
"name": "Example PoC", "url": "https://github.com/owner/poc", "desc": "A widget exploit"},
{"cve": "CVE-2030-9999", "stars": 1, "pushed": "2030-01-01T00:00:00Z",
"name": "New PoC", "url": "https://github.com/owner/new", "desc": "</script><script>alert(1)</script>"},
]},
)
def test_initial_html_has_current_counts_and_crawlable_links(self) -> None:
for total in (123456, 123457):
page = ParsedPage(self.render(total))
text = " ".join(page.text)
self.assertIn(f"{total:,}", text)
self.assertIn("Example PoC", text)
self.assertIn("A widget exploit", text)
links = [attrs.get("href") for tag, attrs in page.tags if tag == "a"]
self.assertIn("/2030", links)
self.assertIn("/2024", links)
self.assertNotIn("/2026", links)
self.assertIn("/CVE-2030-1234", links)
self.assertNotIn("/CVE-2030-9999", links)
def test_metadata_is_consistent_and_does_not_bake_in_counts(self) -> None:
page = ParsedPage(self.render())
descriptions = [attrs["content"] for tag, attrs in page.tags if tag == "meta" and
(attrs.get("name") in {"description", "twitter:description"} or
attrs.get("property") == "og:description")]
self.assertEqual(descriptions, [build_pages.DESCRIPTION] * 3)
self.assertTrue(all("82,000" not in text and "123,456" not in text for text in descriptions))
schemas = [json.loads(text) for text in page.text if text.startswith('{"@context"')]
self.assertEqual(schemas[0]["description"], build_pages.DESCRIPTION)
self.assertIn("pocindex", schemas[0]["alternateName"])
self.assertTrue(any(tag == "section" and "data-trending" in attrs and
"data-nosnippet" in attrs for tag, attrs in page.tags))
self.assertEqual(sum(tag == "div" and "data-nosnippet" in attrs for tag, attrs in page.tags), 3)
def test_untrusted_text_cannot_add_markup_or_end_structured_data(self) -> None:
unsafe = '</script><script>alert("injected")</script>'
with patch.object(build_pages, "DESCRIPTION", unsafe):
page = ParsedPage(self.render())
scripts = [attrs for tag, attrs in page.tags if tag == "script"]
self.assertEqual(len(scripts), 2) # One JSON-LD block and logic.js.
schemas = [json.loads(text) for text in page.text if text.startswith('{"@context"')]
self.assertEqual(schemas[0]["description"], unsafe)
def test_inconsistent_feed_counts_fail_the_build(self) -> None:
with self.assertRaisesRegex(ValueError, "counts disagree"):
build_pages.homepage([], {}, {"total_cves": 1, "with_pocs": 1})
class RepositoryMetadataTests(unittest.TestCase):
def test_repository_metadata_is_shown_for_repository_paths(self) -> None:
metadata = {"owner/repo": [123, "2026-10-06", "2025-01-01"]}
+83
View File
@@ -9,6 +9,89 @@ const { MessageChannel } = require('node:worker_threads');
const script = fs.readFileSync(path.join(__dirname, '../docs/logic.js'), 'utf8');
function startPage(t, { search = '', trending, kev = {}, indexError = false }) {
const elements = new Map();
const calls = [];
const document = {
addEventListener() {},
querySelectorAll: () => [],
querySelector(selector) {
if (!elements.has(selector)) elements.set(selector, {
value: '',
innerHTML: selector === '[data-trend-rows]' ? '<a href="/CVE-2026-1234">Prerendered PoC</a>' : '',
addEventListener() {}, setAttribute() {}, querySelectorAll: () => []
});
return elements.get(selector);
}
};
const context = vm.createContext({
console: { warn() {} }, document, performance, MessageChannel, URLSearchParams,
location: { pathname: '/', search },
window: { matchMedia: () => ({ matches: false }) },
setTimeout() {}, clearTimeout() {},
async fetch(url) {
calls.push(url);
if (url === '/CVE_list.json' && indexError) throw new Error('Index unavailable');
const data = url === '/trending_poc.json' ? await trending
: url === '/kev.json' ? await kev
: url === '/CVE_list.json' ? [{ cve: 'CVE-2026-1234', desc: 'widget', poc: ['https://github.com/owner/poc'] }]
: {};
return { ok: true, json: async () => data };
}
});
vm.runInContext(script, context);
t.after(() => vm.runInContext('yieldPort.port1.close(); yieldPort.port2.close();', context));
return { elements, calls };
}
test('a stalled trending feed does not block a bookmarked search', async t => {
const { elements, calls } = startPage(t, { search: '?q=widget', trending: new Promise(() => {}) });
await new Promise(setImmediate);
assert.ok(calls.includes('/CVE_list.json'));
assert.equal(elements.get('[data-search]').value, 'widget');
assert.match(elements.get('[data-results]').innerHTML, /href="\/CVE-2026-1234"/);
});
test('a failed trending feed preserves the published homepage rows', async t => {
const { elements } = startPage(t, { trending: Promise.reject(new Error('Feed unavailable')) });
await new Promise(setImmediate);
assert.match(elements.get('[data-trend-rows]').innerHTML, /Prerendered PoC/);
});
test('an empty successful feed replaces stale homepage rows', async t => {
const { elements } = startPage(t, { trending: { items: [], total_cves: 1, with_pocs: 1, generated: '2026-10-06' } });
await new Promise(setImmediate);
assert.match(elements.get('[data-trend-rows]').innerHTML, /No recent PoCs/);
});
test('an index failure stays visible when the optional trending feed arrives later', async t => {
let finishTrending;
const trending = new Promise(resolve => { finishTrending = resolve; });
const { elements } = startPage(t, { search: '?q=widget', trending, indexError: true });
await new Promise(setImmediate);
assert.match(elements.get('[data-results]').innerHTML, /could not be loaded/);
assert.equal(elements.get('[data-results]').hidden, false);
finishTrending({ items: [], total_cves: 1, with_pocs: 1, generated: '2026-10-06' });
await new Promise(setImmediate);
assert.match(elements.get('[data-results]').innerHTML, /could not be loaded/);
assert.equal(elements.get('[data-results]').hidden, false);
assert.equal(elements.get('[data-status]').textContent, 'index unavailable');
});
test('late KEV data cannot hide a bookmarked search failure', async t => {
let finishKev;
const kev = new Promise(resolve => { finishKev = resolve; });
const { elements } = startPage(t, {
search: '?q=widget', trending: new Promise(() => {}), kev, indexError: true
});
await new Promise(setImmediate);
finishKev({ 'CVE-2026-1234': ['2026-10-06', false] });
await new Promise(setImmediate);
assert.match(elements.get('[data-results]').innerHTML, /could not be loaded/);
assert.equal(elements.get('[data-results]').hidden, false);
assert.equal(elements.get('[data-trending]').hidden, true);
});
for (const sort of ['relevance', 'newest']) {
test(`late EPSS data refreshes results while respecting ${sort} sort`, async t => {
const elements = new Map();
+48
View File
@@ -218,6 +218,54 @@ class ReferenceAndMarkdownTests(unittest.TestCase):
],
)
def test_excludes_explicitly_unaffected_products_and_vendors(self) -> None:
for versions in ([], [{
"version": "All",
"status": "unaffected",
"changes": [{"at": "2", "status": "unaffected"}],
}]):
with self.subTest(versions=versions):
record = self.record()
record["containers"]["cna"]["affected"].append({
"vendor": "Unaffected vendor",
"product": "Unaffected product",
"defaultStatus": "unaffected",
"versions": versions,
})
details = update_cves.details_from_record(record)
self.assertEqual(details.products, ["Linux"])
self.assertEqual(details.vendors, [])
self.assertEqual(details.versions, ["4.14"])
def test_keeps_products_with_affected_or_incomplete_statuses(self) -> None:
cases = [
{"defaultStatus": "affected", "versions": [{"status": "unaffected"}]},
{"defaultStatus": "unaffected", "versions": [{"status": "affected"}]},
{"defaultStatus": "unaffected", "versions": [{
"status": "unaffected", "changes": [{"at": "2", "status": "affected"}],
}]},
{"defaultStatus": "unknown", "versions": [{"status": "unaffected"}]},
{"versions": [{"status": "unaffected"}]},
{"defaultStatus": "unaffected", "versions": [{"status": "unknown"}]},
{"defaultStatus": "unaffected", "versions": [{"version": "1"}]},
{"defaultStatus": "unaffected", "versions": [{
"status": "unaffected", "changes": [{"at": "2", "status": "unknown"}],
}]},
{"defaultStatus": "unaffected", "versions": [{
"status": "unaffected", "changes": [{"at": "2"}],
}]},
{},
]
for statuses in cases:
with self.subTest(statuses=statuses):
record = self.record()
record["containers"]["cna"]["affected"] = [{
"vendor": "Example vendor", "product": "Example product", **statuses,
}]
details = update_cves.details_from_record(record)
self.assertEqual(details.products, ["Example product"])
self.assertEqual(details.vendors, ["Example vendor"])
def test_preserves_markdown_layout(self) -> None:
details = update_cves.details_from_record(self.record())
self.assertIsNotNone(details)