@blamejs/exceptd-skills 0.14.13 → 0.14.15

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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.15 — 2026-05-27
4
+
5
+ Emitted CSAF 2.0 and SARIF 2.1.0 documents now pass strict schema/profile validation:
6
+ - Every CSAF vulnerability carries `notes` (CVE-keyed entries previously omitted it, failing the security-advisory profile's mandatory test).
7
+ - A clean run's `csaf_informational_advisory` no longer carries a `/vulnerabilities` array or a `/product_tree` (both are wrong for the informational profile) and now includes the required external `/document/references` entry.
8
+ - `tracking.version` equals the last `revision_history` number and uses the same versioning scheme as it (previously the version was the playbook semver while the revision number was the integer `1` — two violations: a version/revision mismatch and mixed versioning schemes).
9
+ - SARIF results with `kind: "informational"` (framework-gap findings) now use `level: "none"` instead of `"note"`; the SARIF spec requires `level: "none"` whenever `kind` is not `"fail"`, so strict validators and GitHub code scanning previously rejected those results.
10
+ - SARIF `artifactLocation.uri` values from a submission-supplied evidence location are normalized to forward slashes. A Windows operator passing a native backslash path previously produced URIs that violate the SARIF URI-reference requirement (the collector-derived locations were already normalized; submission-threaded ones were not).
11
+
12
+ ## 0.14.14 — 2026-05-27
13
+
14
+ Attestation durability and verification:
15
+ - Attestations are now written atomically. The body and its Ed25519 `.sig` sidecar are written to fsync'd temporary files and placed together (the body via an atomic create that still detects a session-id collision, the sidecar alongside it), so a crash or out-of-space mid-write can no longer leave a truncated `attestation.json` or a body without its signature. A failed write also leaves no partial file at the slot.
16
+ - `attest verify` now flags a deleted `.sig` sidecar as tampering (exit 6) when a signature was expected — i.e. when a signing key is present or a sibling attestation in the same session is signed — instead of accepting it as a benign "unsigned" attestation (exit 0). This makes the default `attest verify` agree with `reattest`, which already refused. A genuinely unsigned attestation on a keyless host stays benign.
17
+ - A `run` now blocks (`blocked_by: "mutex"`) when a live concurrent process holds the run lock, rather than proceeding without the lock after losing the acquire race. Same-process reentrancy and filesystem quirks are unaffected.
18
+
3
19
  ## 0.14.13 — 2026-05-27
4
20
 
5
21
  Security: a collector scanning a hostile repository no longer hangs on a crafted file. Three workflow/Dockerfile/manifest scanners (`library-author`, `cicd-pipeline-compromise`, `containers`) had a regex that backtracked catastrophically on a long whitespace line — a single planted file could wedge the scan for minutes. The regexes are fixed and a per-line length cap bounds any future regression.
package/bin/exceptd.js CHANGED
@@ -4564,23 +4564,68 @@ function persistAttestation(args) {
4564
4564
  prior_evidence_hash: priorEvidenceHash,
4565
4565
  prior_captured_at: priorCapturedAt,
4566
4566
  };
4567
- // Atomic-create via O_EXCL ('wx' flag) eliminates the TOCTOU window
4568
- // between existsSync and writeFileSync. Two concurrent run-with-same-
4569
- // session-id invocations now produce one winner + one EEXIST loser,
4570
- // not silent last-write-wins.
4567
+ // Atomic write: the body and its .sig are written to fsync'd tmp files,
4568
+ // then placed with linkSync (create) / rename (force-overwrite) so a
4569
+ // crash mid-write can never leave a TRUNCATED attestation.json, and the
4570
+ // body never appears partially written. linkSync preserves the O_EXCL
4571
+ // collision guarantee the old "wx" flag gave: it throws EEXIST when the
4572
+ // slot is taken (one winner + one EEXIST loser on concurrent same-
4573
+ // session-id runs), and the placed file has the full content instantly.
4571
4574
  //
4572
- // v0.12.38 (cycle 18 P1 F2): mode 0o600 + Windows ACL hardening.
4573
- // Pre-fix attestations were written world-readable (umask-derived
4574
- // 0o644). On multi-tenant shared hosts a different user could read
4575
- // the operator's evidence submission, jurisdiction obligations,
4576
- // and consent records. Mirrors the existing private-key handling
4577
- // in lib/sign.js (mode 0o600 + restrictWindowsAcl).
4578
- fs.writeFileSync(filePath, JSON.stringify(attestation, null, 2), { flag, mode: 0o600 });
4575
+ // v0.12.38: mode 0o600 + Windows ACL hardening — attestations carry the
4576
+ // operator's evidence, jurisdiction obligations, and consent records and
4577
+ // must not be world-readable on multi-tenant hosts.
4578
+ const crypto = require("crypto");
4579
+ const jsonStr = JSON.stringify(attestation, null, 2);
4580
+ const sigPath = filePath + ".sig";
4581
+ const suffix = `.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`;
4582
+ const jsonTmp = filePath + suffix;
4583
+ const sigTmp = sigPath + suffix;
4584
+ const writeFsync = (p, data) => {
4585
+ const fd = fs.openSync(p, "w", 0o600);
4586
+ try { fs.writeFileSync(fd, data); fs.fsyncSync(fd); }
4587
+ finally { fs.closeSync(fd); }
4588
+ };
4589
+ // Sidecar is computed over the SAME normalized bytes that will land, so
4590
+ // the sig always matches the placed body.
4591
+ const sidecarBytes = computeSidecarBytes(normalizeAttestationBytes(jsonStr));
4592
+ writeFsync(jsonTmp, jsonStr);
4593
+ writeFsync(sigTmp, sidecarBytes);
4594
+ try {
4595
+ if (flag === "wx") {
4596
+ // Atomic create + collision detection.
4597
+ try {
4598
+ fs.linkSync(jsonTmp, filePath);
4599
+ } catch (linkErr) {
4600
+ if (linkErr.code === "EEXIST") throw linkErr; // collision — outer handler decides
4601
+ // Filesystems without hard-link support (EPERM/EXDEV/ENOSYS): fall
4602
+ // back to an existsSync collision check + atomic rename. Narrow
4603
+ // TOCTOU window, only on such filesystems.
4604
+ if (fs.existsSync(filePath)) { const e = new Error("EEXIST"); e.code = "EEXIST"; throw e; }
4605
+ fs.renameSync(jsonTmp, filePath);
4606
+ }
4607
+ // Slot won — place the sidecar (sigPath is fresh on a create).
4608
+ fs.renameSync(sigTmp, sigPath);
4609
+ try { fs.unlinkSync(jsonTmp); } catch { /* hard-link path leaves a second name */ }
4610
+ } else {
4611
+ // Force-overwrite, under the persist lock: atomic replace of both.
4612
+ // Both tmps are fully written + fsync'd, so the new body and its
4613
+ // matching new sidecar are placed back-to-back.
4614
+ fs.renameSync(jsonTmp, filePath);
4615
+ fs.renameSync(sigTmp, sigPath);
4616
+ }
4617
+ } catch (placeErr) {
4618
+ // Clean up tmps on any placement failure (incl. EEXIST collision) so
4619
+ // a failed/refused write never leaves orphan tmp files at the slot.
4620
+ try { fs.unlinkSync(jsonTmp); } catch { /* may already be linked/renamed */ }
4621
+ try { fs.unlinkSync(sigTmp); } catch { /* may already be renamed */ }
4622
+ throw placeErr;
4623
+ }
4579
4624
  try {
4580
4625
  const { restrictWindowsAcl } = require(path.join(PKG_ROOT, "lib", "sign.js"));
4581
4626
  restrictWindowsAcl(filePath);
4627
+ restrictWindowsAcl(sigPath);
4582
4628
  } catch { /* sign.js not loadable in some test paths — best-effort */ }
4583
- maybeSignAttestation(filePath);
4584
4629
  };
4585
4630
 
