@blamejs/exceptd-skills 0.15.51 → 0.15.52

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,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.15.52 — 2026-05-30
4
+
5
+ Supply-chain playbook: three new detection-depth checks. Typosquat / homoglyph detection flags a direct dependency whose name impersonates a popular package by edit-distance or a visually-confusable Unicode substitution (reusing the Trojan-Source codepoint tables) — a lure signal that precedes any payload. A static content red-flag screen flags packages that ship only minified/obfuscated source, carry a high ratio of high-entropy strings, are a trivial shell that nonetheless runs an install script or opens the network, or combine dynamic `eval` with dynamic `require` (CWE/ATT&CK T1027) — orthogonal to the capability screens. A dependency-confusion resolution check flags an internal-looking package name served from the public registry, or an inflated-version public squat that wins resolution over the private package — the resolution-source signature that precedes execution, correlated to the MOIKA campaign. Each ships with a paired evidence artifact and false-positive profile.
6
+
3
7
  ## 0.15.51 — 2026-05-30
4
8
 
5
9
  Catalog: three new supply-chain entries. CVE-2022-23812 — the node-ipc "peacenotwar" protestware incident, where a trusted maintainer shipped a geo-targeted file-wiper in the package main module, so `--ignore-scripts` (the usual npm-supply-chain mitigation) does not stop it. TrapDoor — a cross-ecosystem (npm / PyPI / crates.io) credential-stealer campaign whose novel vector plants zero-width-Unicode instructions in `.cursorrules` / `CLAUDE.md` files to subvert AI coding assistants into discovering and exfiltrating local secrets. MOIKA — the catalog's first dependency-confusion entry: public packages published under squatted internal scopes at inflated versions, with a postinstall stager that exfiltrates the full process environment. Each carries its paired zero-day lesson and new framework-lag controls (main-module-payload detection, AI-assistant config-file poisoning detection, internal-scope→registry pinning).
package/NOTICE CHANGED
@@ -45,7 +45,7 @@ URL: https://atlas.mitre.org
45
45
  Version: v5.1.0 (November 2025)
46
46
  Used for: Adversarial Threat Landscape for AI Systems — TTP IDs cited in
