Preserve filter intent and undated CVE visibility through enrichment

This commit is contained in:
0xMarcio committed 2026-10-21 20:52:21 +02:00
1 parent 24ba6ac25a
commit f0d5cca38f
9 files changed
+112 -22

No files matched your search

+3
View File
@@ -820,6 +820,9 @@ Advisory rows are `[URL, NVD reference tags]`.
Repository metadata rows are `[stars, last_push_UTC_date]`. Repo stars describe
the hosting GitHub repository, including links to individual files. `N/A`
means that no public repository reading is available; a measured zero is `0`.
`published` is the CVE record publication date. When it is unavailable,
`poc_observed` records the earliest retained observation of a listed PoC.
Newest uses that fallback and labels it separately as “PoC observed”.
## Sources
+3
View File
@@ -152,6 +152,9 @@ Advisory rows are `[URL, NVD reference tags]`.
Repository metadata rows are `[stars, last_push_UTC_date]`. Repo stars describe
the hosting GitHub repository, including links to individual files. `N/A`
means that no public repository reading is available; a measured zero is `0`.
`published` is the CVE record publication date. When it is unavailable,
`poc_observed` records the earliest retained observation of a listed PoC.
Newest uses that fallback and labels it separately as “PoC observed”.
## Sources
+15 -11
View File
@@ -303,12 +303,13 @@ function matchesMultiFilter(values, filter) {
}
function matchesFilters(entry) {
if (state.kevOnly && !kev[entry.cve]) return false;
if (state.kevOnly && !enrichmentNotices.has('KEV filter unavailable') && !kev[entry.cve]) return false;
if (!matchesMultiFilter(entrySources(entry), state.filters.source)) return false;
// ALL has to mean all. The options list only names the four scored bands, so
// testing membership hid every CVE nobody has scored yet even with nothing
// filtered: 41 of them were unreachable by search, including by exact ID.
if (state.filters.severity.size === FILTER_OPTIONS.severity.length) return true;
if (state.filters.severity.size === FILTER_OPTIONS.severity.length ||
enrichmentNotices.has('severity filter unavailable')) return true;
const severity = entrySeverity(entry);
return severity ? matchesMultiFilter(new Set([severity]), state.filters.severity) : false;
}
@@ -687,7 +688,7 @@ function prepareDataset(raw) {
.toLowerCase();
entry._year = parseInt(parts[1], 10) || 0;
entry._num = parseInt(parts[2], 10) || 0;
entry._published = Date.parse(entry.published || '') || 0;
entry._published = Date.parse(entry.published || entry.poc_observed || '') || 0;
// Title, vendor and product, once the records carry them.
const named = [entry.title || '', ...(entry.vendor || []), ...(entry.product || [])].join(' ');
entry._titleText = named.toLowerCase();
@@ -702,6 +703,7 @@ function prepareDataset(raw) {
const el = {
input: document.querySelector('[data-search]'),
status: document.querySelector('[data-status]'),
notices: document.querySelector('[data-notices]'),
results: document.querySelector('[data-results]'),
trending: document.querySelector('[data-trending]'),
trendDate: document.querySelector('[data-trend-date]'),
@@ -827,9 +829,10 @@ function rankedLinks(entry) {
stars: meta ? meta[0] : -1
};
});
scored.sort((a, b) =>
(b.stars - a.stars) ||
a.url.toLowerCase().localeCompare(b.url.toLowerCase()) || (a.index - b.index));
scored.sort((a, b) => {
const left = a.url.toLowerCase(), right = b.url.toLowerCase();
return (b.stars - a.stars) || (left < right ? -1 : left > right ? 1 : 0) || (a.index - b.index);
});
entry._ranked = scored.map(item => item.url);
return entry._ranked;
}
@@ -938,6 +941,7 @@ function resultRow(entry) {
const dates = [];
if (entry.published) dates.push(`published ${entry.published}`);
else if (entry.poc_observed) dates.push(`PoC observed ${entry.poc_observed}`);
if (entry.modified) dates.push(`modified ${entry.modified}`);
const dateHtml = dates.length
? `<div class="result-dates">${dates.map(d => `<span>${escapeHTML(d)}</span>`).join('')}</div>`
@@ -1021,8 +1025,7 @@ function renderResults(elapsed) {
const note = results.corrected
? results.corrected.map(([typed, used]) => `${used} for ${typed}`).join(', ') + ' · '
: '';
el.status.textContent = `${formatCount(results.length)} CVE${results.length === 1 ? '' : 's'} · ${note}matched in ${Math.max(1, Math.round(elapsed))}ms` +
(enrichmentNotices.size ? ' · ' + [...enrichmentNotices].join(' · ') : '');
el.status.textContent = `${formatCount(results.length)} CVE${results.length === 1 ? '' : 's'} · ${note}matched in ${Math.max(1, Math.round(elapsed))}ms`;
}
}
@@ -1087,7 +1090,7 @@ function paintHeroStats() {
function idleStatus() {
// The hero carries the totals now, so the field keeps its slot for timings.
return [...enrichmentNotices].join(' · ');
return '';
}
/* ---- url state --------------------------------------------------------- */
@@ -1137,6 +1140,8 @@ function syncURL() {
function render() {
syncURL();
el.notices.textContent = [...enrichmentNotices].join(' · ');
el.notices.hidden = !enrichmentNotices.size;
if (state.query.length < MIN_QUERY && !hasActiveFilters()) {
el.status.textContent = state.ready ? idleStatus() : state.loadFailed ? 'index unavailable' : 'loading index…';
renderTrending();
@@ -1328,10 +1333,8 @@ function enrichmentUnavailable(kind, error) {
console.warn(error.message);
if (dataEpoch) return;
if (kind === 'kev') {
state.kevOnly = false;
enrichmentNotices.add('KEV filter unavailable');
} else if (kind === 'severity') {
state.filters.severity = new Set(FILTER_OPTIONS.severity.map(([value]) => value));
enrichmentNotices.add('severity filter unavailable');
} else {
enrichmentNotices.add(`${kind} unavailable`);
@@ -1396,6 +1399,7 @@ async function refreshData(force = false) {
enableFilter('severity');
enableFilter('source');
el.kevOnly.disabled = false;
syncFilterControls();
document.querySelectorAll('.trend-controls .switch button').forEach(button => { button.disabled = false; });
paintHeroStats();
paintRefreshTime(feed);
+5
View File
@@ -138,6 +138,11 @@ a:hover { color: var(--accent-hover); }
font: 400 11.5px/1 var(--mono);
color: var(--fg-muted);
}
.data-notices {
margin-top: 10px;
font: 400 12px/1.5 var(--mono);
color: var(--fg-subtle);
}
.filters {
display: flex;
align-items: center;
+2
View File
@@ -441,6 +441,8 @@ def page(entry: dict, data: dict) -> str:
facts.append(f"<dt>Nuclei</dt><dd>{esc(tpl.get('severity'))}{cwe}</dd>")
if entry.get("published"):
facts.append(f"<dt>Published</dt><dd>{esc(entry['published'])}</dd>")
elif entry.get("poc_observed"):
facts.append(f"<dt>PoC observed</dt><dd>{esc(entry['poc_observed'])}</dd>")
if entry.get("modified"):
facts.append(f"<dt>Updated</dt><dd>{esc(entry['modified'])}</dd>")
+20 -2
View File
@@ -73,7 +73,7 @@ def normalise_block(text: str) -> str:
if re.match(r"\s*<(?:p|div|ul|ol|h[1-6])(?:\s|>)", text, flags=re.I):
text = re.sub(r"</?(?:p|div|br|ul|ol|li|h[1-6])(?:\s[^>]*)?/?>", "\n", text, flags=re.I)
text = re.sub(r"</?(?:a|strong|em|b|i|code|span)(?:\s[^>]*)?>", "", text, flags=re.I)
text = html.unescape(text)
text = html.unescape(text)
text = text.replace("\r\n", "\n")
text = re.sub(r"\n{2,}", "\n", text.strip())
lines = [
@@ -471,6 +471,21 @@ def build_cve_list(blacklist: Collection[str]) -> tuple[List[Dict[str, object]],
return cve_entries, total
def add_observed_dates(cve_entries: list[dict], ledger: dict) -> None:
"""Date undated CVEs by their earliest retained PoC observation, explicitly."""
import releases
for entry in cve_entries:
if entry.get("published"):
continue
stamps = [
stamp for field in ["poc", *(field for _, field in CURATED)]
for url in entry.get(field, [])
if (stamp := releases.timestamp((ledger.get(releases.key(entry["cve"], url)) or {}).get("seen")))
]
if stamps:
entry["poc_observed"] = min(stamps).strftime("%Y-%m-%d")
def build_repo_meta(cve_entries: List[Dict[str, object]]) -> Dict[str, object]:
"""Star count and last-push date for the repositories the index actually links."""
stored = json.loads(REPO_META.read_text(encoding="utf-8"))
@@ -662,7 +677,10 @@ def main() -> int:
DOCS_DIR.mkdir(parents=True, exist_ok=True)
blacklist = load_blacklist()
import releases
ledger = releases.load_ledger()
cve_payload, total_cves = build_cve_list(blacklist)
add_observed_dates(cve_payload, ledger)
write_json(CVE_OUTPUT, cve_payload)
repo_meta = build_repo_meta(cve_payload)
@@ -685,7 +703,7 @@ def main() -> int:
indexed = {entry["cve"] for entry in cve_payload}
frontpage_candidates = build_trending(blacklist)
trending_items = enrich_trending(frontpage_candidates, repo_meta, kev, indexed)
landed_items = build_landed(cve_payload, repo_meta, kev, candidates=frontpage_candidates)
landed_items = build_landed(cve_payload, repo_meta, kev, ledger, candidates=frontpage_candidates)
write_json(
TRENDING_OUTPUT,
{
+1
View File
@@ -59,6 +59,7 @@
<span class="search-status" data-status aria-live="polite"></span>
<button type="submit" class="sr-only">Search</button>
</form>
<div class="data-notices" data-notices role="status" hidden></div>
<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>
+19
View File
@@ -16,6 +16,25 @@ class FrontpageReleaseTests(unittest.TestCase):
'A & B\nExample: <script>alert(1)</script>')
literal = 'A filter can be bypassed with <a href="javascript:example">link</a>.'
self.assertEqual(build_site.normalise_block(literal), literal)
encoded = 'The bypass uses &#x6A;avascript: and the literal &lt;script&gt;.'
self.assertEqual(build_site.normalise_block(encoded), encoded)
def test_undated_cves_use_real_poc_observations_without_inventing_publication_dates(self):
cve, url = 'CVE-2026-1234', 'https://github.com/owner/poc'
other = 'https://github.com/owner/other'
entry = {'cve': cve, 'poc': [url], 'collections': [other]}
dated = {**entry, 'published': '2026-01-01'}
unknown = {'cve': 'CVE-2026-1235', 'poc': [url]}
ledger = {
releases.key(cve, url): {'seen': '2026-10-21T00:30:00+02:00'},
releases.key(cve, other): {'seen': '2026-10-21T10:00:00Z'},
releases.key(unknown['cve'], url): {'imported': True}
}
build_site.add_observed_dates([entry, dated, unknown], ledger)
self.assertEqual(entry['poc_observed'], '2026-10-20')
self.assertNotIn('published', entry)
self.assertNotIn('poc_observed', dated)
self.assertNotIn('poc_observed', unknown)
def test_trending_score_uses_the_same_current_stars_it_displays(self):
cve = 'CVE-2026-1234'
+44 -9
View File
@@ -171,11 +171,44 @@ test('failed enrichment cannot silently apply bookmarked filters against empty d
kev: Promise.reject(new Error('KEV unavailable')), trending: new Promise(() => {})
});
await new Promise(setImmediate);
assert.equal(vm.runInContext('state.kevOnly', context), false);
assert.equal(vm.runInContext('state.filters.severity.size', context), 4);
assert.equal(vm.runInContext('state.kevOnly', context), true);
assert.equal(vm.runInContext('state.filters.severity.size', context), 1);
assert.match(elements.get('[data-results]').innerHTML, /href="\/CVE-2026-1234"/);
assert.match(elements.get('[data-status]').textContent, /KEV filter unavailable/);
assert.match(elements.get('[data-status]').textContent, /severity filter unavailable/);
assert.match(elements.get('[data-notices]').textContent, /KEV filter unavailable/);
assert.match(elements.get('[data-notices]').textContent, /severity filter unavailable/);
assert.equal(elements.get('[data-notices]').hidden, false);
});
test('requested filters recover after an outage without losing the bookmark or ignoring a clear', async t => {
for (const cleared of [false, true]) {
const entries = ['CVE-2026-1234', 'CVE-2026-1235'].map(cve => ({ cve, desc: 'widget', poc: ['https://github.com/owner/' + cve] }));
const feed = { generated: '2026-10-21T10:00:00Z', with_pocs: 2, total_cves: 2, items: [], landed: [] };
const { context, elements } = startPage(t, { search: '?q=widget&kev=1&sev=critical', entries, cvssError: true,
kev: Promise.reject(new Error('Unavailable')), trending: feed });
await new Promise(setImmediate);
assert.match(elements.get('[data-results]').innerHTML, /href="\/CVE-2026-1235"/);
if (cleared) vm.runInContext('state.kevOnly = false; state.filters.severity = new Set(FILTER_OPTIONS.severity.map(([value]) => value))', context);
context.fetch = async url => ({ ok: true, json: async () => {
const file = url.split('?')[0];
return file === '/trending_poc.json' ? feed : file === '/CVE_list.json' ? entries
: file === '/kev.json' ? { 'CVE-2026-1234': ['2026-10-21', false] }
: file === '/cvss.json' ? { 'CVE-2026-1234': [['3.1', 9.8, 'CRITICAL', '', 'nvd', 'Primary']] } : {};
}});
assert.equal(await vm.runInContext('refreshData(true)', context), true);
assert.equal(vm.runInContext('state.kevOnly', context), !cleared);
assert.equal(vm.runInContext('state.filters.severity.size', context), cleared ? 4 : 1);
assert.equal(elements.get('[data-results]').innerHTML.includes('href="/CVE-2026-1235"'), cleared);
assert.equal(elements.get('[data-notices]').hidden, true);
}
});
test('equal-star links use the same code-point ordering as static CVE pages', async t => {
const urls = ['poc_1', 'poc1', 'poc-1'].map(name => 'https://github.com/owner/' + name);
const entries = [{ cve: 'CVE-2026-1234', desc: 'widget', poc: urls }];
const repositories = Object.fromEntries(urls.map(url => [url.slice('https://github.com/'.length), [10, '2026-10-21']]));
const { context } = startPage(t, { entries, repositories, trending: new Promise(() => {}) });
await new Promise(setImmediate);
assert.deepEqual(Array.from(vm.runInContext('rankedLinks(dataset[0])', context)), [...urls].sort());
});
test('missing repository metadata is visible and retried even without a newer publication', async t => {
@@ -184,7 +217,7 @@ test('missing repository metadata is visible and retried even without a newer pu
const { context, elements } = startPage(t, { search: '?q=widget', entries, trending: feed,
repositories: Promise.reject(new Error('Metadata temporarily unavailable')) });
await new Promise(setImmediate);
assert.match(elements.get('[data-status]').textContent, /repository metadata unavailable/);
assert.match(elements.get('[data-notices]').textContent, /repository metadata unavailable/);
context.fetch = async url => ({ ok: true, json: async () => {
const file = url.split('?')[0];
return file === '/trending_poc.json' ? feed : file === '/CVE_list.json' ? entries
@@ -192,7 +225,7 @@ test('missing repository metadata is visible and retried even without a newer pu
}});
assert.equal(await vm.runInContext('refreshData(true)', context), true);
assert.match(elements.get('[data-results]').innerHTML, /0 <span class="star">★/);
assert.doesNotMatch(elements.get('[data-status]').textContent, /repository metadata unavailable/);
assert.doesNotMatch(elements.get('[data-notices]').textContent, /repository metadata unavailable/);
});
test('curated artifact paths survive search rendering without duplicate repository roots', async t => {
@@ -245,15 +278,17 @@ test('a bookmarked typo is corrected when indexing finishes without changing fil
test('newest sorts by publication date rather than identifier allocation order', async t => {
const entries = [
{ cve: 'CVE-2026-99999', published: '2026-01-01' },
{ cve: 'CVE-2026-99999', published: '2026-01-01', poc_observed: '2026-10-23' },
{ cve: 'CVE-2025-1234', published: '2026-10-21' },
{ cve: 'CVE-2026-88888', published: '2026-10-20' },
{ cve: 'CVE-2026-99998' }
{ cve: 'CVE-2026-99998' },
{ cve: 'CVE-2026-1234', metadata_pending: true, poc_observed: '2026-10-22' }
].map(row => ({ ...row, desc: 'widget', poc: ['https://github.com/owner/' + row.cve] }));
const { elements } = startPage(t, { search: '?q=widget&sort=newest', entries, trending: new Promise(() => {}) });
await new Promise(setImmediate);
const ids = [...elements.get('[data-results]').innerHTML.matchAll(/class="result-id" href="\/([^"]+)"/g)].map(match => match[1]);
assert.deepEqual(ids, ['CVE-2025-1234', 'CVE-2026-88888', 'CVE-2026-99999', 'CVE-2026-99998']);
assert.deepEqual(ids, ['CVE-2026-1234', 'CVE-2025-1234', 'CVE-2026-88888', 'CVE-2026-99999', 'CVE-2026-99998']);
assert.match(elements.get('[data-results]').innerHTML, /PoC observed 2026-10-22/);
});
test('a failed trending feed preserves the published homepage rows', async t => {