4586
4631
  try {
@@ -4753,40 +4798,18 @@ function normalizeAttestationBytes(input) {
4753
4798
  return s.replace(/\r\n/g, "\n");
4754
4799
  }
4755
4800
 
4756
- function maybeSignAttestation(filePath) {
4801
+ // Compute the `.sig` sidecar bytes for an attestation's (already-normalized)
4802
+ // content. Pure — does NOT write any file; the persist path writes the
4803
+ // returned string to a tmp and atomically renames it into place alongside the
4804
+ // attestation body, so the body never lands without its sidecar bytes ready.
4805
+ // Emits the one-time-per-process unsigned warning.
4806
+ function computeSidecarBytes(contentNormalized) {
4757
4807
  const crypto = require("crypto");
4758
- const sigPath = filePath + ".sig";
4759
- // v0.12.9 (P2 #3 from production smoke + codex P1 PR #4 review): keep the
4760
- // sign key aligned with the VERIFY key. `attest verify` checks signatures
4761
- // against PKG_ROOT/keys/public.pem; if we sign with cwd/.keys/private.pem
4762
- // (e.g. the maintainer's repo-local keypair) the resulting `.sig` will
4763
- // verify INVALID and report a false tamper signal on every freshly-written
4764
- // attestation. PKG_ROOT-only resolution is the right answer; the original
4765
- // smoke report's "doctor finds key, run does not" gap is fixed in `doctor`
4766
- // (reporting only PKG_ROOT now), not by making `run` follow a cwd key the
4767
- // verifier doesn't trust.
4808
+ // v0.12.9: keep the sign key aligned with the VERIFY key. `attest verify`
4809
+ // checks signatures against PKG_ROOT/keys/public.pem; signing with a
4810
+ // cwd-local key would verify INVALID. PKG_ROOT-only resolution is correct.
4768
4811
  const privKeyPath = path.join(PKG_ROOT, ".keys", "private.pem");
4769
- // Normalize attestation bytes before sign — strip leading UTF-8 BOM +
4770
- // collapse CRLF to LF. Mirrors lib/sign.js / lib/verify.js /
4771
- // lib/refresh-network.js / scripts/verify-shipped-tarball.js. The
4772
- // attestation file lives on disk under .exceptd/ and can pick up CRLF
4773
- // through git-attribute / editor round-trips on Windows; without
4774
- // normalization the sign/verify pair diverges on the same logical content.
4775
- // The byte-stability contract spans five sites; tests/normalize-contract
4776
- // .test.js enforces byte-identical output across all of them.
4777
- const rawContent = fs.readFileSync(filePath, "utf8");
4778
- const content = normalizeAttestationBytes(rawContent);
4779
4812
  // One-time-per-process unsigned warning so cron jobs don't spam stderr.
4780
- // Operators who set `.keys/private.pem` get tamper-evident attestations;
4781
- // operators without the keypair get a single nudge per session telling them
4782
- // exactly how to enable signing.
4783
- //
4784
- // Consumer installs (`npm install -g`) land PKG_ROOT under
4785
- // node_modules/ where the operator typically can't write to
4786
- // .keys/. For those installs the multi-line nudge prescribes a
4787
- // remediation (doctor --fix) the operator doesn't own — surface a
4788
- // single-line marker so the unsigned attestation isn't silent, but
4789
- // skip the call-to-action that doesn't apply.
4790
4813
  if (!fs.existsSync(privKeyPath) && !process.env.EXCEPTD_UNSIGNED_WARNED) {
4791
4814
  const pkgRootSegments = PKG_ROOT.split(/[\\/]/);
4792
4815
  const isConsumerInstall =
@@ -4807,37 +4830,45 @@ function maybeSignAttestation(filePath) {
4807
4830
  try {
4808
4831
  if (fs.existsSync(privKeyPath)) {
4809
4832
  const privateKey = fs.readFileSync(privKeyPath, "utf8");
4810
- const sig = crypto.sign(null, Buffer.from(content, "utf8"), {
4833
+ const sig = crypto.sign(null, Buffer.from(contentNormalized, "utf8"), {
4811
4834
  key: privateKey,
4812
4835
  dsaEncoding: "ieee-p1363",
4813
4836
  });
4814
- // The sidecar's Ed25519 signature covers ONLY the attestation file
4815
- // bytes. Fields that travel inside the .sig but are NOT in the signed
4816
- // message are replay-rewrite trivial: an attacker who can write the
4817
- // directory can mutate them without invalidating the signature. The
4818
- // sidecar therefore carries only the algorithm tag, the Ed25519
4819
- // signature payload, and an explanatory note — no `signed_at`,
4820
- // `signs_path`, or `signs_sha256`. Operators reading freshness use
4821
- // filesystem mtime; the attestation file's `captured_at` field is
4822
- // what's signed.
4823
- fs.writeFileSync(sigPath, JSON.stringify({
4837
+ // The Ed25519 signature covers ONLY the attestation file bytes; no
4838
+ // replay-rewritable metadata travels in the sidecar.
4839
+ return JSON.stringify({
4824
4840
  algorithm: "Ed25519",
4825
4841
  signature_base64: sig.toString("base64"),
4826
4842
  note: "Ed25519 signature covers the attestation file bytes only. Use filesystem mtime for freshness; use the attestation's `captured_at` for the signed timestamp.",
4827
- }, null, 2), { mode: 0o600 });
4828
- // Mirror the v0.12.38 attestation.json hardening: 0o600 on POSIX +
4829
- // icacls inheritance strip on win32. The sidecar carries the
4830
- // signature payload; multi-tenant hosts shouldn't leak it.
4831
- try { require("./../lib/sign.js").restrictWindowsAcl(sigPath); } catch { /* best-effort */ }
4832
- } else {
4833
- fs.writeFileSync(sigPath, JSON.stringify({
4834
- algorithm: "unsigned",
4835
- signed: false,
4836
- note: "No private key at .keys/private.pem — attestation is hash-stable but unsigned. Run `exceptd doctor --fix` to enable signing.",
4837
- }, null, 2), { mode: 0o600 });
4838
- try { require("./../lib/sign.js").restrictWindowsAcl(sigPath); } catch { /* best-effort */ }
4843
+ }, null, 2);
4839
4844
  }
4840
- } catch { /* non-fatal — signing failure shouldn't block the run */ }
4845
+ return JSON.stringify({
4846
+ algorithm: "unsigned",
4847
+ signed: false,
4848
+ note: "No private key at .keys/private.pem — attestation is hash-stable but unsigned. Run `exceptd doctor --fix` to enable signing.",
4849
+ }, null, 2);
4850
+ } catch {
4851
+ // Signing failure must not block the run — fall back to an unsigned marker
4852
+ // so the sidecar always exists alongside the body.
4853
+ return JSON.stringify({
4854
+ algorithm: "unsigned",
4855
+ signed: false,
4856
+ note: "Signing failed at write time; attestation is hash-stable but unsigned.",
4857
+ }, null, 2);
4858
+ }
4859
+ }
4860
+
4861
+ // Sign an already-written file in place by computing + writing its `.sig`
4862
+ // sidecar. The main attestation-persist path writes the sidecar atomically
4863
+ // alongside the body (via computeSidecarBytes); this helper serves the
4864
+ // replay-record path, which writes a uniquely-named file and so needs no
4865
+ // atomic-collision handling. Best-effort: a sign-time failure leaves the
4866
+ // record unsigned (still a valid audit entry) rather than aborting.
4867
+ function maybeSignAttestation(filePath) {
4868
+ const content = normalizeAttestationBytes(fs.readFileSync(filePath, "utf8"));
4869
+ const sidecar = computeSidecarBytes(content);
4870
+ fs.writeFileSync(filePath + ".sig", sidecar, { mode: 0o600 });
4871
+ try { require(path.join(PKG_ROOT, "lib", "sign.js")).restrictWindowsAcl(filePath + ".sig"); } catch { /* best-effort */ }
4841
4872
  }
4842
4873
 
4843
4874
  /**
@@ -5793,6 +5824,22 @@ function cmdAttest(runner, args, runOpts, pretty) {
5793
5824
  // tampered attestation.json and overwrote .sig with the unsigned stub).
5794
5825
  const privKeyPath = path.join(PKG_ROOT, ".keys", "private.pem");
5795
5826
  const hasPrivKey = fs.existsSync(privKeyPath);
5827
+ // Does any sidecar in this session dir carry a real Ed25519 signature? If
5828
+ // so, a sibling attestation with NO sidecar is suspicious (a sig was
5829
+ // expected). Combined with hasPrivKey, this lets default `attest verify`
5830
+ // treat a deleted sidecar as tamper — agreeing with `reattest`, which
5831
+ // already refuses. The keyless case (no key, all-unsigned peers) stays
5832
+ // benign so keyless CI is unaffected.
5833
+ let anyPeerEd25519Signed = false;
5834
+ try {
5835
+ for (const sf of fs.readdirSync(dir)) {
5836
+ if (!sf.endsWith(".sig")) continue;
5837
+ try {
5838
+ const sd = JSON.parse(fs.readFileSync(path.join(dir, sf), "utf8"));
5839
+ if (sd && sd.algorithm === "Ed25519") { anyPeerEd25519Signed = true; break; }
5840
+ } catch { /* skip unparseable sidecar */ }
5841
+ }
5842
+ } catch { /* dir unreadable — fall through */ }
5796
5843
 
5797
5844
  // Sidecar-verify helper shared by both the attestations[] and
5798
5845
  // replay-records[] partitions. Centralising the per-file verify
@@ -5800,7 +5847,16 @@ function cmdAttest(runner, args, runOpts, pretty) {
5800
5847
  // instead of two parallel branches.
5801
5848
  const verifySidecar = (f) => {
5802
5849
  const sigPath = path.join(dir, f + ".sig");
5803
- if (!fs.existsSync(sigPath)) return { file: f, signed: false, verified: false, reason: "no .sig sidecar" };
5850
+ if (!fs.existsSync(sigPath)) {
5851
+ // A missing sidecar is benign ONLY when none was ever expected (the
5852
+ // attestation was written on a keyless host and no peer is signed).
5853
+ // When a sig SHOULD exist, an absent one is a deletion-to-evade-tamper
5854
+ // signal — flag it so default verify matches reattest's refusal.
5855
+ if (hasPrivKey || anyPeerEd25519Signed) {
5856
+ return { file: f, signed: false, verified: false, reason: "no .sig sidecar, but one was expected (signing key present or a signed peer attestation exists) — sidecar deletion suspected", tamper_class: "sidecar-missing" };
5857
+ }
5858
+ return { file: f, signed: false, verified: false, reason: "no .sig sidecar" };
5859
+ }
5804
5860
  let sigDoc;
5805
5861
  try { sigDoc = JSON.parse(fs.readFileSync(sigPath, "utf8")); }
5806
5862
  catch (e) {
@@ -5876,7 +5932,8 @@ function cmdAttest(runner, args, runOpts, pretty) {
5876
5932
  (r.signed && !r.verified)
5877
5933
  || r.tamper_class === "sidecar-corrupt"
5878
5934
  || r.tamper_class === "unsigned-substitution"
5879
- || r.tamper_class === "algorithm-unsupported";
5935
+ || r.tamper_class === "algorithm-unsupported"
5936
+ || r.tamper_class === "sidecar-missing";
5880
5937
  const attTampered = attResults.some(tamperPredicate);
5881
5938
  const replayTampered = replayResults.some(tamperPredicate);
5882
5939
 
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-27T22:17:04.450Z",
3
+ "generated_at": "2026-05-28T00:02:12.970Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "a1f0cb852fd487d12bd1a304d36eb175f3ee36a26e37a1ca9cb25d9c576d2afc",
7
+ "manifest.json": "22b49842257d49e0386c45311639f8e81f671ead4a30b2ca8f14d316731e36bf",
8
8
  "data/atlas-ttps.json": "d24bc02859d40ccf1615db75cca68c077585904e41e0d8f6de448121e9b1abb0",
9
9
  "data/attack-techniques.json": "fa193f0d2d248176a8beddb641e9fe56ba4faa9e15dc253ff876dbf0c5d58a77",
10
10
  "data/cve-catalog.json": "3d451dda7ac0c7d57a4075ae4bafd3148c6184b35dc1bc59d8b81d1f2641e430",
@@ -2059,10 +2059,15 @@ function sarifLocationsForIndicator(playbook, indicator) {
2059
2059
  if (ev && ev.length) {
2060
2060
  const locs = [];
2061
2061
  for (const e of ev) {
2062
+ // SARIF artifactLocation.uri is a URI reference (RFC 3986) — the path
2063
+ // separator must be `/`. Operator/agent-supplied evidence on Windows
2064
+ // arrives with backslashes; normalize them so the emitted SARIF is
2065
+ // valid (the collectors already normalize via buildEvidenceLocations,
2066
+ // but submission-threaded locations bypass that path).
2062
2067
  if (typeof e === "string" && e.trim()) {
2063
- locs.push({ physicalLocation: { artifactLocation: { uri: e.trim() } } });
2068
+ locs.push({ physicalLocation: { artifactLocation: { uri: e.trim().replace(/\\/g, "/") } } });
2064
2069
  } else if (e && typeof e === "object" && typeof e.uri === "string" && e.uri.trim()) {
2065
- const pl = { artifactLocation: { uri: e.uri.trim() } };
2070
+ const pl = { artifactLocation: { uri: e.uri.trim().replace(/\\/g, "/") } };
2066
2071
  if (Number.isInteger(e.startLine) && e.startLine > 0) {
2067
2072
  pl.region = { startLine: e.startLine, ...(Number.isInteger(e.endLine) && e.endLine >= e.startLine ? { endLine: e.endLine } : {}) };
2068
2073
  }
@@ -2381,6 +2386,14 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals,
2381
2386
  }
2382
2387
  }] : [];
2383
2388
  const base = {
2389
+ // CSAF Profile 4 (security_advisory) mandatory test 6.1.27.5 requires
2390
+ // every /vulnerabilities[] item to carry `notes`. Without this the
2391
+ // CVE-keyed entries failed strict validation (the indicator pseudo-CVE
2392
+ // entries already carried notes; real-CVE entries did not).
2393
+ notes: [{
2394
+ category: 'description',
2395
+ text: `${c.cve_id}: RWEP ${c.rwep}${c.active_exploitation ? `, active_exploitation=${c.active_exploitation}` : ''}${c.cisa_kev ? ', CISA KEV' : ''}.`,
2396
+ }],
2384
2397
  scores,
2385
2398
  threats: c.active_exploitation === 'confirmed' ? [{ category: 'exploit_status', details: `Active exploitation confirmed${c.cisa_kev ? ' (CISA KEV)' : ''}.` }] : [],
2386
2399
  remediations,
@@ -2526,6 +2539,13 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals,
2526
2539
  publisher: publisherBlock,
2527
2540
  title: `exceptd finding: ${playbook.domain.name} (${analyze.matched_cves.length} CVE(s), ${indicatorHits.length} indicator hit(s), ${(analyze.framework_gap_mapping || []).length} framework gap(s))`,
2528
2541
  notes: [...namespaceFallbackNote, ...gapNotes],
2542
+ // Profile 3 (csaf_informational_advisory) mandatory test 6.1.27.2
2543
+ // requires /document/references with at least one `external` item. A
2544
+ // security_advisory carries its references inside the vulnerabilities,
2545
+ // so add this only for the informational (clean-run) profile.
2546
+ ...(csafCategory === 'csaf_informational_advisory' ? {
2547
+ references: [{ category: 'external', summary: `exceptd playbook: ${playbook._meta.id}`, url: `https://exceptd.com/playbooks/${playbook._meta.id}` }],
2548
+ } : {}),
2529
2549
  ...(csafDistribution ? { distribution: csafDistribution } : {}),
2530
2550
  tracking: {
2531
2551
  // F2/F9: CSAF tracking.id binds to the run's session_id (threaded
@@ -2545,35 +2565,42 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals,
2545
2565
  },
2546
2566
  initial_release_date: now,
2547
2567
  current_release_date: now,
2548
- revision_history: [{ number: '1', date: now, summary: 'Initial finding emission' }]
2568
+ // CSAF 6.1.30 requires homogeneous versioning (all version_t use the
2569
+ // same scheme) and 6.1.16 requires tracking.version === the last
2570
+ // revision_history number. tracking.version is the playbook semver,
2571
+ // so the single revision entry must carry that same semver (not the
2572
+ // integer "1", which mixed schemes AND mismatched the version).
2573
+ revision_history: [{ number: playbook._meta.version, date: now, summary: 'Initial finding emission' }]
2549
2574
  }
2550
2575
  },
2551
- product_tree: (function () {
2552
- // Synthesize a 3-level branches tree (vendor product → version)
2553
- // from catalog data. CSAF §3.1.5.1 makes branches[] strongly
2554
- // recommended for csaf_security_advisory documents because NVD /
2555
- // ENISA / Red Hat dashboards render the affected-product list off
2556
- // the branches tree, not full_product_names[]. The pre-fix tree
2557
- // emitted only the synthetic exceptd-target product and operators
2558
- // browsing the rendered advisory saw no real-world vendor surface.
2559
- const { branches } = buildCsafBranches(analyze.matched_cves || [], runOpts);
2560
- const tree = { full_product_names: fullProductNames };
2561
- if (branches.length > 0) tree.branches = branches;
2562
- return tree;
2563
- })(),
2564
- vulnerabilities: (function () {
2565
- // v0.12.27: deterministic mode sorts vulnerabilities[] by their
2566
- // primary identifier (cve_id for CVE entries, ids[0].text otherwise)
2567
- // ascending. Default mode preserves insertion order so existing
2568
- // operators see byte-identical output to pre-v0.12.27.
2569
- const all = [...cveVulns, ...indicatorVulns];
2570
- if (runOpts && runOpts.bundleDeterministic === true) {
2571
- const keyOf = (v) => (typeof v.cve === 'string' && v.cve)
2572
- || (Array.isArray(v.ids) && v.ids[0] && typeof v.ids[0].text === 'string' ? v.ids[0].text : '');
2573
- return all.slice().sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
2574
- }
2575
- return all;
2576
- })(),
2576
+ // CSAF Profile 3 (informational_advisory) forbids /vulnerabilities
2577
+ // (mandatory test 6.1.27.3) and an informational advisory carrying a
2578
+ // /product_tree is misleading (§4.3 a reader must assume every named
2579
+ // product is affected). Emit both ONLY for the security_advisory profile;
2580
+ // a clean run's informational advisory carries neither.
2581
+ ...(csafCategory === 'csaf_security_advisory' ? {
2582
+ product_tree: (function () {
2583
+ // Synthesize a 3-level branches tree (vendor product → version)
2584
+ // from catalog data. CSAF §3.1.5.1 makes branches[] strongly
2585
+ // recommended because NVD / ENISA / Red Hat dashboards render the
2586
+ // affected-product list off the branches tree, not full_product_names[].
2587
+ const { branches } = buildCsafBranches(analyze.matched_cves || [], runOpts);
2588
+ const tree = { full_product_names: fullProductNames };
2589
+ if (branches.length > 0) tree.branches = branches;
2590
+ return tree;
2591
+ })(),
2592
+ vulnerabilities: (function () {
2593
+ // v0.12.27: deterministic mode sorts vulnerabilities[] by their
2594
+ // primary identifier ascending; default mode preserves insertion order.
2595
+ const all = [...cveVulns, ...indicatorVulns];
2596
+ if (runOpts && runOpts.bundleDeterministic === true) {
2597
+ const keyOf = (v) => (typeof v.cve === 'string' && v.cve)
2598
+ || (Array.isArray(v.ids) && v.ids[0] && typeof v.ids[0].text === 'string' ? v.ids[0].text : '');
2599
+ return all.slice().sort((a, b) => keyOf(a).localeCompare(keyOf(b)));
2600
+ }
2601
+ return all;
2602
+ })(),
2603
+ } : {}),
2577
2604
  exceptd_extension: {
2578
2605
  classification: analyze._detect_classification,
2579
2606
  rwep: analyze.rwep,
@@ -2652,9 +2679,12 @@ function buildEvidenceBundle(format, playbook, analyze, validate, agentSignals,
2652
2679
  const gapResults = (analyze.framework_gap_mapping || []).map((g, idx) => ({
2653
2680
  ruleId: `${rulePrefix}framework-gap-${idx}`,
2654
2681
  // Framework gaps are control-design observations, not vulnerabilities —
2655
- // SARIF §3.27.9 `kind: informational` routes them appropriately.
2682
+ // SARIF §3.27.9 `kind: informational` routes them appropriately. That
2683
+ // same clause requires: when kind !== 'fail', a present `level` SHALL be
2684
+ // 'none'. Pairing 'informational' with 'note' is a schema violation that
2685
+ // strict validators / GitHub code-scanning reject — use 'none'.
2656
2686
  kind: 'informational',
2657
- level: 'note',
2687
+ level: 'none',
2658
2688
  message: { text: `${g.framework}: ${g.claimed_control} — ${g.actual_gap}${g.required_control ? '. Required: ' + g.required_control : ''}` },
2659
2689
  properties: stripNulls({ kind: 'framework_gap', framework: g.framework, control: g.claimed_control }),
2660
2690
  }));
@@ -3185,10 +3215,38 @@ function run(playbookId, directiveId, agentSubmission = {}, runOpts = {}) {
3185
3215
  };
3186
3216
  }
3187
3217
 
3218
+ // Cross-process mutex lock for this run. Use the diagnostic variant so a
3219
+ // CONFIRMED live foreign holder BLOCKS the run. The bare acquireLock returned
3220
+ // null on a lost race (the TOCTOU window between preflight's check and the
3221
+ // acquire) and the run then proceeded UNLOCKED, defeating the mutex. Block
3222
+ // only on `held_by_live_pid` — same-PID reentrancy and FS quirks still
3223
+ // proceed best-effort (lockPath null), so no legitimate nested/edge case
3224
+ // regresses. Released in the finally block.
3225
+ const lockResult = acquireLockDiagnostic(playbookId);
3226
+ // Block ONLY on a confirmed live foreign holder with a real numeric pid.
3227
+ // acquireLockDiagnostic also returns reason:'held_by_live_pid' with
3228
+ // holder_pid:null for a MALFORMED/truncated lockfile (it can't parse a pid to
3229
+ // probe). Blocking on that would let one corrupted lockfile left by a crash
3230
+ // deny every future run forever. A null-pid (malformed) lock instead falls
3231
+ // through to proceed best-effort (lockPath null) — matching how the preflight
3232
+ // mutex check treats unparsable lockfiles as stale.
3233
+ if (!lockResult.ok && lockResult.reason === 'held_by_live_pid'
3234
+ && Number.isInteger(lockResult.holder_pid) && lockResult.holder_pid > 0) {
3235
+ return {
3236
+ ok: false,
3237
+ playbook_id: playbookId,
3238
+ directive_id: directiveId,
3239
+ verdict: 'blocked',
3240
+ summary_line: `${playbookId}: blocked — a concurrent run holds the mutex (pid ${lockResult.holder_pid})`.slice(0, 240),
3241
+ phase: 'preflight',
3242
+ blocked_by: 'mutex',
3243
+ reason: `A concurrent run of "${playbookId}" holds the run lock (live pid ${lockResult.holder_pid}). Retry after it completes.`,
3244
+ remediation: 'Wait for the in-flight run to finish, then retry.',
3245
+ evidence_completeness: 'not-evaluated'
3246
+ };
3247
+ }
3248
+ const lockPath = lockResult.ok ? lockResult.path : null;
3188
3249
  _activeRuns.add(playbookId);
3189
- // Cross-process mutex lock for this run. preflight verified no other lock
3190
- // exists; we acquire ours and release in the finally block.
3191
- const lockPath = acquireLock(playbookId);
3192
3250
  // Parse the playbook once at run() entry and thread the parsed object
3193
3251
  // through each phase via runOpts._playbookCache. Each phase otherwise
3194
3252
  // calls loadPlaybook() independently; for a single run that's seven
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceptd-security",
3
- "version": "0.14.13",
3
+ "version": "0.14.15",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation",
5
5
  "homepage": "https://exceptd.com",
6
6
  "license": "Apache-2.0",
@@ -53,7 +53,7 @@
53
53
  ],
54
54
  "last_threat_review": "2026-05-15",
55
55
  "signature": "lXhZgoIrrVloO3XaTvo/43AxZn4mwErstd7DR0O/oVhD3AOGODM4HqrageYEou9WKOdMEGP5mJNTjJsXdP5NDA==",
56
- "signed_at": "2026-05-27T22:15:41.262Z",
56
+ "signed_at": "2026-05-28T00:00:43.112Z",
57
57
  "cwe_refs": [
58
58
  "CWE-125",
59
59
  "CWE-362",
@@ -123,7 +123,7 @@
123
123
  ],
124
124
  "last_threat_review": "2026-05-17",
125
125
  "signature": "ztSKk/zFMFbT12qRcEeBKpydBn7fTT86KxMmor0DTCoKQWk5fJ0fSInfP1XMSB6rFk4/SuSjKVxQRMKVJ5a+Cg==",
126
- "signed_at": "2026-05-27T22:15:41.263Z",
126
+ "signed_at": "2026-05-28T00:00:43.114Z",
127
127
  "cwe_refs": [
128
128
  "CWE-1039",
129
129
  "CWE-1426",
@@ -196,7 +196,7 @@
196
196
  ],
197
197
  "last_threat_review": "2026-05-17",
198
198
  "signature": "K6QdPHNK5c4K5QFjrW0QsUhjp71D7SOisSoulwPNSvKRdi2rY+yg0kdckijBMkLMsVPyUvcC9giu93mKJ1OZDg==",
199
- "signed_at": "2026-05-27T22:15:41.264Z",
199
+ "signed_at": "2026-05-28T00:00:43.115Z",
200
200
  "cwe_refs": [
201
201
  "CWE-22",
202
202
  "CWE-345",
@@ -248,7 +248,7 @@
248
248
  "framework_gaps": [],
249
249
  "last_threat_review": "2026-05-22",
250
250
  "signature": "Qd3SBWmUAaaT++e1Ry2wBIz/dCBmNBMl0+4Rb0etvJLES0fIBEAkU1mTbgNZnT5XOg9J5twdUpymWtmKnDDQCQ==",
251
- "signed_at": "2026-05-27T22:15:41.264Z"
251
+ "signed_at": "2026-05-28T00:00:43.115Z"
252
252
  },
253
253
  {
254
254
  "name": "compliance-theater",
@@ -279,7 +279,7 @@
279
279
  ],
280
280
  "last_threat_review": "2026-05-22",
281
281
  "signature": "F2Shxae0ua0gPtvwzTRVzzHaIgJcFDRT3/akLUAZ4aaMQhkleKkcTaTpkjp+pTVEdPfLeLGNCeAOMs+whVYOBg==",
282
- "signed_at": "2026-05-27T22:15:41.265Z"
282
+ "signed_at": "2026-05-28T00:00:43.115Z"
283
283
  },
284
284
  {
285
285
  "name": "exploit-scoring",
@@ -308,7 +308,7 @@
308
308
  ],
309
309
  "last_threat_review": "2026-05-18",
310
310
  "signature": "NA1hoQycvQhSUoG5rwlXX0mOVmGxoXRVezkELGEA2nZOdGis4gXkHT3O6Sfw7zxE4JuMrsCb65TEeOWk9WEPDg==",
311
- "signed_at": "2026-05-27T22:15:41.265Z"
311
+ "signed_at": "2026-05-28T00:00:43.116Z"
312
312
  },
313
313
  {
314
314
  "name": "rag-pipeline-security",
@@ -345,7 +345,7 @@
345
345
  ],
346
346
  "last_threat_review": "2026-05-22",
347
347
  "signature": "W3pS8lnaCP96TQzsJpG5d5yv5IwgaQyS4Z2Ctcz5BOJf6LbajSIgeDgTZ4f4Bhr5m4E7KsgWGjZS4x7Fwd33BQ==",
348
- "signed_at": "2026-05-27T22:15:41.265Z",
348
+ "signed_at": "2026-05-28T00:00:43.117Z",
349
349
  "cwe_refs": [
350
350
  "CWE-1395",
351
351
  "CWE-1426"
@@ -405,7 +405,7 @@
405
405
  ],
406
406
  "last_threat_review": "2026-05-17",
407
407
  "signature": "/WDGygh1Ck4yWlBWDGtEUVCqKB8d+UaJXoAoBXujtt+GAl8JbMNpaN1TvI0WkEltQ9dTxaAzSn20/eVDqv8iDQ==",
408
- "signed_at": "2026-05-27T22:15:41.266Z",
408
+ "signed_at": "2026-05-28T00:00:43.117Z",
409
409
  "d3fend_refs": [
410
410
  "D3-CA",
411
411
  "D3-CSPP",
@@ -440,7 +440,7 @@
440
440
  "framework_gaps": [],
441
441
  "last_threat_review": "2026-05-22",
442
442
  "signature": "za1NKBpy9LC91F/ESO/qhUfmvVr8GNItQOjR5OJLeHm+2dQ9HHiFWQK2eo53V/n/0uhubuggURA3yS6kJuWwBg==",
443
- "signed_at": "2026-05-27T22:15:41.266Z",
443
+ "signed_at": "2026-05-28T00:00:43.117Z",
444
444
  "cwe_refs": [
445
445
  "CWE-1188"
446
446
  ],
@@ -474,7 +474,7 @@
474
474
  "framework_gaps": [],
475
475
  "last_threat_review": "2026-05-18",
476
476
  "signature": "xiHAhhdufm9hCKU8PLiPE0MX65ej2F4OZwtlWLGLCiie9/km+Kiqbt192LcMvr94v83C98pb9wIaqFsFWft6AQ==",
477
- "signed_at": "2026-05-27T22:15:41.266Z",
477
+ "signed_at": "2026-05-28T00:00:43.118Z",
478
478
  "forward_watch": [
479
479
  "New AI attack classes as ATLAS v6 publishes",
480
480
  "Post-quantum adversary capability timeline",
@@ -513,7 +513,7 @@
513
513
  "framework_gaps": [],
514
514
  "last_threat_review": "2026-05-01",
515
515
  "signature": "oYsSk35N2Uzq7MRofACykylcVwkgPhI4luWZ14vmQT+gUKLyZiKVOUJbe1+7lGl6BYPRN0sUDQ0f7S5Eu5w2Ag==",
516
- "signed_at": "2026-05-27T22:15:41.267Z"
516
+ "signed_at": "2026-05-28T00:00:43.118Z"
517
517
  },
518
518
  {
519
519
  "name": "zeroday-gap-learn",
@@ -540,7 +540,7 @@
540
540
  "framework_gaps": [],
541
541
  "last_threat_review": "2026-05-18",
542
542
  "signature": "igRqYyU1unRFH40BsPyAR62SPrk8QZv8dPGb8S9O9EvLCNOZAzm3t+HdT/NKqzWHwrpomOzkkkyLfYI/0qTUDA==",
543
- "signed_at": "2026-05-27T22:15:41.267Z",
543
+ "signed_at": "2026-05-28T00:00:43.119Z",
544
544
  "forward_watch": [
545
545
  "New CISA KEV entries",
546
546
  "New ATLAS TTP additions in each ATLAS release",
@@ -604,7 +604,7 @@
604
604
  ],
605
605
  "last_threat_review": "2026-05-22",
606
606
  "signature": "i/17u4kJiSpcZAz7LnTyRePFugQOstQ1P4kVoe0oGf4E2/j8oIN9U9DccjUn/YHZhKWIJ2AILG/DMhvMrr3bBg==",
607
- "signed_at": "2026-05-27T22:15:41.267Z",
607
+ "signed_at": "2026-05-28T00:00:43.119Z",
608
608
  "cwe_refs": [
609
609
  "CWE-327"
610
610
  ],
@@ -652,7 +652,7 @@
652
652
  ],
653
653
  "last_threat_review": "2026-05-22",
654
654
  "signature": "QuOVaQ4E2Sl39TClbhZ7HA9XrYAyRrDL44HY3RTE7aWLue0hV2cxaBt40ALGmHS++631QGFDlZTLZI77Tr6nAA==",
655
- "signed_at": "2026-05-27T22:15:41.268Z"
655
+ "signed_at": "2026-05-28T00:00:43.119Z"
656
656
  },
657
657
  {
658
658
  "name": "security-maturity-tiers",
@@ -689,7 +689,7 @@
689
689
  ],
690
690
  "last_threat_review": "2026-05-01",
691
691
  "signature": "8Px1s2lDj10/Q6erwEQlXgUHM1+OTruUR8qAHPX7Oo3k/l69N6P9sm0PsafS9wDFtj9l5C/OiLiFgzMlMt6vBw==",
692
- "signed_at": "2026-05-27T22:15:41.268Z",
692
+ "signed_at": "2026-05-28T00:00:43.120Z",
693
693
  "cwe_refs": [
694
694
  "CWE-1188"
695
695
  ]
@@ -724,7 +724,7 @@
724
724
  "framework_gaps": [],
725
725
  "last_threat_review": "2026-05-11",
726
726
  "signature": "urRcataVWg6/utyEkSiOWoNxTL8sABRjPR7ShyDfZGnAozFph/yDktSoaPVxQDXwu9EfJE+qhUW5OYR/yJECBQ==",
727
- "signed_at": "2026-05-27T22:15:41.269Z"
727
+ "signed_at": "2026-05-28T00:00:43.120Z"
728
728
  },
729
729
  {
730
730
  "name": "attack-surface-pentest",
@@ -796,7 +796,7 @@
796
796
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — Microsoft Edge 4-bug sandbox escape by Orange Tsai (DEVCORE); forward-watch only (browser sandbox, out of current playbook scope); track Microsoft Edge security advisory and KEV add"
797
797
  ],
798
798
  "signature": "C7lv65/Ecm8JJgSKxrX5lxx0YFzKWtrIQSKp+vy50I5e8945s1JmifGUUrnQwRQhq/Pkv7EmfiH5XSO8h75bDg==",
799
- "signed_at": "2026-05-27T22:15:41.269Z"
799
+ "signed_at": "2026-05-28T00:00:43.120Z"
800
800
  },
801
801
  {
802
802
  "name": "fuzz-testing-strategy",
@@ -856,7 +856,7 @@
856
856
  "OSS-Fuzz-Gen / AI-assisted harness generation becoming the default expectation for OSS maintainers"
857
857
  ],
858
858
  "signature": "Z7ypCUnXx8JpLtgxxB6RHNi39w74AmrGY1N4ofAGCXhkuM2EaFVm1AU0dvl9UQ1bVLfHKEDGqMO/TwlIY7RABg==",
859
- "signed_at": "2026-05-27T22:15:41.269Z"
859
+ "signed_at": "2026-05-28T00:00:43.121Z"
860
860
  },
861
861
  {
862
862
  "name": "dlp-gap-analysis",
@@ -931,7 +931,7 @@
931
931
  "Quebec Law 25, India DPDPA, KSA PDPL enforcement actions naming AI-tool prompt data as in-scope personal information"
932
932
  ],
933
933
  "signature": "IgEnpHOhCftAyfUNdKsjbrd169T9pJkk/rRM2ZEna+H18y7p5x48+1kME2sJMZjJuyAdQFBJi8PJXZFwLGI+DQ==",
934
- "signed_at": "2026-05-27T22:15:41.270Z"
934
+ "signed_at": "2026-05-28T00:00:43.121Z"
935
935
  },