47
47
  skills/*, data/atlas-ttps.json, and manifest.json. Pinned per
48
- CLAUDE.md hard rule #12.
48
+ AGENTS.md Hard Rule #12 (external data version pinning).
49
49
  Notice: ATLAS is © The MITRE Corporation, released under the terms at
50
50
  https://atlas.mitre.org/resources/terms-of-use.
51
51
  --------------------------------------------------------------------------------
package/bin/exceptd.js CHANGED
@@ -3031,7 +3031,7 @@ function cmdPlan(runner, args, runOpts, pretty) {
3031
3031
  // in `run --scope nonsense` produced `count: 0` + exit 0 (cmd reports
3032
3032
  // "ran 0 playbooks") and in `ci --scope nonsense` silently ran only the
3033
3033
  // cross-cutting set (the union with `framework` produced a false-positive
3034
- // PASS). Both are operator-intent loss patterns CLAUDE.md flags as the
3034
+ // PASS). Both are operator-intent loss patterns of the
3035
3035
  // "field-present, content-wrong" class.
3036
3036
  const VALID_SCOPES = ["system", "code", "service", "cross-cutting", "all"];
3037
3037
 
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "schema_version": "1.1.0",
3
- "generated_at": "2026-05-31T03:23:30.384Z",
3
+ "generated_at": "2026-05-31T04:31:02.570Z",
4
4
  "generator": "scripts/build-indexes.js",
5
5
  "source_count": 54,
6
6
  "source_hashes": {
7
- "manifest.json": "26f77d56ab70b31946f08cd83c8cd5fc43e807c77955a0e9d4ae9450d32e29c9",
7
+ "manifest.json": "e5f2d2a803f6972ef1759593ddbec1e3badc297b8f83e667fedeaf4b68fd9819",
8
8
  "data/atlas-ttps.json": "878b4a08bb73c8d20396d85cf433a88f2bc5e7a8cbf7f6ab773ce7ede0a11251",
9
9
  "data/attack-techniques.json": "318bf8e9c5aee1d0a4a1dc37c4b211f2fbc937bf332a401a22483cc7d0547252",
10
10
  "data/cve-catalog.json": "cb5e305b5488a2a02e177f10e913d22f602d6016109f152903093e9614e0b470",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_meta": {
3
3
  "id": "sbom",
4
- "version": "1.3.0",
4
+ "version": "1.3.1",
5
5
  "last_threat_review": "2026-05-13",
6
6
  "threat_currency_score": 96,
7
7
  "changelog": [
@@ -53,6 +53,11 @@
53
53
  "version": "1.3.0",
54
54
  "date": "2026-05-30",
55
55
  "summary": "Add a package-capability taxonomy (network / filesystem / shell / env / eval / install-script / telemetry / native-binary): a package-capability-surface evidence artifact plus an absolute capability-surface screen that flags a no-CVE package whose install-script combines with shell/network/env/eval (the credential-harvesting delivery shape), complementing the across-version-bump capability-creep detector."
56
+ },
57
+ {
58
+ "version": "1.3.1",
59
+ "date": "2026-05-30",
60
+ "summary": "Add three supply-chain detection-depth indicators: typosquat/homoglyph package-name detection (reusing the vendored codepoint-class confusable tables), a static content red-flag screen (obfuscation / high-entropy / trivial-package / eval+dynamic-require, T1027), and a dependency-confusion resolution-source check (internal scope served by the public registry / inflated-version squat, correlating to MAL-2026-MOIKA-DEPCONFUSION) — each with a paired look artifact and false-positive profile."
56
61
  }
57
62
  ],
58
63
  "owner": "@blamejs/supply-chain",
@@ -619,6 +624,27 @@
619
624
  "source": "For each installed/locked npm + PyPI package, read its manifest to enumerate declared/observable capabilities: package.json scripts (preinstall/install/postinstall), bin, gypfile/binding.gyp (native-binary build), and dependencies on known network/child-process modules; PyPI setup.py/pyproject.toml build hooks + .so/.pyd payloads. Classify each package into the capability vocabulary: network, filesystem, shell, env, eval, install-script, telemetry, native-binary.",
620
625
  "description": "Package-capability surface per dependency, using a capability taxonomy (network / filesystem / shell / env / eval / install-script / telemetry / native-binary). Complements the CVE-match register: a package with no catalogued CVE can still carry an outsized capability surface (an install-script that spawns a shell and reads env is the Shai-Hulud delivery shape). Capability is a property of the package, independent of any known vulnerability.",
621
626
  "required": false
627
+ },
628
+ {
629
+ "id": "package-name-similarity-surface",
630
+ "type": "config_file",
631
+ "source": "For each direct dependency, capture {package name, resolved registry host, a download/popularity signal where available} and compute (a) the Levenshtein edit-distance to a set of high-download popular package names and (b) a homoglyph-normalized / confusable-folded form of the name (via vendor/blamejs/codepoint-class.js) to surface visually-confusable substitutions. Record the closest popular-name match + distance per dependency.",
632
+ "description": "Package-name similarity surface — the evidence the typosquat/homoglyph detector reads. Captures edit-distance and confusable-normalized name forms so name-impersonation is computable offline without a registry call (the popularity set is a static bundled list; absence of a live download count degrades to edit-distance + homoglyph only).",
633
+ "required": false
634
+ },
635
+ {
636
+ "id": "package-source-content-surface",
637
+ "type": "config_file",
638
+ "source": "For each installed/locked package, statically read its shipped source and record: readable-source-present vs minified-only, sourcemap presence, high-entropy-string ratio (Shannon entropy over string literals), effective LOC count, and dynamic eval / dynamic require/import usage. No execution — static read of the on-disk package files only.",
639
+ "description": "Package source-content surface — the static evidence the obfuscation screen reads. Distinct from package-capability-surface (which classifies declared/observable capabilities); this captures source SHAPE (minification, entropy, size, dynamic-eval) for the content red-flag screen.",
640
+ "required": false
641
+ },
642
+ {
643
+ "id": "dep-confusion-resolution-config",
644
+ "type": "config_file",
645
+ "source": "Read scope-to-registry resolution config: .npmrc + $HOME/.npmrc (@scope:registry= lines, registry=, always-auth), .yarnrc.yml (npmScopes), pip.conf / pip.ini (index-url, extra-index-url), and any private-registry manifest listing internal @scopes / package names. Cross with repo-lockfiles resolved URLs to determine which registry actually served each internal-scope name.",
646
+ "description": "Resolution-source configuration — distinguishes an internal-namespace package correctly pinned to a private registry (safe) from one resolvable off the public registry (dependency-confusion exposed). Without it the indicator can only see inflated-version heuristics; with it the resolution-source check becomes high-confidence.",
647
+ "required": false
622
648
  }
623
649
  ],
624
650
  "collection_scope": {
@@ -849,6 +875,55 @@
849
875
  "Telemetry-only capability (a postinstall that pings an analytics endpoint with no env/credential read) is a noise/consent concern, not a credential-exfil primitive. Demote network+telemetry-without-env to informational unless env or filesystem-credential-path access co-occurs.",
850
876
  "If the capability surface was read from a stale or non-build lockfile (archive/ or pre-migration/ subdir), it does not reflect what installs. Confirm against the build-consumed lockfile path before firing (same stale-lockfile check as lockfile-no-integrity)."
851
877
  ]
878
+ },
879
+ {
880
+ "id": "dependency-name-typosquat",
881
+ "type": "behavioral_signal",
882
+ "value": "For each DIRECT dependency name in repo-lockfiles / npm-global-packages / pip-packages, flag when the name is within Levenshtein edit-distance 1-2 of a SPECIFIC high-download popular package it is NOT (typosquat — e.g. lodahs vs lodash, crossenv vs cross-env), OR uses a homoglyph / visually-confusable Unicode substitution of a popular name (route the name through the vendored vendor/blamejs/codepoint-class.js confusable/bidi detection — a Latin/Cyrillic/Greek lookalike codepoint inside an otherwise-ASCII clone of a popular name is a near-certain lure), OR is a scope/namespace lookalike of a well-known org scope. FIRES on name-confusability independent of any CVE match — the typosquat lure precedes payload execution.",
883
+ "description": "Typosquat / homoglyph package-name detector. Flags a direct dependency whose NAME impersonates a popular package by edit-distance or visually-confusable codepoint substitution, a pre-disclosure lure signal that no CVE-match or capability screen catches. Reuses the vendored codepoint-class confusable detection (added v0.15.50). Distinct axis from the dependency-confusion resolution check (name-similarity vs resolution-source).",
884
+ "confidence": "medium",
885
+ "deterministic": false,
886
+ "attack_ref": "T1195.002",
887
+ "atlas_ref": "AML.T0010",
888
+ "false_positive_checks_required": [
889
+ "The package IS the popular one: confirm via download-count / publish-history that the flagged name is not itself the canonical high-download package before treating proximity as a squat. Demote when the package is the legitimate target it resembles.",
890
+ "Intentional short-name alias or sanctioned scoped fork the operator deliberately consumes (e.g. an internal mirror named close to upstream). Cross-check the operator documented dependency allowlist; demote when the near-name is a known sanctioned alias.",
891
+ "Common-word collision: short generic names (ms, qs, fs) sit within edit-distance of many strings by chance. Require a SPECIFIC high-download package the flagged dep plausibly impersonates, not any string within distance 2; demote incidental proximity with no plausible impersonation target.",
892
+ "Homoglyph false alarm: confirm the confusable/non-ASCII codepoint actually substitutes for an ASCII letter in a known popular package name (a Cyrillic a inside an ASCII clone), not an incidental legitimately-non-Latin name that has no popular ASCII twin. Demote names with no ASCII-lookalike target."
893
+ ]
894
+ },
895
+ {
896
+ "id": "package-content-obfuscation-screen",
897
+ "type": "config_value",
898
+ "value": "Within package-source-content-surface: flag a package that ships ONLY minified/obfuscated source with no readable source and no sourcemap, OR a high ratio of high-entropy string literals (packed/encrypted payload), OR is a trivial package (under ~10 effective LOC) that NONETHELESS declares an install-script or network capability (a thin shell wrapping a payload), OR combines dynamic eval with dynamic require/import. These are CONTENT red-flags orthogonal to the capability taxonomy — a package can be capability-light yet content-obfuscated.",
899
+ "description": "Static content red-flag screen — obfuscation / high-entropy / minified-only / trivial-package / eval+dynamic-require. Complements (does not duplicate) the capability-surface screens: capability is WHAT the package can do, content is HOW its source is shaped. Maps T1027 (Obfuscated Files or Information).",
900
+ "confidence": "medium",
901
+ "deterministic": false,
902
+ "attack_ref": "T1027",
903
+ "false_positive_checks_required": [
904
+ "Legitimate minified dist bundle: many front-end packages ship a minified dist/ alongside readable src/ or a sourcemap. Confirm there is NO readable source AND no sourcemap anywhere in the package before flagging minified-only; demote when readable source or a .map file exists.",
905
+ "Known-good build-output / WASM / native-addon packages legitimately carry high-entropy blobs (embedded WASM, prebuilt binaries, fixtures). Cross-check the operator build-dependency allowlist + publisher provenance; demote expected high-entropy classes.",
906
+ "Trivial-package false alarm: a genuinely tiny utility (is-number) is trivial WITHOUT an install-script or network capability. Only elevate trivial packages that ALSO declare install-script or network capability (the co-occurrence is the signal); demote trivial-but-inert packages.",
907
+ "eval / dynamic-require in a legitimate plugin loader or config system (some established frameworks use dynamic require by design). Confirm the eval+dynamic-require pair is in a freshly-published or low-trust package, not a long-standing well-known framework; demote established frameworks with documented dynamic loading."
908
+ ]
909
+ },
910
+ {
911
+ "id": "dependency-confusion-internal-scope-public-resolution",
912
+ "type": "config_value",
913
+ "value": "Within repo-lockfiles + dep-confusion-resolution-config: a dependency whose scope/name matches an organizational-internal naming pattern (a @scope that ALSO appears in the org private-registry manifests, .npmrc/.yarnrc scope-to-registry maps, or pip internal extra-index-url) resolved from the PUBLIC registry (registry.npmjs.org / pypi.org) rather than the internal one — AND/OR a resolved version is anomalously inflated (e.g. 99.x.x, a major far above the package real release history) such that default resolution prefers the public package. The defining condition is RESOLUTION-SOURCE confusion: an internal-namespace identifier served by the public registry, regardless of payload.",
914
+ "description": "Dependency-confusion / namespace-squat resolution check. Fires when an internal-looking package name is satisfied from the public registry, or an inflated semver indicates a public squat outranking the private package — the resolution-layer signature that precedes any payload. Generalizes MAL-2026-MOIKA-DEPCONFUSION to any internal-scope squat; complements the capability-surface screens (postinstall stager) and the catalog CVE-match (specific known names). Distinct axis from the typosquat name-similarity detector.",
915
+ "confidence": "high",
916
+ "deterministic": false,
917
+ "attack_ref": "T1195.001",
918
+ "atlas_ref": "AML.T0010",
919
+ "cve_ref": "MAL-2026-MOIKA-DEPCONFUSION",
920
+ "false_positive_checks_required": [
921
+ "Confirm the @scope is genuinely an INTERNAL/private namespace for this org (present in a private-registry manifest, .npmrc scope-to-registry map, or internal pip index), not a public org scope the org legitimately consumes from npmjs (@types, @babel, @aws-sdk are public-by-design and not confusion). Demote when the scope is a known public namespace.",
922
+ "Verify an actual private package of the same name exists that the public one could shadow. A public-only package under a coincidentally org-like scope, with no private counterpart, is not dependency-confusion — there is nothing to confuse it with. Demote to inconclusive absent a private same-name package.",
923
+ "Confirm install config does not already pin the internal scope to the private registry (.npmrc @scope:registry=, always-auth, pip --index-url with no public fallthrough). A correctly-pinned scope cannot be confused; the public squat exists but cannot win resolution. Demote when resolution is pinned.",
924
+ "Distinguish an inflated version that reflects a real (if unusual) upstream release from a squat. Check the package real release history on the AUTHORITATIVE registry for this scope: a genuine 99.x maintained release is not a squat. Demote when the inflated version is the legitimate publisher actual latest on the correct registry.",
925
+ "Confirm the resolved artifact actually came from the public registry for this run (lockfile resolved/resolution URL host = registry.npmjs.org / files.pythonhosted.org), not the private mirror that merely proxies public names. A private proxy serving a vetted copy under the internal scope is not confusion-exposed. Demote when the resolution host is the internal/proxy registry."
926
+ ]
852
927
  }
853
928
  ],
854
929
  "false_positive_profile": [
@@ -876,6 +951,21 @@
876
951
  "indicator_id": "package-capability-creep",
877
952
  "benign_pattern": "Native-addon and build-tooling packages (node-gyp, sass-embedded, esbuild, sharp, bcrypt, prebuild-install, electron, playwright, puppeteer) legitimately carry install-script + native-binary + (sometimes) network capability — the prebuilt-binary download is network + install-script by design.",
878
953
  "distinguishing_test": "Cross-reference the flagged package against the operator documented build-dependency allowlist and its npm/PyPI provenance attestation. If the package is a known build/native-addon dependency AND its publisher identity matches the expected signed publisher, downgrade to medium/informational. Reserve high confidence for packages whose capability surface is unexplained by their stated function (a date-formatting utility that opens the network and reads env)."
954
+ },
955
+ {
956
+ "indicator_id": "dependency-name-typosquat",
957
+ "benign_pattern": "A short or generic package name sits within edit-distance of a popular one by coincidence (ms, qs), or the operator deliberately consumes a sanctioned near-name (an internal mirror, a scoped fork), or a legitimately non-Latin package name has no popular ASCII twin.",
958
+ "distinguishing_test": "Typosquat-likely only when ALL hold: the name is within edit-distance 1-2 of (or a homoglyph substitution within) a SPECIFIC high-download package it is NOT, the flagged package is far lower download/younger than that target, and it is not on the operator sanctioned-alias allowlist. Any one failing demotes to medium/informational."
959
+ },
960
+ {
961
+ "indicator_id": "package-content-obfuscation-screen",
962
+ "benign_pattern": "A front-end package ships a minified dist/ with a sourcemap or alongside readable src/; a native-addon/WASM package carries legitimate high-entropy binary blobs; an established framework uses documented dynamic require for plugin loading.",
963
+ "distinguishing_test": "Content-malicious-leaning only when the obfuscation co-occurs with risk: minified-only AND no sourcemap AND no readable source; OR trivial-LOC AND (install-script OR network); OR eval+dynamic-require in a fresh/low-trust package. A minified bundle WITH a sourcemap, or high-entropy in an allowlisted native-addon, or dynamic-require in a known framework, demotes to informational."
964
+ },
965
+ {
966
+ "indicator_id": "dependency-confusion-internal-scope-public-resolution",
967
+ "benign_pattern": "An org legitimately consumes a public scoped package (@types/*, @babel/*, @aws-sdk/*) that superficially resembles an internal naming convention; OR an internal scope is already correctly pinned to the private registry so the public squat cannot win resolution.",
968
+ "distinguishing_test": "Resolve each flagged @scope against (1) the org private-registry internal-scope list and (2) the .npmrc/.yarnrc/pip index scope-to-registry mapping. Dependency-confusion-exposed only when ALL hold: the scope is an internal namespace, a same-name private package exists, the scope is NOT pinned to the private registry, and the lockfile resolved host is the public registry. If any fails, downgrade to medium/informational."
879
969
  }
880
970
  ],
881
971
  "minimum_signal": {
package/lib/prefetch.js CHANGED
@@ -696,8 +696,8 @@ async function main() {
696
696
  // contractually correct but visibly noisy. Letting the event loop
697
697
  // drain naturally — via exitCode + return — lets undici's connection
698
698
  // pool and the AbortController signal listeners finish teardown
699
- // before the process exits, eliminating the assertion. Same pattern
700
- // documented in CLAUDE.md for v0.11.11's `ci` #100 regression.
699
+ // before the process exits, eliminating the assertion. Same pattern as
700
+ // the `ci` #100 stdout-flush regression.
701
701
  try {
702
702
  const result = await prefetch(opts);
703
703
  process.exitCode = result.errors > 0 ? 1 : 0;
package/manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "exceptd-security",
3
- "version": "0.15.51",
3
+ "version": "0.15.52",
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": "0H+JfyUVmo/pVFEi5rLENATHjlukPVUqnOWmNPEH77wm8svKGK0aNJ46k6QU5GdHb8c9X9pVJKiuhON6AxDjDw==",
56
- "signed_at": "2026-05-31T03:04:10.828Z",
56
+ "signed_at": "2026-05-31T03:55:39.537Z",
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": "PHwHEsoy7ctBYOtlAfAdCDVfsq2Bpk9+qESSF+5dVkDcez2zp2v9Ihsv2vqMEs3QxMndyQ+t7NVezyt5VamSCg==",
126
- "signed_at": "2026-05-31T03:04:10.830Z",
126
+ "signed_at": "2026-05-31T03:55:39.539Z",
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": "dD4p7lcRtMyfITOncqLkpOeMy6x6gM0V7UlWHgLEdcxqODb1s75ar1cBtTqDWPbMv6ZAzVo2HJLDK1hVjjU2AQ==",
199
- "signed_at": "2026-05-31T03:04:10.830Z",
199
+ "signed_at": "2026-05-31T03:55:39.539Z",
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": "wsw8Mlr/gyw6S7Iaao9BVHdU5LFPWl8WVymW17Lkq9J1Mui0+fCrTg6UbrsaeE3s7EW3TVgzBuK+8EFd1+H5AA==",
251
- "signed_at": "2026-05-31T03:04:10.830Z"
251
+ "signed_at": "2026-05-31T03:55:39.539Z"
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": "uVTc1QRKOKcIVDajBz+q2egjiEAyOQaDNsvVI2ghj5FD0VvquoUBBE5Naca2FkaZa790EHWCsVZ4hhdaSQs2DQ==",
282
- "signed_at": "2026-05-31T03:04:10.831Z"
282
+ "signed_at": "2026-05-31T03:55:39.540Z"
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": "QuNpwnZ6HkCEAXTPC/jLbXSmMIc1JnBczqZAAIZmZj8OcEMVnw9mJYAnU3CxaEI7rvbcMkN2uS5E8yUCm/NiAg==",
311
- "signed_at": "2026-05-31T03:04:10.831Z"
311
+ "signed_at": "2026-05-31T03:55:39.541Z"
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": "5rw2i39SxY2WphBbDLEP28wufnbPPE9+PWt54hmaGdwHXr9RLiVt5liL/5xp14sehlVgFsfpR/bg9vy//xV0DA==",
348
- "signed_at": "2026-05-31T03:04:10.832Z",
348
+ "signed_at": "2026-05-31T03:55:39.541Z",
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": "Vqu49nzntFWjn9A/QeJzm7q/2xk/cZJ6HFQKtiNi1zgcxzXKm+MlFdkaLgYHWj5/9HJohxyIDyBJQTvcJ20eDQ==",
408
- "signed_at": "2026-05-31T03:04:10.832Z",
408
+ "signed_at": "2026-05-31T03:55:39.541Z",
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": "W87VdyVdAxAdcRI6P/8StaV+MS8ZSPKM9HOCK9n/bBO6BM3ZSE3uImVoyJVpAXQlUpUGN+A3lCJZXv64LuxwDg==",
443
- "signed_at": "2026-05-31T03:04:10.832Z",
443
+ "signed_at": "2026-05-31T03:55:39.542Z",
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": "wdVX+edeNekpaIldqkhvtraV6DquLvIsKAjuZVwPQYn3l1vS99HXuFxmNsD7UeMlO3qgC6Dysfsto9EnuH0RBg==",
477
- "signed_at": "2026-05-31T03:04:10.833Z",
477
+ "signed_at": "2026-05-31T03:55:39.542Z",
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": "b5miTiY0cnxETd2btxorfZBdJKt/fLnQx20sGYUb9zEqGqtm0LMLpghkW68j4/9k48KNyuGMtNWiKTSnodUGBw==",
516
- "signed_at": "2026-05-31T03:04:10.833Z"
516
+ "signed_at": "2026-05-31T03:55:39.542Z"
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": "xbkip0AQtWQKAu+O6r/gYECNjezS6O9k9xkkJsYbMlr+j8CdqH3p5/0l+GZmDidImRC/DL07GCnKrk9HRR/yDQ==",
543
- "signed_at": "2026-05-31T03:04:10.833Z",
543
+ "signed_at": "2026-05-31T03:55:39.543Z",
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": "li2NnC1oeVIr22ComP5QbcQoh5xpWITuaKpza1s2SsUkH6kGnnt4wFfFAzaC1ORmH9x2cr8hN8kaNANG/eIMBQ==",
607
- "signed_at": "2026-05-31T03:04:10.834Z",
607
+ "signed_at": "2026-05-31T03:55:39.543Z",
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": "sZHlJ7ueHPdtzVbR+yXQ5+wKgNyjWsa1LKVg9aWTmg/Onl71DvEILMyJiLpPQjseT56Mnr1DMYJE8xOGlffBAw==",
655
- "signed_at": "2026-05-31T03:04:10.834Z"
655
+ "signed_at": "2026-05-31T03:55:39.543Z"
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": "3AwFnEJu6DukPPNep/3SnuPWEuV060fJEQIwThFm7ujmdbFk0/Ii0XwGv1dkvbbK7ymMdOQpp35l4aLONAucDA==",
692
- "signed_at": "2026-05-31T03:04:10.834Z",
692
+ "signed_at": "2026-05-31T03:55:39.544Z",
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": "iJWevUBurLvt2v8X+Ch2eHmZkPWpKeAtIpxTIP4MwbUHyco3igDeBywJCyaR2vURYRx8LkzzIMM8DxQM4LAXBQ==",
727
- "signed_at": "2026-05-31T03:04:10.835Z"
727
+ "signed_at": "2026-05-31T03:55:39.544Z"
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": "DDMzI+4En4aIkwBUCGW6nj1eEkCyLqHGn2LJ2rnwWfYatjPI1U5HrTZNAN/n9JqWtAzk8F3rmsKehaaz5iNWDA==",
799
- "signed_at": "2026-05-31T03:04:10.835Z"
799
+ "signed_at": "2026-05-31T03:55:39.544Z"
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": "dJB0iAstIUbyny+udl3OIkaLScEmqS97LNP73yQ8mxt+0bcqxZjpfXaWLzLuIQblGYvUvz75/H6rO2EJuGd4AQ==",
859
- "signed_at": "2026-05-31T03:04:10.835Z"
859
+ "signed_at": "2026-05-31T03:55:39.545Z"
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": "KEAoMji3VcPX/ZXXqVe6OStxSkTssfY9fIRPyPcDYqh50GzOFQ6koNOTBVAiWOvjDjQ38g12xun5srbqgmvRAw==",
934
- "signed_at": "2026-05-31T03:04:10.835Z"
934
+ "signed_at": "2026-05-31T03:55:39.545Z"
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": "zuW8T0EMbVV83GsUP/W20Use2gBTicBW021T0sY7qsRY/U5qsPWkXYIWp3SdiKLTIKqTEd/0T7LQebjIs2QKCA==",
1013
- "signed_at": "2026-05-31T03:04:10.836Z"
1013
+ "signed_at": "2026-05-31T03:55:39.545Z"
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": "Qe0Hg9BrX3Zm5pj0n2z/oiHbAXWdA2Dq461zc4izkkUjEX2CZ02rODjCI2ELbrVOU3GC7edxqAxA+5U/ObnHDQ==",
1070
- "signed_at": "2026-05-31T03:04:10.836Z"
1070
+ "signed_at": "2026-05-31T03:55:39.546Z"
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": "UV3458QXSkEpenzrOmdlTTfPHUD4hNyKMDHoeZDq/kiFb4mAG0ghQGTTgI9Ru8cJbSmYM1++m9N5TFIJ6JJPBg==",
1137
- "signed_at": "2026-05-31T03:04:10.836Z"
1137
+ "signed_at": "2026-05-31T03:55:39.546Z"
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": "kIVzsPsJ72PzzWQwTuvjoHHoVEDCday5I52M9ohjB3/Ak+zlA8oyWLO/BKb/XuYY4fOApjfxTErSWv5uHQ2zDw==",
1193
- "signed_at": "2026-05-31T03:04:10.837Z"
1193
+ "signed_at": "2026-05-31T03:55:39.546Z"
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": "bWr27Q1uN9xCe1ib4QulszBa7YIDNkGqo72k5nm2cK98LyPblicD+sO9MnGckAyB22BTN/cIB+FwFMcI5IxvBw==",
1245
- "signed_at": "2026-05-31T03:04:10.837Z"
1245
+ "signed_at": "2026-05-31T03:55:39.547Z"
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": "Q854yzLqXdOazc6EyQbZzgAlivuq2vGFDVUCrxSldSvx/HX/ZM/uzmJyP7aBG7ZsMHxj6Lmj/H82YQoo1e+NCQ==",
1295
- "signed_at": "2026-05-31T03:04:10.837Z"
1295
+ "signed_at": "2026-05-31T03:55:39.547Z"
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": "4ccahkJpGJZtwD7EBpnGcN0sEGPMEw8eqV+tvePVS04YAkLgYVWtlkasI/8n0be9xB+77x+Sjj3kIi2j2Lf9CA==",
1369
- "signed_at": "2026-05-31T03:04:10.838Z",
1369
+ "signed_at": "2026-05-31T03:55:39.547Z",
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": "SBB7c3wNYfIdkyOp4g4nW0WP7xS+YokMzg32aaeJdbf14LTGQRzQUvSqb2TCj2HFUSHESOyKT1JpkAfyHLSQBQ==",
1422
- "signed_at": "2026-05-31T03:04:10.838Z"
1422
+ "signed_at": "2026-05-31T03:55:39.548Z"
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": "U04GNLyRas1VmfEsB8khH4iqFZPwx96sPY0Kw9iVsSPU+KTeEFqwgtWK1X1pzgb+T16Pc7HSrCaXDOpTFvQEDw==",
1482
- "signed_at": "2026-05-31T03:04:10.838Z"
1482
+ "signed_at": "2026-05-31T03:55:39.548Z"
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": "xbylLqNPBuEsFE/MNVeGy/01K6yiJXMxQbzC1F4RWU5aseDGbNy5HrAv2JWI2+Aft05ozreNPjccvu66yJ5EBw==",
1563
- "signed_at": "2026-05-31T03:04:10.839Z"
1563
+ "signed_at": "2026-05-31T03:55:39.549Z"
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": "C9c3JuBhUbwcb7uZpDdy+PNT8sYmYIxzD4uRHu421ePW1aSFJ8fkMvuTzSO8vD/F/jOOg5opM4kov/xSAn+qCg==",
1632
- "signed_at": "2026-05-31T03:04:10.839Z"
1632
+ "signed_at": "2026-05-31T03:55:39.549Z"
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": "oz8Q5WVaY8au4IjbaZahx/DSaC00Q44ylSL3mDkTerCEpW/EyPUeiLeGxSrWxBCwVFEKSSJvnhJjhvX5lDPcCg==",
1697
- "signed_at": "2026-05-31T03:04:10.840Z"
1697
+ "signed_at": "2026-05-31T03:55:39.549Z"
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": "NAtyzfLPXlUuB78Snb9nWmbZalC1CNlIYN9rYhdEmtB/xQGC6vVnThgrEAHlm7v/jMCFuknvEpUHKdscUnUADw==",
1783
- "signed_at": "2026-05-31T03:04:10.840Z"
1783
+ "signed_at": "2026-05-31T03:55:39.550Z"
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": "1UTjZNC5Lyrgw93LAizdXVeSmv3jS8YQNT1db5OKsldub50+o1FXmAH4+3MxZozaOGDCX3yXbdDJSJaaSmfuAA==",
1852
- "signed_at": "2026-05-31T03:04:10.840Z",
1852
+ "signed_at": "2026-05-31T03:55:39.550Z",
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": "EdsY4xe7YA8X8m+KZUbq49JwoCXgRKEz2eg3m86O37rvBmpm8ppvl9hrsekygvpBh2VmCHL2dEYiOD8OM2n7CA==",
1938
- "signed_at": "2026-05-31T03:04:10.840Z"
1938
+ "signed_at": "2026-05-31T03:55:39.550Z"
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": "fnLKPLkjjRCJ/F9wdmZ1w1lXmqEJvTYkv6Uu+9OTd5vZTWKz3QMuxKOsas+ctCdOvTaeloqPUUprXx+ZZdDpCg==",
2000
- "signed_at": "2026-05-31T03:04:10.841Z",
2000
+ "signed_at": "2026-05-31T03:55:39.551Z",
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": "t3dkdpTX04zvjitEeOJThpgjurLd1UO9GOut4LXSZgY3ULhfknI4zT7G5+m2RSZZTo7yyeZrwpg+7vEg9K6mAw==",
2074
- "signed_at": "2026-05-31T03:04:10.841Z"
2074
+ "signed_at": "2026-05-31T03:55:39.551Z"
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": "+1kmtA6rAvIyDjjy+cJHK6BcfylyVsa5cUjRFijlFR9GsQfB93JnmkEJOqML50pdlcxtJI3yUodHpL3/YJGtCA==",
2136
- "signed_at": "2026-05-31T03:04:10.841Z"
2136
+ "signed_at": "2026-05-31T03:55:39.551Z"
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": "h48ASCz63aBfHzLKxMVDADMuT4atriK0iE6bJeVzZTsx/e8+hyv4fLP7+zYxT9Oe0Gss3v/Xy+t+Wd9uwzV+Aw==",
2216
- "signed_at": "2026-05-31T03:04:10.842Z"
2216
+ "signed_at": "2026-05-31T03:55:39.552Z"
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": "FVBn4ex2qPIo9SHMVJ6tntoz4tVwjbIq3m6wDjjZyv2JODlS+90GBYCOkNamxxkmw/6de6SMs0YHQiF/xjo/DQ==",
2269
- "signed_at": "2026-05-31T03:04:10.842Z"
2269
+ "signed_at": "2026-05-31T03:55:39.552Z"
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": "ZHVdGWCcfG98tSVB0b9mwrsYwv71V3uUEl+6ss7omSQhmNvqV5s6MAZM5YladBt9MK/8T/zBrTYN4gAonOP+BQ==",
2337
- "signed_at": "2026-05-31T03:04:10.842Z"
2337
+ "signed_at": "2026-05-31T03:55:39.553Z"
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": "r9ii4nb3HJELdtKCGF5qy9PHOiot3GC24yfxfGAKlLENHkdRvRkvvL99eV/6RXyfUaMyrnc2Te8tPQcNu5bsDg==",
2417
- "signed_at": "2026-05-31T03:04:10.843Z",
2417
+ "signed_at": "2026-05-31T03:55:39.553Z",
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": "9mfDtMApMAg9V/lmwpniNxo/6gNZoOEoYDfyFvyWvKrPMtc7H9F8uz06FVoARe/J49saAKTVXOurNE1D/KtpCQ==",
2511
- "signed_at": "2026-05-31T03:04:10.843Z",
2511
+ "signed_at": "2026-05-31T03:55:39.553Z",
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": "P48M4BVX6jOHGUQ6Vz/VukXAFs5C+ups9A+RyQyPDHEKq72oiAa5nP7y894Av8c0FEyGnu2lIW4ThcDlIsTZBg=="
2529
+ "signature_base64": "QhxlOl0wE0iDAOFceWx3srt8+wJQXv///CcqkbNxd/1sosR8aW9nVGfr1Ry9PlVibVFmpFareUeFy3S78rI9Dw=="
2530
2530
  }
2531
2531
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/exceptd-skills",
3
- "version": "0.15.51",
3
+ "version": "0.15.52",
4
4
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 11 catalogs (427 CVEs / 173 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:9acfa07c-b6cd-4814-91c7-1c28fa9036c5",
4
+ "serialNumber": "urn:uuid:7ed2b616-e18e-40a0-bf4e-b08af006675e",
5
5
  "version": 1,
6
6
  "metadata": {
7
- "timestamp": "2108-04-22T07:45:00.000Z",
7
+ "timestamp": "2093-06-04T14:26:30.000Z",
8
8
  "tools": [
9
9
  {
10
10
  "vendor": "blamejs",
11
11
  "name": "scripts/refresh-sbom.js",
12
- "version": "0.15.51"
12
+ "version": "0.15.52"
13
13
  }
14
14
  ],
15
15
  "component": {
16
- "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.15.51",
16
+ "bom-ref": "pkg:npm/@blamejs/exceptd-skills@0.15.52",
17
17
  "type": "application",
18
18
  "name": "@blamejs/exceptd-skills",
19
- "version": "0.15.51",
19
+ "version": "0.15.52",
20
20
  "description": "AI security skills grounded in mid-2026 threat reality, not stale framework documentation. 42 skills, 11 catalogs (427 CVEs / 173 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.15.51",
28
+ "purl": "pkg:npm/%40blamejs/exceptd-skills@0.15.52",
29
29
  "hashes": [
30
30
  {
31
31
  "alg": "SHA-256",
32
- "content": "286ba553798e8b3455fe13a1541b186417e4a817f9c33575e26cf828a1c08d6e"
32
+ "content": "926aefd5a4e9417e02388bf3fba187507cb15c4d562d1b73383978b440923626"
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.15.51"
38
+ "url": "https://www.npmjs.com/package/@blamejs/exceptd-skills/v/0.15.52"
39
39
  },
40
40
  {
41
41
  "type": "vcs",
@@ -116,11 +116,11 @@
116
116
  "hashes": [
117
117
  {
118
118
  "alg": "SHA-256",
119
- "content": "dc00c4b9ab8c88da642c093ff93db1017413c78cc05b9018466be6631c71fec5"
119
+ "content": "efc1b8a8b88115b6f3c39a68fd41633df8bc7992f72ec4683e3c1760323df012"
120
120
  },
121
121
  {
122
122
  "alg": "SHA3-512",
123
- "content": "b05cb2e274f890263a22f70998d287f96470f83443b891c4a2958a9e053af429d53556495b7e5e427f3892223c3f255917560a3610bdea991e058eaf7cf670de"
123
+ "content": "8191b5774e3f765a359738f31c9f1dae30e1fae21e9d65c6dd2abc4531823c08761b037bb808ab6cd57815145b15057aeefef9a103e9222e19502315e13bcf7b"
124
124
  }
125
125
  ]
126
126
  },
@@ -161,11 +161,11 @@
161
161
  "hashes": [
162
162
  {
163
163
  "alg": "SHA-256",
164
- "content": "68a43ab8f9495c0af7a10b4f1c7f8fc729aeee2ab5fe523f932bdd62eb971d83"
164
+ "content": "34dd0f6eab14ad5b2c974b81e7016c841c7849f862b58fcc94fd499a5ed2b8a9"
165
165
  },
166
166
  {
167
167
  "alg": "SHA3-512",
168
- "content": "dbf4c6f0414142a42f37a4f977f07d0d5167abe2e899f6775b29039aff8270b094986747a9c875f362945b0fe8a4173260232a51a57af61ec4553ec598039e18"
168
+ "content": "7f16c6f48c03f0fa2858edeab6388a9c2b6a952e7d40186bac5748e2ca90d4b3b085422416640eff8c02ead3dd8e19071c696c8cf9f8f2dcc13e8b9d725fc36c"
169
169
  }
170
170
  ]
171
171
  },
@@ -281,11 +281,11 @@
281
281
  "hashes": [
282
282
  {
283
283
  "alg": "SHA-256",
284
- "content": "cf9b74140298bdee14d4826d11ed9f7d6b1266db8a782d8dce48425e76ec9af5"
284
+ "content": "d0ca92931278d72229edb9f2526157f2832df1d97cd7e8440a0a841f8b1c664d"
285
285
  },
286
286
  {
287
287
  "alg": "SHA3-512",
288
- "content": "0528bbd4277fe3b8f4cb99eeaf35d5e9f310cfd48a0164fa4e4edb03154f1c7302af7d44617d7b6f1d6ab52e301a7772042e4c609693c49b8ce6eb64991419b9"
288
+ "content": "93377a5256af65b898ffda3586e081e64f6bb3d820305b6995dd37f022857a66e929bec0c167fed5aed5c5b18a8dec7e7e4214b3d5cfacea32e93dc8065f01c0"
289
289
  }
290
290
  ]
291
291
  },
@@ -731,11 +731,11 @@
731
731
  "hashes": [
732
732
  {
733
733
  "alg": "SHA-256",
734
- "content": "c5e35e621cfd1702bd048dca73f0a309323b537aa73680967295a4176da7368b"
734
+ "content": "1e2f307a7207a260f8c9ab23f95e8eebb4950349d79eceb2af5b8c4df2b37dc4"
735
735
  },
736
736
  {
737
737
  "alg": "SHA3-512",
738
- "content": "919da50a7973dd8f64cd1088ebbc12ae11800b57dc2a65704d29aa373e63db841d392eee45da2a1cd2cfe6df605252bbbe06e303586ba9ebb6bad7a613031dad"
738
+ "content": "9298e037ccac473c5ac994e5ef4804d93a7abfbb3fa14c836a97c4503de2e9d10b1ad8a2f371115bf17c1f63851fd252214cb183c8655f1f4150b0e3a2dc9248"
739
739
  }
740
740
  ]
741
741
  },
@@ -1331,11 +1331,11 @@
1331
1331
  "hashes": [
1332
1332
  {
1333
1333
  "alg": "SHA-256",
1334
- "content": "fc9d13e98934b99981965e61affae0b03bffd8d8a0ee8f6fbb6cb8f33f2cadb5"
1334
+ "content": "bd757082645bbe88e0d03e5d533ee7c5c8e2ea922e31e13e28a336f69cd3a756"
1335
1335
  },
1336
1336
  {
1337
1337
  "alg": "SHA3-512",
1338
- "content": "4053e54232a55090a53ff765f43bd7c389bdd8ff7d9458c123e1e297ee613f8ed007805588c3e1b6e3857db088d344077ba4d8889efb2129797708ef5f5b58f2"
1338
+ "content": "85c3844a6562a1df8b6b71e54c6fc4788ad62ad918856ceb441c443625032f547c30e39618631f397c49c02708af4e1420bf2ee78885c23248f9c7f8885cef2f"
1339
1339
  }
1340
1340
  ]
1341
1341
  },
@@ -1751,11 +1751,11 @@
1751
1751
  "hashes": [
1752
1752
  {
1753
1753
  "alg": "SHA-256",
1754
- "content": "26f77d56ab70b31946f08cd83c8cd5fc43e807c77955a0e9d4ae9450d32e29c9"
1754
+ "content": "e5f2d2a803f6972ef1759593ddbec1e3badc297b8f83e667fedeaf4b68fd9819"
1755
1755
  },
1756
1756
  {
1757
1757
  "alg": "SHA3-512",
1758
- "content": "b2bfaea029b51a059fb31a734fa65e73366bf075e38af76723fda119fe9202a17a286b3e20b8e7d12b16e7eed9ef68e13e7b652ddb989d3c5cab30077e7748d2"
1758
+ "content": "3388f49da3b6e08d553623d467dc0114c41c3ff39cc780751867908fcc0ba503f8236f1e740700befc5b862e471196a20ebe9590c2200c81e62db12bebe63dfd"
1759
1759
  }
1760
1760
  ]
1761
1761
  },
@@ -2179,6 +2179,21 @@
2179
2179
  }
2180
2180
  ]
2181
2181
  },
2182
+ {
2183
+ "bom-ref": "file:scripts/check-changelog-extract.js",
2184
+ "type": "file",
2185
+ "name": "scripts/check-changelog-extract.js",
2186
+ "hashes": [
2187
+ {
2188
+ "alg": "SHA-256",
2189
+ "content": "eeee0adf320e7aefee1ebcfa2283c021ca43c80938aafe1873abcf21b927686d"
2190
+ },
2191
+ {
2192
+ "alg": "SHA3-512",
2193
+ "content": "710b6d5b6d1a60c1934a2bd42c7a64aab24150a14d74368d2f5eea27bd568708ae0daa2ca3c7fdee286faec577a926e5cfbaa1b6740ad3886fc5bf3f7ed026c9"
2194
+ }
2195
+ ]
2196
+ },
2182
2197
  {
2183
2198
  "bom-ref": "file:scripts/check-codebase-patterns-currency.js",
2184
2199
  "type": "file",
@@ -2261,11 +2276,11 @@
2261
2276
  "hashes": [
2262
2277
  {
2263
2278
  "alg": "SHA-256",
2264
- "content": "467d2ae2c73a803edc61ee31f04a7a584da6aaa729940de799a482a37b2be6c3"
2279
+ "content": "d43e59b96a252c1228ca709e3753582fc02d4c32d2766b247a69ee78c0a18e28"
2265
2280
  },
2266
2281
  {
2267
2282
  "alg": "SHA3-512",
2268
- "content": "4c3638049e850410bebd27ed06f2497fab89be9c5b1917561b9e5ce3a961351c7f3a7076a58b642fb6e72e5aa2597838d77351a13d9eafe725a9e32fba3adfe1"
2283
+ "content": "994eea61e1d84f9f8c9ab77085f855d167b772e31f07ad49f5b4094d7c939f7854595f37902c4a0f360c2ce3ec7754e9db58eee0d7c865dfa2ea20d0e33885e3"
2269
2284
  }
2270
2285
  ]
2271
2286
  },
@@ -2276,11 +2291,11 @@
2276
2291
  "hashes": [
2277
2292
  {
2278
2293
  "alg": "SHA-256",
2279
- "content": "1d3b7bd15af17a88afb82dada50494433299455e76206b23a895f1ff9ca8a696"
2294
+ "content": "906735e02f452962cc221e129a9d7d08cb645f0b79b48168fea62038c35f6b71"
2280
2295
  },
2281
2296
  {
2282
2297
  "alg": "SHA3-512",
2283
- "content": "d4c3a7dc1af799ed3d8f28ba47b87a3ef4781cb28f688e76322d15404772e5199cc292bcee979eaefb19d0bb519635353a328412c59fd5baa412548282637ec8"
2298
+ "content": "09590d90712ecec967caf198f33f0ea62b24152dcfda51b1bd0422e64add9cf2f97a5873310d11d654846b83dd92c9c82ca014973ca51888d14607e2b3092e0f"
2284
2299
  }
2285
2300
  ]
2286
2301
  },
@@ -2291,11 +2306,11 @@
2291
2306
  "hashes": [
2292
2307
  {
2293
2308
  "alg": "SHA-256",
2294
- "content": "ab58a0d6a5f43e94f63161af45b4e436fe50f898e9a44588bdc15103dbc31aa8"
2309
+ "content": "2306d6560552040baf57e9a552c14f2344967338babdc1a4fff8e3b4fc0d4d9b"
2295
2310
  },
2296
2311
  {
2297
2312
  "alg": "SHA3-512",
2298
- "content": "36a39edf675786de2e7417e6097cd18f03f499ef994b8d728f93f1ca7b657e5c577f2b6ad7360a3470e7fba6287cbc65ae1eb98578c0d4ce689bd9e0f533c398"
2313
+ "content": "12d2a7a167fa40712453f60e5e1f8f57d35a2396e77b40afc38986ddf866984157ed5c312fe6eddb9803df4025e52628d7d636ee123118b64f9eef5b5644cf23"
2299
2314
  }
2300
2315
  ]
2301
2316
  },
@@ -2321,11 +2336,11 @@
2321
2336
  "hashes": [
2322
2337
  {
2323
2338
  "alg": "SHA-256",
2324
- "content": "4622fd121015535bb0a1b47a7bf57abe3a195b02e42a0be99dfadc9d67a72160"
2339
+ "content": "3e37c0d93dfc1ff3c6fd5e90bb65417f3ebcabd8435925c4292b4ca86bdd11c2"
2325
2340
  },
2326
2341
  {
2327
2342
  "alg": "SHA3-512",
2328
- "content": "85f755a9228afe24b704d39d58ee11436c8dd663df47e8c7c84da9454c5bad1cf9f166c833a9648517f9c2689730103617b41bec0739c637d91fafb041f7db63"
2343
+ "content": "8c1be019521e12c483cb5e71ee94e6601497c514b09080c2560ca942bb2979f0b08928866213b13fc882105f6aa6e33901fb7edb8cc7dad9a1f03d8bd8a45ce6"
2329
2344
  }
2330
2345
  ]
2331
2346
  },
@@ -2471,11 +2486,11 @@
2471
2486
  "hashes": [
2472
2487
  {
2473
2488
  "alg": "SHA-256",
2474
- "content": "a5a43d931bcc09f0e5866e5860efd373943e5ce797ff368dc009ea4161d03cd6"
2489
+ "content": "f199cc754f202fa994c1769af5e02a4ef995ee02e87221422b00f945b03e99db"
2475
2490
  },
2476
2491
  {
2477
2492
  "alg": "SHA3-512",
2478
- "content": "dff963f00485e37546f605542d94d5e8730be9b888d98dd9bc9d256f38b4820d3c3346b1bfdbacedefc4b47d7b07afffdd1b3d7a77ba4cf71593efdd5530b9af"
2493
+ "content": "d504c65cda545acda811997487ca8b6fef3ee5b5447b537f502295d5f6f4266cb2ecb743389b04aa2b6118b021af6f2ce9c6f00e44cf60bd7c06b72456a500f8"
2479
2494
  }
2480
2495
  ]
2481
2496
  },
@@ -2531,11 +2546,11 @@
2531
2546
  "hashes": [
2532
2547
  {
2533
2548
  "alg": "SHA-256",
2534
- "content": "59a341868f7f362d4138d61cbbf480fa5e293e06399b58b57ea6ff27e4e66d03"
2549
+ "content": "fd04f3e4122b4f3ca7e9a266c763dd4e954710d496da8072f5c2e500a4dbe32c"
2535
2550
  },
2536
2551
  {
2537
2552
  "alg": "SHA3-512",
2538
- "content": "415dec1034ba285597cf8dcde916b8cb3e9f526a1b7df3cf871c55843de659dcc0dc0cb970e25dbdb646b5da40a7383b8f2cea51d76c8ec1a924fe1913123ff2"
2553
+ "content": "9b606b4d5afcc79a7a39650c3f425af0a8abb21f484e70e5469431a0be26b27969108bd51f167cab23b41d6e0ec7d14b8e6d82d597ca6166942605e15fc016c0"
2539
2554
  }
2540
2555
  ]
2541
2556
  },
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Release-notes extraction + quality gate.
6
+ *
7
+ * The release workflow (.github/workflows/release.yml) publishes the GitHub
8
+ * Release body by awk-extracting the `## <version> ...` CHANGELOG section
9
+ * between the version heading and the next `## ` heading, falling back to a
10
+ * generic "Release of v<version>." line if the extract is empty. This gate
11
+ * runs that SAME extraction locally before tag-push, and additionally lints
12
+ * the extracted notes for operator-facing quality, so a malformed or
13
+ * internal-narrative-laced section fails here rather than shipping as the
14
+ * public release body.
15
+ *
16
+ * Two layers:
17
+ * 1. EXTRACT — the `## <version> — <date>` section exists, is non-empty
18
+ * (won't trigger the workflow's "Release of v…" fallback),
19
+ * the heading version matches package.json, and the heading
20
+ * carries an ISO date.
21
+ * 2. LINT — the extracted body is operator-facing-clean: no internal
22
+ * phase/pass/slice/sweep narrative, no agent-dispatch /
23
+ * conversation residue, no tautological "all tests pass"
24
+ * noise. (Mirrors the operator-facing discipline; the release
25
+ * body is the most public surface there is.)
26
+ *
27
+ * Exit: process.exitCode 0 on pass, 1 on any failure. Functions are exported
28
+ * for fixture-based testing (no subprocess needed).
29
+ *
30
+ * Usage:
31
+ * node scripts/check-changelog-extract.js # uses package.json version
32
+ * node scripts/check-changelog-extract.js <version> # explicit MAJOR.MINOR.PATCH
33
+ */
34
+
35
+ const fs = require('node:fs');
36
+ const path = require('node:path');
37
+
38
+ const ROOT = path.resolve(__dirname, '..');
39
+ const CHANGELOG = path.join(ROOT, 'CHANGELOG.md');
40
+ const PACKAGE_JSON = path.join(ROOT, 'package.json');
41
+
42
+ // Replicates the release.yml awk: capture lines AFTER the `## <version> `
43
+ // heading up to (not including) the next `## ` heading. The trailing space in
44
+ // the heading match mirrors the workflow's `"^## " v " "` so a shorter version
45
+ // heading can't accidentally match a longer one that shares its prefix.
46
+ function extractSection(text, version) {
47
+ const lines = text.split(/\r?\n/);
48
+ const out = [];
49
+ let capturing = false;
50
+ const startRe = new RegExp('^## ' + version.replace(/\./g, '\\.') + ' ');
51
+ for (const ln of lines) {
52
+ if (capturing) {
53
+ if (/^## /.test(ln)) break;
54
+ out.push(ln);
55
+ continue;
56
+ }
57
+ if (startRe.test(ln)) capturing = true;
58
+ }
59
+ // Trim leading/trailing blank lines (awk keeps them; the body is the same
60
+ // either way, but trimming makes the non-empty test honest).
61
+ while (out.length && out[0].trim() === '') out.shift();
62
+ while (out.length && out[out.length - 1].trim() === '') out.pop();
63
+ return out;
64
+ }
65
+
66
+ // Returns the `## <version> — <date>` heading line for the version, or null.
67
+ function headingLine(text, version) {
68
+ const re = new RegExp('^## ' + version.replace(/\./g, '\\.') + ' ');
69
+ return text.split(/\r?\n/).find((l) => re.test(l)) || null;
70
+ }
71
+
72
+ // Operator-facing forbidden patterns. Tight, high-confidence internal-narrative
73
+ // markers only — must not false-positive on legitimate operator prose (e.g. a
74
+ // bare "phase" in "multi-phase attack" is fine; "Phase 9" is the tell). Each
75
+ // entry: { id, re, why }.
76
+ const FORBIDDEN = [
77
+ { id: 'phase-number', re: /\bphase\s+\d/i, why: 'internal phase number (operators have no roadmap)' },
78
+ { id: 'pass-number', re: /\b(?:audit|curation|drift|fix|bug)?[- ]?pass\s+\d/i, why: 'internal pass/batch number' },
79
+ { id: 'slice-number', re: /\bslice\s+\d/i, why: 'internal slice number' },
80
+ { id: 'sweep-number', re: /\bsweep\s+\d/i, why: 'internal sweep number' },
81
+ { id: 'tier-letter', re: /\bTier-[ABC]\b/, why: 'internal tier label' },
82
+ { id: 'agent-dispatch', re: /\b(?:sub-?agent|parallel agent|agent dispatch|fan(?:ned)?[ -]out|multi-agent)\b/i, why: 'implementation detail (agent/parallelization)' },
83
+ { id: 'conversation-residue', re: /\b(?:as discussed|per your|operator-confirmed|as you (?:noted|requested)|per the conversation|PR feedback:)\b/i, why: 'conversation residue (invisible to the reader)' },
84
+ { id: 'process-narrative', re: /\b(?:audit-derived|post-phase-\d|as part of the \d|the \d+-gap closure)\b/i, why: 'internal-process narrative' },
85
+ { id: 'tautological-green', re: /\b(?:all tests (?:pass|passing|green)|CI green|smoke \+ e2e (?:clean|pass)|tests? (?:are )?passing)\b/i, why: 'tautological pass/green claim (noise — the release exists)' },
86
+ ];
87
+
88
+ function lintOperatorClean(sectionLines) {
89
+ const findings = [];
90
+ sectionLines.forEach((ln, i) => {
91
+ for (const rule of FORBIDDEN) {
92
+ const m = ln.match(rule.re);
93
+ if (m) findings.push({ rule: rule.id, why: rule.why, line: i + 1, match: m[0], text: ln.trim().slice(0, 100) });
94
+ }
95
+ });
96
+ return findings;
97
+ }
98
+
99
+ function readPackageVersion() {
100
+ return JSON.parse(fs.readFileSync(PACKAGE_JSON, 'utf8')).version;
101
+ }
102
+
103
+ function main() {
104
+ const version = process.argv[2] || readPackageVersion();
105
+ if (!/^\d+\.\d+\.\d+$/.test(version)) {
106
+ console.error('[check-changelog-extract] FAIL: bad version ' + JSON.stringify(version) + ' (expected MAJOR.MINOR.PATCH)');
107
+ process.exitCode = 1;
108
+ return;
109
+ }
110
+
111
+ let text;
112
+ try { text = fs.readFileSync(CHANGELOG, 'utf8'); }
113
+ catch (e) {
114
+ console.error('[check-changelog-extract] FAIL: cannot read CHANGELOG.md: ' + (e && e.message || e));
115
+ process.exitCode = 1;
116
+ return;
117
+ }
118
+
119
+ const heading = headingLine(text, version);
120
+ if (!heading) {
121
+ console.error('[check-changelog-extract] FAIL: no `## ' + version + ' …` heading in CHANGELOG.md — the release workflow extract would be empty and fall back to "Release of v' + version + '."');
122
+ process.exitCode = 1;
123
+ return;
124
+ }
125
+ // Heading must carry an ISO date: `## <version> — YYYY-MM-DD`.
126
+ if (!new RegExp('^## ' + version.replace(/\./g, '\\.') + ' [—-] \\d{4}-\\d{2}-\\d{2}\\s*$').test(heading)) {
127
+ console.error('[check-changelog-extract] FAIL: heading does not match `## ' + version + ' — YYYY-MM-DD`:');
128
+ console.error('[check-changelog-extract] got: ' + JSON.stringify(heading));
129
+ process.exitCode = 1;
130
+ return;
131
+ }
132
+
133
+ const section = extractSection(text, version);
134
+ if (section.length === 0) {
135
+ console.error('[check-changelog-extract] FAIL: v' + version + ' section is empty — the release body would fall back to the generic "Release of v' + version + '." line.');
136
+ process.exitCode = 1;
137
+ return;
138
+ }
139
+
140
+ const findings = lintOperatorClean(section);
141
+ if (findings.length > 0) {
142
+ console.error('[check-changelog-extract] FAIL: v' + version + ' release notes carry ' + findings.length + ' operator-facing violation(s):');
143
+ for (const f of findings) {
144
+ console.error(' • [' + f.rule + '] "' + f.match + '" — ' + f.why);
145
+ console.error(' ' + f.text);
146
+ }
147
+ console.error('[check-changelog-extract] The CHANGELOG section IS the public GitHub Release body. Describe the change, not how you arrived at it.');
148
+ process.exitCode = 1;
149
+ return;
150
+ }
151
+
152
+ console.log('[check-changelog-extract] OK — v' + version + ' release notes extract cleanly (' + section.length + ' line(s)) and pass the operator-facing lint.');
153
+ process.exitCode = 0;
154
+ }
155
+
156
+ module.exports = { extractSection, headingLine, lintOperatorClean, FORBIDDEN };
157
+
158
+ if (require.main === module) main();
@@ -44,7 +44,7 @@ exports). Missing references become findings.
44
44
  Changes in these locations are accepted without a covering test:
