@blamejs/exceptd-skills 0.21.9 → 0.21.11
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 +14 -0
- package/data/_indexes/_meta.json +4 -4
- package/data/_indexes/activity-feed.json +2 -2
- package/data/_indexes/catalog-summaries.json +2 -2
- package/data/cve-catalog.json +247 -247
- package/data/zeroday-lessons.json +785 -785
- package/lib/cve-batch.js +9 -0
- package/lib/gap-detectors.js +157 -0
- package/manifest.json +53 -53
- package/package.json +1 -1
- package/sbom.cdx.json +26 -26
- package/scripts/audit-catalog-gaps.js +8 -0
- package/scripts/check-catalog-gap-budget.js +3 -2
package/lib/cve-batch.js
CHANGED
|
@@ -195,6 +195,15 @@ async function curateBatch({ factsPath, judgmentsPath, apply, catalogRoot, today
|
|
|
195
195
|
if (f.definite) errors.push(`${where(f)} ${f.reason}.`);
|
|
196
196
|
else warnings.push(`${where(f)} ${f.reason}; check that the date belongs to this CVE.`);
|
|
197
197
|
}
|
|
198
|
+
// CISA's KEV required action never names a restart. The template forms of
|
|
199
|
+
// that claim ("requires a service restart or system reboot per the KEV
|
|
200
|
+
// requiredAction") stop the batch; any other phrasing the detector matches is
|
|
201
|
+
// a warning, since prose can mention the two together without the claim.
|
|
202
|
+
for (const f of gaps.kevActionRestartFindings({ 'cve-catalog': entries, 'zeroday-lessons': lessons }, { includeDrafts: true })) {
|
|
203
|
+
const msg = `${where(f)} ${f.reason}; state the restart without that attribution, or drop it when the entry's patch_required_reboot does not support it.`;
|
|
204
|
+
if (f.definite) errors.push(msg);
|
|
205
|
+
else warnings.push(msg);
|
|
206
|
+
}
|
|
198
207
|
|
|
199
208
|
// Reported as a validation error so the DRY RUN surfaces it; insertEntries
|
|
200
209
|
// throws on the same condition, but only under --apply.
|
package/lib/gap-detectors.js
CHANGED
|
@@ -352,6 +352,157 @@ function kevListingDateFindings(loaded, opts = {}) {
|
|
|
352
352
|
return out;
|
|
353
353
|
}
|
|
354
354
|
|
|
355
|
+
// A restart or reboot credited to CISA's KEV required action. The required
|
|
356
|
+
// action says to apply mitigations per vendor instructions, follow BOD 22-01
|
|
357
|
+
// guidance or discontinue use; it never names a restart. Each pattern is a
|
|
358
|
+
// construction that attaches the attribution to the restart itself, so an
|
|
359
|
+
// accurate "apply mitigations per the KEV requiredAction" does not match. The
|
|
360
|
+
// restart and the attribution must sit in one clause: no comma, "then", "and",
|
|
361
|
+
// "before", "after" or "once" between them. A negation ("not", "no", "never",
|
|
362
|
+
// "nor", "without", "cannot", or a contraction such as "isn't") between them
|
|
363
|
+
// also stops the match.
|
|
364
|
+
const KEV_ACTION_RESTART_CREDIT = [
|
|
365
|
+
/\bfollows (?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action) \(service restart or system reboot\)/i,
|
|
366
|
+
/\b(?:restart|reboot)(?:(?!\b(?:then|and|before|after|once|not|no|never|nor|without|cannot)\b|\w*n['’]t\b)[^.;,()]){0,60}?\brequir(?:es|ed|ing) per (?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action)/i,
|
|
367
|
+
/(?:restart|reboot)(?:ing)?\b(?:(?!\b(?:then|and|before|after|once|not|no|never|nor|without|cannot)\b|\w*n['’]t\b)[^.;,()]){0,25}?,? per (?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action)/i,
|
|
368
|
+
/\b(?:fix|patch|update)(?: that)? per (?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action),? (?:typically |also )?requir(?:es|ing) (?:a )?(?:service )?(?:restart|reboot)/i,
|
|
369
|
+
/\bthe (?:service )?(?:restart|reboot)(?: or (?:system )?(?:restart|reboot))? (?:that )?(?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action) (?:calls for|implies|requires|mandates|specifies|names)\b/i,
|
|
370
|
+
/\b(?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action) (?:requires|required|calls for|mandates|specifies|names) (?:a |an )?(?:(?:service|system|host|device|appliance|server) )?(?:restart|reboot)/i,
|
|
371
|
+
/^[\s"'(]*per (?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action),? (?:(?:an?|the)\s+)?(?:(?:service|system|host|device|appliance|server)\s+)?(?:restart|reboot)\b(?!(?:(?!\b(?:if|when|unless|because|although|though|while|since|even)\b)[^,;.]){0,80}?(?:\b(?:not|never|cannot|no longer)\b|n['’]t\b))/i,
|
|
372
|
+
/\b(?:restart|reboot)(?:(?!\b(?:then|and|before|after|once|not|no|never|nor|without|cannot)\b|\w*n['’]t\b)[^.;,()]){0,40}?\b(?:required|mandated|called for|specified) by (?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action)/i,
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
// Text ending in a negator plus the noun phrase a match starts in: "No
|
|
376
|
+
// service ", "No service restart or system ", "without a ", "Neither a
|
|
377
|
+
// restart nor a ", "does not require a service restart or system ". Only that
|
|
378
|
+
// phrase is checked, so a negation elsewhere in the sentence ("No reboot is
|
|
379
|
+
// needed because the KEV requiredAction requires a service restart") does not
|
|
380
|
+
// hide a match.
|
|
381
|
+
const NEGATED_RESTART_BEFORE = /\b(?:no|not|never|nor|without|neither)\s+(?:(?:require|need|call\s+for|mandate|involve)\s+)?(?:(?:an?|any)\s+)?(?:(?:service|system|host|device|appliance|server)\s+)?(?:(?:restart|reboot)s?\s+(?:or|nor|and)\s+(?:(?:an?|any)\s+)?(?:(?:service|system|host|device|appliance|server)\s+)?)*$/i;
|
|
382
|
+
|
|
383
|
+
// The text is split at periods, semicolons and colons, so a labeled line
|
|
384
|
+
// ("Remediation: Per the KEV requiredAction, reboot ...") is read from the
|
|
385
|
+
// label on. Only a part that names a restart or reboot is checked, so "apply
|
|
386
|
+
// mitigations as required per the KEV requiredAction" does not match. A match
|
|
387
|
+
// whose restart noun phrase is negated is skipped, and the search continues
|
|
388
|
+
// past it.
|
|
389
|
+
function creditsRestartToKevAction(str) {
|
|
390
|
+
if (typeof str !== "string" || str.length === 0) return false;
|
|
391
|
+
return str.split(/(?<=[.;:])\s+/).some((sentence) => {
|
|
392
|
+
if (!/\b(?:restart|reboot)/i.test(sentence)) return false;
|
|
393
|
+
return KEV_ACTION_RESTART_CREDIT.some((re) => {
|
|
394
|
+
const anchored = re.source.startsWith("^");
|
|
395
|
+
for (let from = 0; from < sentence.length;) {
|
|
396
|
+
const m = re.exec(sentence.slice(from));
|
|
397
|
+
if (!m) return false;
|
|
398
|
+
const at = from + m.index;
|
|
399
|
+
if (!NEGATED_RESTART_BEFORE.test(sentence.slice(0, at))) return true;
|
|
400
|
+
if (anchored) return false;
|
|
401
|
+
from = at + 1;
|
|
402
|
+
}
|
|
403
|
+
return false;
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// The two forms in which the claim entered the catalogs: "requires a service
|
|
409
|
+
// restart or system reboot per the KEV requiredAction" and "follows the KEV
|
|
410
|
+
// requiredAction (service restart or system reboot)". A finding whose matching
|
|
411
|
+
// sentence takes one of these forms is marked definite.
|
|
412
|
+
const KEV_ACTION_RESTART_TEMPLATE = [
|
|
413
|
+
/(?<!\bnever\s)\brequires (?:a )?service restart or system reboot,? per (?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action)/i,
|
|
414
|
+
/\bfollows (?:the KEV requiredAction|the KEV required action|the required action in CISA(?:'s|’s) KEV entry|the required action in the KEV entry|CISA(?:'s|’s) KEV required action|CISA(?:'s|’s) KEV requiredAction|the KEV entry(?:'s|’s) required action) \(service restart or system reboot\)/i,
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
// A template match is definite only when its own clause, from the last comma,
|
|
418
|
+
// semicolon, colon, parenthesis, dash, "and", "but", "so", "while", "whereas",
|
|
419
|
+
// "because", "since", "although" or "though" before it, holds no negation
|
|
420
|
+
// ("Documentation does not say that the vendor patch requires ..."), and the
|
|
421
|
+
// rest of that clause does not deny the statement ("... is incorrect"), and the
|
|
422
|
+
// clause text in front of it is the claim's subject (TEMPLATE_SUBJECT). Every
|
|
423
|
+
// occurrence in the sentence is checked.
|
|
424
|
+
const TEMPLATE_CLAUSE_BREAK = /[,;:(—–]|\b(?:and|but|so|while|whereas|because|since|although|though)\b/gi;
|
|
425
|
+
const TEMPLATE_NEGATION_BEFORE = /\b(?:not|never|no|nor|neither|nothing|nowhere|none|cannot|without|false|untrue|incorrect|wrong|whether|if)\b|n['’]t\b/i;
|
|
426
|
+
const TEMPLATE_DENIAL_AFTER = /^(?:(?!\b(?:and|but|so|because|since|while|even|when|if|unless|where)\b)[^,;.()—–]){0,40}?\b(?:not|incorrect|wrong|false|untrue|mistaken|unsupported|unverified|disputed|unfounded|inaccurate|erroneous|misleading|a misreading|a myth)\b/i;
|
|
427
|
+
// A denial attached after a comma or dash: "..., which is incorrect", "... —
|
|
428
|
+
// a claim that is unsupported", including after a closing quote.
|
|
429
|
+
const TEMPLATE_DENIAL_APPOSITIVE = /^["'’”]?\s*[,—–]\s*(?:which|this|that|a\s+(?:claim|reading|statement|view|belief|misreading))\b[^.;]{0,40}?\b(?:not|incorrect|wrong|false|untrue|mistaken|unsupported|unverified|disputed|unfounded|inaccurate|erroneous|misleading|a misreading|a myth)\b/i;
|
|
430
|
+
// The clause text in front of a definite match: the claim's subject ("the
|
|
431
|
+
// vendor patch typically", "the fix", "applying the patch", "remediation",
|
|
432
|
+
// "it"), optionally behind a record frame ("live_patch_notes state that",
|
|
433
|
+
// "the entry states", "with the recorded note '"). "The claim that ...",
|
|
434
|
+
// "Documentation does not say that ..." and "Saying ..." are not subjects.
|
|
435
|
+
const TEMPLATE_SUBJECT = /^(?:with\s+)?(?:(?:the|a)\s+)?(?:(?:recorded\s+)?note|live[_-]patch[_ ]notes?|entry)?\s*(?:states?|stating|records?|recording|reads?)?\s*(?:that\s+)?["'‘“]?\s*(?:applying\s+)?(?:(?:(?:the|a|an|its)\s+)?(?:vendor\s+|fixed\s+|security\s+)?(?:patch|fix|update|release|build|firmware|remediation|it)(?:\s+that)?)?\s*(?:(?:typically|also|usually|generally|often)\s*)?$/i;
|
|
436
|
+
|
|
437
|
+
// A label in front of a colon that holds a directive or a verb of saying ("The
|
|
438
|
+
// control must not state: the vendor patch requires ...", "Do not write:")
|
|
439
|
+
// keeps the clause after it from being definite. A plain label ("Remediation:")
|
|
440
|
+
// or an explanatory one ("there is no live-patch tool:") does not.
|
|
441
|
+
const TEMPLATE_LABEL_DIRECTIVE = /\b(?:must|should|shall|avoid|state|states|stating|say|says|saying|claim|claims|write|writes|assert|asserts|imply|implies|suggest|suggests|describe|describes|false|incorrect|wrong|myth)\b/i;
|
|
442
|
+
// A verb of saying or believing anywhere earlier in the sentence ("Do not
|
|
443
|
+
// claim, without evidence, the vendor patch requires ...") keeps a template
|
|
444
|
+
// match from being definite, whatever commas lie in between. Negation alone
|
|
445
|
+
// ("Live patches do not exist, and the vendor patch requires ..."), the
|
|
446
|
+
// record verbs the catalog frames use ("states", "records") and field values
|
|
447
|
+
// ("live_patch_available is false") do not.
|
|
448
|
+
const TEMPLATE_PREFIX_DIRECTIVE = /\b(?:avoid|say|says|saying|said|claim|claims|claiming|write|writes|written|assert|asserts|assertion|assertions|imply|implies|suggest|suggests|describe|describes|assume|assumes|assumed|believe|believes|allege|alleges|alleged|allegation|allegations)\b/i;
|
|
449
|
+
|
|
450
|
+
// Sentences end at a period or semicolon only. A line break is read as a space,
|
|
451
|
+
// so wrapped prose keeps its framing ("Documentation does not say that\nthe
|
|
452
|
+
// vendor patch requires ...").
|
|
453
|
+
function isTemplateRestartCredit(str) {
|
|
454
|
+
if (typeof str !== "string" || str.length === 0) return false;
|
|
455
|
+
return str.split(/(?<=[.;])\s+/).some((sentence) => {
|
|
456
|
+
if (!creditsRestartToKevAction(sentence)) return false;
|
|
457
|
+
return KEV_ACTION_RESTART_TEMPLATE.some((re) => {
|
|
458
|
+
for (let from = 0; from < sentence.length;) {
|
|
459
|
+
const m = re.exec(sentence.slice(from));
|
|
460
|
+
if (!m) return false;
|
|
461
|
+
const at = from + m.index;
|
|
462
|
+
let start = 0;
|
|
463
|
+
let prev = 0;
|
|
464
|
+
let colon = false;
|
|
465
|
+
for (const b of sentence.slice(0, at).matchAll(TEMPLATE_CLAUSE_BREAK)) {
|
|
466
|
+
prev = start;
|
|
467
|
+
start = b.index + b[0].length;
|
|
468
|
+
colon = b[0] === ":";
|
|
469
|
+
}
|
|
470
|
+
const subject = sentence.slice(start, at).trim();
|
|
471
|
+
const label = colon ? sentence.slice(prev, start - 1) : "";
|
|
472
|
+
if (TEMPLATE_SUBJECT.test(subject) && !TEMPLATE_NEGATION_BEFORE.test(subject) &&
|
|
473
|
+
!TEMPLATE_LABEL_DIRECTIVE.test(label) && !TEMPLATE_PREFIX_DIRECTIVE.test(sentence.slice(0, at)) &&
|
|
474
|
+
!TEMPLATE_DENIAL_AFTER.test(sentence.slice(at + m[0].length)) &&
|
|
475
|
+
!TEMPLATE_DENIAL_APPOSITIVE.test(sentence.slice(at + m[0].length))) return true;
|
|
476
|
+
from = at + 1;
|
|
477
|
+
}
|
|
478
|
+
return false;
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// One finding per catalog or lesson text that credits a restart or reboot to
|
|
484
|
+
// the KEV required action, marked definite when it takes a template form.
|
|
485
|
+
// Drafts are skipped unless opts.includeDrafts is set.
|
|
486
|
+
function kevActionRestartFindings(loaded, opts = {}) {
|
|
487
|
+
const out = [];
|
|
488
|
+
for (const catalog of ["cve-catalog", "zeroday-lessons"]) {
|
|
489
|
+
const cat = loaded[catalog];
|
|
490
|
+
if (!cat) continue;
|
|
491
|
+
for (const id of Object.keys(cat)) {
|
|
492
|
+
if (id === "_meta") continue;
|
|
493
|
+
const e = cat[id];
|
|
494
|
+
if (!e || (e._auto_imported === true && !opts.includeDrafts)) continue;
|
|
495
|
+
for (const [field, s] of _strings(e, "", [])) {
|
|
496
|
+
if (!creditsRestartToKevAction(s)) continue;
|
|
497
|
+
out.push({ class: "logical-consistency", catalog, id, field,
|
|
498
|
+
rule: "restart_not_credited_to_kev_required_action", definite: isTemplateRestartCredit(s),
|
|
499
|
+
reason: "text credits a restart or reboot to CISA's KEV required action, which names none" });
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
return out;
|
|
504
|
+
}
|
|
505
|
+
|
|
355
506
|
// Fields present but weak; what counts as weak is per catalog and field.
|
|
356
507
|
function contentQualityFindings(loaded) {
|
|
357
508
|
const out = [];
|
|
@@ -494,6 +645,7 @@ function logicalConsistencyFindings(loaded) {
|
|
|
494
645
|
}
|
|
495
646
|
}
|
|
496
647
|
out.push(...kevListingDateFindings(loaded));
|
|
648
|
+
out.push(...kevActionRestartFindings(loaded));
|
|
497
649
|
return out;
|
|
498
650
|
}
|
|
499
651
|
|
|
@@ -758,6 +910,11 @@ module.exports = {
|
|
|
758
910
|
PACKET_FIELD_TOKEN,
|
|
759
911
|
CATALOG_FIELD_NAMES,
|
|
760
912
|
kevListingDateFindings,
|
|
913
|
+
kevActionRestartFindings,
|
|
914
|
+
creditsRestartToKevAction,
|
|
915
|
+
KEV_ACTION_RESTART_CREDIT,
|
|
916
|
+
KEV_ACTION_RESTART_TEMPLATE,
|
|
917
|
+
isTemplateRestartCredit,
|
|
761
918
|
KEV_LISTING_DATE,
|
|
762
919
|
KEV_LISTING_DATE_FIRST,
|
|
763
920
|
daysSince,
|