936
936
  {
937
937
  "name": "supply-chain-integrity",
@@ -1010,7 +1010,7 @@
1010
1010
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — NVIDIA Megatron Bridge path traversal by haehae; AI training-stack file-system trust boundary; track patch and SBOM-attestation impact"
1011
1011
  ],
1012
1012
  "signature": "pcLrM98A3vUSZRjwNAk0aZ9umvOwB41XCLLsCOy/IebB2F/06oIrGUKkMHtHwm4pTVPShMMcKdZQQ3jz30FnCg==",
1013
- "signed_at": "2026-05-27T22:15:41.270Z"
1013
+ "signed_at": "2026-05-28T00:00:43.121Z"
1014
1014
  },
1015
1015
  {
1016
1016
  "name": "defensive-countermeasure-mapping",
@@ -1067,7 +1067,7 @@
1067
1067
  ],
1068
1068
  "last_threat_review": "2026-05-11",
1069
1069
  "signature": "G5q5elh7Q7eu2xcwTVQJGDTGfvZR0OGQaLSLJPb2wjzCHFF8PWuZfCHZdjjqisiRzRWPyLlzgfHeMJqOdy7cBw==",
1070
- "signed_at": "2026-05-27T22:15:41.270Z"
1070
+ "signed_at": "2026-05-28T00:00:43.122Z"
1071
1071
  },
