@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
package/src/detectors/source.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
DOC_EXTENSIONS,
|
|
33
33
|
JWT_HOST_EXTENSIONS,
|
|
34
34
|
eachMatch,
|
|
35
|
+
enclosingObject,
|
|
35
36
|
findingFromRule,
|
|
36
37
|
hasExtension,
|
|
37
38
|
nearSortedCall,
|
|
@@ -46,6 +47,135 @@ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cw
|
|
|
46
47
|
// (ordered alternation would otherwise match `rsa` and reject the `-pss` tail).
|
|
47
48
|
const RE_GENERATE_KEYPAIR =
|
|
48
49
|
/generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
|
|
50
|
+
|
|
51
|
+
/** Per-key-type classification for `generateKeyPair(Sync)('<type>', …)`. Hoisted
|
|
52
|
+
* to module scope so the direct matcher AND the import-alias pass (below) share
|
|
53
|
+
* one source of truth for how each key type is classified. */
|
|
54
|
+
interface KeygenInfo {
|
|
55
|
+
algo: Finding["algorithm"];
|
|
56
|
+
cat: Finding["category"];
|
|
57
|
+
sev: Finding["severity"];
|
|
58
|
+
hndl: boolean;
|
|
59
|
+
label: string;
|
|
60
|
+
message?: string;
|
|
61
|
+
remediation?: string;
|
|
62
|
+
}
|
|
63
|
+
const KEYGEN_INFO: Record<string, KeygenInfo> = {
|
|
64
|
+
rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
|
|
65
|
+
// RSA-PSS is signature-only, so classify it as a (forgeable) signature
|
|
66
|
+
// rather than a KEM — no HNDL confidentiality exposure.
|
|
67
|
+
"rsa-pss": {
|
|
68
|
+
algo: "RSA",
|
|
69
|
+
cat: "signature",
|
|
70
|
+
sev: "high",
|
|
71
|
+
hndl: false,
|
|
72
|
+
label: "RSA-PSS",
|
|
73
|
+
message: "Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
|
|
74
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
|
|
75
|
+
},
|
|
76
|
+
// EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
|
|
77
|
+
// HNDL-exposed, so classify conservatively as key-exchange-capable and
|
|
78
|
+
// surface both concerns rather than asserting signature-only (P0-4).
|
|
79
|
+
ec: {
|
|
80
|
+
algo: "ECDH",
|
|
81
|
+
cat: "key-exchange",
|
|
82
|
+
sev: "high",
|
|
83
|
+
hndl: true,
|
|
84
|
+
label: "EC (ECDSA/ECDH)",
|
|
85
|
+
message:
|
|
86
|
+
"Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
|
|
87
|
+
"and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
|
|
88
|
+
remediation:
|
|
89
|
+
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
90
|
+
},
|
|
91
|
+
dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
|
|
92
|
+
dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
|
|
93
|
+
x25519: { algo: "X25519", cat: "key-exchange", sev: "medium", hndl: true, label: "X25519" },
|
|
94
|
+
x448: { algo: "X448", cat: "key-exchange", sev: "medium", hndl: true, label: "X448" },
|
|
95
|
+
ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
|
|
96
|
+
ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Emit a keygen finding for `generateKeyPair(Sync)('<type>')` at [index]. Shared
|
|
100
|
+
* by the direct matcher and the import-alias pass so both classify identically. */
|
|
101
|
+
function pushKeygenFinding(
|
|
102
|
+
findings: Finding[],
|
|
103
|
+
rawType: string,
|
|
104
|
+
file: string,
|
|
105
|
+
content: string,
|
|
106
|
+
index: number,
|
|
107
|
+
matchLength: number,
|
|
108
|
+
): void {
|
|
109
|
+
const info = KEYGEN_INFO[rawType.toLowerCase()];
|
|
110
|
+
if (!info) return;
|
|
111
|
+
findings.push(
|
|
112
|
+
findingFromRule(
|
|
113
|
+
RULE_NODE_KEYGEN,
|
|
114
|
+
{ file, content, index, matchLength },
|
|
115
|
+
{
|
|
116
|
+
title: `${info.label} key generation`,
|
|
117
|
+
category: info.cat,
|
|
118
|
+
severity: info.sev,
|
|
119
|
+
algorithm: info.algo,
|
|
120
|
+
hndl: info.hndl,
|
|
121
|
+
message:
|
|
122
|
+
info.message ??
|
|
123
|
+
`Generates a classical ${info.label} key pair, which is not quantum-safe.`,
|
|
124
|
+
...(info.remediation ? { remediation: info.remediation } : {}),
|
|
125
|
+
},
|
|
126
|
+
),
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Canonical Node-crypto call names whose import-aliases we resolve. */
|
|
131
|
+
const ALIASABLE: readonly string[] = [
|
|
132
|
+
"generateKeyPairSync",
|
|
133
|
+
"generateKeyPair",
|
|
134
|
+
"createECDH",
|
|
135
|
+
"createDiffieHellman",
|
|
136
|
+
"createDiffieHellmanGroup",
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Collect import-aliases for the canonical crypto calls we resolve, so
|
|
141
|
+
* `import { generateKeyPairSync as gk } from 'node:crypto'` (or the CommonJS
|
|
142
|
+
* `const { generateKeyPairSync: gk } = require('crypto')`) is followed when `gk`
|
|
143
|
+
* is called later. Returns Map<canonical, alias[]>. An alias equal to its own
|
|
144
|
+
* canonical name is skipped (already matched directly). Precision-safe: the
|
|
145
|
+
* alias is only ever bound to a known crypto symbol, so a later `gk(` call is
|
|
146
|
+
* unambiguously that symbol.
|
|
147
|
+
*/
|
|
148
|
+
function collectCryptoAliases(content: string): Map<string, string[]> {
|
|
149
|
+
const out = new Map<string, string[]>();
|
|
150
|
+
const add = (canonical: string, alias: string): void => {
|
|
151
|
+
if (!alias || alias === canonical) return;
|
|
152
|
+
const list = out.get(canonical) ?? [];
|
|
153
|
+
if (!list.includes(alias)) list.push(alias);
|
|
154
|
+
out.set(canonical, list);
|
|
155
|
+
};
|
|
156
|
+
// ESM named imports: `import { A as B, C as D } from '...'`.
|
|
157
|
+
const esm = /import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"][^'"]*['"]/g;
|
|
158
|
+
for (let m = esm.exec(content); m; m = esm.exec(content)) {
|
|
159
|
+
const spec = /([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)/g;
|
|
160
|
+
for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
|
|
161
|
+
if (ALIASABLE.includes(s[1])) add(s[1], s[2]);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// CommonJS destructure-rename: `const { A: B } = require('...')`.
|
|
165
|
+
const cjs = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*['"][^'"]*['"]\s*\)/g;
|
|
166
|
+
for (let m = cjs.exec(content); m; m = cjs.exec(content)) {
|
|
167
|
+
const spec = /([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)/g;
|
|
168
|
+
for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
|
|
169
|
+
if (ALIASABLE.includes(s[1])) add(s[1], s[2]);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Escape a string for safe interpolation into a dynamically-built RegExp. */
|
|
176
|
+
function escapeRe(s: string): string {
|
|
177
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
178
|
+
}
|
|
49
179
|
const RE_CREATE_SIGN_VERIFY = /create(?:Sign|Verify)\s*\(/g;
|
|
50
180
|
// One-shot crypto.sign/verify(algorithm, data, key). A LOOKBEHIND (not a
|
|
51
181
|
// consumed char) anchors it so it doesn't fire inside identifiers like `assign(`
|
|
@@ -82,7 +212,7 @@ const RE_JSRSASIGN_SIGN = /KJUR\.crypto\.(?:Signature|ECDSA)\b/g;
|
|
|
82
212
|
const RE_NODE_RSA = /new\s+NodeRSA\s*\(/g;
|
|
83
213
|
// secp256k1 — direct @noble/secp256k1 / secp256k1-style API usage in source.
|
|
84
214
|
const RE_SECP256K1 =
|
|
85
|
-
/\b(?:secp(?:256k1)?|secp)\s*\.\s*(
|
|
215
|
+
/\b(?:secp(?:256k1)?|secp)\s*\.\s*(sign|verify|getPublicKey|getSharedSecret|ecdh|recoverPublicKey)\s*\(/g;
|
|
86
216
|
|
|
87
217
|
// JWT/JOSE.
|
|
88
218
|
const RE_JWT_ALG = /['"`](RS(?:256|384|512)|PS(?:256|384|512)|ES(?:256|384|512|256K)|EdDSA)['"`]/g;
|
|
@@ -96,11 +226,15 @@ const RE_TLS_REJECT = /rejectUnauthorized\s*:\s*false/g;
|
|
|
96
226
|
// Hardened cipher regex: bounded spans (no unbounded `[^'"`]*` straddling the
|
|
97
227
|
// alternation), single-quote-style anchoring removed in favour of {0,256} bounds
|
|
98
228
|
// so worst-case backtracking is linear in the bound, not the file (P0-6).
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
229
|
+
// Skip OpenSSL EXCLUSION syntax — `!MD5` / `:-RC4` / `!ECDHE-RSA-RC4-SHA` DISABLE
|
|
230
|
+
// those ciphers, so a hardened list must not be flagged as weak (audit: crypto #7). The
|
|
231
|
+
// single variable-length lookbehind walks back over the suite-name chars to the
|
|
232
|
+
// ELEMENT boundary (`:` / whitespace / comma / quote / start) and rejects the match
|
|
233
|
+
// when that element begins with `!` or `-` — covering a bare `!RC4`, a space-separated
|
|
234
|
+
// `HIGH !RC4`, and a full-suite `!ECDHE-RSA-RC4-SHA`. The intra-name hyphen in a
|
|
235
|
+
// genuinely-enabled `ECDHE-RSA-RC4-SHA` (no leading `!`/`-`) still matches.
|
|
102
236
|
const RE_TLS_WEAK_CIPHER =
|
|
103
|
-
/ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(?<![!-])(RC4|DES|3DES|MD5|NULL|EXPORT|aNULL|eNULL)\b[^'"`\n]{0,256}?['"`]/gi;
|
|
237
|
+
/ciphers\s*:\s*['"`][^'"`\n]{0,256}?\b(?<![:'"`,\s]\s*[!-][\w-]{0,64})(RC4|DES|3DES|MD5|NULL|EXPORT|aNULL|eNULL)\b[^'"`\n]{0,256}?['"`]/gi;
|
|
104
238
|
|
|
105
239
|
/* -------------------------------------------------------------------------- */
|
|
106
240
|
/* Node.js `crypto` module */
|
|
@@ -238,74 +372,54 @@ const nodeCryptoDetector: Detector = {
|
|
|
238
372
|
|
|
239
373
|
// generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
|
|
240
374
|
eachMatch(RE_GENERATE_KEYPAIR, content, (m) => {
|
|
241
|
-
|
|
242
|
-
const map: Record<
|
|
243
|
-
string,
|
|
244
|
-
{
|
|
245
|
-
algo: Finding["algorithm"];
|
|
246
|
-
cat: Finding["category"];
|
|
247
|
-
sev: Finding["severity"];
|
|
248
|
-
hndl: boolean;
|
|
249
|
-
label: string;
|
|
250
|
-
message?: string;
|
|
251
|
-
remediation?: string;
|
|
252
|
-
}
|
|
253
|
-
> = {
|
|
254
|
-
rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
|
|
255
|
-
// RSA-PSS is signature-only, so classify it as a (forgeable) signature
|
|
256
|
-
// rather than a KEM — no HNDL confidentiality exposure.
|
|
257
|
-
"rsa-pss": {
|
|
258
|
-
algo: "RSA",
|
|
259
|
-
cat: "signature",
|
|
260
|
-
sev: "high",
|
|
261
|
-
hndl: false,
|
|
262
|
-
label: "RSA-PSS",
|
|
263
|
-
message:
|
|
264
|
-
"Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
|
|
265
|
-
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
|
|
266
|
-
},
|
|
267
|
-
// EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
|
|
268
|
-
// HNDL-exposed, so classify conservatively as key-exchange-capable and
|
|
269
|
-
// surface both concerns rather than asserting signature-only (P0-4).
|
|
270
|
-
ec: {
|
|
271
|
-
algo: "ECDH",
|
|
272
|
-
cat: "key-exchange",
|
|
273
|
-
sev: "high",
|
|
274
|
-
hndl: true,
|
|
275
|
-
label: "EC (ECDSA/ECDH)",
|
|
276
|
-
message:
|
|
277
|
-
"Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
|
|
278
|
-
"and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
|
|
279
|
-
remediation:
|
|
280
|
-
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
281
|
-
},
|
|
282
|
-
dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
|
|
283
|
-
dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
|
|
284
|
-
x25519: { algo: "X25519", cat: "key-exchange", sev: "medium", hndl: true, label: "X25519" },
|
|
285
|
-
x448: { algo: "X448", cat: "key-exchange", sev: "medium", hndl: true, label: "X448" },
|
|
286
|
-
ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
|
|
287
|
-
ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
|
|
288
|
-
};
|
|
289
|
-
const info = map[type];
|
|
290
|
-
findings.push(
|
|
291
|
-
findingFromRule(
|
|
292
|
-
RULE_NODE_KEYGEN,
|
|
293
|
-
{ file, content, index: m.index, matchLength: m[0].length },
|
|
294
|
-
{
|
|
295
|
-
title: `${info.label} key generation`,
|
|
296
|
-
category: info.cat,
|
|
297
|
-
severity: info.sev,
|
|
298
|
-
algorithm: info.algo,
|
|
299
|
-
hndl: info.hndl,
|
|
300
|
-
message:
|
|
301
|
-
info.message ??
|
|
302
|
-
`Generates a classical ${info.label} key pair, which is not quantum-safe.`,
|
|
303
|
-
...(info.remediation ? { remediation: info.remediation } : {}),
|
|
304
|
-
},
|
|
305
|
-
),
|
|
306
|
-
);
|
|
375
|
+
pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
|
|
307
376
|
});
|
|
308
377
|
|
|
378
|
+
// Import-alias resolution: follow `import { generateKeyPairSync as gk }` (and
|
|
379
|
+
// the CommonJS destructure-rename) so an aliased call still detects. Only the
|
|
380
|
+
// keygen / ECDH / DH constructors are resolved — their classification is
|
|
381
|
+
// unambiguous. The alias regexes run on the ORIGINAL content, so locations
|
|
382
|
+
// stay exact; it fires only when the alias was explicitly bound to a known
|
|
383
|
+
// crypto symbol, so precision is unaffected.
|
|
384
|
+
const aliases = collectCryptoAliases(content);
|
|
385
|
+
for (const [canonical, names] of aliases) {
|
|
386
|
+
for (const alias of names) {
|
|
387
|
+
const a = escapeRe(alias);
|
|
388
|
+
if (canonical === "generateKeyPairSync" || canonical === "generateKeyPair") {
|
|
389
|
+
const re = new RegExp(
|
|
390
|
+
`\\b${a}\\s*\\(\\s*['"\`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"\`]`,
|
|
391
|
+
"g",
|
|
392
|
+
);
|
|
393
|
+
eachMatch(re, content, (m) =>
|
|
394
|
+
pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length),
|
|
395
|
+
);
|
|
396
|
+
} else if (canonical === "createECDH") {
|
|
397
|
+
eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) =>
|
|
398
|
+
findings.push(
|
|
399
|
+
findingFromRule(RULE_NODE_ECDH, {
|
|
400
|
+
file,
|
|
401
|
+
content,
|
|
402
|
+
index: m.index,
|
|
403
|
+
matchLength: m[0].length,
|
|
404
|
+
}),
|
|
405
|
+
),
|
|
406
|
+
);
|
|
407
|
+
} else {
|
|
408
|
+
// createDiffieHellman / createDiffieHellmanGroup.
|
|
409
|
+
eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) =>
|
|
410
|
+
findings.push(
|
|
411
|
+
findingFromRule(RULE_NODE_DH, {
|
|
412
|
+
file,
|
|
413
|
+
content,
|
|
414
|
+
index: m.index,
|
|
415
|
+
matchLength: m[0].length,
|
|
416
|
+
}),
|
|
417
|
+
),
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
309
423
|
// createSign / createVerify — RSA / ECDSA / DSA signatures.
|
|
310
424
|
eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
|
|
311
425
|
findings.push(
|
|
@@ -452,7 +566,9 @@ const webCryptoDetector: Detector = {
|
|
|
452
566
|
algorithm = name === "X448" ? "X448" : "X25519";
|
|
453
567
|
category = "key-exchange";
|
|
454
568
|
hndl = true;
|
|
455
|
-
|
|
569
|
+
// `medium`, aligned with X25519/X448 key agreement in node/rust/go — HNDL-exposed
|
|
570
|
+
// key agreement scores the same regardless of which surface uses it.
|
|
571
|
+
severity = "medium";
|
|
456
572
|
} else if (name === "ED25519" || name === "ED448") {
|
|
457
573
|
algorithm = "EdDSA";
|
|
458
574
|
category = "signature";
|
|
@@ -611,7 +727,27 @@ const libraryDetector: Detector = {
|
|
|
611
727
|
add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
|
|
612
728
|
add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
|
|
613
729
|
add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
|
|
614
|
-
|
|
730
|
+
// secp256k1: classify per method — getSharedSecret/ecdh are key AGREEMENT
|
|
731
|
+
// (ECDH, harvest-now-decrypt-later); sign/verify/getPublicKey/recover are
|
|
732
|
+
// ECDSA signatures (not HNDL). The old rule flagged everything as ECDSA.
|
|
733
|
+
eachMatch(RE_SECP256K1, content, (m) => {
|
|
734
|
+
const kex = m[1] === "getSharedSecret" || m[1] === "ecdh";
|
|
735
|
+
findings.push(
|
|
736
|
+
findingFromRule(
|
|
737
|
+
RULE_SECP256K1,
|
|
738
|
+
{ file, content, index: m.index, matchLength: m[0].length },
|
|
739
|
+
kex
|
|
740
|
+
? {
|
|
741
|
+
title: "secp256k1 ECDH key agreement",
|
|
742
|
+
category: "key-exchange",
|
|
743
|
+
algorithm: "ECDH",
|
|
744
|
+
hndl: true,
|
|
745
|
+
message: `secp256k1 ECDH key agreement (.${m[1]}()) is classical and harvest-now-decrypt-later exposed.`,
|
|
746
|
+
}
|
|
747
|
+
: undefined,
|
|
748
|
+
),
|
|
749
|
+
);
|
|
750
|
+
});
|
|
615
751
|
add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
|
|
616
752
|
add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
|
|
617
753
|
add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
|
|
@@ -689,8 +825,21 @@ const jwtDetector: Detector = {
|
|
|
689
825
|
detect({ file, content }): Finding[] {
|
|
690
826
|
const findings: Finding[] = [];
|
|
691
827
|
|
|
828
|
+
// Deferral guards against double-counting:
|
|
829
|
+
// - webCryptoDetector owns a quoted `RSA-OAEP`/`ECDH-ES` sitting next to a
|
|
830
|
+
// `subtle.*(` call (it matches the `RSA-OAEP`/`ECDH` name). Collect the subtle
|
|
831
|
+
// call offsets so the JOSE key-management loops can skip those. (In non-JS host
|
|
832
|
+
// files there are no subtle calls, so nothing is skipped.)
|
|
833
|
+
// - jwkDetector owns any alg/`kty` that lives inside a JWK object; skip a token
|
|
834
|
+
// whose enclosing `{…}` carries a `"kty"` so a JWK's own `alg` is not counted by
|
|
835
|
+
// both detectors. Mirrors the same guard jose.ts applies for other languages.
|
|
836
|
+
const subtleCalls: number[] = [];
|
|
837
|
+
eachMatch(RE_SUBTLE_CALL, content, (m) => subtleCalls.push(m.index));
|
|
838
|
+
const inJwk = (index: number): boolean => enclosingObject(content, index).includes('"kty"');
|
|
839
|
+
|
|
692
840
|
// Classical JWS signature alg tokens. Anchored to quotes to avoid words.
|
|
693
841
|
eachMatch(RE_JWT_ALG, content, (m) => {
|
|
842
|
+
if (inJwk(m.index)) return; // jwk-{rsa,ec,eddsa} owns a JWK's own alg
|
|
694
843
|
const alg = m[1];
|
|
695
844
|
let algorithm: Finding["algorithm"];
|
|
696
845
|
if (alg.startsWith("RS") || alg.startsWith("PS")) algorithm = "RSA";
|
|
@@ -711,6 +860,8 @@ const jwtDetector: Detector = {
|
|
|
711
860
|
|
|
712
861
|
// JOSE ECDH-ES key agreement (and ECDH-ES+A*KW) — confidentiality, HNDL.
|
|
713
862
|
eachMatch(RE_JOSE_ECDH, content, (m) => {
|
|
863
|
+
if (inJwk(m.index)) return; // jwk-ec owns a JWK's own alg
|
|
864
|
+
if (nearSortedCall(subtleCalls, m.index, 400)) return; // webcrypto owns it
|
|
714
865
|
findings.push(
|
|
715
866
|
findingFromRule(
|
|
716
867
|
RULE_JOSE_ECDH,
|
|
@@ -725,6 +876,10 @@ const jwtDetector: Detector = {
|
|
|
725
876
|
|
|
726
877
|
// JOSE RSA key transport (RSA-OAEP / RSA1_5) — classical RSA encryption, HNDL.
|
|
727
878
|
eachMatch(RE_JOSE_KEM, content, (m) => {
|
|
879
|
+
if (inJwk(m.index)) return; // jwk-rsa owns a JWK's own alg
|
|
880
|
+
// webCryptoDetector's regex matches `RSA-OAEP` but NOT `RSA1_5`, so only defer the
|
|
881
|
+
// OAEP form near a subtle call — deferring RSA1_5 would drop it entirely.
|
|
882
|
+
if (m[1].startsWith("RSA-OAEP") && nearSortedCall(subtleCalls, m.index, 400)) return;
|
|
728
883
|
findings.push(
|
|
729
884
|
findingFromRule(
|
|
730
885
|
RULE_JOSE_RSA_OAEP,
|
|
@@ -852,6 +1007,56 @@ const RE_CERT_SIG_ALG =
|
|
|
852
1007
|
const RE_SSH_KEX =
|
|
853
1008
|
/\b(diffie-hellman-group(?:1|14|15|16|17|18)(?:-sha1|-sha256|-sha512)?|diffie-hellman-group-exchange-sha(?:1|256)|ecdh-sha2-nistp(?:256|384|521)|curve25519-sha256)\b/g;
|
|
854
1009
|
|
|
1010
|
+
/**
|
|
1011
|
+
* True when the match at `index` sits inside a DOCUMENTATION field value — a
|
|
1012
|
+
* `description` / `help` / `summary` / `doc` / `comment` / `note` assignment
|
|
1013
|
+
* (`key = "…"` or `key: "…"`), as used by Terraform/Packer variable descriptions,
|
|
1014
|
+
* JSON-schema docs, etc. Classical KEX/cipher algorithm names LISTED in such a field
|
|
1015
|
+
* are prose ("acceptable values include ecdh-sha2-nistp256, …"), not an active
|
|
1016
|
+
* setting, so a transport-token rule must not fire on them. Scans the current line's
|
|
1017
|
+
* prefix only (cheap; the common single-line description case).
|
|
1018
|
+
*/
|
|
1019
|
+
function inDocFieldValue(content: string, index: number): boolean {
|
|
1020
|
+
const lineStart = content.lastIndexOf("\n", index - 1) + 1;
|
|
1021
|
+
const prefix = content.slice(lineStart, index);
|
|
1022
|
+
return /\b(?:description|help|summary|doc|comment|note)\b\s*[:=]/i.test(prefix);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// A bare key-type token (`ssh-rsa`, `ssh-ed25519`, …) is real SSH crypto only
|
|
1026
|
+
// when it is EITHER (a) followed by base64 key material — an actual
|
|
1027
|
+
// authorized_keys / known_hosts entry — OR (b) one of ≥2 DISTINCT ssh key/host-key
|
|
1028
|
+
// algorithm tokens on the same line — an algorithm-preference list
|
|
1029
|
+
// (`HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256,ssh-rsa`, `%w[…]`, …). A
|
|
1030
|
+
// lone token, e.g. a UI label / i18n value like `"ssh-rsa": "ssh-rsa"`, is
|
|
1031
|
+
// neither, so it must NOT be flagged. `RE_SSH_ALGO_TOKEN` is a separate (cloned-
|
|
1032
|
+
// on-use) copy so counting per line never disturbs the outer `eachMatch` state.
|
|
1033
|
+
const RE_SSH_ALGO_TOKEN = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
|
|
1034
|
+
/** How far around a token we look for a sibling algorithm on the same line. A
|
|
1035
|
+
* real `HostKeyAlgorithms` list is short; bounding the window keeps the per-token
|
|
1036
|
+
* work O(1) so a pathological single huge line can't make detection quadratic. */
|
|
1037
|
+
const SSH_LINE_WINDOW = 512;
|
|
1038
|
+
function isRealSshKeyOrAlgoList(content: string, index: number, matchLen: number): boolean {
|
|
1039
|
+
// (a) base64 key material immediately after the token → a real SSH public key.
|
|
1040
|
+
if (/^\s+[A-Za-z0-9+/]{20,}/.test(content.slice(index + matchLen, index + matchLen + 80))) {
|
|
1041
|
+
return true;
|
|
1042
|
+
}
|
|
1043
|
+
// (b) ≥2 distinct ssh key/host-key algorithm tokens on the same line → a list.
|
|
1044
|
+
// Bound both the back-scan for the line start and the window itself so the work
|
|
1045
|
+
// is constant per token and never crosses a line boundary (i18n entries live on
|
|
1046
|
+
// separate lines, so a lone repeated token stays a single distinct token).
|
|
1047
|
+
const from = Math.max(0, index - SSH_LINE_WINDOW);
|
|
1048
|
+
const pre = content.slice(from, index);
|
|
1049
|
+
const preNl = pre.lastIndexOf("\n");
|
|
1050
|
+
const lineStart = preNl === -1 ? from : from + preNl + 1;
|
|
1051
|
+
const to = Math.min(content.length, index + matchLen + SSH_LINE_WINDOW);
|
|
1052
|
+
const post = content.slice(index, to);
|
|
1053
|
+
const postNl = post.indexOf("\n");
|
|
1054
|
+
const lineEnd = postNl === -1 ? to : index + postNl;
|
|
1055
|
+
const distinct = new Set<string>();
|
|
1056
|
+
for (const t of content.slice(lineStart, lineEnd).matchAll(RE_SSH_ALGO_TOKEN)) distinct.add(t[1]);
|
|
1057
|
+
return distinct.size >= 2;
|
|
1058
|
+
}
|
|
1059
|
+
|
|
855
1060
|
const RULE_SSH_PUBKEY: RuleMeta = {
|
|
856
1061
|
id: "ssh-public-key",
|
|
857
1062
|
title: "Classical SSH public key",
|
|
@@ -917,6 +1122,9 @@ const sshCertDetector: Detector = {
|
|
|
917
1122
|
|
|
918
1123
|
// SSH public keys: ssh-rsa AAAA…, ecdsa-sha2-nistp256 …, ssh-ed25519 …
|
|
919
1124
|
eachMatch(RE_SSH_PUBKEY, content, (m) => {
|
|
1125
|
+
// Skip a bare token that is neither a real key nor an algorithm list (e.g.
|
|
1126
|
+
// a `"ssh-rsa": "ssh-rsa"` i18n label) — see `isRealSshKeyOrAlgoList`.
|
|
1127
|
+
if (!isRealSshKeyOrAlgoList(content, m.index, m[0].length)) return;
|
|
920
1128
|
const tok = m[1];
|
|
921
1129
|
const algorithm: Finding["algorithm"] = tok.startsWith("ssh-rsa")
|
|
922
1130
|
? "RSA"
|
|
@@ -961,6 +1169,7 @@ const sshCertDetector: Detector = {
|
|
|
961
1169
|
|
|
962
1170
|
// SSH key-exchange algorithm identifiers (finite-field DH / ECDH / X25519).
|
|
963
1171
|
eachMatch(RE_SSH_KEX, content, (m) => {
|
|
1172
|
+
if (inDocFieldValue(content, m.index)) return; // names listed in a doc string
|
|
964
1173
|
const tok = m[1];
|
|
965
1174
|
const algorithm: Finding["algorithm"] = tok.startsWith("diffie-hellman")
|
|
966
1175
|
? "DH"
|
|
@@ -1029,6 +1238,7 @@ const tlsClassicalKexDetector: Detector = {
|
|
|
1029
1238
|
detect({ file, content }): Finding[] {
|
|
1030
1239
|
const findings: Finding[] = [];
|
|
1031
1240
|
eachMatch(RE_TLS_CLASSICAL_KEX, content, (m) => {
|
|
1241
|
+
if (inDocFieldValue(content, m.index)) return; // names listed in a doc string
|
|
1032
1242
|
const tok = m[0];
|
|
1033
1243
|
const algorithm: Finding["algorithm"] = tok.includes("ECDH") ? "ECDH" : "DH";
|
|
1034
1244
|
findings.push(
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: classical key types configured for SPIFFE X.509 SVIDs in
|
|
3
|
+
* SPIRE server/agent configuration (HCL, and the `.conf`/YAML/JSON variants
|
|
4
|
+
* SPIRE deployments ship).
|
|
5
|
+
*
|
|
6
|
+
* SPIRE is the reference SPIFFE implementation: it issues each workload a
|
|
7
|
+
* short-lived X.509-SVID certificate, signed by the SPIRE server's CA (itself
|
|
8
|
+
* often chained to an UpstreamAuthority). The SVID leaf key and the CA signing
|
|
9
|
+
* key are configured by explicit key-type strings:
|
|
10
|
+
* - server `ca_key_type = "rsa-2048" | "rsa-4096" | "ec-p256" | "ec-p384"` —
|
|
11
|
+
* the CA that signs every workload SVID in the trust domain (the identity
|
|
12
|
+
* ROOT: forge this and you forge the whole trust domain).
|
|
13
|
+
* - agent/server `svid_key_type = "rsa-2048" | "ec-p256"` — the per-workload
|
|
14
|
+
* leaf key type.
|
|
15
|
+
* - per-plugin `key_type = "rsa-2048" | "ec-p256"` on UpstreamAuthority /
|
|
16
|
+
* KeyManager plugins.
|
|
17
|
+
*
|
|
18
|
+
* These are SIGNING / identity keys, not confidentiality keys: an SVID X.509
|
|
19
|
+
* signature is verified, never used to wrap harvestable ciphertext. So there is
|
|
20
|
+
* nothing to "harvest now, decrypt later" — `hndl: false` — but every SVID
|
|
21
|
+
* signature (and the CA signature over it) becomes FORGEABLE the moment a
|
|
22
|
+
* cryptographically-relevant quantum computer (CRQC) exists, letting an attacker
|
|
23
|
+
* mint certificates for any SPIFFE ID in the trust domain. Findings are
|
|
24
|
+
* therefore `category: "certificate"` (X.509 / PKI material), `severity:
|
|
25
|
+
* "medium"`, `confidence: "high"`.
|
|
26
|
+
*
|
|
27
|
+
* Algorithm mapping (SPIRE key-type value → {@link AlgorithmFamily}):
|
|
28
|
+
* - `rsa-2048` / `rsa-4096` / any `rsa-*` → RSA
|
|
29
|
+
* - `ec-p256` / `ec-p384` / `ecdsa` → ECDSA
|
|
30
|
+
*
|
|
31
|
+
* Match shape: the key-type attribute name (`ca_key_type`, `svid_key_type`, or
|
|
32
|
+
* the bare per-plugin `key_type`) followed by `=`/`:` and a quoted value, in
|
|
33
|
+
* both HCL (`=`) and YAML/JSON (`:`) forms. The `\b(?:ca_|svid_)?key_type\b`
|
|
34
|
+
* anchor covers all three names in one pattern — the leading `\b` won't fire
|
|
35
|
+
* inside `ca_key_type`/`svid_key_type` for the bare `key_type` alternative
|
|
36
|
+
* because `_` is a word character, so those longer names are matched whole.
|
|
37
|
+
*
|
|
38
|
+
* Fast reject: `detect()` bails unless the file carries a SPIFFE/SPIRE-specific
|
|
39
|
+
* marker (`spiffe`, `spire`, the distinctive `ca_key_type`, or `svid`). A bare
|
|
40
|
+
* `key_type = "rsa-2048"` is deliberately NOT a sufficient marker — `key_type`
|
|
41
|
+
* is a generic attribute (Vault PKI roles, cloud IaC, …), so without a
|
|
42
|
+
* spiffe/spire/svid/ca_key_type anchor an unrelated config must not fire. This
|
|
43
|
+
* keeps the generic per-plugin `key_type` rule precise while still catching a
|
|
44
|
+
* real SPIRE config, which always names its trust domain / server / SVIDs.
|
|
45
|
+
*/
|
|
46
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
47
|
+
import {
|
|
48
|
+
DOC_EXTENSIONS,
|
|
49
|
+
eachMatch,
|
|
50
|
+
findingFromRule,
|
|
51
|
+
hasExtension,
|
|
52
|
+
maskCommentLines,
|
|
53
|
+
} from "../detect-utils.js";
|
|
54
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
55
|
+
|
|
56
|
+
// SPIRE config is HCL/conf, but deployments also template it as YAML (Helm
|
|
57
|
+
// values / ConfigMaps) or JSON. Prose (`.md`, …) is excluded in `appliesTo`.
|
|
58
|
+
const SPIRE_EXTENSIONS: readonly string[] = [".conf", ".hcl", ".yaml", ".yml", ".json"];
|
|
59
|
+
|
|
60
|
+
// The three SPIRE key-type attribute names in one anchor: `ca_key_type`,
|
|
61
|
+
// `svid_key_type`, and the bare per-plugin `key_type`.
|
|
62
|
+
const KEY_ATTR = "(?:ca_|svid_)?key_type";
|
|
63
|
+
// HCL `=`, unquoted YAML `:`, and JSON `"key_type": "..."` assignment forms. The
|
|
64
|
+
// attribute-name and value quotes are BOTH optional (`"?`): HCL uses `key_type =
|
|
65
|
+
// "rsa-2048"`, Helm/YAML uses the unquoted `key_type: rsa-2048`, and JSON quotes
|
|
66
|
+
// the key (`"key_type": "rsa-2048"`) — a required-quote regex missed the latter
|
|
67
|
+
// two entirely. Case-insensitive, though SPIRE writes these lowercase.
|
|
68
|
+
const RE_SPIRE_RSA = new RegExp(`\\b${KEY_ATTR}"?\\s*[:=]\\s*"?rsa-\\d+`, "gi");
|
|
69
|
+
const RE_SPIRE_ECDSA = new RegExp(`\\b${KEY_ATTR}"?\\s*[:=]\\s*"?(?:ec-p\\d+|ecdsa)\\b`, "gi");
|
|
70
|
+
|
|
71
|
+
const RULE_SPIRE_RSA: RuleMeta = {
|
|
72
|
+
id: "spire-rsa-key",
|
|
73
|
+
title: "SPIRE/SPIFFE RSA SVID key type",
|
|
74
|
+
description: 'SPIRE ca_key_type/svid_key_type/key_type = "rsa-*" (classical X.509-SVID key)',
|
|
75
|
+
category: "certificate",
|
|
76
|
+
severity: "medium",
|
|
77
|
+
confidence: "high",
|
|
78
|
+
algorithm: "RSA",
|
|
79
|
+
hndl: false,
|
|
80
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
81
|
+
message:
|
|
82
|
+
"SPIRE issues SPIFFE X.509-SVIDs with a classical RSA key; the SVID (and CA) signatures become forgeable once a CRQC exists, letting an attacker mint identities for the trust domain.",
|
|
83
|
+
remediation:
|
|
84
|
+
"SPIFFE/SPIRE has no PQC SVID key type yet — track the SPIFFE roadmap for PQC signature support. Prioritise the SPIRE server ca_key_type: the classical CA is the identity root, so its forgeability compromises every workload SVID.",
|
|
85
|
+
};
|
|
86
|
+
const RULE_SPIRE_ECDSA: RuleMeta = {
|
|
87
|
+
id: "spire-ec-key",
|
|
88
|
+
title: "SPIRE/SPIFFE ECDSA SVID key type",
|
|
89
|
+
description:
|
|
90
|
+
'SPIRE ca_key_type/svid_key_type/key_type = "ec-p256|ec-p384|ecdsa" (classical X.509-SVID key)',
|
|
91
|
+
category: "certificate",
|
|
92
|
+
severity: "medium",
|
|
93
|
+
confidence: "high",
|
|
94
|
+
algorithm: "ECDSA",
|
|
95
|
+
hndl: false,
|
|
96
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
97
|
+
message:
|
|
98
|
+
"SPIRE issues SPIFFE X.509-SVIDs with a classical ECDSA (P-256/P-384) key; the SVID (and CA) signatures become forgeable once a CRQC exists, letting an attacker mint identities for the trust domain.",
|
|
99
|
+
remediation:
|
|
100
|
+
"SPIFFE/SPIRE has no PQC SVID key type yet — track the SPIFFE roadmap for PQC signature support. Prioritise the SPIRE server ca_key_type: the classical CA is the identity root, so its forgeability compromises every workload SVID.",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* True when `content` carries a SPIFFE/SPIRE-specific marker. A bare generic
|
|
105
|
+
* `key_type` is intentionally NOT enough — only `spiffe`/`spire` context, the
|
|
106
|
+
* distinctive `ca_key_type`, or an `svid` token qualifies — so an unrelated
|
|
107
|
+
* config that merely sets `key_type = "rsa-2048"` cannot fire.
|
|
108
|
+
*/
|
|
109
|
+
function hasSpireMarker(content: string): boolean {
|
|
110
|
+
// Word-anchored so `inspired` / `aspire` / a `svid` substring inside a longer
|
|
111
|
+
// token don't qualify; `ca_key_type` is distinctive enough to match unanchored.
|
|
112
|
+
return /\b(?:spiffe|spire|svid)\b/i.test(content) || content.includes("ca_key_type");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Detects classical key types configured for SPIFFE X.509 SVIDs in SPIRE config. */
|
|
116
|
+
export const spireDetector: Detector = {
|
|
117
|
+
id: "spire-crypto",
|
|
118
|
+
description: "Classical key types for SPIFFE X.509 SVIDs in SPIRE server/agent config",
|
|
119
|
+
scope: "config",
|
|
120
|
+
language: "any",
|
|
121
|
+
rules: [RULE_SPIRE_RSA, RULE_SPIRE_ECDSA],
|
|
122
|
+
// Gate to SPIRE's config extensions, and never run on prose (a README that
|
|
123
|
+
// describes `ca_key_type = "rsa-2048"` is documentation, not live config).
|
|
124
|
+
appliesTo: (f) => hasExtension(f, SPIRE_EXTENSIONS) && !hasExtension(f, DOC_EXTENSIONS),
|
|
125
|
+
detect({ file, content }): Finding[] {
|
|
126
|
+
if (!hasSpireMarker(content)) return [];
|
|
127
|
+
|
|
128
|
+
// Mask whole comment lines (HCL/YAML `#`, HCL/JSON5 `//`) so a commented-out
|
|
129
|
+
// key-type directive can't fire. Offsets are preserved, so finding
|
|
130
|
+
// line/column/snippet for the remaining live config stay exact.
|
|
131
|
+
const scan = maskCommentLines(content, ["#", "//"]);
|
|
132
|
+
const findings: Finding[] = [];
|
|
133
|
+
const add = (re: RegExp, rule: RuleMeta) =>
|
|
134
|
+
eachMatch(re, scan, (m) =>
|
|
135
|
+
findings.push(
|
|
136
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
add(RE_SPIRE_RSA, RULE_SPIRE_RSA);
|
|
140
|
+
add(RE_SPIRE_ECDSA, RULE_SPIRE_ECDSA);
|
|
141
|
+
return findings;
|
|
142
|
+
},
|
|
143
|
+
};
|