@blamejs/exceptd-skills 0.12.15 → 0.12.16
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 +67 -0
- package/bin/exceptd.js +377 -20
- package/data/_indexes/_meta.json +3 -3
- package/data/cve-catalog.json +1 -1
- package/data/playbooks/ai-api.json +3 -1
- package/data/playbooks/containers.json +11 -3
- package/data/playbooks/cred-stores.json +3 -1
- package/data/playbooks/crypto-codebase.json +11 -11
- package/data/playbooks/crypto.json +1 -1
- package/data/playbooks/hardening.json +3 -1
- package/data/playbooks/kernel.json +3 -1
- package/data/playbooks/library-author.json +21 -10
- package/data/playbooks/mcp.json +1 -1
- package/data/playbooks/runtime.json +3 -1
- package/data/playbooks/sbom.json +2 -2
- package/data/playbooks/secrets.json +3 -1
- package/lib/auto-discovery.js +36 -31
- package/lib/cve-curation.js +15 -9
- package/lib/prefetch.js +30 -8
- package/lib/refresh-network.js +40 -0
- package/lib/scoring.js +171 -11
- package/lib/validate-playbooks.js +46 -0
- package/manifest.json +39 -39
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/scripts/verify-shipped-tarball.js +35 -6
package/data/_indexes/_meta.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.1.0",
|
|
3
|
-
"generated_at": "2026-05-
|
|
3
|
+
"generated_at": "2026-05-14T17:43:16.339Z",
|
|
4
4
|
"generator": "scripts/build-indexes.js",
|
|
5
5
|
"source_count": 50,
|
|
6
6
|
"source_hashes": {
|
|
7
|
-
"manifest.json": "
|
|
7
|
+
"manifest.json": "bfe5d173b001ae1f279ec620f84a1b5e7ae9236d51c1e814f22bf6fe66361835",
|
|
8
8
|
"data/atlas-ttps.json": "20339e0ae3cd89c06f1385be31c50f408f827edc2e8ab8aef026ade3bcf0a917",
|
|
9
9
|
"data/attack-techniques.json": "6db08a8e8a4d03d9309b1d185112de7f3c9595d2cd3d24566b7ce0b3b8aa5d1a",
|
|
10
|
-
"data/cve-catalog.json": "
|
|
10
|
+
"data/cve-catalog.json": "6e198d414a3a86dcae93ef36a2b1978734d0b1224fa66ba5184819ea0e3fb49f",
|
|
11
11
|
"data/cwe-catalog.json": "19893d2a7139d86ff3fcf296b0e6cda10e357727a1d1ffb56af282104e99157a",
|
|
12
12
|
"data/d3fend-catalog.json": "d219520c8d3eb61a270b25ea60f64721035e98a8d5d51d1a4e1f1140d9a586f9",
|
|
13
13
|
"data/dlp-controls.json": "8ea8d907aea0a2cfd772b048a62122a322ba3284a5c36a272ad5e9d392564cb5",
|
package/data/cve-catalog.json
CHANGED
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
"on_fail": "halt"
|
|
30
30
|
}
|
|
31
31
|
],
|
|
32
|
-
"mutex": [
|
|
32
|
+
"mutex": [
|
|
33
|
+
"library-author"
|
|
34
|
+
],
|
|
33
35
|
"feeds_into": [
|
|
34
36
|
{
|
|
35
37
|
"playbook_id": "kernel",
|
|
@@ -38,13 +40,19 @@
|
|
|
38
40
|
{
|
|
39
41
|
"playbook_id": "secrets",
|
|
40
42
|
"condition": "always"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"playbook_id": "sbom",
|
|
46
|
+
"condition": "container-image-layers.length > 0"
|
|
41
47
|
}
|
|
42
48
|
]
|
|
43
49
|
},
|
|
44
50
|
"domain": {
|
|
45
51
|
"name": "Container runtime posture + manifest review",
|
|
46
52
|
"attack_class": "container-escape",
|
|
47
|
-
"atlas_refs": [
|
|
53
|
+
"atlas_refs": [
|
|
54
|
+
"AML.T0010"
|
|
55
|
+
],
|
|
48
56
|
"attack_refs": [
|
|
49
57
|
"T1611",
|
|
50
58
|
"T1610",
|
|
@@ -213,7 +221,7 @@
|
|
|
213
221
|
]
|
|
214
222
|
},
|
|
215
223
|
"direct": {
|
|
216
|
-
"threat_context": "Container escape attack class in 2025-2026 is dominated by the runc / containerd / kubelet CVE chain.
|
|
224
|
+
"threat_context": "Container escape attack class in 2025-2026 is dominated by the runc / containerd / kubelet CVE chain. Public runc / containerd / kubelet escape primitives — including the 2024 'Leaky Vessels' working-directory race class — establish the canonical pattern: host-resource leakage (fd, mount, /proc) via misconfigured isolation. Subsequent CVEs in the runc/containerd/kubelet chain follow the same shape. Mandiant 2025 IR report: ~22% of cloud-tenant compromises involved a container-escape step. The escape is exquisitely sensitive to manifest posture: privileged: true grants the attacker the full host kernel (LPE without escape); hostPID/hostNetwork/hostIPC grant cross-pod visibility; runAsUser: 0 + writable host mount = direct host write. seccompProfile + AppArmor profile presence is the primary mitigation. Manifests in repos are the attestation-vs-reality battleground — admission controllers reject some but not all anti-patterns, depending on policy maturity.",
|
|
217
225
|
"rwep_threshold": {
|
|
218
226
|
"escalate": 85,
|
|
219
227
|
"monitor": 65,
|
|
@@ -393,77 +393,77 @@
|
|
|
393
393
|
},
|
|
394
394
|
{
|
|
395
395
|
"id": "hash-primitive-call-sites",
|
|
396
|
-
"type": "
|
|
396
|
+
"type": "file",
|
|
397
397
|
"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.",
|
|
398
398
|
"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
399
|
"required": true
|
|
400
400
|
},
|
|
401
401
|
{
|
|
402
402
|
"id": "cipher-and-kex-call-sites",
|
|
403
|
-
"type": "
|
|
403
|
+
"type": "file",
|
|
404
404
|
"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\"`.",
|
|
405
405
|
"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
406
|
"required": true
|
|
407
407
|
},
|
|
408
408
|
{
|
|
409
409
|
"id": "signature-call-sites",
|
|
410
|
-
"type": "
|
|
410
|
+
"type": "file",
|
|
411
411
|
"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).",
|
|
412
412
|
"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
413
|
"required": true
|
|
414
414
|
},
|
|
415
415
|
{
|
|
416
416
|
"id": "kdf-call-sites",
|
|
417
|
-
"type": "
|
|
417
|
+
"type": "file",
|
|
418
418
|
"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.",
|
|
419
419
|
"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
420
|
"required": true
|
|
421
421
|
},
|
|
422
422
|
{
|
|
423
423
|
"id": "rng-call-sites",
|
|
424
|
-
"type": "
|
|
424
|
+
"type": "file",
|
|
425
425
|
"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.",
|
|
426
426
|
"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
427
|
"required": true
|
|
428
428
|
},
|
|
429
429
|
{
|
|
430
430
|
"id": "hardcoded-key-material",
|
|
431
|
-
"type": "
|
|
431
|
+
"type": "file",
|
|
432
432
|
"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).",
|
|
433
433
|
"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
434
|
"required": false
|
|
435
435
|
},
|
|
436
436
|
{
|
|
437
437
|
"id": "tls-config-construction",
|
|
438
|
-
"type": "
|
|
438
|
+
"type": "file",
|
|
439
439
|
"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`.",
|
|
440
440
|
"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
441
|
"required": false
|
|
442
442
|
},
|
|
443
443
|
{
|
|
444
444
|
"id": "pqc-adoption-signals",
|
|
445
|
-
"type": "
|
|
445
|
+
"type": "file",
|
|
446
446
|
"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`.",
|
|
447
447
|
"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
448
|
"required": false
|
|
449
449
|
},
|
|
450
450
|
{
|
|
451
451
|
"id": "fips-provider-activation",
|
|
452
|
-
"type": "
|
|
452
|
+
"type": "file",
|
|
453
453
|
"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.",
|
|
454
454
|
"description": "FIPS-provider activation evidence. The `fips-validation-status` directive uses this to distinguish runtime FIPS activation from link-time FIPS claims.",
|
|
455
455
|
"required": false
|
|
456
456
|
},
|
|
457
457
|
{
|
|
458
458
|
"id": "vendored-crypto-tree",
|
|
459
|
-
"type": "
|
|
459
|
+
"type": "file",
|
|
460
460
|
"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.",
|
|
461
461
|
"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
462
|
"required": false
|
|
463
463
|
},
|
|
464
464
|
{
|
|
465
465
|
"id": "ci-crypto-tests",
|
|
466
|
-
"type": "
|
|
466
|
+
"type": "file",
|
|
467
467
|
"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`).",
|
|
468
468
|
"description": "Build-time crypto verification. Absence of constant-time tests on vendored PQC primitives is a finding for the `pqc-readiness-gap` directive.",
|
|
469
469
|
"required": false
|
|
@@ -339,7 +339,7 @@
|
|
|
339
339
|
},
|
|
340
340
|
{
|
|
341
341
|
"id": "libssl-libraries",
|
|
342
|
-
"type": "
|
|
342
|
+
"type": "file",
|
|
343
343
|
"source": "ldconfig -p | grep -E 'libssl|libcrypto|libtls' AND find /usr/lib /usr/local/lib -name 'libssl*' -o -name 'libcrypto*' 2>/dev/null",
|
|
344
344
|
"description": "Installed TLS libraries — catches non-default OpenSSL builds, vendored libcrypto, LibreSSL, BoringSSL.",
|
|
345
345
|
"required": false
|
|
@@ -43,7 +43,10 @@
|
|
|
43
43
|
"on_fail": "warn"
|
|
44
44
|
}
|
|
45
45
|
],
|
|
46
|
-
"mutex": [
|
|
46
|
+
"mutex": [
|
|
47
|
+
"secrets",
|
|
48
|
+
"containers"
|
|
49
|
+
],
|
|
47
50
|
"feeds_into": [
|
|
48
51
|
{
|
|
49
52
|
"playbook_id": "sbom",
|
|
@@ -563,14 +566,14 @@
|
|
|
563
566
|
},
|
|
564
567
|
{
|
|
565
568
|
"id": "signing-key-material",
|
|
566
|
-
"type": "
|
|
569
|
+
"type": "file",
|
|
567
570
|
"source": "Locate: cosign.pub, *.pem (public-key signing artifacts), keys/public.pem, .well-known/openpgpkey/, sigstore-cosign metadata in workflow",
|
|
568
571
|
"description": "Public signing material committed to the repo or referenced from the release workflow.",
|
|
569
572
|
"required": false
|
|
570
573
|
},
|
|
571
574
|
{
|
|
572
575
|
"id": "intoto-attestations",
|
|
573
|
-
"type": "
|
|
576
|
+
"type": "file",
|
|
574
577
|
"source": "Glob: *.intoto.jsonl, *.sigstore, *.sbom.json.sig, *.cdx.json.sig, attestations directory; AND for npm packages: GET https://registry.npmjs.org/${pkg} and inspect dist.attestations",
|
|
575
578
|
"description": "in-toto / Sigstore / SLSA provenance attestations attached to releases.",
|
|
576
579
|
"required": false,
|
|
@@ -592,7 +595,7 @@
|
|
|
592
595
|
},
|
|
593
596
|
{
|
|
594
597
|
"id": "vendored-code",
|
|
595
|
-
"type": "
|
|
598
|
+
"type": "file",
|
|
596
599
|
"source": "List directories: vendor/, third_party/, external/, deps/, internal/vendor/; AND for each: locate provenance manifest (vendor.json, THIRD_PARTY_PROVENANCE.md, modules.txt for Go vendored)",
|
|
597
600
|
"description": "Vendored / bundled third-party code carried in the publisher's own release.",
|
|
598
601
|
"required": false
|
|
@@ -644,7 +647,7 @@
|
|
|
644
647
|
},
|
|
645
648
|
{
|
|
646
649
|
"id": "skill-signing-infrastructure",
|
|
647
|
-
"type": "
|
|
650
|
+
"type": "file",
|
|
648
651
|
"source": "If repo ships skills / plugins / extensions: locate lib/sign.js, lib/verify.js, scripts/sign-skills.*, signatures/, keys/, AND read package.json scripts for sign/verify entries",
|
|
649
652
|
"description": "Specific to AI-tool / plugin / skill publishers: Ed25519 / cosign signing scaffolding.",
|
|
650
653
|
"required": false
|
|
@@ -786,7 +789,11 @@
|
|
|
786
789
|
"description": "Mutable action reference — upstream owner can substitute action code under the same tag.",
|
|
787
790
|
"confidence": "deterministic",
|
|
788
791
|
"deterministic": true,
|
|
789
|
-
"attack_ref": "T1195.001"
|
|
792
|
+
"attack_ref": "T1195.001",
|
|
793
|
+
"false_positive_checks_required": [
|
|
794
|
+
"If Dependabot is configured (.github/dependabot.yml with package-ecosystem: github-actions, schedule >= weekly) AND the repo has a recent (within last 60 days) Dependabot PR updating action SHAs — the mutable-ref window is bounded; demote to lower-confidence finding (not miss; tag-pinning is still strictly safer than SHA-pinning-with-Dependabot).",
|
|
795
|
+
"If every mutable ref points to a github-owned action (actions/*, github/*) the supply-chain risk is materially lower than third-party action refs; tag-with-rationale-in-comment is acceptable for github-owned. Demote third-party-only mutable refs above github-owned ones in reporting."
|
|
796
|
+
]
|
|
790
797
|
},
|
|
791
798
|
{
|
|
792
799
|
"id": "package-json-provenance-missing",
|
|
@@ -839,11 +846,15 @@
|
|
|
839
846
|
},
|
|
840
847
|
{
|
|
841
848
|
"id": "tag-protection-absent",
|
|
842
|
-
"type": "
|
|
849
|
+
"type": "api_call_sequence",
|
|
843
850
|
"value": "Within the branch-tag-protection artifact: gh api repos/${owner}/${repo}/tags/protection returns empty array OR 404 AND the release-workflows artifact contains a publish workflow that triggers on push tags: ['v*']",
|
|
844
851
|
"description": "Release tags unprotected (theater #6) — any branch can push v* and trigger publish.",
|
|
845
852
|
"confidence": "deterministic",
|
|
846
|
-
"deterministic": true
|
|
853
|
+
"deterministic": true,
|
|
854
|
+
"false_positive_checks_required": [
|
|
855
|
+
"If repository rulesets (newer model) protect the tag pattern v*.*.* with delete + non-fast-forward + update blocked AND zero bypass actors — the legacy tag-protection-rules API may return empty while protection is in place. Cross-check the rulesets API before flagging.",
|
|
856
|
+
"If the repository is read-only / archived (archived: true via the repo API) — tag protection is moot; demote to miss."
|
|
857
|
+
]
|
|
847
858
|
},
|
|
848
859
|
{
|
|
849
860
|
"id": "release-tag-not-signed",
|
|
@@ -871,7 +882,7 @@
|
|
|
871
882
|
},
|
|
872
883
|
{
|
|
873
884
|
"id": "no-security-txt",
|
|
874
|
-
"type": "
|
|
885
|
+
"type": "api_call_sequence",
|
|
875
886
|
"value": "If product has a primary domain: HTTP 404 / connection failure on /.well-known/security.txt",
|
|
876
887
|
"description": "No RFC 9116 machine-discoverable disclosure path (theater #10).",
|
|
877
888
|
"confidence": "deterministic",
|
|
@@ -879,7 +890,7 @@
|
|
|
879
890
|
},
|
|
880
891
|
{
|
|
881
892
|
"id": "private-vuln-reporting-disabled",
|
|
882
|
-
"type": "
|
|
893
|
+
"type": "api_call_sequence",
|
|
883
894
|
"value": "gh api repos/${owner}/${repo} --jq '.security_and_analysis.private_vulnerability_reporting.status' returns 'disabled'",
|
|
884
895
|
"description": "GitHub private vulnerability reporting off — no OIDC-authenticated disclosure intake.",
|
|
885
896
|
"confidence": "deterministic",
|
package/data/playbooks/mcp.json
CHANGED
|
@@ -440,7 +440,7 @@
|
|
|
440
440
|
},
|
|
441
441
|
{
|
|
442
442
|
"id": "mcp-tool-response-log",
|
|
443
|
-
"type": "
|
|
443
|
+
"type": "log",
|
|
444
444
|
"source": "AI client MCP-protocol logs: ~/.claude/logs/mcp/*.jsonl (Claude Code), ~/.cursor/logs/mcp-*.log (Cursor), ~/.codeium/windsurf/logs/mcp_*.log (Windsurf)",
|
|
445
445
|
"description": "v0.12.6: Verbatim tools/list and tools/call response capture. The only artifact that lets ANSI-escape, Unicode-Tag-smuggling, instruction-coercion-grammar, and sensitive-path-reference indicators fire. If client doesn't log MCP responses, mark inconclusive and recommend enabling MCP request/response verbose logging in client settings.",
|
|
446
446
|
"required": false
|
package/data/playbooks/sbom.json
CHANGED
|
@@ -499,7 +499,7 @@
|
|
|
499
499
|
},
|
|
500
500
|
{
|
|
501
501
|
"id": "model-weight-files",
|
|
502
|
-
"type": "
|
|
502
|
+
"type": "file",
|
|
503
503
|
"source": "find $HOME ~/.cache/huggingface ~/.cache/torch /var/lib/model-store -type f \\( -name '*.pt' -o -name '*.ckpt' -o -name '*.bin' -o -name '*.safetensors' -o -name '*.gguf' \\) 2>/dev/null AND for each: file <path> AND attempt to identify Sigstore signature / OpenSSF model-signing attestation",
|
|
504
504
|
"description": "Model weight artifacts — needed for AI-supply-chain analysis (ATLAS AML.T0018).",
|
|
505
505
|
"required": false
|
|
@@ -528,7 +528,7 @@
|
|
|
528
528
|
},
|
|
529
529
|
{
|
|
530
530
|
"id": "tanstack-payload-sweep",
|
|
531
|
-
"type": "
|
|
531
|
+
"type": "file",
|
|
532
532
|
"source": "find node_modules -path '*/@tanstack/*' \\( -name 'router_init.js' -o -name 'router_runtime.js' \\) 2>/dev/null",
|
|
533
533
|
"description": "CVE-2026-45321 IoC sweep — payload markers inside any installed @tanstack/* package. Captures both flat npm and pnpm-style nested layouts.",
|
|
534
534
|
"required": false
|
package/lib/auto-discovery.js
CHANGED
|
@@ -31,6 +31,31 @@ const fs = require("fs");
|
|
|
31
31
|
const path = require("path");
|
|
32
32
|
const { scoreCustom } = require("./scoring");
|
|
33
33
|
|
|
34
|
+
// audit M P1-C: stored rwep_factors must reproduce the stored rwep_score.
|
|
35
|
+
// `buildScoringInputs` is the single source of truth for both — it captures
|
|
36
|
+
// the conservative defaults applied to a freshly-imported KEV draft (CISA
|
|
37
|
+
// only lists vulnerabilities with documented exploitation, so we assume a
|
|
38
|
+
// public PoC exists; reboot defaults to true because most KEV-listed CVEs
|
|
39
|
+
// land in the kernel / hypervisor / vendor firmware where reboot is the
|
|
40
|
+
// norm). The same input object is then handed to scoreCustom for the score
|
|
41
|
+
// AND mapped into the `rwep_factors` shape stored on the draft. Calling
|
|
42
|
+
// scoring.validate() on the post-import catalog will no longer flag every
|
|
43
|
+
// auto-imported draft for divergence > 5.
|
|
44
|
+
function buildScoringInputs(kevEntry /*, nvdPayload */) {
|
|
45
|
+
void kevEntry;
|
|
46
|
+
return {
|
|
47
|
+
cisa_kev: true,
|
|
48
|
+
poc_available: true,
|
|
49
|
+
ai_assisted_weapon: false,
|
|
50
|
+
ai_discovered: false,
|
|
51
|
+
active_exploitation: "suspected",
|
|
52
|
+
blast_radius: 15,
|
|
53
|
+
patch_available: false,
|
|
54
|
+
live_patch_available: false,
|
|
55
|
+
reboot_required: true,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
34
59
|
const TODAY = new Date().toISOString().slice(0, 10);
|
|
35
60
|
const TIMEOUT_MS = 10_000;
|
|
36
61
|
const USER_AGENT = "exceptd-security/auto-discovery (+https://exceptd.com)";
|
|
@@ -110,37 +135,17 @@ function buildKevDraftEntry(kevEntry, nvdPayload, epssPayload) {
|
|
|
110
135
|
const knownRansomware =
|
|
111
136
|
String(kevEntry.knownRansomwareCampaignUse || "").toLowerCase() === "known";
|
|
112
137
|
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
patch_available: null,
|
|
125
|
-
live_patch_available: null,
|
|
126
|
-
reboot_required: null,
|
|
127
|
-
};
|
|
128
|
-
// scoreCustom() treats null fields as false, which under-counts the
|
|
129
|
-
// score. Pass concrete defaults for unknowns: poc_available=true is
|
|
130
|
-
// the conservative assumption for KEV entries (CISA generally only
|
|
131
|
-
// adds entries with documented exploitation), and reboot_required=
|
|
132
|
-
// true biases toward urgency.
|
|
133
|
-
const rwep_score = scoreCustom({
|
|
134
|
-
cisa_kev: true,
|
|
135
|
-
poc_available: true,
|
|
136
|
-
ai_assisted_weapon: false,
|
|
137
|
-
ai_discovered: false,
|
|
138
|
-
active_exploitation: "suspected",
|
|
139
|
-
blast_radius: 15,
|
|
140
|
-
patch_available: false,
|
|
141
|
-
live_patch_available: false,
|
|
142
|
-
reboot_required: true,
|
|
143
|
-
});
|
|
138
|
+
// audit M P1-C: stored rwep_factors and computed rwep_score MUST agree.
|
|
139
|
+
// Previously rwep_factors held nulls (for unknown poc/ai/reboot) but
|
|
140
|
+
// rwep_score was computed from concrete defaults (poc=true, reboot=true).
|
|
141
|
+
// `scoring.validate()` then flagged every auto-imported draft for
|
|
142
|
+
// divergence > 5. Now: one canonical input object → both surfaces.
|
|
143
|
+
// The curation flow rewrites these once an operator answers the editorial
|
|
144
|
+
// questions; until then, the boolean shape on rwep_factors is the
|
|
145
|
+
// conservative-default snapshot and reproduces the score exactly.
|
|
146
|
+
const scoringInputs = buildScoringInputs(kevEntry, nvdPayload);
|
|
147
|
+
const rwep_factors = { ...scoringInputs };
|
|
148
|
+
const rwep_score = scoreCustom(scoringInputs);
|
|
144
149
|
|
|
145
150
|
const product = [kevEntry.vendorProject, kevEntry.product]
|
|
146
151
|
.filter(Boolean)
|
package/lib/cve-curation.js
CHANGED
|
@@ -43,6 +43,14 @@ const path = require("path");
|
|
|
43
43
|
// before deciding promotion.
|
|
44
44
|
const { withCatalogLock } = require("./refresh-external");
|
|
45
45
|
const { validate: validateAgainstSchema } = require("./validate-cve-catalog");
|
|
46
|
+
// audit J F3: derive rwep_score via the canonical scoring helper rather
|
|
47
|
+
// than a blind `Object.values(...).reduce(sum)`. The helper detects shape
|
|
48
|
+
// (boolean inputs → scoreCustom; post-weight numeric inputs → sum + clamp)
|
|
49
|
+
// so the curation apply-path produces a score that matches whatever the
|
|
50
|
+
// catalog scorer or playbook-runner would have produced for the same
|
|
51
|
+
// factors. Direct dependency on scoring.js is intentional — scoring.js is
|
|
52
|
+
// the authoritative formula.
|
|
53
|
+
const { deriveRwepFromFactors } = require("./scoring");
|
|
46
54
|
|
|
47
55
|
const ROOT = path.resolve(__dirname, "..");
|
|
48
56
|
const CVE_SCHEMA_PATH = path.join(ROOT, "lib", "schemas", "cve-catalog.schema.json");
|
|
@@ -562,17 +570,15 @@ function applyAnswersUnderLock(cveId, catalog, catalogPath, answers) {
|
|
|
562
570
|
appliedFields.push(field);
|
|
563
571
|
}
|
|
564
572
|
|
|
565
|
-
//
|
|
566
|
-
//
|
|
567
|
-
// numeric
|
|
573
|
+
// audit J F3: derive rwep_score via the canonical scoring helper rather
|
|
574
|
+
// than a blind sum. deriveRwepFromFactors detects shape (boolean inputs
|
|
575
|
+
// → scoreCustom; post-weight numeric inputs → sum + clamp) and routes
|
|
576
|
+
// accordingly, so the apply-path produces a score that agrees with
|
|
577
|
+
// scoring.validate() instead of diverging from it.
|
|
568
578
|
if ("rwep_factors" in answers && !("rwep_score" in answers)
|
|
569
579
|
&& entry.rwep_factors && typeof entry.rwep_factors === "object") {
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
if (typeof v === "number") sum += v;
|
|
573
|
-
}
|
|
574
|
-
entry.rwep_score = Math.max(0, Math.min(100, sum));
|
|
575
|
-
appliedFields.push("rwep_score (derived from rwep_factors)");
|
|
580
|
+
entry.rwep_score = deriveRwepFromFactors(entry.rwep_factors);
|
|
581
|
+
appliedFields.push("rwep_score (derived from rwep_factors via scoring.deriveRwepFromFactors)");
|
|
576
582
|
}
|
|
577
583
|
|
|
578
584
|
// last_updated reflects the apply moment.
|
package/lib/prefetch.js
CHANGED
|
@@ -15,8 +15,14 @@
|
|
|
15
15
|
* kev/known_exploited_vulnerabilities.json — full KEV feed
|
|
16
16
|
* nvd/<cve-id>.json — NVD 2.0 per-CVE response
|
|
17
17
|
* epss/<cve-id>.json — EPSS per-CVE response
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* rfc/<doc-name>.json — IETF Datatracker doc record
|
|
19
|
+
* pins/<owner>__<repo>__releases.json — MITRE GitHub releases listing
|
|
20
|
+
*
|
|
21
|
+
* audit M P2-K: the registered source names in SOURCES below are `rfc` and
|
|
22
|
+
* `pins`. Earlier comments + --help text said `ietf` and `github`; an
|
|
23
|
+
* operator running `--source ietf` or `--source github` would hit "unknown
|
|
24
|
+
* source" because no such key exists. The names below are the canonical
|
|
25
|
+
* ones consumed by --source filtering.
|
|
20
26
|
*
|
|
21
27
|
* Usage:
|
|
22
28
|
* node lib/prefetch.js # fetch everything not fresh
|
|
@@ -137,8 +143,8 @@ Sources:
|
|
|
137
143
|
kev CISA Known Exploited Vulnerabilities
|
|
138
144
|
nvd NIST NVD 2.0 per-CVE
|
|
139
145
|
epss FIRST EPSS per-CVE
|
|
140
|
-
|
|
141
|
-
|
|
146
|
+
rfc IETF Datatracker per-RFC
|
|
147
|
+
pins MITRE GitHub releases (ATLAS / ATT&CK)
|
|
142
148
|
|
|
143
149
|
Options:
|
|
144
150
|
--max-age <dur> skip entries fresher than this (e.g. 12h, 1d). Default: 24h.
|
|
@@ -296,7 +302,15 @@ function isFresh(idx, source, id, maxAgeMs) {
|
|
|
296
302
|
|
|
297
303
|
function authHeadersForSource(source) {
|
|
298
304
|
if (source === "nvd" && process.env.NVD_API_KEY) return { apiKey: process.env.NVD_API_KEY };
|
|
299
|
-
|
|
305
|
+
// audit M P2-J: the registered source name for MITRE GitHub releases is
|
|
306
|
+
// `pins` (see SOURCES above). The prior check looked for `github`, so
|
|
307
|
+
// GITHUB_TOKEN never reached the per-request Authorization header and
|
|
308
|
+
// anonymous-rate-limited fetches were always used even when an operator
|
|
309
|
+
// had supplied a token. Accept both spellings so this is forgiving of
|
|
310
|
+
// the historical naming and the registered name.
|
|
311
|
+
if ((source === "pins" || source === "github") && process.env.GITHUB_TOKEN) {
|
|
312
|
+
return { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` };
|
|
313
|
+
}
|
|
300
314
|
return {};
|
|
301
315
|
}
|
|
302
316
|
|
|
@@ -459,13 +473,21 @@ function readCached(cacheDir, source, id, opts = {}) {
|
|
|
459
473
|
const idx = loadIndex(cacheDir);
|
|
460
474
|
const meta = idx.entries[entryKey(source, id)];
|
|
461
475
|
if (!meta) return null;
|
|
462
|
-
|
|
463
|
-
|
|
476
|
+
// audit M P2-L: when `fetched_at` is missing / non-string / unparseable,
|
|
477
|
+
// `new Date(undefined).getTime()` is NaN and `NaN > maxAgeMs` is false —
|
|
478
|
+
// so the cached entry would have been returned as if fresh. Treat any
|
|
479
|
+
// non-finite age as "no provenance, refuse" unless the caller explicitly
|
|
480
|
+
// opted into allowStale.
|
|
481
|
+
const ageMs = meta.fetched_at ? Date.now() - new Date(meta.fetched_at).getTime() : NaN;
|
|
482
|
+
if (!opts.allowStale) {
|
|
483
|
+
if (!meta.fetched_at || !Number.isFinite(ageMs)) return null;
|
|
484
|
+
if (ageMs > maxAgeMs) return null;
|
|
485
|
+
}
|
|
464
486
|
const p = entryPath(cacheDir, source, id);
|
|
465
487
|
if (!fs.existsSync(p)) return null;
|
|
466
488
|
try {
|
|
467
489
|
const data = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
468
|
-
return { data, age_ms: ageMs, meta };
|
|
490
|
+
return { data, age_ms: Number.isFinite(ageMs) ? ageMs : null, meta };
|
|
469
491
|
} catch {
|
|
470
492
|
return null;
|
|
471
493
|
}
|
package/lib/refresh-network.js
CHANGED
|
@@ -357,6 +357,46 @@ async function main() {
|
|
|
357
357
|
process.exitCode = 5; return;
|
|
358
358
|
}
|
|
359
359
|
|
|
360
|
+
// v0.12.16 (audit I P1-5): cross-check the local public key against
|
|
361
|
+
// keys/EXPECTED_FINGERPRINT (the CI-pinned signing key). The prior
|
|
362
|
+
// refresh-network code only compared LOCAL ↔ TARBALL fingerprints, so a
|
|
363
|
+
// coordinated attacker who swapped both `keys/public.pem` on the operator's
|
|
364
|
+
// host AND the registry tarball passed every check — fingerprints match
|
|
365
|
+
// each other but match the attacker's key. The pin in EXPECTED_FINGERPRINT
|
|
366
|
+
// is the external trust anchor that closes this gap.
|
|
367
|
+
//
|
|
368
|
+
// Honors `KEYS_ROTATED=1` env to allow legitimate key rotation without
|
|
369
|
+
// re-bootstrap. Missing EXPECTED_FINGERPRINT file → warn-and-continue
|
|
370
|
+
// (don't break existing installs whose tree predates the pin file).
|
|
371
|
+
const expectedFingerprintPath = path.join(ROOT, "keys", "EXPECTED_FINGERPRINT");
|
|
372
|
+
if (fs.existsSync(expectedFingerprintPath) && !process.env.KEYS_ROTATED) {
|
|
373
|
+
try {
|
|
374
|
+
const expectedFp = fs.readFileSync(expectedFingerprintPath, "utf8")
|
|
375
|
+
.split(/\r?\n/).map(l => l.trim()).find(l => l.length > 0);
|
|
376
|
+
// v0.12.16 (codex P1 PR #11): `expectedFp` is read verbatim from
|
|
377
|
+
// keys/EXPECTED_FINGERPRINT (formatted as `SHA256:<base64>`), but
|
|
378
|
+
// `fingerprintPublicKey()` returns the raw base64 without the
|
|
379
|
+
// `SHA256:` prefix. Comparing the two raw strings would refuse every
|
|
380
|
+
// legitimate run unless KEYS_ROTATED=1 was set. Normalize by stripping
|
|
381
|
+
// the prefix from the pin file before compare. lib/verify.js's
|
|
382
|
+
// checkExpectedFingerprint() does the symmetric thing (adds the
|
|
383
|
+
// prefix to localFp); either side works as long as one is canonical.
|
|
384
|
+
const expectedFpBase64 = expectedFp && expectedFp.startsWith("SHA256:")
|
|
385
|
+
? expectedFp.slice("SHA256:".length)
|
|
386
|
+
: expectedFp;
|
|
387
|
+
if (expectedFpBase64 && expectedFpBase64 !== localFp) {
|
|
388
|
+
emit({
|
|
389
|
+
ok: false,
|
|
390
|
+
error: `local keys/public.pem fingerprint diverges from keys/EXPECTED_FINGERPRINT pin`,
|
|
391
|
+
local_fingerprint: "SHA256:" + localFp,
|
|
392
|
+
pinned_fingerprint: expectedFp,
|
|
393
|
+
hint: "Either keys/public.pem was rotated since the pin was set (rerun `npm run bootstrap` to re-pin), or the local public.pem was tampered with. Set KEYS_ROTATED=1 to bypass once. Refusing to swap on --network.",
|
|
394
|
+
}, opts.json);
|
|
395
|
+
process.exitCode = 5; return;
|
|
396
|
+
}
|
|
397
|
+
} catch { /* unreadable pin file = warn-and-continue */ }
|
|
398
|
+
}
|
|
399
|
+
|
|
360
400
|
// Verify every signed entry in the tarball manifest using the local key.
|
|
361
401
|
let tarballManifest;
|
|
362
402
|
try { tarballManifest = JSON.parse(tarballManifestEntry.body.toString("utf8")); }
|