1072
1072
  {
1073
1073
  "name": "identity-assurance",
@@ -1134,7 +1134,7 @@
1134
1134
  "d3fend_refs": [],
1135
1135
  "last_threat_review": "2026-05-11",
1136
1136
  "signature": "Wv5hGMeHjlaQK1zwicVCA7AvdKgJBgvcjdpGM9Ywahh9tagAKhbkOjybowDQZzu7OZ3bDkbh6pBYc1Sdwr6NAA==",
1137
- "signed_at": "2026-05-27T22:15:41.271Z"
1137
+ "signed_at": "2026-05-28T00:00:43.122Z"
1138
1138
  },
1139
1139
  {
1140
1140
  "name": "ot-ics-security",
@@ -1190,7 +1190,7 @@
1190
1190
  "d3fend_refs": [],
1191
1191
  "last_threat_review": "2026-05-11",
1192
1192
  "signature": "8t5qKHd3yWi57dvG36YQkLN/X9bQWqtEiYjay4IfSmqhJpM/xXPaQVKNGz3wscrO8OLKUZ0OaX7Mj5kzpgBKBQ==",
1193
- "signed_at": "2026-05-27T22:15:41.271Z"
1193
+ "signed_at": "2026-05-28T00:00:43.122Z"
1194
1194
  },
