@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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical asymmetric keys held in / generated by a PKCS#11 HSM
|
|
3
|
+
* or token. HSMs hold an organization's longest-lived roots (CA keys, code-signing
|
|
4
|
+
* keys), so they are exactly the keys a PQC migration must find first — yet they
|
|
5
|
+
* live behind a PKCS#11 layer the source packs never see.
|
|
6
|
+
*
|
|
7
|
+
* Covered, distinctive lexical surfaces:
|
|
8
|
+
* - `pkcs11-tool --keypairgen --key-type rsa:2048 | EC:secp256r1` — the standard
|
|
9
|
+
* OpenSC keygen invocation recorded in ops scripts / runbooks.
|
|
10
|
+
* - `CKM_RSA_PKCS(_KEY_PAIR_GEN|_OAEP|_PSS)` / `CKM_EC(DSA|DH)` / `CKM_DSA` — the
|
|
11
|
+
* PKCS#11 mechanism constants (SoftHSM, SunPKCS11, pkcs11js, python-pkcs11), which
|
|
12
|
+
* name the exact algorithm.
|
|
13
|
+
*/
|
|
14
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
15
|
+
import { DOC_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
|
|
16
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
17
|
+
|
|
18
|
+
// `pkcs11-tool --keypairgen --key-type rsa:2048` / `EC:secp256r1` (OpenSC).
|
|
19
|
+
const RE_PKCS11_TOOL_RSA = /--key-type\s+rsa:\d+/gi;
|
|
20
|
+
const RE_PKCS11_TOOL_EC = /--key-type\s+EC:[A-Za-z0-9]+/gi;
|
|
21
|
+
// PKCS#11 mechanism constants (CKM_*), which name the algorithm exactly.
|
|
22
|
+
const RE_CKM_RSA = /\bCKM_RSA_(?:PKCS(?:_KEY_PAIR_GEN|_OAEP|_PSS)?|X_509|9796)\b/g;
|
|
23
|
+
const RE_CKM_EC = /\bCKM_EC(?:DSA(?:_SHA\d+)?|DH1?_DERIVE|_KEY_PAIR_GEN)\b/g;
|
|
24
|
+
const RE_CKM_DSA = /\bCKM_DSA(?:_SHA\d+|_KEY_PAIR_GEN)?\b/g;
|
|
25
|
+
const RE_CKM_DH = /\bCKM_DH_PKCS_(?:KEY_PAIR_GEN|DERIVE)\b/g;
|
|
26
|
+
|
|
27
|
+
const RULE_PKCS11_RSA: RuleMeta = {
|
|
28
|
+
id: "pkcs11-rsa",
|
|
29
|
+
title: "PKCS#11 (HSM) RSA key",
|
|
30
|
+
description: "PKCS#11 RSA key generation / mechanism (pkcs11-tool, CKM_RSA_*)",
|
|
31
|
+
category: "kem",
|
|
32
|
+
severity: "high",
|
|
33
|
+
confidence: "high",
|
|
34
|
+
algorithm: "RSA",
|
|
35
|
+
hndl: true,
|
|
36
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
37
|
+
message:
|
|
38
|
+
"A classical RSA key in a PKCS#11 HSM / token — likely a long-lived root (CA / code-signing), and not quantum-safe.",
|
|
39
|
+
remediation:
|
|
40
|
+
"Inventory HSM-held RSA keys first (longest-lived); plan re-keying to PQC (ML-KEM-768 / ML-DSA-65) as HSM firmware adds support.",
|
|
41
|
+
};
|
|
42
|
+
const RULE_PKCS11_EC: RuleMeta = {
|
|
43
|
+
id: "pkcs11-ec",
|
|
44
|
+
title: "PKCS#11 (HSM) EC key",
|
|
45
|
+
description: "PKCS#11 EC key generation / mechanism (CKM_EC*, --key-type EC)",
|
|
46
|
+
category: "key-exchange",
|
|
47
|
+
severity: "high",
|
|
48
|
+
confidence: "high",
|
|
49
|
+
algorithm: "ECDH",
|
|
50
|
+
hndl: true,
|
|
51
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
52
|
+
message:
|
|
53
|
+
"A classical EC key in a PKCS#11 HSM / token; EC keys feed ECDSA signatures and ECDH key agreement (the ECDH path is harvest-now-decrypt-later exposed).",
|
|
54
|
+
remediation:
|
|
55
|
+
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204). Prioritise HSM-held keys.",
|
|
56
|
+
};
|
|
57
|
+
const RULE_PKCS11_DSA: RuleMeta = {
|
|
58
|
+
id: "pkcs11-dsa",
|
|
59
|
+
title: "PKCS#11 (HSM) DSA key",
|
|
60
|
+
description: "PKCS#11 DSA key generation / mechanism (CKM_DSA*)",
|
|
61
|
+
category: "signature",
|
|
62
|
+
severity: "medium",
|
|
63
|
+
confidence: "high",
|
|
64
|
+
algorithm: "DSA",
|
|
65
|
+
hndl: false,
|
|
66
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
67
|
+
message: "A classical DSA key in a PKCS#11 HSM / token — deprecated and quantum-forgeable.",
|
|
68
|
+
remediation: "Rotate off DSA; migrate to ML-DSA-65 (FIPS 204).",
|
|
69
|
+
};
|
|
70
|
+
const RULE_PKCS11_DH: RuleMeta = {
|
|
71
|
+
id: "pkcs11-dh",
|
|
72
|
+
title: "PKCS#11 (HSM) Diffie-Hellman",
|
|
73
|
+
description: "PKCS#11 finite-field DH key generation / derive (CKM_DH_PKCS_*)",
|
|
74
|
+
category: "key-exchange",
|
|
75
|
+
severity: "medium",
|
|
76
|
+
confidence: "high",
|
|
77
|
+
algorithm: "DH",
|
|
78
|
+
hndl: true,
|
|
79
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
80
|
+
message:
|
|
81
|
+
"Finite-field Diffie-Hellman in a PKCS#11 HSM / token; classical key agreement, harvest-now-decrypt-later exposed.",
|
|
82
|
+
remediation: "Migrate key agreement to hybrid X25519MLKEM768 (ML-KEM-768).",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Detects classical asymmetric keys behind a PKCS#11 HSM / token. */
|
|
86
|
+
export const pkcs11Detector: Detector = {
|
|
87
|
+
id: "pkcs11-crypto",
|
|
88
|
+
description: "Classical asymmetric keys in a PKCS#11 HSM / token",
|
|
89
|
+
scope: "config",
|
|
90
|
+
language: "any",
|
|
91
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
92
|
+
rules: [RULE_PKCS11_RSA, RULE_PKCS11_EC, RULE_PKCS11_DSA, RULE_PKCS11_DH],
|
|
93
|
+
detect({ file, content }): Finding[] {
|
|
94
|
+
// Fast reject: only proceed on a distinctive PKCS#11 marker.
|
|
95
|
+
if (!content.includes("CKM_") && !content.includes("--key-type")) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const findings: Finding[] = [];
|
|
99
|
+
const add = (re: RegExp, rule: RuleMeta): void =>
|
|
100
|
+
eachMatch(re, content, (m) =>
|
|
101
|
+
findings.push(
|
|
102
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
add(RE_PKCS11_TOOL_RSA, RULE_PKCS11_RSA);
|
|
106
|
+
add(RE_PKCS11_TOOL_EC, RULE_PKCS11_EC);
|
|
107
|
+
add(RE_CKM_RSA, RULE_PKCS11_RSA);
|
|
108
|
+
add(RE_CKM_EC, RULE_PKCS11_EC);
|
|
109
|
+
add(RE_CKM_DSA, RULE_PKCS11_DSA);
|
|
110
|
+
add(RE_CKM_DH, RULE_PKCS11_DH);
|
|
111
|
+
return findings;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical-certificate TLS termination in standalone
|
|
3
|
+
* reverse-proxy / load-balancer configuration and in-code gRPC channel
|
|
4
|
+
* credentials.
|
|
5
|
+
*
|
|
6
|
+
* A standalone Envoy, Nginx, HAProxy, Traefik, or Caddy config that terminates
|
|
7
|
+
* TLS with an RSA/ECDSA leaf certificate and negotiates an ECDHE key exchange is
|
|
8
|
+
* the canonical harvest-now-decrypt-later (HNDL) surface: an attacker who
|
|
9
|
+
* records today's handshake + ciphertext can decrypt it retroactively once a
|
|
10
|
+
* cryptographically-relevant quantum computer (CRQC) can break the ephemeral
|
|
11
|
+
* (EC)DHE secret. The Kubernetes / service-mesh detectors (`k8s.ts`, `mesh.ts`)
|
|
12
|
+
* cover the CLUSTER-managed cert + mesh-mTLS surfaces; the STANDALONE proxy
|
|
13
|
+
* config FILES and the in-code gRPC credential constructors below are seen by
|
|
14
|
+
* neither them nor the language packs, so they are covered here.
|
|
15
|
+
*
|
|
16
|
+
* Because the config token itself does not name the cert's key algorithm (the
|
|
17
|
+
* key lives in a referenced PEM file, out of band), these findings use
|
|
18
|
+
* `algorithm: "unknown"` — the evidence is "a classical-cert TLS channel is
|
|
19
|
+
* configured HERE", not a specific family. They are `category: "key-exchange"`
|
|
20
|
+
* (the ECDHE handshake is the harvestable primitive) and `hndl: true`.
|
|
21
|
+
* Confidence is `medium`: a config-level indicator of a classical TLS channel,
|
|
22
|
+
* not a matched algorithm string.
|
|
23
|
+
*
|
|
24
|
+
* PRECISION / fast-reject: several of the matched tokens (`ssl_certificate`,
|
|
25
|
+
* `private_key:`, `certificates:`) are generic enough to appear in unrelated
|
|
26
|
+
* YAML/config, so `detect()` bails unless the file carries a distinctive
|
|
27
|
+
* proxy/gRPC marker (see `hasProxyMarker`). This keeps a random YAML that merely
|
|
28
|
+
* has a `private_key:` key, or a Traefik-shaped `certificates:` list that isn't
|
|
29
|
+
* a Traefik config, from firing. Comments are masked (YAML/nginx/HCL `#`, plus
|
|
30
|
+
* `//` and C-style `/* … *\/` blocks) so a commented-out directive can't fire.
|
|
31
|
+
*
|
|
32
|
+
* Covered surfaces:
|
|
33
|
+
* - Envoy — `common_tls_context:` / `DownstreamTlsContext` /
|
|
34
|
+
* `UpstreamTlsContext` with `tls_certificates:` carrying a
|
|
35
|
+
* `certificate_chain:` + `private_key:` (and the
|
|
36
|
+
* `envoy.transport_sockets.tls` transport socket).
|
|
37
|
+
* - Nginx — `ssl_certificate` + `ssl_certificate_key` directives.
|
|
38
|
+
* - HAProxy — `bind … ssl crt …` lines and `crt-store` sections.
|
|
39
|
+
* - Traefik — a `tls:` block's `certificates:` with `certFile:` + `keyFile:`
|
|
40
|
+
* (dynamic config) or a `certResolver` (ACME) reference.
|
|
41
|
+
* - gRPC — in-code TLS channel credentials: `grpc.ssl_channel_credentials(`
|
|
42
|
+
* (Python), `grpc.credentials.createSsl(` (Node),
|
|
43
|
+
* `TlsChannelCredentials` (Java), `credentials.NewTLS(` (Go).
|
|
44
|
+
*/
|
|
45
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
46
|
+
import {
|
|
47
|
+
DOC_EXTENSIONS,
|
|
48
|
+
eachMatch,
|
|
49
|
+
findingFromRule,
|
|
50
|
+
hasExtension,
|
|
51
|
+
maskBlockComments,
|
|
52
|
+
maskCommentLines,
|
|
53
|
+
} from "../detect-utils.js";
|
|
54
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
55
|
+
|
|
56
|
+
// --- Envoy: match ONLY the Envoy-DISTINCTIVE tokens (`common_tls_context:`, the
|
|
57
|
+
// `*TlsContext` type names, `tls_certificates:`, the `envoy.transport_sockets.tls`
|
|
58
|
+
// transport socket). The generic `certificate_chain:` / `private_key:` fields are
|
|
59
|
+
// deliberately NOT matched here — they appear in unrelated YAML and would
|
|
60
|
+
// cross-label a non-Envoy file as Envoy (audit L2). A distinctive token IS the
|
|
61
|
+
// evidence, so no separate marker gate is needed for this rule (audit L1).
|
|
62
|
+
const RE_ENVOY_TLS =
|
|
63
|
+
/\b(?:DownstreamTlsContext|UpstreamTlsContext|common_tls_context\s*:|tls_certificates\s*:|envoy\.transport_sockets\.tls)/g;
|
|
64
|
+
|
|
65
|
+
// --- Nginx: the classic `ssl_certificate` / `ssl_certificate_key` directive
|
|
66
|
+
// pair. `ssl_certificate_key` is matched first-class; `ssl_certificate` requires
|
|
67
|
+
// a following space so it doesn't also swallow `ssl_certificate_key` as a second
|
|
68
|
+
// hit on the same directive (kept as a single rule regardless).
|
|
69
|
+
const RE_NGINX_TLS = /\bssl_certificate(?:_key)?\s+\S/g;
|
|
70
|
+
|
|
71
|
+
// --- HAProxy: a `bind` line that terminates TLS (`ssl crt <path>`) or a
|
|
72
|
+
// `crt-store` section (HAProxy 2.8+ certificate store). `ssl` and `crt` may be
|
|
73
|
+
// separated by other bind args, so match them independently on a bind context.
|
|
74
|
+
const RE_HAPROXY_TLS =
|
|
75
|
+
/\bcrt-store\b|\bbind\b[^\n]*\bssl\b[^\n]*\bcrt\b|\bbind\b[^\n]*\bcrt\b[^\n]*\bssl\b/g;
|
|
76
|
+
|
|
77
|
+
// --- Traefik: dynamic-config TLS certificate entries (`certFile:` + `keyFile:`)
|
|
78
|
+
// or an ACME `certResolver` reference. `certFile`/`keyFile` are Traefik-specific
|
|
79
|
+
// enough (paired with the `certResolver`/`tls:` marker gate) to anchor on.
|
|
80
|
+
const RE_TRAEFIK_TLS = /\bcertResolver\b|\b(?:certFile|keyFile)\s*:/g;
|
|
81
|
+
|
|
82
|
+
// --- gRPC in-code TLS channel credentials across languages.
|
|
83
|
+
const RE_GRPC_TLS =
|
|
84
|
+
/grpc\.ssl_channel_credentials\s*\(|grpc\.credentials\.createSsl\s*\(|\bTlsChannelCredentials\b|\bcredentials\.NewTLS\s*\(/g;
|
|
85
|
+
|
|
86
|
+
const MESSAGE = (proxy: string): string =>
|
|
87
|
+
`${proxy} terminates TLS with a classical certificate + ECDHE key exchange; the recorded handshake is harvest-now-decrypt-later exposed — verify the cert key algorithm and plan a PQC-hybrid (X25519MLKEM768) TLS migration.`;
|
|
88
|
+
|
|
89
|
+
const REMEDIATION =
|
|
90
|
+
"Enable hybrid PQC key exchange (X25519MLKEM768) once the proxy/gRPC stack supports it; re-key certificates to PQC signatures (ML-DSA) when available.";
|
|
91
|
+
|
|
92
|
+
const RULE_ENVOY: RuleMeta = {
|
|
93
|
+
id: "proxy-envoy-tls",
|
|
94
|
+
title: "Envoy classical TLS termination",
|
|
95
|
+
description: "Envoy TLS context terminates TLS with a classical certificate + ECDHE key exchange",
|
|
96
|
+
category: "key-exchange",
|
|
97
|
+
severity: "medium",
|
|
98
|
+
confidence: "medium",
|
|
99
|
+
algorithm: "unknown",
|
|
100
|
+
hndl: true,
|
|
101
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
102
|
+
message: MESSAGE("Envoy"),
|
|
103
|
+
remediation: REMEDIATION,
|
|
104
|
+
};
|
|
105
|
+
const RULE_NGINX: RuleMeta = {
|
|
106
|
+
id: "proxy-nginx-tls",
|
|
107
|
+
title: "Nginx classical TLS termination",
|
|
108
|
+
description:
|
|
109
|
+
"Nginx ssl_certificate/ssl_certificate_key terminates TLS with a classical cert + ECDHE",
|
|
110
|
+
category: "key-exchange",
|
|
111
|
+
severity: "medium",
|
|
112
|
+
confidence: "medium",
|
|
113
|
+
algorithm: "unknown",
|
|
114
|
+
hndl: true,
|
|
115
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
116
|
+
message: MESSAGE("Nginx"),
|
|
117
|
+
remediation: REMEDIATION,
|
|
118
|
+
};
|
|
119
|
+
const RULE_HAPROXY: RuleMeta = {
|
|
120
|
+
id: "proxy-haproxy-tls",
|
|
121
|
+
title: "HAProxy classical TLS termination",
|
|
122
|
+
description: "HAProxy bind ssl crt / crt-store terminates TLS with a classical cert + ECDHE",
|
|
123
|
+
category: "key-exchange",
|
|
124
|
+
severity: "medium",
|
|
125
|
+
confidence: "medium",
|
|
126
|
+
algorithm: "unknown",
|
|
127
|
+
hndl: true,
|
|
128
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
129
|
+
message: MESSAGE("HAProxy"),
|
|
130
|
+
remediation: REMEDIATION,
|
|
131
|
+
};
|
|
132
|
+
const RULE_TRAEFIK: RuleMeta = {
|
|
133
|
+
id: "proxy-traefik-tls",
|
|
134
|
+
title: "Traefik classical TLS termination",
|
|
135
|
+
description:
|
|
136
|
+
"Traefik tls certificates (certFile/keyFile) or certResolver terminate TLS with a classical cert + ECDHE",
|
|
137
|
+
category: "key-exchange",
|
|
138
|
+
severity: "medium",
|
|
139
|
+
confidence: "medium",
|
|
140
|
+
algorithm: "unknown",
|
|
141
|
+
hndl: true,
|
|
142
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
143
|
+
message: MESSAGE("Traefik"),
|
|
144
|
+
remediation: REMEDIATION,
|
|
145
|
+
};
|
|
146
|
+
const RULE_GRPC: RuleMeta = {
|
|
147
|
+
id: "grpc-tls-credentials",
|
|
148
|
+
title: "gRPC classical TLS channel credentials",
|
|
149
|
+
description: "gRPC channel established with classical TLS credentials (RSA/ECDSA cert + ECDHE)",
|
|
150
|
+
category: "key-exchange",
|
|
151
|
+
severity: "medium",
|
|
152
|
+
confidence: "medium",
|
|
153
|
+
algorithm: "unknown",
|
|
154
|
+
hndl: true,
|
|
155
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
156
|
+
message: MESSAGE("gRPC"),
|
|
157
|
+
remediation: REMEDIATION,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* PERF fast-reject only — a superset of substrings that appear in every rule's
|
|
162
|
+
* matchable input, so it can never drop a file a rule would flag. Correctness is
|
|
163
|
+
* in the per-rule regexes (each is distinctive enough to self-gate); this list
|
|
164
|
+
* exists purely to skip the regex work on files with no proxy/gRPC token at all.
|
|
165
|
+
*/
|
|
166
|
+
const PROXY_FAST_REJECT: readonly string[] = [
|
|
167
|
+
// Envoy
|
|
168
|
+
"tls_context",
|
|
169
|
+
"TlsContext",
|
|
170
|
+
"tls_certificates",
|
|
171
|
+
"transport_sockets.tls",
|
|
172
|
+
// Nginx
|
|
173
|
+
"ssl_certificate",
|
|
174
|
+
// HAProxy (`crt-store` and `bind … ssl … crt` both contain "crt")
|
|
175
|
+
"crt",
|
|
176
|
+
// Traefik
|
|
177
|
+
"certResolver",
|
|
178
|
+
"certFile",
|
|
179
|
+
"keyFile",
|
|
180
|
+
// gRPC
|
|
181
|
+
"ssl_channel_credentials",
|
|
182
|
+
"createSsl",
|
|
183
|
+
"TlsChannelCredentials",
|
|
184
|
+
"credentials.NewTLS",
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
interface ProxyRule {
|
|
188
|
+
meta: RuleMeta;
|
|
189
|
+
re: RegExp;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const PROXY_RULES: readonly ProxyRule[] = [
|
|
193
|
+
{ meta: RULE_ENVOY, re: RE_ENVOY_TLS },
|
|
194
|
+
{ meta: RULE_NGINX, re: RE_NGINX_TLS },
|
|
195
|
+
{ meta: RULE_HAPROXY, re: RE_HAPROXY_TLS },
|
|
196
|
+
{ meta: RULE_TRAEFIK, re: RE_TRAEFIK_TLS },
|
|
197
|
+
{ meta: RULE_GRPC, re: RE_GRPC_TLS },
|
|
198
|
+
];
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Detects classical-cert TLS termination in standalone reverse-proxy /
|
|
202
|
+
* load-balancer config files and in-code gRPC channel credentials. See the
|
|
203
|
+
* module docstring for the covered surfaces and the fast-reject rationale.
|
|
204
|
+
*/
|
|
205
|
+
export const proxyDetector: Detector = {
|
|
206
|
+
id: "proxy-tls-crypto",
|
|
207
|
+
description:
|
|
208
|
+
"Classical-cert TLS termination in reverse-proxy / load-balancer config (Envoy, Nginx, HAProxy, Traefik) and gRPC channel credentials",
|
|
209
|
+
scope: "config",
|
|
210
|
+
language: "any",
|
|
211
|
+
rules: PROXY_RULES.map((r) => r.meta),
|
|
212
|
+
// Config + source files are all in scope (gRPC creds live in code); only prose
|
|
213
|
+
// documentation is excluded, so a README mentioning `ssl_certificate` in a
|
|
214
|
+
// sentence can't fire (the marker gate already handles most of this).
|
|
215
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
216
|
+
detect({ file, content }): Finding[] {
|
|
217
|
+
// Mask comments first so a directive that only appears inside a commented-out
|
|
218
|
+
// block cannot fire.
|
|
219
|
+
const scan = maskCommentLines(maskBlockComments(content), ["#", "//"]);
|
|
220
|
+
// Cheap fast-reject: skip files with none of the distinctive tokens. This is a
|
|
221
|
+
// perf gate only — each rule's regex is distinctive enough to SELF-GATE (a
|
|
222
|
+
// separate too-narrow marker list previously dropped canonical HAProxy/Traefik
|
|
223
|
+
// configs entirely — audit H1/H2), so correctness lives in the per-rule regexes.
|
|
224
|
+
if (!PROXY_FAST_REJECT.some((mk) => scan.includes(mk))) return [];
|
|
225
|
+
|
|
226
|
+
const findings: Finding[] = [];
|
|
227
|
+
for (const { meta, re } of PROXY_RULES) {
|
|
228
|
+
eachMatch(re, scan, (m) =>
|
|
229
|
+
findings.push(
|
|
230
|
+
findingFromRule(meta, { file, content, index: m.index, matchLength: m[0].length }),
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
return findings;
|
|
235
|
+
},
|
|
236
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IaC detector: classical asymmetric keys provisioned by Pulumi's `tls` provider
|
|
3
|
+
* (`@pulumi/tls` / `pulumi_tls` / `pulumi-tls`), across the languages Pulumi programs
|
|
4
|
+
* are written in (TS/JS, Python, Go). A `tls.PrivateKey` resource mints real key
|
|
5
|
+
* material at deploy time — the IaC surface, like Terraform's `tls_private_key`, but
|
|
6
|
+
* expressed as SDK calls in a general-purpose source file rather than HCL.
|
|
7
|
+
*
|
|
8
|
+
* Detection is gated to files that actually use the pulumi-tls provider (a
|
|
9
|
+
* `@pulumi/tls` / `pulumi_tls` / `pulumi-tls` import or a `tls.PrivateKey` /
|
|
10
|
+
* `tls.NewPrivateKey` construction), then classifies each `algorithm` value:
|
|
11
|
+
* - `"RSA"` → RSA key (kem, HNDL)
|
|
12
|
+
* - `"ECDSA"` → EC signing key (signature)
|
|
13
|
+
* - `"ED25519"` → Ed25519 signing key (signature)
|
|
14
|
+
* Comment/string suppression is handled centrally for the host languages.
|
|
15
|
+
*/
|
|
16
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
17
|
+
import { eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
|
|
18
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
19
|
+
|
|
20
|
+
const PULUMI_EXTENSIONS: readonly string[] = [".ts", ".js", ".mjs", ".cjs", ".py", ".go"];
|
|
21
|
+
|
|
22
|
+
// The `algorithm` value of a pulumi-tls PrivateKey, in the TS/Python (`algorithm:
|
|
23
|
+
// "RSA"` / `algorithm="RSA"`) and Go (`Algorithm: pulumi.String("RSA")`) spellings.
|
|
24
|
+
const RE_PULUMI_TLS_ALG =
|
|
25
|
+
/\b[Aa]lgorithm\s*[:=]\s*(?:pulumi\.String\(\s*)?["'](RSA|ECDSA|ED25519)["']/g;
|
|
26
|
+
|
|
27
|
+
/** True when the file actually uses the pulumi-tls provider (gate against FPs). */
|
|
28
|
+
function usesPulumiTls(content: string): boolean {
|
|
29
|
+
return (
|
|
30
|
+
content.includes("@pulumi/tls") ||
|
|
31
|
+
content.includes("pulumi_tls") ||
|
|
32
|
+
content.includes("pulumi-tls") ||
|
|
33
|
+
content.includes("tls.PrivateKey") ||
|
|
34
|
+
content.includes("tls.NewPrivateKey")
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const RULE_PULUMI_RSA: RuleMeta = {
|
|
39
|
+
id: "pulumi-tls-rsa",
|
|
40
|
+
title: "Pulumi tls.PrivateKey RSA key",
|
|
41
|
+
description: 'Pulumi tls.PrivateKey with algorithm "RSA"',
|
|
42
|
+
category: "kem",
|
|
43
|
+
severity: "high",
|
|
44
|
+
confidence: "high",
|
|
45
|
+
algorithm: "RSA",
|
|
46
|
+
hndl: true,
|
|
47
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
48
|
+
message: "Pulumi provisions a classical RSA key (tls.PrivateKey), which is not quantum-safe.",
|
|
49
|
+
remediation: "Plan migration to PQC (ML-KEM-768 for encryption, ML-DSA-65 for signatures).",
|
|
50
|
+
};
|
|
51
|
+
const RULE_PULUMI_ECDSA: RuleMeta = {
|
|
52
|
+
id: "pulumi-tls-ecdsa",
|
|
53
|
+
title: "Pulumi tls.PrivateKey ECDSA key",
|
|
54
|
+
description: 'Pulumi tls.PrivateKey with algorithm "ECDSA"',
|
|
55
|
+
category: "signature",
|
|
56
|
+
severity: "high",
|
|
57
|
+
confidence: "high",
|
|
58
|
+
algorithm: "ECDSA",
|
|
59
|
+
hndl: false,
|
|
60
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
61
|
+
message:
|
|
62
|
+
"Pulumi provisions a classical ECDSA key (tls.PrivateKey), forgeable by a quantum attacker.",
|
|
63
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
|
|
64
|
+
};
|
|
65
|
+
const RULE_PULUMI_ED25519: RuleMeta = {
|
|
66
|
+
id: "pulumi-tls-ed25519",
|
|
67
|
+
title: "Pulumi tls.PrivateKey Ed25519 key",
|
|
68
|
+
description: 'Pulumi tls.PrivateKey with algorithm "ED25519"',
|
|
69
|
+
category: "signature",
|
|
70
|
+
severity: "medium",
|
|
71
|
+
confidence: "high",
|
|
72
|
+
algorithm: "EdDSA",
|
|
73
|
+
hndl: false,
|
|
74
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
75
|
+
message:
|
|
76
|
+
"Pulumi provisions a classical Ed25519 key (tls.PrivateKey), forgeable by a quantum attacker.",
|
|
77
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const RULE_BY_ALG: Record<string, RuleMeta> = {
|
|
81
|
+
RSA: RULE_PULUMI_RSA,
|
|
82
|
+
ECDSA: RULE_PULUMI_ECDSA,
|
|
83
|
+
ED25519: RULE_PULUMI_ED25519,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** Detects classical asymmetric keys provisioned by Pulumi's tls provider. */
|
|
87
|
+
export const pulumiDetector: Detector = {
|
|
88
|
+
id: "pulumi-crypto",
|
|
89
|
+
description: "Classical asymmetric keys provisioned by Pulumi's tls provider (IaC)",
|
|
90
|
+
scope: "config",
|
|
91
|
+
language: "any",
|
|
92
|
+
rules: [RULE_PULUMI_RSA, RULE_PULUMI_ECDSA, RULE_PULUMI_ED25519],
|
|
93
|
+
appliesTo: (f) => hasExtension(f, PULUMI_EXTENSIONS),
|
|
94
|
+
detect({ file, content }): Finding[] {
|
|
95
|
+
if (!usesPulumiTls(content)) return [];
|
|
96
|
+
const findings: Finding[] = [];
|
|
97
|
+
eachMatch(RE_PULUMI_TLS_ALG, content, (m) => {
|
|
98
|
+
const rule = RULE_BY_ALG[m[1]];
|
|
99
|
+
if (!rule) return;
|
|
100
|
+
findings.push(
|
|
101
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
return findings;
|
|
105
|
+
},
|
|
106
|
+
};
|
package/src/detectors/python.ts
CHANGED
|
@@ -241,6 +241,63 @@ const RULE_PY_TLS_LEGACY: RuleMeta = {
|
|
|
241
241
|
"Use ssl.PROTOCOL_TLS_CLIENT with minimum_version = ssl.TLSVersion.TLSv1_3 and prefer PQC-hybrid key exchange.",
|
|
242
242
|
};
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Aliasable `cryptography` / PyCryptodome modules → the (method, rule) pairs
|
|
246
|
+
* reachable through them. Lets an aliased module import
|
|
247
|
+
* (`from ... import rsa as _rsa` → `_rsa.generate_private_key(`) resolve back to
|
|
248
|
+
* the same rule the direct `rsa.generate_private_key(` would fire, since the
|
|
249
|
+
* detector's regexes are module-qualified and miss a renamed prefix.
|
|
250
|
+
*/
|
|
251
|
+
const PY_MODULE_RULES: Record<string, ReadonlyArray<{ method: string; rule: RuleMeta }>> = {
|
|
252
|
+
rsa: [{ method: "generate_private_key", rule: RULE_PY_RSA_KEYGEN }],
|
|
253
|
+
ec: [
|
|
254
|
+
{ method: "generate_private_key", rule: RULE_PY_EC_KEYGEN },
|
|
255
|
+
{ method: "ECDSA", rule: RULE_PY_ECDSA },
|
|
256
|
+
{ method: "ECDH", rule: RULE_PY_ECDH },
|
|
257
|
+
],
|
|
258
|
+
dsa: [{ method: "generate_private_key", rule: RULE_PY_HAZMAT_DSA }],
|
|
259
|
+
dh: [{ method: "generate_parameters", rule: RULE_PY_DH }],
|
|
260
|
+
padding: [{ method: "OAEP", rule: RULE_PY_RSA_ENCRYPT }],
|
|
261
|
+
// PyCryptodome factory modules (`.generate(`).
|
|
262
|
+
RSA: [{ method: "generate", rule: RULE_PY_RSA_KEYGEN }],
|
|
263
|
+
ECC: [{ method: "generate", rule: RULE_PY_EC_KEYGEN }],
|
|
264
|
+
DSA: [{ method: "generate", rule: RULE_PY_DSA }],
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/** Escape a string for interpolation into a dynamically-built RegExp. */
|
|
268
|
+
function escapePyRe(s: string): string {
|
|
269
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Collect module aliases for the aliasable crypto modules, so a later
|
|
274
|
+
* `<alias>.<method>(` call still resolves. Handles both `from <path> import
|
|
275
|
+
* <mod> as <alias>` (including comma-separated specifiers) and the bare
|
|
276
|
+
* `import <path.mod> as <alias>`. Returns Map<module, alias[]>; an alias equal to
|
|
277
|
+
* its own module name is skipped (matched directly already).
|
|
278
|
+
*/
|
|
279
|
+
function collectPyModuleAliases(content: string): Map<string, string[]> {
|
|
280
|
+
const out = new Map<string, string[]>();
|
|
281
|
+
const add = (mod: string, alias: string): void => {
|
|
282
|
+
if (!alias || alias === mod || !(mod in PY_MODULE_RULES)) return;
|
|
283
|
+
const list = out.get(mod) ?? [];
|
|
284
|
+
if (!list.includes(alias)) list.push(alias);
|
|
285
|
+
out.set(mod, list);
|
|
286
|
+
};
|
|
287
|
+
// `from <path> import a as b, c as d` — scan the import list for `X as Y`.
|
|
288
|
+
const fromRe = /(?:^|\n)[ \t]*from\s+[\w.]+\s+import\s+([^\n#]+)/g;
|
|
289
|
+
for (let m = fromRe.exec(content); m; m = fromRe.exec(content)) {
|
|
290
|
+
const specRe = /([A-Za-z_]\w*)\s+as\s+([A-Za-z_]\w*)/g;
|
|
291
|
+
for (let s = specRe.exec(m[1]); s; s = specRe.exec(m[1])) add(s[1], s[2]);
|
|
292
|
+
}
|
|
293
|
+
// `import <path.mod> as <alias>` — the aliased module is the last dotted segment.
|
|
294
|
+
const impRe = /(?:^|\n)[ \t]*import\s+([\w.]+)\s+as\s+([A-Za-z_]\w*)/g;
|
|
295
|
+
for (let m = impRe.exec(content); m; m = impRe.exec(content)) {
|
|
296
|
+
add(m[1].split(".").pop() ?? m[1], m[2]);
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
|
|
244
301
|
/** Detects classical asymmetric crypto in Python source. */
|
|
245
302
|
export const pythonDetector: Detector = {
|
|
246
303
|
id: "python-crypto",
|
|
@@ -286,6 +343,20 @@ export const pythonDetector: Detector = {
|
|
|
286
343
|
add(RE_PY_TLS_REJECT, RULE_PY_TLS_REJECT);
|
|
287
344
|
add(RE_PY_TLS_LEGACY, RULE_PY_TLS_LEGACY);
|
|
288
345
|
|
|
346
|
+
// Module-alias resolution: `from ... import rsa as _rsa` then
|
|
347
|
+
// `_rsa.generate_private_key(` — the direct regexes are module-qualified
|
|
348
|
+
// (`\brsa\.`) and miss a renamed prefix. Runs on the ORIGINAL content so
|
|
349
|
+
// locations stay exact; fires only for an alias explicitly bound to a known
|
|
350
|
+
// crypto module, so precision is unaffected.
|
|
351
|
+
for (const [mod, aliasList] of collectPyModuleAliases(content)) {
|
|
352
|
+
for (const alias of aliasList) {
|
|
353
|
+
const a = escapePyRe(alias);
|
|
354
|
+
for (const { method, rule } of PY_MODULE_RULES[mod]) {
|
|
355
|
+
add(new RegExp(`\\b${a}\\.${method}\\s*\\(`, "g"), rule);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
289
360
|
return findings;
|
|
290
361
|
},
|
|
291
362
|
};
|
package/src/detectors/rust.ts
CHANGED
|
@@ -27,6 +27,10 @@ const RE_RUST_ECDH = /\becdh::EphemeralSecret\b|\bagreement::ECDH_P(?:256|384)\b
|
|
|
27
27
|
const RE_RUST_ED25519 = /\bed25519_dalek::(?:SigningKey|Keypair|SecretKey)\b|\bEd25519KeyPair::/g;
|
|
28
28
|
// X25519 key agreement (dalek).
|
|
29
29
|
const RE_RUST_X25519 = /\bx25519_dalek::(?:EphemeralSecret|StaticSecret)\b/g;
|
|
30
|
+
// X448 key agreement (`x448` crate). The qualified `x448::Secret` path — covers the
|
|
31
|
+
// plain `use x448::Secret;` / fully-qualified form the alias mechanism (which only
|
|
32
|
+
// fires on a RENAMED `use … as`) misses.
|
|
33
|
+
const RE_RUST_X448 = /\bx448::Secret\b/g;
|
|
30
34
|
// openssl crate bindings — the `Type::generate(` / `Dh::` call forms, distinct
|
|
31
35
|
// from the ring / RustCrypto path syntax above (audit F5/F9 false-negatives).
|
|
32
36
|
const RE_RUST_OPENSSL_RSA = /\bRsa::generate\s*\(/g;
|
|
@@ -115,6 +119,18 @@ const RULE_RUST_X25519: RuleMeta = {
|
|
|
115
119
|
cwe: CWE_BROKEN_CRYPTO,
|
|
116
120
|
message: "X25519 (Rust) is modern but still classical key agreement — harvest-now-decrypt-later.",
|
|
117
121
|
};
|
|
122
|
+
const RULE_RUST_X448: RuleMeta = {
|
|
123
|
+
id: "rust-x448",
|
|
124
|
+
title: "Rust X448 key agreement",
|
|
125
|
+
description: "the `x448` crate Secret key agreement",
|
|
126
|
+
category: "key-exchange",
|
|
127
|
+
severity: "medium",
|
|
128
|
+
confidence: "high",
|
|
129
|
+
algorithm: "X448",
|
|
130
|
+
hndl: true,
|
|
131
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
132
|
+
message: "X448 (Rust) is modern but still classical key agreement — harvest-now-decrypt-later.",
|
|
133
|
+
};
|
|
118
134
|
const RULE_RUST_OPENSSL_RSA: RuleMeta = {
|
|
119
135
|
id: "rust-openssl-rsa",
|
|
120
136
|
title: "Rust openssl RSA key generation",
|
|
@@ -251,6 +267,51 @@ const RULE_RUST_TLS_DANGEROUS: RuleMeta = {
|
|
|
251
267
|
remediation: "Avoid the dangerous() escape hatch; keep the default certificate verifier.",
|
|
252
268
|
};
|
|
253
269
|
|
|
270
|
+
/**
|
|
271
|
+
* Aliasable Rust crypto types (`<crate>::<OrigType>` → rule). A renamed `use`
|
|
272
|
+
* (`use x25519_dalek::{EphemeralSecret as MontgomerySecret}`) binds the alias to a
|
|
273
|
+
* classical-key type; a later `MontgomerySecret::<ctor>(` is that key's
|
|
274
|
+
* construction. The `::`-qualified and braced rules above miss this because the
|
|
275
|
+
* call site uses the renamed identifier, not the original path.
|
|
276
|
+
*/
|
|
277
|
+
const RUST_ALIASABLE: Record<string, RuleMeta> = {
|
|
278
|
+
"x25519_dalek::EphemeralSecret": RULE_RUST_X25519,
|
|
279
|
+
"x25519_dalek::StaticSecret": RULE_RUST_X25519,
|
|
280
|
+
"x448::Secret": RULE_RUST_X448,
|
|
281
|
+
"ed25519_dalek::SigningKey": RULE_RUST_ED25519,
|
|
282
|
+
"ed25519_dalek::Keypair": RULE_RUST_ED25519,
|
|
283
|
+
"ed25519_dalek::SecretKey": RULE_RUST_ED25519,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
/** Escape a string for interpolation into a dynamically-built RegExp. */
|
|
287
|
+
function escapeRustRe(s: string): string {
|
|
288
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Collect `use <crate>::<OrigType> as <Alias>` bindings (braced or single) that
|
|
293
|
+
* rename an aliasable crypto type, returning {alias, rule} pairs. An alias equal
|
|
294
|
+
* to its own type name is skipped (the qualified/bare rules already catch it).
|
|
295
|
+
*/
|
|
296
|
+
function collectRustTypeAliases(content: string): Array<{ alias: string; rule: RuleMeta }> {
|
|
297
|
+
const out: Array<{ alias: string; rule: RuleMeta }> = [];
|
|
298
|
+
const push = (crate: string, orig: string, alias: string): void => {
|
|
299
|
+
if (!alias || alias === orig) return;
|
|
300
|
+
const rule = RUST_ALIASABLE[`${crate}::${orig}`];
|
|
301
|
+
if (rule) out.push({ alias, rule });
|
|
302
|
+
};
|
|
303
|
+
// Braced: `use <crate>::{ Orig as Alias, ... }`.
|
|
304
|
+
const braced = /\buse\s+([\w:]+)::\{([^}]*)\}/g;
|
|
305
|
+
for (let m = braced.exec(content); m; m = braced.exec(content)) {
|
|
306
|
+
const specRe = /([A-Za-z_]\w*)\s+as\s+([A-Za-z_]\w*)/g;
|
|
307
|
+
for (let s = specRe.exec(m[2]); s; s = specRe.exec(m[2])) push(m[1], s[1], s[2]);
|
|
308
|
+
}
|
|
309
|
+
// Single: `use <crate>::Orig as Alias;`.
|
|
310
|
+
const single = /\buse\s+([\w:]+)::([A-Za-z_]\w*)\s+as\s+([A-Za-z_]\w*)/g;
|
|
311
|
+
for (let m = single.exec(content); m; m = single.exec(content)) push(m[1], m[2], m[3]);
|
|
312
|
+
return out;
|
|
313
|
+
}
|
|
314
|
+
|
|
254
315
|
/** Detects classical asymmetric crypto in Rust (rsa, ring, dalek, p256/k256). */
|
|
255
316
|
export const rustDetector: Detector = {
|
|
256
317
|
id: "rust-crypto",
|
|
@@ -263,6 +324,7 @@ export const rustDetector: Detector = {
|
|
|
263
324
|
RULE_RUST_ECDH,
|
|
264
325
|
RULE_RUST_ED25519,
|
|
265
326
|
RULE_RUST_X25519,
|
|
327
|
+
RULE_RUST_X448,
|
|
266
328
|
RULE_RUST_OPENSSL_RSA,
|
|
267
329
|
RULE_RUST_OPENSSL_EC,
|
|
268
330
|
RULE_RUST_OPENSSL_DSA,
|
|
@@ -288,6 +350,7 @@ export const rustDetector: Detector = {
|
|
|
288
350
|
add(RE_RUST_ECDH, RULE_RUST_ECDH);
|
|
289
351
|
add(RE_RUST_ED25519, RULE_RUST_ED25519);
|
|
290
352
|
add(RE_RUST_X25519, RULE_RUST_X25519);
|
|
353
|
+
add(RE_RUST_X448, RULE_RUST_X448);
|
|
291
354
|
add(RE_RUST_OPENSSL_RSA, RULE_RUST_OPENSSL_RSA);
|
|
292
355
|
add(RE_RUST_OPENSSL_EC, RULE_RUST_OPENSSL_EC);
|
|
293
356
|
add(RE_RUST_OPENSSL_DSA, RULE_RUST_OPENSSL_DSA);
|
|
@@ -311,6 +374,19 @@ export const rustDetector: Detector = {
|
|
|
311
374
|
});
|
|
312
375
|
add(RE_RUST_TLS_ACCEPT_INVALID, RULE_RUST_TLS_ACCEPT_INVALID);
|
|
313
376
|
add(RE_RUST_TLS_DANGEROUS, RULE_RUST_TLS_DANGEROUS);
|
|
377
|
+
|
|
378
|
+
// Type-alias resolution: `use x25519_dalek::{EphemeralSecret as MontgomerySecret}`
|
|
379
|
+
// then `MontgomerySecret::random_from_rng(` — the braced+renamed `use` defeats
|
|
380
|
+
// the `::`-qualified rules. Match the aliased type's construction calls; runs
|
|
381
|
+
// on the ORIGINAL content so locations stay exact, and fires only for an alias
|
|
382
|
+
// explicitly bound to a known crypto type (precision-safe).
|
|
383
|
+
for (const { alias, rule } of collectRustTypeAliases(content)) {
|
|
384
|
+
const a = escapeRustRe(alias);
|
|
385
|
+
add(
|
|
386
|
+
new RegExp(`\\b${a}::(?:new|random|random_from_rng|generate|from_bytes)\\s*\\(`, "g"),
|
|
387
|
+
rule,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
314
390
|
return findings;
|
|
315
391
|
},
|
|
316
392
|
};
|