@blamejs/exceptd-skills 0.16.23 → 0.16.24
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 +18 -0
- package/agents/report-generator.md +2 -2
- package/bin/exceptd.js +72 -25
- package/data/_indexes/_meta.json +2 -2
- package/data/_indexes/chains.json +354 -177
- package/data/_indexes/section-offsets.json +35 -35
- package/lib/collectors/ai-api.js +112 -7
- package/lib/collectors/citation-hygiene.js +27 -0
- package/lib/collectors/crypto-codebase.js +25 -0
- package/lib/collectors/kernel.js +32 -2
- package/lib/collectors/library-author.js +30 -0
- package/lib/collectors/runtime.js +38 -3
- package/lib/collectors/sbom.js +21 -2
- package/lib/collectors/secrets.js +125 -0
- package/lib/cve-regression-watcher.js +5 -2
- package/lib/playbook-runner.js +16 -3
- package/manifest.json +53 -53
- package/orchestrator/README.md +1 -1
- package/orchestrator/index.js +17 -3
- package/package.json +1 -1
- package/sbom.cdx.json +50 -50
- package/scripts/builders/cwe-chains.js +1 -0
- package/scripts/builders/section-offsets.js +10 -2
- package/scripts/builders/token-budget.js +3 -3
- package/scripts/check-changelog-extract.js +38 -1
- package/scripts/check-version-tags.js +5 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.24 — 2026-06-05
|
|
4
|
+
|
|
5
|
+
Attestation replay tamper-gating, collector detection integrity, and lock-lifecycle fixes.
|
|
6
|
+
|
|
7
|
+
### Bugs
|
|
8
|
+
|
|
9
|
+
A host whose `keys/public.pem` fails the `EXPECTED_FINGERPRINT` pin is now a first-class tamper state on the replay path: `reattest` refuses with exit 6 (TAMPERED) unless `--force-replay`, the same way `attest verify` already refused. Previously the pin mismatch — the exact key-swap attack the pin exists to surface — fell through to a benign "unsigned attestations are an operator config issue" note and the replay proceeded. The sidecar audit classification gains a matching `fingerprint-mismatch` label, and the replay-refusal predicate now lives in one shared helper so a future tamper class has exactly one place to extend.
|
|
10
|
+
|
|
11
|
+
Piping a collector into a run — `exceptd collect secrets | exceptd run secrets --evidence -` — now classifies real findings as detected instead of silently inconclusive. Collectors flipped false-positive-gated indicators to "hit" without attesting the FP checks they had actually performed, so the engine's honest-downgrade gate demoted every such hit. Eight collectors now perform and attest their deterministic FP checks (example-key demotion, placeholder and entropy floors, context co-occurrence, test-path exclusion, kernel-config reads, sticky-bit/socket classification, registry-tarball and mutable-ref analysis); indicators whose remaining checks genuinely require a network probe or operator judgement stay inconclusive by design, and a guard test enforces that any collector flipping a gated indicator either attests its checks or documents why it abstains.
|
|
12
|
+
|
|
13
|
+
A run invoked with `--bundle-deterministic` against pathologically nested evidence no longer leaks the playbook's cross-process mutex: the depth-limit refusal threw after the lock was taken but before the release path was armed, so every later run of that playbook on the same host was blocked with a phantom "concurrent run holds the mutex" until the process exited. Attestation creation no longer strands an orphaned, unsigned attestation body holding the session slot when the body lands but its signature sidecar cannot be placed — the slot is released and the create can be retried; the unsafe-filename refusal also now returns its structured envelope instead of crashing. `doctor --ai-config --fix` no longer builds an `icacls` grant from an unset `USERNAME` (which stripped ACL inheritance and then failed on the unresolvable account, locking the file out); it resolves the user via the OS and withholds the automatic fix when no name is resolvable. The watch daemon's lockfile stale-reclaim race now exits 75 (WATCH_LOCK_CONTENTION) like every other contention path instead of a generic failure, and the CVE regression watcher stamps reports with the run's clock rather than a module-load timestamp.
|
|
14
|
+
|
|
15
|
+
End-to-end detection scenarios now assert that the staged indicator actually fired instead of accepting the operator-supplied verdict alone, one scenario no longer masks the engine's computed classification with an override, and RWEP floors are pinned at the computed values so a per-indicator weight regression trips them. The derived-index builders count `###` headings fence-aware (code-block lines no longer inflate section offsets), emit the documented `dlp_refs` key on CWE chains, and describe their real output shape; the zero-day response report template and remaining docs now state the blast-radius ceiling as 0-30.
|
|
16
|
+
|
|
3
17
|
## 0.16.23 — 2026-06-05
|
|
4
18
|
|
|
5
19
|
A broad correctness and hardening pass across the runtime, the data-refresh pipeline, the catalogs, and the release tooling.
|
|
@@ -26,6 +40,8 @@ The release orchestrator could report a release complete while the publish was u
|
|
|
26
40
|
|
|
27
41
|
The secret-scanning configuration no longer allowlists the entire playbooks directory by path — targeted patterns cover the legitimate example values instead, so a real credential committed into a playbook file is detectable again. Workflow version comments now match their pinned action SHAs, the publish workflow pins the same exact Node version the gates run on (held in lockstep by a three-way test against CI and the Docker harness), the ATLAS-currency workflow detects stale skills structurally instead of grepping a prose string, and the automated data-refresh PR body describes the structural gate it actually runs. The packaging gate pins every module the CLI requires at launch, the SBOM gate verifies the entry counts embedded in the published description against the live catalogs, skill frontmatter is validated against the shipped schema (previously decorative), and a new guard asserts that every byte-hashed shipped file type carries an LF-forcing `.gitattributes` rule. The catalog inventory in CONTEXT.md and the counts in the package description are pinned by tests so they fail loudly instead of rotting silently.
|
|
28
42
|
|
|
43
|
+
## 0.16.22 — 2026-06-05
|
|
44
|
+
|
|
29
45
|
When the automated external-data refresh applies a CISA KEV listing change to a catalog entry, it now updates the entry's RWEP factor and score in the same write, honouring whichever factor shape the entry stores. Previously only the flag flipped, leaving the stored score failing the factor-sum invariant the catalog enforces — the first real KEV listing the refresh applied surfaced this as a 25-point mismatch. A first listing also now carries its KEV date: the drift check emitted a date change only when the local entry already had one, so a newly listed CVE arrived dated null. The refresh workflow's post-apply gate now runs structural validation (catalog schema and RWEP coherence, cross-references, index freshness, skill lint) instead of the full test suite: the suite's curation-completeness checks assert guarantees that human curation supplies after import, so they gate the automated data PR's merge rather than its creation. The workflow also regenerates the SBOM over freshly applied data and consumes its own just-built prefetch cache on runners that have no signing key.
|
|
30
46
|
|
|
31
47
|
## 0.16.21 — 2026-06-04
|
|
@@ -2152,6 +2168,8 @@ Documentation accuracy pass. README.md + ARCHITECTURE.md were still pinning ATLA
|
|
|
2152
2168
|
**`tests/docs-catalog-counts-pinned.test.js`** — new contract test asserts that README.md and ARCHITECTURE.md text matches the live catalog state for: ATLAS version (`data/atlas-ttps.json._meta.atlas_version`), ATT&CK version (`data/attack-techniques.json._meta.attack_version`), skill count (`manifest.json.skills.length`), D3FEND entry count, CVE catalog count, framework-gap entry count. Any future PR that bumps a catalog without updating the operator-facing docs fails the gate at CI time — eliminates the silent-drift class that v0.12.34 cleaned up.
|
|
2153
2169
|
|
|
2154
2170
|
|
|
2171
|
+
## 0.12.33 — 2026-05-15
|
|
2172
|
+
|
|
2155
2173
|
Same-day CVE intake (node-ipc supply-chain compromise) + cleanup of the long-standing `cred-stores` skill-vs-playbook semantic confusion.
|
|
2156
2174
|
|
|
2157
2175
|
### Features
|
|
@@ -36,7 +36,7 @@ Generate structured, audience-appropriate reports from skill outputs. Translates
|
|
|
36
36
|
### 2. Technical Assessment Report
|
|
37
37
|
|
|
38
38
|
**Audience:** Security engineers, DevOps, Platform teams
|
|
39
|
-
**Template:**
|
|
39
|
+
**Template:** No canonical template — assemble from the content list below.
|
|
40
40
|
**Content:**
|
|
41
41
|
- Full CVE inventory with CVSS + RWEP
|
|
42
42
|
- Specific remediation commands and configurations
|
|
@@ -74,7 +74,7 @@ Generate structured, audience-appropriate reports from skill outputs. Translates
|
|
|
74
74
|
### 4. Threat Model Update Report
|
|
75
75
|
|
|
76
76
|
**Audience:** Security architects, threat modeling teams
|
|
77
|
-
**Template:**
|
|
77
|
+
**Template:** No canonical template — assemble from the content list below.
|
|
78
78
|
**Content:**
|
|
79
79
|
- Currency score before and after update
|
|
80
80
|
- Specific threat classes added
|
package/bin/exceptd.js
CHANGED
|
@@ -4449,7 +4449,7 @@ function persistAttestation(args) {
|
|
|
4449
4449
|
if (!/^[A-Za-z0-9._-]{1,64}\.json$/.test(filename || "")) {
|
|
4450
4450
|
return {
|
|
4451
4451
|
ok: false,
|
|
4452
|
-
error: `Refusing to persist attestation with unsafe filename: ${JSON.stringify(filename).slice(0, 80)}.`,
|
|
4452
|
+
error: `Refusing to persist attestation with unsafe filename: ${String(JSON.stringify(filename)).slice(0, 80)}.`,
|
|
4453
4453
|
existingPath: null,
|
|
4454
4454
|
};
|
|
4455
4455
|
}
|
|
@@ -4526,7 +4526,17 @@ function persistAttestation(args) {
|
|
|
4526
4526
|
fs.renameSync(jsonTmp, filePath);
|
|
4527
4527
|
}
|
|
4528
4528
|
// Slot won — place the sidecar (sigPath is fresh on a create).
|
|
4529
|
-
|
|
4529
|
+
try {
|
|
4530
|
+
fs.renameSync(sigTmp, sigPath);
|
|
4531
|
+
} catch (sigErr) {
|
|
4532
|
+
// The body landed but its sidecar did not. Left in place, the
|
|
4533
|
+
// orphaned unsigned body would hold the slot forever: every
|
|
4534
|
+
// retry collides with EEXIST and verification reports the
|
|
4535
|
+
// attestation unsigned. Release the slot before rethrowing so
|
|
4536
|
+
// the create can be retried cleanly.
|
|
4537
|
+
try { fs.unlinkSync(filePath); } catch { /* best-effort slot release */ }
|
|
4538
|
+
throw sigErr;
|
|
4539
|
+
}
|
|
4530
4540
|
try { fs.unlinkSync(jsonTmp); } catch { /* hard-link path leaves a second name */ }
|
|
4531
4541
|
} else {
|
|
4532
4542
|
// Force-overwrite, under the persist lock: atomic replace of both.
|
|
@@ -4913,7 +4923,12 @@ function verifyAttestationSidecar(attFile) {
|
|
|
4913
4923
|
if (pubKey) {
|
|
4914
4924
|
const pinError = assertExpectedFingerprint(pubKey);
|
|
4915
4925
|
if (pinError) {
|
|
4916
|
-
|
|
4926
|
+
// A pin mismatch means the host's public key is NOT the published
|
|
4927
|
+
// one — verification against it would prove nothing. Carry a tamper
|
|
4928
|
+
// class so consumers (reattest's refusal predicate, the sidecar
|
|
4929
|
+
// classifier) treat this as tamper evidence, not a benign
|
|
4930
|
+
// unsigned-attestation config state.
|
|
4931
|
+
return { file: attFile, signed: false, verified: false, tamper_class: "fingerprint-mismatch", reason: pinError };
|
|
4917
4932
|
}
|
|
4918
4933
|
}
|
|
4919
4934
|
if (!fs.existsSync(sigPath)) {
|
|
@@ -5137,25 +5152,7 @@ function cmdReattest(runner, args, runOpts, pretty) {
|
|
|
5137
5152
|
// tampering. `verified === false && signed === true` is the real tamper
|
|
5138
5153
|
// signal.
|
|
5139
5154
|
const verify = verifyAttestationSidecar(attFile);
|
|
5140
|
-
|
|
5141
|
-
// (signed-but-invalid, sidecar-corrupt, unsigned-substitution) refuses
|
|
5142
|
-
// replay unless --force-replay is set. A predicate of only
|
|
5143
|
-
// `verify.signed && !verify.verified` would miss corrupt-JSON sidecars
|
|
5144
|
-
// and substituted "unsigned" sidecars on a host WITH a private key —
|
|
5145
|
-
// both of which let replay proceed against forged input.
|
|
5146
|
-
const isSignedTamper = verify.signed && !verify.verified;
|
|
5147
|
-
const isClassTamper = !verify.signed && (
|
|
5148
|
-
verify.tamper_class === "sidecar-corrupt"
|
|
5149
|
-
|| verify.tamper_class === "unsigned-substitution"
|
|
5150
|
-
// Extend tamper-class refusal to algorithm-unsupported sidecars —
|
|
5151
|
-
// anything other than "Ed25519" or "unsigned". Without explicit
|
|
5152
|
-
// refusal, a sidecar that throws inside crypto.verify (e.g.
|
|
5153
|
-
// signature_base64 missing on a downgrade-bait shape) emerges as
|
|
5154
|
-
// signed:true + verified:false through the catch block by accident.
|
|
5155
|
-
// The strict pre-check surfaces the class directly; refuse on it too.
|
|
5156
|
-
|| verify.tamper_class === "algorithm-unsupported"
|
|
5157
|
-
);
|
|
5158
|
-
if ((isSignedTamper || isClassTamper) && !args["force-replay"]) {
|
|
5155
|
+
if (isTamperedSidecarVerify(verify) && !args["force-replay"]) {
|
|
5159
5156
|
process.stderr.write(`[exceptd reattest] TAMPERED: attestation at ${attFile} failed Ed25519 verification (${verify.reason}). Refusing to replay against forged input. Pass --force-replay to override (the replay output records sidecar_verify so the audit trail captures the override).\n`);
|
|
5160
5157
|
const body = {
|
|
5161
5158
|
ok: false,
|
|
@@ -5170,7 +5167,7 @@ function cmdReattest(runner, args, runOpts, pretty) {
|
|
|
5170
5167
|
process.exitCode = EXIT_CODES.TAMPERED;
|
|
5171
5168
|
return;
|
|
5172
5169
|
}
|
|
5173
|
-
if ((
|
|
5170
|
+
if (isTamperedSidecarVerify(verify) && args["force-replay"]) {
|
|
5174
5171
|
process.stderr.write(`[exceptd reattest] WARNING: --force-replay overriding failed signature verification on ${attFile} (${verify.reason}). The replay output records sidecar_verify so the override is audit-visible.\n`);
|
|
5175
5172
|
} else if (!verify.signed && verify.reason && verify.reason.includes("no .sig sidecar") && !args["force-replay"]) {
|
|
5176
5173
|
// missing-sidecar is NOT benign. The previous flow accepted
|
|
@@ -5456,6 +5453,39 @@ function cmdReattest(runner, args, runOpts, pretty) {
|
|
|
5456
5453
|
* sidecar_verify object so auditors can filter override events by class
|
|
5457
5454
|
* without regexing the human-readable reason string.
|
|
5458
5455
|
*/
|
|
5456
|
+
/**
|
|
5457
|
+
* Tamper predicate over a verifyAttestationSidecar() result. Collapses
|
|
5458
|
+
* tamper-class detection: any non-benign sidecar state refuses replay
|
|
5459
|
+
* unless --force-replay is set. A predicate of only
|
|
5460
|
+
* `verify.signed && !verify.verified` would miss corrupt-JSON sidecars,
|
|
5461
|
+
* substituted "unsigned" sidecars on a host WITH a private key,
|
|
5462
|
+
* downgrade-bait algorithm shapes, and a keys/public.pem failing the
|
|
5463
|
+
* EXPECTED_FINGERPRINT pin — each of which would let replay proceed
|
|
5464
|
+
* against forged input. Keeping the class list HERE (one shared helper)
|
|
5465
|
+
* means a new tamper class added to the verifier has exactly one refusal
|
|
5466
|
+
* site to extend.
|
|
5467
|
+
*/
|
|
5468
|
+
function isTamperedSidecarVerify(verify) {
|
|
5469
|
+
if (!verify || typeof verify !== "object") return false;
|
|
5470
|
+
const isSignedTamper = verify.signed && !verify.verified;
|
|
5471
|
+
const isClassTamper = !verify.signed && (
|
|
5472
|
+
verify.tamper_class === "sidecar-corrupt"
|
|
5473
|
+
|| verify.tamper_class === "unsigned-substitution"
|
|
5474
|
+
// Anything other than "Ed25519" or "unsigned": a sidecar that throws
|
|
5475
|
+
// inside crypto.verify (e.g. signature_base64 missing on a
|
|
5476
|
+
// downgrade-bait shape) would otherwise emerge as signed:true +
|
|
5477
|
+
// verified:false through the catch block by accident. The strict
|
|
5478
|
+
// pre-check surfaces the class directly; refuse on it too.
|
|
5479
|
+
|| verify.tamper_class === "algorithm-unsupported"
|
|
5480
|
+
// A keys/public.pem that fails the EXPECTED_FINGERPRINT pin is the
|
|
5481
|
+
// key-swap attack the pin exists for — verification "against" the
|
|
5482
|
+
// swapped key proves nothing, so replay refuses exactly like the
|
|
5483
|
+
// other tamper classes (attest verify already refuses on this).
|
|
5484
|
+
|| verify.tamper_class === "fingerprint-mismatch"
|
|
5485
|
+
);
|
|
5486
|
+
return Boolean(isSignedTamper || isClassTamper);
|
|
5487
|
+
}
|
|
5488
|
+
|
|
5459
5489
|
function classifySidecarVerify(verify) {
|
|
5460
5490
|
if (!verify || typeof verify !== "object") return "unknown";
|
|
5461
5491
|
if (verify.signed && verify.verified) return "verified";
|
|
@@ -5465,6 +5495,7 @@ function classifySidecarVerify(verify) {
|
|
|
5465
5495
|
// `algorithm-unsupported` is its own class label so log scrapers /
|
|
5466
5496
|
// dashboards can filter downgrade-bait events without parsing the reason.
|
|
5467
5497
|
if (verify.tamper_class === "algorithm-unsupported") return "algorithm-unsupported";
|
|
5498
|
+
if (verify.tamper_class === "fingerprint-mismatch") return "fingerprint-mismatch";
|
|
5468
5499
|
if (typeof verify.reason === "string" && verify.reason.startsWith("attestation explicitly unsigned")) return "explicitly-unsigned";
|
|
5469
5500
|
if (typeof verify.reason === "string" && verify.reason.includes("no .sig sidecar")) return "no-sidecar";
|
|
5470
5501
|
if (typeof verify.reason === "string" && verify.reason.includes("no public key")) return "no-public-key";
|
|
@@ -6944,6 +6975,14 @@ function cmdDoctor(runner, args, runOpts, pretty) {
|
|
|
6944
6975
|
// /grant:r <USER>:F` to strip inherited entries.
|
|
6945
6976
|
const aclCheck = checkWindowsAcl(childAbs);
|
|
6946
6977
|
if (aclCheck.ok) continue;
|
|
6978
|
+
// Resolve the grant principal defensively: an unset USERNAME
|
|
6979
|
+
// would otherwise interpolate "undefined:F", and icacls applies
|
|
6980
|
+
// /inheritance:r BEFORE failing on the unresolvable account —
|
|
6981
|
+
// stripping every inherited entry and locking the file out.
|
|
6982
|
+
// os.userInfo() works even when the env var is absent.
|
|
6983
|
+
const aclUser = process.env.USERNAME || (() => {
|
|
6984
|
+
try { return require('os').userInfo().username; } catch { return null; }
|
|
6985
|
+
})();
|
|
6947
6986
|
findings.push({
|
|
6948
6987
|
path: `${displayRoot}/${childRel}`,
|
|
6949
6988
|
mode: null,
|
|
@@ -6951,7 +6990,9 @@ function cmdDoctor(runner, args, runOpts, pretty) {
|
|
|
6951
6990
|
issue: 'broader_than_user_only_acl',
|
|
6952
6991
|
acl_extra_principals: aclCheck.extraPrincipals,
|
|
6953
6992
|
hint: `icacls "${childAbs}" /inheritance:r /grant:r %USERNAME%:F # NEW-CTRL-050: AI-assistant configs holding MCP tokens / API keys must restrict ACL to the workstation user`,
|
|
6954
|
-
|
|
6993
|
+
...(aclUser
|
|
6994
|
+
? { fix_command: ['icacls', childAbs, '/inheritance:r', '/grant:r', `${aclUser}:F`] }
|
|
6995
|
+
: { fix_unavailable: 'current user name unresolvable (USERNAME unset); apply the hint manually' }),
|
|
6955
6996
|
});
|
|
6956
6997
|
continue;
|
|
6957
6998
|
}
|
|
@@ -9021,4 +9062,10 @@ function cmdCi(runner, args, runOpts, pretty) {
|
|
|
9021
9062
|
|
|
9022
9063
|
if (require.main === module) main();
|
|
9023
9064
|
|
|
9024
|
-
module.exports = {
|
|
9065
|
+
module.exports = {
|
|
9066
|
+
COMMANDS, PKG_ROOT, PLAYBOOK_VERBS, persistAttestation,
|
|
9067
|
+
// internal helpers exposed for tests
|
|
9068
|
+
_isTamperedSidecarVerify: isTamperedSidecarVerify,
|
|
9069
|
+
_classifySidecarVerify: classifySidecarVerify,
|
|
9070
|
+
_verifyAttestationSidecar: verifyAttestationSidecar,
|
|
9071
|
+
};
|
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-06-
|
|
3
|
+
"generated_at": "2026-06-05T22:20:42.479Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 63,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "fe65c62f46bbb3c069247eb19169ac4ab99bf0354e3e9cfe432782bf402a918a",
|
|
8
8
|
"data/atlas-ttps.json": "f66b456cf82a3c20575d8479de41f7b11b7ee5693eb1fcf64a67e162ae1b88a2",
|
|
9
9
|
"data/attack-techniques.json": "c39f28e3402ef13ad9b7076819f63fda67a22f97e3e375cfe01c4a4e0beff7c9",
|
|
10
10
|
"data/cve-catalog.json": "51d8425a49e5cc0375d0a154a83a16816e99c3141a5bbafe6383607ca11be240",
|