@quantakrypto/core 0.4.3 → 0.5.0
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/README.md +76 -27
- package/dist/cbom-merge.d.ts +9 -0
- package/dist/cbom-merge.d.ts.map +1 -0
- package/dist/cbom-merge.js +121 -0
- package/dist/cbom-merge.js.map +1 -0
- package/dist/cbom.d.ts +5 -0
- package/dist/cbom.d.ts.map +1 -1
- package/dist/cbom.js +132 -37
- package/dist/cbom.js.map +1 -1
- package/dist/comments.d.ts +6 -26
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +77 -38
- package/dist/comments.js.map +1 -1
- package/dist/dependencies.d.ts.map +1 -1
- package/dist/dependencies.js +1 -1
- package/dist/dependencies.js.map +1 -1
- package/dist/detect-utils.d.ts +59 -21
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +143 -8
- package/dist/detect-utils.js.map +1 -1
- package/dist/detectors/age.d.ts +16 -0
- package/dist/detectors/age.d.ts.map +1 -0
- package/dist/detectors/age.js +40 -0
- package/dist/detectors/age.js.map +1 -0
- package/dist/detectors/ansible.d.ts +13 -0
- package/dist/detectors/ansible.d.ts.map +1 -0
- package/dist/detectors/ansible.js +108 -0
- package/dist/detectors/ansible.js.map +1 -0
- package/dist/detectors/bicep.d.ts +20 -0
- package/dist/detectors/bicep.d.ts.map +1 -0
- package/dist/detectors/bicep.js +74 -0
- package/dist/detectors/bicep.js.map +1 -0
- package/dist/detectors/c.d.ts +4 -2
- package/dist/detectors/c.d.ts.map +1 -1
- package/dist/detectors/c.js +222 -4
- package/dist/detectors/c.js.map +1 -1
- package/dist/detectors/cicd.d.ts +27 -0
- package/dist/detectors/cicd.d.ts.map +1 -0
- package/dist/detectors/cicd.js +129 -0
- package/dist/detectors/cicd.js.map +1 -0
- package/dist/detectors/cloud-kms.d.ts +24 -0
- package/dist/detectors/cloud-kms.d.ts.map +1 -0
- package/dist/detectors/cloud-kms.js +99 -0
- package/dist/detectors/cloud-kms.js.map +1 -0
- package/dist/detectors/cloudformation.d.ts +38 -0
- package/dist/detectors/cloudformation.d.ts.map +1 -0
- package/dist/detectors/cloudformation.js +201 -0
- package/dist/detectors/cloudformation.js.map +1 -0
- package/dist/detectors/codesign.d.ts +50 -0
- package/dist/detectors/codesign.d.ts.map +1 -0
- package/dist/detectors/codesign.js +143 -0
- package/dist/detectors/codesign.js.map +1 -0
- package/dist/detectors/csharp.d.ts.map +1 -1
- package/dist/detectors/csharp.js +13 -3
- package/dist/detectors/csharp.js.map +1 -1
- package/dist/detectors/dart.d.ts +35 -0
- package/dist/detectors/dart.d.ts.map +1 -0
- package/dist/detectors/dart.js +177 -0
- package/dist/detectors/dart.js.map +1 -0
- package/dist/detectors/database.d.ts +19 -0
- package/dist/detectors/database.d.ts.map +1 -0
- package/dist/detectors/database.js +97 -0
- package/dist/detectors/database.js.map +1 -0
- package/dist/detectors/dkim.d.ts +54 -0
- package/dist/detectors/dkim.d.ts.map +1 -0
- package/dist/detectors/dkim.js +92 -0
- package/dist/detectors/dkim.js.map +1 -0
- package/dist/detectors/dnssec.d.ts +55 -0
- package/dist/detectors/dnssec.d.ts.map +1 -0
- package/dist/detectors/dnssec.js +116 -0
- package/dist/detectors/dnssec.js.map +1 -0
- package/dist/detectors/elixir.d.ts +20 -0
- package/dist/detectors/elixir.d.ts.map +1 -0
- package/dist/detectors/elixir.js +227 -0
- package/dist/detectors/elixir.js.map +1 -0
- package/dist/detectors/java.d.ts.map +1 -1
- package/dist/detectors/java.js +34 -4
- package/dist/detectors/java.js.map +1 -1
- package/dist/detectors/jose.d.ts +20 -0
- package/dist/detectors/jose.d.ts.map +1 -0
- package/dist/detectors/jose.js +72 -0
- package/dist/detectors/jose.js.map +1 -0
- package/dist/detectors/jwk.d.ts +25 -0
- package/dist/detectors/jwk.d.ts.map +1 -0
- package/dist/detectors/jwk.js +131 -0
- package/dist/detectors/jwk.js.map +1 -0
- package/dist/detectors/k8s.d.ts +17 -0
- package/dist/detectors/k8s.d.ts.map +1 -0
- package/dist/detectors/k8s.js +88 -0
- package/dist/detectors/k8s.js.map +1 -0
- package/dist/detectors/keystore.d.ts +19 -0
- package/dist/detectors/keystore.d.ts.map +1 -0
- package/dist/detectors/keystore.js +76 -0
- package/dist/detectors/keystore.js.map +1 -0
- package/dist/detectors/mesh.d.ts +34 -0
- package/dist/detectors/mesh.d.ts.map +1 -0
- package/dist/detectors/mesh.js +91 -0
- package/dist/detectors/mesh.js.map +1 -0
- package/dist/detectors/messaging.d.ts +16 -0
- package/dist/detectors/messaging.d.ts.map +1 -0
- package/dist/detectors/messaging.js +79 -0
- package/dist/detectors/messaging.js.map +1 -0
- package/dist/detectors/objc.d.ts +41 -0
- package/dist/detectors/objc.d.ts.map +1 -0
- package/dist/detectors/objc.js +146 -0
- package/dist/detectors/objc.js.map +1 -0
- package/dist/detectors/openpgp.d.ts +20 -0
- package/dist/detectors/openpgp.d.ts.map +1 -0
- package/dist/detectors/openpgp.js +151 -0
- package/dist/detectors/openpgp.js.map +1 -0
- package/dist/detectors/pem.d.ts.map +1 -1
- package/dist/detectors/pem.js +33 -1
- package/dist/detectors/pem.js.map +1 -1
- package/dist/detectors/php.d.ts +25 -0
- package/dist/detectors/php.d.ts.map +1 -0
- package/dist/detectors/php.js +171 -0
- package/dist/detectors/php.js.map +1 -0
- package/dist/detectors/pkcs11.d.ts +17 -0
- package/dist/detectors/pkcs11.d.ts.map +1 -0
- package/dist/detectors/pkcs11.js +87 -0
- package/dist/detectors/pkcs11.js.map +1 -0
- package/dist/detectors/proxy.d.ts +52 -0
- package/dist/detectors/proxy.d.ts.map +1 -0
- package/dist/detectors/proxy.js +157 -0
- package/dist/detectors/proxy.js.map +1 -0
- package/dist/detectors/pulumi.d.ts +19 -0
- package/dist/detectors/pulumi.d.ts.map +1 -0
- package/dist/detectors/pulumi.js +80 -0
- package/dist/detectors/pulumi.js.map +1 -0
- package/dist/detectors/python.d.ts.map +1 -1
- package/dist/detectors/python.js +70 -0
- package/dist/detectors/python.js.map +1 -1
- package/dist/detectors/rust.d.ts.map +1 -1
- package/dist/detectors/rust.js +73 -0
- package/dist/detectors/rust.js.map +1 -1
- package/dist/detectors/secrets.d.ts +24 -0
- package/dist/detectors/secrets.d.ts.map +1 -0
- package/dist/detectors/secrets.js +63 -0
- package/dist/detectors/secrets.js.map +1 -0
- package/dist/detectors/solidity.d.ts +50 -0
- package/dist/detectors/solidity.d.ts.map +1 -0
- package/dist/detectors/solidity.js +108 -0
- package/dist/detectors/solidity.js.map +1 -0
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +243 -53
- package/dist/detectors/source.js.map +1 -1
- package/dist/detectors/spire.d.ts +49 -0
- package/dist/detectors/spire.d.ts.map +1 -0
- package/dist/detectors/spire.js +77 -0
- package/dist/detectors/spire.js.map +1 -0
- package/dist/detectors/ssh-ca.d.ts +72 -0
- package/dist/detectors/ssh-ca.d.ts.map +1 -0
- package/dist/detectors/ssh-ca.js +119 -0
- package/dist/detectors/ssh-ca.js.map +1 -0
- package/dist/detectors/stateful-hbs.d.ts.map +1 -1
- package/dist/detectors/stateful-hbs.js +9 -6
- package/dist/detectors/stateful-hbs.js.map +1 -1
- package/dist/detectors/supply-chain.d.ts +17 -0
- package/dist/detectors/supply-chain.d.ts.map +1 -0
- package/dist/detectors/supply-chain.js +89 -0
- package/dist/detectors/supply-chain.js.map +1 -0
- package/dist/detectors/swift.d.ts +22 -0
- package/dist/detectors/swift.d.ts.map +1 -0
- package/dist/detectors/swift.js +120 -0
- package/dist/detectors/swift.js.map +1 -0
- package/dist/detectors/terraform.d.ts +21 -0
- package/dist/detectors/terraform.d.ts.map +1 -0
- package/dist/detectors/terraform.js +150 -0
- package/dist/detectors/terraform.js.map +1 -0
- package/dist/detectors/vault.d.ts +15 -0
- package/dist/detectors/vault.d.ts.map +1 -0
- package/dist/detectors/vault.js +97 -0
- package/dist/detectors/vault.js.map +1 -0
- package/dist/detectors/vpn.d.ts +22 -0
- package/dist/detectors/vpn.d.ts.map +1 -0
- package/dist/detectors/vpn.js +73 -0
- package/dist/detectors/vpn.js.map +1 -0
- package/dist/detectors/weak-hash.d.ts +71 -0
- package/dist/detectors/weak-hash.d.ts.map +1 -0
- package/dist/detectors/weak-hash.js +116 -0
- package/dist/detectors/weak-hash.js.map +1 -0
- package/dist/detectors/webauthn.d.ts +61 -0
- package/dist/detectors/webauthn.d.ts.map +1 -0
- package/dist/detectors/webauthn.js +109 -0
- package/dist/detectors/webauthn.js.map +1 -0
- package/dist/detectors/xmldsig.d.ts +23 -0
- package/dist/detectors/xmldsig.d.ts.map +1 -0
- package/dist/detectors/xmldsig.js +84 -0
- package/dist/detectors/xmldsig.js.map +1 -0
- package/dist/evidence.d.ts +79 -4
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +76 -0
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +17 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -9
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +0 -2
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +15 -6
- package/dist/inventory.js.map +1 -1
- package/dist/policy.d.ts +67 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +115 -0
- package/dist/policy.js.map +1 -0
- package/dist/registry.d.ts +9 -4
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +81 -4
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts +0 -3
- package/dist/remediate-pipeline.d.ts.map +1 -1
- package/dist/remediate-pipeline.js +23 -2
- package/dist/remediate-pipeline.js.map +1 -1
- package/dist/remediation.d.ts +10 -0
- package/dist/remediation.d.ts.map +1 -1
- package/dist/remediation.js +32 -0
- package/dist/remediation.js.map +1 -1
- package/dist/report.d.ts +9 -0
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +38 -1
- package/dist/report.js.map +1 -1
- package/dist/scan-worker.js +5 -3
- package/dist/scan-worker.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +35 -6
- package/dist/scan.js.map +1 -1
- package/dist/standards-profiles.d.ts +58 -0
- package/dist/standards-profiles.d.ts.map +1 -0
- package/dist/standards-profiles.js +100 -0
- package/dist/standards-profiles.js.map +1 -0
- package/dist/standards.d.ts +96 -0
- package/dist/standards.d.ts.map +1 -0
- package/dist/standards.js +107 -0
- package/dist/standards.js.map +1 -0
- package/dist/types.d.ts +6 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/dist/vex.d.ts +39 -0
- package/dist/vex.d.ts.map +1 -0
- package/dist/vex.js +87 -0
- package/dist/vex.js.map +1 -0
- package/dist/walk.d.ts +8 -3
- package/dist/walk.d.ts.map +1 -1
- package/dist/walk.js +32 -1
- package/dist/walk.js.map +1 -1
- package/package.json +1 -1
- package/src/cbom-merge.ts +146 -0
- package/src/cbom.ts +153 -39
- package/src/comments.ts +84 -39
- package/src/dependencies.ts +2 -1
- package/src/detect-utils.ts +146 -12
- package/src/detectors/age.ts +60 -0
- package/src/detectors/ansible.ts +140 -0
- package/src/detectors/bicep.ts +107 -0
- package/src/detectors/c.ts +240 -6
- package/src/detectors/cicd.ts +171 -0
- package/src/detectors/cloud-kms.ts +141 -0
- package/src/detectors/cloudformation.ts +250 -0
- package/src/detectors/codesign.ts +214 -0
- package/src/detectors/csharp.ts +13 -3
- package/src/detectors/dart.ts +237 -0
- package/src/detectors/database.ts +141 -0
- package/src/detectors/dkim.ts +172 -0
- package/src/detectors/dnssec.ts +195 -0
- package/src/detectors/elixir.ts +268 -0
- package/src/detectors/java.ts +39 -4
- package/src/detectors/jose.ts +110 -0
- package/src/detectors/jwk.ts +170 -0
- package/src/detectors/k8s.ts +115 -0
- package/src/detectors/keystore.ts +109 -0
- package/src/detectors/mesh.ts +130 -0
- package/src/detectors/messaging.ts +106 -0
- package/src/detectors/objc.ts +209 -0
- package/src/detectors/openpgp.ts +194 -0
- package/src/detectors/pem.ts +31 -1
- package/src/detectors/php.ts +230 -0
- package/src/detectors/pkcs11.ts +113 -0
- package/src/detectors/proxy.ts +236 -0
- package/src/detectors/pulumi.ts +106 -0
- package/src/detectors/python.ts +71 -0
- package/src/detectors/rust.ts +76 -0
- package/src/detectors/secrets.ts +103 -0
- package/src/detectors/solidity.ts +176 -0
- package/src/detectors/source.ts +283 -73
- package/src/detectors/spire.ts +143 -0
- package/src/detectors/ssh-ca.ts +215 -0
- package/src/detectors/stateful-hbs.ts +9 -6
- package/src/detectors/supply-chain.ts +117 -0
- package/src/detectors/swift.ts +149 -0
- package/src/detectors/terraform.ts +188 -0
- package/src/detectors/vault.ts +120 -0
- package/src/detectors/vpn.ts +103 -0
- package/src/detectors/weak-hash.ts +217 -0
- package/src/detectors/webauthn.ts +205 -0
- package/src/detectors/xmldsig.ts +115 -0
- package/src/evidence.ts +138 -4
- package/src/index.ts +50 -12
- package/src/inventory.ts +15 -7
- package/src/policy.ts +180 -0
- package/src/registry.ts +81 -4
- package/src/remediate-pipeline.ts +26 -3
- package/src/remediation.ts +39 -0
- package/src/report.ts +43 -1
- package/src/scan-worker.ts +5 -3
- package/src/scan.ts +41 -6
- package/src/standards-profiles.ts +136 -0
- package/src/standards.ts +186 -0
- package/src/types.ts +11 -4
- package/src/version.ts +1 -1
- package/src/vex.ts +122 -0
- package/src/walk.ts +33 -2
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical DNSSEC signing algorithms in zone files and
|
|
3
|
+
* signer/resolver config (BIND, Knot DNS, PowerDNS, `ldns-signzone` /
|
|
4
|
+
* `dnssec-signzone` invocations recorded in ops config).
|
|
5
|
+
*
|
|
6
|
+
* DNSSEC is "communication between things" infrastructure: resolvers verify
|
|
7
|
+
* DNSKEY/RRSIG chains to authenticate zone data. Unlike a KEM or key-exchange
|
|
8
|
+
* finding, a DNSSEC signing key is not harvest-now-decrypt-later exposed —
|
|
9
|
+
* there is no confidentiality to harvest — but every classical signature it
|
|
10
|
+
* produces becomes FORGEABLE the moment a cryptographically-relevant quantum
|
|
11
|
+
* computer (CRQC) exists. A forged DNSKEY/RRSIG chain lets an attacker spoof
|
|
12
|
+
* DNSSEC-validated records (cache poisoning that a validating resolver would
|
|
13
|
+
* otherwise reject). So these findings are `category: "signature"`,
|
|
14
|
+
* `hndl: false`.
|
|
15
|
+
*
|
|
16
|
+
* RFC 8624 / IANA "DNSSEC Algorithm Numbers" families covered, mapped to
|
|
17
|
+
* {@link AlgorithmFamily}:
|
|
18
|
+
* - RSASHA1 (5), RSASHA1-NSEC3-SHA1 (7), RSASHA256 (8), RSASHA512 (10) → RSA
|
|
19
|
+
* - ECDSAP256SHA256 (13), ECDSAP384SHA384 (14) → ECDSA
|
|
20
|
+
* - ED25519 (15), ED448 (16) → EdDSA
|
|
21
|
+
* - DSA (3), DSA-NSEC3-SHA1 (6) → DSA
|
|
22
|
+
*
|
|
23
|
+
* Two independent match shapes per family, both gated to `.zone` / `.db` /
|
|
24
|
+
* `.conf` files AND to a file-level DNSSEC marker (see `hasDnssecMarker`
|
|
25
|
+
* below) so the algorithm tokens can't fire on unrelated config:
|
|
26
|
+
*
|
|
27
|
+
* 1. NAMED form — the algorithm's mnemonic name, as written in signer/policy
|
|
28
|
+
* config (Knot `algorithm RSASHA256;`, a `dnssec-policy` block, BIND
|
|
29
|
+
* `algorithm: ecdsap256sha256;`) or on an `ldns-signzone -a ECDSAP256SHA256`
|
|
30
|
+
* / `dnssec-signzone -a ECDSAP256SHA256` command line. These mnemonics
|
|
31
|
+
* (`RSASHA256`, `ECDSAP384SHA384`, `ED25519`, …) are distinctive enough to
|
|
32
|
+
* match bare, EXCEPT plain `DSA`, which is too generic a token on its own
|
|
33
|
+
* (used well outside DNSSEC); that one is only matched immediately after
|
|
34
|
+
* an `algorithm` keyword.
|
|
35
|
+
* 2. STRUCTURAL form — the DNSKEY presentation-format RDATA, e.g.
|
|
36
|
+
* `example.com. IN DNSKEY 257 3 8 AwEAAd...`, where the fields after the
|
|
37
|
+
* `DNSKEY` keyword are `flags protocol algorithm public-key` (RFC 4034
|
|
38
|
+
* §2). Anchoring on the literal `DNSKEY` keyword AND the literal
|
|
39
|
+
* protocol value `3` (always 3 for DNSSEC) before a restricted algorithm
|
|
40
|
+
* number keeps this precise despite being numeric. Bare algorithm
|
|
41
|
+
* numbers with no such anchor are deliberately NOT matched anywhere —
|
|
42
|
+
* `algorithm 8;` alone is far too ambiguous to key off of.
|
|
43
|
+
*
|
|
44
|
+
* Fast reject: `detect()` bails unless the file contains `DNSKEY`, `RRSIG`,
|
|
45
|
+
* a case-insensitive `dnssec` substring (present in `dnssec-policy`,
|
|
46
|
+
* `dnssec-signing: on`, etc. in any real signer config), or an
|
|
47
|
+
* `ldns-signzone` / `dnssec-signzone` invocation. This stops the DSA/EdDSA
|
|
48
|
+
* named-form tokens — which are individually generic outside DNSSEC — from
|
|
49
|
+
* firing on, say, an unrelated SSH or TLS `.conf` file that happens to
|
|
50
|
+
* mention `ED25519` or `DSA` for its own reasons.
|
|
51
|
+
*/
|
|
52
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
53
|
+
import { eachMatch, findingFromRule, hasExtension, maskCommentLines } from "../detect-utils.js";
|
|
54
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
55
|
+
|
|
56
|
+
const DNSSEC_EXTENSIONS: readonly string[] = [".zone", ".db", ".conf"];
|
|
57
|
+
|
|
58
|
+
// IANA DNSSEC algorithm numbers, grouped by family.
|
|
59
|
+
const NUM_RSA = "5|7|8|10"; // RSASHA1, RSASHA1-NSEC3-SHA1, RSASHA256, RSASHA512
|
|
60
|
+
const NUM_ECDSA = "13|14"; // ECDSAP256SHA256, ECDSAP384SHA384
|
|
61
|
+
const NUM_EDDSA = "15|16"; // ED25519, ED448
|
|
62
|
+
const NUM_DSA = "3|6"; // DSA, DSA-NSEC3-SHA1
|
|
63
|
+
|
|
64
|
+
// --- NAMED form: mnemonic algorithm names in signer/policy config or CLI args. ---
|
|
65
|
+
// Case-insensitive: BIND `dnssec-policy` and Knot config conventionally write the
|
|
66
|
+
// mnemonics in lowercase (`algorithm rsasha256;`, `algorithm ed25519;`), so a
|
|
67
|
+
// case-sensitive match would miss the primary real-world signer-config syntax. Still
|
|
68
|
+
// gated by `hasDnssecMarker` + the DNSSEC extensions, so generic tokens can't fire.
|
|
69
|
+
const RE_NAMED_RSA = /\bRSASHA(?:256|512|1(?:-NSEC3-SHA1)?)\b/gi;
|
|
70
|
+
const RE_NAMED_ECDSA = /\bECDSAP(?:256SHA256|384SHA384)\b/gi;
|
|
71
|
+
const RE_NAMED_EDDSA = /\bED(?:25519|448)\b/gi;
|
|
72
|
+
// Bare "DSA" is too generic to key off alone; require it right after an
|
|
73
|
+
// `algorithm` keyword (Knot/BIND config / dnssec-policy style), optionally
|
|
74
|
+
// quoted or separated by `:`/`=`.
|
|
75
|
+
const RE_NAMED_DSA = /\balgorithm\s*[:=]?\s*"?DSA(?:-NSEC3-SHA1)?"?\b/gi;
|
|
76
|
+
|
|
77
|
+
// --- STRUCTURAL form: `[C]DNSKEY <flags> 3 <algorithm>` presentation RDATA (RFC 4034
|
|
78
|
+
// §2). `C?` also matches CDNSKEY (child-zone key with identical RDATA, RFC 7344). ---
|
|
79
|
+
const RE_DNSKEY_RSA = new RegExp(`\\bC?DNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_RSA})\\b`, "g");
|
|
80
|
+
const RE_DNSKEY_ECDSA = new RegExp(`\\bC?DNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_ECDSA})\\b`, "g");
|
|
81
|
+
const RE_DNSKEY_EDDSA = new RegExp(`\\bC?DNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_EDDSA})\\b`, "g");
|
|
82
|
+
const RE_DNSKEY_DSA = new RegExp(`\\bC?DNSKEY\\s+\\d+\\s+3\\s+(?:${NUM_DSA})\\b`, "g");
|
|
83
|
+
|
|
84
|
+
const RULE_DNSSEC_RSA: RuleMeta = {
|
|
85
|
+
id: "dnssec-rsa-sig",
|
|
86
|
+
title: "DNSSEC RSA signing algorithm",
|
|
87
|
+
description: "DNSSEC zone signed with a classical RSA algorithm (RSASHA1/256/512)",
|
|
88
|
+
category: "signature",
|
|
89
|
+
severity: "medium",
|
|
90
|
+
confidence: "high",
|
|
91
|
+
algorithm: "RSA",
|
|
92
|
+
hndl: false,
|
|
93
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
94
|
+
message:
|
|
95
|
+
"DNSSEC zone is signed with a classical RSA algorithm (RSASHA1/RSASHA1-NSEC3-SHA1/RSASHA256/RSASHA512); DNSKEY/RRSIG signatures become forgeable once a CRQC exists.",
|
|
96
|
+
remediation:
|
|
97
|
+
"Track IETF dnsop post-quantum DNSSEC signing work (ML-DSA); plan re-signing with a PQC algorithm once assigned an IANA DNSSEC algorithm number.",
|
|
98
|
+
};
|
|
99
|
+
const RULE_DNSSEC_ECDSA: RuleMeta = {
|
|
100
|
+
id: "dnssec-ecdsa-sig",
|
|
101
|
+
title: "DNSSEC ECDSA signing algorithm",
|
|
102
|
+
description: "DNSSEC zone signed with a classical ECDSA algorithm (ECDSAP256SHA256/384SHA384)",
|
|
103
|
+
category: "signature",
|
|
104
|
+
severity: "medium",
|
|
105
|
+
confidence: "high",
|
|
106
|
+
algorithm: "ECDSA",
|
|
107
|
+
hndl: false,
|
|
108
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
109
|
+
message:
|
|
110
|
+
"DNSSEC zone is signed with a classical ECDSA algorithm (ECDSAP256SHA256/ECDSAP384SHA384); DNSKEY/RRSIG signatures become forgeable once a CRQC exists.",
|
|
111
|
+
remediation:
|
|
112
|
+
"Track IETF dnsop post-quantum DNSSEC signing work (ML-DSA); plan re-signing with a PQC algorithm once assigned an IANA DNSSEC algorithm number.",
|
|
113
|
+
};
|
|
114
|
+
const RULE_DNSSEC_EDDSA: RuleMeta = {
|
|
115
|
+
id: "dnssec-eddsa-sig",
|
|
116
|
+
title: "DNSSEC EdDSA signing algorithm",
|
|
117
|
+
description: "DNSSEC zone signed with a classical EdDSA algorithm (ED25519/ED448)",
|
|
118
|
+
category: "signature",
|
|
119
|
+
severity: "medium",
|
|
120
|
+
confidence: "high",
|
|
121
|
+
algorithm: "EdDSA",
|
|
122
|
+
hndl: false,
|
|
123
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
124
|
+
message:
|
|
125
|
+
"DNSSEC zone is signed with a classical EdDSA algorithm (ED25519/ED448); modern but still classical — DNSKEY/RRSIG signatures become forgeable once a CRQC exists.",
|
|
126
|
+
remediation:
|
|
127
|
+
"Track IETF dnsop post-quantum DNSSEC signing work (ML-DSA); plan re-signing with a PQC algorithm once assigned an IANA DNSSEC algorithm number.",
|
|
128
|
+
};
|
|
129
|
+
const RULE_DNSSEC_DSA: RuleMeta = {
|
|
130
|
+
id: "dnssec-dsa-sig",
|
|
131
|
+
title: "DNSSEC DSA signing algorithm (deprecated)",
|
|
132
|
+
description:
|
|
133
|
+
"DNSSEC zone signed with the deprecated classical DSA algorithm (DSA/DSA-NSEC3-SHA1)",
|
|
134
|
+
category: "signature",
|
|
135
|
+
severity: "medium",
|
|
136
|
+
confidence: "high",
|
|
137
|
+
algorithm: "DSA",
|
|
138
|
+
hndl: false,
|
|
139
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
140
|
+
message:
|
|
141
|
+
"DNSSEC zone is signed with DSA/DSA-NSEC3-SHA1 — deprecated by RFC 8624 (MUST NOT sign) and, independent of that, forgeable once a CRQC exists.",
|
|
142
|
+
remediation:
|
|
143
|
+
"Re-sign with a non-deprecated algorithm today (RFC 8624); track IETF dnsop post-quantum DNSSEC signing work (ML-DSA) for the eventual PQC migration.",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/** True when `content` carries some DNSSEC-specific marker (not just a bare algorithm token). */
|
|
147
|
+
function hasDnssecMarker(content: string): boolean {
|
|
148
|
+
return (
|
|
149
|
+
content.includes("DNSKEY") ||
|
|
150
|
+
content.includes("RRSIG") ||
|
|
151
|
+
/dnssec/i.test(content) ||
|
|
152
|
+
/ldns-signzone|dnssec-signzone/.test(content)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface DnssecRule {
|
|
157
|
+
meta: RuleMeta;
|
|
158
|
+
res: readonly RegExp[];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const DNSSEC_RULES: readonly DnssecRule[] = [
|
|
162
|
+
{ meta: RULE_DNSSEC_RSA, res: [RE_NAMED_RSA, RE_DNSKEY_RSA] },
|
|
163
|
+
{ meta: RULE_DNSSEC_ECDSA, res: [RE_NAMED_ECDSA, RE_DNSKEY_ECDSA] },
|
|
164
|
+
{ meta: RULE_DNSSEC_EDDSA, res: [RE_NAMED_EDDSA, RE_DNSKEY_EDDSA] },
|
|
165
|
+
{ meta: RULE_DNSSEC_DSA, res: [RE_NAMED_DSA, RE_DNSKEY_DSA] },
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
/** Detects classical DNSSEC signing algorithms in zone files and signer config. */
|
|
169
|
+
export const dnssecDetector: Detector = {
|
|
170
|
+
id: "dnssec-crypto",
|
|
171
|
+
description: "Classical DNSSEC signing algorithms in zone files / signer config",
|
|
172
|
+
scope: "config",
|
|
173
|
+
language: "any",
|
|
174
|
+
rules: DNSSEC_RULES.map((r) => r.meta),
|
|
175
|
+
appliesTo: (f) => hasExtension(f, DNSSEC_EXTENSIONS),
|
|
176
|
+
detect({ file, content }): Finding[] {
|
|
177
|
+
if (!hasDnssecMarker(content)) return [];
|
|
178
|
+
|
|
179
|
+
// Mask whole comment lines so a commented-out algorithm can't fire: zone files
|
|
180
|
+
// use `;`, named.conf uses `#` and `//`. These extensions are not centrally
|
|
181
|
+
// stripped; offsets are preserved so finding locations stay exact.
|
|
182
|
+
const scan = maskCommentLines(content, [";", "#", "//"]);
|
|
183
|
+
const findings: Finding[] = [];
|
|
184
|
+
for (const { meta, res } of DNSSEC_RULES) {
|
|
185
|
+
for (const re of res) {
|
|
186
|
+
eachMatch(re, scan, (m) =>
|
|
187
|
+
findings.push(
|
|
188
|
+
findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return findings;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Source-code detector for classical asymmetric cryptography in Elixir (the BEAM
|
|
3
|
+
* / Phoenix ecosystem), previously uncovered. Handles the three common surfaces:
|
|
4
|
+
*
|
|
5
|
+
* - **Erlang `:crypto`** — `:crypto.generate_key(:rsa|:ecdh|:dh|:eddsa, …)`
|
|
6
|
+
* (the curve atom disambiguates X25519/X448 from a generic ECDH curve) and
|
|
7
|
+
* `:crypto.sign/:crypto.verify(:rsa|:ecdsa|:eddsa, …)`.
|
|
8
|
+
* - **`X509`** (the `x509` hex package) — `X509.PrivateKey.new_rsa` / `new_ec`.
|
|
9
|
+
* - **`JOSE`** (erlang-jose) — `JOSE.JWK.generate_key({:rsa|:ec|:okp, …})`.
|
|
10
|
+
*
|
|
11
|
+
* Lexical, like the other packs; the `:crypto.` atom-module syntax and the
|
|
12
|
+
* `X509.` / `JOSE.` module paths are distinctive, so the false-positive rate is low.
|
|
13
|
+
*
|
|
14
|
+
* HNDL: RSA and (EC/X)DH key agreement are harvest-now-decrypt-later exposed
|
|
15
|
+
* (hndl:true); ECDSA / EdDSA signatures are hndl:false but forgeable.
|
|
16
|
+
*/
|
|
17
|
+
import type { AlgorithmFamily, Detector, Finding, RuleMeta } from "../types.js";
|
|
18
|
+
import { ELIXIR_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
|
|
19
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
20
|
+
|
|
21
|
+
const RE_EX_GEN = /:crypto\.generate_key\s*\(\s*:(\w+)/g;
|
|
22
|
+
// `:crypto.compute_key(:ecdh|:dh, PeerPub, MyKey, :curve)` is the DH/ECDH AGREEMENT
|
|
23
|
+
// operation itself — the harvest-now-decrypt-later step — which generate_key alone
|
|
24
|
+
// misses. Classified by the same type/curve logic as generate_key.
|
|
25
|
+
const RE_EX_COMPUTE = /:crypto\.compute_key\s*\(\s*:(\w+)/g;
|
|
26
|
+
const RE_EX_SIGN = /:crypto\.(?:sign|verify)\s*\(\s*:(\w+)/g;
|
|
27
|
+
const RE_EX_X509_RSA = /\bX509\.PrivateKey\.new_rsa\s*\(/g;
|
|
28
|
+
const RE_EX_X509_EC = /\bX509\.PrivateKey\.new_ec\s*\(/g;
|
|
29
|
+
const RE_EX_JOSE = /\bJOSE\.JWK\.generate_key\s*\(\s*\{\s*:(\w+)/g;
|
|
30
|
+
|
|
31
|
+
interface Cls {
|
|
32
|
+
algo: AlgorithmFamily;
|
|
33
|
+
cat: Finding["category"];
|
|
34
|
+
sev: Finding["severity"];
|
|
35
|
+
hndl: boolean;
|
|
36
|
+
label: string;
|
|
37
|
+
remediation?: string;
|
|
38
|
+
}
|
|
39
|
+
const SIG_REM = "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)";
|
|
40
|
+
const KEX_REM = "hybrid X25519MLKEM768 (ML-KEM-768)";
|
|
41
|
+
|
|
42
|
+
const RULE_EX_KEYGEN: RuleMeta = {
|
|
43
|
+
id: "elixir-crypto-keygen",
|
|
44
|
+
title: "Elixir :crypto key generation",
|
|
45
|
+
description: ":crypto.generate_key (rsa/ecdh/dh/eddsa)",
|
|
46
|
+
category: "kem",
|
|
47
|
+
severity: "high",
|
|
48
|
+
confidence: "high",
|
|
49
|
+
algorithm: "RSA",
|
|
50
|
+
hndl: true,
|
|
51
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
52
|
+
message: "Generates a classical key pair via Erlang :crypto (Elixir) — not quantum-safe.",
|
|
53
|
+
};
|
|
54
|
+
const RULE_EX_SIGN: RuleMeta = {
|
|
55
|
+
id: "elixir-crypto-sign",
|
|
56
|
+
title: "Elixir :crypto signature",
|
|
57
|
+
description: ":crypto.sign / :crypto.verify (rsa/ecdsa/eddsa)",
|
|
58
|
+
category: "signature",
|
|
59
|
+
severity: "high",
|
|
60
|
+
confidence: "high",
|
|
61
|
+
algorithm: "unknown",
|
|
62
|
+
hndl: false,
|
|
63
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
64
|
+
message: "Classical signature via Erlang :crypto (Elixir) is forgeable by a quantum attacker.",
|
|
65
|
+
remediation: SIG_REM,
|
|
66
|
+
};
|
|
67
|
+
const RULE_EX_X509: RuleMeta = {
|
|
68
|
+
id: "elixir-x509-keygen",
|
|
69
|
+
title: "Elixir X509 key generation",
|
|
70
|
+
description: "X509.PrivateKey.new_rsa / new_ec",
|
|
71
|
+
category: "kem",
|
|
72
|
+
severity: "high",
|
|
73
|
+
confidence: "high",
|
|
74
|
+
algorithm: "RSA",
|
|
75
|
+
hndl: true,
|
|
76
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
77
|
+
message: "Generates a classical key pair via the X509 library (Elixir) — not quantum-safe.",
|
|
78
|
+
};
|
|
79
|
+
const RULE_EX_JOSE: RuleMeta = {
|
|
80
|
+
id: "elixir-jose-jwk",
|
|
81
|
+
title: "Elixir JOSE JWK generation",
|
|
82
|
+
description: "JOSE.JWK.generate_key ({:rsa|:ec|:okp})",
|
|
83
|
+
category: "kem",
|
|
84
|
+
severity: "high",
|
|
85
|
+
confidence: "high",
|
|
86
|
+
algorithm: "RSA",
|
|
87
|
+
hndl: true,
|
|
88
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
89
|
+
message: "Generates a classical JWK via JOSE (Elixir) — not quantum-safe.",
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const RSA_CLS: Cls = { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" };
|
|
93
|
+
const DH_CLS: Cls = { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "DH" };
|
|
94
|
+
const ECDH_CLS: Cls = {
|
|
95
|
+
algo: "ECDH",
|
|
96
|
+
cat: "key-exchange",
|
|
97
|
+
sev: "high",
|
|
98
|
+
hndl: true,
|
|
99
|
+
label: "ECDH",
|
|
100
|
+
remediation: KEX_REM,
|
|
101
|
+
};
|
|
102
|
+
const X25519_CLS: Cls = {
|
|
103
|
+
algo: "X25519",
|
|
104
|
+
cat: "key-exchange",
|
|
105
|
+
sev: "medium",
|
|
106
|
+
hndl: true,
|
|
107
|
+
label: "X25519",
|
|
108
|
+
remediation: KEX_REM,
|
|
109
|
+
};
|
|
110
|
+
const X448_CLS: Cls = { ...X25519_CLS, algo: "X448", label: "X448" };
|
|
111
|
+
const EDDSA_CLS: Cls = {
|
|
112
|
+
algo: "EdDSA",
|
|
113
|
+
cat: "signature",
|
|
114
|
+
sev: "low",
|
|
115
|
+
hndl: false,
|
|
116
|
+
label: "EdDSA",
|
|
117
|
+
remediation: SIG_REM,
|
|
118
|
+
};
|
|
119
|
+
const EC_CLS: Cls = {
|
|
120
|
+
algo: "ECDH",
|
|
121
|
+
cat: "key-exchange",
|
|
122
|
+
sev: "high",
|
|
123
|
+
hndl: true,
|
|
124
|
+
label: "EC (ECDSA/ECDH)",
|
|
125
|
+
remediation: KEX_REM,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/** Classify a `:crypto.generate_key(:<type>, …)` by its type atom (+ the curve
|
|
129
|
+
* atom in a short window for the ecdh X25519/X448 case). */
|
|
130
|
+
function classifyGen(type: string, window: string): Cls | null {
|
|
131
|
+
switch (type) {
|
|
132
|
+
case "rsa":
|
|
133
|
+
return RSA_CLS;
|
|
134
|
+
case "dh":
|
|
135
|
+
return DH_CLS;
|
|
136
|
+
case "eddsa":
|
|
137
|
+
case "ed25519":
|
|
138
|
+
return EDDSA_CLS;
|
|
139
|
+
case "ecdh":
|
|
140
|
+
if (/:x25519\b/i.test(window)) return X25519_CLS;
|
|
141
|
+
if (/:x448\b/i.test(window)) return X448_CLS;
|
|
142
|
+
return ECDH_CLS;
|
|
143
|
+
default:
|
|
144
|
+
return null; // srp / other non-broken-in-this-model types
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Signature algorithm classification for `:crypto.sign(:<type>, …)`. */
|
|
149
|
+
function classifySign(type: string): AlgorithmFamily | null {
|
|
150
|
+
if (type === "rsa") return "RSA";
|
|
151
|
+
if (type === "ecdsa") return "ECDSA";
|
|
152
|
+
if (type === "eddsa" || type === "ed25519") return "EdDSA";
|
|
153
|
+
if (type === "dss") return "DSA"; // Erlang's algorithm atom for DSA signatures
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Detects classical asymmetric crypto in Elixir (:crypto, X509, JOSE). */
|
|
158
|
+
export const elixirDetector: Detector = {
|
|
159
|
+
id: "elixir-crypto",
|
|
160
|
+
description: "Classical asymmetric crypto in Elixir (:crypto, X509, JOSE)",
|
|
161
|
+
scope: "source",
|
|
162
|
+
language: "elixir",
|
|
163
|
+
rules: [RULE_EX_KEYGEN, RULE_EX_SIGN, RULE_EX_X509, RULE_EX_JOSE],
|
|
164
|
+
appliesTo: (f) => hasExtension(f, ELIXIR_EXTENSIONS),
|
|
165
|
+
detect({ file, content }): Finding[] {
|
|
166
|
+
const findings: Finding[] = [];
|
|
167
|
+
const at = (m: RegExpExecArray) => ({
|
|
168
|
+
file,
|
|
169
|
+
content,
|
|
170
|
+
index: m.index,
|
|
171
|
+
matchLength: m[0].length,
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// :crypto.generate_key(:type, …) — classify by the type atom (+ curve window).
|
|
175
|
+
eachMatch(RE_EX_GEN, content, (m) => {
|
|
176
|
+
const cls = classifyGen(m[1], content.slice(m.index, m.index + 80));
|
|
177
|
+
if (!cls) return;
|
|
178
|
+
findings.push(
|
|
179
|
+
findingFromRule(RULE_EX_KEYGEN, at(m), {
|
|
180
|
+
title: `Elixir :crypto ${cls.label} key generation`,
|
|
181
|
+
category: cls.cat,
|
|
182
|
+
severity: cls.sev,
|
|
183
|
+
algorithm: cls.algo,
|
|
184
|
+
hndl: cls.hndl,
|
|
185
|
+
message: `Generates a classical ${cls.label} key pair via Erlang :crypto (Elixir) — not quantum-safe.`,
|
|
186
|
+
...(cls.remediation ? { remediation: cls.remediation } : {}),
|
|
187
|
+
}),
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// :crypto.compute_key(:type, …) — the (EC)DH agreement operation.
|
|
192
|
+
eachMatch(RE_EX_COMPUTE, content, (m) => {
|
|
193
|
+
const cls = classifyGen(m[1], content.slice(m.index, m.index + 120));
|
|
194
|
+
if (!cls) return;
|
|
195
|
+
findings.push(
|
|
196
|
+
findingFromRule(RULE_EX_KEYGEN, at(m), {
|
|
197
|
+
title: `Elixir :crypto ${cls.label} key agreement`,
|
|
198
|
+
category: cls.cat,
|
|
199
|
+
severity: cls.sev,
|
|
200
|
+
algorithm: cls.algo,
|
|
201
|
+
hndl: cls.hndl,
|
|
202
|
+
message: `Performs classical ${cls.label} key agreement via Erlang :crypto (Elixir) — harvest-now-decrypt-later exposed.`,
|
|
203
|
+
...(cls.remediation ? { remediation: cls.remediation } : {}),
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// :crypto.sign / :crypto.verify(:type, …).
|
|
209
|
+
eachMatch(RE_EX_SIGN, content, (m) => {
|
|
210
|
+
const algo = classifySign(m[1]);
|
|
211
|
+
if (!algo) return;
|
|
212
|
+
findings.push(
|
|
213
|
+
findingFromRule(RULE_EX_SIGN, at(m), {
|
|
214
|
+
algorithm: algo,
|
|
215
|
+
message: `Classical ${algo} signature via Erlang :crypto (Elixir) is forgeable by a quantum attacker.`,
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
// X509.PrivateKey.new_rsa / new_ec.
|
|
221
|
+
eachMatch(RE_EX_X509_RSA, content, (m) =>
|
|
222
|
+
findings.push(findingFromRule(RULE_EX_X509, at(m), { algorithm: "RSA" })),
|
|
223
|
+
);
|
|
224
|
+
eachMatch(RE_EX_X509_EC, content, (m) =>
|
|
225
|
+
findings.push(
|
|
226
|
+
findingFromRule(RULE_EX_X509, at(m), {
|
|
227
|
+
title: "Elixir X509 EC key generation",
|
|
228
|
+
category: EC_CLS.cat,
|
|
229
|
+
algorithm: EC_CLS.algo,
|
|
230
|
+
hndl: EC_CLS.hndl,
|
|
231
|
+
message:
|
|
232
|
+
"Generates a classical EC key pair via the X509 library (Elixir); EC keys feed BOTH ECDSA and ECDH.",
|
|
233
|
+
remediation: KEX_REM,
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// JOSE.JWK.generate_key({:rsa|:ec|:okp, …}). An OKP key is EdDSA (Ed25519/Ed448,
|
|
239
|
+
// signature) OR a Montgomery key-agreement key (X25519/X448, HNDL) — inspect the
|
|
240
|
+
// curve atom in a short window so an X25519 OKP is not mislabeled as an EdDSA
|
|
241
|
+
// signature.
|
|
242
|
+
eachMatch(RE_EX_JOSE, content, (m) => {
|
|
243
|
+
const kind = m[1];
|
|
244
|
+
let okpCls: Cls = EDDSA_CLS;
|
|
245
|
+
if (kind === "okp") {
|
|
246
|
+
const window = content.slice(m.index, m.index + 80);
|
|
247
|
+
if (/:x25519\b/i.test(window)) okpCls = X25519_CLS;
|
|
248
|
+
else if (/:x448\b/i.test(window)) okpCls = X448_CLS;
|
|
249
|
+
}
|
|
250
|
+
const cls =
|
|
251
|
+
kind === "rsa" ? RSA_CLS : kind === "ec" ? EC_CLS : kind === "okp" ? okpCls : null;
|
|
252
|
+
if (!cls) return;
|
|
253
|
+
findings.push(
|
|
254
|
+
findingFromRule(RULE_EX_JOSE, at(m), {
|
|
255
|
+
title: `Elixir JOSE ${cls.label} JWK`,
|
|
256
|
+
category: cls.cat,
|
|
257
|
+
severity: cls.sev,
|
|
258
|
+
algorithm: cls.algo,
|
|
259
|
+
hndl: cls.hndl,
|
|
260
|
+
message: `Generates a classical ${cls.label} JWK via JOSE (Elixir) — not quantum-safe.`,
|
|
261
|
+
...(cls.remediation ? { remediation: cls.remediation } : {}),
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return findings;
|
|
267
|
+
},
|
|
268
|
+
};
|
package/src/detectors/java.ts
CHANGED
|
@@ -35,7 +35,7 @@ const RE_JAVA_GETINSTANCE =
|
|
|
35
35
|
|
|
36
36
|
// BouncyCastle lightweight-API class instantiations.
|
|
37
37
|
const RE_JAVA_BC =
|
|
38
|
-
/\bnew\s+(RSAKeyPairGenerator|DSAKeyPairGenerator|ECKeyPairGenerator|ECDSASigner|Ed25519Signer|Ed448Signer|X25519Agreement|X448Agreement|ECDHBasicAgreement|DHBasicAgreement|X25519KeyPairGenerator|Ed25519KeyPairGenerator|RSAEngine|OAEPEncoding)\s*\(/g;
|
|
38
|
+
/\bnew\s+(RSAKeyPairGenerator|DSAKeyPairGenerator|ECKeyPairGenerator|ECDSASigner|Ed25519Signer|Ed448Signer|X25519Agreement|X448Agreement|ECDHBasicAgreement|DHBasicAgreement|X25519KeyPairGenerator|X448KeyPairGenerator|Ed25519KeyPairGenerator|Ed448KeyPairGenerator|X448PrivateKeyParameters|RSAEngine|OAEPEncoding)\s*\(/g;
|
|
39
39
|
|
|
40
40
|
// BouncyCastle lightweight-API curve/EdDSA classes as a BARE constructor call —
|
|
41
41
|
// the Kotlin form (`X448KeyPairGenerator()`, no `new` keyword). RE_JAVA_BC above
|
|
@@ -47,13 +47,30 @@ const RE_JAVA_BC =
|
|
|
47
47
|
const RE_JAVA_BC_CURVE =
|
|
48
48
|
/(?<!\bnew\s+)\b(X448KeyPairGenerator|X448Agreement|X448PrivateKeyParameters|X25519KeyPairGenerator|X25519Agreement|Ed448KeyPairGenerator|Ed448Signer|Ed25519KeyPairGenerator|Ed25519Signer)\s*\(/g;
|
|
49
49
|
|
|
50
|
+
// The non-curve BouncyCastle classes (RSA/EC/DSA/DH keygen, signers, engines) as a
|
|
51
|
+
// BARE constructor call — the Kotlin form, `ECKeyPairGenerator()` with no `new`. Java
|
|
52
|
+
// and Scala instantiate Java classes with `new` (caught by RE_JAVA_BC), so bare calls
|
|
53
|
+
// to these distinctive class names are Kotlin. The `(?<!\bnew\s+)` avoids double-
|
|
54
|
+
// counting the `new` form; routed through the same BC_CLASS_RULES map.
|
|
55
|
+
const RE_JAVA_BC_BARE =
|
|
56
|
+
/(?<!\bnew\s+)\b(RSAKeyPairGenerator|DSAKeyPairGenerator|ECKeyPairGenerator|ECDSASigner|ECDHBasicAgreement|DHBasicAgreement|RSAEngine|OAEPEncoding)\s*\(/g;
|
|
57
|
+
|
|
50
58
|
// Insecure JSSE / TLS configuration expressed in Java source. Mirrors the JS
|
|
51
59
|
// tlsDetector in source.ts (category "tls"): a legacy SSL/TLS protocol version
|
|
52
60
|
// requested from SSLContext, and TLS hostname verification neutered via an
|
|
53
61
|
// all-trusting verifier (Apache HttpClient's NoopHostnameVerifier /
|
|
54
62
|
// ALLOW_ALL_HOSTNAME_VERIFIER).
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
// `TLSv1(?:\.1)?` catches both TLS 1.0 and the equally-deprecated TLS 1.1 (RFC 8996),
|
|
64
|
+
// mirroring the JS tlsDetector; the closing `"` keeps `"TLSv1.2"`/`"TLSv1.3"`/`"TLS"`
|
|
65
|
+
// from matching. `SSLv2` is included alongside `SSLv3`.
|
|
66
|
+
const RE_JAVA_TLS_LEGACY =
|
|
67
|
+
/\bSSLContext\s*\.\s*getInstance\s*\(\s*"(SSL|SSLv2|SSLv3|TLSv1(?:\.1)?)"/g;
|
|
68
|
+
// Require a USAGE position, not a bare identifier: `NoopHostnameVerifier` followed by
|
|
69
|
+
// `(` (construction) or `.` (`.INSTANCE`), and `ALLOW_ALL_HOSTNAME_VERIFIER` preceded
|
|
70
|
+
// by `.` (member access). This keeps `new NoopHostnameVerifier()` and
|
|
71
|
+
// `SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER` while dropping the common
|
|
72
|
+
// false positive of a plain `import …NoopHostnameVerifier;` line.
|
|
73
|
+
const RE_JAVA_TLS_NOVERIFY = /\bNoopHostnameVerifier\s*[.(]|\.\s*ALLOW_ALL_HOSTNAME_VERIFIER\b/g;
|
|
57
74
|
|
|
58
75
|
// Identifier-form JWT/JOSE signature algorithms (audit F7). The quoted-string
|
|
59
76
|
// alg token ("RS256") is caught by the language-agnostic jwt-jose detector, but
|
|
@@ -278,6 +295,10 @@ function classifyGetInstance(factory: string, rawAlg: string): RuleMeta | null {
|
|
|
278
295
|
const isSignature = factory === "Signature";
|
|
279
296
|
|
|
280
297
|
if (alg.includes("ECDSA")) return RULE_JAVA_ECDSA_SIGN;
|
|
298
|
+
// ECIES (EC Integrated Encryption Scheme, incl. `ECIESwithAES-CBC`) is classical EC
|
|
299
|
+
// public-key ENCRYPTION — harvest-now-decrypt-later exposed. Checked before the EC
|
|
300
|
+
// branches below (which would otherwise miss it) and the RSA/DSA checks.
|
|
301
|
+
if (alg.includes("ECIES")) return RULE_JAVA_ECDH;
|
|
281
302
|
if (alg.includes("ECDH")) return RULE_JAVA_ECDH;
|
|
282
303
|
if (alg === "EC") return isSignature ? RULE_JAVA_ECDSA_SIGN : RULE_JAVA_EC_KEYGEN;
|
|
283
304
|
if (alg.includes("ED25519") || alg.includes("ED448") || alg.includes("EDDSA"))
|
|
@@ -307,7 +328,10 @@ const BC_CLASS_RULES: Record<string, RuleMeta> = {
|
|
|
307
328
|
ECDHBasicAgreement: RULE_JAVA_ECDH,
|
|
308
329
|
DHBasicAgreement: RULE_JAVA_DH,
|
|
309
330
|
X25519KeyPairGenerator: RULE_JAVA_XDH,
|
|
331
|
+
X448KeyPairGenerator: RULE_JAVA_XDH,
|
|
332
|
+
X448PrivateKeyParameters: RULE_JAVA_XDH,
|
|
310
333
|
Ed25519KeyPairGenerator: RULE_JAVA_EDDSA,
|
|
334
|
+
Ed448KeyPairGenerator: RULE_JAVA_EDDSA,
|
|
311
335
|
RSAEngine: RULE_JAVA_RSA,
|
|
312
336
|
OAEPEncoding: RULE_JAVA_RSA,
|
|
313
337
|
};
|
|
@@ -328,7 +352,8 @@ const BC_CURVE_CLASS_RULES: Record<string, RuleMeta> = {
|
|
|
328
352
|
/** Detects classical asymmetric crypto in Java / Kotlin (JCA + BouncyCastle). */
|
|
329
353
|
export const javaDetector: Detector = {
|
|
330
354
|
id: "java-crypto",
|
|
331
|
-
description:
|
|
355
|
+
description:
|
|
356
|
+
"Classical asymmetric crypto on the JVM — Java/Kotlin/Scala (JCA getInstance + BouncyCastle)",
|
|
332
357
|
scope: "source",
|
|
333
358
|
language: "java",
|
|
334
359
|
rules: [
|
|
@@ -380,6 +405,16 @@ export const javaDetector: Detector = {
|
|
|
380
405
|
);
|
|
381
406
|
});
|
|
382
407
|
|
|
408
|
+
// Bare (Kotlin-form) constructor calls to the non-curve BC classes — same rules
|
|
409
|
+
// as the `new` form, routed via BC_CLASS_RULES.
|
|
410
|
+
eachMatch(RE_JAVA_BC_BARE, content, (m) => {
|
|
411
|
+
const rule = BC_CLASS_RULES[m[1]];
|
|
412
|
+
if (!rule) return;
|
|
413
|
+
findings.push(
|
|
414
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
415
|
+
);
|
|
416
|
+
});
|
|
417
|
+
|
|
383
418
|
// Legacy SSL/TLS protocol version requested from SSLContext.
|
|
384
419
|
eachMatch(RE_JAVA_TLS_LEGACY, content, (m) => {
|
|
385
420
|
findings.push(
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical KEY-MANAGEMENT (JWE) algorithms in JOSE. This is
|
|
3
|
+
* distinct from the JWS signature side (RS256/ES256, handled elsewhere) and from
|
|
4
|
+
* the JWK key-material detector: here we flag the `alg` values that wrap a
|
|
5
|
+
* content-encryption key with classical asymmetric crypto, because the encrypted
|
|
6
|
+
* payload is CONFIDENTIALITY — harvest-now-decrypt-later exposed. A JWE token
|
|
7
|
+
* captured today (from a log, an audit trail, a stored assertion) is decryptable
|
|
8
|
+
* once the wrapping key falls to a CRQC.
|
|
9
|
+
*
|
|
10
|
+
* Covered `alg` (RFC 7518 §4) values:
|
|
11
|
+
* - `RSA-OAEP`, `RSA-OAEP-256`, `RSA1_5` → RSA key encryption.
|
|
12
|
+
* - `ECDH-ES`, `ECDH-ES+A128KW`, … → classical EC Diffie-Hellman key agreement.
|
|
13
|
+
*
|
|
14
|
+
* These exact strings are JOSE identifiers, so a content-level match is precise;
|
|
15
|
+
* the detector applies to any file (config, JSON, source) after a fast reject.
|
|
16
|
+
*/
|
|
17
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
18
|
+
import {
|
|
19
|
+
JWT_HOST_EXTENSIONS,
|
|
20
|
+
DOC_EXTENSIONS,
|
|
21
|
+
eachMatch,
|
|
22
|
+
enclosingObject,
|
|
23
|
+
findingFromRule,
|
|
24
|
+
hasExtension,
|
|
25
|
+
} from "../detect-utils.js";
|
|
26
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
27
|
+
|
|
28
|
+
interface JoseRule {
|
|
29
|
+
re: RegExp;
|
|
30
|
+
meta: RuleMeta;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const JOSE_RULES: JoseRule[] = [
|
|
34
|
+
{
|
|
35
|
+
re: /"alg"\s*:\s*"RSA(?:-OAEP(?:-256)?|1_5)"/g,
|
|
36
|
+
meta: {
|
|
37
|
+
id: "jose-jwe-rsa",
|
|
38
|
+
title: "JWE RSA key wrapping",
|
|
39
|
+
description: 'JWE "alg" of RSA-OAEP / RSA-OAEP-256 / RSA1_5 (RFC 7518)',
|
|
40
|
+
category: "kem",
|
|
41
|
+
severity: "high",
|
|
42
|
+
confidence: "high",
|
|
43
|
+
algorithm: "RSA",
|
|
44
|
+
hndl: true,
|
|
45
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
46
|
+
message:
|
|
47
|
+
"JWE wraps the content-encryption key with classical RSA (RSA-OAEP/RSA1_5); the encrypted payload is harvest-now-decrypt-later exposed.",
|
|
48
|
+
remediation:
|
|
49
|
+
"Plan migration to a post-quantum KEM (ML-KEM-768) for key wrapping as JOSE/COSE PQ algorithms are standardised.",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
re: /"alg"\s*:\s*"ECDH-ES(?:\+A\d{3}KW)?"/g,
|
|
54
|
+
meta: {
|
|
55
|
+
id: "jose-jwe-ecdh",
|
|
56
|
+
title: "JWE ECDH-ES key agreement",
|
|
57
|
+
description: 'JWE "alg" of ECDH-ES / ECDH-ES+A*KW (RFC 7518)',
|
|
58
|
+
category: "key-exchange",
|
|
59
|
+
severity: "high",
|
|
60
|
+
confidence: "high",
|
|
61
|
+
algorithm: "ECDH",
|
|
62
|
+
hndl: true,
|
|
63
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
64
|
+
message:
|
|
65
|
+
"JWE derives the content-encryption key via classical ECDH-ES; the encrypted payload is harvest-now-decrypt-later exposed.",
|
|
66
|
+
remediation:
|
|
67
|
+
"Plan migration to hybrid post-quantum key agreement (X25519MLKEM768) as JOSE PQ algorithms are standardised.",
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/** Detects classical JWE key-management (`alg`) values in JOSE tokens/config. */
|
|
73
|
+
export const joseDetector: Detector = {
|
|
74
|
+
id: "jose-jwe-keymgmt",
|
|
75
|
+
description:
|
|
76
|
+
"Classical JWE key-management algorithms (RSA-OAEP, ECDH-ES) — confidentiality, HNDL",
|
|
77
|
+
scope: "config",
|
|
78
|
+
language: "any",
|
|
79
|
+
rules: JOSE_RULES.map((r) => r.meta),
|
|
80
|
+
// Prose examples (a README showing `"alg":"RSA-OAEP"`) are not JOSE config. In the
|
|
81
|
+
// JS/TS/Python/Go/Ruby source files that source.ts's `jose-rsa-oaep`/`jose-ecdh-es`
|
|
82
|
+
// token rules actually cover (JWT_HOST_EXTENSIONS), defer to them to avoid a
|
|
83
|
+
// double-count — but STILL run on other source languages (Java/C#/Rust/PHP/Elixir/
|
|
84
|
+
// C/C++), which source.ts does not cover for these tokens.
|
|
85
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS) && !hasExtension(f, JWT_HOST_EXTENSIONS),
|
|
86
|
+
detect({ file, content }): Finding[] {
|
|
87
|
+
// Fast reject: no JOSE key-management alg token present.
|
|
88
|
+
if (
|
|
89
|
+
!content.includes("RSA-OAEP") &&
|
|
90
|
+
!content.includes("RSA1_5") &&
|
|
91
|
+
!content.includes("ECDH-ES")
|
|
92
|
+
) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const findings: Finding[] = [];
|
|
96
|
+
for (const rule of JOSE_RULES) {
|
|
97
|
+
eachMatch(rule.re, content, (m) => {
|
|
98
|
+
// If this `alg` belongs to a JWK (a `"kty"` sits in the SAME object), the jwk
|
|
99
|
+
// detector already reports the key — defer so it isn't counted twice. Scoped
|
|
100
|
+
// to the enclosing object so an unrelated nearby JWK doesn't suppress a real
|
|
101
|
+
// standalone JWE header.
|
|
102
|
+
if (enclosingObject(content, m.index).includes('"kty"')) return;
|
|
103
|
+
findings.push(
|
|
104
|
+
findingFromRule(rule.meta, { file, content, index: m.index, matchLength: m[0].length }),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return findings;
|
|
109
|
+
},
|
|
110
|
+
};
|