mirror of
https://github.com/0xMarcio/cve.git
synced 2026-10-23 16:35:19 +00:00
Preserve filter intent and undated CVE visibility through enrichment
This commit is contained in:
1 parent
24ba6ac25a
commit
f0d5cca38f
9 files changed
+112
-22
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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,
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 javascript: and the literal <script>.'
|
||||
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
@@ -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 => {
|
||||
|
||||
Reference in new issue
Block a user