@blamejs/exceptd-skills 0.12.13 → 0.12.16
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 +217 -0
- package/bin/exceptd.js +522 -27
- package/data/_indexes/_meta.json +45 -45
- package/data/_indexes/activity-feed.json +4 -4
- package/data/_indexes/catalog-summaries.json +29 -29
- package/data/_indexes/chains.json +3238 -3210
- package/data/_indexes/frequency.json +3 -0
- package/data/_indexes/jurisdiction-map.json +5 -3
- package/data/_indexes/section-offsets.json +712 -685
- package/data/_indexes/theater-fingerprints.json +1 -1
- package/data/_indexes/token-budget.json +355 -340
- package/data/atlas-ttps.json +144 -129
- package/data/attack-techniques.json +319 -76
- package/data/cve-catalog.json +516 -476
- package/data/cwe-catalog.json +1081 -759
- package/data/exploit-availability.json +63 -15
- package/data/framework-control-gaps.json +867 -843
- package/data/playbooks/ai-api.json +3 -1
- package/data/playbooks/containers.json +11 -3
- package/data/playbooks/cred-stores.json +3 -1
- package/data/playbooks/crypto-codebase.json +11 -11
- package/data/playbooks/crypto.json +1 -1
- package/data/playbooks/hardening.json +3 -1
- package/data/playbooks/kernel.json +3 -1
- package/data/playbooks/library-author.json +21 -10
- package/data/playbooks/mcp.json +1 -1
- package/data/playbooks/runtime.json +3 -1
- package/data/playbooks/sbom.json +2 -2
- package/data/playbooks/secrets.json +3 -1
- package/data/rfc-references.json +276 -276
- package/keys/EXPECTED_FINGERPRINT +1 -0
- package/lib/auto-discovery.js +57 -35
- package/lib/cross-ref-api.js +39 -6
- package/lib/cve-curation.js +33 -14
- package/lib/lint-skills.js +6 -1
- package/lib/playbook-runner.js +742 -78
- package/lib/prefetch.js +30 -8
- package/lib/refresh-external.js +40 -22
- package/lib/refresh-network.js +233 -17
- package/lib/scoring.js +191 -18
- package/lib/source-ghsa.js +219 -37
- package/lib/source-osv.js +381 -122
- package/lib/validate-catalog-meta.js +64 -9
- package/lib/validate-cve-catalog.js +56 -18
- package/lib/validate-indexes.js +88 -37
- package/lib/validate-playbooks.js +46 -0
- package/lib/verify.js +72 -0
- package/manifest-snapshot.json +1 -1
- package/manifest-snapshot.sha256 +1 -0
- package/manifest.json +73 -73
- package/orchestrator/dispatcher.js +21 -1
- package/orchestrator/event-bus.js +52 -8
- package/orchestrator/index.js +279 -20
- package/orchestrator/pipeline.js +63 -2
- package/orchestrator/scanner.js +32 -10
- package/orchestrator/scheduler.js +150 -17
- package/package.json +3 -1
- package/sbom.cdx.json +7 -7
- package/scripts/check-manifest-snapshot.js +32 -0
- package/scripts/check-sbom-currency.js +65 -3
- package/scripts/check-test-coverage.js +142 -19
- package/scripts/predeploy.js +83 -39
- package/scripts/refresh-manifest-snapshot.js +55 -4
- package/scripts/validate-vendor-online.js +169 -0
- package/scripts/verify-shipped-tarball.js +141 -9
- package/skills/ai-attack-surface/skill.md +18 -10
- package/skills/ai-c2-detection/skill.md +7 -2
- package/skills/ai-risk-management/skill.md +5 -4
- package/skills/api-security/skill.md +3 -3
- package/skills/attack-surface-pentest/skill.md +5 -5
- package/skills/cloud-security/skill.md +1 -1
- package/skills/compliance-theater/skill.md +8 -8
- package/skills/container-runtime-security/skill.md +1 -1
- package/skills/dlp-gap-analysis/skill.md +5 -1
- package/skills/email-security-anti-phishing/skill.md +1 -1
- package/skills/exploit-scoring/skill.md +18 -18
- package/skills/framework-gap-analysis/skill.md +6 -6
- package/skills/global-grc/skill.md +3 -2
- package/skills/identity-assurance/skill.md +2 -2
- package/skills/incident-response-playbook/skill.md +4 -4
- package/skills/kernel-lpe-triage/skill.md +21 -2
- package/skills/mcp-agent-trust/skill.md +17 -10
- package/skills/mlops-security/skill.md +2 -1
- package/skills/ot-ics-security/skill.md +1 -1
- package/skills/policy-exception-gen/skill.md +3 -3
- package/skills/pqc-first/skill.md +1 -1
- package/skills/rag-pipeline-security/skill.md +7 -3
- package/skills/researcher/skill.md +20 -3
- package/skills/sector-energy/skill.md +1 -1
- package/skills/sector-federal-government/skill.md +1 -1
- package/skills/sector-financial/skill.md +3 -3
- package/skills/sector-healthcare/skill.md +2 -2
- package/skills/security-maturity-tiers/skill.md +7 -7
- package/skills/skill-update-loop/skill.md +19 -3
- package/skills/supply-chain-integrity/skill.md +1 -1
- package/skills/threat-model-currency/skill.md +11 -11
- package/skills/threat-modeling-methodology/skill.md +3 -3
- package/skills/webapp-security/skill.md +1 -1
- package/skills/zeroday-gap-learn/skill.md +51 -7
- package/vendor/blamejs/_PROVENANCE.json +4 -1
- package/vendor/blamejs/worker-pool.js +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
SHA256:JX04VjFprM7+3gHJdO0Wi4tTCf1RKI9Roza3XOzAe0Y=
|
package/lib/auto-discovery.js
CHANGED
|
@@ -31,6 +31,31 @@ const fs = require("fs");
|
|
|
31
31
|
const path = require("path");
|
|
32
32
|
const { scoreCustom } = require("./scoring");
|
|
33
33
|
|
|
34
|
+
// audit M P1-C: stored rwep_factors must reproduce the stored rwep_score.
|
|
35
|
+
// `buildScoringInputs` is the single source of truth for both — it captures
|
|
36
|
+
// the conservative defaults applied to a freshly-imported KEV draft (CISA
|
|
37
|
+
// only lists vulnerabilities with documented exploitation, so we assume a
|
|
38
|
+
// public PoC exists; reboot defaults to true because most KEV-listed CVEs
|
|
39
|
+
// land in the kernel / hypervisor / vendor firmware where reboot is the
|
|
40
|
+
// norm). The same input object is then handed to scoreCustom for the score
|
|
41
|
+
// AND mapped into the `rwep_factors` shape stored on the draft. Calling
|
|
42
|
+
// scoring.validate() on the post-import catalog will no longer flag every
|
|
43
|
+
// auto-imported draft for divergence > 5.
|
|
44
|
+
function buildScoringInputs(kevEntry /*, nvdPayload */) {
|
|
45
|
+
void kevEntry;
|
|
46
|
+
return {
|
|
47
|
+
cisa_kev: true,
|
|
48
|
+
poc_available: true,
|
|
49
|
+
ai_assisted_weapon: false,
|
|
50
|
+
ai_discovered: false,
|
|
51
|
+
active_exploitation: "suspected",
|
|
52
|
+
blast_radius: 15,
|
|
53
|
+
patch_available: false,
|
|
54
|
+
live_patch_available: false,
|
|
55
|
+
reboot_required: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
const TODAY = new Date().toISOString().slice(0, 10);
|
|
35
60
|
const TIMEOUT_MS = 10_000;
|
|
36
61
|
const USER_AGENT = "exceptd-security/auto-discovery (+https://exceptd.com)";
|
|
@@ -110,37 +135,17 @@ function buildKevDraftEntry(kevEntry, nvdPayload, epssPayload) {
|
|
|
110
135
|
const knownRansomware =
|
|
111
136
|
String(kevEntry.knownRansomwareCampaignUse || "").toLowerCase() === "known";
|
|
112
137
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
patch_available: null,
|
|
125
|
-
live_patch_available: null,
|
|
126
|
-
reboot_required: null,
|
|
127
|
-
};
|
|
128
|
-
// scoreCustom() treats null fields as false, which under-counts the
|
|
129
|
-
// score. Pass concrete defaults for unknowns: poc_available=true is
|
|
130
|
-
// the conservative assumption for KEV entries (CISA generally only
|
|
131
|
-
// adds entries with documented exploitation), and reboot_required=
|
|
132
|
-
// true biases toward urgency.
|
|
133
|
-
const rwep_score = scoreCustom({
|
|
134
|
-
cisa_kev: true,
|
|
135
|
-
poc_available: true,
|
|
136
|
-
ai_assisted_weapon: false,
|
|
137
|
-
ai_discovered: false,
|
|
138
|
-
active_exploitation: "suspected",
|
|
139
|
-
blast_radius: 15,
|
|
140
|
-
patch_available: false,
|
|
141
|
-
live_patch_available: false,
|
|
142
|
-
reboot_required: true,
|
|
143
|
-
});
|
|
138
|
+
// audit M P1-C: stored rwep_factors and computed rwep_score MUST agree.
|
|
139
|
+
// Previously rwep_factors held nulls (for unknown poc/ai/reboot) but
|
|
140
|
+
// rwep_score was computed from concrete defaults (poc=true, reboot=true).
|
|
141
|
+
// `scoring.validate()` then flagged every auto-imported draft for
|
|
142
|
+
// divergence > 5. Now: one canonical input object → both surfaces.
|
|
143
|
+
// The curation flow rewrites these once an operator answers the editorial
|
|
144
|
+
// questions; until then, the boolean shape on rwep_factors is the
|
|
145
|
+
// conservative-default snapshot and reproduces the score exactly.
|
|
146
|
+
const scoringInputs = buildScoringInputs(kevEntry, nvdPayload);
|
|
147
|
+
const rwep_factors = { ...scoringInputs };
|
|
148
|
+
const rwep_score = scoreCustom(scoringInputs);
|
|
144
149
|
|
|
145
150
|
const product = [kevEntry.vendorProject, kevEntry.product]
|
|
146
151
|
.filter(Boolean)
|
|
@@ -184,10 +189,21 @@ function buildKevDraftEntry(kevEntry, nvdPayload, epssPayload) {
|
|
|
184
189
|
"https://www.cisa.gov/known-exploited-vulnerabilities-catalog",
|
|
185
190
|
kevEntry.notes ? String(kevEntry.notes) : null,
|
|
186
191
|
].filter(Boolean),
|
|
187
|
-
|
|
192
|
+
// v0.12.15 (audit M P1-B): schema requires source_verified to be a
|
|
193
|
+
// YYYY-MM-DD string OR null; the prior `false` boolean produced an
|
|
194
|
+
// entry that failed strict catalog validation. Use null to mean
|
|
195
|
+
// "not yet verified" — operators populate the date during curation.
|
|
196
|
+
source_verified: null,
|
|
188
197
|
last_updated: TODAY,
|
|
189
198
|
last_verified: TODAY,
|
|
190
|
-
|
|
199
|
+
// v0.12.15 (audit M P1-D): `_auto_imported` must be the boolean `true`
|
|
200
|
+
// for lib/validate-cve-catalog.js's draft-recognition check (strict
|
|
201
|
+
// `=== true` comparison). The prior object-shape was non-recognizable
|
|
202
|
+
// and the strict validator treated KEV-discovered drafts as
|
|
203
|
+
// hard-error entries instead of warning-tier drafts. The provenance
|
|
204
|
+
// metadata that used to be inline now lives in `_auto_imported_meta`.
|
|
205
|
+
_auto_imported: true,
|
|
206
|
+
_auto_imported_meta: {
|
|
191
207
|
source: "KEV discovery",
|
|
192
208
|
imported_at: TODAY,
|
|
193
209
|
curation_needed: [
|
|
@@ -476,14 +492,20 @@ async function discoverNewRfcs(ctx, opts = {}) {
|
|
|
476
492
|
skills_referencing: [],
|
|
477
493
|
errata_count: null,
|
|
478
494
|
last_verified: TODAY,
|
|
479
|
-
_auto_imported:
|
|
495
|
+
// v0.12.15 (audit M P1-D, P3-T): boolean `_auto_imported: true` for
|
|
496
|
+
// strict-validator recognition; provenance moved to sibling
|
|
497
|
+
// `_auto_imported_meta`. Errata-URL hint converted to a real template
|
|
498
|
+
// literal so the rfc number actually interpolates (the previous double-
|
|
499
|
+
// quoted string left `${number}` as literal text in operator output).
|
|
500
|
+
_auto_imported: true,
|
|
501
|
+
_auto_imported_meta: {
|
|
480
502
|
source: `RFC discovery (IETF ${wg} working group)`,
|
|
481
503
|
imported_at: TODAY,
|
|
482
504
|
curation_needed: [
|
|
483
505
|
"relevance — project-specific framing of how this RFC matters for mid-2026 threats",
|
|
484
506
|
"lag_notes — what gaps remain or where the RFC falls short",
|
|
485
507
|
"skills_referencing — list of skills that should cite this RFC",
|
|
486
|
-
|
|
508
|
+
`errata_count — populate from <rfc-editor.org/errata/rfc${number}>`,
|
|
487
509
|
],
|
|
488
510
|
},
|
|
489
511
|
};
|
package/lib/cross-ref-api.js
CHANGED
|
@@ -21,6 +21,15 @@ const INDEX_DIR = path.join(DATA_DIR, '_indexes');
|
|
|
21
21
|
|
|
22
22
|
const _cache = new Map();
|
|
23
23
|
|
|
24
|
+
// v0.12.14 (audit C-F7): catalog corruption no longer crashes the runner
|
|
25
|
+
// uncaught. A malformed JSON file in data/ used to produce a SyntaxError
|
|
26
|
+
// at require-time of any consumer (lib/playbook-runner.js), which threw
|
|
27
|
+
// out of the run() entrypoint without honoring AGENTS.md's "non-zero
|
|
28
|
+
// exit + {ok:false, error} to stderr" contract. Now: caught + degraded
|
|
29
|
+
// to an empty catalog with a recorded _loadError that downstream code
|
|
30
|
+
// can inspect.
|
|
31
|
+
const _loadErrors = [];
|
|
32
|
+
|
|
24
33
|
function loadCatalog(filename) {
|
|
25
34
|
if (_cache.has(filename)) return _cache.get(filename);
|
|
26
35
|
const full = path.join(DATA_DIR, filename);
|
|
@@ -28,9 +37,17 @@ function loadCatalog(filename) {
|
|
|
28
37
|
_cache.set(filename, {});
|
|
29
38
|
return {};
|
|
30
39
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
42
|
+
_cache.set(filename, parsed);
|
|
43
|
+
return parsed;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
_loadErrors.push({ kind: 'catalog', file: filename, error: e.message });
|
|
46
|
+
const stub = {};
|
|
47
|
+
Object.defineProperty(stub, '_loadError', { value: e.message, enumerable: false });
|
|
48
|
+
_cache.set(filename, stub);
|
|
49
|
+
return stub;
|
|
50
|
+
}
|
|
34
51
|
}
|
|
35
52
|
|
|
36
53
|
function loadIndex(filename) {
|
|
@@ -40,9 +57,21 @@ function loadIndex(filename) {
|
|
|
40
57
|
_cache.set('idx:' + filename, {});
|
|
41
58
|
return {};
|
|
42
59
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
try {
|
|
61
|
+
const parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
62
|
+
_cache.set('idx:' + filename, parsed);
|
|
63
|
+
return parsed;
|
|
64
|
+
} catch (e) {
|
|
65
|
+
_loadErrors.push({ kind: 'index', file: filename, error: e.message });
|
|
66
|
+
const stub = {};
|
|
67
|
+
Object.defineProperty(stub, '_loadError', { value: e.message, enumerable: false });
|
|
68
|
+
_cache.set('idx:' + filename, stub);
|
|
69
|
+
return stub;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getLoadErrors() {
|
|
74
|
+
return _loadErrors.slice();
|
|
46
75
|
}
|
|
47
76
|
|
|
48
77
|
function entries(catalog) {
|
|
@@ -221,4 +250,8 @@ module.exports = {
|
|
|
221
250
|
// Lower-level access (engine uses these directly)
|
|
222
251
|
_loadCatalog: loadCatalog,
|
|
223
252
|
_loadIndex: loadIndex,
|
|
253
|
+
// v0.12.14: surface accumulated catalog/index load errors. Returns
|
|
254
|
+
// [{kind, file, error}, ...] for every catalog/index whose JSON
|
|
255
|
+
// parse failed. Empty array on a healthy install.
|
|
256
|
+
getLoadErrors,
|
|
224
257
|
};
|
package/lib/cve-curation.js
CHANGED
|
@@ -43,6 +43,14 @@ const path = require("path");
|
|
|
43
43
|
// before deciding promotion.
|
|
44
44
|
const { withCatalogLock } = require("./refresh-external");
|
|
45
45
|
const { validate: validateAgainstSchema } = require("./validate-cve-catalog");
|
|
46
|
+
// audit J F3: derive rwep_score via the canonical scoring helper rather
|
|
47
|
+
// than a blind `Object.values(...).reduce(sum)`. The helper detects shape
|
|
48
|
+
// (boolean inputs → scoreCustom; post-weight numeric inputs → sum + clamp)
|
|
49
|
+
// so the curation apply-path produces a score that matches whatever the
|
|
50
|
+
// catalog scorer or playbook-runner would have produced for the same
|
|
51
|
+
// factors. Direct dependency on scoring.js is intentional — scoring.js is
|
|
52
|
+
// the authoritative formula.
|
|
53
|
+
const { deriveRwepFromFactors } = require("./scoring");
|
|
46
54
|
|
|
47
55
|
const ROOT = path.resolve(__dirname, "..");
|
|
48
56
|
const CVE_SCHEMA_PATH = path.join(ROOT, "lib", "schemas", "cve-catalog.schema.json");
|
|
@@ -50,11 +58,18 @@ let _cveSchemaCache = null;
|
|
|
50
58
|
function loadCveEntrySchema() {
|
|
51
59
|
if (_cveSchemaCache) return _cveSchemaCache;
|
|
52
60
|
try {
|
|
61
|
+
// v0.12.15 (audit M P1-A): the prior version of this function looked for
|
|
62
|
+
// either `root.patternProperties["^CVE-\\d{4}-\\d+$"]` or an object
|
|
63
|
+
// `root.additionalProperties`. The actual schema at lib/schemas/cve-
|
|
64
|
+
// catalog.schema.json has NEITHER — its top level IS the entry shape
|
|
65
|
+
// (`{type:'object', required:[...], properties: {...}}`) because
|
|
66
|
+
// validate-cve-catalog.js iterates each CVE id key manually and runs
|
|
67
|
+
// the schema validator over each value. Result: loadCveEntrySchema()
|
|
68
|
+
// always returned null, the v0.12.12 codex P1 #1 fix (strict-schema
|
|
69
|
+
// gating of promotion) was silently disabled, and schema-violating
|
|
70
|
+
// entries promoted anyway. Use the root schema directly.
|
|
53
71
|
const root = JSON.parse(fs.readFileSync(CVE_SCHEMA_PATH, "utf8"));
|
|
54
|
-
|
|
55
|
-
(root.patternProperties && root.patternProperties["^CVE-\\d{4}-\\d+$"]) ||
|
|
56
|
-
(root.additionalProperties && typeof root.additionalProperties === "object" ? root.additionalProperties : null);
|
|
57
|
-
_cveSchemaCache = entrySchema || null;
|
|
72
|
+
_cveSchemaCache = root || null;
|
|
58
73
|
return _cveSchemaCache;
|
|
59
74
|
} catch {
|
|
60
75
|
return null;
|
|
@@ -264,7 +279,13 @@ function buildQuestionnaire(cveId, draft) {
|
|
|
264
279
|
// Pull candidate catalogs. Each is optional — missing catalogs are skipped
|
|
265
280
|
// gracefully. J7 makes these one-shot loads per process.
|
|
266
281
|
const atlas = loadJson("data/atlas-ttps.json");
|
|
267
|
-
|
|
282
|
+
// v0.12.15 (audit M P1-E): the catalog ships as data/attack-techniques.json
|
|
283
|
+
// (renamed from data/attack-ttps.json before the v0.12.12 release; the
|
|
284
|
+
// canonical file path is also what lib/validate-cve-catalog.js consumes).
|
|
285
|
+
// The prior `data/attack-ttps.json` lookup silently fell back to an empty
|
|
286
|
+
// object via loadJsonRaw's ENOENT handling, so the ATT&CK candidate
|
|
287
|
+
// questionnaire branch always returned zero proposals.
|
|
288
|
+
const attack = loadJson("data/attack-techniques.json");
|
|
268
289
|
const cwe = loadJson("data/cwe-catalog.json");
|
|
269
290
|
const frameworkGaps = loadJson("data/framework-control-gaps.json");
|
|
270
291
|
|
|
@@ -549,17 +570,15 @@ function applyAnswersUnderLock(cveId, catalog, catalogPath, answers) {
|
|
|
549
570
|
appliedFields.push(field);
|
|
550
571
|
}
|
|
551
572
|
|
|
552
|
-
//
|
|
553
|
-
//
|
|
554
|
-
// numeric
|
|
573
|
+
// audit J F3: derive rwep_score via the canonical scoring helper rather
|
|
574
|
+
// than a blind sum. deriveRwepFromFactors detects shape (boolean inputs
|
|
575
|
+
// → scoreCustom; post-weight numeric inputs → sum + clamp) and routes
|
|
576
|
+
// accordingly, so the apply-path produces a score that agrees with
|
|
577
|
+
// scoring.validate() instead of diverging from it.
|
|
555
578
|
if ("rwep_factors" in answers && !("rwep_score" in answers)
|
|
556
579
|
&& entry.rwep_factors && typeof entry.rwep_factors === "object") {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
if (typeof v === "number") sum += v;
|
|
560
|
-
}
|
|
561
|
-
entry.rwep_score = Math.max(0, Math.min(100, sum));
|
|
562
|
-
appliedFields.push("rwep_score (derived from rwep_factors)");
|
|
580
|
+
entry.rwep_score = deriveRwepFromFactors(entry.rwep_factors);
|
|
581
|
+
appliedFields.push("rwep_score (derived from rwep_factors via scoring.deriveRwepFromFactors)");
|
|
563
582
|
}
|
|
564
583
|
|
|
565
584
|
// last_updated reflects the apply moment.
|
package/lib/lint-skills.js
CHANGED
|
@@ -635,8 +635,13 @@ function loadContext() {
|
|
|
635
635
|
*/
|
|
636
636
|
function findOrphanSkillFiles(manifestSkills) {
|
|
637
637
|
if (!fs.existsSync(SKILLS_DIR)) return [];
|
|
638
|
+
// F19 — manifest paths are stored as forward-slash strings by contract
|
|
639
|
+
// (lib/verify.js validateSkillPath() rejects backslashes). The previous
|
|
640
|
+
// path.sep split was a no-op on Linux and incorrect on Windows when
|
|
641
|
+
// mixed separators arrived through other ingest paths; the cleaner
|
|
642
|
+
// contract is to normalise the comparison key directly.
|
|
638
643
|
const referenced = new Set(
|
|
639
|
-
manifestSkills.map((s) => s.path
|
|
644
|
+
manifestSkills.map((s) => String(s.path).replace(/\\/g, '/')),
|
|
640
645
|
);
|
|
641
646
|
const orphans = [];
|
|
642
647
|
for (const entry of fs.readdirSync(SKILLS_DIR, { withFileTypes: true })) {
|