@quantakrypto/core 0.4.3 → 0.4.4
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/dist/cbom-merge.d.ts +9 -0
- package/dist/cbom-merge.d.ts.map +1 -0
- package/dist/cbom-merge.js +99 -0
- package/dist/cbom-merge.js.map +1 -0
- package/dist/comments.d.ts +4 -3
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +53 -33
- 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 +5 -1
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +11 -2
- package/dist/detect-utils.js.map +1 -1
- package/dist/detectors/c.d.ts +4 -2
- package/dist/detectors/c.d.ts.map +1 -1
- package/dist/detectors/c.js +192 -1
- 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 +116 -0
- package/dist/detectors/cicd.js.map +1 -0
- package/dist/detectors/cloud-kms.d.ts +21 -0
- package/dist/detectors/cloud-kms.d.ts.map +1 -0
- package/dist/detectors/cloud-kms.js +57 -0
- package/dist/detectors/cloud-kms.js.map +1 -0
- package/dist/detectors/cloudformation.d.ts +30 -0
- package/dist/detectors/cloudformation.d.ts.map +1 -0
- package/dist/detectors/cloudformation.js +170 -0
- package/dist/detectors/cloudformation.js.map +1 -0
- package/dist/detectors/database.d.ts +17 -0
- package/dist/detectors/database.d.ts.map +1 -0
- package/dist/detectors/database.js +60 -0
- package/dist/detectors/database.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 +107 -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 +195 -0
- package/dist/detectors/elixir.js.map +1 -0
- package/dist/detectors/java.d.ts.map +1 -1
- package/dist/detectors/java.js +1 -1
- 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 +62 -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 +95 -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 +85 -0
- package/dist/detectors/k8s.js.map +1 -0
- package/dist/detectors/mesh.d.ts +35 -0
- package/dist/detectors/mesh.d.ts.map +1 -0
- package/dist/detectors/mesh.js +109 -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 +72 -0
- package/dist/detectors/messaging.js.map +1 -0
- package/dist/detectors/pem.js +1 -1
- package/dist/detectors/pem.js.map +1 -1
- package/dist/detectors/php.d.ts +24 -0
- package/dist/detectors/php.d.ts.map +1 -0
- package/dist/detectors/php.js +167 -0
- package/dist/detectors/php.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 +68 -0
- package/dist/detectors/rust.js.map +1 -1
- package/dist/detectors/secrets.d.ts +21 -0
- package/dist/detectors/secrets.d.ts.map +1 -0
- package/dist/detectors/secrets.js +79 -0
- package/dist/detectors/secrets.js.map +1 -0
- package/dist/detectors/source.d.ts.map +1 -1
- package/dist/detectors/source.js +190 -47
- package/dist/detectors/source.js.map +1 -1
- 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/terraform.d.ts +21 -0
- package/dist/detectors/terraform.d.ts.map +1 -0
- package/dist/detectors/terraform.js +123 -0
- package/dist/detectors/terraform.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 +99 -0
- package/dist/detectors/vpn.js.map +1 -0
- package/dist/evidence.d.ts +5 -0
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js +5 -0
- package/dist/evidence.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.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.map +1 -1
- package/dist/registry.js +30 -0
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts.map +1 -1
- package/dist/remediate-pipeline.js +22 -1
- package/dist/remediate-pipeline.js.map +1 -1
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +7 -0
- package/dist/report.js.map +1 -1
- 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 +1 -1
- 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/package.json +1 -1
- package/src/cbom-merge.ts +116 -0
- package/src/comments.ts +59 -33
- package/src/dependencies.ts +2 -1
- package/src/detect-utils.ts +14 -2
- package/src/detectors/c.ts +209 -3
- package/src/detectors/cicd.ts +158 -0
- package/src/detectors/cloud-kms.ts +87 -0
- package/src/detectors/cloudformation.ts +215 -0
- package/src/detectors/database.ts +88 -0
- package/src/detectors/dnssec.ts +186 -0
- package/src/detectors/elixir.ts +237 -0
- package/src/detectors/java.ts +2 -1
- package/src/detectors/jose.ts +94 -0
- package/src/detectors/jwk.ts +128 -0
- package/src/detectors/k8s.ts +112 -0
- package/src/detectors/mesh.ts +154 -0
- package/src/detectors/messaging.ts +98 -0
- package/src/detectors/pem.ts +1 -1
- package/src/detectors/php.ts +225 -0
- package/src/detectors/python.ts +71 -0
- package/src/detectors/rust.ts +71 -0
- package/src/detectors/secrets.ts +114 -0
- package/src/detectors/source.ts +235 -68
- package/src/detectors/stateful-hbs.ts +9 -6
- package/src/detectors/terraform.ts +154 -0
- package/src/detectors/vpn.ts +131 -0
- package/src/evidence.ts +11 -0
- package/src/index.ts +10 -0
- package/src/policy.ts +180 -0
- package/src/registry.ts +30 -0
- package/src/remediate-pipeline.ts +25 -2
- package/src/report.ts +7 -0
- package/src/standards.ts +186 -0
- package/src/types.ts +2 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config detector: secrets encrypted at rest with classical asymmetric key
|
|
3
|
+
* wrapping. This is the purest "harvest now, decrypt later" surface — the
|
|
4
|
+
* ciphertext is often committed to a git repository, which means it is already
|
|
5
|
+
* replicated, effectively immortal, and retroactively un-fixable (you can
|
|
6
|
+
* re-encrypt HEAD, not history). Every recipient key here is classical.
|
|
7
|
+
*
|
|
8
|
+
* Covered:
|
|
9
|
+
* - Mozilla SOPS / age recipients: `age1…` bech32 public keys (X25519 key
|
|
10
|
+
* agreement wrapping the data key).
|
|
11
|
+
* - PGP-encrypted payloads: `-----BEGIN PGP MESSAGE-----` (RSA/ElGamal ESK).
|
|
12
|
+
* - Bitnami Sealed Secrets: `kind: SealedSecret` (controller wraps with RSA-OAEP).
|
|
13
|
+
*
|
|
14
|
+
* Symmetric-only schemes (ansible-vault AES, age with a scrypt passphrase) are
|
|
15
|
+
* intentionally NOT flagged: a strong symmetric key is only Grover-weakened, not
|
|
16
|
+
* broken, so it is out of scope for a *classical-asymmetric* readiness signal.
|
|
17
|
+
*/
|
|
18
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
19
|
+
import { DOC_EXTENSIONS, eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
|
|
20
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
21
|
+
|
|
22
|
+
interface SecretRule {
|
|
23
|
+
re: RegExp;
|
|
24
|
+
meta: RuleMeta;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SECRET_RULES: SecretRule[] = [
|
|
28
|
+
{
|
|
29
|
+
// age/SOPS recipient: `age1` + 58 bech32 chars. Distinctive enough for any file.
|
|
30
|
+
re: /\bage1[0-9a-z]{58}\b/g,
|
|
31
|
+
meta: {
|
|
32
|
+
id: "secrets-age-recipient",
|
|
33
|
+
title: "age / SOPS recipient (X25519)",
|
|
34
|
+
description: "An age (SOPS) recipient public key wraps secrets with classical X25519",
|
|
35
|
+
category: "key-exchange",
|
|
36
|
+
severity: "high",
|
|
37
|
+
confidence: "high",
|
|
38
|
+
algorithm: "X25519",
|
|
39
|
+
hndl: true,
|
|
40
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
41
|
+
message:
|
|
42
|
+
"Secrets are wrapped to an age/SOPS X25519 recipient (classical key agreement); harvest-now-decrypt-later exposed, and if committed to git the ciphertext is retroactively un-fixable.",
|
|
43
|
+
remediation:
|
|
44
|
+
"Track a post-quantum age recipient / KMS (ML-KEM) and re-encrypt; rotate any secret whose ciphertext has left your control.",
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
re: /-----BEGIN PGP MESSAGE-----/g,
|
|
49
|
+
meta: {
|
|
50
|
+
id: "secrets-pgp-message",
|
|
51
|
+
title: "PGP-encrypted secret (RSA/ElGamal)",
|
|
52
|
+
description: "A PGP MESSAGE block: the session key is wrapped with classical RSA/ElGamal",
|
|
53
|
+
category: "kem",
|
|
54
|
+
severity: "high",
|
|
55
|
+
confidence: "high",
|
|
56
|
+
algorithm: "RSA",
|
|
57
|
+
hndl: true,
|
|
58
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
59
|
+
message:
|
|
60
|
+
"A PGP-encrypted secret whose session key is wrapped with classical RSA/ElGamal; harvest-now-decrypt-later exposed.",
|
|
61
|
+
remediation:
|
|
62
|
+
"Re-encrypt with a post-quantum KEM (ML-KEM-768) once available; rotate the underlying secret.",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
re: /\bkind:\s*["']?SealedSecret\b/g,
|
|
67
|
+
meta: {
|
|
68
|
+
id: "secrets-sealed-secret",
|
|
69
|
+
title: "Bitnami Sealed Secret (RSA-OAEP)",
|
|
70
|
+
description: "A SealedSecret is wrapped by the controller's classical RSA-OAEP key",
|
|
71
|
+
category: "kem",
|
|
72
|
+
severity: "high",
|
|
73
|
+
confidence: "high",
|
|
74
|
+
algorithm: "RSA",
|
|
75
|
+
hndl: true,
|
|
76
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
77
|
+
message:
|
|
78
|
+
"A Bitnami SealedSecret is wrapped with the controller's classical RSA-OAEP key; harvest-now-decrypt-later exposed, and typically committed to git.",
|
|
79
|
+
remediation:
|
|
80
|
+
"Plan migration as sealed-secrets adds PQC support; rotate the sealing key and secrets when it does.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/** Detects secrets encrypted at rest with classical asymmetric key wrapping. */
|
|
86
|
+
export const secretsDetector: Detector = {
|
|
87
|
+
id: "secrets-at-rest",
|
|
88
|
+
description:
|
|
89
|
+
"Secrets wrapped at rest with classical asymmetric crypto (SOPS/age, PGP, Sealed Secrets)",
|
|
90
|
+
scope: "config",
|
|
91
|
+
language: "any",
|
|
92
|
+
rules: SECRET_RULES.map((r) => r.meta),
|
|
93
|
+
// Skip prose/docs: a tutorial showing an example age recipient is not a secret store.
|
|
94
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
95
|
+
detect({ file, content }): Finding[] {
|
|
96
|
+
// Fast reject: none of the distinctive markers present.
|
|
97
|
+
if (
|
|
98
|
+
!content.includes("age1") &&
|
|
99
|
+
!content.includes("BEGIN PGP MESSAGE") &&
|
|
100
|
+
!content.includes("SealedSecret")
|
|
101
|
+
) {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
const findings: Finding[] = [];
|
|
105
|
+
for (const rule of SECRET_RULES) {
|
|
106
|
+
eachMatch(rule.re, content, (m) => {
|
|
107
|
+
findings.push(
|
|
108
|
+
findingFromRule(rule.meta, { file, content, index: m.index, matchLength: m[0].length }),
|
|
109
|
+
);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return findings;
|
|
113
|
+
},
|
|
114
|
+
};
|
package/src/detectors/source.ts
CHANGED
|
@@ -46,6 +46,135 @@ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cw
|
|
|
46
46
|
// (ordered alternation would otherwise match `rsa` and reject the `-pss` tail).
|
|
47
47
|
const RE_GENERATE_KEYPAIR =
|
|
48
48
|
/generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
|
|
49
|
+
|
|
50
|
+
/** Per-key-type classification for `generateKeyPair(Sync)('<type>', …)`. Hoisted
|
|
51
|
+
* to module scope so the direct matcher AND the import-alias pass (below) share
|
|
52
|
+
* one source of truth for how each key type is classified. */
|
|
53
|
+
interface KeygenInfo {
|
|
54
|
+
algo: Finding["algorithm"];
|
|
55
|
+
cat: Finding["category"];
|
|
56
|
+
sev: Finding["severity"];
|
|
57
|
+
hndl: boolean;
|
|
58
|
+
label: string;
|
|
59
|
+
message?: string;
|
|
60
|
+
remediation?: string;
|
|
61
|
+
}
|
|
62
|
+
const KEYGEN_INFO: Record<string, KeygenInfo> = {
|
|
63
|
+
rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
|
|
64
|
+
// RSA-PSS is signature-only, so classify it as a (forgeable) signature
|
|
65
|
+
// rather than a KEM — no HNDL confidentiality exposure.
|
|
66
|
+
"rsa-pss": {
|
|
67
|
+
algo: "RSA",
|
|
68
|
+
cat: "signature",
|
|
69
|
+
sev: "high",
|
|
70
|
+
hndl: false,
|
|
71
|
+
label: "RSA-PSS",
|
|
72
|
+
message: "Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
|
|
73
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
|
|
74
|
+
},
|
|
75
|
+
// EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
|
|
76
|
+
// HNDL-exposed, so classify conservatively as key-exchange-capable and
|
|
77
|
+
// surface both concerns rather than asserting signature-only (P0-4).
|
|
78
|
+
ec: {
|
|
79
|
+
algo: "ECDH",
|
|
80
|
+
cat: "key-exchange",
|
|
81
|
+
sev: "high",
|
|
82
|
+
hndl: true,
|
|
83
|
+
label: "EC (ECDSA/ECDH)",
|
|
84
|
+
message:
|
|
85
|
+
"Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
|
|
86
|
+
"and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
|
|
87
|
+
remediation:
|
|
88
|
+
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
89
|
+
},
|
|
90
|
+
dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
|
|
91
|
+
dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
|
|
92
|
+
x25519: { algo: "X25519", cat: "key-exchange", sev: "medium", hndl: true, label: "X25519" },
|
|
93
|
+
x448: { algo: "X448", cat: "key-exchange", sev: "medium", hndl: true, label: "X448" },
|
|
94
|
+
ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
|
|
95
|
+
ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/** Emit a keygen finding for `generateKeyPair(Sync)('<type>')` at [index]. Shared
|
|
99
|
+
* by the direct matcher and the import-alias pass so both classify identically. */
|
|
100
|
+
function pushKeygenFinding(
|
|
101
|
+
findings: Finding[],
|
|
102
|
+
rawType: string,
|
|
103
|
+
file: string,
|
|
104
|
+
content: string,
|
|
105
|
+
index: number,
|
|
106
|
+
matchLength: number,
|
|
107
|
+
): void {
|
|
108
|
+
const info = KEYGEN_INFO[rawType.toLowerCase()];
|
|
109
|
+
if (!info) return;
|
|
110
|
+
findings.push(
|
|
111
|
+
findingFromRule(
|
|
112
|
+
RULE_NODE_KEYGEN,
|
|
113
|
+
{ file, content, index, matchLength },
|
|
114
|
+
{
|
|
115
|
+
title: `${info.label} key generation`,
|
|
116
|
+
category: info.cat,
|
|
117
|
+
severity: info.sev,
|
|
118
|
+
algorithm: info.algo,
|
|
119
|
+
hndl: info.hndl,
|
|
120
|
+
message:
|
|
121
|
+
info.message ??
|
|
122
|
+
`Generates a classical ${info.label} key pair, which is not quantum-safe.`,
|
|
123
|
+
...(info.remediation ? { remediation: info.remediation } : {}),
|
|
124
|
+
},
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Canonical Node-crypto call names whose import-aliases we resolve. */
|
|
130
|
+
const ALIASABLE: readonly string[] = [
|
|
131
|
+
"generateKeyPairSync",
|
|
132
|
+
"generateKeyPair",
|
|
133
|
+
"createECDH",
|
|
134
|
+
"createDiffieHellman",
|
|
135
|
+
"createDiffieHellmanGroup",
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Collect import-aliases for the canonical crypto calls we resolve, so
|
|
140
|
+
* `import { generateKeyPairSync as gk } from 'node:crypto'` (or the CommonJS
|
|
141
|
+
* `const { generateKeyPairSync: gk } = require('crypto')`) is followed when `gk`
|
|
142
|
+
* is called later. Returns Map<canonical, alias[]>. An alias equal to its own
|
|
143
|
+
* canonical name is skipped (already matched directly). Precision-safe: the
|
|
144
|
+
* alias is only ever bound to a known crypto symbol, so a later `gk(` call is
|
|
145
|
+
* unambiguously that symbol.
|
|
146
|
+
*/
|
|
147
|
+
function collectCryptoAliases(content: string): Map<string, string[]> {
|
|
148
|
+
const out = new Map<string, string[]>();
|
|
149
|
+
const add = (canonical: string, alias: string): void => {
|
|
150
|
+
if (!alias || alias === canonical) return;
|
|
151
|
+
const list = out.get(canonical) ?? [];
|
|
152
|
+
if (!list.includes(alias)) list.push(alias);
|
|
153
|
+
out.set(canonical, list);
|
|
154
|
+
};
|
|
155
|
+
// ESM named imports: `import { A as B, C as D } from '...'`.
|
|
156
|
+
const esm = /import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"][^'"]*['"]/g;
|
|
157
|
+
for (let m = esm.exec(content); m; m = esm.exec(content)) {
|
|
158
|
+
const spec = /([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)/g;
|
|
159
|
+
for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
|
|
160
|
+
if (ALIASABLE.includes(s[1])) add(s[1], s[2]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// CommonJS destructure-rename: `const { A: B } = require('...')`.
|
|
164
|
+
const cjs = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*['"][^'"]*['"]\s*\)/g;
|
|
165
|
+
for (let m = cjs.exec(content); m; m = cjs.exec(content)) {
|
|
166
|
+
const spec = /([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)/g;
|
|
167
|
+
for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
|
|
168
|
+
if (ALIASABLE.includes(s[1])) add(s[1], s[2]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Escape a string for safe interpolation into a dynamically-built RegExp. */
|
|
175
|
+
function escapeRe(s: string): string {
|
|
176
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
177
|
+
}
|
|
49
178
|
const RE_CREATE_SIGN_VERIFY = /create(?:Sign|Verify)\s*\(/g;
|
|
50
179
|
// One-shot crypto.sign/verify(algorithm, data, key). A LOOKBEHIND (not a
|
|
51
180
|
// consumed char) anchors it so it doesn't fire inside identifiers like `assign(`
|
|
@@ -82,7 +211,7 @@ const RE_JSRSASIGN_SIGN = /KJUR\.crypto\.(?:Signature|ECDSA)\b/g;
|
|
|
82
211
|
const RE_NODE_RSA = /new\s+NodeRSA\s*\(/g;
|
|
83
212
|
// secp256k1 — direct @noble/secp256k1 / secp256k1-style API usage in source.
|
|
84
213
|
const RE_SECP256K1 =
|
|
85
|
-
/\b(?:secp(?:256k1)?|secp)\s*\.\s*(
|
|
214
|
+
/\b(?:secp(?:256k1)?|secp)\s*\.\s*(sign|verify|getPublicKey|getSharedSecret|ecdh|recoverPublicKey)\s*\(/g;
|
|
86
215
|
|
|
87
216
|
// JWT/JOSE.
|
|
88
217
|
const RE_JWT_ALG = /['"`](RS(?:256|384|512)|PS(?:256|384|512)|ES(?:256|384|512|256K)|EdDSA)['"`]/g;
|
|
@@ -238,74 +367,54 @@ const nodeCryptoDetector: Detector = {
|
|
|
238
367
|
|
|
239
368
|
// generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
|
|
240
369
|
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
|
-
);
|
|
370
|
+
pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
|
|
307
371
|
});
|
|
308
372
|
|
|
373
|
+
// Import-alias resolution: follow `import { generateKeyPairSync as gk }` (and
|
|
374
|
+
// the CommonJS destructure-rename) so an aliased call still detects. Only the
|
|
375
|
+
// keygen / ECDH / DH constructors are resolved — their classification is
|
|
376
|
+
// unambiguous. The alias regexes run on the ORIGINAL content, so locations
|
|
377
|
+
// stay exact; it fires only when the alias was explicitly bound to a known
|
|
378
|
+
// crypto symbol, so precision is unaffected.
|
|
379
|
+
const aliases = collectCryptoAliases(content);
|
|
380
|
+
for (const [canonical, names] of aliases) {
|
|
381
|
+
for (const alias of names) {
|
|
382
|
+
const a = escapeRe(alias);
|
|
383
|
+
if (canonical === "generateKeyPairSync" || canonical === "generateKeyPair") {
|
|
384
|
+
const re = new RegExp(
|
|
385
|
+
`\\b${a}\\s*\\(\\s*['"\`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"\`]`,
|
|
386
|
+
"g",
|
|
387
|
+
);
|
|
388
|
+
eachMatch(re, content, (m) =>
|
|
389
|
+
pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length),
|
|
390
|
+
);
|
|
391
|
+
} else if (canonical === "createECDH") {
|
|
392
|
+
eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) =>
|
|
393
|
+
findings.push(
|
|
394
|
+
findingFromRule(RULE_NODE_ECDH, {
|
|
395
|
+
file,
|
|
396
|
+
content,
|
|
397
|
+
index: m.index,
|
|
398
|
+
matchLength: m[0].length,
|
|
399
|
+
}),
|
|
400
|
+
),
|
|
401
|
+
);
|
|
402
|
+
} else {
|
|
403
|
+
// createDiffieHellman / createDiffieHellmanGroup.
|
|
404
|
+
eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) =>
|
|
405
|
+
findings.push(
|
|
406
|
+
findingFromRule(RULE_NODE_DH, {
|
|
407
|
+
file,
|
|
408
|
+
content,
|
|
409
|
+
index: m.index,
|
|
410
|
+
matchLength: m[0].length,
|
|
411
|
+
}),
|
|
412
|
+
),
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
309
418
|
// createSign / createVerify — RSA / ECDSA / DSA signatures.
|
|
310
419
|
eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
|
|
311
420
|
findings.push(
|
|
@@ -611,7 +720,27 @@ const libraryDetector: Detector = {
|
|
|
611
720
|
add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
|
|
612
721
|
add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
|
|
613
722
|
add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
|
|
614
|
-
|
|
723
|
+
// secp256k1: classify per method — getSharedSecret/ecdh are key AGREEMENT
|
|
724
|
+
// (ECDH, harvest-now-decrypt-later); sign/verify/getPublicKey/recover are
|
|
725
|
+
// ECDSA signatures (not HNDL). The old rule flagged everything as ECDSA.
|
|
726
|
+
eachMatch(RE_SECP256K1, content, (m) => {
|
|
727
|
+
const kex = m[1] === "getSharedSecret" || m[1] === "ecdh";
|
|
728
|
+
findings.push(
|
|
729
|
+
findingFromRule(
|
|
730
|
+
RULE_SECP256K1,
|
|
731
|
+
{ file, content, index: m.index, matchLength: m[0].length },
|
|
732
|
+
kex
|
|
733
|
+
? {
|
|
734
|
+
title: "secp256k1 ECDH key agreement",
|
|
735
|
+
category: "key-exchange",
|
|
736
|
+
algorithm: "ECDH",
|
|
737
|
+
hndl: true,
|
|
738
|
+
message: `secp256k1 ECDH key agreement (.${m[1]}()) is classical and harvest-now-decrypt-later exposed.`,
|
|
739
|
+
}
|
|
740
|
+
: undefined,
|
|
741
|
+
),
|
|
742
|
+
);
|
|
743
|
+
});
|
|
615
744
|
add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
|
|
616
745
|
add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
|
|
617
746
|
add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
|
|
@@ -852,6 +981,41 @@ const RE_CERT_SIG_ALG =
|
|
|
852
981
|
const RE_SSH_KEX =
|
|
853
982
|
/\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
983
|
|
|
984
|
+
// A bare key-type token (`ssh-rsa`, `ssh-ed25519`, …) is real SSH crypto only
|
|
985
|
+
// when it is EITHER (a) followed by base64 key material — an actual
|
|
986
|
+
// authorized_keys / known_hosts entry — OR (b) one of ≥2 DISTINCT ssh key/host-key
|
|
987
|
+
// algorithm tokens on the same line — an algorithm-preference list
|
|
988
|
+
// (`HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256,ssh-rsa`, `%w[…]`, …). A
|
|
989
|
+
// lone token, e.g. a UI label / i18n value like `"ssh-rsa": "ssh-rsa"`, is
|
|
990
|
+
// neither, so it must NOT be flagged. `RE_SSH_ALGO_TOKEN` is a separate (cloned-
|
|
991
|
+
// on-use) copy so counting per line never disturbs the outer `eachMatch` state.
|
|
992
|
+
const RE_SSH_ALGO_TOKEN = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
|
|
993
|
+
/** How far around a token we look for a sibling algorithm on the same line. A
|
|
994
|
+
* real `HostKeyAlgorithms` list is short; bounding the window keeps the per-token
|
|
995
|
+
* work O(1) so a pathological single huge line can't make detection quadratic. */
|
|
996
|
+
const SSH_LINE_WINDOW = 512;
|
|
997
|
+
function isRealSshKeyOrAlgoList(content: string, index: number, matchLen: number): boolean {
|
|
998
|
+
// (a) base64 key material immediately after the token → a real SSH public key.
|
|
999
|
+
if (/^\s+[A-Za-z0-9+/]{20,}/.test(content.slice(index + matchLen, index + matchLen + 80))) {
|
|
1000
|
+
return true;
|
|
1001
|
+
}
|
|
1002
|
+
// (b) ≥2 distinct ssh key/host-key algorithm tokens on the same line → a list.
|
|
1003
|
+
// Bound both the back-scan for the line start and the window itself so the work
|
|
1004
|
+
// is constant per token and never crosses a line boundary (i18n entries live on
|
|
1005
|
+
// separate lines, so a lone repeated token stays a single distinct token).
|
|
1006
|
+
const from = Math.max(0, index - SSH_LINE_WINDOW);
|
|
1007
|
+
const pre = content.slice(from, index);
|
|
1008
|
+
const preNl = pre.lastIndexOf("\n");
|
|
1009
|
+
const lineStart = preNl === -1 ? from : from + preNl + 1;
|
|
1010
|
+
const to = Math.min(content.length, index + matchLen + SSH_LINE_WINDOW);
|
|
1011
|
+
const post = content.slice(index, to);
|
|
1012
|
+
const postNl = post.indexOf("\n");
|
|
1013
|
+
const lineEnd = postNl === -1 ? to : index + postNl;
|
|
1014
|
+
const distinct = new Set<string>();
|
|
1015
|
+
for (const t of content.slice(lineStart, lineEnd).matchAll(RE_SSH_ALGO_TOKEN)) distinct.add(t[1]);
|
|
1016
|
+
return distinct.size >= 2;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
855
1019
|
const RULE_SSH_PUBKEY: RuleMeta = {
|
|
856
1020
|
id: "ssh-public-key",
|
|
857
1021
|
title: "Classical SSH public key",
|
|
@@ -917,6 +1081,9 @@ const sshCertDetector: Detector = {
|
|
|
917
1081
|
|
|
918
1082
|
// SSH public keys: ssh-rsa AAAA…, ecdsa-sha2-nistp256 …, ssh-ed25519 …
|
|
919
1083
|
eachMatch(RE_SSH_PUBKEY, content, (m) => {
|
|
1084
|
+
// Skip a bare token that is neither a real key nor an algorithm list (e.g.
|
|
1085
|
+
// a `"ssh-rsa": "ssh-rsa"` i18n label) — see `isRealSshKeyOrAlgoList`.
|
|
1086
|
+
if (!isRealSshKeyOrAlgoList(content, m.index, m[0].length)) return;
|
|
920
1087
|
const tok = m[1];
|
|
921
1088
|
const algorithm: Finding["algorithm"] = tok.startsWith("ssh-rsa")
|
|
922
1089
|
? "RSA"
|
|
@@ -39,8 +39,9 @@ interface HbsRule {
|
|
|
39
39
|
|
|
40
40
|
const HBS_RULES: HbsRule[] = [
|
|
41
41
|
{
|
|
42
|
-
// LMS parameter set, e.g. LMS_SHA256_M32_H10
|
|
43
|
-
|
|
42
|
+
// LMS parameter set, e.g. LMS_SHA256_M32_H10 / LMS_SHAKE_M24_H10 (SP 800-208
|
|
43
|
+
// adds SHAKE256 and the 192-bit M24/N24 sets to RFC 8554's SHA-256 sets).
|
|
44
|
+
re: /\bLMS_(?:SHA256|SHAKE(?:256)?)_[MN]\d+_[HW]\d+\b/g,
|
|
44
45
|
meta: {
|
|
45
46
|
id: "stateful-hbs-lms-param",
|
|
46
47
|
title: "LMS parameter set (stateful hash-based signature)",
|
|
@@ -93,8 +94,9 @@ const HBS_RULES: HbsRule[] = [
|
|
|
93
94
|
},
|
|
94
95
|
},
|
|
95
96
|
{
|
|
96
|
-
// XMSS parameter set, e.g. XMSS-SHA2_10_256
|
|
97
|
-
|
|
97
|
+
// XMSS parameter set, e.g. XMSS-SHA2_10_256 / XMSS-SHAKE256_10_192 (SP 800-208
|
|
98
|
+
// adds the SHAKE256 and 192-bit variants to RFC 8391's SHA-2/256 sets).
|
|
99
|
+
re: /\bXMSS-(?:SHA2|SHAKE(?:256)?)_\d+_(?:192|256)\b/g,
|
|
98
100
|
meta: {
|
|
99
101
|
id: "stateful-hbs-xmss-param",
|
|
100
102
|
title: "XMSS parameter set (stateful hash-based signature)",
|
|
@@ -111,8 +113,9 @@ const HBS_RULES: HbsRule[] = [
|
|
|
111
113
|
},
|
|
112
114
|
},
|
|
113
115
|
{
|
|
114
|
-
// XMSSMT (multi-tree XMSS) parameter set, e.g. XMSSMT-SHA2_20/2_256
|
|
115
|
-
|
|
116
|
+
// XMSSMT (multi-tree XMSS) parameter set, e.g. XMSSMT-SHA2_20/2_256 or the
|
|
117
|
+
// SP 800-208 SHAKE256 variant XMSSMT-SHAKE256_20/2_256.
|
|
118
|
+
re: /\bXMSSMT-(?:SHA2|SHAKE(?:256)?)_\d+\b/g,
|
|
116
119
|
meta: {
|
|
117
120
|
id: "stateful-hbs-xmssmt-param",
|
|
118
121
|
title: "XMSSMT parameter set (stateful hash-based signature)",
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IaC detector: classical asymmetric cryptography provisioned by Terraform /
|
|
3
|
+
* OpenTofu (`.tf`, `.tf.json`). Infrastructure-as-code mints real keys and CMKs
|
|
4
|
+
* that never appear in application source, so this is a distinct surface from the
|
|
5
|
+
* language packs.
|
|
6
|
+
*
|
|
7
|
+
* Covered:
|
|
8
|
+
* - hashicorp/tls `tls_private_key`: `algorithm = "RSA" | "ECDSA"`
|
|
9
|
+
* - Google Cloud KMS `version_template`: `algorithm = "RSA_SIGN_…" | "EC_SIGN_…"`
|
|
10
|
+
* - AWS KMS `aws_kms_key`: `customer_master_key_spec = "RSA_…" | "ECC_…"`
|
|
11
|
+
* - Azure `azurerm_key_vault_key`: `key_type = "RSA" | "EC"` (+ HSM variants)
|
|
12
|
+
*
|
|
13
|
+
* The HCL assignment forms (`algorithm = "…"`, `customer_master_key_spec = "…"`,
|
|
14
|
+
* `key_type = "…"`) are specific enough inside `.tf` files that the false-positive
|
|
15
|
+
* risk is low. EC keys are classified conservatively as key-agreement-capable
|
|
16
|
+
* (hndl:true) since a provisioned EC key can feed ECDH.
|
|
17
|
+
*/
|
|
18
|
+
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
19
|
+
import { eachMatch, findingFromRule, hasExtension } from "../detect-utils.js";
|
|
20
|
+
import { CWE_BROKEN_CRYPTO } from "../cwe.js";
|
|
21
|
+
|
|
22
|
+
const TF_EXTENSIONS: readonly string[] = [".tf", ".tf.json"];
|
|
23
|
+
|
|
24
|
+
// Each attribute is matched in BOTH HCL (`algorithm = "RSA"`) and the `.tf.json`
|
|
25
|
+
// variant (`"algorithm": "RSA"`): an optional quote around the key, and `:` or
|
|
26
|
+
// `=` as the separator. The `(?<![\w"-])` lookbehind stops a longer attribute
|
|
27
|
+
// name (`my_algorithm`) from matching on its suffix.
|
|
28
|
+
// RSA: tls_private_key `"RSA"` plus the Google KMS `RSA_SIGN_…` / `RSA_DECRYPT_…`.
|
|
29
|
+
const RE_TF_RSA = /(?<![\w"-])"?algorithm"?\s*[:=]\s*"RSA(?:_[A-Z0-9_]+)?"/g;
|
|
30
|
+
// ECDSA / EC signing: tls_private_key `"ECDSA"` and Google KMS `"EC_SIGN_…"`.
|
|
31
|
+
const RE_TF_ECDSA = /(?<![\w"-])"?algorithm"?\s*[:=]\s*"(?:ECDSA|EC_SIGN_[A-Z0-9_]+)"/g;
|
|
32
|
+
// AWS KMS customer master key specs.
|
|
33
|
+
const RE_TF_KMS_RSA = /(?<![\w"-])"?customer_master_key_spec"?\s*[:=]\s*"RSA_\d+"/g;
|
|
34
|
+
const RE_TF_KMS_EC = /(?<![\w"-])"?customer_master_key_spec"?\s*[:=]\s*"ECC_[A-Z0-9_]+"/g;
|
|
35
|
+
// Azure Key Vault key type (incl. the `-HSM` variants).
|
|
36
|
+
const RE_TF_AZ_RSA = /(?<![\w"-])"?key_type"?\s*[:=]\s*"RSA(?:-HSM)?"/g;
|
|
37
|
+
const RE_TF_AZ_EC = /(?<![\w"-])"?key_type"?\s*[:=]\s*"EC(?:-HSM)?"/g;
|
|
38
|
+
|
|
39
|
+
const RULE_TF_RSA: RuleMeta = {
|
|
40
|
+
id: "tf-rsa-key",
|
|
41
|
+
title: "Terraform RSA key",
|
|
42
|
+
description: "Terraform tls_private_key / KMS RSA key material",
|
|
43
|
+
category: "kem",
|
|
44
|
+
severity: "high",
|
|
45
|
+
confidence: "high",
|
|
46
|
+
algorithm: "RSA",
|
|
47
|
+
hndl: true,
|
|
48
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
49
|
+
message: "Terraform provisions a classical RSA key, which is not quantum-safe.",
|
|
50
|
+
remediation: "Plan migration to PQC (ML-KEM-768 for encryption, ML-DSA-65 for signatures).",
|
|
51
|
+
};
|
|
52
|
+
const RULE_TF_ECDSA: RuleMeta = {
|
|
53
|
+
id: "tf-ecdsa-key",
|
|
54
|
+
title: "Terraform ECDSA key",
|
|
55
|
+
description: "Terraform tls_private_key / KMS EC signing key",
|
|
56
|
+
category: "signature",
|
|
57
|
+
severity: "high",
|
|
58
|
+
confidence: "high",
|
|
59
|
+
algorithm: "ECDSA",
|
|
60
|
+
hndl: false,
|
|
61
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
62
|
+
message: "Terraform provisions a classical ECDSA key, forgeable by a quantum attacker.",
|
|
63
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205).",
|
|
64
|
+
};
|
|
65
|
+
const RULE_TF_KMS_RSA: RuleMeta = {
|
|
66
|
+
id: "tf-kms-rsa",
|
|
67
|
+
title: "Terraform AWS KMS RSA CMK",
|
|
68
|
+
description: 'Terraform aws_kms_key customer_master_key_spec = "RSA_*"',
|
|
69
|
+
category: "kem",
|
|
70
|
+
severity: "high",
|
|
71
|
+
confidence: "high",
|
|
72
|
+
algorithm: "RSA",
|
|
73
|
+
hndl: true,
|
|
74
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
75
|
+
message:
|
|
76
|
+
"Terraform provisions a classical RSA KMS customer master key (harvest-now-decrypt-later exposed for encryption CMKs).",
|
|
77
|
+
remediation: "Plan migration to PQC as cloud KMS adds ML-KEM / ML-DSA key specs.",
|
|
78
|
+
};
|
|
79
|
+
const RULE_TF_KMS_EC: RuleMeta = {
|
|
80
|
+
id: "tf-kms-ec",
|
|
81
|
+
title: "Terraform AWS KMS EC CMK",
|
|
82
|
+
description: 'Terraform aws_kms_key customer_master_key_spec = "ECC_*"',
|
|
83
|
+
category: "key-exchange",
|
|
84
|
+
severity: "high",
|
|
85
|
+
confidence: "high",
|
|
86
|
+
algorithm: "ECDH",
|
|
87
|
+
hndl: true,
|
|
88
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
89
|
+
message:
|
|
90
|
+
"Terraform provisions a classical EC KMS customer master key; EC keys feed ECDSA signatures and ECDH key agreement (the ECDH path is harvest-now-decrypt-later exposed).",
|
|
91
|
+
remediation:
|
|
92
|
+
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
93
|
+
};
|
|
94
|
+
const RULE_TF_AZ_RSA: RuleMeta = {
|
|
95
|
+
id: "tf-keyvault-rsa",
|
|
96
|
+
title: "Terraform Azure Key Vault RSA key",
|
|
97
|
+
description: 'Terraform azurerm_key_vault_key key_type = "RSA"',
|
|
98
|
+
category: "kem",
|
|
99
|
+
severity: "high",
|
|
100
|
+
confidence: "high",
|
|
101
|
+
algorithm: "RSA",
|
|
102
|
+
hndl: true,
|
|
103
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
104
|
+
message: "Terraform provisions a classical RSA Azure Key Vault key, which is not quantum-safe.",
|
|
105
|
+
remediation: "Plan migration to PQC (ML-KEM-768 / ML-DSA-65).",
|
|
106
|
+
};
|
|
107
|
+
const RULE_TF_AZ_EC: RuleMeta = {
|
|
108
|
+
id: "tf-keyvault-ec",
|
|
109
|
+
title: "Terraform Azure Key Vault EC key",
|
|
110
|
+
description: 'Terraform azurerm_key_vault_key key_type = "EC"',
|
|
111
|
+
category: "key-exchange",
|
|
112
|
+
severity: "high",
|
|
113
|
+
confidence: "high",
|
|
114
|
+
algorithm: "ECDH",
|
|
115
|
+
hndl: true,
|
|
116
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
117
|
+
message:
|
|
118
|
+
"Terraform provisions a classical EC Azure Key Vault key; EC keys feed ECDSA signatures and ECDH key agreement (the ECDH path is harvest-now-decrypt-later exposed).",
|
|
119
|
+
remediation:
|
|
120
|
+
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** Detects classical asymmetric crypto provisioned in Terraform / OpenTofu. */
|
|
124
|
+
export const terraformDetector: Detector = {
|
|
125
|
+
id: "terraform-crypto",
|
|
126
|
+
description: "Classical asymmetric crypto provisioned by Terraform / OpenTofu (IaC)",
|
|
127
|
+
scope: "config",
|
|
128
|
+
language: "any",
|
|
129
|
+
rules: [
|
|
130
|
+
RULE_TF_RSA,
|
|
131
|
+
RULE_TF_ECDSA,
|
|
132
|
+
RULE_TF_KMS_RSA,
|
|
133
|
+
RULE_TF_KMS_EC,
|
|
134
|
+
RULE_TF_AZ_RSA,
|
|
135
|
+
RULE_TF_AZ_EC,
|
|
136
|
+
],
|
|
137
|
+
appliesTo: (f) => hasExtension(f, TF_EXTENSIONS),
|
|
138
|
+
detect({ file, content }): Finding[] {
|
|
139
|
+
const findings: Finding[] = [];
|
|
140
|
+
const add = (re: RegExp, rule: RuleMeta) =>
|
|
141
|
+
eachMatch(re, content, (m) =>
|
|
142
|
+
findings.push(
|
|
143
|
+
findingFromRule(rule, { file, content, index: m.index, matchLength: m[0].length }),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
add(RE_TF_RSA, RULE_TF_RSA);
|
|
147
|
+
add(RE_TF_ECDSA, RULE_TF_ECDSA);
|
|
148
|
+
add(RE_TF_KMS_RSA, RULE_TF_KMS_RSA);
|
|
149
|
+
add(RE_TF_KMS_EC, RULE_TF_KMS_EC);
|
|
150
|
+
add(RE_TF_AZ_RSA, RULE_TF_AZ_RSA);
|
|
151
|
+
add(RE_TF_AZ_EC, RULE_TF_AZ_EC);
|
|
152
|
+
return findings;
|
|
153
|
+
},
|
|
154
|
+
};
|