1195
1195
  {
1196
1196
  "name": "coordinated-vuln-disclosure",
@@ -1242,7 +1242,7 @@
1242
1242
  "NYDFS 23 NYCRR 500 amendments potentially adding explicit CVD program requirements"
1243
1243
  ],
1244
1244
  "signature": "GDGt4UPqBa04PjlpSmpyihGzd3OgfBN7jaAK5tfwp+LRSs3ygKOdbeivUCCHNagTY1hE6hG2Ou40ADfBFuXeAg==",
1245
- "signed_at": "2026-05-27T22:15:41.271Z"
1245
+ "signed_at": "2026-05-28T00:00:43.123Z"
1246
1246
  },
1247
1247
  {
1248
1248
  "name": "threat-modeling-methodology",
@@ -1292,7 +1292,7 @@
1292
1292
  "PASTA v2 updates incorporating AI/ML application threats"
1293
1293
  ],
1294
1294
  "signature": "rFBpOQEJUPpl+v88Lw/WqVJRhTl80vy0VbPAbzQj3Q0suJRRrJg368I9uKu5LXIBKFDvKxnGIcIzbGg9NUtaCA==",
1295
- "signed_at": "2026-05-27T22:15:41.272Z"
1295
+ "signed_at": "2026-05-28T00:00:43.123Z"
1296
1296
  },
