mirror of
https://github.com/0xMarcio/cve.git
synced 2026-10-23 16:35:19 +00:00
Refine footer links and fix search and index edge cases
This commit is contained in:
1 parent
ffdd1e5971
commit
e67993929c
14 files changed
+120
-19
No files matched your search
@@ -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}",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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 · cve program · github · github advisories "
|
||||
"· cisa vulnrichment · cisa kev · first epss"
|
||||
'sources: <a href="https://nvd.nist.gov/">nvd</a> · '
|
||||
'<a href="https://www.cve.org/">cve program</a> · '
|
||||
'<a href="https://github.com/search?q=cve+poc&type=repositories">github</a> · '
|
||||
'<a href="https://github.com/advisories">github advisories</a> · '
|
||||
'<a href="https://github.com/cisagov/vulnrichment">cisa vulnrichment</a> · '
|
||||
'<a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">cisa kev</a> · '
|
||||
'<a href="https://www.first.org/epss/">first epss</a>'
|
||||
)
|
||||
REPO_LINK = f'<a href="https://github.com/{SLUG}">github.com/{SLUG}</a>'
|
||||
@@ -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),
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user