@blamejs/exceptd-skills 0.14.0 → 0.14.1
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/AGENTS.md +3 -1
- package/CHANGELOG.md +10 -0
- package/README.md +31 -0
- package/bin/exceptd.js +6 -0
- package/data/_indexes/_meta.json +2 -2
- package/data/playbooks/citation-hygiene.json +1 -1
- package/lib/citation-resolve.js +226 -0
- package/lib/cve-cli.js +51 -0
- package/lib/rfc-cli.js +68 -0
- package/lib/schemas/cve-catalog.schema.json +13 -0
- package/lib/source-ghsa.js +3 -0
- package/lib/source-osv.js +4 -0
- package/lib/validate-package.js +7 -2
- package/manifest.json +44 -44
- package/package.json +1 -1
- package/sbom.cdx.json +75 -30
- package/sources/validators/cve-validator.js +46 -1
|
@@ -112,6 +112,27 @@ function extractNvdCvss(nvdJson) {
|
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// NVD carries the authoritative assignment lifecycle in `vulnStatus`
|
|
116
|
+
// ("Rejected", "Disputed", "Analyzed", "Awaiting Analysis", "Modified", …)
|
|
117
|
+
// and dispute/quality flags in `cveTags[].tags` (e.g. "disputed",
|
|
118
|
+
// "unsupported-when-assigned"). Both live on the same vuln object the CVSS
|
|
119
|
+
// extractor already reads — surfacing them is what lets exceptd answer
|
|
120
|
+
// "is this CVE rejected/disputed?" instead of leaving an agent to look it up.
|
|
121
|
+
function extractNvdStatus(nvdJson) {
|
|
122
|
+
const vuln = nvdJson?.vulnerabilities?.[0]?.cve;
|
|
123
|
+
if (!vuln) return { vulnStatus: null, tags: [] };
|
|
124
|
+
const tags = [];
|
|
125
|
+
for (const ct of (Array.isArray(vuln.cveTags) ? vuln.cveTags : [])) {
|
|
126
|
+
for (const t of (Array.isArray(ct?.tags) ? ct.tags : [])) {
|
|
127
|
+
if (typeof t === 'string') tags.push(t);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
vulnStatus: typeof vuln.vulnStatus === 'string' ? vuln.vulnStatus : null,
|
|
132
|
+
tags,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
115
136
|
function pushDiscrepancy(list, field, local, fetched, severity = 'warning') {
|
|
116
137
|
list.push({ field, local, fetched, severity });
|
|
117
138
|
}
|
|
@@ -153,6 +174,9 @@ async function validateCve(cveId, localEntry) {
|
|
|
153
174
|
in_kev: null,
|
|
154
175
|
kev_date: null,
|
|
155
176
|
epss: null,
|
|
177
|
+
nvd_vuln_status: null, // NVD's authoritative status string, e.g. "Rejected"
|
|
178
|
+
cve_tags: [], // NVD cveTags, e.g. ["disputed"]
|
|
179
|
+
description: null, // NVD English description — the product/scope claim a citation must match
|
|
156
180
|
sources: { nvd: null, kev: null, epss: null },
|
|
157
181
|
};
|
|
158
182
|
const discrepancies = [];
|
|
@@ -175,6 +199,12 @@ async function validateCve(cveId, localEntry) {
|
|
|
175
199
|
fetched.sources.nvd = { reachable: true, found: false };
|
|
176
200
|
} else {
|
|
177
201
|
cveFoundInNvd = true;
|
|
202
|
+
const st = extractNvdStatus(nvdResult.json);
|
|
203
|
+
fetched.nvd_vuln_status = st.vulnStatus;
|
|
204
|
+
fetched.cve_tags = st.tags;
|
|
205
|
+
const vuln = nvdResult.json?.vulnerabilities?.[0]?.cve;
|
|
206
|
+
const enDesc = (Array.isArray(vuln?.descriptions) ? vuln.descriptions : []).find(d => d?.lang === "en");
|
|
207
|
+
fetched.description = (enDesc && typeof enDesc.value === "string") ? enDesc.value : null;
|
|
178
208
|
const cvss = extractNvdCvss(nvdResult.json);
|
|
179
209
|
if (cvss) {
|
|
180
210
|
fetched.cvss_score = cvss.score;
|
|
@@ -224,6 +254,21 @@ async function validateCve(cveId, localEntry) {
|
|
|
224
254
|
return { cve_id: cveId, status: 'missing', discrepancies, fetched, local, drift };
|
|
225
255
|
}
|
|
226
256
|
|
|
257
|
+
// --- Rejected / disputed short-circuit ---
|
|
258
|
+
// A MITRE/NVD-REJECTED record still returns totalResults:1, so without this
|
|
259
|
+
// a rejected CVE would fall through and be CVSS-drift-checked as if valid.
|
|
260
|
+
// Surface it as its own status — this is the signal that catches a cited
|
|
261
|
+
// CVE that was withdrawn after assignment (the class an agent otherwise has
|
|
262
|
+
// to confirm by hand against NVD).
|
|
263
|
+
if (cveFoundInNvd && /^rejected$/i.test(fetched.nvd_vuln_status || '')) {
|
|
264
|
+
return { cve_id: cveId, status: 'rejected', discrepancies, fetched, local, drift };
|
|
265
|
+
}
|
|
266
|
+
const disputed = (fetched.cve_tags || []).some(t => /disputed/i.test(t)) ||
|
|
267
|
+
/disputed/i.test(fetched.nvd_vuln_status || '');
|
|
268
|
+
if (cveFoundInNvd && disputed) {
|
|
269
|
+
pushDiscrepancy(discrepancies, 'cve_status', local?.status ?? null, 'disputed', 'high');
|
|
270
|
+
}
|
|
271
|
+
|
|
227
272
|
// --- Compare CVSS (only if NVD reachable & has data) ---
|
|
228
273
|
if (cveFoundInNvd && fetched.cvss_score !== null && local.cvss_score !== null) {
|
|
229
274
|
if (Math.abs(fetched.cvss_score - local.cvss_score) > 0.05) {
|
|
@@ -274,4 +319,4 @@ async function validateCve(cveId, localEntry) {
|
|
|
274
319
|
return { cve_id: cveId, status, discrepancies, fetched, local, drift };
|
|
275
320
|
}
|
|
276
321
|
|
|
277
|
-
module.exports = { validateCve, getKevCache, resetKevCache };
|
|
322
|
+
module.exports = { validateCve, getKevCache, resetKevCache, extractNvdStatus };
|