45
45
 
46
46
  - `*.md` outside `data/`, `.gitignore`, `.npmrc`, `.editorconfig`
47
- - `CHANGELOG.md` / `README.md` / `CONTRIBUTING.md` / `SECURITY.md` / `LICENSE` / `NOTICE` / `CODE_OF_CONDUCT.md` / `AGENTS.md` / `CLAUDE.md`
47
+ - `CHANGELOG.md` / `README.md` / `CONTRIBUTING.md` / `SECURITY.md` / `LICENSE` / `NOTICE` / `CODE_OF_CONDUCT.md` / `AGENTS.md`
48
48
  - Whitespace-only diffs (detected via `git diff --ignore-all-space --ignore-blank-lines`)
49
49
  - Any file under `tests/` (no test-of-tests recursion)
50
50
  - `skills/<name>/skill.md` (signature gate already covers content integrity)
@@ -198,7 +198,7 @@ function readMaybe(p) {
198
198
  // / .editorconfig are tooling). Edits here never need a regression test.
199
199
  const DOCS_ALWAYS_GREEN = new Set([
200
200
  "CONTRIBUTING.md", "LICENSE", "NOTICE", "CODE_OF_CONDUCT.md",
201
- "CLAUDE.md", "SUPPORT.md", ".gitignore", ".npmrc", ".editorconfig",
201
+ "SUPPORT.md", ".gitignore", ".npmrc", ".editorconfig",
202
202
  ]);
203
203
 
204
204
  // Operator-facing docs (release notes, install instructions, security
@@ -497,7 +497,7 @@ function coversCveIoc(corpus, cveId) {
497
497
 
498
498
  // --- Class-level lint: ban coincidence-passing notEqual(r.status, 0) --------
499
499
  //
500
- // CLAUDE.md anti-coincidence rule: every exit-code assertion must pin the
500
+ // Anti-coincidence rule: every exit-code assertion must pin the
501
501
  // EXACT code. `assert.notEqual(r.status, 0)` silently passes when an
502
502
  // unrelated failure produces ANY non-zero exit, hiding the regression the
503
503
  // test was meant to catch. This lint walks tests/*.test.js and rejects the
@@ -635,7 +635,7 @@ function analyze(opts) {
635
635
  file: f.file,
636
636
  kind: "coincidence-assert",
637
637
  surface: f.snippet,
638
- change: `line ${f.line}: pin to exact exit code; see CLAUDE.md anti-coincidence rule. Opt out only with \`// allow-notEqual: <reason>\` on the same line for genuine refusal-pins.`,
638
+ change: `line ${f.line}: pin to exact exit code (anti-coincidence rule). Opt out only with \`// allow-notEqual: <reason>\` on the same line for genuine refusal-pins.`,
639
639
  });
640
640
  }