1297
1297
  {
1298
1298
  "name": "webapp-security",
@@ -1366,7 +1366,7 @@
1366
1366
  "d3fend_refs": [],
1367
1367
  "last_threat_review": "2026-05-11",
1368
1368
  "signature": "ux85YI4t2mVHOyt744Yin1HHy+z11JIFygjKfFfQOBBl5QVV3A267jeIy7utix85irMcpZm/T3yx/ooqiK2tBA==",
1369
- "signed_at": "2026-05-27T22:15:41.272Z",
1369
+ "signed_at": "2026-05-28T00:00:43.123Z",
1370
1370
  "forward_watch": [
1371
1371
  "NGINX Rift CVE-2026-42945 (disclosed 2026-05-13, source depthfirst) — KEV-watch predicted CISA KEV listing by 2026-05-29; AI-assisted discovery angle; track for active-exploitation confirmation and patch advisory affecting front-door web app deployments"
1372
1372
  ]
@@ -1419,7 +1419,7 @@
1419
1419
  "d3fend_refs": [],
1420
1420
  "last_threat_review": "2026-05-15",
1421
1421
  "signature": "IIXnkZ5ZNqFwOto5KfytADTLLZLoyXNZACD1ORZ40P1HUAQxe6u2uyXFzzsfuob4Uy06jNkRGr2FFgCphUH1Cw==",
1422
- "signed_at": "2026-05-27T22:15:41.272Z"
1422
+ "signed_at": "2026-05-28T00:00:43.123Z"
1423
1423
  },
1424
1424
  {
1425
1425
  "name": "sector-healthcare",
@@ -1479,7 +1479,7 @@
1479
1479
  "d3fend_refs": [],
1480
1480
  "last_threat_review": "2026-05-11",
1481
1481
  "signature": "AhF9KF8ZBlDteciV+F8IBSmFVYCvQOn44GmD4rZjgLoPxfIv/QE1/vSkK32zyqDKtHWkLSXExbkkPkxA/V6dDw==",
1482
- "signed_at": "2026-05-27T22:15:41.273Z"
1482
+ "signed_at": "2026-05-28T00:00:43.124Z"
1483
1483
  },
1484
1484
  {
1485
1485
  "name": "sector-financial",
@@ -1560,7 +1560,7 @@
1560
1560
  "TIBER-EU framework v2.0 alignment with DORA TLPT RTS (JC 2024/40); cross-recognition with CBEST and iCAST"
1561
1561
  ],
1562
1562
  "signature": "HQgZvb4ReziEz5rNFr8i/O8/rJEZR+iHRROT7m/D2QUqhrcNISPkYXENsUZlG8xapzy/Ik92ehkseyj4hdmhCQ==",
1563
- "signed_at": "2026-05-27T22:15:41.273Z"
1563
+ "signed_at": "2026-05-28T00:00:43.124Z"
1564
1564
  },
