@blamejs/exceptd-skills 0.12.39 → 0.12.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -0
- package/ARCHITECTURE.md +7 -4
- package/CHANGELOG.md +136 -237
- package/CONTEXT.md +2 -2
- package/README.md +2 -8
- package/agents/threat-researcher.md +2 -2
- package/bin/exceptd.js +134 -39
- package/data/_indexes/_meta.json +9 -9
- package/data/_indexes/activity-feed.json +1 -1
- package/data/_indexes/catalog-summaries.json +1 -1
- package/data/_indexes/chains.json +2794 -800
- package/data/_indexes/frequency.json +4 -0
- package/data/_indexes/section-offsets.json +20 -20
- package/data/_indexes/token-budget.json +5 -5
- package/data/cve-catalog.json +21 -28
- package/data/exploit-availability.json +1 -0
- package/data/framework-control-gaps.json +229 -193
- package/data/global-frameworks.json +1 -0
- package/data/playbooks/crypto-codebase.json +13 -0
- package/data/zeroday-lessons.json +1 -0
- package/lib/framework-gap.js +13 -3
- package/lib/lint-skills.js +1 -1
- package/lib/playbook-runner.js +8 -4
- package/lib/scoring.js +9 -1
- package/lib/sign.js +40 -7
- package/lib/verify.js +5 -5
- package/manifest-snapshot.json +1 -1
- package/manifest-snapshot.sha256 +1 -1
- package/manifest.json +45 -45
- package/orchestrator/README.md +7 -7
- package/orchestrator/index.js +32 -14
- package/orchestrator/scheduler.js +2 -2
- package/package.json +1 -1
- package/sbom.cdx.json +36 -36
- package/scripts/check-test-coverage.js +6 -6
- package/scripts/refresh-reverse-refs.js +42 -15
- package/skills/mlops-security/skill.md +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
"schema_version": "1.3.0",
|
|
4
4
|
"version": "1.3.0",
|
|
5
5
|
"last_updated": "2026-05-15",
|
|
6
|
+
"last_threat_review": "2026-05-17",
|
|
6
7
|
"note": "Multi-jurisdiction framework registry. patch_sla in hours. notification_sla in hours. source field must be primary regulatory source. v1.3.0 expansion: NO, MX, AR, TR, TH, PH, US_CALIFORNIA top-level jurisdictions added; EU member-state sub-regulator blocks added for Germany (BSI), France (ANSSI), Spain (AEPD + AESIA), Italy (ACN + AgID); EU-level technical body ENISA added as cross-cutting reference. v1.2.0 expansion: IL, CH, HK, TW, ID, VN, US_NYDFS added; JP expanded with APPI/PPC, FISC, NISC, METI, Economic Security Promotion Act, AI Strategy Council guidance. v1.1.0 expansion: BR, CN, ZA, AE, SA, NZ, KR, CL added; IN and CA enriched with data-protection law entries (DPDPA, Quebec Law 25, PIPEDA).",
|
|
7
8
|
"tlp": "CLEAR",
|
|
8
9
|
"source_confidence": {
|
|
@@ -381,6 +381,7 @@
|
|
|
381
381
|
"id": "package-manifests",
|
|
382
382
|
"type": "config_file",
|
|
383
383
|
"source": "Read package.json, pyproject.toml, setup.py, go.mod, Cargo.toml, pom.xml, build.gradle, Gemfile, composer.json, mix.exs at repo root and at every nested package boundary. Use Glob `**/{package.json,pyproject.toml,go.mod,Cargo.toml,pom.xml,build.gradle,Gemfile,composer.json}` excluding `node_modules`, `vendor`, `.venv`, `target`, `dist`, `build`.",
|
|
384
|
+
"air_gap_alternative": "Identical local-filesystem read; no network dependency. Same Glob/Read against the working repo.",
|
|
384
385
|
"description": "Package manifests — establish the language ecosystems present, declared crypto-adjacent dependencies, and the public API surface.",
|
|
385
386
|
"required": true
|
|
386
387
|
},
|
|
@@ -388,6 +389,7 @@
|
|
|
388
389
|
"id": "lockfiles",
|
|
389
390
|
"type": "config_file",
|
|
390
391
|
"source": "Read package-lock.json, yarn.lock, pnpm-lock.yaml, poetry.lock, uv.lock, Pipfile.lock, go.sum, Cargo.lock, composer.lock at repo root and at every package boundary.",
|
|
392
|
+
"air_gap_alternative": "Identical local-filesystem read; no network dependency. Same Read against the working repo.",
|
|
391
393
|
"description": "Lockfiles — concrete versions of crypto-adjacent deps actually shipped to consumers (openssl, sodium, libsodium-wrappers, tweetnacl, noble-curves, noble-hashes, noble-post-quantum, jsrsasign, node-forge, pqcrypto, oqs-rs, kyber-crystals, dilithium, aws-lc-rs, ring, rustls, rust-openssl).",
|
|
392
394
|
"required": false
|
|
393
395
|
},
|
|
@@ -395,6 +397,7 @@
|
|
|
395
397
|
"id": "hash-primitive-call-sites",
|
|
396
398
|
"type": "file",
|
|
397
399
|
"source": "Grep across the repo (excluding test/spec/fixture/node_modules/vendor/.venv/target/dist/build) for hash-primitive call sites. Patterns: `crypto.createHash\\(`, `crypto.createHmac\\(`, `hashlib\\.(md5|sha1|sha224|sha256|sha384|sha512|blake2b|blake2s|sha3_)`, `MessageDigest\\.getInstance\\(`, `Digest::(MD5|SHA1|SHA256)`, `hash/md5`, `hash/sha1`, `\"md5\"`, `\"sha1\"`, `\"sha-1\"`, `\"sha256\"`, `\"sha3-256\"`. Use Grep with multiline=true where the algorithm-name string is on a different line from the constructor.",
|
|
400
|
+
"air_gap_alternative": "Identical local-filesystem Grep; no network dependency. Run the same patterns against the working repo.",
|
|
398
401
|
"description": "Every hash-primitive call site. Distinguish security-context usage (signature input, MAC input, integrity check, password derivation, token derivation) from non-security usage (cache key, ETag, dedup). The non-security distinction must be evidenced inline; absent evidence, treat as security-context.",
|
|
399
402
|
"required": true
|
|
400
403
|
},
|
|
@@ -402,6 +405,7 @@
|
|
|
402
405
|
"id": "cipher-and-kex-call-sites",
|
|
403
406
|
"type": "file",
|
|
404
407
|
"source": "Grep for cipher and KEX call sites: `createCipheriv\\(`, `createDecipheriv\\(`, `crypto\\.publicEncrypt\\(`, `crypto\\.privateDecrypt\\(`, `createDiffieHellman\\(`, `createECDH\\(`, `crypto\\.generateKeyPair`, `Cipher\\.getInstance\\(`, `aesgcm`, `ChaCha20Poly1305`, `\\.(seal|open)\\(`, `OpenSSL::Cipher`, hardcoded curve names `\"P-256\"`, `\"secp256r1\"`, `\"prime256v1\"`, `\"P-384\"`, `\"secp384r1\"`, `\"P-521\"`, `\"secp521r1\"`, `\"secp256k1\"`, `\"X25519\"`, `\"X448\"`.",
|
|
408
|
+
"air_gap_alternative": "Identical local-filesystem Grep; no network dependency. Run the same patterns against the working repo.",
|
|
405
409
|
"description": "Cipher and key-exchange call sites. Identify mode (GCM/CBC/CTR/ECB), curve, key size, IV-generation pattern. ECB mode anywhere is a hard fail. CBC without HMAC is a hard fail. AES-128 without GCM authenticator is a finding.",
|
|
406
410
|
"required": true
|
|
407
411
|
},
|
|
@@ -409,6 +413,7 @@
|
|
|
409
413
|
"id": "signature-call-sites",
|
|
410
414
|
"type": "file",
|
|
411
415
|
"source": "Grep for signature operations: `crypto\\.sign\\(`, `crypto\\.verify\\(`, `Signature\\.getInstance\\(`, `\\.sign_pss\\(`, `\\.verify_pss\\(`, `RSA-PSS`, `RSA-PKCS1`, `ECDSA`, `Ed25519`, `Ed448`, `ML-DSA`, `Dilithium`, `SLH-DSA`, `SPHINCS`. Capture key sizes for RSA (look for `modulusLength`, `key_size`, `--rsa-2048` literals), curve choice for ECDSA, hash choice paired with signature (RSA-SHA1 is a hard fail).",
|
|
416
|
+
"air_gap_alternative": "Identical local-filesystem Grep; no network dependency. Run the same patterns against the working repo.",
|
|
412
417
|
"description": "Signature scheme inventory. RSA-1024 anywhere is a hard fail; RSA-2048 with > 5-year sensitivity requires PQC roadmap; ECDSA-P256 acceptable today but needs hybrid ML-DSA migration plan; bare RSA-PKCS1 (not PSS) for new signatures is a finding.",
|
|
413
418
|
"required": true
|
|
414
419
|
},
|
|
@@ -416,6 +421,7 @@
|
|
|
416
421
|
"id": "kdf-call-sites",
|
|
417
422
|
"type": "file",
|
|
418
423
|
"source": "Grep for key-derivation calls: `pbkdf2(Sync)?\\(`, `PBKDF2`, `hashlib\\.pbkdf2_hmac`, `bcrypt\\.(hash|hashSync|compare)`, `scrypt(Sync)?\\(`, `argon2\\.(hash|verify)`, `argon2id`, `hkdf\\(`, `HKDF`, `derive_key`. For each, extract the cost parameters: PBKDF2 iterations, bcrypt cost factor, scrypt N/r/p, argon2 t/m/p.",
|
|
424
|
+
"air_gap_alternative": "Identical local-filesystem Grep; no network dependency. Run the same patterns against the working repo.",
|
|
419
425
|
"description": "Key-derivation parameter inventory. Apply OWASP 2023 minimums: PBKDF2-HMAC-SHA256 >= 600,000; PBKDF2-HMAC-SHA512 >= 210,000; bcrypt cost >= 12; scrypt N >= 2^17, r=8, p=1; argon2id m >= 19 MiB (19456 KiB), t >= 2, p >= 1. Any parameter below minimum is a finding.",
|
|
420
426
|
"required": true
|
|
421
427
|
},
|
|
@@ -423,6 +429,7 @@
|
|
|
423
429
|
"id": "rng-call-sites",
|
|
424
430
|
"type": "file",
|
|
425
431
|
"source": "Grep for RNG sources: `Math\\.random\\(`, `random\\.random\\(`, `random\\.randint\\(`, `random\\.choice\\(`, `rand\\(`, `srand\\(`, `mt_rand\\(`, `secrets\\.(token_|randbits|choice)`, `crypto\\.randomBytes\\(`, `crypto\\.getRandomValues\\(`, `crypto\\.randomUUID\\(`, `os\\.urandom\\(`, `getrandom\\(`, `SecureRandom`, `OsRng`, `ThreadRng`, `/dev/urandom`, `/dev/random`. Capture file_path:line for each.",
|
|
432
|
+
"air_gap_alternative": "Identical local-filesystem Grep; no network dependency. Run the same patterns against the working repo.",
|
|
426
433
|
"description": "RNG-source inventory. Production-context Math.random / random.random / rand without cryptographic-RNG fallback is CWE-338. Distinguish test/spec/fixture usage explicitly via path allowlist; production usage requires a cryptographic RNG.",
|
|
427
434
|
"required": true
|
|
428
435
|
},
|
|
@@ -430,6 +437,7 @@
|
|
|
430
437
|
"id": "hardcoded-key-material",
|
|
431
438
|
"type": "file",
|
|
432
439
|
"source": "Grep for hardcoded crypto material: PEM markers `-----BEGIN (RSA |EC |DSA |PRIVATE |PUBLIC |CERTIFICATE )?(PRIVATE|PUBLIC) KEY-----`, SSH key prefixes `ssh-rsa AAAA`, `ssh-ed25519 AAAA`, `ecdsa-sha2-nistp256 AAAA`, hex-blob heuristics for keys (>= 64 hex chars on a single literal line), base64-encoded blobs >= 256 chars in source files. Cross-reference with the `secrets` playbook for the exfil-secret angle; here the focus is library-author shipping defaults that look like keys (e.g. example certs, demo keys, sample HMAC seeds that downstream consumers fail to rotate).",
|
|
440
|
+
"air_gap_alternative": "Identical local-filesystem Grep; no network dependency. Run the same patterns against the working repo.",
|
|
433
441
|
"description": "Hardcoded key material shipped with the library. Library-author angle: any 'demo' or 'example' key that downstream consumers fail to rotate becomes a universal-default vulnerability (cf. embedded-router demo keys, Wi-Fi default WPA keys, IoT bootloader signing keys).",
|
|
434
442
|
"required": false
|
|
435
443
|
},
|
|
@@ -437,6 +445,7 @@
|
|
|
437
445
|
"id": "tls-config-construction",
|
|
438
446
|
"type": "file",
|
|
439
447
|
"source": "Grep for in-code TLS context construction: `tls\\.createSecureContext`, `tls\\.createServer`, `https\\.createServer`, `ssl\\.SSLContext\\(`, `ssl\\.create_default_context`, `rustls::ServerConfig`, `rustls::ClientConfig`, `tls\\.Config\\{`, `SSL_CTX_new`, options like `minVersion`, `maxVersion`, `secureProtocol`, `ciphers`, `ecdhCurve`, `sigalgs`, `groups`, `ALPNProtocols`.",
|
|
448
|
+
"air_gap_alternative": "Identical local-filesystem Grep; no network dependency. Run the same patterns against the working repo.",
|
|
440
449
|
"description": "In-code TLS configuration. Library authors that construct TLS contexts internally must default to TLS 1.3 minimum, X25519MLKEM768 group preference (when openssl >= 3.5 detected), modern cipher list. Hardcoded `secureProtocol: 'TLSv1_method'` or `minVersion: 'TLSv1'` is a hard fail.",
|
|
441
450
|
"required": false
|
|
442
451
|
},
|
|
@@ -444,6 +453,7 @@
|
|
|
444
453
|
"id": "pqc-adoption-signals",
|
|
445
454
|
"type": "file",
|
|
446
455
|
"source": "Grep for PQC adoption: `ml[-_]?kem`, `ml[-_]?dsa`, `slh[-_]?dsa`, `kyber`, `dilithium`, `sphincs`, `falcon`, `kemEncapsulate`, `kemDecapsulate`, `EVP_KEM_`, `OQS_KEM_`, `oqsprovider`, `liboqs`, `noble-post-quantum`, `pqcrypto::`, `aws-lc-rs::pqc`, `circl/sign/dilithium`, `circl/kem/kyber`.",
|
|
456
|
+
"air_gap_alternative": "Identical local-filesystem Grep; no network dependency. Run the same patterns against the working repo.",
|
|
447
457
|
"description": "PQC adoption signals. If `pqc-readiness-gap` directive runs, this artifact is the primary input. Distinguish concrete cryptographic operations from configuration strings, feature-flag names, and comments.",
|
|
448
458
|
"required": false
|
|
449
459
|
},
|
|
@@ -451,6 +461,7 @@
|
|
|
451
461
|
"id": "fips-provider-activation",
|
|
452
462
|
"type": "file",
|
|
453
463
|
"source": "Grep for FIPS-mode activation: `OSSL_PROVIDER_load.*fips`, `crypto\\.setFips\\(`, `openssl::provider::Provider::load_default`, `Provider::load.*\"fips\"`, openssl.cnf or fipsmodule.cnf files in the repo, environment-variable references to `OPENSSL_FIPS`, `OPENSSL_CONF`. Capture whether the activation is conditional (e.g. only if env var set) or unconditional.",
|
|
464
|
+
"air_gap_alternative": "Identical local-filesystem Grep; no network dependency. Run the same patterns against the working repo.",
|
|
454
465
|
"description": "FIPS-provider activation evidence. The `fips-validation-status` directive uses this to distinguish runtime FIPS activation from link-time FIPS claims.",
|
|
455
466
|
"required": false
|
|
456
467
|
},
|
|
@@ -458,6 +469,7 @@
|
|
|
458
469
|
"id": "vendored-crypto-tree",
|
|
459
470
|
"type": "file",
|
|
460
471
|
"source": "Glob for vendored crypto: `vendor/**/{crypto,openssl,sodium,nacl,kyber,dilithium,sphincs,curve25519,blake2,sha3,argon2}*`, `third_party/**/*crypto*`, `crates/**/*crypto*` (excluding the package's own crypto module). Look for upstream-reference files: `UPSTREAM`, `ORIGIN`, `PROVENANCE.md`, `.upstream-commit`, integrity hashes in lockfiles.",
|
|
472
|
+
"air_gap_alternative": "Identical local-filesystem Glob; no network dependency. Run the same patterns against the working repo.",
|
|
461
473
|
"description": "Vendored cryptographic primitives. Library authors sometimes vendor crypto to reduce dep tree or for licensing reasons. Without provenance + integrity audit, vendored crypto is a supply-chain backdoor opportunity.",
|
|
462
474
|
"required": false
|
|
463
475
|
},
|
|
@@ -465,6 +477,7 @@
|
|
|
465
477
|
"id": "ci-crypto-tests",
|
|
466
478
|
"type": "file",
|
|
467
479
|
"source": "Glob for CI configs: `.github/workflows/**/*.yml`, `.gitlab-ci.yml`, `.circleci/config.yml`, `azure-pipelines.yml`, `Jenkinsfile`. Grep for crypto-test invocations, FIPS-test runs, constant-time analysis tools (`dudect`, `valgrind --tool=memcheck` on secret-dependent code, `ctgrind`).",
|
|
480
|
+
"air_gap_alternative": "Identical local-filesystem Glob + Grep; no network dependency. Run the same patterns against the working repo.",
|
|
468
481
|
"description": "Build-time crypto verification. Absence of constant-time tests on vendored PQC primitives is a finding for the `pqc-readiness-gap` directive.",
|
|
469
482
|
"required": false
|
|
470
483
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"_meta": {
|
|
3
3
|
"schema_version": "1.1.0",
|
|
4
4
|
"last_updated": "2026-05-15",
|
|
5
|
+
"last_threat_review": "2026-05-17",
|
|
5
6
|
"purpose": "Zero-day learning loop output. Each entry maps a CVE to: attack vector, defense chain analysis, framework coverage, new control requirements generated, and exposure scoring. v1.1.0 (2026-05-15): every entry now carries ai_discovered_zeroday boolean + ai_discovery_source enum + ai_discovery_date + ai_assist_factor ladder, per AGENTS.md Hard Rule #7.",
|
|
6
7
|
"note": "Never delete entries. Closed gaps are marked status: closed. History is data.",
|
|
7
8
|
"tlp": "CLEAR",
|
package/lib/framework-gap.js
CHANGED
|
@@ -189,12 +189,22 @@ function gapReport(frameworkIds, threatScenario, controlGaps, cveCatalog = {}) {
|
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
// Cycle 20 A P1 (v0.12.40): pre-fix this filtered on `theater_pattern`
|
|
193
|
+
// (a legacy field) but the v0.12.29 backfill added a structured
|
|
194
|
+
// `theater_test` block on all 118 entries while leaving most without
|
|
195
|
+
// `theater_pattern`. Result: the per-entry badge (line 188 above)
|
|
196
|
+
// showed "⚠ THEATER RISK" for every open gap, but the summary
|
|
197
|
+
// footer reported "0 theater-risk controls" because nothing matched
|
|
198
|
+
// the legacy field. Now: an entry is theater-risk if it's open AND
|
|
199
|
+
// carries EITHER `theater_test` OR `theater_pattern`. Footer + badge
|
|
200
|
+
// count agree.
|
|
192
201
|
const theaterRisks = relevantGaps
|
|
193
|
-
.filter(([, g]) => g.status === 'open' && g.theater_pattern)
|
|
202
|
+
.filter(([, g]) => g.status === 'open' && (g.theater_test || g.theater_pattern))
|
|
194
203
|
.map(([key, g]) => ({
|
|
195
204
|
control: key,
|
|
196
|
-
pattern: g.theater_pattern,
|
|
197
|
-
framework: g.framework
|
|
205
|
+
pattern: g.theater_pattern || (g.theater_test && g.theater_test.claim) || null,
|
|
206
|
+
framework: g.framework,
|
|
207
|
+
theater_test_present: !!g.theater_test,
|
|
198
208
|
}));
|
|
199
209
|
|
|
200
210
|
return {
|
package/lib/lint-skills.js
CHANGED
|
@@ -759,7 +759,7 @@ function main() {
|
|
|
759
759
|
for (const o of orphans) {
|
|
760
760
|
console.log(`FAIL <orphan>`);
|
|
761
761
|
console.log(` - skill.md exists on disk but not in manifest: ${o}`);
|
|
762
|
-
console.log(` fix: re-run \`node lib/sign.js sign-all\` after adding it to manifest.json, OR delete the orphan directory`);
|
|
762
|
+
console.log(` fix: re-run sign-all (\`node $(exceptd path)/lib/sign.js sign-all\` from a contributor checkout) after adding it to manifest.json, OR delete the orphan directory`);
|
|
763
763
|
}
|
|
764
764
|
// P4 — air-gap completeness lint over data/playbooks/*.json.
|
|
765
765
|
airGapWarnings = lintPlaybookAirGap();
|
package/lib/playbook-runner.js
CHANGED
|
@@ -1679,7 +1679,7 @@ function close(playbookId, directiveId, analyzeResult, validateResult, agentSign
|
|
|
1679
1679
|
evidencePackage.signature_algorithm = 'HMAC-SHA256-session-key';
|
|
1680
1680
|
} else if (evidencePackage && evidencePackage.signed) {
|
|
1681
1681
|
evidencePackage.signature = null;
|
|
1682
|
-
evidencePackage.signature_pending = 'No session_key provided. Sign with Ed25519 via `node lib/sign.js sign-evidence <bundle.json>` post-emit.';
|
|
1682
|
+
evidencePackage.signature_pending = 'No session_key provided. Sign with Ed25519 via `node $(exceptd path)/lib/sign.js sign-evidence <bundle.json>` post-emit (contributor checkout) or `exceptd doctor --fix` to enable signing.';
|
|
1683
1683
|
}
|
|
1684
1684
|
|
|
1685
1685
|
// learning_loop lesson
|
|
@@ -2830,13 +2830,17 @@ function normalizeSubmission(submission, playbook) {
|
|
|
2830
2830
|
// which confuses detect()'s indicator-id lookup. Strip and log instead.
|
|
2831
2831
|
if (submission.signal_overrides !== undefined && submission.signal_overrides !== null
|
|
2832
2832
|
&& (typeof submission.signal_overrides !== 'object' || Array.isArray(submission.signal_overrides))) {
|
|
2833
|
-
|
|
2834
|
-
|
|
2833
|
+
// Clone before mutating _runErrors so a frozen / shared input
|
|
2834
|
+
// submission isn't modified in place. Pre-fix a caller passing a
|
|
2835
|
+
// frozen submission (Object.freeze for safety, or a shared reference
|
|
2836
|
+
// across parallel runs) threw uncaught on the _runErrors push.
|
|
2837
|
+
const carry = Array.isArray(submission._runErrors) ? submission._runErrors.slice() : [];
|
|
2838
|
+
pushRunError(carry, {
|
|
2835
2839
|
kind: 'signal_overrides_invalid',
|
|
2836
2840
|
supplied_type: Array.isArray(submission.signal_overrides) ? 'array' : typeof submission.signal_overrides,
|
|
2837
2841
|
reason: 'signal_overrides must be a plain object mapping indicator-id → verdict.'
|
|
2838
2842
|
}, { dedupeKey: e => String(e.supplied_type) });
|
|
2839
|
-
submission = { ...submission, signal_overrides: {} };
|
|
2843
|
+
submission = { ...submission, signal_overrides: {}, _runErrors: carry };
|
|
2840
2844
|
}
|
|
2841
2845
|
|
|
2842
2846
|
// v0.11.3 #71 fix: the CLI may inject `signals._bundle_formats` before
|
package/lib/scoring.js
CHANGED
|
@@ -300,7 +300,15 @@ function compare(cveId, catalog, opts) {
|
|
|
300
300
|
// SLA is insufficient. ±10 is the tightest classifier that still treats
|
|
301
301
|
// ordinary CVSS rounding noise as alignment.
|
|
302
302
|
let explanation = '';
|
|
303
|
-
|
|
303
|
+
// Surface the "no scoring signal" case distinctly from "broadly
|
|
304
|
+
// aligned". Pre-fix a CVE with rwep_score: 0 AND cvss_score: 0 (e.g.
|
|
305
|
+
// catalog entry created before scoring backfill) printed "broadly
|
|
306
|
+
// aligned" — coincidence-passing per the field-present-not-populated
|
|
307
|
+
// pitfall. Now the operator sees a specific signal pointing at the
|
|
308
|
+
// catalog gap rather than a false sense of alignment.
|
|
309
|
+
if ((rwep == null || rwep === 0) && (cvss == null || cvss === 0)) {
|
|
310
|
+
explanation = 'No scoring signal — both RWEP and CVSS are zero/null. Investigate the catalog entry; this CVE has no usable risk score.';
|
|
311
|
+
} else if (delta > 10) {
|
|
304
312
|
explanation = `RWEP significantly higher than CVSS equivalent. Factors driving delta: `;
|
|
305
313
|
const driving = [];
|
|
306
314
|
if (entry.cisa_kev) driving.push('CISA KEV (+25)');
|
package/lib/sign.js
CHANGED
|
@@ -101,6 +101,22 @@ function generateKeypair({ rotate = false } = {}) {
|
|
|
101
101
|
process.exit(1);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// Refuse to silently overwrite an existing public key when no private key
|
|
105
|
+
// is present. This is the v0.11.x signature-regression class: a host with
|
|
106
|
+
// a working pubkey but missing privkey running generate-keypair would
|
|
107
|
+
// produce a fresh pubkey divergent from every shipped signature. Operators
|
|
108
|
+
// running `exceptd doctor --fix` on a stock install would replace the
|
|
109
|
+
// shipped keys/public.pem with one whose private half exists only on
|
|
110
|
+
// their machine — every subsequent verify against shipped signatures fails.
|
|
111
|
+
// Force the operator to be explicit via --rotate (which signals intent to
|
|
112
|
+
// re-sign).
|
|
113
|
+
if (fs.existsSync(PUBLIC_KEY_PATH) && !rotate) {
|
|
114
|
+
console.error('[sign] Public key already exists at keys/public.pem but no matching private key.');
|
|
115
|
+
console.error('[sign] Refusing to overwrite the public key — that would orphan every existing signature.');
|
|
116
|
+
console.error('[sign] If you are setting up a fresh signing identity, pass --rotate to confirm. After --rotate you must re-sign all skills with sign-all.');
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
104
120
|
fs.mkdirSync(KEYS_DIR, { recursive: true, mode: 0o700 });
|
|
105
121
|
fs.mkdirSync(PUBLIC_KEYS_DIR, { recursive: true });
|
|
106
122
|
|
|
@@ -115,20 +131,35 @@ function generateKeypair({ rotate = false } = {}) {
|
|
|
115
131
|
// on win32, fs.writeFileSync `mode` does not produce
|
|
116
132
|
// a POSIX-style restrictive ACL. Tighten via icacls so other desktop
|
|
117
133
|
// users on the same workstation / CI runner can't read the key.
|
|
118
|
-
restrictWindowsAcl(PRIVATE_KEY_PATH);
|
|
134
|
+
const aclHardened = restrictWindowsAcl(PRIVATE_KEY_PATH);
|
|
119
135
|
|
|
120
136
|
if (rotate) {
|
|
121
|
-
console.log('[sign] Keypair rotated. All existing signatures are now invalid —
|
|
137
|
+
console.log('[sign] Keypair rotated. All existing signatures are now invalid — re-sign with sign-all.');
|
|
122
138
|
} else {
|
|
123
139
|
console.log('[sign] Ed25519 keypair generated.');
|
|
124
140
|
console.log(` Private key: .keys/private.pem (gitignored — do not commit)`);
|
|
125
141
|
console.log(` Public key: keys/public.pem (tracked — commit this)`);
|
|
126
142
|
}
|
|
143
|
+
if (process.platform === 'win32') {
|
|
144
|
+
console.log(` Windows ACL hardened: ${aclHardened ? 'yes' : 'NO — other desktop users on this machine may be able to read the private key'}`);
|
|
145
|
+
}
|
|
127
146
|
|
|
128
147
|
console.log('\nNext steps:');
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
148
|
+
if (rotate) {
|
|
149
|
+
// After --rotate the private key IS present, so `doctor --fix`'s
|
|
150
|
+
// missing-key path won't fire. Tell the operator to re-sign
|
|
151
|
+
// directly. (doctor --fix v0.12.41+ also detects this case and
|
|
152
|
+
// chains sign-all, so either path converges.)
|
|
153
|
+
console.log(' 1. exceptd doctor --fix — detects post-rotate stale signatures and chains sign-all');
|
|
154
|
+
console.log(' (or: node $(exceptd path)/lib/sign.js sign-all — re-sign directly)');
|
|
155
|
+
console.log(' 2. exceptd doctor — confirm signatures verify against the new public key');
|
|
156
|
+
console.log(' 3. git add keys/public.pem && git commit -m "rotate signing public key"');
|
|
157
|
+
} else {
|
|
158
|
+
console.log(' 1. exceptd doctor --fix — chains sign-all after first key generation');
|
|
159
|
+
console.log(' 2. exceptd doctor — confirm signatures verify');
|
|
160
|
+
console.log(' 3. git add keys/public.pem && git commit -m "add signing public key"');
|
|
161
|
+
}
|
|
162
|
+
return { aclHardened };
|
|
132
163
|
}
|
|
133
164
|
|
|
134
165
|
/**
|
|
@@ -380,11 +411,11 @@ function signCanonicalManifest(manifest, privateKey) {
|
|
|
380
411
|
* @param {string} targetPath absolute path of the private key file
|
|
381
412
|
*/
|
|
382
413
|
function restrictWindowsAcl(targetPath) {
|
|
383
|
-
if (process.platform !== 'win32') return;
|
|
414
|
+
if (process.platform !== 'win32') return true;
|
|
384
415
|
const user = process.env.USERNAME;
|
|
385
416
|
if (!user) {
|
|
386
417
|
console.warn('[sign] WARN: USERNAME env var not set — skipping Windows ACL hardening on ' + targetPath);
|
|
387
|
-
return;
|
|
418
|
+
return false;
|
|
388
419
|
}
|
|
389
420
|
try {
|
|
390
421
|
execFileSync('icacls', [
|
|
@@ -393,6 +424,7 @@ function restrictWindowsAcl(targetPath) {
|
|
|
393
424
|
'/grant:r',
|
|
394
425
|
`${user}:F`,
|
|
395
426
|
], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
427
|
+
return true;
|
|
396
428
|
} catch (err) {
|
|
397
429
|
console.warn(
|
|
398
430
|
'[sign] WARN: icacls hardening failed on ' + targetPath + ': ' +
|
|
@@ -400,6 +432,7 @@ function restrictWindowsAcl(targetPath) {
|
|
|
400
432
|
' — the key was written but ACL inheritance was not stripped. ' +
|
|
401
433
|
'Other desktop users on this machine may be able to read it.'
|
|
402
434
|
);
|
|
435
|
+
return false;
|
|
403
436
|
}
|
|
404
437
|
}
|
|
405
438
|
|
package/lib/verify.js
CHANGED
|
@@ -87,7 +87,7 @@ const EXPECTED_FINGERPRINT_PATH = path.join(ROOT, 'keys', 'EXPECTED_FINGERPRINT'
|
|
|
87
87
|
function verifyAll() {
|
|
88
88
|
const publicKey = loadPublicKey();
|
|
89
89
|
if (!publicKey) {
|
|
90
|
-
console.error('[verify] No public key at keys/public.pem — run
|
|
90
|
+
console.error('[verify] No public key at keys/public.pem — run `exceptd doctor --fix` (or `node $(exceptd path)/lib/sign.js generate-keypair` from a contributor checkout)');
|
|
91
91
|
return { valid: [], invalid: [], missing_sig: [], missing_file: [], no_key: true };
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -128,7 +128,7 @@ function verifyOne(skillName) {
|
|
|
128
128
|
*/
|
|
129
129
|
function signAll() {
|
|
130
130
|
const privateKey = loadPrivateKey();
|
|
131
|
-
if (!privateKey) throw new Error('No private key at .keys/private.pem — run
|
|
131
|
+
if (!privateKey) throw new Error('No private key at .keys/private.pem — run `exceptd doctor --fix` (or `node $(exceptd path)/lib/sign.js generate-keypair` from a contributor checkout)');
|
|
132
132
|
|
|
133
133
|
// P1-4: load the manifest without the signature gate. We're about to
|
|
134
134
|
// mutate the manifest (re-sign skills + re-sign the manifest itself),
|
|
@@ -236,7 +236,7 @@ function validateSkillPath(skillPath) {
|
|
|
236
236
|
|
|
237
237
|
function verifySkill(skill, publicKey) {
|
|
238
238
|
if (!skill.signature) {
|
|
239
|
-
return { status: 'missing_sig', reason: 'No Ed25519 signature in manifest — run
|
|
239
|
+
return { status: 'missing_sig', reason: 'No Ed25519 signature in manifest — run `exceptd doctor --fix` (or `node $(exceptd path)/lib/sign.js sign-all` from a contributor checkout)' };
|
|
240
240
|
}
|
|
241
241
|
|
|
242
242
|
const skillPath = path.join(ROOT, skill.path);
|
|
@@ -447,7 +447,7 @@ function loadManifestValidated() {
|
|
|
447
447
|
// console.warn would spam stderr per call. Node's emitWarning() with
|
|
448
448
|
// a stable `code` collapses repeated emissions automatically.
|
|
449
449
|
process.emitWarning(
|
|
450
|
-
'manifest.json has no top-level manifest_signature field. This tarball predates v0.12.17 manifest signing; skills will still be verified but a coordinated rewrite of manifest.json could go undetected. Re-run `node lib/sign.js sign-all` to add the signature.',
|
|
450
|
+
'manifest.json has no top-level manifest_signature field. This tarball predates v0.12.17 manifest signing; skills will still be verified but a coordinated rewrite of manifest.json could go undetected. Re-run `exceptd doctor --fix` (or `node $(exceptd path)/lib/sign.js sign-all` from a contributor checkout) to add the signature.',
|
|
451
451
|
{ code: 'EXCEPTD_MANIFEST_UNSIGNED' }
|
|
452
452
|
);
|
|
453
453
|
} else if (sigResult.status === 'no-key') {
|
|
@@ -693,7 +693,7 @@ if (require.main === module) {
|
|
|
693
693
|
if (arg === 'check-key') {
|
|
694
694
|
const pub = loadPublicKey();
|
|
695
695
|
if (!pub) {
|
|
696
|
-
console.error('[verify] No public key — run
|
|
696
|
+
console.error('[verify] No public key — run `exceptd doctor --fix` (or `node $(exceptd path)/lib/sign.js generate-keypair` from a contributor checkout)');
|
|
697
697
|
process.exit(1);
|
|
698
698
|
}
|
|
699
699
|
console.log('[verify] Public key present at keys/public.pem');
|
package/manifest-snapshot.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Auto-generated by scripts/refresh-manifest-snapshot.js — do not hand-edit. Public skill surface used by check-manifest-snapshot.js to detect breaking removals.",
|
|
3
|
-
"_generated_at": "2026-05-16T15:
|
|
3
|
+
"_generated_at": "2026-05-16T15:50:05.906Z",
|
|
4
4
|
"atlas_version": "5.4.0",
|
|
5
5
|
"skill_count": 42,
|
|
6
6
|
"skills": [
|
package/manifest-snapshot.sha256
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
1185d2dadcd6f08e15fb2b142c1b9c8bc106730893bbb439614510ca4bdec2af manifest-snapshot.json
|