641
641
 
@@ -34,6 +34,7 @@
34
34
 
35
35
  const fs = require("node:fs");
36
36
  const path = require("node:path");
37
+ const { execFileSync } = require("node:child_process");
37
38
 
38
39
  const ROOT = path.join(__dirname, "..");
39
40
  const BASELINE_PATH = path.join(ROOT, "tests", ".version-tag-baseline.json");
@@ -64,10 +65,33 @@ const COMMENT_EXEMPT = new Set([
64
65
  "CHANGELOG.md",
65
66
  "lib/version-pins.js",
66
67
  "scripts/check-version-tags.js",
67
- // Gitignored local-only contributor docs never shipped.
68
- "CLAUDE.md",
68
+ // The release-notes-extract gate test asserts version-based CHANGELOG
69
+ // extraction + the shorter-vs-longer prefix-collision guard, so its fixtures
70
+ // MUST embed real `## X.Y.Z` headings (e.g. 0.15.5 vs 0.15.50) — load-bearing
71
+ // test data, not sprinkled release tags.
72
+ "tests/check-changelog-extract.test.js",
69
73
  ]);
70
74
 
75
+ // Git-ignored files (a contributor's local-only working docs, scratch) are
76
+ // never scanned — the gate enforces on the would-be-shipped surface, with no
77
+ // need to name individual local-only files. Untracked-but-NOT-ignored files
78
+ // ARE still scanned: a new file a contributor is about to commit is exactly
79
+ // what the gate must catch. Computed via `git check-ignore` over the walked set.
80
+ function gitIgnoredSet(relPaths) {
81
+ if (!relPaths.length) return new Set();
82
+ try {
83
+ const out = execFileSync("git", ["check-ignore", "--stdin"], {
84
+ cwd: ROOT, input: relPaths.join("\n"), encoding: "utf8", maxBuffer: 64 * 1024 * 1024,
85
+ });
86
+ return new Set(out.split(/\r?\n/).filter(Boolean));
87
+ } catch (e) {
88
+ // `git check-ignore --stdin` exits 1 when NO path is ignored (not an
89
+ // error); any paths it did match are on stdout. Absent that, none ignored.
90
+ const out = e && e.stdout ? String(e.stdout) : "";
91
+ return new Set(out.split(/\r?\n/).filter(Boolean));
92
+ }
93
+ }
94
+
71
95
  // Pattern: project version like `v0.13.22` or bare `0.13.22`. Matches
