@quantakrypto/core 0.4.2 → 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/LICENSE +201 -0
- package/README.md +38 -10
- package/dist/agent-types.js.map +1 -1
- package/dist/baseline.js.map +1 -1
- package/dist/cache.js.map +1 -1
- 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/cbom.js.map +1 -1
- package/dist/changed.js.map +1 -1
- package/dist/codemods/config-toggle.js.map +1 -1
- package/dist/codemods/registry.js.map +1 -1
- package/dist/comments.d.ts +29 -2
- package/dist/comments.d.ts.map +1 -1
- package/dist/comments.js +178 -18
- package/dist/comments.js.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/cwe.js.map +1 -1
- package/dist/dependencies.d.ts +3 -2
- package/dist/dependencies.d.ts.map +1 -1
- package/dist/dependencies.js +138 -2
- package/dist/dependencies.js.map +1 -1
- package/dist/detect-utils.d.ts +19 -4
- package/dist/detect-utils.d.ts.map +1 -1
- package/dist/detect-utils.js +43 -6
- 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 +382 -3
- 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/csharp.d.ts.map +1 -1
- package/dist/detectors/csharp.js +141 -3
- package/dist/detectors/csharp.js.map +1 -1
- 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/go.d.ts +5 -3
- package/dist/detectors/go.d.ts.map +1 -1
- package/dist/detectors/go.js +187 -3
- package/dist/detectors/go.js.map +1 -1
- package/dist/detectors/java.d.ts.map +1 -1
- package/dist/detectors/java.js +169 -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 +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.d.ts.map +1 -1
- package/dist/detectors/pem.js +50 -2
- 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 +144 -3
- package/dist/detectors/python.js.map +1 -1
- package/dist/detectors/ruby.d.ts +9 -5
- package/dist/detectors/ruby.d.ts.map +1 -1
- package/dist/detectors/ruby.js +122 -2
- package/dist/detectors/ruby.js.map +1 -1
- package/dist/detectors/rust.d.ts +8 -3
- package/dist/detectors/rust.d.ts.map +1 -1
- package/dist/detectors/rust.js +253 -3
- 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 +305 -52
- package/dist/detectors/source.js.map +1 -1
- package/dist/detectors/stateful-hbs.d.ts +22 -0
- package/dist/detectors/stateful-hbs.d.ts.map +1 -0
- package/dist/detectors/stateful-hbs.js +142 -0
- package/dist/detectors/stateful-hbs.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 +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/errors.js.map +1 -1
- package/dist/evidence.d.ts +53 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +81 -0
- package/dist/evidence.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/inventory.d.ts +2 -0
- package/dist/inventory.d.ts.map +1 -1
- package/dist/inventory.js +27 -1
- package/dist/inventory.js.map +1 -1
- package/dist/parallel.js.map +1 -1
- package/dist/patch-policy.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/redact.d.ts.map +1 -1
- package/dist/redact.js +39 -0
- package/dist/redact.js.map +1 -1
- package/dist/registry.d.ts +2 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +34 -2
- package/dist/registry.js.map +1 -1
- package/dist/remediate-pipeline.d.ts +3 -0
- package/dist/remediate-pipeline.d.ts.map +1 -1
- package/dist/remediate-pipeline.js +71 -0
- package/dist/remediate-pipeline.js.map +1 -1
- package/dist/remediate-request.js.map +1 -1
- package/dist/remediation.d.ts +9 -3
- package/dist/remediation.d.ts.map +1 -1
- package/dist/remediation.js +17 -5
- 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 -0
- package/dist/report.js.map +1 -1
- package/dist/scan-worker.js.map +1 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +8 -1
- package/dist/scan.js.map +1 -1
- package/dist/severity.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/triage.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/verify.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/walk.js.map +1 -1
- package/dist/worktree.js.map +1 -1
- package/package.json +1 -1
- package/src/cbom-merge.ts +116 -0
- package/src/comments.ts +183 -18
- package/src/dependencies.ts +144 -4
- package/src/detect-utils.ts +47 -6
- package/src/detectors/c.ts +411 -5
- 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/csharp.ts +154 -3
- package/src/detectors/database.ts +88 -0
- package/src/detectors/dnssec.ts +186 -0
- package/src/detectors/elixir.ts +237 -0
- package/src/detectors/go.ts +209 -6
- package/src/detectors/java.ts +191 -4
- 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 +54 -2
- package/src/detectors/php.ts +225 -0
- package/src/detectors/python.ts +153 -3
- package/src/detectors/ruby.ts +138 -7
- package/src/detectors/rust.ts +282 -7
- package/src/detectors/secrets.ts +114 -0
- package/src/detectors/source.ts +382 -72
- package/src/detectors/stateful-hbs.ts +180 -0
- package/src/detectors/terraform.ts +154 -0
- package/src/detectors/vpn.ts +131 -0
- package/src/evidence.ts +135 -0
- package/src/index.ts +15 -1
- package/src/inventory.ts +31 -1
- package/src/policy.ts +180 -0
- package/src/redact.ts +41 -0
- package/src/registry.ts +34 -2
- package/src/remediate-pipeline.ts +78 -0
- package/src/remediation.ts +23 -8
- package/src/report.ts +48 -2
- package/src/scan.ts +13 -1
- package/src/standards.ts +186 -0
- package/src/types.ts +3 -1
- package/src/version.ts +1 -1
package/src/detectors/source.ts
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
import type { Detector, Finding, RuleMeta } from "../types.js";
|
|
30
30
|
import {
|
|
31
31
|
JS_TS_EXTENSIONS,
|
|
32
|
+
DOC_EXTENSIONS,
|
|
32
33
|
JWT_HOST_EXTENSIONS,
|
|
33
34
|
eachMatch,
|
|
34
35
|
findingFromRule,
|
|
@@ -45,6 +46,135 @@ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cw
|
|
|
45
46
|
// (ordered alternation would otherwise match `rsa` and reject the `-pss` tail).
|
|
46
47
|
const RE_GENERATE_KEYPAIR =
|
|
47
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
|
+
}
|
|
48
178
|
const RE_CREATE_SIGN_VERIFY = /create(?:Sign|Verify)\s*\(/g;
|
|
49
179
|
// One-shot crypto.sign/verify(algorithm, data, key). A LOOKBEHIND (not a
|
|
50
180
|
// consumed char) anchors it so it doesn't fire inside identifiers like `assign(`
|
|
@@ -81,7 +211,7 @@ const RE_JSRSASIGN_SIGN = /KJUR\.crypto\.(?:Signature|ECDSA)\b/g;
|
|
|
81
211
|
const RE_NODE_RSA = /new\s+NodeRSA\s*\(/g;
|
|
82
212
|
// secp256k1 — direct @noble/secp256k1 / secp256k1-style API usage in source.
|
|
83
213
|
const RE_SECP256K1 =
|
|
84
|
-
/\b(?:secp(?:256k1)?|secp)\s*\.\s*(
|
|
214
|
+
/\b(?:secp(?:256k1)?|secp)\s*\.\s*(sign|verify|getPublicKey|getSharedSecret|ecdh|recoverPublicKey)\s*\(/g;
|
|
85
215
|
|
|
86
216
|
// JWT/JOSE.
|
|
87
217
|
const RE_JWT_ALG = /['"`](RS(?:256|384|512)|PS(?:256|384|512)|ES(?:256|384|512|256K)|EdDSA)['"`]/g;
|
|
@@ -237,74 +367,54 @@ const nodeCryptoDetector: Detector = {
|
|
|
237
367
|
|
|
238
368
|
// generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
|
|
239
369
|
eachMatch(RE_GENERATE_KEYPAIR, content, (m) => {
|
|
240
|
-
|
|
241
|
-
const map: Record<
|
|
242
|
-
string,
|
|
243
|
-
{
|
|
244
|
-
algo: Finding["algorithm"];
|
|
245
|
-
cat: Finding["category"];
|
|
246
|
-
sev: Finding["severity"];
|
|
247
|
-
hndl: boolean;
|
|
248
|
-
label: string;
|
|
249
|
-
message?: string;
|
|
250
|
-
remediation?: string;
|
|
251
|
-
}
|
|
252
|
-
> = {
|
|
253
|
-
rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
|
|
254
|
-
// RSA-PSS is signature-only, so classify it as a (forgeable) signature
|
|
255
|
-
// rather than a KEM — no HNDL confidentiality exposure.
|
|
256
|
-
"rsa-pss": {
|
|
257
|
-
algo: "RSA",
|
|
258
|
-
cat: "signature",
|
|
259
|
-
sev: "high",
|
|
260
|
-
hndl: false,
|
|
261
|
-
label: "RSA-PSS",
|
|
262
|
-
message:
|
|
263
|
-
"Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
|
|
264
|
-
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
|
|
265
|
-
},
|
|
266
|
-
// EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
|
|
267
|
-
// HNDL-exposed, so classify conservatively as key-exchange-capable and
|
|
268
|
-
// surface both concerns rather than asserting signature-only (P0-4).
|
|
269
|
-
ec: {
|
|
270
|
-
algo: "ECDH",
|
|
271
|
-
cat: "key-exchange",
|
|
272
|
-
sev: "high",
|
|
273
|
-
hndl: true,
|
|
274
|
-
label: "EC (ECDSA/ECDH)",
|
|
275
|
-
message:
|
|
276
|
-
"Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
|
|
277
|
-
"and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
|
|
278
|
-
remediation:
|
|
279
|
-
"For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
280
|
-
},
|
|
281
|
-
dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
|
|
282
|
-
dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
|
|
283
|
-
x25519: { algo: "X25519", cat: "key-exchange", sev: "low", hndl: true, label: "X25519" },
|
|
284
|
-
x448: { algo: "X448", cat: "key-exchange", sev: "low", hndl: true, label: "X448" },
|
|
285
|
-
ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
|
|
286
|
-
ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
|
|
287
|
-
};
|
|
288
|
-
const info = map[type];
|
|
289
|
-
findings.push(
|
|
290
|
-
findingFromRule(
|
|
291
|
-
RULE_NODE_KEYGEN,
|
|
292
|
-
{ file, content, index: m.index, matchLength: m[0].length },
|
|
293
|
-
{
|
|
294
|
-
title: `${info.label} key generation`,
|
|
295
|
-
category: info.cat,
|
|
296
|
-
severity: info.sev,
|
|
297
|
-
algorithm: info.algo,
|
|
298
|
-
hndl: info.hndl,
|
|
299
|
-
message:
|
|
300
|
-
info.message ??
|
|
301
|
-
`Generates a classical ${info.label} key pair, which is not quantum-safe.`,
|
|
302
|
-
...(info.remediation ? { remediation: info.remediation } : {}),
|
|
303
|
-
},
|
|
304
|
-
),
|
|
305
|
-
);
|
|
370
|
+
pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
|
|
306
371
|
});
|
|
307
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
|
+
|
|
308
418
|
// createSign / createVerify — RSA / ECDSA / DSA signatures.
|
|
309
419
|
eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
|
|
310
420
|
findings.push(
|
|
@@ -610,7 +720,27 @@ const libraryDetector: Detector = {
|
|
|
610
720
|
add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
|
|
611
721
|
add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
|
|
612
722
|
add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
|
|
613
|
-
|
|
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
|
+
});
|
|
614
744
|
add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
|
|
615
745
|
add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
|
|
616
746
|
add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
|
|
@@ -623,6 +753,11 @@ const libraryDetector: Detector = {
|
|
|
623
753
|
/* JWT / JOSE / COSE algorithm strings */
|
|
624
754
|
/* -------------------------------------------------------------------------- */
|
|
625
755
|
|
|
756
|
+
// JOSE RSA key-transport algorithms (JWE `alg`): quoted RSA-OAEP / RSA-OAEP-256
|
|
757
|
+
// /384/512 / RSA1_5. Classical RSA *encryption* (key transport) — distinct from
|
|
758
|
+
// the RS*/PS* *signature* tokens above, and a harvest-now-decrypt-later surface.
|
|
759
|
+
const RE_JOSE_KEM = /['"`](RSA-OAEP(?:-(?:256|384|512))?|RSA1_5)['"`]/g;
|
|
760
|
+
|
|
626
761
|
const RULE_JWT_ALG: RuleMeta = {
|
|
627
762
|
id: "jwt-classical-alg",
|
|
628
763
|
title: "Classical JWT/JOSE algorithm",
|
|
@@ -650,6 +785,20 @@ const RULE_JOSE_ECDH: RuleMeta = {
|
|
|
650
785
|
"JOSE ECDH-ES performs classical ECDH key agreement — harvest-now-decrypt-later exposed.",
|
|
651
786
|
remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
|
|
652
787
|
};
|
|
788
|
+
const RULE_JOSE_RSA_OAEP: RuleMeta = {
|
|
789
|
+
id: "jose-rsa-oaep",
|
|
790
|
+
title: "JOSE RSA key-transport algorithm",
|
|
791
|
+
description: "JWE RSA-OAEP / RSA-OAEP-256/384/512 / RSA1_5 key encryption",
|
|
792
|
+
category: "key-exchange",
|
|
793
|
+
severity: "high",
|
|
794
|
+
confidence: "medium",
|
|
795
|
+
algorithm: "RSA",
|
|
796
|
+
hndl: true,
|
|
797
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
798
|
+
message:
|
|
799
|
+
"JOSE RSA key transport (RSA-OAEP / RSA1_5) is classical RSA encryption — harvest-now-decrypt-later exposed.",
|
|
800
|
+
remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
|
|
801
|
+
};
|
|
653
802
|
|
|
654
803
|
/**
|
|
655
804
|
* Detects classical signature algorithm identifiers used by JWT/JOSE, plus
|
|
@@ -664,7 +813,7 @@ const jwtDetector: Detector = {
|
|
|
664
813
|
// signal in JS/TS or Python (e.g. PyJWT `algorithm="RS256"`), so this detector
|
|
665
814
|
// is un-gated from JS-only to the JWT host surfaces.
|
|
666
815
|
language: "any",
|
|
667
|
-
rules: [RULE_JWT_ALG, RULE_JOSE_ECDH],
|
|
816
|
+
rules: [RULE_JWT_ALG, RULE_JOSE_ECDH, RULE_JOSE_RSA_OAEP],
|
|
668
817
|
appliesTo: (f) => hasExtension(f, JWT_HOST_EXTENSIONS),
|
|
669
818
|
detect({ file, content }): Finding[] {
|
|
670
819
|
const findings: Finding[] = [];
|
|
@@ -703,6 +852,20 @@ const jwtDetector: Detector = {
|
|
|
703
852
|
);
|
|
704
853
|
});
|
|
705
854
|
|
|
855
|
+
// JOSE RSA key transport (RSA-OAEP / RSA1_5) — classical RSA encryption, HNDL.
|
|
856
|
+
eachMatch(RE_JOSE_KEM, content, (m) => {
|
|
857
|
+
findings.push(
|
|
858
|
+
findingFromRule(
|
|
859
|
+
RULE_JOSE_RSA_OAEP,
|
|
860
|
+
{ file, content, index: m.index, matchLength: m[0].length },
|
|
861
|
+
{
|
|
862
|
+
title: `JOSE RSA key transport ${m[1]}`,
|
|
863
|
+
message: `JOSE "${m[1]}" is classical RSA key transport — harvest-now-decrypt-later exposed.`,
|
|
864
|
+
},
|
|
865
|
+
),
|
|
866
|
+
);
|
|
867
|
+
});
|
|
868
|
+
|
|
706
869
|
return findings;
|
|
707
870
|
},
|
|
708
871
|
};
|
|
@@ -810,6 +973,48 @@ const tlsDetector: Detector = {
|
|
|
810
973
|
const RE_SSH_PUBKEY = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
|
|
811
974
|
const RE_CERT_SIG_ALG =
|
|
812
975
|
/\b(sha(?:1|256|384|512)WithRSAEncryption|ecdsa-with-SHA(?:1|256|384|512)|rsassaPss|dsaWithSHA(?:1|256))\b/g;
|
|
976
|
+
// Classical SSH key-exchange algorithm identifiers: finite-field DH groups
|
|
977
|
+
// (diffie-hellman-group{1,14,15,16,17,18} + group-exchange), ECDH over NIST
|
|
978
|
+
// curves (ecdh-sha2-nistp*), and Curve25519 (curve25519-sha256). All are
|
|
979
|
+
// Shor-broken key agreement — the harvest-now-decrypt-later surface a lexical
|
|
980
|
+
// scan misses when only the `crypto/*` API is matched, not the negotiated kex.
|
|
981
|
+
const RE_SSH_KEX =
|
|
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;
|
|
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
|
+
}
|
|
813
1018
|
|
|
814
1019
|
const RULE_SSH_PUBKEY: RuleMeta = {
|
|
815
1020
|
id: "ssh-public-key",
|
|
@@ -823,7 +1028,8 @@ const RULE_SSH_PUBKEY: RuleMeta = {
|
|
|
823
1028
|
cwe: CWE_BROKEN_CRYPTO,
|
|
824
1029
|
sensitive: true,
|
|
825
1030
|
message: "A classical SSH public key is forgeable by a quantum attacker.",
|
|
826
|
-
remediation:
|
|
1031
|
+
remediation:
|
|
1032
|
+
"Plan migration to PQC-capable SSH: prefer the mlkem768x25519-sha256 KEX (ML-KEM-768 hybrid, OpenSSH 10's default since Apr 2025); sntrup761x25519 is an acceptable interim. Rotate to PQC host keys as they land.",
|
|
827
1033
|
};
|
|
828
1034
|
const RULE_CERT_SIG_ALG: RuleMeta = {
|
|
829
1035
|
id: "cert-signature-algorithm",
|
|
@@ -839,6 +1045,21 @@ const RULE_CERT_SIG_ALG: RuleMeta = {
|
|
|
839
1045
|
"A classical certificate signature algorithm (RSA/ECDSA/DSA) is a quantum forgery surface.",
|
|
840
1046
|
remediation: "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
|
|
841
1047
|
};
|
|
1048
|
+
const RULE_SSH_KEX: RuleMeta = {
|
|
1049
|
+
id: "ssh-kex-classical",
|
|
1050
|
+
title: "Classical SSH key exchange",
|
|
1051
|
+
description: "diffie-hellman-group* / group-exchange / ecdh-sha2-* / curve25519-sha256 kex",
|
|
1052
|
+
category: "key-exchange",
|
|
1053
|
+
severity: "medium",
|
|
1054
|
+
confidence: "medium",
|
|
1055
|
+
algorithm: "unknown",
|
|
1056
|
+
hndl: true,
|
|
1057
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
1058
|
+
message:
|
|
1059
|
+
"A classical SSH key-exchange algorithm (finite-field DH / ECDH / X25519) is harvest-now-decrypt-later exposed.",
|
|
1060
|
+
remediation:
|
|
1061
|
+
"Prefer the mlkem768x25519-sha256 KEX (ML-KEM-768 hybrid, OpenSSH 10 default); sntrup761x25519 is an acceptable interim.",
|
|
1062
|
+
};
|
|
842
1063
|
|
|
843
1064
|
/**
|
|
844
1065
|
* Detects classical SSH public keys (`authorized_keys` / `known_hosts` lines)
|
|
@@ -851,13 +1072,18 @@ const sshCertDetector: Detector = {
|
|
|
851
1072
|
description: "SSH public keys and TLS/X.509 certificate signature algorithms in config",
|
|
852
1073
|
scope: "config",
|
|
853
1074
|
language: "any",
|
|
854
|
-
rules: [RULE_SSH_PUBKEY, RULE_CERT_SIG_ALG],
|
|
855
|
-
|
|
1075
|
+
rules: [RULE_SSH_PUBKEY, RULE_CERT_SIG_ALG, RULE_SSH_KEX],
|
|
1076
|
+
// Skip prose/docs: a changelog or README that merely mentions `ssh-rsa` in a
|
|
1077
|
+
// sentence is not crypto config. PEM material is caught by its own detector.
|
|
1078
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
856
1079
|
detect({ file, content }): Finding[] {
|
|
857
1080
|
const findings: Finding[] = [];
|
|
858
1081
|
|
|
859
1082
|
// SSH public keys: ssh-rsa AAAA…, ecdsa-sha2-nistp256 …, ssh-ed25519 …
|
|
860
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;
|
|
861
1087
|
const tok = m[1];
|
|
862
1088
|
const algorithm: Finding["algorithm"] = tok.startsWith("ssh-rsa")
|
|
863
1089
|
? "RSA"
|
|
@@ -900,6 +1126,89 @@ const sshCertDetector: Detector = {
|
|
|
900
1126
|
);
|
|
901
1127
|
});
|
|
902
1128
|
|
|
1129
|
+
// SSH key-exchange algorithm identifiers (finite-field DH / ECDH / X25519).
|
|
1130
|
+
eachMatch(RE_SSH_KEX, content, (m) => {
|
|
1131
|
+
const tok = m[1];
|
|
1132
|
+
const algorithm: Finding["algorithm"] = tok.startsWith("diffie-hellman")
|
|
1133
|
+
? "DH"
|
|
1134
|
+
: tok.startsWith("ecdh")
|
|
1135
|
+
? "ECDH"
|
|
1136
|
+
: "X25519";
|
|
1137
|
+
findings.push(
|
|
1138
|
+
findingFromRule(
|
|
1139
|
+
RULE_SSH_KEX,
|
|
1140
|
+
{ file, content, index: m.index, matchLength: m[0].length },
|
|
1141
|
+
{
|
|
1142
|
+
title: `Classical SSH key exchange (${tok})`,
|
|
1143
|
+
algorithm,
|
|
1144
|
+
message: `SSH key-exchange "${tok}" is classical (${algorithm}) — harvest-now-decrypt-later exposed.`,
|
|
1145
|
+
},
|
|
1146
|
+
),
|
|
1147
|
+
);
|
|
1148
|
+
});
|
|
1149
|
+
|
|
1150
|
+
return findings;
|
|
1151
|
+
},
|
|
1152
|
+
};
|
|
1153
|
+
|
|
1154
|
+
/* -------------------------------------------------------------------------- */
|
|
1155
|
+
/* Classical TLS key-exchange cipher suites (language-agnostic config) */
|
|
1156
|
+
/* -------------------------------------------------------------------------- */
|
|
1157
|
+
|
|
1158
|
+
// Classical KEX cipher-suite identifiers, in both the OpenSSL (`ECDHE-RSA-…`) and
|
|
1159
|
+
// IANA (`TLS_ECDHE_RSA_WITH_…`) spellings, with `-` or `_` separators. These name
|
|
1160
|
+
// a Shor-broken key exchange (ECDHE/DHE), so they are the harvest-now surface a
|
|
1161
|
+
// PQC scanner should flag — distinct from the legacy-*version* / weak-cipher rules
|
|
1162
|
+
// above. Case-sensitive on purpose: cipher-suite tokens are upper-case constants,
|
|
1163
|
+
// so prose like "ecdhe rsa" does not misfire. No trailing \b so the IANA
|
|
1164
|
+
// `TLS_ECDHE_RSA_WITH_…` form (a `_` follows `RSA`) still matches.
|
|
1165
|
+
const RE_TLS_CLASSICAL_KEX = /\b(?:TLS_)?(?:ECDHE|ECDH|DHE)[-_](?:RSA|ECDSA|DSS)/g;
|
|
1166
|
+
|
|
1167
|
+
const RULE_TLS_CLASSICAL_KEX: RuleMeta = {
|
|
1168
|
+
id: "tls-classical-kex",
|
|
1169
|
+
title: "Classical TLS key-exchange cipher suite",
|
|
1170
|
+
description: "ECDHE / DHE cipher suites negotiate Shor-broken key exchange",
|
|
1171
|
+
category: "tls",
|
|
1172
|
+
severity: "medium",
|
|
1173
|
+
confidence: "medium",
|
|
1174
|
+
algorithm: "unknown",
|
|
1175
|
+
hndl: true,
|
|
1176
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
1177
|
+
message:
|
|
1178
|
+
"Classical TLS key exchange (ECDHE/DHE) is harvest-now-decrypt-later exposed — the session key can be recorded now and recovered by a quantum attacker.",
|
|
1179
|
+
remediation:
|
|
1180
|
+
"Adopt a PQC-hybrid TLS 1.3 key exchange (e.g. X25519MLKEM768) as your stack and peers support it; keep classical suites only as a transitional fallback.",
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
/**
|
|
1184
|
+
* Detects classical TLS key-exchange cipher suites in any text/config file
|
|
1185
|
+
* (OpenSSL cipher lists, IANA suite constants in Go/Java/C#/Rust, `.properties`,
|
|
1186
|
+
* etc.). Language-agnostic because the suite identifiers are the same tokens
|
|
1187
|
+
* everywhere — this is the cross-language TLS gap the legacy-version rule misses.
|
|
1188
|
+
*/
|
|
1189
|
+
const tlsClassicalKexDetector: Detector = {
|
|
1190
|
+
id: "tls-cipher-suite",
|
|
1191
|
+
description: "Classical TLS key-exchange cipher suites (ECDHE/DHE) in any config",
|
|
1192
|
+
scope: "config",
|
|
1193
|
+
language: "any",
|
|
1194
|
+
rules: [RULE_TLS_CLASSICAL_KEX],
|
|
1195
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
1196
|
+
detect({ file, content }): Finding[] {
|
|
1197
|
+
const findings: Finding[] = [];
|
|
1198
|
+
eachMatch(RE_TLS_CLASSICAL_KEX, content, (m) => {
|
|
1199
|
+
const tok = m[0];
|
|
1200
|
+
const algorithm: Finding["algorithm"] = tok.includes("ECDH") ? "ECDH" : "DH";
|
|
1201
|
+
findings.push(
|
|
1202
|
+
findingFromRule(
|
|
1203
|
+
RULE_TLS_CLASSICAL_KEX,
|
|
1204
|
+
{ file, content, index: m.index, matchLength: m[0].length },
|
|
1205
|
+
{
|
|
1206
|
+
algorithm,
|
|
1207
|
+
message: `Classical TLS key-exchange suite "${tok}…" (${algorithm}) is harvest-now-decrypt-later exposed.`,
|
|
1208
|
+
},
|
|
1209
|
+
),
|
|
1210
|
+
);
|
|
1211
|
+
});
|
|
903
1212
|
return findings;
|
|
904
1213
|
},
|
|
905
1214
|
};
|
|
@@ -912,4 +1221,5 @@ export const sourceDetectors: Detector[] = [
|
|
|
912
1221
|
jwtDetector,
|
|
913
1222
|
tlsDetector,
|
|
914
1223
|
sshCertDetector,
|
|
1224
|
+
tlsClassicalKexDetector,
|
|
915
1225
|
];
|