@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/dist/detectors/source.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { JS_TS_EXTENSIONS, JWT_HOST_EXTENSIONS, eachMatch, findingFromRule, hasExtension, nearSortedCall, } from "../detect-utils.js";
|
|
1
|
+
import { JS_TS_EXTENSIONS, DOC_EXTENSIONS, JWT_HOST_EXTENSIONS, eachMatch, findingFromRule, hasExtension, nearSortedCall, } from "../detect-utils.js";
|
|
2
2
|
import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cwe.js";
|
|
3
3
|
/* -------------------------------------------------------------------------- */
|
|
4
4
|
/* Precompiled regexes (module scope — never recreated per file) */
|
|
@@ -6,6 +6,107 @@ import { CWE_BROKEN_CRYPTO, CWE_CERT_VALIDATION, CWE_WEAK_STRENGTH } from "../cw
|
|
|
6
6
|
// `rsa-pss` is listed before `rsa` so the alternation consumes the full token
|
|
7
7
|
// (ordered alternation would otherwise match `rsa` and reject the `-pss` tail).
|
|
8
8
|
const RE_GENERATE_KEYPAIR = /generateKeyPair(?:Sync)?\s*\(\s*['"`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"`]/g;
|
|
9
|
+
const KEYGEN_INFO = {
|
|
10
|
+
rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
|
|
11
|
+
// RSA-PSS is signature-only, so classify it as a (forgeable) signature
|
|
12
|
+
// rather than a KEM — no HNDL confidentiality exposure.
|
|
13
|
+
"rsa-pss": {
|
|
14
|
+
algo: "RSA",
|
|
15
|
+
cat: "signature",
|
|
16
|
+
sev: "high",
|
|
17
|
+
hndl: false,
|
|
18
|
+
label: "RSA-PSS",
|
|
19
|
+
message: "Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
|
|
20
|
+
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
|
|
21
|
+
},
|
|
22
|
+
// EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
|
|
23
|
+
// HNDL-exposed, so classify conservatively as key-exchange-capable and
|
|
24
|
+
// surface both concerns rather than asserting signature-only (P0-4).
|
|
25
|
+
ec: {
|
|
26
|
+
algo: "ECDH",
|
|
27
|
+
cat: "key-exchange",
|
|
28
|
+
sev: "high",
|
|
29
|
+
hndl: true,
|
|
30
|
+
label: "EC (ECDSA/ECDH)",
|
|
31
|
+
message: "Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
|
|
32
|
+
"and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
|
|
33
|
+
remediation: "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
34
|
+
},
|
|
35
|
+
dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
|
|
36
|
+
dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
|
|
37
|
+
x25519: { algo: "X25519", cat: "key-exchange", sev: "medium", hndl: true, label: "X25519" },
|
|
38
|
+
x448: { algo: "X448", cat: "key-exchange", sev: "medium", hndl: true, label: "X448" },
|
|
39
|
+
ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
|
|
40
|
+
ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
|
|
41
|
+
};
|
|
42
|
+
/** Emit a keygen finding for `generateKeyPair(Sync)('<type>')` at [index]. Shared
|
|
43
|
+
* by the direct matcher and the import-alias pass so both classify identically. */
|
|
44
|
+
function pushKeygenFinding(findings, rawType, file, content, index, matchLength) {
|
|
45
|
+
const info = KEYGEN_INFO[rawType.toLowerCase()];
|
|
46
|
+
if (!info)
|
|
47
|
+
return;
|
|
48
|
+
findings.push(findingFromRule(RULE_NODE_KEYGEN, { file, content, index, matchLength }, {
|
|
49
|
+
title: `${info.label} key generation`,
|
|
50
|
+
category: info.cat,
|
|
51
|
+
severity: info.sev,
|
|
52
|
+
algorithm: info.algo,
|
|
53
|
+
hndl: info.hndl,
|
|
54
|
+
message: info.message ??
|
|
55
|
+
`Generates a classical ${info.label} key pair, which is not quantum-safe.`,
|
|
56
|
+
...(info.remediation ? { remediation: info.remediation } : {}),
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
/** Canonical Node-crypto call names whose import-aliases we resolve. */
|
|
60
|
+
const ALIASABLE = [
|
|
61
|
+
"generateKeyPairSync",
|
|
62
|
+
"generateKeyPair",
|
|
63
|
+
"createECDH",
|
|
64
|
+
"createDiffieHellman",
|
|
65
|
+
"createDiffieHellmanGroup",
|
|
66
|
+
];
|
|
67
|
+
/**
|
|
68
|
+
* Collect import-aliases for the canonical crypto calls we resolve, so
|
|
69
|
+
* `import { generateKeyPairSync as gk } from 'node:crypto'` (or the CommonJS
|
|
70
|
+
* `const { generateKeyPairSync: gk } = require('crypto')`) is followed when `gk`
|
|
71
|
+
* is called later. Returns Map<canonical, alias[]>. An alias equal to its own
|
|
72
|
+
* canonical name is skipped (already matched directly). Precision-safe: the
|
|
73
|
+
* alias is only ever bound to a known crypto symbol, so a later `gk(` call is
|
|
74
|
+
* unambiguously that symbol.
|
|
75
|
+
*/
|
|
76
|
+
function collectCryptoAliases(content) {
|
|
77
|
+
const out = new Map();
|
|
78
|
+
const add = (canonical, alias) => {
|
|
79
|
+
if (!alias || alias === canonical)
|
|
80
|
+
return;
|
|
81
|
+
const list = out.get(canonical) ?? [];
|
|
82
|
+
if (!list.includes(alias))
|
|
83
|
+
list.push(alias);
|
|
84
|
+
out.set(canonical, list);
|
|
85
|
+
};
|
|
86
|
+
// ESM named imports: `import { A as B, C as D } from '...'`.
|
|
87
|
+
const esm = /import\s*(?:type\s+)?\{([^}]*)\}\s*from\s*['"][^'"]*['"]/g;
|
|
88
|
+
for (let m = esm.exec(content); m; m = esm.exec(content)) {
|
|
89
|
+
const spec = /([A-Za-z_$][\w$]*)\s+as\s+([A-Za-z_$][\w$]*)/g;
|
|
90
|
+
for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
|
|
91
|
+
if (ALIASABLE.includes(s[1]))
|
|
92
|
+
add(s[1], s[2]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// CommonJS destructure-rename: `const { A: B } = require('...')`.
|
|
96
|
+
const cjs = /(?:const|let|var)\s*\{([^}]*)\}\s*=\s*require\s*\(\s*['"][^'"]*['"]\s*\)/g;
|
|
97
|
+
for (let m = cjs.exec(content); m; m = cjs.exec(content)) {
|
|
98
|
+
const spec = /([A-Za-z_$][\w$]*)\s*:\s*([A-Za-z_$][\w$]*)/g;
|
|
99
|
+
for (let s = spec.exec(m[1]); s; s = spec.exec(m[1])) {
|
|
100
|
+
if (ALIASABLE.includes(s[1]))
|
|
101
|
+
add(s[1], s[2]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
/** Escape a string for safe interpolation into a dynamically-built RegExp. */
|
|
107
|
+
function escapeRe(s) {
|
|
108
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
109
|
+
}
|
|
9
110
|
const RE_CREATE_SIGN_VERIFY = /create(?:Sign|Verify)\s*\(/g;
|
|
10
111
|
// One-shot crypto.sign/verify(algorithm, data, key). A LOOKBEHIND (not a
|
|
11
112
|
// consumed char) anchors it so it doesn't fire inside identifiers like `assign(`
|
|
@@ -35,7 +136,7 @@ const RE_JSRSASIGN_KEYGEN = /KEYUTIL\.generateKeypair\s*\(/g;
|
|
|
35
136
|
const RE_JSRSASIGN_SIGN = /KJUR\.crypto\.(?:Signature|ECDSA)\b/g;
|
|
36
137
|
const RE_NODE_RSA = /new\s+NodeRSA\s*\(/g;
|
|
37
138
|
// secp256k1 — direct @noble/secp256k1 / secp256k1-style API usage in source.
|
|
38
|
-
const RE_SECP256K1 = /\b(?:secp(?:256k1)?|secp)\s*\.\s*(
|
|
139
|
+
const RE_SECP256K1 = /\b(?:secp(?:256k1)?|secp)\s*\.\s*(sign|verify|getPublicKey|getSharedSecret|ecdh|recoverPublicKey)\s*\(/g;
|
|
39
140
|
// JWT/JOSE.
|
|
40
141
|
const RE_JWT_ALG = /['"`](RS(?:256|384|512)|PS(?:256|384|512)|ES(?:256|384|512|256K)|EdDSA)['"`]/g;
|
|
41
142
|
// JOSE ECDH-ES key agreement (HNDL) and COSE algorithm identifiers.
|
|
@@ -177,52 +278,41 @@ const nodeCryptoDetector = {
|
|
|
177
278
|
const findings = [];
|
|
178
279
|
// generateKeyPair(Sync)('rsa' | 'ec' | 'dsa' | 'dh' | 'x25519' | 'ed25519', ...)
|
|
179
280
|
eachMatch(RE_GENERATE_KEYPAIR, content, (m) => {
|
|
180
|
-
|
|
181
|
-
const map = {
|
|
182
|
-
rsa: { algo: "RSA", cat: "kem", sev: "high", hndl: true, label: "RSA" },
|
|
183
|
-
// RSA-PSS is signature-only, so classify it as a (forgeable) signature
|
|
184
|
-
// rather than a KEM — no HNDL confidentiality exposure.
|
|
185
|
-
"rsa-pss": {
|
|
186
|
-
algo: "RSA",
|
|
187
|
-
cat: "signature",
|
|
188
|
-
sev: "high",
|
|
189
|
-
hndl: false,
|
|
190
|
-
label: "RSA-PSS",
|
|
191
|
-
message: "Generates a classical RSA-PSS signing key, which is forgeable by a quantum attacker.",
|
|
192
|
-
remediation: "ML-DSA-65 (FIPS 204) or SLH-DSA (FIPS 205)",
|
|
193
|
-
},
|
|
194
|
-
// EC keys feed BOTH ECDSA (sign) and ECDH (key agreement). ECDH is
|
|
195
|
-
// HNDL-exposed, so classify conservatively as key-exchange-capable and
|
|
196
|
-
// surface both concerns rather than asserting signature-only (P0-4).
|
|
197
|
-
ec: {
|
|
198
|
-
algo: "ECDH",
|
|
199
|
-
cat: "key-exchange",
|
|
200
|
-
sev: "high",
|
|
201
|
-
hndl: true,
|
|
202
|
-
label: "EC (ECDSA/ECDH)",
|
|
203
|
-
message: "Generates a classical EC key pair. EC keys feed BOTH ECDSA signatures " +
|
|
204
|
-
"and ECDH key agreement; the ECDH path is harvest-now-decrypt-later exposed.",
|
|
205
|
-
remediation: "For key agreement: hybrid X25519MLKEM768 (ML-KEM-768). For signatures: ML-DSA-65 (FIPS 204).",
|
|
206
|
-
},
|
|
207
|
-
dsa: { algo: "DSA", cat: "signature", sev: "high", hndl: false, label: "DSA" },
|
|
208
|
-
dh: { algo: "DH", cat: "key-exchange", sev: "high", hndl: true, label: "Diffie-Hellman" },
|
|
209
|
-
x25519: { algo: "X25519", cat: "key-exchange", sev: "low", hndl: true, label: "X25519" },
|
|
210
|
-
x448: { algo: "X448", cat: "key-exchange", sev: "low", hndl: true, label: "X448" },
|
|
211
|
-
ed25519: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed25519" },
|
|
212
|
-
ed448: { algo: "EdDSA", cat: "signature", sev: "low", hndl: false, label: "Ed448" },
|
|
213
|
-
};
|
|
214
|
-
const info = map[type];
|
|
215
|
-
findings.push(findingFromRule(RULE_NODE_KEYGEN, { file, content, index: m.index, matchLength: m[0].length }, {
|
|
216
|
-
title: `${info.label} key generation`,
|
|
217
|
-
category: info.cat,
|
|
218
|
-
severity: info.sev,
|
|
219
|
-
algorithm: info.algo,
|
|
220
|
-
hndl: info.hndl,
|
|
221
|
-
message: info.message ??
|
|
222
|
-
`Generates a classical ${info.label} key pair, which is not quantum-safe.`,
|
|
223
|
-
...(info.remediation ? { remediation: info.remediation } : {}),
|
|
224
|
-
}));
|
|
281
|
+
pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length);
|
|
225
282
|
});
|
|
283
|
+
// Import-alias resolution: follow `import { generateKeyPairSync as gk }` (and
|
|
284
|
+
// the CommonJS destructure-rename) so an aliased call still detects. Only the
|
|
285
|
+
// keygen / ECDH / DH constructors are resolved — their classification is
|
|
286
|
+
// unambiguous. The alias regexes run on the ORIGINAL content, so locations
|
|
287
|
+
// stay exact; it fires only when the alias was explicitly bound to a known
|
|
288
|
+
// crypto symbol, so precision is unaffected.
|
|
289
|
+
const aliases = collectCryptoAliases(content);
|
|
290
|
+
for (const [canonical, names] of aliases) {
|
|
291
|
+
for (const alias of names) {
|
|
292
|
+
const a = escapeRe(alias);
|
|
293
|
+
if (canonical === "generateKeyPairSync" || canonical === "generateKeyPair") {
|
|
294
|
+
const re = new RegExp(`\\b${a}\\s*\\(\\s*['"\`](rsa-pss|rsa|ec|dsa|dh|x25519|x448|ed25519|ed448)['"\`]`, "g");
|
|
295
|
+
eachMatch(re, content, (m) => pushKeygenFinding(findings, m[1], file, content, m.index, m[0].length));
|
|
296
|
+
}
|
|
297
|
+
else if (canonical === "createECDH") {
|
|
298
|
+
eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) => findings.push(findingFromRule(RULE_NODE_ECDH, {
|
|
299
|
+
file,
|
|
300
|
+
content,
|
|
301
|
+
index: m.index,
|
|
302
|
+
matchLength: m[0].length,
|
|
303
|
+
})));
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// createDiffieHellman / createDiffieHellmanGroup.
|
|
307
|
+
eachMatch(new RegExp(`\\b${a}\\s*\\(`, "g"), content, (m) => findings.push(findingFromRule(RULE_NODE_DH, {
|
|
308
|
+
file,
|
|
309
|
+
content,
|
|
310
|
+
index: m.index,
|
|
311
|
+
matchLength: m[0].length,
|
|
312
|
+
})));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
226
316
|
// createSign / createVerify — RSA / ECDSA / DSA signatures.
|
|
227
317
|
eachMatch(RE_CREATE_SIGN_VERIFY, content, (m) => {
|
|
228
318
|
findings.push(findingFromRule(RULE_NODE_SIGN, {
|
|
@@ -485,7 +575,21 @@ const libraryDetector = {
|
|
|
485
575
|
add(RE_FORGE_RSA, RULE_FORGE_RSA); // node-forge: pki.rsa.generateKeyPair(...)
|
|
486
576
|
add(RE_FORGE_ED25519, RULE_FORGE_ED25519); // node-forge: forge.ed25519.*
|
|
487
577
|
add(RE_ELLIPTIC_EC, RULE_ELLIPTIC_EC); // elliptic: new EC('secp256k1')
|
|
488
|
-
|
|
578
|
+
// secp256k1: classify per method — getSharedSecret/ecdh are key AGREEMENT
|
|
579
|
+
// (ECDH, harvest-now-decrypt-later); sign/verify/getPublicKey/recover are
|
|
580
|
+
// ECDSA signatures (not HNDL). The old rule flagged everything as ECDSA.
|
|
581
|
+
eachMatch(RE_SECP256K1, content, (m) => {
|
|
582
|
+
const kex = m[1] === "getSharedSecret" || m[1] === "ecdh";
|
|
583
|
+
findings.push(findingFromRule(RULE_SECP256K1, { file, content, index: m.index, matchLength: m[0].length }, kex
|
|
584
|
+
? {
|
|
585
|
+
title: "secp256k1 ECDH key agreement",
|
|
586
|
+
category: "key-exchange",
|
|
587
|
+
algorithm: "ECDH",
|
|
588
|
+
hndl: true,
|
|
589
|
+
message: `secp256k1 ECDH key agreement (.${m[1]}()) is classical and harvest-now-decrypt-later exposed.`,
|
|
590
|
+
}
|
|
591
|
+
: undefined));
|
|
592
|
+
});
|
|
489
593
|
add(RE_JSRSASIGN_KEYGEN, RULE_JSRSASIGN_KEYGEN); // jsrsasign: KEYUTIL.generateKeypair(...)
|
|
490
594
|
add(RE_JSRSASIGN_SIGN, RULE_JSRSASIGN_SIGN); // jsrsasign: KJUR.crypto.*
|
|
491
595
|
add(RE_NODE_RSA, RULE_NODE_RSA_LIB); // node-rsa: new NodeRSA(...)
|
|
@@ -495,6 +599,10 @@ const libraryDetector = {
|
|
|
495
599
|
/* -------------------------------------------------------------------------- */
|
|
496
600
|
/* JWT / JOSE / COSE algorithm strings */
|
|
497
601
|
/* -------------------------------------------------------------------------- */
|
|
602
|
+
// JOSE RSA key-transport algorithms (JWE `alg`): quoted RSA-OAEP / RSA-OAEP-256
|
|
603
|
+
// /384/512 / RSA1_5. Classical RSA *encryption* (key transport) — distinct from
|
|
604
|
+
// the RS*/PS* *signature* tokens above, and a harvest-now-decrypt-later surface.
|
|
605
|
+
const RE_JOSE_KEM = /['"`](RSA-OAEP(?:-(?:256|384|512))?|RSA1_5)['"`]/g;
|
|
498
606
|
const RULE_JWT_ALG = {
|
|
499
607
|
id: "jwt-classical-alg",
|
|
500
608
|
title: "Classical JWT/JOSE algorithm",
|
|
@@ -521,6 +629,19 @@ const RULE_JOSE_ECDH = {
|
|
|
521
629
|
message: "JOSE ECDH-ES performs classical ECDH key agreement — harvest-now-decrypt-later exposed.",
|
|
522
630
|
remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
|
|
523
631
|
};
|
|
632
|
+
const RULE_JOSE_RSA_OAEP = {
|
|
633
|
+
id: "jose-rsa-oaep",
|
|
634
|
+
title: "JOSE RSA key-transport algorithm",
|
|
635
|
+
description: "JWE RSA-OAEP / RSA-OAEP-256/384/512 / RSA1_5 key encryption",
|
|
636
|
+
category: "key-exchange",
|
|
637
|
+
severity: "high",
|
|
638
|
+
confidence: "medium",
|
|
639
|
+
algorithm: "RSA",
|
|
640
|
+
hndl: true,
|
|
641
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
642
|
+
message: "JOSE RSA key transport (RSA-OAEP / RSA1_5) is classical RSA encryption — harvest-now-decrypt-later exposed.",
|
|
643
|
+
remediation: "Track IETF PQC JOSE/COSE; adopt hybrid X25519MLKEM768 KEM-based encryption.",
|
|
644
|
+
};
|
|
524
645
|
/**
|
|
525
646
|
* Detects classical signature algorithm identifiers used by JWT/JOSE, plus
|
|
526
647
|
* ECDH-ES key-agreement identifiers (HNDL-exposed). These appear as string
|
|
@@ -534,7 +655,7 @@ const jwtDetector = {
|
|
|
534
655
|
// signal in JS/TS or Python (e.g. PyJWT `algorithm="RS256"`), so this detector
|
|
535
656
|
// is un-gated from JS-only to the JWT host surfaces.
|
|
536
657
|
language: "any",
|
|
537
|
-
rules: [RULE_JWT_ALG, RULE_JOSE_ECDH],
|
|
658
|
+
rules: [RULE_JWT_ALG, RULE_JOSE_ECDH, RULE_JOSE_RSA_OAEP],
|
|
538
659
|
appliesTo: (f) => hasExtension(f, JWT_HOST_EXTENSIONS),
|
|
539
660
|
detect({ file, content }) {
|
|
540
661
|
const findings = [];
|
|
@@ -561,6 +682,13 @@ const jwtDetector = {
|
|
|
561
682
|
message: `JOSE "${m[1]}" performs classical ECDH key agreement — harvest-now-decrypt-later exposed.`,
|
|
562
683
|
}));
|
|
563
684
|
});
|
|
685
|
+
// JOSE RSA key transport (RSA-OAEP / RSA1_5) — classical RSA encryption, HNDL.
|
|
686
|
+
eachMatch(RE_JOSE_KEM, content, (m) => {
|
|
687
|
+
findings.push(findingFromRule(RULE_JOSE_RSA_OAEP, { file, content, index: m.index, matchLength: m[0].length }, {
|
|
688
|
+
title: `JOSE RSA key transport ${m[1]}`,
|
|
689
|
+
message: `JOSE "${m[1]}" is classical RSA key transport — harvest-now-decrypt-later exposed.`,
|
|
690
|
+
}));
|
|
691
|
+
});
|
|
564
692
|
return findings;
|
|
565
693
|
},
|
|
566
694
|
};
|
|
@@ -648,6 +776,47 @@ const tlsDetector = {
|
|
|
648
776
|
/* -------------------------------------------------------------------------- */
|
|
649
777
|
const RE_SSH_PUBKEY = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
|
|
650
778
|
const RE_CERT_SIG_ALG = /\b(sha(?:1|256|384|512)WithRSAEncryption|ecdsa-with-SHA(?:1|256|384|512)|rsassaPss|dsaWithSHA(?:1|256))\b/g;
|
|
779
|
+
// Classical SSH key-exchange algorithm identifiers: finite-field DH groups
|
|
780
|
+
// (diffie-hellman-group{1,14,15,16,17,18} + group-exchange), ECDH over NIST
|
|
781
|
+
// curves (ecdh-sha2-nistp*), and Curve25519 (curve25519-sha256). All are
|
|
782
|
+
// Shor-broken key agreement — the harvest-now-decrypt-later surface a lexical
|
|
783
|
+
// scan misses when only the `crypto/*` API is matched, not the negotiated kex.
|
|
784
|
+
const RE_SSH_KEX = /\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;
|
|
785
|
+
// A bare key-type token (`ssh-rsa`, `ssh-ed25519`, …) is real SSH crypto only
|
|
786
|
+
// when it is EITHER (a) followed by base64 key material — an actual
|
|
787
|
+
// authorized_keys / known_hosts entry — OR (b) one of ≥2 DISTINCT ssh key/host-key
|
|
788
|
+
// algorithm tokens on the same line — an algorithm-preference list
|
|
789
|
+
// (`HostKeyAlgorithms ssh-ed25519,ecdsa-sha2-nistp256,ssh-rsa`, `%w[…]`, …). A
|
|
790
|
+
// lone token, e.g. a UI label / i18n value like `"ssh-rsa": "ssh-rsa"`, is
|
|
791
|
+
// neither, so it must NOT be flagged. `RE_SSH_ALGO_TOKEN` is a separate (cloned-
|
|
792
|
+
// on-use) copy so counting per line never disturbs the outer `eachMatch` state.
|
|
793
|
+
const RE_SSH_ALGO_TOKEN = /\b(ssh-rsa|ssh-ed25519|ssh-dss|ecdsa-sha2-nistp(?:256|384|521))\b/g;
|
|
794
|
+
/** How far around a token we look for a sibling algorithm on the same line. A
|
|
795
|
+
* real `HostKeyAlgorithms` list is short; bounding the window keeps the per-token
|
|
796
|
+
* work O(1) so a pathological single huge line can't make detection quadratic. */
|
|
797
|
+
const SSH_LINE_WINDOW = 512;
|
|
798
|
+
function isRealSshKeyOrAlgoList(content, index, matchLen) {
|
|
799
|
+
// (a) base64 key material immediately after the token → a real SSH public key.
|
|
800
|
+
if (/^\s+[A-Za-z0-9+/]{20,}/.test(content.slice(index + matchLen, index + matchLen + 80))) {
|
|
801
|
+
return true;
|
|
802
|
+
}
|
|
803
|
+
// (b) ≥2 distinct ssh key/host-key algorithm tokens on the same line → a list.
|
|
804
|
+
// Bound both the back-scan for the line start and the window itself so the work
|
|
805
|
+
// is constant per token and never crosses a line boundary (i18n entries live on
|
|
806
|
+
// separate lines, so a lone repeated token stays a single distinct token).
|
|
807
|
+
const from = Math.max(0, index - SSH_LINE_WINDOW);
|
|
808
|
+
const pre = content.slice(from, index);
|
|
809
|
+
const preNl = pre.lastIndexOf("\n");
|
|
810
|
+
const lineStart = preNl === -1 ? from : from + preNl + 1;
|
|
811
|
+
const to = Math.min(content.length, index + matchLen + SSH_LINE_WINDOW);
|
|
812
|
+
const post = content.slice(index, to);
|
|
813
|
+
const postNl = post.indexOf("\n");
|
|
814
|
+
const lineEnd = postNl === -1 ? to : index + postNl;
|
|
815
|
+
const distinct = new Set();
|
|
816
|
+
for (const t of content.slice(lineStart, lineEnd).matchAll(RE_SSH_ALGO_TOKEN))
|
|
817
|
+
distinct.add(t[1]);
|
|
818
|
+
return distinct.size >= 2;
|
|
819
|
+
}
|
|
651
820
|
const RULE_SSH_PUBKEY = {
|
|
652
821
|
id: "ssh-public-key",
|
|
653
822
|
title: "Classical SSH public key",
|
|
@@ -660,7 +829,7 @@ const RULE_SSH_PUBKEY = {
|
|
|
660
829
|
cwe: CWE_BROKEN_CRYPTO,
|
|
661
830
|
sensitive: true,
|
|
662
831
|
message: "A classical SSH public key is forgeable by a quantum attacker.",
|
|
663
|
-
remediation: "Plan migration to PQC-capable SSH
|
|
832
|
+
remediation: "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.",
|
|
664
833
|
};
|
|
665
834
|
const RULE_CERT_SIG_ALG = {
|
|
666
835
|
id: "cert-signature-algorithm",
|
|
@@ -675,6 +844,19 @@ const RULE_CERT_SIG_ALG = {
|
|
|
675
844
|
message: "A classical certificate signature algorithm (RSA/ECDSA/DSA) is a quantum forgery surface.",
|
|
676
845
|
remediation: "Plan re-issuance with PQC-capable CAs as ML-DSA certificate profiles mature.",
|
|
677
846
|
};
|
|
847
|
+
const RULE_SSH_KEX = {
|
|
848
|
+
id: "ssh-kex-classical",
|
|
849
|
+
title: "Classical SSH key exchange",
|
|
850
|
+
description: "diffie-hellman-group* / group-exchange / ecdh-sha2-* / curve25519-sha256 kex",
|
|
851
|
+
category: "key-exchange",
|
|
852
|
+
severity: "medium",
|
|
853
|
+
confidence: "medium",
|
|
854
|
+
algorithm: "unknown",
|
|
855
|
+
hndl: true,
|
|
856
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
857
|
+
message: "A classical SSH key-exchange algorithm (finite-field DH / ECDH / X25519) is harvest-now-decrypt-later exposed.",
|
|
858
|
+
remediation: "Prefer the mlkem768x25519-sha256 KEX (ML-KEM-768 hybrid, OpenSSH 10 default); sntrup761x25519 is an acceptable interim.",
|
|
859
|
+
};
|
|
678
860
|
/**
|
|
679
861
|
* Detects classical SSH public keys (`authorized_keys` / `known_hosts` lines)
|
|
680
862
|
* and X.509 certificate signature-algorithm identifiers in any text file. These
|
|
@@ -686,12 +868,18 @@ const sshCertDetector = {
|
|
|
686
868
|
description: "SSH public keys and TLS/X.509 certificate signature algorithms in config",
|
|
687
869
|
scope: "config",
|
|
688
870
|
language: "any",
|
|
689
|
-
rules: [RULE_SSH_PUBKEY, RULE_CERT_SIG_ALG],
|
|
690
|
-
|
|
871
|
+
rules: [RULE_SSH_PUBKEY, RULE_CERT_SIG_ALG, RULE_SSH_KEX],
|
|
872
|
+
// Skip prose/docs: a changelog or README that merely mentions `ssh-rsa` in a
|
|
873
|
+
// sentence is not crypto config. PEM material is caught by its own detector.
|
|
874
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
691
875
|
detect({ file, content }) {
|
|
692
876
|
const findings = [];
|
|
693
877
|
// SSH public keys: ssh-rsa AAAA…, ecdsa-sha2-nistp256 …, ssh-ed25519 …
|
|
694
878
|
eachMatch(RE_SSH_PUBKEY, content, (m) => {
|
|
879
|
+
// Skip a bare token that is neither a real key nor an algorithm list (e.g.
|
|
880
|
+
// a `"ssh-rsa": "ssh-rsa"` i18n label) — see `isRealSshKeyOrAlgoList`.
|
|
881
|
+
if (!isRealSshKeyOrAlgoList(content, m.index, m[0].length))
|
|
882
|
+
return;
|
|
695
883
|
const tok = m[1];
|
|
696
884
|
const algorithm = tok.startsWith("ssh-rsa")
|
|
697
885
|
? "RSA"
|
|
@@ -720,6 +908,70 @@ const sshCertDetector = {
|
|
|
720
908
|
message: `Certificate signature algorithm "${tok}" is classical (RSA/ECDSA/DSA) — a quantum forgery surface.`,
|
|
721
909
|
}));
|
|
722
910
|
});
|
|
911
|
+
// SSH key-exchange algorithm identifiers (finite-field DH / ECDH / X25519).
|
|
912
|
+
eachMatch(RE_SSH_KEX, content, (m) => {
|
|
913
|
+
const tok = m[1];
|
|
914
|
+
const algorithm = tok.startsWith("diffie-hellman")
|
|
915
|
+
? "DH"
|
|
916
|
+
: tok.startsWith("ecdh")
|
|
917
|
+
? "ECDH"
|
|
918
|
+
: "X25519";
|
|
919
|
+
findings.push(findingFromRule(RULE_SSH_KEX, { file, content, index: m.index, matchLength: m[0].length }, {
|
|
920
|
+
title: `Classical SSH key exchange (${tok})`,
|
|
921
|
+
algorithm,
|
|
922
|
+
message: `SSH key-exchange "${tok}" is classical (${algorithm}) — harvest-now-decrypt-later exposed.`,
|
|
923
|
+
}));
|
|
924
|
+
});
|
|
925
|
+
return findings;
|
|
926
|
+
},
|
|
927
|
+
};
|
|
928
|
+
/* -------------------------------------------------------------------------- */
|
|
929
|
+
/* Classical TLS key-exchange cipher suites (language-agnostic config) */
|
|
930
|
+
/* -------------------------------------------------------------------------- */
|
|
931
|
+
// Classical KEX cipher-suite identifiers, in both the OpenSSL (`ECDHE-RSA-…`) and
|
|
932
|
+
// IANA (`TLS_ECDHE_RSA_WITH_…`) spellings, with `-` or `_` separators. These name
|
|
933
|
+
// a Shor-broken key exchange (ECDHE/DHE), so they are the harvest-now surface a
|
|
934
|
+
// PQC scanner should flag — distinct from the legacy-*version* / weak-cipher rules
|
|
935
|
+
// above. Case-sensitive on purpose: cipher-suite tokens are upper-case constants,
|
|
936
|
+
// so prose like "ecdhe rsa" does not misfire. No trailing \b so the IANA
|
|
937
|
+
// `TLS_ECDHE_RSA_WITH_…` form (a `_` follows `RSA`) still matches.
|
|
938
|
+
const RE_TLS_CLASSICAL_KEX = /\b(?:TLS_)?(?:ECDHE|ECDH|DHE)[-_](?:RSA|ECDSA|DSS)/g;
|
|
939
|
+
const RULE_TLS_CLASSICAL_KEX = {
|
|
940
|
+
id: "tls-classical-kex",
|
|
941
|
+
title: "Classical TLS key-exchange cipher suite",
|
|
942
|
+
description: "ECDHE / DHE cipher suites negotiate Shor-broken key exchange",
|
|
943
|
+
category: "tls",
|
|
944
|
+
severity: "medium",
|
|
945
|
+
confidence: "medium",
|
|
946
|
+
algorithm: "unknown",
|
|
947
|
+
hndl: true,
|
|
948
|
+
cwe: CWE_BROKEN_CRYPTO,
|
|
949
|
+
message: "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.",
|
|
950
|
+
remediation: "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.",
|
|
951
|
+
};
|
|
952
|
+
/**
|
|
953
|
+
* Detects classical TLS key-exchange cipher suites in any text/config file
|
|
954
|
+
* (OpenSSL cipher lists, IANA suite constants in Go/Java/C#/Rust, `.properties`,
|
|
955
|
+
* etc.). Language-agnostic because the suite identifiers are the same tokens
|
|
956
|
+
* everywhere — this is the cross-language TLS gap the legacy-version rule misses.
|
|
957
|
+
*/
|
|
958
|
+
const tlsClassicalKexDetector = {
|
|
959
|
+
id: "tls-cipher-suite",
|
|
960
|
+
description: "Classical TLS key-exchange cipher suites (ECDHE/DHE) in any config",
|
|
961
|
+
scope: "config",
|
|
962
|
+
language: "any",
|
|
963
|
+
rules: [RULE_TLS_CLASSICAL_KEX],
|
|
964
|
+
appliesTo: (f) => !hasExtension(f, DOC_EXTENSIONS),
|
|
965
|
+
detect({ file, content }) {
|
|
966
|
+
const findings = [];
|
|
967
|
+
eachMatch(RE_TLS_CLASSICAL_KEX, content, (m) => {
|
|
968
|
+
const tok = m[0];
|
|
969
|
+
const algorithm = tok.includes("ECDH") ? "ECDH" : "DH";
|
|
970
|
+
findings.push(findingFromRule(RULE_TLS_CLASSICAL_KEX, { file, content, index: m.index, matchLength: m[0].length }, {
|
|
971
|
+
algorithm,
|
|
972
|
+
message: `Classical TLS key-exchange suite "${tok}…" (${algorithm}) is harvest-now-decrypt-later exposed.`,
|
|
973
|
+
}));
|
|
974
|
+
});
|
|
723
975
|
return findings;
|
|
724
976
|
},
|
|
725
977
|
};
|
|
@@ -731,5 +983,6 @@ export const sourceDetectors = [
|
|
|
731
983
|
jwtDetector,
|
|
732
984
|
tlsDetector,
|
|
733
985
|
sshCertDetector,
|
|
986
|
+
tlsClassicalKexDetector,
|
|
734
987
|
];
|
|
735
988
|
//# sourceMappingURL=source.js.map
|