@blamejs/exceptd-skills 0.12.7 → 0.12.9
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/AGENTS.md +15 -1
- package/ARCHITECTURE.md +21 -5
- package/CHANGELOG.md +150 -0
- package/README.md +1 -1
- package/bin/exceptd.js +416 -69
- package/data/_indexes/_meta.json +44 -44
- package/data/_indexes/activity-feed.json +34 -34
- package/data/_indexes/catalog-summaries.json +9 -9
- package/data/_indexes/chains.json +249 -11
- package/data/_indexes/frequency.json +63 -5
- package/data/_indexes/jurisdiction-map.json +13 -3
- package/data/_indexes/section-offsets.json +1171 -1027
- package/data/_indexes/summary-cards.json +2 -2
- package/data/_indexes/token-budget.json +232 -152
- package/data/atlas-ttps.json +189 -1
- package/data/cve-catalog.json +34 -22
- package/data/cwe-catalog.json +290 -1
- package/data/d3fend-catalog.json +163 -1
- package/data/framework-control-gaps.json +243 -0
- package/data/playbooks/containers.json +23 -5
- package/data/playbooks/cred-stores.json +9 -9
- package/data/playbooks/crypto.json +8 -8
- package/data/playbooks/hardening.json +46 -10
- package/data/playbooks/library-author.json +16 -20
- package/data/playbooks/mcp.json +1 -0
- package/data/playbooks/runtime.json +7 -7
- package/data/playbooks/sbom.json +11 -11
- package/data/playbooks/secrets.json +4 -4
- package/data/rfc-references.json +144 -0
- package/lib/playbook-runner.js +119 -35
- package/lib/prefetch.js +27 -6
- package/lib/refresh-external.js +32 -9
- package/lib/schemas/skill-frontmatter.schema.json +2 -2
- package/manifest-snapshot.json +1 -1
- package/manifest.json +73 -73
- package/orchestrator/index.js +1 -1
- package/package.json +2 -1
- package/sbom.cdx.json +6 -6
- package/scripts/check-sbom-currency.js +87 -0
- package/scripts/check-test-coverage.README.md +148 -0
- package/scripts/check-test-coverage.js +476 -0
- package/scripts/hooks/pre-commit.sh +19 -0
- package/scripts/predeploy.js +14 -30
- package/skills/age-gates-child-safety/skill.md +3 -0
- package/skills/ai-attack-surface/skill.md +29 -1
- package/skills/ai-c2-detection/skill.md +30 -1
- package/skills/ai-risk-management/skill.md +3 -0
- package/skills/api-security/skill.md +3 -0
- package/skills/attack-surface-pentest/skill.md +3 -0
- package/skills/cloud-security/skill.md +3 -0
- package/skills/compliance-theater/skill.md +6 -0
- package/skills/container-runtime-security/skill.md +3 -0
- package/skills/coordinated-vuln-disclosure/skill.md +8 -1
- package/skills/defensive-countermeasure-mapping/skill.md +1 -1
- package/skills/dlp-gap-analysis/skill.md +3 -0
- package/skills/email-security-anti-phishing/skill.md +9 -1
- package/skills/exploit-scoring/skill.md +6 -0
- package/skills/identity-assurance/skill.md +6 -1
- package/skills/incident-response-playbook/skill.md +8 -2
- package/skills/kernel-lpe-triage/skill.md +24 -4
- package/skills/mcp-agent-trust/skill.md +28 -1
- package/skills/mlops-security/skill.md +3 -0
- package/skills/ot-ics-security/skill.md +3 -0
- package/skills/policy-exception-gen/skill.md +6 -0
- package/skills/rag-pipeline-security/skill.md +30 -1
- package/skills/researcher/skill.md +6 -0
- package/skills/sector-energy/skill.md +3 -0
- package/skills/sector-federal-government/skill.md +3 -0
- package/skills/sector-financial/skill.md +3 -0
- package/skills/sector-healthcare/skill.md +3 -0
- package/skills/security-maturity-tiers/skill.md +25 -1
- package/skills/skill-update-loop/skill.md +38 -0
- package/skills/supply-chain-integrity/skill.md +3 -0
- package/skills/threat-model-currency/skill.md +4 -0
- package/skills/threat-modeling-methodology/skill.md +3 -0
- package/skills/webapp-security/skill.md +3 -0
- package/skills/zeroday-gap-learn/skill.md +6 -0
package/data/rfc-references.json
CHANGED
|
@@ -320,5 +320,149 @@
|
|
|
320
320
|
"pqc-first"
|
|
321
321
|
],
|
|
322
322
|
"last_verified": "2026-05-11"
|
|
323
|
+
},
|
|
324
|
+
"RFC-7489": {
|
|
325
|
+
"number": 7489,
|
|
326
|
+
"title": "Domain-based Message Authentication, Reporting, and Conformance (DMARC)",
|
|
327
|
+
"status": "Informational",
|
|
328
|
+
"published": "2015-03",
|
|
329
|
+
"tracker": "https://www.rfc-editor.org/info/rfc7489",
|
|
330
|
+
"relevance": "Defines DMARC — the email-authentication policy framework that binds SPF + DKIM results to a published domain-owner policy. Operator-facing email security depends on a published DMARC record with `p=reject` or `p=quarantine`; relaxed policies (`p=none`) are equivalent to no DMARC for spoofing-defense purposes. Cited alongside DKIM (RFC 6376) and SPF (RFC 7208) as the authoritative tri-RFC for anti-spoofing posture.",
|
|
331
|
+
"skills_referencing": [
|
|
332
|
+
"email-security-anti-phishing"
|
|
333
|
+
],
|
|
334
|
+
"last_verified": "2026-05-13"
|
|
335
|
+
},
|
|
336
|
+
"RFC-6376": {
|
|
337
|
+
"number": 6376,
|
|
338
|
+
"title": "DomainKeys Identified Mail (DKIM) Signatures",
|
|
339
|
+
"status": "Internet Standard",
|
|
340
|
+
"published": "2011-09",
|
|
341
|
+
"tracker": "https://www.rfc-editor.org/info/rfc6376",
|
|
342
|
+
"relevance": "Cryptographic signing of email by the originating domain. DMARC verification relies on either SPF or DKIM aligning with the From-header domain. Operationally, DKIM is the load-bearing half of DMARC — SPF breaks on legitimate forwarding paths; DKIM survives them. Key-length lag is the recurring framework gap: many deployments still use 1024-bit RSA keys despite IETF guidance to rotate to ≥2048-bit.",
|
|
343
|
+
"skills_referencing": [
|
|
344
|
+
"email-security-anti-phishing"
|
|
345
|
+
],
|
|
346
|
+
"last_verified": "2026-05-13"
|
|
347
|
+
},
|
|
348
|
+
"RFC-7208": {
|
|
349
|
+
"number": 7208,
|
|
350
|
+
"title": "Sender Policy Framework (SPF) for Authorizing Use of Domains in Email",
|
|
351
|
+
"status": "Proposed Standard",
|
|
352
|
+
"published": "2014-04",
|
|
353
|
+
"tracker": "https://www.rfc-editor.org/info/rfc7208",
|
|
354
|
+
"relevance": "Authorizes which IPs may send mail on behalf of a domain. DMARC's path-based authentication half. Limits: a strict 10-DNS-lookup ceiling that operators routinely blow past through nested includes, leading to permerror DMARC outcomes; SPF breaks across legitimate forwarders, which is why DKIM (RFC 6376) is the load-bearing half operationally.",
|
|
355
|
+
"skills_referencing": [
|
|
356
|
+
"email-security-anti-phishing"
|
|
357
|
+
],
|
|
358
|
+
"last_verified": "2026-05-13"
|
|
359
|
+
},
|
|
360
|
+
"RFC-8616": {
|
|
361
|
+
"number": 8616,
|
|
362
|
+
"title": "Email Authentication for Internationalized Mail",
|
|
363
|
+
"status": "Proposed Standard",
|
|
364
|
+
"published": "2019-06",
|
|
365
|
+
"tracker": "https://www.rfc-editor.org/info/rfc8616",
|
|
366
|
+
"relevance": "Updates DKIM / SPF / DMARC handling for internationalized domain names (IDN) and internationalized email-address local parts. Operator-facing: phishing campaigns increasingly leverage IDN homoglyphs (e.g. Cyrillic `а` for Latin `a`), and a DMARC implementation that doesn't honor RFC 8616 normalization rules can fail to align IDN-bearing From-headers correctly. Treat as a compatibility prerequisite for any anti-spoofing posture covering global mail flows.",
|
|
367
|
+
"skills_referencing": [
|
|
368
|
+
"email-security-anti-phishing"
|
|
369
|
+
],
|
|
370
|
+
"last_verified": "2026-05-13"
|
|
371
|
+
},
|
|
372
|
+
"RFC-8461": {
|
|
373
|
+
"number": 8461,
|
|
374
|
+
"title": "SMTP MTA Strict Transport Security (MTA-STS)",
|
|
375
|
+
"status": "Proposed Standard",
|
|
376
|
+
"published": "2018-09",
|
|
377
|
+
"tracker": "https://www.rfc-editor.org/info/rfc8461",
|
|
378
|
+
"relevance": "Forces TLS on inbound SMTP between MTAs that publish a `_mta-sts` policy. Closes the historical 'opportunistic-TLS-downgrade' attack window where a network-positioned attacker stripped the STARTTLS handshake. Operator-facing: an MTA-STS policy in `enforce` mode plus monitoring via TLSRPT (RFC 8460) is the baseline for inbound email confidentiality. Phishing-defense relevance is indirect — STARTTLS stripping enables session-layer manipulation that downstream defenses (DMARC, content classifiers) cannot recover from.",
|
|
379
|
+
"skills_referencing": [
|
|
380
|
+
"email-security-anti-phishing"
|
|
381
|
+
],
|
|
382
|
+
"last_verified": "2026-05-13"
|
|
383
|
+
},
|
|
384
|
+
"ISO-29147": {
|
|
385
|
+
"number": null,
|
|
386
|
+
"title": "ISO/IEC 29147:2018 Information technology — Security techniques — Vulnerability disclosure",
|
|
387
|
+
"status": "International Standard",
|
|
388
|
+
"published": "2018-10",
|
|
389
|
+
"tracker": "https://www.iso.org/standard/72311.html",
|
|
390
|
+
"relevance": "Internationally-recognized vulnerability-disclosure procedure standard. Operator-facing: an org claiming a CVD program must demonstrate the documented procedures cover receipt, triage, advisory drafting, notification, and post-disclosure review. Twin to ISO 30111 (handling); together they form the disclosure ↔ handling pair. The RFC 9116 (security.txt) artifact is the operator-facing surface that operationalizes ISO 29147 receipt requirements.",
|
|
391
|
+
"skills_referencing": [
|
|
392
|
+
"coordinated-vuln-disclosure"
|
|
393
|
+
],
|
|
394
|
+
"last_verified": "2026-05-13"
|
|
395
|
+
},
|
|
396
|
+
"ISO-30111": {
|
|
397
|
+
"number": null,
|
|
398
|
+
"title": "ISO/IEC 30111:2019 Information technology — Security techniques — Vulnerability handling processes",
|
|
399
|
+
"status": "International Standard",
|
|
400
|
+
"published": "2019-10",
|
|
401
|
+
"tracker": "https://www.iso.org/standard/69725.html",
|
|
402
|
+
"relevance": "Internal handling counterpart to ISO 29147. Defines the internal processes (investigation, resolution, release) that follow a disclosed vulnerability through to fix. Operator-facing: paired with ISO 29147 it supplies the end-to-end CVD lifecycle. Many compliance frameworks (NIS2, EU CRA) reference 'a documented vulnerability handling process'; ISO 30111 is the canonical artifact that satisfies the wording.",
|
|
403
|
+
"skills_referencing": [
|
|
404
|
+
"coordinated-vuln-disclosure"
|
|
405
|
+
],
|
|
406
|
+
"last_verified": "2026-05-13"
|
|
407
|
+
},
|
|
408
|
+
"RFC-9116": {
|
|
409
|
+
"number": 9116,
|
|
410
|
+
"title": "A File Format to Aid in Security Vulnerability Disclosure",
|
|
411
|
+
"status": "Proposed Standard",
|
|
412
|
+
"published": "2022-04",
|
|
413
|
+
"tracker": "https://www.rfc-editor.org/info/rfc9116",
|
|
414
|
+
"relevance": "Defines the `/.well-known/security.txt` file format. Operator-facing CVD entry point: researchers reaching a domain consult `/.well-known/security.txt` for the disclosure contact + policy URL + preferred encryption + acknowledgments page. Absence is a recurring CVD-friction finding; presence with a stale `Expires` header is equivalent to absence. Pair with ISO 29147 receipt requirements.",
|
|
415
|
+
"skills_referencing": [
|
|
416
|
+
"coordinated-vuln-disclosure"
|
|
417
|
+
],
|
|
418
|
+
"last_verified": "2026-05-13"
|
|
419
|
+
},
|
|
420
|
+
"CSAF-2.0": {
|
|
421
|
+
"number": null,
|
|
422
|
+
"title": "OASIS Common Security Advisory Framework Version 2.0",
|
|
423
|
+
"status": "OASIS Standard",
|
|
424
|
+
"published": "2022-11",
|
|
425
|
+
"tracker": "https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html",
|
|
426
|
+
"relevance": "Machine-readable security advisory format adopted by EU CRA, CISA, and major vendor PSIRTs. Replaces the CVRF-1.2 format. Operator-facing: CSAF 2.0 documents carry the same advisory content traditionally found in vendor PDFs but in a parseable JSON form that consumers can ingest for fleet-wide impact assessment. exceptd emits CSAF-2.0 close.evidence_package bundles from `exceptd run --format csaf-2.0` for downstream auditor consumption.",
|
|
427
|
+
"skills_referencing": [
|
|
428
|
+
"coordinated-vuln-disclosure"
|
|
429
|
+
],
|
|
430
|
+
"last_verified": "2026-05-13"
|
|
431
|
+
},
|
|
432
|
+
"RFC-6545": {
|
|
433
|
+
"number": 6545,
|
|
434
|
+
"title": "Real-time Inter-network Defense (RID)",
|
|
435
|
+
"status": "Proposed Standard",
|
|
436
|
+
"published": "2012-04",
|
|
437
|
+
"tracker": "https://www.rfc-editor.org/info/rfc6545",
|
|
438
|
+
"relevance": "Defines the RID schema for exchanging incident-response data between organizations and CSIRTs. Operator-facing relevance is via the IODEF (RFC 7970) payload that RID transports — the pair is the IETF-standardized cross-organizational incident-coordination protocol. Adoption lags; in practice many SOCs use ad-hoc CSV/JSON exchanges instead, leaving compliance with 'documented coordination channel' requirements weakly evidenced.",
|
|
439
|
+
"skills_referencing": [
|
|
440
|
+
"incident-response-playbook"
|
|
441
|
+
],
|
|
442
|
+
"last_verified": "2026-05-13"
|
|
443
|
+
},
|
|
444
|
+
"RFC-6546": {
|
|
445
|
+
"number": 6546,
|
|
446
|
+
"title": "Transport of Real-time Inter-network Defense (RID) Messages over HTTP/TLS",
|
|
447
|
+
"status": "Proposed Standard",
|
|
448
|
+
"published": "2012-04",
|
|
449
|
+
"tracker": "https://www.rfc-editor.org/info/rfc6546",
|
|
450
|
+
"relevance": "Specifies the HTTP-over-TLS transport for RID (RFC 6545) messages. Operator-facing: provides the wire-level protocol for IODEF/RID message exchange when an organization commits to standardized incident-data coordination. Pair this with RFC 6545 (schema) and RFC 7970 (IODEF v2 payload).",
|
|
451
|
+
"skills_referencing": [
|
|
452
|
+
"incident-response-playbook"
|
|
453
|
+
],
|
|
454
|
+
"last_verified": "2026-05-13"
|
|
455
|
+
},
|
|
456
|
+
"RFC-7970": {
|
|
457
|
+
"number": 7970,
|
|
458
|
+
"title": "The Incident Object Description Exchange Format Version 2",
|
|
459
|
+
"status": "Proposed Standard",
|
|
460
|
+
"published": "2016-11",
|
|
461
|
+
"tracker": "https://www.rfc-editor.org/info/rfc7970",
|
|
462
|
+
"relevance": "IODEF v2 — the structured-incident payload format carried by RID. Operator-facing: IODEF v2 is the canonical machine-readable incident record. Use cases include cross-CSIRT coordination, regulator submissions where structured data is requested, and SIEM-to-SIEM federation. Adoption is sector-uneven; healthcare and financial sectors have stronger uptake than general industry.",
|
|
463
|
+
"skills_referencing": [
|
|
464
|
+
"incident-response-playbook"
|
|
465
|
+
],
|
|
466
|
+
"last_verified": "2026-05-13"
|
|
323
467
|
}
|
|
324
468
|
}
|
package/lib/playbook-runner.js
CHANGED
|
@@ -418,26 +418,118 @@ function analyze(playbookId, directiveId, detectResult, agentSignals = {}) {
|
|
|
418
418
|
const an = resolvedPhase(playbook, directiveId, 'analyze');
|
|
419
419
|
const directive = findDirective(playbook, directiveId);
|
|
420
420
|
|
|
421
|
-
//
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
425
|
-
//
|
|
426
|
-
//
|
|
421
|
+
// Resolve catalogued CVEs from the domain.cve_refs list. This list is the
|
|
422
|
+
// playbook's CVE scan-coverage enumeration — every CVE this playbook can
|
|
423
|
+
// detect. By itself it is NOT a statement that the operator is affected by
|
|
424
|
+
// any of these CVEs; affected-ness requires evidence correlation in detect.
|
|
425
|
+
//
|
|
426
|
+
// Two distinct sets are computed below:
|
|
427
|
+
//
|
|
428
|
+
// catalogBaselineCves — every CVE the playbook scans for, with full
|
|
429
|
+
// per-CVE catalog context (RWEP / KEV / CVSS / AI-discovery /
|
|
430
|
+
// active-exploitation / patch state). Always populated when the
|
|
431
|
+
// playbook has domain.cve_refs. Each entry carries correlated_via=null
|
|
432
|
+
// and a `note` flagging it as catalog-only.
|
|
433
|
+
//
|
|
434
|
+
// matchedCves — CVEs the operator's submitted evidence actually
|
|
435
|
+
// correlates to. Correlation paths:
|
|
436
|
+
// (a) An indicator fired (verdict === 'hit') whose attack_ref or
|
|
437
|
+
// atlas_ref intersects the CVE's attack_refs / atlas_refs in
|
|
438
|
+
// the catalog.
|
|
439
|
+
// (b) An agentSignal explicitly references the CVE id with a
|
|
440
|
+
// truthy value (`agentSignals[cveId] === true`) or with a
|
|
441
|
+
// string value 'hit' / 'detected' / 'affected'.
|
|
442
|
+
// Each entry carries correlated_via=<reason string> so downstream
|
|
443
|
+
// consumers (CSAF / SARIF / OpenVEX / human renderer) can show the
|
|
444
|
+
// provenance, and so an empty matchedCves means "no evidence
|
|
445
|
+
// correlated to operator's submission" rather than "playbook has
|
|
446
|
+
// no CVEs of interest."
|
|
447
|
+
//
|
|
448
|
+
// VEX filter (agentSignals.vex_filter): a set of CVE IDs the operator has
|
|
449
|
+
// formally declared not_affected via CycloneDX/OpenVEX. VEX-dropped CVEs
|
|
450
|
+
// are removed from BOTH arrays (they're not affected — neither correlated
|
|
451
|
+
// nor part of effective scan coverage for this run).
|
|
427
452
|
const cveRefs = playbook.domain.cve_refs || [];
|
|
428
453
|
const vexFilter = agentSignals.vex_filter instanceof Set ? agentSignals.vex_filter
|
|
429
454
|
: (Array.isArray(agentSignals.vex_filter) ? new Set(agentSignals.vex_filter) : null);
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
?
|
|
433
|
-
:
|
|
455
|
+
const allCves = cveRefs.map(id => xref.byCve(id)).filter(r => r.found);
|
|
456
|
+
const catalogBaselineCves = vexFilter
|
|
457
|
+
? allCves.filter(c => !vexFilter.has(c.cve_id))
|
|
458
|
+
: allCves;
|
|
434
459
|
const vexDropped = vexFilter
|
|
435
|
-
?
|
|
460
|
+
? allCves.filter(c => vexFilter.has(c.cve_id)).map(c => c.cve_id)
|
|
436
461
|
: [];
|
|
437
462
|
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
463
|
+
// Build correlation map: cve_id -> array of "indicator_hit:<id>" / "signal:<id>" reasons.
|
|
464
|
+
const correlationsByCve = new Map();
|
|
465
|
+
const addCorrelation = (cveId, reason) => {
|
|
466
|
+
if (!correlationsByCve.has(cveId)) correlationsByCve.set(cveId, []);
|
|
467
|
+
const arr = correlationsByCve.get(cveId);
|
|
468
|
+
if (!arr.includes(reason)) arr.push(reason);
|
|
469
|
+
};
|
|
470
|
+
// (a) indicator-hit → CVE via shared attack_ref / atlas_ref.
|
|
471
|
+
const playbookDetect = resolvedPhase(playbook, directiveId, 'detect');
|
|
472
|
+
const indicatorRefs = new Map(); // indicator.id -> { attack_ref, atlas_ref }
|
|
473
|
+
for (const ind of (playbookDetect.indicators || [])) {
|
|
474
|
+
indicatorRefs.set(ind.id, { attack_ref: ind.attack_ref || null, atlas_ref: ind.atlas_ref || null });
|
|
475
|
+
}
|
|
476
|
+
const firedIndicators = (detectResult.indicators || []).filter(i => i.verdict === 'hit');
|
|
477
|
+
for (const fired of firedIndicators) {
|
|
478
|
+
const refs = indicatorRefs.get(fired.id) || { attack_ref: fired.attack_ref || null, atlas_ref: fired.atlas_ref || null };
|
|
479
|
+
if (!refs.attack_ref && !refs.atlas_ref) continue;
|
|
480
|
+
for (const c of catalogBaselineCves) {
|
|
481
|
+
const attackHit = refs.attack_ref && Array.isArray(c.attack_refs) && c.attack_refs.includes(refs.attack_ref);
|
|
482
|
+
const atlasHit = refs.atlas_ref && Array.isArray(c.atlas_refs) && c.atlas_refs.includes(refs.atlas_ref);
|
|
483
|
+
if (attackHit || atlasHit) addCorrelation(c.cve_id, `indicator_hit:${fired.id}`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
// (b) agentSignals explicitly referencing a CVE id.
|
|
487
|
+
for (const c of catalogBaselineCves) {
|
|
488
|
+
const sig = agentSignals[c.cve_id];
|
|
489
|
+
if (sig === true || sig === 'hit' || sig === 'detected' || sig === 'affected') {
|
|
490
|
+
addCorrelation(c.cve_id, `signal:${c.cve_id}`);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const matchedCves = catalogBaselineCves.filter(c => correlationsByCve.has(c.cve_id));
|
|
495
|
+
|
|
496
|
+
// Per-CVE shape — identical between matched_cves and catalog_baseline_cves
|
|
497
|
+
// so consumers can iterate either without branching. matched_cves entries
|
|
498
|
+
// carry a non-null correlated_via array; catalog_baseline_cves entries
|
|
499
|
+
// carry correlated_via:null and a `note` clarifying the field's intent.
|
|
500
|
+
const cveShape = (c, correlatedVia) => ({
|
|
501
|
+
cve_id: c.cve_id,
|
|
502
|
+
rwep: c.rwep_score,
|
|
503
|
+
cvss_score: c.entry?.cvss_score ?? null,
|
|
504
|
+
cvss_vector: c.entry?.cvss_vector ?? null,
|
|
505
|
+
cisa_kev: c.cisa_kev,
|
|
506
|
+
cisa_kev_date: c.entry?.cisa_kev_date ?? null,
|
|
507
|
+
cisa_kev_due_date: c.entry?.cisa_kev_due_date ?? null,
|
|
508
|
+
poc_available: c.entry?.poc_available ?? null,
|
|
509
|
+
ai_discovered: c.ai_discovered,
|
|
510
|
+
ai_assisted_weaponization: c.entry?.ai_assisted_weaponization ?? null,
|
|
511
|
+
active_exploitation: c.active_exploitation,
|
|
512
|
+
patch_available: c.entry?.patch_available ?? null,
|
|
513
|
+
patch_required_reboot: c.entry?.patch_required_reboot ?? null,
|
|
514
|
+
live_patch_available: c.entry?.live_patch_available ?? null,
|
|
515
|
+
epss_score: c.entry?.epss_score ?? null,
|
|
516
|
+
epss_date: c.entry?.epss_date ?? null,
|
|
517
|
+
atlas_refs: c.atlas_refs,
|
|
518
|
+
attack_refs: c.attack_refs,
|
|
519
|
+
affected_versions: c.entry?.affected_versions ?? null,
|
|
520
|
+
correlated_via: correlatedVia,
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const matchedCveEntries = matchedCves.map(c => cveShape(c, correlationsByCve.get(c.cve_id)));
|
|
524
|
+
const catalogBaselineEntries = catalogBaselineCves.map(c => ({
|
|
525
|
+
...cveShape(c, null),
|
|
526
|
+
note: 'Catalog-baseline entry — this CVE is in the playbook\'s scan coverage but no submitted evidence correlated to it. Not a statement that the operator is affected.',
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
// RWEP composition: start from the per-CVE rwep_score of evidence-correlated
|
|
530
|
+
// matches (NOT catalog baseline) so RWEP base reflects what the operator's
|
|
531
|
+
// evidence actually surfaced. Adjust by playbook's rwep_inputs based on
|
|
532
|
+
// detect hits + agent signals.
|
|
441
533
|
const baseRwep = matchedCves.length ? Math.max(...matchedCves.map(c => c.rwep_score)) : 0;
|
|
442
534
|
let adjustedRwep = baseRwep;
|
|
443
535
|
const rwepBreakdown = [];
|
|
@@ -495,27 +587,19 @@ function analyze(playbookId, directiveId, detectResult, agentSignals = {}) {
|
|
|
495
587
|
// Pull every required field from the catalog entry; null is only emitted
|
|
496
588
|
// when the catalog itself lacks the value, never when we just forgot to
|
|
497
589
|
// forward it. EPSS is included because validate-cves --live populates it.
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
patch_required_reboot: c.entry?.patch_required_reboot ?? null,
|
|
512
|
-
live_patch_available: c.entry?.live_patch_available ?? null,
|
|
513
|
-
epss_score: c.entry?.epss_score ?? null,
|
|
514
|
-
epss_date: c.entry?.epss_date ?? null,
|
|
515
|
-
atlas_refs: c.atlas_refs,
|
|
516
|
-
attack_refs: c.attack_refs,
|
|
517
|
-
affected_versions: c.entry?.affected_versions ?? null,
|
|
518
|
-
})),
|
|
590
|
+
//
|
|
591
|
+
// matched_cves — evidence-correlated only. Each entry has a non-null
|
|
592
|
+
// correlated_via[] array naming the indicator hits or agent signals that
|
|
593
|
+
// tied the operator's submission to this CVE. Empty array means the
|
|
594
|
+
// playbook's scan coverage saw no matching evidence in this run.
|
|
595
|
+
matched_cves: matchedCveEntries,
|
|
596
|
+
// catalog_baseline_cves — every CVE the playbook scans for, with the
|
|
597
|
+
// same per-CVE shape but correlated_via=null and a note explaining the
|
|
598
|
+
// field is scan-coverage metadata, NOT an operator-affected list. Use
|
|
599
|
+
// this when surfacing "what CVEs does this playbook check for?" Use
|
|
600
|
+
// matched_cves when surfacing "what CVEs is the operator actually
|
|
601
|
+
// affected by based on submitted evidence?"
|
|
602
|
+
catalog_baseline_cves: catalogBaselineEntries,
|
|
519
603
|
rwep: { base: baseRwep, adjusted: adjustedRwep, breakdown: rwepBreakdown, threshold: directive ? resolvedPhase(playbook, directiveId, 'direct').rwep_threshold : null },
|
|
520
604
|
blast_radius_score: blastRadiusScore,
|
|
521
605
|
blast_radius_basis: blastRubric.find(r => r.blast_radius_score === blastRadiusScore) || null,
|
package/lib/prefetch.js
CHANGED
|
@@ -80,15 +80,24 @@ const SOURCES = {
|
|
|
80
80
|
.filter(Boolean),
|
|
81
81
|
},
|
|
82
82
|
pins: {
|
|
83
|
-
description: "MITRE GitHub releases for ATLAS / ATT&CK
|
|
83
|
+
description: "MITRE GitHub releases for ATLAS / ATT&CK pin checks",
|
|
84
84
|
rate: { tokens: 30, windowMs: 60 * 60_000 }, // anon: 60/h, leave headroom
|
|
85
85
|
rate_with_key: { tokens: 500, windowMs: 60 * 60_000 },
|
|
86
86
|
concurrency: 2,
|
|
87
|
+
// D3FEND and CWE were previously listed here but neither project
|
|
88
|
+
// publishes via GitHub Releases — D3FEND distributes the ontology
|
|
89
|
+
// from d3fend/d3fend-ontology without tagged releases, and CWE
|
|
90
|
+
// ships its catalog as XML/JSON downloads from cwe.mitre.org rather
|
|
91
|
+
// than a GitHub repo. The old api.github.com URLs (mitre/cwe and
|
|
92
|
+
// d3fend/d3fend-data) returned HTTP 404 on every refresh, surfacing
|
|
93
|
+
// as "2 error(s)" in the prefetch summary. Pin currency for those
|
|
94
|
+
// two frameworks is tracked via lib/upstream-check.js against
|
|
95
|
+
// cwe.mitre.org and d3fend.mitre.org respectively; the prefetch
|
|
96
|
+
// registry only contains sources that actually have a GitHub
|
|
97
|
+
// Releases feed to poll.
|
|
87
98
|
expand: () => [
|
|
88
99
|
{ id: "mitre-atlas__atlas-data__releases", url: "https://api.github.com/repos/mitre-atlas/atlas-data/releases?per_page=5" },
|
|
89
100
|
{ id: "mitre-attack__attack-stix-data__releases", url: "https://api.github.com/repos/mitre-attack/attack-stix-data/releases?per_page=5" },
|
|
90
|
-
{ id: "d3fend__d3fend-data__releases", url: "https://api.github.com/repos/d3fend/d3fend-data/releases?per_page=5" },
|
|
91
|
-
{ id: "mitre__cwe__releases", url: "https://api.github.com/repos/mitre/cwe/releases?per_page=5" },
|
|
92
101
|
],
|
|
93
102
|
},
|
|
94
103
|
};
|
|
@@ -364,14 +373,26 @@ async function main() {
|
|
|
364
373
|
const opts = parseArgs(process.argv);
|
|
365
374
|
if (opts.help) {
|
|
366
375
|
printHelp();
|
|
367
|
-
|
|
376
|
+
return;
|
|
368
377
|
}
|
|
378
|
+
// Why process.exitCode and not process.exit():
|
|
379
|
+
// On Windows + Node 25 (libuv), calling process.exit() synchronously
|
|
380
|
+
// while in-flight fetch / AbortController teardown is still mid-close
|
|
381
|
+
// produced `Assertion failed: !(handle->flags & UV_HANDLE_CLOSING),
|
|
382
|
+
// file src\win\async.c, line 76` followed by exit 3221226505
|
|
383
|
+
// (STATUS_STACK_BUFFER_OVERRUN). The summary line had already
|
|
384
|
+
// flushed, so operators saw the crash *after* their summary —
|
|
385
|
+
// contractually correct but visibly noisy. Letting the event loop
|
|
386
|
+
// drain naturally — via exitCode + return — lets undici's connection
|
|
387
|
+
// pool and the AbortController signal listeners finish teardown
|
|
388
|
+
// before the process exits, eliminating the assertion. Same pattern
|
|
389
|
+
// documented in CLAUDE.md for v0.11.11's `ci` #100 regression.
|
|
369
390
|
try {
|
|
370
391
|
const result = await prefetch(opts);
|
|
371
|
-
process.
|
|
392
|
+
process.exitCode = result.errors > 0 ? 1 : 0;
|
|
372
393
|
} catch (err) {
|
|
373
394
|
console.error(`prefetch: fatal: ${err.message}`);
|
|
374
|
-
process.
|
|
395
|
+
process.exitCode = 2;
|
|
375
396
|
}
|
|
376
397
|
}
|
|
377
398
|
|
package/lib/refresh-external.js
CHANGED
|
@@ -43,6 +43,21 @@ const ROOT = path.join(__dirname, "..");
|
|
|
43
43
|
const ABS = (p) => path.join(ROOT, p);
|
|
44
44
|
const TODAY = new Date().toISOString().slice(0, 10);
|
|
45
45
|
|
|
46
|
+
// v0.12.8: the CVE catalog path used by refresh-external is overridable so
|
|
47
|
+
// tests can redirect to a tempdir instead of mutating the real shipped
|
|
48
|
+
// data/cve-catalog.json. Resolution order:
|
|
49
|
+
// 1. opts.catalog (--catalog CLI arg)
|
|
50
|
+
// 2. process.env.EXCEPTD_CVE_CATALOG (env var)
|
|
51
|
+
// 3. ROOT/data/cve-catalog.json (default)
|
|
52
|
+
// All four write-sites in this file route through resolveCatalogPath() so
|
|
53
|
+
// that the redirect is consistent across the advisory-import, GHSA-import,
|
|
54
|
+
// and per-source merge code paths.
|
|
55
|
+
function resolveCatalogPath(opts) {
|
|
56
|
+
if (opts && opts.catalog) return path.resolve(opts.catalog);
|
|
57
|
+
if (process.env.EXCEPTD_CVE_CATALOG) return path.resolve(process.env.EXCEPTD_CVE_CATALOG);
|
|
58
|
+
return ABS("data/cve-catalog.json");
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
function parseArgs(argv) {
|
|
47
62
|
const out = {
|
|
48
63
|
apply: false,
|
|
@@ -64,6 +79,8 @@ function parseArgs(argv) {
|
|
|
64
79
|
else if (a === "--help" || a === "-h") out.help = true;
|
|
65
80
|
else if (a === "--advisory") { out.advisory = argv[++i]; }
|
|
66
81
|
else if (a.startsWith("--advisory=")) { out.advisory = a.slice("--advisory=".length); }
|
|
82
|
+
else if (a === "--catalog") { out.catalog = argv[++i]; }
|
|
83
|
+
else if (a.startsWith("--catalog=")) { out.catalog = a.slice("--catalog=".length); }
|
|
67
84
|
else if (a === "--from-cache") {
|
|
68
85
|
// accept either --from-cache <path> or --from-cache (default path)
|
|
69
86
|
const next = argv[i + 1];
|
|
@@ -204,7 +221,7 @@ const KEV_SOURCE = {
|
|
|
204
221
|
}
|
|
205
222
|
ctx.cveCatalog._meta = ctx.cveCatalog._meta || {};
|
|
206
223
|
ctx.cveCatalog._meta.last_updated = TODAY;
|
|
207
|
-
writeJson(ABS("data/cve-catalog.json"), ctx.cveCatalog);
|
|
224
|
+
writeJson(ctx.cvePath || ABS("data/cve-catalog.json"), ctx.cveCatalog);
|
|
208
225
|
return { updated: updated + added, added, drift_updated: updated, errors };
|
|
209
226
|
},
|
|
210
227
|
};
|
|
@@ -279,7 +296,7 @@ const EPSS_SOURCE = {
|
|
|
279
296
|
}
|
|
280
297
|
ctx.cveCatalog._meta = ctx.cveCatalog._meta || {};
|
|
281
298
|
ctx.cveCatalog._meta.last_updated = TODAY;
|
|
282
|
-
writeJson(ABS("data/cve-catalog.json"), ctx.cveCatalog);
|
|
299
|
+
writeJson(ctx.cvePath || ABS("data/cve-catalog.json"), ctx.cveCatalog);
|
|
283
300
|
return { updated, errors };
|
|
284
301
|
},
|
|
285
302
|
};
|
|
@@ -324,7 +341,7 @@ const NVD_SOURCE = {
|
|
|
324
341
|
}
|
|
325
342
|
ctx.cveCatalog._meta = ctx.cveCatalog._meta || {};
|
|
326
343
|
ctx.cveCatalog._meta.last_updated = TODAY;
|
|
327
|
-
writeJson(ABS("data/cve-catalog.json"), ctx.cveCatalog);
|
|
344
|
+
writeJson(ctx.cvePath || ABS("data/cve-catalog.json"), ctx.cveCatalog);
|
|
328
345
|
return { updated, errors };
|
|
329
346
|
},
|
|
330
347
|
};
|
|
@@ -652,17 +669,20 @@ function rfcDiffFromCache(ctx) {
|
|
|
652
669
|
|
|
653
670
|
function pinsDiffFromCache(ctx) {
|
|
654
671
|
// Cache layout under pins/: <owner>__<repo>__releases.json arrays.
|
|
672
|
+
// Only repos that publish via GitHub Releases live here — D3FEND and CWE
|
|
673
|
+
// were removed in the same pass that pruned them from lib/prefetch.js's
|
|
674
|
+
// SOURCES.pins (neither project tags releases on GitHub; D3FEND ships
|
|
675
|
+
// the ontology from d3fend/d3fend-ontology without tagged releases,
|
|
676
|
+
// and CWE distributes XML from cwe.mitre.org). Pin currency for those
|
|
677
|
+
// two frameworks is monitored via lib/upstream-check.js against their
|
|
678
|
+
// canonical mitre.org endpoints, not through the prefetch cache.
|
|
655
679
|
const PIN_REPOS = {
|
|
656
680
|
atlas_version: "mitre-atlas__atlas-data__releases",
|
|
657
681
|
attack_version: "mitre-attack__attack-stix-data__releases",
|
|
658
|
-
d3fend_version: "d3fend__d3fend-data__releases",
|
|
659
|
-
cwe_version: "mitre__cwe__releases",
|
|
660
682
|
};
|
|
661
683
|
const localOf = {
|
|
662
684
|
atlas_version: ctx.manifest.atlas_version,
|
|
663
685
|
attack_version: ctx.manifest.attack_version,
|
|
664
|
-
d3fend_version: ctx.d3fendCatalog?._meta?.version || ctx.d3fendCatalog?._meta?.d3fend_version || null,
|
|
665
|
-
cwe_version: ctx.cweCatalog?._meta?.version || ctx.cweCatalog?._meta?.cwe_version || null,
|
|
666
686
|
};
|
|
667
687
|
const diffs = [];
|
|
668
688
|
let errors = 0;
|
|
@@ -714,9 +734,11 @@ function synthesizeFromFixture(ctx, sourceName) {
|
|
|
714
734
|
// --- IO helpers --------------------------------------------------------
|
|
715
735
|
|
|
716
736
|
function loadCtx(opts) {
|
|
737
|
+
const cvePath = resolveCatalogPath(opts);
|
|
717
738
|
const ctx = {
|
|
718
739
|
manifest: JSON.parse(fs.readFileSync(ABS("manifest.json"), "utf8")),
|
|
719
|
-
|
|
740
|
+
cvePath, // remember the resolved path; applyDiff callbacks write through it
|
|
741
|
+
cveCatalog: JSON.parse(fs.readFileSync(cvePath, "utf8")),
|
|
720
742
|
rfcCatalog: JSON.parse(fs.readFileSync(ABS("data/rfc-references.json"), "utf8")),
|
|
721
743
|
cweCatalog: JSON.parse(fs.readFileSync(ABS("data/cwe-catalog.json"), "utf8")),
|
|
722
744
|
d3fendCatalog: JSON.parse(fs.readFileSync(ABS("data/d3fend-catalog.json"), "utf8")),
|
|
@@ -828,7 +850,8 @@ async function seedSingleAdvisory(opts) {
|
|
|
828
850
|
}
|
|
829
851
|
|
|
830
852
|
// Apply: write to cve-catalog.json with the _auto_imported flag.
|
|
831
|
-
|
|
853
|
+
// v0.12.8: honor --catalog / EXCEPTD_CVE_CATALOG so tests can redirect.
|
|
854
|
+
const catalogPath = resolveCatalogPath(opts);
|
|
832
855
|
const catalog = JSON.parse(fs.readFileSync(catalogPath, "utf8"));
|
|
833
856
|
if (catalog[cveId] && !catalog[cveId]._auto_imported && !catalog[cveId]._draft) {
|
|
834
857
|
// Refuse to overwrite a human-curated entry.
|
|
@@ -71,9 +71,9 @@
|
|
|
71
71
|
"type": "array",
|
|
72
72
|
"items": {
|
|
73
73
|
"type": "string",
|
|
74
|
-
"pattern": "^(RFC-[0-9]+|DRAFT-[A-Z0-9-]+)$"
|
|
74
|
+
"pattern": "^(RFC-[0-9]+|DRAFT-[A-Z0-9-]+|ISO-[0-9]+|CSAF-[0-9]+\\.[0-9]+)$"
|
|
75
75
|
},
|
|
76
|
-
"description": "Optional. IETF RFC numbers (e.g. RFC-8446)
|
|
76
|
+
"description": "Optional. IETF RFC numbers (e.g. RFC-8446), Internet-Draft slugs (e.g. DRAFT-IETF-TLS-ECDHE-MLKEM), ISO standards (e.g. ISO-29147), or OASIS CSAF advisory format (CSAF-2.0). Each must resolve in data/rfc-references.json."
|
|
77
77
|
},
|
|
78
78
|
"cwe_refs": {
|
|
79
79
|
"type": "array",
|
package/manifest-snapshot.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Auto-generated by scripts/refresh-manifest-snapshot.js — do not hand-edit. Public skill surface used by check-manifest-snapshot.js to detect breaking removals.",
|
|
3
|
-
"_generated_at": "2026-05-
|
|
3
|
+
"_generated_at": "2026-05-13T15:30:27.029Z",
|
|
4
4
|
"atlas_version": "5.1.0",
|
|
5
5
|
"skill_count": 38,
|
|
6
6
|
"skills": [
|