@blamejs/exceptd-skills 0.19.32 → 0.19.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/bin/exceptd.js +896 -2824
- package/data/_indexes/_meta.json +8 -8
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +7 -7
- package/data/_indexes/chains.json +60118 -0
- package/data/attack-techniques.json +267 -7
- package/data/cve-catalog.json +9991 -3
- package/data/cwe-catalog.json +109 -2
- package/data/framework-control-gaps.json +578 -3
- package/data/zeroday-lessons.json +8330 -1
- package/lib/auto-discovery.js +56 -286
- package/lib/canonical-eq.js +7 -40
- package/lib/citation-resolve.js +22 -70
- package/lib/collectors/ai-api.js +20 -54
- package/lib/collectors/cicd-pipeline-compromise.js +40 -108
- package/lib/collectors/citation-hygiene.js +72 -210
- package/lib/collectors/containers.js +41 -130
- package/lib/collectors/cred-stores.js +31 -115
- package/lib/collectors/crypto-codebase.js +55 -138
- package/lib/collectors/crypto.js +24 -54
- package/lib/collectors/hardening.js +20 -78
- package/lib/collectors/kernel.js +16 -46
- package/lib/collectors/library-author.js +57 -206
- package/lib/collectors/mcp.js +24 -70
- package/lib/collectors/runtime.js +24 -86
- package/lib/collectors/sbom.js +34 -106
- package/lib/collectors/scan-excludes.js +31 -138
- package/lib/collectors/secrets.js +62 -178
- package/lib/cross-ref-api.js +39 -123
- package/lib/currency-severity.js +8 -27
- package/lib/cve-batch.js +13 -21
- package/lib/cve-cli.js +13 -20
- package/lib/cve-curation.js +72 -239
- package/lib/cve-regression-watcher.js +29 -152
- package/lib/cvss.js +13 -54
- package/lib/doctor-bucketing.js +3 -19
- package/lib/exit-codes.js +10 -42
- package/lib/flag-suggest.js +7 -25
- package/lib/framework-gap.js +35 -114
- package/lib/gap-detectors.js +37 -159
- package/lib/id-validation.js +9 -30
- package/lib/job-queue.js +13 -36
- package/lib/lint-skills.js +64 -232
- package/lib/playbook-runner.js +693 -2095
- package/lib/prefetch.js +100 -376
- package/lib/refresh-external.js +199 -627
- package/lib/refresh-network.js +75 -307
- package/lib/rfc-cli.js +23 -68
- package/lib/scoring.js +77 -145
- package/lib/sign.js +43 -229
- package/lib/source-advisories.js +43 -194
- package/lib/source-ghsa.js +37 -120
- package/lib/source-osv.js +94 -266
- package/lib/ttp-mapper.js +14 -24
- package/lib/upstream-check-cli.js +10 -28
- package/lib/upstream-check.js +19 -44
- package/lib/validate-catalog-meta.js +17 -61
- package/lib/validate-cve-catalog.js +43 -119
- package/lib/validate-indexes.js +25 -76
- package/lib/validate-package.js +16 -62
- package/lib/validate-playbooks.js +69 -275
- package/lib/validate-vendor.js +16 -49
- package/lib/verify.js +56 -286
- package/lib/version-pins.js +5 -34
- package/lib/worker-pool.js +11 -30
- package/lib/xml-tokenizer.js +47 -152
- package/manifest.json +53 -53
- package/orchestrator/dispatcher.js +17 -68
- package/orchestrator/event-bus.js +11 -74
- package/orchestrator/index.js +138 -412
- package/orchestrator/pipeline.js +28 -85
- package/orchestrator/scanner.js +34 -138
- package/orchestrator/scheduler.js +20 -84
- package/package.json +2 -2
- package/sbom.cdx.json +253 -253
- package/scripts/audit-catalog-gaps.js +9 -62
- package/scripts/audit-cross-skill.js +5 -31
- package/scripts/audit-perf.js +6 -16
- package/scripts/backfill-theater-test.js +7 -64
- package/scripts/bootstrap.js +12 -44
- package/scripts/build-indexes.js +40 -154
- package/scripts/builders/activity-feed.js +4 -14
- package/scripts/builders/catalog-summaries.js +3 -10
- package/scripts/builders/currency.js +7 -20
- package/scripts/builders/cwe-chains.js +7 -30
- package/scripts/builders/did-ladders.js +6 -13
- package/scripts/builders/frequency.js +5 -19
- package/scripts/builders/jurisdiction-clocks.js +6 -25
- package/scripts/builders/recipes.js +6 -14
- package/scripts/builders/section-offsets.js +13 -51
- package/scripts/builders/stale-content.js +7 -28
- package/scripts/builders/summary-cards.js +8 -29
- package/scripts/builders/theater-fingerprints.js +12 -27
- package/scripts/builders/token-budget.js +4 -31
- package/scripts/check-agents-md-collectors.js +11 -54
- package/scripts/check-catalog-gap-budget.js +15 -32
- package/scripts/check-changelog-extract.js +18 -48
- package/scripts/check-codebase-patterns-currency.js +6 -22
- package/scripts/check-codebase-patterns.js +50 -143
- package/scripts/check-epss-consistency.js +9 -64
- package/scripts/check-framework-gap-coverage.js +13 -31
- package/scripts/check-manifest-snapshot.js +13 -73
- package/scripts/check-sbom-currency.js +44 -142
- package/scripts/check-test-count.js +15 -52
- package/scripts/check-test-coverage.js +66 -197
- package/scripts/check-test-subjects.js +21 -62
- package/scripts/check-ttp-references.js +14 -38
- package/scripts/check-ttp-upstream.js +8 -40
- package/scripts/check-version-bump.js +9 -61
- package/scripts/check-version-tags.js +20 -121
- package/scripts/predeploy.js +38 -184
- package/scripts/refresh-manifest-snapshot.js +16 -38
- package/scripts/refresh-mitre-atlas.js +3 -8
- package/scripts/refresh-mitre-attack.js +1 -8
- package/scripts/refresh-mitre-d3fend.js +3 -9
- package/scripts/refresh-mitre-ics-attack.js +3 -8
- package/scripts/refresh-reverse-refs.js +27 -94
- package/scripts/refresh-rfc-index.js +2 -10
- package/scripts/refresh-sbom.js +31 -161
- package/scripts/refresh-upstream-catalogs.js +40 -137
- package/scripts/release.js +69 -232
- package/scripts/run-e2e-scenarios.js +24 -71
- package/scripts/sync-manifest-metadata.js +10 -34
- package/scripts/sync-package-description.js +8 -17
- package/scripts/validate-vendor-online.js +13 -44
- package/scripts/verify-shipped-tarball.js +35 -140
|
@@ -1,30 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* tree (source, comments, docstrings, and security documentation) and
|
|
8
|
-
* extracts every CVE and RFC citation, then cross-references each against
|
|
9
|
-
* the shipped CVE catalog (data/cve-catalog.json) and RFC index
|
|
10
|
-
* (data/rfc-references.json).
|
|
11
|
-
*
|
|
12
|
-
* It flips signal_overrides only for verdicts determinable offline from
|
|
13
|
-
* the catalogs:
|
|
14
|
-
* - fabricated-cve-id: a citation whose tail is not the canonical
|
|
15
|
-
* all-numeric CVE form (CVE-2024-XXXX, CVE-2024-zlib). Deterministic.
|
|
16
|
-
* - rejected-or-disputed-cve: a well-formed citation that resolves to a
|
|
17
|
-
* catalog entry whose analyst notes mark it rejected / disputed.
|
|
18
|
-
* - rfc-number-title-mismatch: a citation pairing a number with a title
|
|
19
|
-
* that conflicts with the index title for that number.
|
|
20
|
-
*
|
|
21
|
-
* Indicators that need an out-of-band lookup or human judgement
|
|
22
|
-
* (cve-citation-needs-external-verification, draft-mislabeled-as-rfc) are
|
|
23
|
-
* surfaced in the artifacts text and left UNFLIPPED so the runner returns
|
|
24
|
-
* inconclusive rather than a forced miss — the catalog is curated, not
|
|
25
|
-
* exhaustive, so absence is never a clean clear or a false fabrication.
|
|
26
|
-
*
|
|
27
|
-
* Interface: see lib/collectors/README.md
|
|
4
|
+
* Companion collector for the `citation-hygiene` playbook: cross-references CVE
|
|
5
|
+
* and RFC citations in the cwd tree against data/cve-catalog.json and
|
|
6
|
+
* data/rfc-references.json. Interface: see lib/collectors/README.md
|
|
28
7
|
*/
|
|
29
8
|
|
|
30
9
|
const fs = require("node:fs");
|
|
@@ -34,10 +13,7 @@ const { codeExcludeSet, walkTree, buildEvidenceLocations, lineFromOffset } = req
|
|
|
34
13
|
|
|
35
14
|
const COLLECTOR_ID = "citation-hygiene";
|
|
36
15
|
|
|
37
|
-
//
|
|
38
|
-
// collector resolve the citations the offline catalog can't confirm — once,
|
|
39
|
-
// through the shared cache — instead of parking them as inconclusive for an
|
|
40
|
-
// agent to research. Required lazily so a plain collect() never loads it.
|
|
16
|
+
// Required lazily so a plain collect() never loads the resolver.
|
|
41
17
|
function loadResolver() {
|
|
42
18
|
return require("../citation-resolve.js");
|
|
43
19
|
}
|
|
@@ -45,9 +21,7 @@ function loadResolver() {
|
|
|
45
21
|
const DEFAULT_MAX_DEPTH = 8;
|
|
46
22
|
const EXCLUDES = codeExcludeSet();
|
|
47
23
|
|
|
48
|
-
//
|
|
49
|
-
// markup/docs, config that carries security prose. Citations live in
|
|
50
|
-
// comments and docstrings (source) and in docs (md / rst / txt / adoc).
|
|
24
|
+
// Citations live in source comments, and in docs and config prose.
|
|
51
25
|
const SCAN_EXTS = new Set([
|
|
52
26
|
".js", ".mjs", ".cjs", ".jsx", ".ts", ".tsx", ".mts", ".cts",
|
|
53
27
|
".py", ".pyi",
|
|
@@ -66,16 +40,13 @@ const SCAN_EXTS = new Set([
|
|
|
66
40
|
|
|
67
41
|
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
68
42
|
|
|
69
|
-
// Paths whose citations are illustrative
|
|
70
|
-
// scanner's own pattern catalogue), not real self-citations.
|
|
43
|
+
// Paths whose citations are illustrative, not real self-citations.
|
|
71
44
|
const ILLUSTRATIVE_PATH_SEGMENTS = [
|
|
72
45
|
"/test/", "/tests/", "/spec/", "/specs/", "/__tests__/",
|
|
73
46
|
"/fixtures/", "/fixture/",
|
|
74
47
|
"/.github/issue_template/", "/.github/pull_request_template/",
|
|
75
48
|
"/issue_template/", "/pull_request_template/",
|
|
76
|
-
//
|
|
77
|
-
// RFC patterns and example citations; scanning them would flag the
|
|
78
|
-
// scanner itself. The playbook's intent is the consumer's source.
|
|
49
|
+
// These directories hold the patterns themselves; scanning them flags the scanner.
|
|
79
50
|
"/lib/collectors/", "/data/playbooks/", "/lib/schemas/",
|
|
80
51
|
];
|
|
81
52
|
|
|
@@ -91,60 +62,37 @@ function isIllustrativePath(rel) {
|
|
|
91
62
|
|
|
92
63
|
function readSafe(full) {
|
|
93
64
|
try {
|
|
94
|
-
//
|
|
95
|
-
// Replaces a statSync-before-read with a single read; the cap is
|
|
96
|
-
// byte-based, so Buffer.length is the correct measure and an oversized
|
|
97
|
-
// file is rejected before any UTF-8 decode.
|
|
65
|
+
// The cap is byte-based: enforced on the buffer, before any UTF-8 decode.
|
|
98
66
|
const raw = fs.readFileSync(full);
|
|
99
67
|
if (raw.length > MAX_FILE_BYTES) return null;
|
|
100
68
|
return raw.toString("utf8");
|
|
101
69
|
} catch { return null; }
|
|
102
70
|
}
|
|
103
71
|
|
|
104
|
-
// Permissive
|
|
105
|
-
//
|
|
106
|
-
// not silently skipped). The canonical-form test is applied afterwards.
|
|
72
|
+
// Permissive: a letter tail is captured, not skipped, so the canonical-form test
|
|
73
|
+
// below can flag it.
|
|
107
74
|
const CVE_CITATION_RE = /CVE-(\d{4})-([0-9A-Za-z]+)/g;
|
|
108
75
|
const CVE_CANONICAL_RE = /^CVE-\d{4}-\d{4,}$/;
|
|
109
76
|
|
|
110
|
-
// RFC citation: `RFC 9404`, `RFC9404`, `RFC-9404`. Capture the number.
|
|
111
77
|
const RFC_CITATION_RE = /RFC[\s-]?(\d{1,5})\b/gi;
|
|
112
78
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// DIFFERENT CVE the note merely mentions. A bare word-anywhere scan matched all
|
|
117
|
-
// three, producing a false "citation to a rejected record" hit AND a false
|
|
118
|
-
// __fp_checks[1] attestation (telling the runner not to downgrade), so a valid,
|
|
119
|
-
// often actively-exploited CVE surfaced as a confirmed rejected/disputed
|
|
120
|
-
// citation. `selfId` is the citation's own CVE id.
|
|
79
|
+
// True only when a reject/dispute/withdraw word refers to THIS citation's own
|
|
80
|
+
// record — not a CVSS disagreement, a coordination dispute, or another CVE the
|
|
81
|
+
// note mentions. `selfId` is the citation's own CVE id.
|
|
121
82
|
function recordRejectedOrDisputed(note, selfId) {
|
|
122
83
|
if (!note) return false;
|
|
123
84
|
const self = String(selfId || "").toUpperCase();
|
|
124
85
|
const re = /\b(reject(?:ed|s|ion)?|disputed?|withdrawn)\b/gi;
|
|
125
|
-
// Qualifier nouns that make a "dispute" a disagreement about something
|
|
126
|
-
// than the record's validity
|
|
127
|
-
// process, …). 'rejected'/'withdrawn' are record-level words and bypass this.
|
|
86
|
+
// Qualifier nouns that make a "dispute" a disagreement about something other
|
|
87
|
+
// than the record's validity; 'rejected' / 'withdrawn' bypass this.
|
|
128
88
|
const QUALIFIER = /\b(cvss|scoring|score|severity|coordination|disclosure|methodolog\w*|attribution|naming|assignment|priorit\w*)\b/i;
|
|
129
|
-
// A "duplicate of / superseded by /
|
|
130
|
-
//
|
|
131
|
-
// one, so a different cve appearing as that replacement must NOT suppress the
|
|
132
|
-
// flag (e.g. "this record was rejected as a duplicate of CVE-Y").
|
|
89
|
+
// A "duplicate of / superseded by / in favour of" construction names the
|
|
90
|
+
// REPLACEMENT cve, so that other id must not suppress the flag.
|
|
133
91
|
const REPLACEMENT_OF = /\b(?:duplicate|dup)\b[\s\w-]*\bof\b|\b(?:supersed\w+|replaced|merged)\b[\s\w-]*\b(?:by|into)\b|\bin\s+favou?r\s+of\b/i;
|
|
134
|
-
// "Reject" is also
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
// entry, an identifier — or who rejected it (MITRE, NVD, the CNA), so require
|
|
139
|
-
// one of those nearby before treating a reject-word as record-level. The
|
|
140
|
-
// alternative is rewording accurate mechanism prose to dodge a regex, which
|
|
141
|
-
// costs the catalog more than the false positive does.
|
|
142
|
-
// Deliberately excludes bare "entry" and "advisory". Both are ordinary words
|
|
143
|
-
// in mechanism prose — "rejection log entry", "cache entry", "registry entry" —
|
|
144
|
-
// and including them let an Exim entry describing SMTP message rejection read
|
|
145
|
-
// as a claim that the CVE record itself was rejected. The surviving tokens all
|
|
146
|
-
// name the record or the body that maintains it, so they cannot be satisfied
|
|
147
|
-
// by a sentence about software rejecting input.
|
|
92
|
+
// "Reject" is also the ordinary verb for what code does to input, so the record
|
|
93
|
+
// sense must name what was rejected or who rejected it. Bare "entry" and
|
|
94
|
+
// "advisory" stay out: they are ordinary words in mechanism prose, and admitting
|
|
95
|
+
// them reads SMTP message rejection as a rejected CVE record.
|
|
148
96
|
const RECORD_CONTEXT = /\b(CVE record|catalog(?:ue)? entry|record status|identifier|assignment|CVE|MITRE|NVD|CNA)\b/i;
|
|
149
97
|
const otherCve = (s) => (s.match(/CVE-\d{4}-\d{4,}/gi) || []).some((c) => c.toUpperCase() !== self);
|
|
150
98
|
let m;
|
|
@@ -152,21 +100,16 @@ function recordRejectedOrDisputed(note, selfId) {
|
|
|
152
100
|
const word = m[1].toLowerCase();
|
|
153
101
|
const before = note.slice(Math.max(0, m.index - 60), m.index);
|
|
154
102
|
const after = note.slice(re.lastIndex, re.lastIndex + 60);
|
|
155
|
-
//
|
|
156
|
-
// — the status is about that record, not this citation.
|
|
103
|
+
// A different cve BEFORE the word is the subject: that record's status.
|
|
157
104
|
if (otherCve(before)) continue;
|
|
158
|
-
//
|
|
159
|
-
// replacement target of a duplicate-of/superseded-by construction, in
|
|
160
|
-
// which case THIS record is the rejected one — keep it flagged.
|
|
105
|
+
// A cve AFTER suppresses too, unless it is a duplicate-of replacement target.
|
|
161
106
|
if (otherCve(after) && !REPLACEMENT_OF.test(after)) continue;
|
|
162
|
-
//
|
|
163
|
-
// that noun, not a record rejection.
|
|
107
|
+
// A 'dispute' qualified by a non-record noun is about that noun.
|
|
164
108
|
if (word.startsWith("disput")) {
|
|
165
109
|
const lastTokens = before.trim().split(/[\s-]+/).slice(-3).join(" ");
|
|
166
110
|
if (QUALIFIER.test(lastTokens)) continue;
|
|
167
111
|
}
|
|
168
|
-
//
|
|
169
|
-
// — code rejecting input — not a statement about this CVE's record.
|
|
112
|
+
// A 'reject' with nothing record-shaped around it is the ordinary verb.
|
|
170
113
|
if (word.startsWith("reject") && !RECORD_CONTEXT.test(before + " " + after)) continue;
|
|
171
114
|
return true;
|
|
172
115
|
}
|
|
@@ -177,10 +120,9 @@ function recordRejectedOrDisputed(note, selfId) {
|
|
|
177
120
|
const DRAFT_LANGUAGE_RE = /\b(draft-[a-z0-9-]+|internet[- ]draft|work[- ]in[- ]progress|i-d\b)\b/i;
|
|
178
121
|
|
|
179
122
|
/**
|
|
180
|
-
* Load the shipped CVE catalog and RFC index
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* install. Returns { cveKeys:Set, cveNotes:Map<id,string>, rfcTitles:Map<number,string>, errors:[] }.
|
|
123
|
+
* Load the shipped CVE catalog and RFC index, resolved relative to this module so
|
|
124
|
+
* the collector works from a source tree or a node_modules install. Returns
|
|
125
|
+
* { cveKeys:Set, cveNotes:Map<id,string>, rfcTitles:Map<number,string>, errors:[] }.
|
|
184
126
|
*/
|
|
185
127
|
function loadCatalogs() {
|
|
186
128
|
const errors = [];
|
|
@@ -195,9 +137,8 @@ function loadCatalogs() {
|
|
|
195
137
|
if (k.startsWith("_")) continue;
|
|
196
138
|
cveKeys.add(k);
|
|
197
139
|
if (v && typeof v === "object") {
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
// neighbour's) is enforced by per-entry concatenation.
|
|
140
|
+
// Analyst-note fields carrying rejected / disputed status, concatenated
|
|
141
|
+
// per entry so a citation matches its own notes and never a neighbour's.
|
|
201
142
|
const noteParts = [
|
|
202
143
|
v.cvss_note, v.active_exploitation_notes, v.vector,
|
|
203
144
|
v.discovery_attribution_note, v.ai_discovery_notes,
|
|
@@ -225,8 +166,6 @@ function loadCatalogs() {
|
|
|
225
166
|
return { cveKeys, cveNotes, rfcTitles, errors };
|
|
226
167
|
}
|
|
227
168
|
|
|
228
|
-
// Normalise a title for comparison: lowercase, drop punctuation, collapse
|
|
229
|
-
// whitespace, and strip a leading "the".
|
|
230
169
|
function normalizeTitle(s) {
|
|
231
170
|
return s
|
|
232
171
|
.toLowerCase()
|
|
@@ -247,42 +186,24 @@ function titleTokens(s) {
|
|
|
247
186
|
);
|
|
248
187
|
}
|
|
249
188
|
|
|
250
|
-
// Ordered list of meaningful (post-stopword, non-numeric) tokens in a
|
|
251
|
-
// title — used both for overlap and for acronym construction.
|
|
252
189
|
function orderedTitleTokens(s) {
|
|
253
190
|
return normalizeTitle(s)
|
|
254
191
|
.split(" ")
|
|
255
192
|
.filter((t) => t.length >= 3 && !/^\d+$/.test(t) && !TITLE_STOPWORDS.has(t));
|
|
256
193
|
}
|
|
257
194
|
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
// stopwords). Lets a nickname / abbreviation in the adjacent text be
|
|
261
|
-
// recognised as the same document, not a wrong title.
|
|
195
|
+
// Lowercase acronym from the title's meaningful words: "Transport Layer Security
|
|
196
|
+
// Protocol" gives "tls". Lets a nickname be recognised as the same document.
|
|
262
197
|
function titleAcronym(realTitle) {
|
|
263
198
|
return orderedTitleTokens(realTitle).map((w) => w[0]).join("");
|
|
264
199
|
}
|
|
265
200
|
|
|
266
201
|
/**
|
|
267
|
-
* Decide whether an EXPLICITLY STATED title conflicts with the real index
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
* This is deliberately strict because the dominant real-world pattern is a
|
|
273
|
-
* mechanism citation — "CRLF line endings per RFC 5322", "renders values per
|
|
274
|
-
* RFC 8785", "ETag repeated per RFC 7232 §4.1" — where the prose describes
|
|
275
|
-
* what the code does *per* the RFC using vocabulary that never overlaps the
|
|
276
|
-
* RFC's formal title. Comparing that prose against the title produced a
|
|
277
|
-
* false "mismatch" on correct citations. Such references state no title and
|
|
278
|
-
* are filtered out before this function is reached.
|
|
279
|
-
*
|
|
280
|
-
* - at least TWO meaningful tokens in the stated title (a bare acronym /
|
|
281
|
-
* nickname is not a title),
|
|
282
|
-
* - the title's acronym appearing in the stated title is the same document
|
|
283
|
-
* (TLS for Transport Layer Security); not a mismatch,
|
|
284
|
-
* - only ZERO overlap between stated-title tokens and real-title tokens
|
|
285
|
-
* flags a mismatch; any shared content word is a paraphrase — demote.
|
|
202
|
+
* Decide whether an EXPLICITLY STATED title conflicts with the real index title.
|
|
203
|
+
* Only a title introduced immediately after the RFC number counts (see
|
|
204
|
+
* statedTitleAfter): a mechanism citation states no title, and comparing that
|
|
205
|
+
* prose against a formal title reports a mismatch on a correct citation. Only
|
|
206
|
+
* ZERO token overlap is a mismatch — any shared content word is a paraphrase.
|
|
286
207
|
* Returns "mismatch" | "match" | "no-title-claim".
|
|
287
208
|
*/
|
|
288
209
|
function classifyRfcTitle(statedTitle, realTitle) {
|
|
@@ -290,30 +211,21 @@ function classifyRfcTitle(statedTitle, realTitle) {
|
|
|
290
211
|
if (adjTokens.size < 2) return "no-title-claim";
|
|
291
212
|
const realTokens = titleTokens(realTitle);
|
|
292
213
|
if (realTokens.size === 0) return "no-title-claim";
|
|
293
|
-
// Acronym recognition: "tls" in the stated title matches "Transport
|
|
294
|
-
// Layer Security". Same document, not a wrong title.
|
|
295
214
|
const acronym = titleAcronym(realTitle);
|
|
296
215
|
if (acronym.length >= 2 && adjTokens.has(acronym)) return "match";
|
|
297
216
|
let overlap = 0;
|
|
298
217
|
for (const t of adjTokens) {
|
|
299
218
|
if (realTokens.has(t)) { overlap++; continue; }
|
|
300
|
-
// Nickname
|
|
301
|
-
//
|
|
302
|
-
// under a common name — "IMAP4rev2" carries the real token "imap"
|
|
303
|
-
// ("...Access Protocol (IMAP)..."). Avoids false mismatches on the way
|
|
304
|
-
// developers actually cite RFCs by their well-known short names.
|
|
219
|
+
// Nickname recognition: a stated token containing (or contained by) a real
|
|
220
|
+
// token of length >= 4 is the same document — "IMAP4rev2" carries "imap".
|
|
305
221
|
for (const rt of realTokens) {
|
|
306
222
|
if (rt.length >= 4 && (t.includes(rt) || rt.includes(t))) { overlap++; break; }
|
|
307
223
|
}
|
|
308
224
|
}
|
|
309
|
-
// Any shared content word -> the author is describing the right
|
|
310
|
-
// document. Only a stated title with ZERO overlap is a conflicting
|
|
311
|
-
// claim. This trades recall for precision intentionally.
|
|
312
225
|
return overlap === 0 ? "mismatch" : "match";
|
|
313
226
|
}
|
|
314
227
|
|
|
315
|
-
//
|
|
316
|
-
// for the RFC title comparison.
|
|
228
|
+
// The whole line containing `index`, as adjacent text for the title comparison.
|
|
317
229
|
function lineAround(content, index) {
|
|
318
230
|
const start = content.lastIndexOf("\n", index) + 1;
|
|
319
231
|
let end = content.indexOf("\n", index);
|
|
@@ -321,24 +233,12 @@ function lineAround(content, index) {
|
|
|
321
233
|
return content.slice(start, end);
|
|
322
234
|
}
|
|
323
235
|
|
|
324
|
-
// Extract a title EXPLICITLY QUOTED immediately after the RFC number on the
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
// and "X per RFC N" mechanism attributions describe usage, not the title;
|
|
331
|
-
// - bare nicknames ("RFC 9051 (IMAP4rev2)") are common short names;
|
|
332
|
-
// - and crucially, an RFC-number-shaped token inside CODE (`envelope.rfc822`
|
|
333
|
-
// matches "RFC 822"; `RFC 3339:` ahead of an object literal) is followed
|
|
334
|
-
// by code punctuation, never a quoted title — so comparing a code fragment
|
|
335
|
-
// against a formal title can no longer produce a phantom mismatch.
|
|
336
|
-
// The opening quote must be SEPARATED from the number by whitespace or a
|
|
337
|
-
// `:` / `(` introducer. A quote touching the last digit (`"…RFC 3339"`) is the
|
|
338
|
-
// CLOSING quote of a string that happens to end with the citation, not the
|
|
339
|
-
// opening quote of a title — without this guard the following code was
|
|
340
|
-
// captured as a phantom "title". The closing quote bounds the title; straight
|
|
341
|
-
// and typographic quotes are accepted.
|
|
236
|
+
// Extract a title EXPLICITLY QUOTED immediately after the RFC number on the same
|
|
237
|
+
// line — `RFC N "The Title"`, `RFC N: "The Title"`, `RFC N ("The Title")`. Prose,
|
|
238
|
+
// section pointers, bare nicknames and `envelope.rfc822` state no title. The
|
|
239
|
+
// opening quote must be SEPARATED from the number by whitespace or a `:` / `(`
|
|
240
|
+
// introducer: one touching the last digit is the CLOSING quote of a string ending
|
|
241
|
+
// with the citation, and the code after it is then captured as a phantom title.
|
|
342
242
|
function statedTitleAfter(after) {
|
|
343
243
|
const m = after.match(/^(?:\s*[:(]\s*|\s+)["“]([^"”\n]{3,100})["”]/);
|
|
344
244
|
return m ? m[1].trim() : null;
|
|
@@ -370,16 +270,13 @@ function collect({ cwd = process.cwd() } = {}) {
|
|
|
370
270
|
|
|
371
271
|
const scanFiles = files.filter((f) => SCAN_EXTS.has(path.extname(f.name).toLowerCase()));
|
|
372
272
|
|
|
373
|
-
//
|
|
374
|
-
// artifact summary is auditable. CVE / RFC literals are references, not
|
|
375
|
-
// secrets, so they are safe to retain in the value text.
|
|
273
|
+
// Each entry keeps file and citation text so the artifact summary is auditable.
|
|
376
274
|
const hits = {
|
|
377
275
|
"fabricated-cve-id": [],
|
|
378
276
|
"rejected-or-disputed-cve": [],
|
|
379
277
|
"rfc-number-title-mismatch": [],
|
|
380
278
|
};
|
|
381
|
-
//
|
|
382
|
-
// never flipped to a deterministic verdict.
|
|
279
|
+
// Needs-verification buckets: surfaced in artifacts, never flipped to a verdict.
|
|
383
280
|
const needsVerify = {
|
|
384
281
|
cve_not_in_catalog: [],
|
|
385
282
|
rfc_not_in_index: [],
|
|
@@ -397,36 +294,29 @@ function collect({ cwd = process.cwd() } = {}) {
|
|
|
397
294
|
}
|
|
398
295
|
const illustrative = isIllustrativePath(f.rel);
|
|
399
296
|
|
|
400
|
-
// ---- CVE citations ----
|
|
401
297
|
for (const m of content.matchAll(CVE_CITATION_RE)) {
|
|
402
298
|
const full = m[0];
|
|
403
299
|
totalCveCitations++;
|
|
404
|
-
// 1-based
|
|
405
|
-
// startLine region. Does not change any hit/miss verdict.
|
|
300
|
+
// 1-based, so the evidence location carries a SARIF startLine region.
|
|
406
301
|
const cveLine = lineFromOffset(content, m.index);
|
|
407
302
|
const canonical = CVE_CANONICAL_RE.test(full);
|
|
408
303
|
if (!canonical) {
|
|
409
|
-
// Fabricated / malformed. Illustrative surfaces (templates,
|
|
410
|
-
// fixtures, the format-explaining docs) are demoted.
|
|
411
304
|
if (!illustrative) {
|
|
412
305
|
hits["fabricated-cve-id"].push({ file: f.rel, citation: full, line: cveLine });
|
|
413
306
|
}
|
|
414
307
|
continue;
|
|
415
308
|
}
|
|
416
|
-
// Well-formed. Cross-reference the catalog.
|
|
417
309
|
if (cveKeys.has(full)) {
|
|
418
310
|
const note = cveNotes.get(full) || "";
|
|
419
311
|
if (recordRejectedOrDisputed(note, full) && !illustrative) {
|
|
420
312
|
hits["rejected-or-disputed-cve"].push({ file: f.rel, citation: full, line: cveLine });
|
|
421
313
|
}
|
|
422
314
|
} else if (catalogsLoaded && !illustrative) {
|
|
423
|
-
// Absent from the curated catalog
|
|
424
|
-
// NOT a fabrication — inconclusive by design.
|
|
315
|
+
// Absent from the curated catalog is inconclusive, not a fabrication.
|
|
425
316
|
needsVerify.cve_not_in_catalog.push({ file: f.rel, citation: full });
|
|
426
317
|
}
|
|
427
318
|
}
|
|
428
319
|
|
|
429
|
-
// ---- RFC citations ----
|
|
430
320
|
for (const m of content.matchAll(RFC_CITATION_RE)) {
|
|
431
321
|
totalRfcCitations++;
|
|
432
322
|
const num = Number(m[1]);
|
|
@@ -447,9 +337,7 @@ function collect({ cwd = process.cwd() } = {}) {
|
|
|
447
337
|
});
|
|
448
338
|
}
|
|
449
339
|
} else if (catalogsLoaded && !illustrative) {
|
|
450
|
-
//
|
|
451
|
-
// language is adjacent, record it as a draft-as-RFC candidate
|
|
452
|
-
// (still inconclusive — left unflipped).
|
|
340
|
+
// Adjacent draft language makes it a draft-as-RFC candidate, still unflipped.
|
|
453
341
|
needsVerify.rfc_not_in_index.push({ file: f.rel, citation: `RFC ${num}` });
|
|
454
342
|
if (DRAFT_LANGUAGE_RE.test(line)) {
|
|
455
343
|
needsVerify.draft_as_rfc_candidates.push({ file: f.rel, citation: `RFC ${num}` });
|
|
@@ -458,53 +346,39 @@ function collect({ cwd = process.cwd() } = {}) {
|
|
|
458
346
|
}
|
|
459
347
|
}
|
|
460
348
|
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
// returns inconclusive for them.
|
|
349
|
+
// Only deterministically-decidable indicators flip; the rest stay absent so the
|
|
350
|
+
// runner returns inconclusive for them.
|
|
464
351
|
const signal_overrides = {
|
|
465
352
|
"fabricated-cve-id": hits["fabricated-cve-id"].length > 0 ? "hit" : "miss",
|
|
466
353
|
"rfc-number-title-mismatch": hits["rfc-number-title-mismatch"].length > 0 ? "hit" : "miss",
|
|
467
354
|
};
|
|
468
|
-
//
|
|
469
|
-
// on a catalog-backed match, otherwise miss. Only assert a verdict when
|
|
470
|
-
// the catalog actually loaded; without it the check could not run.
|
|
355
|
+
// Only assert a verdict when the catalog loaded; without it the check cannot run.
|
|
471
356
|
if (cveKeys.size > 0) {
|
|
472
357
|
signal_overrides["rejected-or-disputed-cve"] =
|
|
473
358
|
hits["rejected-or-disputed-cve"].length > 0 ? "hit" : "miss";
|
|
474
359
|
} else {
|
|
475
360
|
signal_overrides["rejected-or-disputed-cve"] = "inconclusive";
|
|
476
361
|
}
|
|
477
|
-
//
|
|
478
|
-
// offline catalog cannot confirm" — itself an inconclusive state, so it
|
|
479
|
-
// maps to inconclusive (not a clean miss) when such citations exist.
|
|
362
|
+
// Citations the offline catalog cannot confirm are inconclusive, not a miss.
|
|
480
363
|
if (needsVerify.cve_not_in_catalog.length > 0) {
|
|
481
364
|
signal_overrides["cve-citation-needs-external-verification"] = "inconclusive";
|
|
482
365
|
}
|
|
483
366
|
|
|
484
|
-
// __fp_checks
|
|
485
|
-
//
|
|
486
|
-
// fixture / doc-snippet) paths and is keyed off the shipped catalogs, so the
|
|
487
|
-
// path / catalog-cross-reference / same-citation checks the collector ran
|
|
488
|
-
// are attested; surrounding-text-acknowledgement remains operator judgement.
|
|
489
|
-
// Without this the runner downgrades a real bad citation to inconclusive.
|
|
367
|
+
// __fp_checks attest the FP checks this collector ran; without the attestation
|
|
368
|
+
// the runner downgrades a real bad citation to inconclusive.
|
|
490
369
|
if (signal_overrides["fabricated-cve-id"] === "hit") {
|
|
491
|
-
// [0]
|
|
492
|
-
//
|
|
493
|
-
// pending) never match the numeric citation regex, so a fired hit is
|
|
494
|
-
// not a placeholder.
|
|
370
|
+
// [0] illustrative paths are excluded before the hit; [1] placeholder forms
|
|
371
|
+
// never match the numeric citation regex.
|
|
495
372
|
signal_overrides["fabricated-cve-id__fp_checks"] = { "0": true, "1": true };
|
|
496
373
|
}
|
|
497
374
|
if (signal_overrides["rejected-or-disputed-cve"] === "hit") {
|
|
498
|
-
// [1] the catalog note marks THIS
|
|
499
|
-
//
|
|
500
|
-
// [0] inline dispute-acknowledgement in surrounding prose is operator
|
|
501
|
-
// judgement — left unattested.
|
|
375
|
+
// [1] the catalog note marks THIS identifier; [2] it is in the catalog.
|
|
376
|
+
// [0] inline dispute acknowledgement in prose is left unattested.
|
|
502
377
|
signal_overrides["rejected-or-disputed-cve__fp_checks"] = { "1": true, "2": true };
|
|
503
378
|
}
|
|
504
379
|
if (signal_overrides["rfc-number-title-mismatch"] === "hit") {
|
|
505
|
-
// [0] a paraphrase / nickname
|
|
506
|
-
//
|
|
507
|
-
// extracted from the SAME citation line.
|
|
380
|
+
// [0] a paraphrase / nickname does not fire; [1] numbers absent from the
|
|
381
|
+
// index do not fire; [2] the stated title comes from the same line.
|
|
508
382
|
signal_overrides["rfc-number-title-mismatch__fp_checks"] = { "0": true, "1": true, "2": true };
|
|
509
383
|
}
|
|
510
384
|
|
|
@@ -549,10 +423,7 @@ function collect({ cwd = process.cwd() } = {}) {
|
|
|
549
423
|
},
|
|
550
424
|
};
|
|
551
425
|
|
|
552
|
-
//
|
|
553
|
-
// so SARIF results point at the source file that carries the bad
|
|
554
|
-
// citation. The hits record a 1-based `line` (from the match offset),
|
|
555
|
-
// so locations include a startLine region.
|
|
426
|
+
// Locations for flipped indicators, so SARIF results point at the source line.
|
|
556
427
|
const evidence_locations = {};
|
|
557
428
|
for (const id of Object.keys(hits)) {
|
|
558
429
|
if (signal_overrides[id] === "hit") {
|
|
@@ -568,9 +439,7 @@ function collect({ cwd = process.cwd() } = {}) {
|
|
|
568
439
|
artifacts,
|
|
569
440
|
signal_overrides,
|
|
570
441
|
...(Object.keys(evidence_locations).length ? { evidence_locations } : {}),
|
|
571
|
-
//
|
|
572
|
-
// (opt-in --resolve) consumes this to resolve + flip them; on a plain
|
|
573
|
-
// collect it documents what still needs verification.
|
|
442
|
+
// Consumed by applyResolution (--resolve) to resolve and flip these.
|
|
574
443
|
needs_verification: needsVerify,
|
|
575
444
|
collector_meta: {
|
|
576
445
|
collector_id: COLLECTOR_ID,
|
|
@@ -590,14 +459,11 @@ function collect({ cwd = process.cwd() } = {}) {
|
|
|
590
459
|
}
|
|
591
460
|
|
|
592
461
|
/**
|
|
593
|
-
* Resolve the citations
|
|
594
|
-
*
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
* Mutates a shallow copy of the submission's signal_overrides and records a
|
|
600
|
-
* resolution summary artifact. Returns the updated submission.
|
|
462
|
+
* Resolve the citations collect() left as needs-verification, flipping the parked
|
|
463
|
+
* signals from inconclusive to a real verdict. Each uncatalogued CVE goes through
|
|
464
|
+
* the shared resolver (catalog -> cache -> one NVD lookup, cached), so a fan-out
|
|
465
|
+
* resolves each id once. Returns a shallow copy carrying a resolution summary
|
|
466
|
+
* artifact; `submission` itself is not mutated.
|
|
601
467
|
*
|
|
602
468
|
* @param {object} submission the object returned by collect()
|
|
603
469
|
* @param {object} [opts] { airGap?: boolean, _resolveCve?, _resolveRfc? }
|
|
@@ -630,9 +496,7 @@ async function applyResolution(submission, opts = {}) {
|
|
|
630
496
|
}
|
|
631
497
|
if (rejectedHit) signals["rejected-or-disputed-cve"] = "hit";
|
|
632
498
|
if (fabricatedHit) signals["fabricated-cve-id"] = "hit";
|
|
633
|
-
//
|
|
634
|
-
// classified, inconclusive while any remain unresolvable (NVD unreachable /
|
|
635
|
-
// air-gap), and absent when there was nothing to verify.
|
|
499
|
+
// A miss once every parked CVE is classified; inconclusive while any stay unknown.
|
|
636
500
|
if (cveList.length > 0) {
|
|
637
501
|
signals["cve-citation-needs-external-verification"] = cveUnknown > 0 ? "inconclusive" : "miss";
|
|
638
502
|
}
|
|
@@ -643,9 +507,7 @@ async function applyResolution(submission, opts = {}) {
|
|
|
643
507
|
const num = (cite.match(/(\d+)/) || [])[1];
|
|
644
508
|
const r = await resolver.resolveRfc(num || cite, { airGap });
|
|
645
509
|
resolved.rfc.push({ citation: cite, file: item.file, status: r.status, found: r.found, from: r.from, title: r.title || null });
|
|
646
|
-
// A
|
|
647
|
-
// as a fabricated CVE — surface it instead of discarding the verdict. (An
|
|
648
|
-
// obsoleted/historic RFC that resolves IS a real RFC, so it isn't flagged.)
|
|
510
|
+
// A number resolving to nothing is a bad citation; an obsoleted RFC still resolves.
|
|
649
511
|
if (r.status === "nonexistent") rfcNonexistentHit = true;
|
|
650
512
|
}
|
|
651
513
|
if (rfcNonexistentHit) signals["rfc-number-title-mismatch"] = "hit";
|