Refine footer links and fix search and index edge cases

This commit is contained in:
0xMarcio committed 2026-10-06 22:39:15 +02:00
1 parent ffdd1e5971
commit e67993929c
14 files changed
+120 -19

No files matched your search

+1 -1
View File
@@ -559,7 +559,7 @@ def header(stamp: str, synced: str) -> list[str]:
"",
"</div>",
"",
f"**[{BRAND}]({home})** — {DESCRIPTION} {SEARCH_GUIDE}",
f"**[{BRAND}]({home})**: {DESCRIPTION} {SEARCH_GUIDE}",
"",
]
+1 -1
View File
@@ -8,7 +8,7 @@
</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.
**[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
+1
View File
@@ -359,6 +359,7 @@ function indexSlice(budget) {
wordIndex.words = [...postings.keys()].sort();
wordIndex.ready = true;
wordIndex.elapsed = Math.round(performance.now() - wordIndex.startedAt);
if (state.ready && state.query.length >= MIN_QUERY && !state.results.length) render();
}
// Hands the thread back between slices. A timer would do, except that a
+13 -4
View File
@@ -425,7 +425,7 @@ a.chip:hover { color: var(--fg); border-color: var(--fg-muted); }
}
.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); }
.index-intro { margin: 0 0 14px; 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);
@@ -439,12 +439,21 @@ a.chip:hover { color: var(--fg); border-color: var(--fg-muted); }
.site-footer { border-top: 1px solid var(--border-muted); padding: 14px 0; }
.site-footer .container {
display: flex;
gap: 20px;
justify-content: flex-end;
gap: 6px 20px;
flex-wrap: wrap;
font: 400 11px/1.6 var(--mono);
color: var(--fg-muted);
}
.site-footer span:last-child { margin-left: auto; }
.site-footer span:first-child { margin-right: auto; }
.site-footer a {
color: inherit;
white-space: nowrap;
text-decoration: underline;
text-decoration-color: var(--border);
text-underline-offset: 3px;
}
.site-footer a:hover { color: var(--accent); }
/* ---- narrow viewports --------------------------------------------------- */
@@ -536,7 +545,7 @@ a.chip:hover { color: var(--fg); border-color: var(--fg-muted); }
color: var(--danger); background: rgba(248, 81, 73, .12); border: 1px solid rgba(248, 81, 73, .4);
}
.hub-neighbours { margin: 24px 0 0; font: 400 12.5px/1 var(--mono); color: var(--neutral); }
.years { display: flex; flex-wrap: wrap; gap: 6px 14px; padding: 0 32px 18px; font: 400 11px/1.6 var(--mono); color: var(--neutral); }
.years { display: flex; flex-wrap: wrap; gap: 6px 14px; padding-bottom: 18px; font: 400 11px/1.6 var(--mono); color: var(--neutral); }
.years a { color: var(--fg-muted); }
.years a:hover { color: var(--accent); }
@media (max-width: 700px) {
+1 -1
View File
@@ -260,7 +260,7 @@ def dead_references(urls: list[str], *, workers: int, timeout: int) -> set[str]:
code = exc.code
exc.close()
# a server that dislikes HEAD is not a server without the page
if method == "GET" or code not in {403, 405, 501}:
if method == "GET" or code not in {403, 404, 405, 410, 501}:
return url, code in {404, 410}
except Exception:
# a timeout or a refused connection keeps the link, and asking
+8 -2
View File
@@ -44,6 +44,12 @@ FONTS = (
# One footer credit line. The homepage and the generated pages listed different
# sources, so a reader met two different claims about where the data comes from.
SOURCES_LINE = (
"sources: nvd &middot; cve program &middot; github &middot; github advisories "
"&middot; cisa vulnrichment &middot; cisa kev &middot; first epss"
'sources: <a href="https://nvd.nist.gov/">nvd</a>&nbsp;&middot; '
'<a href="https://www.cve.org/">cve program</a>&nbsp;&middot; '
'<a href="https://github.com/search?q=cve+poc&amp;type=repositories">github</a>&nbsp;&middot; '
'<a href="https://github.com/advisories">github advisories</a>&nbsp;&middot; '
'<a href="https://github.com/cisagov/vulnrichment">cisa vulnrichment</a>&nbsp;&middot; '
'<a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">cisa kev</a>&nbsp;&middot; '
'<a href="https://www.first.org/epss/">first epss</a>'
)
REPO_LINK = f'<a href="https://github.com/{SLUG}">github.com/{SLUG}</a>'
+7 -6
View File
@@ -28,7 +28,7 @@ from urllib import error, request
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, DESCRIPTION, FONTS, SEARCH_GUIDE, SITE, SLUG, SOURCES_LINE, SUBTITLE, TITLE, host
from brand import BRAND, DESCRIPTION, FONTS, REPO_LINK, SITE, SLUG, SOURCES_LINE, SUBTITLE, TITLE, host
DOCS = os.path.join(ROOT, "docs")
RELATED = 6
@@ -52,7 +52,7 @@ def load(name: str) -> dict | list:
def esc(value) -> str:
return html.escape(str(value or ""), quote=True)
return html.escape(str("" if value is None else value), quote=True)
def json_ld(payload: dict) -> str:
@@ -114,6 +114,7 @@ ASSESSORS = {
"[email protected]": "NVD",
"[email protected]": "MITRE",
"github advisory database": "GitHub",
"[email protected]": "IBM",
}
@@ -121,7 +122,7 @@ def assessor(row: list) -> str:
"""Who scored it, from row[4].
row[5] is the assessment type, Primary or Secondary, and labelling with it
left 129 pages showing two rows both marked "Secondary" — the repetition
left 129 pages showing two rows both marked "Secondary", the repetition
the label was added to explain. row[4] names the party, which is the thing
that actually differs. Scoring CNAs identify themselves by contact address
or by UUID, so an address becomes its domain and a UUID stays generic.
@@ -312,7 +313,7 @@ def header(count: int | None = None) -> str:
def footer(tail: str = '<a href="/">Back to the index</a>') -> str:
return (f'<footer class="site-footer"><div class="container">'
f"<span>{SOURCES_LINE}</span><span>{tail}</span></div></footer>")
f"<span>{SOURCES_LINE}</span><span>{tail}</span>{REPO_LINK}</div></footer>")
def page(entry: dict, data: dict) -> str:
@@ -683,8 +684,8 @@ def homepage(cves: list, kev: dict, trending: dict) -> str:
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),
description=esc(DESCRIPTION), site=esc(SITE), fonts=FONTS,
sources_line=SOURCES_LINE, repo_link=REPO_LINK, 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),
-3
View File
@@ -23,9 +23,6 @@ DOCS = os.path.join(ROOT, "docs")
ROBOTS = os.path.join(DOCS, "robots.txt")
SITEMAP = os.path.join(DOCS, "sitemap.xml")
# 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
# are over 13 MB on the wire, which is crawl budget spent on nothing. Blocking
# them here does not touch curl: robots.txt binds crawlers, not clients.
+6
View File
@@ -321,6 +321,12 @@ def collect_links(block: str, *, blacklist: Optional[Collection[str]] = None) ->
entry = entry[2:].strip()
if not entry:
continue
try:
parsed = urlparse(entry)
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
continue
except ValueError:
continue
if is_blacklisted(entry, blacklist):
continue
if entry not in links:
+1 -1
View File
@@ -97,7 +97,6 @@
<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">
@@ -109,6 +108,7 @@
<div class="container">
<span>${sources_line}</span>
<span data-refreshed data-nosnippet>index built ${generated}</span>
${repo_link}
</div>
</footer>
+26
View File
@@ -1,9 +1,11 @@
from __future__ import annotations
import io
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
from urllib.error import HTTPError
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "scripts"))
@@ -59,5 +61,29 @@ class RepositorySurveyTests(unittest.TestCase):
}, ["owner/poc", "owner/healthy"]), (set(), {"owner/healthy": [10, "2026-10-06"]}))
class ReferenceAuditTests(unittest.TestCase):
def test_head_not_found_keeps_reference_when_get_succeeds(self) -> None:
url = "https://example.com/poc"
for code in (404, 410):
with self.subTest(code=code), patch.object(
audit_poc_links.request,
"urlopen",
side_effect=[HTTPError(url, code, "No HEAD route", {}, None), io.BytesIO(b"PoC")],
) as fetch:
self.assertEqual(audit_poc_links.dead_references([url], workers=1, timeout=1), set())
self.assertEqual([call.args[0].method for call in fetch.call_args_list], ["HEAD", "GET"])
def test_get_confirms_reference_is_missing(self) -> None:
url = "https://example.com/poc"
for code in (404, 410):
with self.subTest(code=code), patch.object(
audit_poc_links.request,
"urlopen",
side_effect=[HTTPError(url, code, "Missing", {}, None), HTTPError(url, code, "Missing", {}, None)],
) as fetch:
self.assertEqual(audit_poc_links.dead_references([url], workers=1, timeout=1), {url})
self.assertEqual([call.args[0].method for call in fetch.call_args_list], ["HEAD", "GET"])
if __name__ == "__main__":
unittest.main()
+20
View File
@@ -177,6 +177,26 @@ class HomepageTests(unittest.TestCase):
build_pages.homepage([], {}, {"total_cves": 1, "with_pocs": 1})
class AssessmentRenderingTests(unittest.TestCase):
def test_zero_cvss_score_is_visible_in_chip_and_assessment(self) -> None:
entry = {"cve": "CVE-1999-0497", "poc": ["https://example.com/poc"]}
data = {
"cves": [entry],
"meta": {entry["cve"]: {"cvss": [[
"2.0", 0.0, "LOW", "AV:N/AC:L/Au:N/C:N/I:N/A:N", "[email protected]", "Primary",
]]}},
"epss": {}, "kev": {}, "nuclei": {}, "repo_meta": {}, "related": {},
"lastmod": {entry["cve"]: "2026-10-06"},
}
markup = build_pages.page(entry, data)
self.assertIn("LOW 0.0</span>", markup)
self.assertIn("<dd>0.0 LOW<code>", markup)
def test_ibm_assessment_names_its_scorer(self) -> None:
row = ["3.0", 7.1, "HIGH", "", "[email protected]", "Secondary"]
self.assertEqual(build_pages.assessor(row), "IBM")
class RepositoryMetadataTests(unittest.TestCase):
def test_repository_advisories_do_not_inherit_repository_metadata(self) -> None:
url = "https://github.com/vendor/project/security/advisories/GHSA-1234"
+17
View File
@@ -64,6 +64,23 @@ test('a stalled trending feed does not block a bookmarked search', async t => {
assert.match(elements.get('[data-results]').innerHTML, /href="\/CVE-2026-1234"/);
});
test('a bookmarked typo is corrected when indexing finishes without changing filters', async t => {
const { elements, context } = startPage(t, {
search: '?q=wdiget&src=github&kev=1&sort=newest',
trending: new Promise(() => {}),
kev: { 'CVE-2026-1234': ['2026-10-06', false] }
});
await new Promise(setImmediate);
assert.equal(vm.runInContext('wordIndex.ready', context), true);
assert.match(elements.get('[data-results]').innerHTML, /href="\/CVE-2026-1234"/);
assert.match(elements.get('[data-status]').textContent, /widget for wdiget/);
assert.equal(elements.get('[data-search]').value, 'wdiget');
assert.equal(vm.runInContext('state.query', context), 'wdiget');
assert.equal(vm.runInContext('state.sort', context), 'NEWEST');
assert.equal(vm.runInContext('state.kevOnly', context), true);
assert.equal(vm.runInContext("[...state.filters.source].join(',')", context), 'GITHUB');
});
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);
+18
View File
@@ -416,6 +416,24 @@ ui.run(port=8080)
class PublishedLinkDeduplicationTests(unittest.TestCase):
def test_published_links_require_absolute_http_urls(self) -> None:
valid = [
"https://Example.com/PoC.py?Version=One#Example",
"http://example.com/poc",
"HTTPS://example.com/CaseSensitive",
]
invalid = [
"javascript:alert(1)",
"data:text/html,test",
"//example.com/poc",
"/relative/poc",
"https:///missing-host",
"https://[invalid/poc",
"ordinary description text",
]
block = "\n".join(f"- {url}" for url in [*invalid, *valid])
self.assertEqual(build_site.collect_links(block), valid)
def test_unverified_reference_is_not_a_poc(self) -> None:
cve_id = "CVE-2024-41968"
url = "https://cert.vde.com/en/advisories/VDE-2024-047"