1565
1565
  {
1566
1566
  "name": "sector-federal-government",
@@ -1629,7 +1629,7 @@
1629
1629
  "Australia PSPF 2024 revision and ISM quarterly updates — track for Essential Eight Maturity Level requirements for federal entities"
1630
1630
  ],
1631
1631
  "signature": "linxmsXZiOYtcs71sSWgGCrvb8xQfmxmtTY5PRvZJ0/8FgJulo0tQtejzexYG775s7XhjAmGsDP238BQTQ8ADA==",
1632
- "signed_at": "2026-05-27T22:15:41.274Z"
1632
+ "signed_at": "2026-05-28T00:00:43.125Z"
1633
1633
  },
1634
1634
  {
1635
1635
  "name": "sector-energy",
@@ -1694,7 +1694,7 @@
1694
1694
  "ICS-CERT advisory feed (https://www.cisa.gov/news-events/cybersecurity-advisories/ics-advisories) for vendor CVEs in Siemens, Rockwell, Schneider Electric, ABB, GE Vernova, Hitachi Energy, AVEVA / OSIsoft PI"
1695
1695
  ],
1696
1696
  "signature": "JjBfc0ovta560Clk0x3QGRM5osFJDwcvpy3rT7QEGdCIL827jzE8QCow1C8deXq+4JhY2sA/d7/8IsxikdlkCg==",
1697
- "signed_at": "2026-05-27T22:15:41.274Z"
1697
+ "signed_at": "2026-05-28T00:00:43.125Z"
1698
1698
  },
1699
1699
  {
1700
1700
  "name": "sector-telecom",
@@ -1780,7 +1780,7 @@
1780
1780
  "O-RAN SFG / WG11 security specifications"
1781
1781
  ],
1782
1782
  "signature": "JWVxKFoKrbX4d+Tko1d4OBdwyg25MfFFKn4CT6E/CzH+YwnU3T6Y76uBQIKg3+gIGTvPduqyvQwQQ5FxKDuPBw==",
1783
- "signed_at": "2026-05-27T22:15:41.274Z"
1783
+ "signed_at": "2026-05-28T00:00:43.126Z"
1784
1784
  },
1785
1785
  {
1786
1786
  "name": "api-security",
@@ -1849,7 +1849,7 @@
1849
1849
  "d3fend_refs": [],
1850
1850
  "last_threat_review": "2026-05-18",
1851
1851
  "signature": "BmCRCestWqr55+fCynEhtAl5NWLT+xLTkpwS0Icp3SaoZOw/ce3Y6TtqjHRSKn4CBJq7YDiLRWxmhO3MStvOAA==",
1852
- "signed_at": "2026-05-27T22:15:41.275Z",
1852
+ "signed_at": "2026-05-28T00:00:43.126Z",
1853
1853
  "forward_watch": [
1854
1854
  "NGINX Rift CVE-2026-42945 (disclosed 2026-05-13, source depthfirst) — KEV-watch predicted CISA KEV listing by 2026-05-29; track for active-exploitation confirmation and patch advisory affecting API gateway / reverse-proxy deployments",
1855
1855
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — LiteLLM 3-bug SSRF + Code Injection chain by k3vg3n; LLM-proxy API surface; track upstream patch and CVE assignments",
@@ -1935,7 +1935,7 @@
1935
1935
  "CISA KEV additions for cloud-control-plane CVEs (IMDSv1 abuses, federation token mishandling, cross-tenant boundary failures); CISA Cybersecurity Advisories for cross-cloud advisories"
1936
1936
  ],
1937
1937
  "signature": "/DV3pmZwrRySrk1OCbyI+0BQESacjupJfUX3eC2NGtXuYOBro0vndIP+z27heFxumnjU3a9sfla7/U9X+pqnDw==",
1938
- "signed_at": "2026-05-27T22:15:41.275Z"
1938
+ "signed_at": "2026-05-28T00:00:43.126Z"
1939
1939
  },
1940
1940
  {
1941
1941
  "name": "container-runtime-security",
@@ -1997,7 +1997,7 @@
1997
1997
  "d3fend_refs": [],
1998
1998
  "last_threat_review": "2026-05-15",
1999
1999
  "signature": "E2UGSf9ATyYgzBr8uM/0ubOUmDqo1jVA7f9mVxv6LHfWGCNuQNXDyuNou9VAmUCeeXEeUYIi3AFjXkJqpOkxDA==",
2000
- "signed_at": "2026-05-27T22:15:41.275Z",
2000
+ "signed_at": "2026-05-28T00:00:43.127Z",
2001
2001
  "forward_watch": [
2002
2002
  "Pwn2Own Berlin 2026 (disclosed 2026-05-14, embargo ends 2026-08-12) — NVIDIA Container Toolkit container escape ($50K award) by chompie / IBM X-Force XOR; high-severity container/hypervisor boundary break; track patch and KEV add post-embargo"
2003
2003
  ]
@@ -2071,7 +2071,7 @@
2071
2071
  "MITRE ATLAS v5.6.0 (released May 2026) shipped the AML.T0010 sub-technique expansion this forecast tracked plus new techniques (\"Publish Poisoned AI Agent Tool\", \"Escape to Host\"); inventory now 16 tactics, 84 techniques, 56 sub-techniques. Forward watch: subsequent ATLAS minor and major releases — track next-cadence updates to agentic-AI TTPs and MLOps-pipeline-specific techniques"
2072
2072
  ],
2073
2073
  "signature": "IL+DlRCDJN/p08iiJCFkasKcoyjcB0uWrJ6ORLjQcS1HrUa5Xt62QxVjYPHzaevlm5y36ZdmfESqsZJmzK3lCg==",
2074
- "signed_at": "2026-05-27T22:15:41.276Z"
2074
+ "signed_at": "2026-05-28T00:00:43.127Z"
2075
2075
  },
2076
2076
  {
2077
2077
  "name": "incident-response-playbook",
@@ -2133,7 +2133,7 @@
2133
2133
  "NYDFS 23 NYCRR 500.17 amendments tightening ransom-payment 24h disclosure operationalization"
2134
2134
  ],
2135
2135
  "signature": "MmjLjlmOMLjhJJ4ZfR8MYlHam+ZB+eSqfh6Nv+DecaG4O5zeo9DBP/iL3cbyDVZxmhnhivgJild2ccYeWTeZAg==",
2136
- "signed_at": "2026-05-27T22:15:41.276Z"
2136
+ "signed_at": "2026-05-28T00:00:43.127Z"
2137
2137
  },
2138
2138
  {
2139
2139
  "name": "ransomware-response",
@@ -2213,7 +2213,7 @@
2213
2213
  ],
2214
2214
  "last_threat_review": "2026-05-22",
2215
2215
  "signature": "ssueL03g9fWlhXpTe+IiY5l7RqQkunN4DTN5QETKE+VOX+qggdjAR8PONxk77ol4xWYmHrM/VcH8CNtXUEvgBA==",
2216
- "signed_at": "2026-05-27T22:15:41.276Z"
2216
+ "signed_at": "2026-05-28T00:00:43.128Z"
2217
2217
  },
2218
2218
  {
2219
2219
  "name": "email-security-anti-phishing",
@@ -2266,7 +2266,7 @@
2266
2266
  "d3fend_refs": [],
2267
2267
  "last_threat_review": "2026-05-18",
2268
2268
  "signature": "rK+WnuS+9tqEABmwc0jO/PEmxcLjG1/tmUb897HsClQeKzf+TQOlwBE+OsbtuKxpjYNwur62Xxs3TxObkwm8Cw==",
2269
- "signed_at": "2026-05-27T22:15:41.277Z"
2269
+ "signed_at": "2026-05-28T00:00:43.128Z"
2270
2270
  },
2271
2271
  {
2272
2272
  "name": "age-gates-child-safety",
@@ -2334,7 +2334,7 @@
2334
2334
  "US state adult-site age-verification laws — 19+ states by mid-2026 (TX HB 18 upheld by SCOTUS June 2025 in Free Speech Coalition v. Paxton); track ongoing challenges in remaining states"
2335
2335
  ],
2336
2336
  "signature": "Rgho5TOFUL1txOzcVR0kASCNdovSU4yt99JlGilJlJRyg0A+BdeeQYrZrhPF6Vx2reUAVG0BeHfcZtSbi+cwCg==",
2337
- "signed_at": "2026-05-27T22:15:41.277Z"
2337
+ "signed_at": "2026-05-28T00:00:43.129Z"
2338
2338
  },
2339
2339
  {
2340
2340
  "name": "cloud-iam-incident",
@@ -2414,7 +2414,7 @@
2414
2414
  ],
