@blamejs/exceptd-skills 0.15.45 → 0.15.47
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 +20 -0
- package/CONTEXT.md +1 -1
- package/bin/exceptd.js +85 -136
- package/data/_indexes/_meta.json +2 -2
- package/lib/flag-suggest.js +7 -11
- package/lib/lint-skills.js +18 -19
- package/lib/playbook-runner.js +11 -3
- package/lib/validate-catalog-meta.js +6 -5
- package/lib/validate-cve-catalog.js +8 -8
- package/lib/validate-playbooks.js +14 -14
- package/manifest.json +44 -44
- package/orchestrator/index.js +8 -4
- package/orchestrator/scanner.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +34 -34
- package/scripts/check-test-count.js +11 -4
package/lib/lint-skills.js
CHANGED
|
@@ -91,14 +91,13 @@ const REQUIRED_SECTIONS = [
|
|
|
91
91
|
|
|
92
92
|
// L3 — Defensive Countermeasure Mapping became a required section for skills
|
|
93
93
|
// reviewed on or after this cutoff (documented in AGENTS.md). Pre-cutoff
|
|
94
|
-
// skills remain exempt to preserve patch-class compatibility
|
|
95
|
-
// broaden the cutoff.
|
|
94
|
+
// skills remain exempt to preserve patch-class compatibility.
|
|
96
95
|
const COUNTERMEASURE_SECTION = 'Defensive Countermeasure Mapping';
|
|
97
96
|
const COUNTERMEASURE_CUTOFF = '2026-05-11';
|
|
98
97
|
|
|
99
98
|
// L1 — Minimum number of words of body text between a section heading and the
|
|
100
99
|
// next heading (or EOF) for the section to count as populated. Header-only
|
|
101
|
-
// sections surface as
|
|
100
|
+
// sections surface as warnings; promoted to failures under --strict.
|
|
102
101
|
const MIN_SECTION_BODY_WORDS = 20;
|
|
103
102
|
|
|
104
103
|
const PLACEHOLDER_PATTERNS = [
|
|
@@ -396,8 +395,8 @@ function validateFrontmatter(fm, skillName) {
|
|
|
396
395
|
* in the body (case-insensitive). Hard failure.
|
|
397
396
|
* - headerOnly[] — sections whose heading exists but whose body between
|
|
398
397
|
* that heading and the next heading is shorter than
|
|
399
|
-
* MIN_SECTION_BODY_WORDS words.
|
|
400
|
-
*
|
|
398
|
+
* MIN_SECTION_BODY_WORDS words. A warning by default;
|
|
399
|
+
* promoted to an error under --strict. */
|
|
401
400
|
function findMissingSections(body, requiredSections) {
|
|
402
401
|
const sections = requiredSections || REQUIRED_SECTIONS;
|
|
403
402
|
const lines = body.split(/\r?\n/);
|
|
@@ -563,16 +562,16 @@ function lintSkill(entry, ctx) {
|
|
|
563
562
|
}
|
|
564
563
|
}
|
|
565
564
|
|
|
566
|
-
// L2 — attack_refs cross-catalog resolution. Surface as
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
//
|
|
570
|
-
//
|
|
565
|
+
// L2 — attack_refs cross-catalog resolution. Surface as warnings by
|
|
566
|
+
// default (preserving patch-class compatibility); promoted to hard
|
|
567
|
+
// failures under --strict (the predeploy gate). If
|
|
568
|
+
// data/attack-techniques.json is missing entirely the ctx.attackKeys set
|
|
569
|
+
// is null — skip the check (the gate degrades gracefully).
|
|
571
570
|
if (Array.isArray(fm.attack_refs) && ctx.attackKeys) {
|
|
572
571
|
for (const ref of fm.attack_refs) {
|
|
573
572
|
if (!ctx.attackKeys.has(ref)) {
|
|
574
573
|
skillWarnings.push(
|
|
575
|
-
`attack_refs: "${ref}" not present in data/attack-techniques.json (
|
|
574
|
+
`attack_refs: "${ref}" not present in data/attack-techniques.json (an error under --strict)`,
|
|
576
575
|
);
|
|
577
576
|
}
|
|
578
577
|
}
|
|
@@ -617,18 +616,18 @@ function lintSkill(entry, ctx) {
|
|
|
617
616
|
|
|
618
617
|
// L3 — Defensive Countermeasure Mapping is required for skills reviewed
|
|
619
618
|
// on or after COUNTERMEASURE_CUTOFF. Pre-cutoff skills are exempt. The
|
|
620
|
-
// section's absence on a post-cutoff skill is a
|
|
621
|
-
// existing skills can add the section gradually;
|
|
622
|
-
//
|
|
619
|
+
// section's absence on a post-cutoff skill is a warning by default so
|
|
620
|
+
// existing skills can add the section gradually; promoted to a hard
|
|
621
|
+
// failure under --strict.
|
|
623
622
|
const { missing, headerOnly } = findMissingSections(body, REQUIRED_SECTIONS);
|
|
624
623
|
for (const s of missing) {
|
|
625
624
|
skillErrors.push(`body: missing required section "${s}"`);
|
|
626
625
|
}
|
|
627
626
|
for (const ho of headerOnly) {
|
|
628
|
-
// L1 — Header-only sections are
|
|
629
|
-
//
|
|
627
|
+
// L1 — Header-only sections are warnings by default; promoted to a
|
|
628
|
+
// failure under --strict.
|
|
630
629
|
skillWarnings.push(
|
|
631
|
-
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS});
|
|
630
|
+
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
|
|
632
631
|
);
|
|
633
632
|
}
|
|
634
633
|
if (
|
|
@@ -639,12 +638,12 @@ function lintSkill(entry, ctx) {
|
|
|
639
638
|
const cmResult = findMissingSections(body, [COUNTERMEASURE_SECTION]);
|
|
640
639
|
if (cmResult.missing.length > 0) {
|
|
641
640
|
skillWarnings.push(
|
|
642
|
-
`body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF};
|
|
641
|
+
`body: missing required section "${COUNTERMEASURE_SECTION}" (required for skills with last_threat_review >= ${COUNTERMEASURE_CUTOFF}; an error under --strict)`,
|
|
643
642
|
);
|
|
644
643
|
} else {
|
|
645
644
|
for (const ho of cmResult.headerOnly) {
|
|
646
645
|
skillWarnings.push(
|
|
647
|
-
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS});
|
|
646
|
+
`body: section "${ho.section}" has only ${ho.wordCount} words of body text (need >= ${MIN_SECTION_BODY_WORDS}); an error under --strict`,
|
|
648
647
|
);
|
|
649
648
|
}
|
|
650
649
|
}
|
package/lib/playbook-runner.js
CHANGED
|
@@ -74,7 +74,7 @@ try {
|
|
|
74
74
|
|
|
75
75
|
// Probe the catalog (parse it, surface any load error) LAZILY on first need
|
|
76
76
|
// rather than at module load. The probe parses the ~2.6MB CVE catalog (~8.5ms);
|
|
77
|
-
// doing it eagerly charged that to every cheap verb (brief/
|
|
77
|
+
// doing it eagerly charged that to every cheap verb (brief/ask/lint/
|
|
78
78
|
// discover) that never analyzes. run() calls this before the analyze path, so
|
|
79
79
|
// a corrupt catalog still surfaces as blocked_by:'catalog_corrupt' before
|
|
80
80
|
// analyze — just not on verbs that don't touch the catalog. Memoized: probes
|
|
@@ -1829,7 +1829,11 @@ function close(playbookId, directiveId, analyzeResult, validateResult, agentSign
|
|
|
1829
1829
|
// Severity ladder for active_exploitation. The worst-of reduction lets
|
|
1830
1830
|
// analyzeFindingShape report the most-exploited CVE in the matched set, not
|
|
1831
1831
|
// the first-encountered one. Higher index = worse.
|
|
1832
|
-
|
|
1832
|
+
// `theoretical` (PoC exists, no in-the-wild use) must rank between `none` and
|
|
1833
|
+
// `unknown`; omitting it made `?? -1` lose to the -1 start, so an all-theoretical
|
|
1834
|
+
// matched set wrongly reduced to 'unknown' and a theoretical+none set dropped
|
|
1835
|
+
// the theoretical entry entirely. This vocabulary is first-class in scoring.js.
|
|
1836
|
+
const ACTIVE_EXPLOITATION_RANK = { none: 0, theoretical: 1, unknown: 2, suspected: 3, confirmed: 4 };
|
|
1833
1837
|
|
|
1834
1838
|
function worstActiveExploitation(matchedCves) {
|
|
1835
1839
|
let worst = null;
|
|
@@ -1840,7 +1844,9 @@ function worstActiveExploitation(matchedCves) {
|
|
|
1840
1844
|
const rank = ACTIVE_EXPLOITATION_RANK[v] ?? -1;
|
|
1841
1845
|
if (rank > worstRank) { worst = v; worstRank = rank; }
|
|
1842
1846
|
}
|
|
1843
|
-
|
|
1847
|
+
// Empty / all-unrecognized matched set → 'none' (a draft must not assert
|
|
1848
|
+
// 'unknown' exploitation it never observed).
|
|
1849
|
+
return worst || 'none';
|
|
1844
1850
|
}
|
|
1845
1851
|
|
|
1846
1852
|
// Severity ladder derived from rwep_adjusted. Playbooks reference
|
|
@@ -3838,4 +3844,6 @@ module.exports = {
|
|
|
3838
3844
|
_acquireLockDiagnostic: acquireLockDiagnostic,
|
|
3839
3845
|
_releaseLock: releaseLock,
|
|
3840
3846
|
_lockFilePath: lockFilePath,
|
|
3847
|
+
_vulnIdToUrn: vulnIdToUrn,
|
|
3848
|
+
_worstActiveExploitation: worstActiveExploitation,
|
|
3841
3849
|
};
|
|
@@ -62,7 +62,7 @@ function parseArgs(argv) {
|
|
|
62
62
|
'Usage: node lib/validate-catalog-meta.js [--quiet] [--strict]\n' +
|
|
63
63
|
'\n' +
|
|
64
64
|
' --quiet Suppress per-catalog PASS output; show failures only.\n' +
|
|
65
|
-
' --strict Promote
|
|
65
|
+
' --strict Promote freshness warnings to errors (used by the predeploy gate).\n',
|
|
66
66
|
);
|
|
67
67
|
process.exit(0);
|
|
68
68
|
} else {
|
|
@@ -165,11 +165,12 @@ function validateMeta(catalogPath, opts) {
|
|
|
165
165
|
|
|
166
166
|
/* freshness enforcement. When both meta.last_updated and
|
|
167
167
|
* freshness_policy.stale_after_days are present, surface a warning if
|
|
168
|
-
* (now - last_updated) > stale_after_days.
|
|
169
|
-
*
|
|
168
|
+
* (now - last_updated) > stale_after_days. Emitted at WARN level by
|
|
169
|
+
* default (does not fail validation).
|
|
170
170
|
*
|
|
171
171
|
* Optional `opts.strict` (or `opts.errorOnStale`) promotes the warning
|
|
172
|
-
* to an error
|
|
172
|
+
* to an error; the predeploy gate runs --strict, plain validation keeps
|
|
173
|
+
* the warning posture.
|
|
173
174
|
*/
|
|
174
175
|
if (
|
|
175
176
|
typeof meta.last_updated === 'string' &&
|
|
@@ -185,7 +186,7 @@ function validateMeta(catalogPath, opts) {
|
|
|
185
186
|
const msg =
|
|
186
187
|
`_meta freshness: last_updated ${meta.last_updated} is ${ageDays} days old ` +
|
|
187
188
|
`(stale_after_days = ${fp.stale_after_days}); refresh the catalog or bump _meta.last_updated. ` +
|
|
188
|
-
`
|
|
189
|
+
`Promoted to an error under --strict.`;
|
|
189
190
|
if (opts && (opts.strict || opts.errorOnStale)) {
|
|
190
191
|
errors.push(msg);
|
|
191
192
|
} else {
|
|
@@ -40,8 +40,8 @@ const FRAMEWORK_GAPS_PATH = path.join(REPO_ROOT, 'data', 'framework-control-gaps
|
|
|
40
40
|
// v0.12.12 — patterns that mark a verification_sources URL as a public exploit
|
|
41
41
|
// or PoC location. When poc_available: true AND a verification source matches
|
|
42
42
|
// one of these, the entry must carry an `iocs` block per AGENTS.md Hard Rule
|
|
43
|
-
// #14. Surfaced as
|
|
44
|
-
//
|
|
43
|
+
// #14. Surfaced as a warning by default so drafts and pre-IoC entries don't
|
|
44
|
+
// break patch-class compatibility; promoted to an error under --strict.
|
|
45
45
|
const PUBLIC_EXPLOIT_URL_PATTERNS = [
|
|
46
46
|
/github\.com\/.+\/(exploits?|poc|pocs)\b/i,
|
|
47
47
|
/\bexploit-?db\.com\b/i,
|
|
@@ -53,8 +53,8 @@ const PUBLIC_EXPLOIT_URL_PATTERNS = [
|
|
|
53
53
|
|
|
54
54
|
// v0.12.12 — Tightened CVSS-vector prefix. Schema's existing pattern accepts
|
|
55
55
|
// any "CVSS:<digits>/"; the strict pattern below admits only known CVSS
|
|
56
|
-
// versions (2.0 / 3.0 / 3.1 / 4.0). Emitted as
|
|
57
|
-
//
|
|
56
|
+
// versions (2.0 / 3.0 / 3.1 / 4.0). Emitted as a warning by default;
|
|
57
|
+
// promoted to an error under --strict.
|
|
58
58
|
const STRICT_CVSS_PATTERN = /^CVSS:(2\.0|3\.[01]|4\.0)\//;
|
|
59
59
|
|
|
60
60
|
// v0.12.12 — Impossible-date guard. Reject obviously bogus year ranges
|
|
@@ -80,7 +80,7 @@ function parseArgs(argv) {
|
|
|
80
80
|
'Usage: node lib/validate-cve-catalog.js [--quiet] [--strict]\n' +
|
|
81
81
|
'\n' +
|
|
82
82
|
' --quiet Suppress per-CVE PASS output; show failures only.\n' +
|
|
83
|
-
' --strict Promote
|
|
83
|
+
' --strict Promote advisory warnings to errors (used by the predeploy gate). Off by default.\n',
|
|
84
84
|
);
|
|
85
85
|
process.exit(0);
|
|
86
86
|
} else {
|
|
@@ -244,7 +244,7 @@ function additionalChecks(key, entry, ctx) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
// V2 — Cross-catalog reference resolution. Unresolved refs are warnings
|
|
247
|
-
//
|
|
247
|
+
// by default; promoted to hard failures under --strict. V2 expansion
|
|
248
248
|
// extends the walk from cwe_refs only to attack_refs, atlas_refs,
|
|
249
249
|
// d3fend_refs, AND framework_control_gaps.
|
|
250
250
|
const REF_FIELDS = [
|
|
@@ -266,7 +266,7 @@ function additionalChecks(key, entry, ctx) {
|
|
|
266
266
|
if (typeof ref !== 'string') continue;
|
|
267
267
|
if (!set.has(ref)) {
|
|
268
268
|
warnings.push(
|
|
269
|
-
`${key}: ${field} entry "${ref}" not in ${file} (
|
|
269
|
+
`${key}: ${field} entry "${ref}" not in ${file} (an error under --strict)`,
|
|
270
270
|
);
|
|
271
271
|
}
|
|
272
272
|
}
|
|
@@ -302,7 +302,7 @@ function additionalChecks(key, entry, ctx) {
|
|
|
302
302
|
if (typeof entry.cvss_vector === 'string' && entry.cvss_vector.length > 0) {
|
|
303
303
|
if (!STRICT_CVSS_PATTERN.test(entry.cvss_vector)) {
|
|
304
304
|
warnings.push(
|
|
305
|
-
`${key}: cvss_vector ${JSON.stringify(entry.cvss_vector)} does not match the strict prefix /^CVSS:(2.0|3.0|3.1|4.0)\\//.
|
|
305
|
+
`${key}: cvss_vector ${JSON.stringify(entry.cvss_vector)} does not match the strict prefix /^CVSS:(2.0|3.0|3.1|4.0)\\//. A warning by default; promoted to an error under --strict.`,
|
|
306
306
|
);
|
|
307
307
|
}
|
|
308
308
|
}
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
* jurisdiction_obligations an explicit `id` field; the shipped playbooks
|
|
43
43
|
* reference them by this composite string).
|
|
44
44
|
* - _meta.mutex is symmetric across the whole playbook set: if A lists B,
|
|
45
|
-
* B must list A. Asymmetry surfaces as a warning
|
|
46
|
-
*
|
|
45
|
+
* B must list A. Asymmetry surfaces as a warning by default (promoted to
|
|
46
|
+
* an error under --strict) — see checkMutexReciprocity().
|
|
47
47
|
*
|
|
48
48
|
* Finding severity:
|
|
49
49
|
* - error — structural problems that block the runner (missing required
|
|
@@ -51,9 +51,9 @@
|
|
|
51
51
|
* duplicate indicator id).
|
|
52
52
|
* - warning — schema-shape drift the runner can still tolerate (enum
|
|
53
53
|
* vocabulary lag, cross-catalog refs introduced after the
|
|
54
|
-
* playbook last shipped).
|
|
55
|
-
*
|
|
56
|
-
*
|
|
54
|
+
* playbook last shipped). Surfaced to the operator without
|
|
55
|
+
* failing the gate by default; promoted to hard errors under
|
|
56
|
+
* --strict (predeploy `informational: false`).
|
|
57
57
|
*
|
|
58
58
|
* Exit code: 0 if no errors (warnings allowed), 1 if any errors, 2 on
|
|
59
59
|
* argv error.
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
* Usage:
|
|
62
62
|
* node lib/validate-playbooks.js validate every playbook
|
|
63
63
|
* node lib/validate-playbooks.js --quiet only print FAIL playbooks + summary
|
|
64
|
-
* node lib/validate-playbooks.js --strict treat warnings as errors (
|
|
65
|
-
*
|
|
64
|
+
* node lib/validate-playbooks.js --strict treat warnings as errors (used by
|
|
65
|
+
* the predeploy gate).
|
|
66
66
|
*/
|
|
67
67
|
|
|
68
68
|
'use strict';
|
|
@@ -92,7 +92,7 @@ function parseArgs(argv) {
|
|
|
92
92
|
'Usage: node lib/validate-playbooks.js [--quiet] [--strict]\n' +
|
|
93
93
|
'\n' +
|
|
94
94
|
' --quiet Suppress per-playbook PASS output; show failures only.\n' +
|
|
95
|
-
' --strict Treat warnings as errors (
|
|
95
|
+
' --strict Treat warnings as errors (used by the predeploy gate).\n',
|
|
96
96
|
);
|
|
97
97
|
process.exit(0);
|
|
98
98
|
} else {
|
|
@@ -129,8 +129,8 @@ function typeMatches(value, expected) {
|
|
|
129
129
|
* shaped as { severity, message }. Severity defaults to 'error'; enum
|
|
130
130
|
* mismatches and unknown additional properties under
|
|
131
131
|
* additionalProperties:false are downgraded to 'warning' so vocabulary drift
|
|
132
|
-
* between the schema and shipped playbooks does not hard-fail
|
|
133
|
-
*
|
|
132
|
+
* between the schema and shipped playbooks does not hard-fail by default.
|
|
133
|
+
* Promoted to errors under --strict / predeploy informational:false. */
|
|
134
134
|
function validate(value, schema, schemaName, pathStr) {
|
|
135
135
|
const findings = [];
|
|
136
136
|
const here = pathStr || schemaName;
|
|
@@ -222,7 +222,7 @@ function validate(value, schema, schemaName, pathStr) {
|
|
|
222
222
|
findings.push(...validate(v, addlSchema, schemaName, `${here}.${k}`));
|
|
223
223
|
} else if (!allowAdditional) {
|
|
224
224
|
// Drift between schema and shipped data: surface as warning, not
|
|
225
|
-
// an error.
|
|
225
|
+
// an error. Promoted to an error under --strict.
|
|
226
226
|
err(`${here}: unexpected property "${k}"`, 'warning');
|
|
227
227
|
}
|
|
228
228
|
}
|
|
@@ -528,8 +528,8 @@ function checkCrossRefs(playbook, ctx, playbookIds) {
|
|
|
528
528
|
* undeclared side is started first.
|
|
529
529
|
*
|
|
530
530
|
* Emits one warning per asymmetric pair (keyed off the side that declares
|
|
531
|
-
* the edge).
|
|
532
|
-
* cadence;
|
|
531
|
+
* the edge). Kept at warning severity by default per the patch-class
|
|
532
|
+
* cadence; promoted to an error under --strict / predeploy
|
|
533
533
|
* `informational: false`.
|
|
534
534
|
*/
|
|
535
535
|
function checkMutexReciprocity(playbooks) {
|
|
@@ -547,7 +547,7 @@ function checkMutexReciprocity(playbooks) {
|
|
|
547
547
|
const otherSet = mutexMap.get(other);
|
|
548
548
|
if (!otherSet) continue; // unresolved-id warning is already emitted by checkCrossRefs
|
|
549
549
|
if (!otherSet.has(id)) {
|
|
550
|
-
const msg = `_meta.mutex: asymmetric mutex with "${other}" — "${other}" does not list "${id}" in its _meta.mutex.
|
|
550
|
+
const msg = `_meta.mutex: asymmetric mutex with "${other}" — "${other}" does not list "${id}" in its _meta.mutex. Promoted to a hard error under --strict.`;
|
|
551
551
|
if (!byPlaybook.has(id)) byPlaybook.set(id, []);
|
|
552
552
|
byPlaybook.get(id).push(msg);
|
|
553
553
|
}
|