72
96
  // our pre-1.0 release range. External package versions like ATLAS
73
97
  // `v5.6.0` or CycloneDX `1.6` don't match because the major is 0.
@@ -119,9 +143,14 @@ function countCommentViolations(rel) {
119
143
 
120
144
  function scanCurrent() {
121
145
  const files = walk(ROOT);
146
+ const ignored = gitIgnoredSet(files);
122
147
  const byFile = {};
123
148
  const filenameViolations = [];
124
149
  for (const rel of files) {
150
+ // Skip git-ignored, local-only files (a contributor's private working notes
151
+ // that `git clone` never ships). Untracked-but-not-ignored files are still
152
+ // scanned — a new file about to be committed is what the gate guards.
153
+ if (ignored.has(rel)) continue;
125
154
  if (FILENAME_VERSION_RE.test(rel)) filenameViolations.push(rel);
126
155
  const n = countCommentViolations(rel);
127
156
  if (n > 0) byFile[rel] = n;
@@ -249,6 +249,19 @@ const GATES = [
249
249
  args: [path.join(ROOT, "scripts", "check-codebase-patterns.js")],
250
250
  ciJobName: "Data integrity (catalog + manifest snapshot)",
251
251
  },
252
+ {
253
+ // Release-notes extract + quality gate. Runs the same `## <version>`
254
+ // CHANGELOG extraction the release workflow publishes as the GitHub
255
+ // Release body, and lints it for operator-facing quality (no internal
256
+ // phase/pass/slice narrative, no agent-dispatch / conversation residue,
257
+ // no tautological green claims). A malformed or internal-narrative section
258
+ // fails here rather than shipping as the public release body / falling
259
+ // back to the generic "Release of v<version>." line.
260
+ name: "Release-notes extract + operator-facing lint (CHANGELOG section)",
261
+ command: process.execPath,
262
+ args: [path.join(ROOT, "scripts", "check-changelog-extract.js")],
263
+ ciJobName: "Data integrity (catalog + manifest snapshot)",
264
+ },
252
265
  ];
253
266
 
254
267
  function runGate(gate) {
@@ -254,6 +254,11 @@ function cmdPrepare(opts) {
254
254
  process.exit(2);
255
255
  }
256
256
 
257
+ // The `## <next>` heading exists; confirm the section extracts cleanly and
258
+ // passes the operator-facing lint (the release workflow publishes it verbatim
259
+ // as the GitHub Release body). Fail fast here rather than at the gates phase.
260
+ _run("node", ["scripts/check-changelog-extract.js", next]);
261
+
257
262
  _writeJsonVersion("package.json", next);
258
263
  _writeJsonVersion("manifest.json", next);
259
264
  _ok("bumped package.json + manifest.json → " + next);
@@ -344,7 +344,7 @@ try {
344
344
  // itself report 0/38 on any tree where line-ending normalization
345
345
  // touched the source between sign and pack — a Windows contributor
346
346
  // with `core.autocrlf=true`, or a tool like Prettier between sign and
347
- // pack. CLAUDE.md flags this as the recurring CRLF-bypass class.
347
+ // pack. This is the recurring CRLF/line-ending-bypass class.
348
348
  const rawContent = fs.readFileSync(skillPath);
349
349
  const normalizedContent = normalizeSkillBytes(rawContent);
350
350
  const ok = crypto.verify(null, normalizedContent, pubKey, Buffer.from(s.signature, "base64"));