2415
2415
  "last_threat_review": "2026-05-15",
2416
2416
  "signature": "e/kij7GtKaytROyIj7V5RH+FC9WtmVFzrmG2kIlNDNn29ep/CRNlIQKwXLpzo/81AIf634pmdr1qy/+vwIuUDA==",
2417
- "signed_at": "2026-05-27T22:15:41.278Z",
2417
+ "signed_at": "2026-05-28T00:00:43.129Z",
2418
2418
  "forward_watch": [
2419
2419
  "AWS IAM Identity Center session-policy refresh and step-up-on-admin enforcement (anticipated 2026-H2 release)",
2420
2420
  "GCP Workload Identity Federation principal-set attribute mapping tightening (post-2026 Q3 Federation hardening guide)",
@@ -2508,7 +2508,7 @@
2508
2508
  ],
2509
2509
  "last_threat_review": "2026-05-15",
2510
2510
  "signature": "ew9Kglc9fAZzbn0ZIfGP7WSK/j4eV2VhSvpy+s5bEfNEVYIMa2kZjnGBapgUsyGDLes9H9K2ovjQyX17+GKiBw==",
2511
- "signed_at": "2026-05-27T22:15:41.278Z",
2511
+ "signed_at": "2026-05-28T00:00:43.129Z",
2512
2512
  "forward_watch": [
2513
2513
  "Entra ID conditional access evolution post-Midnight Blizzard — Microsoft's 2025-2026 commitments on legacy-tenant MFA enforcement and OAuth-app consent gating",
2514
2514
  "Okta IPSIE (Interoperability Profile for Secure Identity in the Enterprise) OpenID Foundation working-group output and adoption timeline",
@@ -2526,6 +2526,6 @@
2526
2526
  ],
2527
2527
  "manifest_signature": {
2528
2528
  "algorithm": "Ed25519",
2529
- "signature_base64": "XvNerZTXJt8fW7G+X8lLqi22bKAf0AU1rg63L2yy/I2smf8dA+nnbhicXsnfheVfWYVWfYmkL+9Dhznpb3hSBA=="
2529
+ "signature_base64": "TYYZN6U1HmGknYT6D0XjoASN/5582uJRHLWt9w5Kp3FNZ+PR9spGiwKVVcu2g1ca68DjjuEWmjxM2069/VNcBA=="
2530
2530
  }
2531
2531
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.14.13",
3
+ "version": "0.14.15",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 11 catalogs (406 CVEs / 171 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 8888 RFCs), 35 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
5
5
  "keywords": [
6
6
  "ai-security",
package/sbom.cdx.json CHANGED
@@ -1,22 +1,22 @@
1
1
  {
2
2
  "bomFormat": "CycloneDX",
3
3
  "specVersion": "1.6",
4
- "serialNumber": "urn:uuid:b3078c68-9af8-416e-8f22-f5b3e4ced341",
4
+ "serialNumber": "urn:uuid:392e0013-1220-48fb-95ea-20eeaddf5a1d",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2121-03-08T01:52:40.000Z",
7
+ "timestamp": "2056-05-26T04:39:47.000Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "blamejs",
11
11
  "name": "scripts/refresh-sbom.js",
12
- "version": "0.14.13"
12
+ "version": "0.14.15"
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.14.13",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.14.15",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.14.13",
19
+ "version": "0.14.15",
20
20
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 11 catalogs (406 CVEs / 171 CWEs / 805 ATT&CK + ICS / 170 ATLAS / 468 D3FEND / 8888 RFCs), 35 jurisdictions, 10-class catalog gap detector + budget gate, real XML parser + canonical-form diff + content-pattern regression detection, Ed25519-signed.",
21
21
  "licenses": [
22
22
  {
@@ -25,17 +25,17 @@
25
25
  }
26
26
  }
27
27
  ],
28
- "purl": "pkg:npm/%40blamejs/exceptd-skills@0.14.13",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.14.15",
29
29
  "hashes": [
30
30
  {
31
31
  "alg": "SHA-256",
32
- "content": "1a85b5379311c8bf13781c09de6514abf2313891a9f12616c4790996aff541f2"
32
+ "content": "25ca72f69f91acc60ea8818a4b2de880302049897b9a3491738185e659e0b0fa"
33
33
  }
34
34
  ],
35
35
  "externalReferences": [
36
36
  {
37
37
  "type": "distribution",
38
- "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.14.13"
38
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.14.15"
39
39
  },
40
40
  {
41
41
  "type": "vcs",
@@ -116,11 +116,11 @@
116
116
  "hashes": [
117
117
  {
118
118
  "alg": "SHA-256",
119
- "content": "db93e367948634b7fe245b7527559864577b5000abf6fce8f3e8274ca6df506f"
119
+ "content": "e0f8b6434a4e651ef54c776e8a5f4922e825f186b15ded49b95cd31b87c48593"
120
120
  },
121
121
  {
122
122
  "alg": "SHA3-512",
123
- "content": "1c823697ecab14f0b338390854ace4eb6059d46bd94c35591c11ffeb568aeaa358cb5c0587d08e449a3be4cc0771fb68004e8262b288dcdf47209c79e2b644a4"
123
+ "content": "7fe6a6a104278c5ef793837d1051448d077d5ff4c0da20582c32417c75dc3bb754262b3c60b0eeb4c66f168e8ab14b2bbe4657ac8cf6f7ab23cfd2e8ab8ad3d2"
124
124
  }
125
125
  ]
126
126
  },
@@ -281,11 +281,11 @@
281
281
  "hashes": [
282
282
  {
283
283
  "alg": "SHA-256",
284
- "content": "d86676c1ab40a796ff0c08498431d9c2bcd86e0f9d5f14acc9a1b543d208f25d"
284
+ "content": "447e6ba93e05120de060cca6636c8419c465034744d93606bbe6c7175be1a844"
285
285
  },
286
286
  {
287
287
  "alg": "SHA3-512",
288
- "content": "2d4bcb3c99183bbeca53a671726dbfaf2354213abc9f9a1469a4cbd1dcb43689cc0181e9c9b1a0bd9618265acf48fbda5708efc597b11c7fd66160e4a1785589"
288
+ "content": "23f28a5eefab97d5ad447033826279f34424a728123a2a2c6e551fea601504f06fd9b2860b718e515a7e31bfa1a9cb349bffb926a91c63682753c0c979826db6"
289
289
  }
290
290
  ]
291
291
  },
@@ -1316,11 +1316,11 @@
1316
1316
  "hashes": [
1317
1317
  {
1318
1318
  "alg": "SHA-256",
1319
- "content": "a53c28d4574bda9c3e4dfbb73e96be48f203257dfe16f7235506c07f0128464d"
1319
+ "content": "05afc567fa7500cfd3aa6b8c67a060fc421c8d3f37d68470ffe674a7d7f4e210"
1320
1320
  },
1321
1321
  {
1322
1322
  "alg": "SHA3-512",
1323
- "content": "c3fe772f15a10c575057fc931d98389773a9dbb1169fd2a1debcb1e07be65dbebb4b42c4ca632c78560991c3c383eb1081d93b07a96c184b7a2868a296a9d9c5"
1323
+ "content": "5c6245e69b1e85a72247fb195cc6f071d7af9b92ba0243c57b5221a3e576dba4f62741604e595d2e00fcbc15f4319495ca470567437b869a868b736f0e6243a0"
1324
1324
  }
1325
1325
  ]
1326
1326
  },
@@ -1751,11 +1751,11 @@
1751
1751
  "hashes": [
1752
1752
  {
1753
1753
  "alg": "SHA-256",
1754
- "content": "a1f0cb852fd487d12bd1a304d36eb175f3ee36a26e37a1ca9cb25d9c576d2afc"
1754
+ "content": "22b49842257d49e0386c45311639f8e81f671ead4a30b2ca8f14d316731e36bf"
1755
1755
  },
1756
1756
  {
1757
1757
  "alg": "SHA3-512",
1758
- "content": "f29f2c4acd42b83c85d1352c875a8a00a13f5216f7ac9e451b250f79197cce6f7bc82151cb938c133922a14a9d8bbe5883df20aac8b6db17576a6499e80c4651"
1758
+ "content": "4b391ca37058a11ee9ba1d1e88fae252f54a8a20c9a458d6d9fb1345f11827e33d556b4934fbad23082f9296021922330c7c8da89435f4fc5585f12cf8a534f9"
1759
1759
  }
1760
1760
  ]
1761
1761
  },