@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.
@@ -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 };