@clear-capabilities/agentic-security-scanner 0.79.0 → 0.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/178.index.js +1 -1
- package/dist/333.index.js +283 -0
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +90 -1
- package/dist/agentic-security.mjs +83 -83
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +6 -4
- package/src/.agentic-security/findings.json +104638 -0
- package/src/.agentic-security/last-scan.json +104638 -0
- package/src/.agentic-security/last-scan.json.sig +1 -0
- package/src/.agentic-security/scan-history.json +12562 -0
- package/src/.agentic-security/streak.json +21 -0
- package/src/dataflow/.agentic-security/findings.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json +6086 -0
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
- package/src/dataflow/.agentic-security/scan-history.json +250 -0
- package/src/dataflow/.agentic-security/streak.json +21 -0
- package/src/dataflow/cross-service-taint.js +201 -0
- package/src/dataflow/formal-verify.js +204 -0
- package/src/dataflow/ifds-precise.js +222 -0
- package/src/dataflow/k2-summary-cache.js +153 -0
- package/src/dataflow/lib-taint-summaries.js +198 -0
- package/src/dataflow/privacy-taint.js +205 -0
- package/src/dataflow/smt-feasibility.js +189 -0
- package/src/engine.js +784 -127
- package/src/ir/.agentic-security/findings.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json +4011 -0
- package/src/ir/.agentic-security/last-scan.json.sig +1 -0
- package/src/ir/.agentic-security/scan-history.json +193 -0
- package/src/ir/.agentic-security/streak.json +20 -0
- package/src/ir/cpp-preprocessor.js +142 -0
- package/src/ir/csharp-ir.js +604 -0
- package/src/ir/universal-ir.js +403 -0
- package/src/mcp/.agentic-security/findings.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json +8632 -0
- package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
- package/src/mcp/.agentic-security/scan-history.json +143 -0
- package/src/mcp/.agentic-security/streak.json +20 -0
- package/src/mcp/tools.js +90 -1
- package/src/posture/.agentic-security/findings.json +64004 -0
- package/src/posture/.agentic-security/last-scan.json +64004 -0
- package/src/posture/.agentic-security/last-scan.json.sig +1 -0
- package/src/posture/.agentic-security/scan-history.json +7162 -0
- package/src/posture/.agentic-security/streak.json +21 -0
- package/src/posture/api-contract.js +193 -0
- package/src/posture/attack-taxonomy.js +227 -0
- package/src/posture/compliance-policy.js +218 -0
- package/src/posture/composite-risk.js +122 -0
- package/src/posture/csharp-analysis.js +330 -0
- package/src/posture/exploit-bundle.js +210 -0
- package/src/posture/federated-learning.js +172 -0
- package/src/posture/license-attributions.js +94 -0
- package/src/posture/license-graph.js +238 -0
- package/src/posture/pqc-migration-plan.js +158 -0
- package/src/posture/reachability-filter.js +33 -2
- package/src/posture/realtime-cve-monitor.js +214 -0
- package/src/posture/runtime-correlation.js +174 -0
- package/src/posture/sbom-diff.js +171 -0
- package/src/posture/sca-policy.js +235 -0
- package/src/posture/sca-upgrade.js +259 -0
- package/src/posture/threat-model-auto.js +268 -0
- package/src/posture/triage-learning.js +170 -0
- package/src/posture/triage.js +26 -1
- package/src/sast/.agentic-security/findings.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json +6154 -0
- package/src/sast/.agentic-security/last-scan.json.sig +1 -0
- package/src/sast/.agentic-security/scan-history.json +941 -0
- package/src/sast/.agentic-security/streak.json +22 -0
- package/src/sast/_secret-entropy.js +145 -0
- package/src/sast/cloud-iam.js +312 -0
- package/src/sast/cpp.js +138 -4
- package/src/sast/crypto-protocol.js +388 -0
- package/src/sast/csharp-tokenizer.js +392 -0
- package/src/sast/csharp.js +924 -138
- package/src/sast/dapp-frontend.js +200 -0
- package/src/sast/k8s-admission.js +271 -0
- package/src/sast/llm-app.js +272 -0
- package/src/sast/ml-supply-chain.js +259 -0
- package/src/sast/mobile.js +224 -0
- package/src/sast/post-quantum-crypto.js +348 -0
- package/src/sast/web3-advanced.js +375 -0
- package/src/sca/.agentic-security/findings.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json +7460 -0
- package/src/sca/.agentic-security/last-scan.json.sig +1 -0
- package/src/sca/.agentic-security/scan-history.json +113 -0
- package/src/sca/.agentic-security/streak.json +21 -0
- package/src/sca/CLAUDE.md +161 -0
- package/src/sca/binary-metadata.js +37 -15
- package/src/sca/sigstore-verify.js +215 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
// Mobile security pipeline — Recommendation #4 of the world-class+2 plan.
|
|
2
|
+
//
|
|
3
|
+
// Android: parses AndroidManifest.xml + Kotlin/Java source for mobile-
|
|
4
|
+
// specific vulnerability patterns. iOS: parses Info.plist + Swift / Obj-C
|
|
5
|
+
// source for the matching iOS patterns.
|
|
6
|
+
//
|
|
7
|
+
// Android families covered:
|
|
8
|
+
// - mobile-exported-component Activity/Service/Receiver exported without
|
|
9
|
+
// permission (CWE-925/926)
|
|
10
|
+
// - mobile-debug-build android:debuggable="true" in release manifest
|
|
11
|
+
// - mobile-allow-backup android:allowBackup="true" (data exfil at
|
|
12
|
+
// device level)
|
|
13
|
+
// - mobile-cleartext-transit android:usesCleartextTraffic="true"
|
|
14
|
+
// (CWE-319)
|
|
15
|
+
// - mobile-webview-js-iface WebView.addJavascriptInterface (CWE-749)
|
|
16
|
+
// - mobile-intent-spoof Intent.parseUri / startActivity(tainted)
|
|
17
|
+
// (CWE-927)
|
|
18
|
+
// - mobile-keychain-misuse SharedPreferences MODE_WORLD_* writes
|
|
19
|
+
//
|
|
20
|
+
// iOS families covered:
|
|
21
|
+
// - ios-cleartext-transit NSExceptionAllowsInsecureHTTPLoads /
|
|
22
|
+
// NSAllowsArbitraryLoads = true
|
|
23
|
+
// - ios-keychain-accessible kSecAttrAccessibleAlways /
|
|
24
|
+
// kSecAttrAccessibleAlwaysThisDeviceOnly
|
|
25
|
+
// - ios-debug-build DEBUG flag in release configuration
|
|
26
|
+
// - ios-biometric-fallback LAPolicy.deviceOwnerAuthentication (allows
|
|
27
|
+
// passcode fallback when Face/Touch ID is
|
|
28
|
+
// required for sensitive ops)
|
|
29
|
+
// - ios-webview-untrusted-url WKWebView.load(URLRequest with tainted url)
|
|
30
|
+
|
|
31
|
+
import { blankComments } from './_comment-strip.js';
|
|
32
|
+
|
|
33
|
+
function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
34
|
+
function _snip(raw, line) { return (raw.split('\n')[line - 1] || '').trim().slice(0, 200); }
|
|
35
|
+
|
|
36
|
+
function _finding(file, line, raw, ruleId, vuln, family, severity, cwe, remediation, subfamily) {
|
|
37
|
+
return {
|
|
38
|
+
id: `${ruleId}:${file}:${line}`, file, line, vuln, severity, cwe,
|
|
39
|
+
stride: severity === 'critical' ? 'Elevation of Privilege' : 'Information Disclosure',
|
|
40
|
+
snippet: _snip(raw, line),
|
|
41
|
+
remediation, family, subfamily,
|
|
42
|
+
confidence: 0.85, parser: 'MOBILE',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Android: AndroidManifest.xml ───────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
function _scanAndroidManifest(file, raw, out, seen) {
|
|
49
|
+
const code = raw;
|
|
50
|
+
// exported="true" components without permission attribute
|
|
51
|
+
const expRe = /<\s*(activity|service|receiver|provider)\b([^>]*?)\bandroid:exported\s*=\s*"true"([^>]*?)\/?\s*>/gi;
|
|
52
|
+
let m;
|
|
53
|
+
while ((m = expRe.exec(code))) {
|
|
54
|
+
const attrs = (m[2] + m[3]) || '';
|
|
55
|
+
if (/android:permission\s*=/.test(attrs)) continue;
|
|
56
|
+
const line = _lineOf(raw, m.index);
|
|
57
|
+
const id = `mobile-exported-component:${file}:${line}`;
|
|
58
|
+
if (seen.has(id)) continue;
|
|
59
|
+
seen.add(id);
|
|
60
|
+
out.push(_finding(file, line, raw, 'mobile-exported-component',
|
|
61
|
+
`Exported ${m[1]} without permission attribute`,
|
|
62
|
+
'mobile-exported-component', 'high', 'CWE-925',
|
|
63
|
+
'Set android:exported="false" if the component is internal-only. If exported, add android:permission="<custom-permission>" requiring a signature-level permission so only your own app or apps you control can invoke it.',
|
|
64
|
+
'exported-no-permission'));
|
|
65
|
+
}
|
|
66
|
+
// debuggable=true
|
|
67
|
+
if (/android:debuggable\s*=\s*"true"/i.test(code)) {
|
|
68
|
+
const idx = code.search(/android:debuggable\s*=\s*"true"/i);
|
|
69
|
+
const line = _lineOf(raw, idx);
|
|
70
|
+
out.push(_finding(file, line, raw, 'mobile-debug-build',
|
|
71
|
+
'android:debuggable="true" — debug build flag set in manifest',
|
|
72
|
+
'mobile-debug-build', 'high', 'CWE-489',
|
|
73
|
+
'Remove android:debuggable from the manifest entirely (Gradle controls the debuggable flag automatically per build type). A debuggable release build allows JDWP attach + heap dump from any host.',
|
|
74
|
+
'debuggable-true'));
|
|
75
|
+
}
|
|
76
|
+
// allowBackup=true (Android < 12 backs up app data including SharedPreferences without encryption)
|
|
77
|
+
if (/android:allowBackup\s*=\s*"true"/i.test(code)) {
|
|
78
|
+
const idx = code.search(/android:allowBackup\s*=\s*"true"/i);
|
|
79
|
+
const line = _lineOf(raw, idx);
|
|
80
|
+
out.push(_finding(file, line, raw, 'mobile-allow-backup',
|
|
81
|
+
'android:allowBackup="true" — application data backed up via adb backup / cloud',
|
|
82
|
+
'mobile-allow-backup', 'medium', 'CWE-200',
|
|
83
|
+
'Set android:allowBackup="false" unless you have a documented backup strategy. Alternatively, define an explicit android:fullBackupContent rules file that excludes sensitive paths (databases, SharedPreferences).',
|
|
84
|
+
'allow-backup'));
|
|
85
|
+
}
|
|
86
|
+
// cleartext traffic
|
|
87
|
+
if (/android:usesCleartextTraffic\s*=\s*"true"/i.test(code)) {
|
|
88
|
+
const idx = code.search(/android:usesCleartextTraffic\s*=\s*"true"/i);
|
|
89
|
+
const line = _lineOf(raw, idx);
|
|
90
|
+
out.push(_finding(file, line, raw, 'mobile-cleartext-transit',
|
|
91
|
+
'android:usesCleartextTraffic="true" — HTTP transmission allowed',
|
|
92
|
+
'mobile-cleartext-transit', 'high', 'CWE-319',
|
|
93
|
+
'Remove the attribute (defaults to false on API 28+) and use a network-security-config XML to allow-list specific debug hosts if needed. Any cleartext traffic is sniffable on hostile Wi-Fi.',
|
|
94
|
+
'cleartext-true'));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Android: Kotlin/Java source ────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
function _scanAndroidSource(file, raw, out, seen) {
|
|
101
|
+
const code = blankComments(raw);
|
|
102
|
+
// WebView.addJavascriptInterface (CWE-749)
|
|
103
|
+
const wRe = /\b(\w+)\s*\.\s*addJavascriptInterface\s*\(/g;
|
|
104
|
+
let m;
|
|
105
|
+
while ((m = wRe.exec(code))) {
|
|
106
|
+
const line = _lineOf(raw, m.index);
|
|
107
|
+
const id = `mobile-webview-js-iface:${file}:${line}`;
|
|
108
|
+
if (seen.has(id)) continue;
|
|
109
|
+
seen.add(id);
|
|
110
|
+
out.push(_finding(file, line, raw, 'mobile-webview-js-iface',
|
|
111
|
+
'WebView.addJavascriptInterface — JS bridge exposes app code to web content',
|
|
112
|
+
'mobile-webview-js-iface', 'high', 'CWE-749',
|
|
113
|
+
'Avoid addJavascriptInterface entirely. Use JavascriptInterface-annotated methods ONLY when minSdk ≥ 17 AND validate every public method does no privilege escalation. Prefer @JavascriptInterface-tagged interfaces with explicit allow-listed methods.',
|
|
114
|
+
'webview-js-iface'));
|
|
115
|
+
}
|
|
116
|
+
// Intent.parseUri with non-literal
|
|
117
|
+
const piRe = /\bIntent\s*\.\s*parseUri\s*\(\s*(?!["'])\w/g;
|
|
118
|
+
while ((m = piRe.exec(code))) {
|
|
119
|
+
const line = _lineOf(raw, m.index);
|
|
120
|
+
const id = `mobile-intent-spoof:${file}:${line}`;
|
|
121
|
+
if (seen.has(id)) continue;
|
|
122
|
+
seen.add(id);
|
|
123
|
+
out.push(_finding(file, line, raw, 'mobile-intent-spoof',
|
|
124
|
+
'Intent.parseUri with non-literal URI string — intent injection risk',
|
|
125
|
+
'mobile-intent-spoof', 'high', 'CWE-927',
|
|
126
|
+
'Validate the URI against an allow-list of schemes and authorities before parseUri. Better: take a structured input (component name + extras) and build the Intent manually.',
|
|
127
|
+
'intent-parse-uri'));
|
|
128
|
+
}
|
|
129
|
+
// SharedPreferences MODE_WORLD_*
|
|
130
|
+
const sRe = /\bContext\s*\.\s*(?:MODE_WORLD_(?:READABLE|WRITEABLE))\b/g;
|
|
131
|
+
while ((m = sRe.exec(code))) {
|
|
132
|
+
const line = _lineOf(raw, m.index);
|
|
133
|
+
const id = `mobile-keychain-misuse:${file}:${line}`;
|
|
134
|
+
if (seen.has(id)) continue;
|
|
135
|
+
seen.add(id);
|
|
136
|
+
out.push(_finding(file, line, raw, 'mobile-keychain-misuse',
|
|
137
|
+
'SharedPreferences in MODE_WORLD_* — readable/writable by any installed app',
|
|
138
|
+
'mobile-keychain-misuse', 'high', 'CWE-732',
|
|
139
|
+
'Use MODE_PRIVATE (the default). MODE_WORLD_* is deprecated since API 17 and exposes credentials to every other app on the device.',
|
|
140
|
+
'shared-prefs-world-mode'));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ── iOS: Info.plist ────────────────────────────────────────────────────────
|
|
145
|
+
|
|
146
|
+
function _scanIosPlist(file, raw, out, seen) {
|
|
147
|
+
// NSAllowsArbitraryLoads
|
|
148
|
+
if (/NSAllowsArbitraryLoads\s*<\/key>\s*<true\b/i.test(raw)) {
|
|
149
|
+
const idx = raw.search(/NSAllowsArbitraryLoads/i);
|
|
150
|
+
const line = _lineOf(raw, idx);
|
|
151
|
+
out.push(_finding(file, line, raw, 'ios-cleartext-transit',
|
|
152
|
+
'NSAllowsArbitraryLoads = YES — App Transport Security disabled for all hosts',
|
|
153
|
+
'ios-cleartext-transit', 'high', 'CWE-319',
|
|
154
|
+
'Remove NSAllowsArbitraryLoads. If a specific upstream legitimately requires cleartext, use NSExceptionDomains with the smallest possible per-host exception.',
|
|
155
|
+
'ats-disabled'));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── iOS: Swift / Obj-C ─────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
function _scanIosSource(file, raw, out, seen) {
|
|
162
|
+
const code = blankComments(raw);
|
|
163
|
+
// kSecAttrAccessibleAlways — Keychain item accessible at any time
|
|
164
|
+
const kRe = /\bkSecAttrAccessibleAlways(?:ThisDeviceOnly)?\b/g;
|
|
165
|
+
let m;
|
|
166
|
+
while ((m = kRe.exec(code))) {
|
|
167
|
+
const line = _lineOf(raw, m.index);
|
|
168
|
+
const id = `ios-keychain-accessible:${file}:${line}`;
|
|
169
|
+
if (seen.has(id)) continue;
|
|
170
|
+
seen.add(id);
|
|
171
|
+
out.push(_finding(file, line, raw, 'ios-keychain-accessible',
|
|
172
|
+
'Keychain item set to kSecAttrAccessibleAlways — accessible without device unlock',
|
|
173
|
+
'ios-keychain-accessible', 'high', 'CWE-922',
|
|
174
|
+
'Use kSecAttrAccessibleWhenUnlocked (or kSecAttrAccessibleWhenUnlockedThisDeviceOnly for highest sensitivity). The "Always" variants don\'t require device unlock and are accessible to forensic tools.',
|
|
175
|
+
'keychain-always-accessible'));
|
|
176
|
+
}
|
|
177
|
+
// LAPolicy.deviceOwnerAuthentication (allows passcode fallback)
|
|
178
|
+
const bRe = /\bLAPolicy\s*\.\s*deviceOwnerAuthentication\b(?!WithBiometrics)/g;
|
|
179
|
+
while ((m = bRe.exec(code))) {
|
|
180
|
+
const line = _lineOf(raw, m.index);
|
|
181
|
+
const id = `ios-biometric-fallback:${file}:${line}`;
|
|
182
|
+
if (seen.has(id)) continue;
|
|
183
|
+
seen.add(id);
|
|
184
|
+
out.push(_finding(file, line, raw, 'ios-biometric-fallback',
|
|
185
|
+
'LAPolicy.deviceOwnerAuthentication — allows passcode fallback for biometric prompt',
|
|
186
|
+
'ios-biometric-fallback', 'medium', 'CWE-308',
|
|
187
|
+
'For sensitive operations (payments, vault access) use LAPolicy.deviceOwnerAuthenticationWithBiometrics to require Face/Touch ID with NO passcode fallback. Passcode fallback opens the operation to anyone who knows the passcode.',
|
|
188
|
+
'biometric-with-fallback'));
|
|
189
|
+
}
|
|
190
|
+
// WKWebView.load(URLRequest(url: tainted))
|
|
191
|
+
const wRe = /\b\w+\s*\.\s*load\s*\(\s*URLRequest\s*\(\s*url\s*:\s*(?!URL\s*\(\s*string\s*:\s*["'])/g;
|
|
192
|
+
while ((m = wRe.exec(code))) {
|
|
193
|
+
const line = _lineOf(raw, m.index);
|
|
194
|
+
const id = `ios-webview-untrusted-url:${file}:${line}`;
|
|
195
|
+
if (seen.has(id)) continue;
|
|
196
|
+
seen.add(id);
|
|
197
|
+
out.push(_finding(file, line, raw, 'ios-webview-untrusted-url',
|
|
198
|
+
'WKWebView loaded with non-literal URL — open-redirect / phishing risk',
|
|
199
|
+
'ios-webview-untrusted-url', 'medium', 'CWE-601',
|
|
200
|
+
'Validate the URL against an allow-list of schemes (https only) and hosts before passing to URLRequest. WKWebView reachable from a deeplink with attacker-controlled URL = in-app phishing surface.',
|
|
201
|
+
'webview-untrusted-url'));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Public entry point ─────────────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export function scanMobile(fp, raw) {
|
|
208
|
+
if (!raw || raw.length > 500_000) return [];
|
|
209
|
+
const out = [];
|
|
210
|
+
const seen = new Set();
|
|
211
|
+
try {
|
|
212
|
+
if (/AndroidManifest\.xml$/i.test(fp)) _scanAndroidManifest(fp, raw, out, seen);
|
|
213
|
+
else if (/Info\.plist$/i.test(fp)) _scanIosPlist(fp, raw, out, seen);
|
|
214
|
+
else if (/\.(?:kt|java)$/i.test(fp) && /\b(?:android\.|androidx\.|Activity|Service|BroadcastReceiver|WebView|SharedPreferences)\b/.test(raw)) {
|
|
215
|
+
_scanAndroidSource(fp, raw, out, seen);
|
|
216
|
+
}
|
|
217
|
+
else if (/\.(?:swift|m|mm)$/i.test(fp) && /\b(?:NS|UI|WK|LA|kSec|Foundation|UIKit|SwiftUI)\w*/.test(raw)) {
|
|
218
|
+
_scanIosSource(fp, raw, out, seen);
|
|
219
|
+
}
|
|
220
|
+
} catch {}
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export const _internals = { _scanAndroidManifest, _scanAndroidSource, _scanIosPlist, _scanIosSource };
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// Post-quantum cryptography migration scanner — Recommendation #2 of the
|
|
2
|
+
// world-class+3 plan.
|
|
3
|
+
//
|
|
4
|
+
// NIST finalized the first PQC standards in 2024 (FIPS 203 ML-KEM, FIPS 204
|
|
5
|
+
// ML-DSA, FIPS 205 SLH-DSA). The Harvest-Now-Decrypt-Later (HNDL) threat is
|
|
6
|
+
// already real: traffic encrypted with RSA/ECDH today can be stored and
|
|
7
|
+
// decrypted by a future cryptographically-relevant quantum computer (CRQC).
|
|
8
|
+
// Federal mandates target full migration by 2035; private-sector exposure
|
|
9
|
+
// runs through long-lived signing keys (code-signing, TLS, JWT), data at
|
|
10
|
+
// rest, and authenticated key exchange.
|
|
11
|
+
//
|
|
12
|
+
// This module catalogs every pre-quantum asymmetric primitive used in the
|
|
13
|
+
// codebase and emits a migration finding with:
|
|
14
|
+
//
|
|
15
|
+
// - Algorithm family (RSA, ECDSA, ECDH, DSA, DH, X25519/Ed25519)
|
|
16
|
+
// - Use case (signing, encryption, KEX, key generation)
|
|
17
|
+
// - Recommended PQC replacement (ML-KEM, ML-DSA, SLH-DSA, hybrid)
|
|
18
|
+
// - Migration tier (LONG_LIVED_KEY, SIGNING, KEX, EPHEMERAL)
|
|
19
|
+
// - Sensitivity context (HNDL-relevant when wrapping PII / secrets)
|
|
20
|
+
//
|
|
21
|
+
// Findings live under family 'pqc-migration' with subfamily strings:
|
|
22
|
+
// pqc-rsa-keygen RSA key generation (signing or encryption)
|
|
23
|
+
// pqc-rsa-encrypt RSA encryption of data (HNDL-critical)
|
|
24
|
+
// pqc-rsa-sign RSA signing (degrades when CRQC arrives)
|
|
25
|
+
// pqc-ecdsa-sign ECDSA signing (curve agnostic)
|
|
26
|
+
// pqc-ecdh-kex ECDH key exchange (HNDL-critical)
|
|
27
|
+
// pqc-dh-kex Classical Diffie-Hellman
|
|
28
|
+
// pqc-dsa DSA (already weak; PQ migration is acute)
|
|
29
|
+
// pqc-x25519 X25519 KEM-style use (HNDL-critical when long-lived)
|
|
30
|
+
// pqc-ed25519 Ed25519 signing (PQ-vulnerable but well-studied)
|
|
31
|
+
// pqc-tls-config TLS configuration not allowing PQ hybrid groups
|
|
32
|
+
// pqc-jwt-classical JWT signed with RS256/ES256 (long-lived tokens are HNDL surfaces)
|
|
33
|
+
//
|
|
34
|
+
// Detection runs over the comment-stripped source. Recognizes:
|
|
35
|
+
// JavaScript/TypeScript — node crypto, jsencrypt, node-forge, jsonwebtoken
|
|
36
|
+
// Python — cryptography, pycryptodome, paramiko, PyJWT
|
|
37
|
+
// Java — KeyPairGenerator, Signature, KeyAgreement
|
|
38
|
+
// Go — crypto/rsa, crypto/ecdsa, crypto/ecdh, crypto/dsa
|
|
39
|
+
// C# — RSACryptoServiceProvider, RSA.Create, ECDsa.Create
|
|
40
|
+
// C/C++ (OpenSSL) — RSA_generate_key, EVP_PKEY_RSA, EC_KEY_new_by_curve
|
|
41
|
+
//
|
|
42
|
+
// HNDL severity bumps: a hit inside a routine handling PII / secrets / TLS
|
|
43
|
+
// is upgraded to high. A naked RSA keygen in a CLI demo stays medium.
|
|
44
|
+
//
|
|
45
|
+
// Opt-out: AGENTIC_SECURITY_NO_PQC=1 disables the module entirely.
|
|
46
|
+
|
|
47
|
+
import { blankComments } from './_comment-strip.js';
|
|
48
|
+
|
|
49
|
+
const _CRYPTO_RELEVANCE = [
|
|
50
|
+
/\bcrypto\b/, /\bRSA\b/, /\bECDSA\b/, /\bECDH\b/, /\bEd25519\b/, /\bX25519\b/,
|
|
51
|
+
/\bDiffie[-_]?Hellman\b/i, /\bDH\b/, /\bDSA\b/,
|
|
52
|
+
/\bjsonwebtoken\b/, /\bPyJWT\b/, /\bjwt\b/i,
|
|
53
|
+
/\bcryptography\b/, /\bpycryptodome\b/, /\bparamiko\b/,
|
|
54
|
+
/\bKeyPairGenerator\b/, /\bSignature\b/, /\bKeyAgreement\b/,
|
|
55
|
+
/\bOpenSSL\b/i, /\bEVP_PKEY\b/, /\bRSA_generate\b/, /\bEC_KEY_\b/,
|
|
56
|
+
/\bRSACryptoServiceProvider\b/, /\bECDsa\b/,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function _isCryptoRelevant(text) {
|
|
60
|
+
return _CRYPTO_RELEVANCE.some(re => re.test(text));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const _HNDL_HINTS = [
|
|
64
|
+
/\bpii\b/i, /\bpersonal\b/i, /\bssn\b/i, /\bemail\b/i, /\bpassword\b/i,
|
|
65
|
+
/\bsecret\b/i, /\bcredential\b/i, /\btoken\b/i, /\bsession\b/i,
|
|
66
|
+
/\bencrypt(?:ed|ing)?\b/i, /\bwrap(?:ped)?\b/i, /\btls\b/i, /\bhttps?\b/i,
|
|
67
|
+
/\bcustomer\b/i, /\bmedical\b/i, /\bphi\b/i, /\bhipaa\b/i, /\bgdpr\b/i,
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
function _hndlContext(rawSlice) {
|
|
71
|
+
return _HNDL_HINTS.some(re => re.test(rawSlice));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
75
|
+
function _snip(raw, line) { return (raw.split('\n')[line - 1] || '').trim().slice(0, 200); }
|
|
76
|
+
function _context(raw, line, half = 8) {
|
|
77
|
+
const lines = raw.split('\n');
|
|
78
|
+
const start = Math.max(0, line - 1 - half);
|
|
79
|
+
const end = Math.min(lines.length, line - 1 + half);
|
|
80
|
+
return lines.slice(start, end).join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const PQC_REPLACEMENTS = {
|
|
84
|
+
'rsa-encrypt': { primary: 'ML-KEM-768', alt: 'ML-KEM-1024 (192-bit security)', hybrid: 'X25519+ML-KEM-768 (CNSA 2.0 transitional)' },
|
|
85
|
+
'rsa-sign': { primary: 'ML-DSA-65', alt: 'ML-DSA-87 / SLH-DSA-SHA2-128s (stateless hash-based)', hybrid: 'RSA-3072 + ML-DSA-65 (composite signature)' },
|
|
86
|
+
'ecdsa-sign': { primary: 'ML-DSA-65', alt: 'FALCON-512 (smaller signatures) / SLH-DSA (conservative)', hybrid: 'ECDSA-P256 + ML-DSA-65' },
|
|
87
|
+
'ecdh-kex': { primary: 'ML-KEM-768', alt: 'ML-KEM-1024', hybrid: 'X25519 + ML-KEM-768 (RFC 9794)' },
|
|
88
|
+
'dh-kex': { primary: 'ML-KEM-768', alt: 'ML-KEM-1024', hybrid: 'ML-KEM-768 only — classical DH is end-of-life' },
|
|
89
|
+
'dsa': { primary: 'ML-DSA-65', alt: 'SLH-DSA', hybrid: 'no hybrid — DSA is already deprecated' },
|
|
90
|
+
'x25519': { primary: 'ML-KEM-768', alt: 'ML-KEM-1024', hybrid: 'X25519 + ML-KEM-768' },
|
|
91
|
+
'ed25519': { primary: 'ML-DSA-65', alt: 'FALCON-512', hybrid: 'Ed25519 + ML-DSA-65 (composite)' },
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function _findingShape(file, raw, line, ruleId, subfamily, useCase, family, isHndl) {
|
|
95
|
+
const replacement = PQC_REPLACEMENTS[useCase] || { primary: 'ML-KEM/ML-DSA', alt: '—', hybrid: '—' };
|
|
96
|
+
return {
|
|
97
|
+
id: `${ruleId}:${file}:${line}`,
|
|
98
|
+
file, line,
|
|
99
|
+
severity: isHndl ? 'high' : 'medium',
|
|
100
|
+
confidence: 0.85,
|
|
101
|
+
stride: 'Information Disclosure',
|
|
102
|
+
snippet: _snip(raw, line),
|
|
103
|
+
parser: 'PQC',
|
|
104
|
+
family: 'pqc-migration',
|
|
105
|
+
subfamily,
|
|
106
|
+
cwe: 'CWE-327',
|
|
107
|
+
vuln: `Pre-quantum ${family.toUpperCase()} (${useCase}) — replace with ${replacement.primary} before CRQC arrives`,
|
|
108
|
+
description: isHndl
|
|
109
|
+
? `${family.toUpperCase()} appears alongside PII / secrets / TLS context. HNDL exposure: any traffic captured today is decryptable when a cryptographically-relevant quantum computer arrives. NIST recommends migration complete by 2035; federal mandates set 2030 for high-impact systems.`
|
|
110
|
+
: `${family.toUpperCase()} is vulnerable to Shor's algorithm. Schedule migration to post-quantum primitives. NIST finalized FIPS 203 (ML-KEM), FIPS 204 (ML-DSA), and FIPS 205 (SLH-DSA) in 2024.`,
|
|
111
|
+
remediation: [
|
|
112
|
+
`Recommended replacement: **${replacement.primary}**`,
|
|
113
|
+
`Alternative: ${replacement.alt}`,
|
|
114
|
+
`Hybrid (transitional): ${replacement.hybrid}`,
|
|
115
|
+
'See NIST IR 8547 (migration to PQC) and CNSA 2.0 timeline. Open-source libraries: liboqs (C), open-quantum-safe/oqs-provider (OpenSSL 3 provider), pq-crystals (reference impls).',
|
|
116
|
+
].join('\n'),
|
|
117
|
+
pqcRecommendation: replacement,
|
|
118
|
+
hndlCritical: isHndl,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ── Detectors ──────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
function detectJavaScript(file, raw, code, out, seen) {
|
|
125
|
+
const patterns = [
|
|
126
|
+
// node crypto: generateKeyPair('rsa', ...) / generateKeyPairSync('rsa')
|
|
127
|
+
{ re: /\bgenerateKeyPair(?:Sync)?\s*\(\s*['"`]rsa['"`]/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
128
|
+
{ re: /\bgenerateKeyPair(?:Sync)?\s*\(\s*['"`]ec['"`]/g, sub: 'pqc-ecdh-kex', use: 'ecdh-kex', fam: 'ec' },
|
|
129
|
+
{ re: /\bgenerateKeyPair(?:Sync)?\s*\(\s*['"`]dsa['"`]/g, sub: 'pqc-dsa', use: 'dsa', fam: 'dsa' },
|
|
130
|
+
{ re: /\bgenerateKeyPair(?:Sync)?\s*\(\s*['"`]dh['"`]/g, sub: 'pqc-dh-kex', use: 'dh-kex', fam: 'dh' },
|
|
131
|
+
{ re: /\bgenerateKeyPair(?:Sync)?\s*\(\s*['"`]x25519['"`]/g, sub: 'pqc-x25519', use: 'x25519', fam: 'x25519' },
|
|
132
|
+
{ re: /\bgenerateKeyPair(?:Sync)?\s*\(\s*['"`]ed25519['"`]/g, sub: 'pqc-ed25519', use: 'ed25519', fam: 'ed25519' },
|
|
133
|
+
// createSign('RSA-SHA*') / createVerify
|
|
134
|
+
{ re: /\bcreate(?:Sign|Verify)\s*\(\s*['"`]RSA-SHA/g, sub: 'pqc-rsa-sign', use: 'rsa-sign', fam: 'rsa' },
|
|
135
|
+
{ re: /\bcreate(?:Sign|Verify)\s*\(\s*['"`]ecdsa/gi, sub: 'pqc-ecdsa-sign', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
136
|
+
// publicEncrypt / privateDecrypt (RSA encryption path)
|
|
137
|
+
{ re: /\bpublicEncrypt\s*\(|\bprivateDecrypt\s*\(/g, sub: 'pqc-rsa-encrypt', use: 'rsa-encrypt', fam: 'rsa' },
|
|
138
|
+
// diffieHellman
|
|
139
|
+
{ re: /\bcreateDiffieHellman\s*\(|\bcreateECDH\s*\(/g, sub: 'pqc-dh-kex', use: 'dh-kex', fam: 'dh' },
|
|
140
|
+
// jsonwebtoken algorithms (HNDL-relevant for long-lived JWTs)
|
|
141
|
+
{ re: /algorithm\s*:\s*['"`](?:RS|PS)256['"`]/g, sub: 'pqc-jwt-classical', use: 'rsa-sign', fam: 'rsa' },
|
|
142
|
+
{ re: /algorithm\s*:\s*['"`]ES256['"`]/g, sub: 'pqc-jwt-classical', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
143
|
+
// node-forge / jsencrypt
|
|
144
|
+
{ re: /\bforge\.pki\.rsa\.generateKeyPair\b/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
145
|
+
{ re: /\bnew\s+JSEncrypt\s*\(/g, sub: 'pqc-rsa-encrypt', use: 'rsa-encrypt', fam: 'rsa' },
|
|
146
|
+
];
|
|
147
|
+
for (const p of patterns) {
|
|
148
|
+
let m;
|
|
149
|
+
while ((m = p.re.exec(code))) {
|
|
150
|
+
const line = _lineOf(raw, m.index);
|
|
151
|
+
const id = `${p.sub}:${file}:${line}`;
|
|
152
|
+
if (seen.has(id)) continue;
|
|
153
|
+
seen.add(id);
|
|
154
|
+
const isHndl = _hndlContext(_context(raw, line));
|
|
155
|
+
out.push(_findingShape(file, raw, line, p.sub, p.sub, p.use, p.fam, isHndl));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function detectPython(file, raw, code, out, seen) {
|
|
161
|
+
const patterns = [
|
|
162
|
+
{ re: /\brsa\.generate_private_key\s*\(/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
163
|
+
{ re: /\bec\.generate_private_key\s*\(/g, sub: 'pqc-ecdh-kex', use: 'ecdh-kex', fam: 'ec' },
|
|
164
|
+
{ re: /\bdsa\.generate_private_key\s*\(/g, sub: 'pqc-dsa', use: 'dsa', fam: 'dsa' },
|
|
165
|
+
{ re: /\bdh\.generate_parameters\s*\(/g, sub: 'pqc-dh-kex', use: 'dh-kex', fam: 'dh' },
|
|
166
|
+
// pycryptodome
|
|
167
|
+
{ re: /\bRSA\.generate\s*\(/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
168
|
+
{ re: /\bECC\.generate\s*\(/g, sub: 'pqc-ecdsa-sign', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
169
|
+
{ re: /\bDSA\.generate\s*\(/g, sub: 'pqc-dsa', use: 'dsa', fam: 'dsa' },
|
|
170
|
+
// paramiko
|
|
171
|
+
{ re: /\bparamiko\.RSAKey\b/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
172
|
+
{ re: /\bparamiko\.ECDSAKey\b/g, sub: 'pqc-ecdsa-sign', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
173
|
+
{ re: /\bparamiko\.Ed25519Key\b/g, sub: 'pqc-ed25519', use: 'ed25519', fam: 'ed25519' },
|
|
174
|
+
// PyJWT
|
|
175
|
+
{ re: /\bjwt\.encode\s*\([^)]*algorithm\s*=\s*['"](?:RS|PS)256['"]/g, sub: 'pqc-jwt-classical', use: 'rsa-sign', fam: 'rsa' },
|
|
176
|
+
{ re: /\bjwt\.encode\s*\([^)]*algorithm\s*=\s*['"]ES256['"]/g, sub: 'pqc-jwt-classical', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
177
|
+
// cryptography hazmat encryption
|
|
178
|
+
{ re: /\bpadding\.OAEP\s*\(/g, sub: 'pqc-rsa-encrypt', use: 'rsa-encrypt', fam: 'rsa' },
|
|
179
|
+
{ re: /\bpadding\.PKCS1v15\s*\(/g, sub: 'pqc-rsa-sign', use: 'rsa-sign', fam: 'rsa' },
|
|
180
|
+
{ re: /\bX25519PrivateKey\.generate\s*\(/g, sub: 'pqc-x25519', use: 'x25519', fam: 'x25519' },
|
|
181
|
+
{ re: /\bEd25519PrivateKey\.generate\s*\(/g, sub: 'pqc-ed25519', use: 'ed25519', fam: 'ed25519' },
|
|
182
|
+
];
|
|
183
|
+
for (const p of patterns) {
|
|
184
|
+
let m;
|
|
185
|
+
while ((m = p.re.exec(code))) {
|
|
186
|
+
const line = _lineOf(raw, m.index);
|
|
187
|
+
const id = `${p.sub}:${file}:${line}`;
|
|
188
|
+
if (seen.has(id)) continue;
|
|
189
|
+
seen.add(id);
|
|
190
|
+
const isHndl = _hndlContext(_context(raw, line));
|
|
191
|
+
out.push(_findingShape(file, raw, line, p.sub, p.sub, p.use, p.fam, isHndl));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function detectJava(file, raw, code, out, seen) {
|
|
197
|
+
const patterns = [
|
|
198
|
+
{ re: /\bKeyPairGenerator\.getInstance\s*\(\s*"RSA"/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
199
|
+
{ re: /\bKeyPairGenerator\.getInstance\s*\(\s*"EC(?:DSA)?"/g, sub: 'pqc-ecdsa-sign', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
200
|
+
{ re: /\bKeyPairGenerator\.getInstance\s*\(\s*"DSA"/g, sub: 'pqc-dsa', use: 'dsa', fam: 'dsa' },
|
|
201
|
+
{ re: /\bKeyPairGenerator\.getInstance\s*\(\s*"DH"/g, sub: 'pqc-dh-kex', use: 'dh-kex', fam: 'dh' },
|
|
202
|
+
{ re: /\bSignature\.getInstance\s*\(\s*"[A-Za-z0-9]+with(?:RSA|RSAandMGF1)"/g, sub: 'pqc-rsa-sign', use: 'rsa-sign', fam: 'rsa' },
|
|
203
|
+
{ re: /\bSignature\.getInstance\s*\(\s*"[A-Za-z0-9]+withECDSA"/g, sub: 'pqc-ecdsa-sign', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
204
|
+
{ re: /\bSignature\.getInstance\s*\(\s*"Ed25519"/g, sub: 'pqc-ed25519', use: 'ed25519', fam: 'ed25519' },
|
|
205
|
+
{ re: /\bKeyAgreement\.getInstance\s*\(\s*"ECDH"/g, sub: 'pqc-ecdh-kex', use: 'ecdh-kex', fam: 'ecdh' },
|
|
206
|
+
{ re: /\bKeyAgreement\.getInstance\s*\(\s*"DH"/g, sub: 'pqc-dh-kex', use: 'dh-kex', fam: 'dh' },
|
|
207
|
+
{ re: /\bKeyAgreement\.getInstance\s*\(\s*"XDH"/g, sub: 'pqc-x25519', use: 'x25519', fam: 'x25519' },
|
|
208
|
+
{ re: /\bCipher\.getInstance\s*\(\s*"RSA[^"]*"/g, sub: 'pqc-rsa-encrypt', use: 'rsa-encrypt', fam: 'rsa' },
|
|
209
|
+
];
|
|
210
|
+
for (const p of patterns) {
|
|
211
|
+
let m;
|
|
212
|
+
while ((m = p.re.exec(code))) {
|
|
213
|
+
const line = _lineOf(raw, m.index);
|
|
214
|
+
const id = `${p.sub}:${file}:${line}`;
|
|
215
|
+
if (seen.has(id)) continue;
|
|
216
|
+
seen.add(id);
|
|
217
|
+
const isHndl = _hndlContext(_context(raw, line));
|
|
218
|
+
out.push(_findingShape(file, raw, line, p.sub, p.sub, p.use, p.fam, isHndl));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function detectGo(file, raw, code, out, seen) {
|
|
224
|
+
const patterns = [
|
|
225
|
+
{ re: /\brsa\.GenerateKey\s*\(/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
226
|
+
{ re: /\bcrypto\/rsa\b/g, sub: 'pqc-rsa-sign', use: 'rsa-sign', fam: 'rsa' },
|
|
227
|
+
{ re: /\becdsa\.GenerateKey\s*\(/g, sub: 'pqc-ecdsa-sign', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
228
|
+
{ re: /\bcrypto\/ecdh\b/g, sub: 'pqc-ecdh-kex', use: 'ecdh-kex', fam: 'ecdh' },
|
|
229
|
+
{ re: /\bdsa\.GenerateParameters\s*\(/g, sub: 'pqc-dsa', use: 'dsa', fam: 'dsa' },
|
|
230
|
+
{ re: /\bed25519\.GenerateKey\s*\(/g, sub: 'pqc-ed25519', use: 'ed25519', fam: 'ed25519' },
|
|
231
|
+
{ re: /\brsa\.EncryptOAEP\s*\(|\brsa\.EncryptPKCS1v15\s*\(/g, sub: 'pqc-rsa-encrypt', use: 'rsa-encrypt', fam: 'rsa' },
|
|
232
|
+
{ re: /\brsa\.SignPKCS1v15\s*\(|\brsa\.SignPSS\s*\(/g, sub: 'pqc-rsa-sign', use: 'rsa-sign', fam: 'rsa' },
|
|
233
|
+
];
|
|
234
|
+
for (const p of patterns) {
|
|
235
|
+
let m;
|
|
236
|
+
while ((m = p.re.exec(code))) {
|
|
237
|
+
const line = _lineOf(raw, m.index);
|
|
238
|
+
const id = `${p.sub}:${file}:${line}`;
|
|
239
|
+
if (seen.has(id)) continue;
|
|
240
|
+
seen.add(id);
|
|
241
|
+
const isHndl = _hndlContext(_context(raw, line));
|
|
242
|
+
out.push(_findingShape(file, raw, line, p.sub, p.sub, p.use, p.fam, isHndl));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function detectCSharp(file, raw, code, out, seen) {
|
|
248
|
+
const patterns = [
|
|
249
|
+
{ re: /\bnew\s+RSACryptoServiceProvider\s*\(/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
250
|
+
{ re: /\bRSA\.Create\s*\(/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
251
|
+
{ re: /\bECDsa\.Create\s*\(/g, sub: 'pqc-ecdsa-sign', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
252
|
+
{ re: /\bECDiffieHellman\.Create\s*\(/g, sub: 'pqc-ecdh-kex', use: 'ecdh-kex', fam: 'ecdh' },
|
|
253
|
+
{ re: /\bnew\s+DSACryptoServiceProvider\s*\(/g, sub: 'pqc-dsa', use: 'dsa', fam: 'dsa' },
|
|
254
|
+
{ re: /\bRSA\.Create\s*\([^)]*\)\.Encrypt\s*\(/g, sub: 'pqc-rsa-encrypt', use: 'rsa-encrypt', fam: 'rsa' },
|
|
255
|
+
];
|
|
256
|
+
for (const p of patterns) {
|
|
257
|
+
let m;
|
|
258
|
+
while ((m = p.re.exec(code))) {
|
|
259
|
+
const line = _lineOf(raw, m.index);
|
|
260
|
+
const id = `${p.sub}:${file}:${line}`;
|
|
261
|
+
if (seen.has(id)) continue;
|
|
262
|
+
seen.add(id);
|
|
263
|
+
const isHndl = _hndlContext(_context(raw, line));
|
|
264
|
+
out.push(_findingShape(file, raw, line, p.sub, p.sub, p.use, p.fam, isHndl));
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function detectCpp(file, raw, code, out, seen) {
|
|
270
|
+
const patterns = [
|
|
271
|
+
{ re: /\bRSA_generate_key(?:_ex)?\s*\(/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
272
|
+
{ re: /\bEVP_PKEY_keygen\s*\(/g, sub: 'pqc-rsa-keygen', use: 'rsa-sign', fam: 'rsa' },
|
|
273
|
+
{ re: /\bEC_KEY_new_by_curve_name\s*\(/g, sub: 'pqc-ecdsa-sign', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
274
|
+
{ re: /\bEVP_PKEY_RSA\b|\bEVP_PKEY_RSA_PSS\b/g, sub: 'pqc-rsa-sign', use: 'rsa-sign', fam: 'rsa' },
|
|
275
|
+
{ re: /\bEVP_PKEY_EC\b/g, sub: 'pqc-ecdsa-sign', use: 'ecdsa-sign', fam: 'ecdsa' },
|
|
276
|
+
{ re: /\bDH_generate_parameters\s*\(|\bDH_generate_key\s*\(/g, sub: 'pqc-dh-kex', use: 'dh-kex', fam: 'dh' },
|
|
277
|
+
{ re: /\bEVP_PKEY_X25519\b/g, sub: 'pqc-x25519', use: 'x25519', fam: 'x25519' },
|
|
278
|
+
{ re: /\bEVP_PKEY_ED25519\b/g, sub: 'pqc-ed25519', use: 'ed25519', fam: 'ed25519' },
|
|
279
|
+
];
|
|
280
|
+
for (const p of patterns) {
|
|
281
|
+
let m;
|
|
282
|
+
while ((m = p.re.exec(code))) {
|
|
283
|
+
const line = _lineOf(raw, m.index);
|
|
284
|
+
const id = `${p.sub}:${file}:${line}`;
|
|
285
|
+
if (seen.has(id)) continue;
|
|
286
|
+
seen.add(id);
|
|
287
|
+
const isHndl = _hndlContext(_context(raw, line));
|
|
288
|
+
out.push(_findingShape(file, raw, line, p.sub, p.sub, p.use, p.fam, isHndl));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function detectTlsConfig(file, raw, code, out, seen) {
|
|
294
|
+
// Detect TLS configs that limit groups/curves to classical ones only —
|
|
295
|
+
// a missed opportunity to enable PQ hybrid (x25519_kyber768, etc).
|
|
296
|
+
// Pattern: setEnabledCurves / honorCipherOrder / minProtocolVersion etc.
|
|
297
|
+
const patterns = [
|
|
298
|
+
{ re: /\bsetEnabledProtocols\s*\(\s*new\s+String\[\]\s*\{\s*"TLSv1\.[0-2]"/g },
|
|
299
|
+
{ re: /\bssl_min_version\s*=\s*['"]TLSv?1\.[0-2]['"]/g },
|
|
300
|
+
{ re: /\bgroups?\s*[:=]\s*['"]\s*(?:secp256r1|secp384r1|x25519)[\s'",]*['"]?\s*$/gm },
|
|
301
|
+
];
|
|
302
|
+
for (const p of patterns) {
|
|
303
|
+
let m;
|
|
304
|
+
while ((m = p.re.exec(code))) {
|
|
305
|
+
const line = _lineOf(raw, m.index);
|
|
306
|
+
const id = `pqc-tls-config:${file}:${line}`;
|
|
307
|
+
if (seen.has(id)) continue;
|
|
308
|
+
seen.add(id);
|
|
309
|
+
out.push({
|
|
310
|
+
id, file, line,
|
|
311
|
+
severity: 'medium', confidence: 0.7,
|
|
312
|
+
stride: 'Information Disclosure',
|
|
313
|
+
snippet: _snip(raw, line),
|
|
314
|
+
parser: 'PQC', family: 'pqc-migration',
|
|
315
|
+
subfamily: 'pqc-tls-config',
|
|
316
|
+
cwe: 'CWE-327',
|
|
317
|
+
vuln: 'TLS configuration restricted to classical curves/groups — no PQ-hybrid available',
|
|
318
|
+
description: 'Modern TLS stacks (OpenSSL 3.2+, BoringSSL, Rustls 0.23+) support hybrid PQ key exchange groups such as X25519MLKEM768 (RFC 9794). Restricting groups to classical curves only forecloses negotiating PQ-safe sessions even when the peer supports them — extending HNDL exposure unnecessarily.',
|
|
319
|
+
remediation: 'Enable hybrid PQ groups in TLS configuration. Example: append "X25519MLKEM768" / "P256MLKEM768" to the curves list. See draft-ietf-tls-hybrid-design and oqs-provider docs for ALPN-compatible deployment.',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ── Entry point ────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
export function scanPqc(fp, raw) {
|
|
328
|
+
if (process.env.AGENTIC_SECURITY_NO_PQC === '1') return [];
|
|
329
|
+
if (!raw || raw.length > 500_000) return [];
|
|
330
|
+
if (!_isCryptoRelevant(raw)) return [];
|
|
331
|
+
const lang = /\.py$/.test(fp) ? 'py' : null;
|
|
332
|
+
const code = blankComments(raw, lang);
|
|
333
|
+
const out = [];
|
|
334
|
+
const seen = new Set();
|
|
335
|
+
try { detectJavaScript(fp, raw, code, out, seen); } catch {}
|
|
336
|
+
try { detectPython(fp, raw, code, out, seen); } catch {}
|
|
337
|
+
try { detectJava(fp, raw, code, out, seen); } catch {}
|
|
338
|
+
try { detectGo(fp, raw, code, out, seen); } catch {}
|
|
339
|
+
try { detectCSharp(fp, raw, code, out, seen); } catch {}
|
|
340
|
+
try { detectCpp(fp, raw, code, out, seen); } catch {}
|
|
341
|
+
try { detectTlsConfig(fp, raw, code, out, seen); } catch {}
|
|
342
|
+
for (const f of out) f.file = fp;
|
|
343
|
+
return out;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export const _internals = {
|
|
347
|
+
PQC_REPLACEMENTS, _CRYPTO_RELEVANCE, _isCryptoRelevant, _hndlContext, _HNDL_HINTS,
|
|
348
|
+
};
|