@blamejs/exceptd-skills 0.21.9 → 0.21.10

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/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.
@@ -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,