@clear-capabilities/agentic-security-scanner 0.78.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/bin/.agentic-security/findings.json +16 -16
- package/bin/.agentic-security/last-scan.json +16 -16
- package/bin/.agentic-security/last-scan.json.sig +1 -1
- package/bin/.agentic-security/scan-history.json +51 -0
- package/bin/.agentic-security/streak.json +5 -5
- package/bin/agentic-security.js +22 -7
- package/dist/178.index.js +1 -1
- package/dist/333.index.js +283 -0
- package/dist/384.index.js +1 -1
- package/dist/476.index.js +5 -5
- package/dist/637.index.js +1 -1
- package/dist/700.index.js +138 -0
- package/dist/718.index.js +53 -0
- package/dist/838.index.js +1 -1
- package/dist/985.index.js +95 -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 +29799 -7803
- package/src/.agentic-security/last-scan.json +29799 -7803
- package/src/.agentic-security/last-scan.json.sig +1 -1
- package/src/.agentic-security/scan-history.json +5119 -2611
- package/src/.agentic-security/streak.json +6 -6
- package/src/dataflow/.agentic-security/findings.json +2879 -308
- package/src/dataflow/.agentic-security/last-scan.json +2879 -308
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
- package/src/dataflow/.agentic-security/scan-history.json +68 -520
- package/src/dataflow/.agentic-security/streak.json +6 -7
- package/src/dataflow/cross-service-taint.js +201 -0
- package/src/dataflow/engine.js +52 -8
- 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 +890 -132
- package/src/integrations/index.js +2 -1
- package/src/ir/.agentic-security/findings.json +240 -6
- package/src/ir/.agentic-security/last-scan.json +240 -6
- package/src/ir/.agentic-security/last-scan.json.sig +1 -1
- package/src/ir/.agentic-security/scan-history.json +16 -594
- package/src/ir/.agentic-security/streak.json +8 -9
- package/src/ir/callgraph.js +27 -7
- 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/llm-validator/index.js +7 -5
- 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/audit.js +5 -0
- package/src/mcp/tools.js +90 -1
- package/src/posture/.agentic-security/findings.json +16809 -4367
- package/src/posture/.agentic-security/last-scan.json +16809 -4367
- package/src/posture/.agentic-security/last-scan.json.sig +1 -1
- package/src/posture/.agentic-security/scan-history.json +6689 -177
- package/src/posture/.agentic-security/streak.json +8 -7
- package/src/posture/api-contract.js +193 -0
- package/src/posture/attack-taxonomy.js +227 -0
- package/src/posture/calibration-drift.js +2 -1
- package/src/posture/calibration.js +3 -2
- 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/fix-history.js +8 -2
- 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/profile.js +4 -5
- package/src/posture/reachability-filter.js +33 -2
- package/src/posture/realtime-cve-monitor.js +214 -0
- package/src/posture/rule-overrides.js +2 -3
- package/src/posture/rule-pack-signing.js +2 -3
- package/src/posture/rule-synthesis.js +5 -6
- 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/security-trend.js +4 -7
- package/src/posture/state-dir.js +124 -0
- package/src/posture/streak.js +3 -0
- package/src/posture/suppressions.js +5 -8
- package/src/posture/threat-model-auto.js +268 -0
- package/src/posture/triage-learning.js +170 -0
- package/src/posture/triage.js +29 -6
- package/src/posture/validator-metrics.js +3 -6
- package/src/sast/.agentic-security/findings.json +996 -32
- package/src/sast/.agentic-security/last-scan.json +996 -32
- package/src/sast/.agentic-security/last-scan.json.sig +1 -1
- package/src/sast/.agentic-security/scan-history.json +565 -32
- package/src/sast/.agentic-security/streak.json +10 -8
- 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/db-taint.js +24 -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/rust.js +26 -0
- package/src/sast/web3-advanced.js +375 -0
- package/src/sca/.agentic-security/findings.json +6044 -171
- package/src/sca/.agentic-security/last-scan.json +6044 -171
- package/src/sca/.agentic-security/last-scan.json.sig +1 -1
- package/src/sca/.agentic-security/scan-history.json +83 -6
- package/src/sca/.agentic-security/streak.json +9 -9
- package/src/sca/CLAUDE.md +161 -0
- package/src/sca/binary-metadata.js +146 -0
- package/src/sca/py-package-functions.js +118 -0
- package/src/sca/sigstore-verify.js +215 -0
- package/src/sca/vendor-detect.js +53 -0
- package/src/report/.agentic-security/findings.json +0 -80
- package/src/report/.agentic-security/last-scan.json +0 -80
- package/src/report/.agentic-security/last-scan.json.sig +0 -1
- package/src/report/.agentic-security/scan-history.json +0 -35
- package/src/report/.agentic-security/streak.json +0 -22
package/src/sast/cpp.js
CHANGED
|
@@ -81,6 +81,33 @@ function _isStrcpyGuarded(ctx) {
|
|
|
81
81
|
return _SIZEOF_GUARD_RE.test(window);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
// Destination-size guard — Recommendation #6 of the SCA/SAST improvement
|
|
85
|
+
// plan. Read the first arg of a strcpy/strcat/sprintf call site and look
|
|
86
|
+
// backwards in the function for a `char dest[N];` declaration. If found,
|
|
87
|
+
// classify the buffer as "small" (≤ 256 bytes) or "large" (> 256) — the
|
|
88
|
+
// large case suggests the developer sized intentionally and we downgrade
|
|
89
|
+
// to medium confidence; the small case keeps the high-confidence finding.
|
|
90
|
+
// If no fixed-size declaration is found at all (heap-allocated, struct
|
|
91
|
+
// member, function parameter), we keep the original finding shape.
|
|
92
|
+
const _CHAR_BUFFER_DECL_RE = /\b(?:char|unsigned\s+char|signed\s+char|wchar_t|int8_t|uint8_t)\s+(\w+)\s*\[\s*(\d+|\w+)\s*\]\s*;/g;
|
|
93
|
+
function _classifyDestBuffer(ctx, destName) {
|
|
94
|
+
if (!destName) return { kind: 'unknown' };
|
|
95
|
+
const lines = ctx.raw.split('\n');
|
|
96
|
+
// Walk backwards from the current line; cap at file start.
|
|
97
|
+
const before = lines.slice(0, ctx.line).join('\n');
|
|
98
|
+
let bestSize = null;
|
|
99
|
+
let m;
|
|
100
|
+
const re = new RegExp(_CHAR_BUFFER_DECL_RE.source, 'g');
|
|
101
|
+
while ((m = re.exec(before))) {
|
|
102
|
+
if (m[1] !== destName) continue;
|
|
103
|
+
const sizeTxt = m[2];
|
|
104
|
+
if (/^\d+$/.test(sizeTxt)) bestSize = parseInt(sizeTxt, 10);
|
|
105
|
+
}
|
|
106
|
+
if (bestSize === null) return { kind: 'unknown' };
|
|
107
|
+
if (bestSize <= 256) return { kind: 'small-fixed', size: bestSize };
|
|
108
|
+
return { kind: 'large-fixed', size: bestSize };
|
|
109
|
+
}
|
|
110
|
+
|
|
84
111
|
// Format-string: only fire when the variable holding the format string was
|
|
85
112
|
// not assigned from a string literal earlier in the file.
|
|
86
113
|
function _isPrintfVarLiteral(ctx, varName) {
|
|
@@ -99,10 +126,21 @@ const FINDINGS = [
|
|
|
99
126
|
// variants on Windows and strlcpy on BSD/macOS.
|
|
100
127
|
{
|
|
101
128
|
id: 'cpp-strcpy', severity: 'high', cwe: 'CWE-120', family: 'buffer-overflow',
|
|
102
|
-
|
|
129
|
+
// Capture the destination identifier so we can apply the destination-size
|
|
130
|
+
// guard (Recommendation #6) — large fixed buffers downgrade severity.
|
|
131
|
+
re: /\b(strcpy|strcat|gets|stpcpy|sprintf)\s*\(\s*(\w+)/g,
|
|
103
132
|
vuln: 'Banned API — unbounded string copy/format (potential buffer overflow)',
|
|
104
133
|
remediation: 'Replace with the bounded variant: strcpy → strlcpy / strcpy_s; strcat → strlcat / strcat_s; gets → fgets(buf, sizeof(buf), stdin); sprintf → snprintf(buf, sizeof(buf), "%s", v). The unbounded form will silently overflow on attacker-controlled input.',
|
|
105
|
-
gate: (ctx) =>
|
|
134
|
+
gate: (ctx, m) => {
|
|
135
|
+
if (_isStrcpyGuarded(ctx)) return false;
|
|
136
|
+
// Destination classification — large fixed buffers are intentional
|
|
137
|
+
// and we demote them to a less-noisy emission. Small fixed buffers
|
|
138
|
+
// stay high-severity; unknown (heap / param) keeps the original behavior.
|
|
139
|
+
const destName = m && m[2];
|
|
140
|
+
ctx._destClass = _classifyDestBuffer(ctx, destName);
|
|
141
|
+
return true;
|
|
142
|
+
},
|
|
143
|
+
severityFor: (ctx) => ctx._destClass && ctx._destClass.kind === 'large-fixed' ? 'medium' : 'high',
|
|
106
144
|
},
|
|
107
145
|
{
|
|
108
146
|
// printf/warn-family: format string is ARG 1.
|
|
@@ -196,8 +234,97 @@ const FINDINGS = [
|
|
|
196
234
|
// common (bad) example pattern, not a real vulnerability.
|
|
197
235
|
gate: (ctx) => _isCryptoContextRand(ctx),
|
|
198
236
|
},
|
|
237
|
+
|
|
238
|
+
// ── Recommendation #6/7: C/C++ family expansion ──────────────────────────
|
|
239
|
+
|
|
240
|
+
// exec*-family with non-literal argument (CWE-78). Beyond system/popen.
|
|
241
|
+
{
|
|
242
|
+
id: 'cpp-exec-family', severity: 'critical', cwe: 'CWE-78', family: 'command-injection',
|
|
243
|
+
re: /\b(?:execl|execle|execlp|execlpe|execv|execve|execvp|execvpe|posix_spawn)\s*\(\s*(?!["'])\w+/g,
|
|
244
|
+
vuln: 'Command Injection — exec*() family with non-literal program path',
|
|
245
|
+
remediation: 'Pin the program path to a constant and pass arguments as a separate argv. Never pass user-controlled data as the program path itself; an attacker can substitute any binary on $PATH.',
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
// Weak crypto — OpenSSL legacy / EVP_des / MD5 / SHA1 (CWE-327).
|
|
249
|
+
{
|
|
250
|
+
id: 'cpp-weak-crypto-md', severity: 'high', cwe: 'CWE-327', family: 'weak-crypto',
|
|
251
|
+
re: /\b(?:MD5_(?:Init|Update|Final|MD5)|MD4_|MD2_|SHA1_(?:Init|Update|Final|SHA1)|RIPEMD160_)\s*\(/g,
|
|
252
|
+
vuln: 'Weak Cryptography — legacy MD5/MD4/SHA1/RIPEMD160 hash primitive',
|
|
253
|
+
remediation: 'Use SHA-256 or SHA-3 via the OpenSSL EVP interface (`EVP_sha256()` → `EVP_DigestInit_ex` → `EVP_DigestUpdate` → `EVP_DigestFinal_ex`). For password hashing use Argon2 (libsodium) or scrypt.',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 'cpp-weak-crypto-des', severity: 'high', cwe: 'CWE-327', family: 'weak-crypto',
|
|
257
|
+
re: /\b(?:DES_(?:set_key|ecb_encrypt|ncbc_encrypt|cbc_encrypt|ede3_cbc_encrypt)|RC2_|RC4_(?:set_key|encrypt|decrypt)|BF_(?:set_key|ecb_encrypt))\s*\(/g,
|
|
258
|
+
vuln: 'Weak Cryptography — legacy DES/3DES/RC2/RC4/Blowfish primitive',
|
|
259
|
+
remediation: 'Use AES-256 in GCM mode via the EVP interface (`EVP_aes_256_gcm()` → `EVP_EncryptInit_ex`). DES and RC4 are broken; 3DES is deprecated; Blowfish has a 64-bit block (Sweet32).',
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
id: 'cpp-weak-crypto-evp', severity: 'high', cwe: 'CWE-327', family: 'weak-crypto',
|
|
263
|
+
re: /\bEVP_(?:des_(?:ede3?)?_(?:cbc|ecb|cfb|ofb)|md5|md4|md2|sha1|rc4|rc2_(?:cbc|ecb|cfb|ofb)|bf_(?:cbc|ecb|cfb|ofb))\s*\(/g,
|
|
264
|
+
vuln: 'Weak Cryptography — EVP factory for legacy primitive (MD5/SHA1/DES/3DES/RC2/RC4/BF)',
|
|
265
|
+
remediation: 'Use a modern EVP factory: `EVP_aes_256_gcm()` for AEAD, `EVP_sha256()` / `EVP_sha3_256()` for hashing.',
|
|
266
|
+
},
|
|
267
|
+
|
|
268
|
+
// Hardcoded secret in C/C++ (CWE-798) — a string literal assigned to a
|
|
269
|
+
// variable matching credential naming. Same idea as the Java/JS detector
|
|
270
|
+
// but tuned to C idioms.
|
|
271
|
+
{
|
|
272
|
+
id: 'cpp-hardcoded-secret', severity: 'high', cwe: 'CWE-798', family: 'hardcoded-secret',
|
|
273
|
+
// `static const char *password = "literal";` or `#define PASSWORD "literal"`
|
|
274
|
+
re: /\b(?:const\s+)?char\s*(?:\*\s*)?(?:const\s+)?(\w*(?:password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|cred(?:ential)?s?|priv(?:ate)?[_-]?key)\w*)\s*\[?\s*\]?\s*=\s*"([^"]{8,})"/gi,
|
|
275
|
+
vuln: 'Hardcoded Secret — credential-named char assigned a string literal',
|
|
276
|
+
remediation: 'Load secrets at runtime from environment variables (`getenv("API_KEY")`), a secrets file with restricted permissions, or a vault SDK. Never compile a literal credential into the binary — `strings(1)` extracts every literal string and the binary ships with the secret embedded.',
|
|
277
|
+
gate: (ctx, m) => {
|
|
278
|
+
// Apply the same entropy filter to avoid the 468 FPs/1 TP problem.
|
|
279
|
+
const val = m && m[2];
|
|
280
|
+
if (!val) return false;
|
|
281
|
+
try {
|
|
282
|
+
// Lazy import — avoids a circular dep on the entropy module being
|
|
283
|
+
// present in older snapshots.
|
|
284
|
+
// eslint-disable-next-line no-unused-vars
|
|
285
|
+
const { classifySecretCandidate } = _entropyMod || {};
|
|
286
|
+
if (classifySecretCandidate) {
|
|
287
|
+
const r = classifySecretCandidate(val);
|
|
288
|
+
if (r.skip) return false;
|
|
289
|
+
}
|
|
290
|
+
} catch { /* fail open */ }
|
|
291
|
+
return true;
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
// Use-after-free / double-free — CWE-416 / CWE-415. Heuristic: same pointer
|
|
296
|
+
// referenced in a free() call earlier in the function, then dereferenced
|
|
297
|
+
// (or free'd again) later. Conservative on declared `nullptr`-after-free.
|
|
298
|
+
{
|
|
299
|
+
id: 'cpp-uaf-heuristic', severity: 'high', cwe: 'CWE-416', family: 'mem-unsafe',
|
|
300
|
+
re: /\bfree\s*\(\s*(\w+)\s*\)\s*;[\s\S]{0,400}?\b\1\s*(?:->|\[|=\s*[^=])/g,
|
|
301
|
+
vuln: 'Use-After-Free heuristic — free(p) followed by p->/p[ access in same function window',
|
|
302
|
+
remediation: 'After `free(p)`, set `p = NULL;` immediately. The compiler can\'t catch UAF in general; explicit nulling means later derefs crash on a null check instead of executing on freed memory.',
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
id: 'cpp-double-free', severity: 'high', cwe: 'CWE-415', family: 'mem-unsafe',
|
|
306
|
+
re: /\bfree\s*\(\s*(\w+)\s*\)\s*;[\s\S]{0,400}?\bfree\s*\(\s*\1\s*\)/g,
|
|
307
|
+
vuln: 'Double-free — free(p) followed by free(p) without nulling in between',
|
|
308
|
+
remediation: 'After `free(p)`, set `p = NULL;`. `free(NULL)` is a defined no-op in C; `free(already_freed_p)` is undefined behavior that can corrupt the allocator and pivot to RCE.',
|
|
309
|
+
},
|
|
310
|
+
|
|
311
|
+
// Cookie / Session / token = rand() shape — when rand() is used to mint
|
|
312
|
+
// session identifiers, even outside a strict "crypto" context. Distinct
|
|
313
|
+
// from the gated rand() rule above to catch the obvious Juliet shape.
|
|
314
|
+
{
|
|
315
|
+
id: 'cpp-rand-session-token', severity: 'high', cwe: 'CWE-338', family: 'weak-rng',
|
|
316
|
+
re: /\b(?:session_id|token|cookie|nonce|csrf|secret_key)\s*(?:=|\.|->)[\s\S]{0,200}?\b(?:rand|random)\s*\(/gi,
|
|
317
|
+
vuln: 'Weak Randomness — session/token/cookie value derived from rand()/random()',
|
|
318
|
+
remediation: 'Use getrandom() (Linux), RAND_bytes() (OpenSSL), or BCryptGenRandom() (Windows) for any identifier that has to be unguessable. rand() outputs are predictable to within 2^31 internal states.',
|
|
319
|
+
},
|
|
199
320
|
];
|
|
200
321
|
|
|
322
|
+
// Late-bound entropy module — imported via a dynamic require shim so the
|
|
323
|
+
// rule table can lazy-call it from inside gate functions without creating
|
|
324
|
+
// a circular import at module load time.
|
|
325
|
+
let _entropyMod = null;
|
|
326
|
+
import('./_secret-entropy.js').then(m => { _entropyMod = m; }).catch(() => { _entropyMod = null; });
|
|
327
|
+
|
|
201
328
|
function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
|
|
202
329
|
|
|
203
330
|
export function scanCpp(fp, raw) {
|
|
@@ -223,15 +350,22 @@ export function scanCpp(fp, raw) {
|
|
|
223
350
|
if (/^\s*#\s*define\b/.test(lineText)) continue;
|
|
224
351
|
// Per-rule contextual gate (Action 2). Suppress when the surrounding
|
|
225
352
|
// file/line context shows the call is not security-relevant.
|
|
353
|
+
const gateCtx = { file: fp, raw, line, lineText };
|
|
226
354
|
if (typeof rule.gate === 'function') {
|
|
227
355
|
try {
|
|
228
|
-
if (!rule.gate(
|
|
356
|
+
if (!rule.gate(gateCtx, m)) continue;
|
|
229
357
|
} catch { /* gate threw → fail open, keep finding */ }
|
|
230
358
|
}
|
|
359
|
+
// Per-finding severity override (Recommendation #6 — destination-size
|
|
360
|
+
// classifier downgrades large-fixed-buffer strcpy to medium).
|
|
361
|
+
let sev = rule.severity;
|
|
362
|
+
if (typeof rule.severityFor === 'function') {
|
|
363
|
+
try { sev = rule.severityFor(gateCtx, m) || sev; } catch { /* keep default */ }
|
|
364
|
+
}
|
|
231
365
|
out.push({
|
|
232
366
|
id, file: fp, line,
|
|
233
367
|
vuln: rule.vuln,
|
|
234
|
-
severity:
|
|
368
|
+
severity: sev,
|
|
235
369
|
cwe: rule.cwe,
|
|
236
370
|
stride: rule.family === 'buffer-overflow' || rule.family === 'mem-unsafe' ? 'Tampering'
|
|
237
371
|
: rule.family === 'command-injection' ? 'Elevation of Privilege'
|
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
// Crypto protocol analyzer — Item #6 of the world-class+3 plan.
|
|
2
|
+
//
|
|
3
|
+
// Coverage groups (one module, six families):
|
|
4
|
+
//
|
|
5
|
+
// TLS / mTLS:
|
|
6
|
+
// - crypto-tls-min-version minVersion / minProtocolVersion < TLS 1.2
|
|
7
|
+
// - crypto-tls-no-verify cert verification disabled (NODE_TLS_REJECT_UNAUTHORIZED,
|
|
8
|
+
// verify=False, InsecureSkipVerify, ALLOW_ALL_HOSTNAME_VERIFIER)
|
|
9
|
+
// - crypto-tls-weak-cipher RC4, 3DES, NULL, EXPORT, anonymous, DES
|
|
10
|
+
// - crypto-tls-fallback-scsv-missing (informational — when ciphers explicitly listed)
|
|
11
|
+
//
|
|
12
|
+
// Symmetric crypto:
|
|
13
|
+
// - crypto-weak-cipher DES / 3DES / RC4 / Blowfish primitive usage
|
|
14
|
+
// - crypto-ecb-mode AES/DES in ECB mode (deterministic plaintext)
|
|
15
|
+
// - crypto-static-iv Hard-coded IV / zero IV / 16-zero-byte literal
|
|
16
|
+
// - crypto-weak-hash MD5 / SHA1 used for security purpose
|
|
17
|
+
//
|
|
18
|
+
// Key derivation:
|
|
19
|
+
// - crypto-pbkdf2-low-iter PBKDF2 with iterations < OWASP floor
|
|
20
|
+
// - crypto-bcrypt-low-cost bcrypt rounds < 12
|
|
21
|
+
// - crypto-scrypt-weak-params scrypt N < 2^15 or unrealistic r/p
|
|
22
|
+
//
|
|
23
|
+
// JOSE / JWT:
|
|
24
|
+
// - crypto-jwt-none-alg alg: 'none' accepted
|
|
25
|
+
// - crypto-jwt-no-algs-allowlist jwt.verify without algorithms whitelist
|
|
26
|
+
// - crypto-jwt-no-iss-aud missing issuer/audience validation
|
|
27
|
+
// - crypto-jose-key-confusion asymmetric pub key passed where HMAC accepted
|
|
28
|
+
//
|
|
29
|
+
// Random:
|
|
30
|
+
// - crypto-weak-random Math.random / random.random / rand for crypto
|
|
31
|
+
//
|
|
32
|
+
// Timing:
|
|
33
|
+
// - crypto-non-ct-compare `==` / `equals` on secret string comparison
|
|
34
|
+
// (already partial in comparison-safety.js;
|
|
35
|
+
// this adds JS/Python/Go gaps)
|
|
36
|
+
//
|
|
37
|
+
// Per-language detection; defers to existing jwt-exp.js for the exp-claim check.
|
|
38
|
+
// Opt-out: AGENTIC_SECURITY_NO_CRYPTO_PROTO=1
|
|
39
|
+
|
|
40
|
+
import { blankComments } from './_comment-strip.js';
|
|
41
|
+
|
|
42
|
+
function _line(raw, idx) { return raw.slice(0, idx).split('\n').length; }
|
|
43
|
+
function _snip(raw, line) { return (raw.split('\n')[line - 1] || '').trim().slice(0, 200); }
|
|
44
|
+
|
|
45
|
+
function _shape(file, line, ruleId, vuln, fam, sev, cwe, remediation, description) {
|
|
46
|
+
return {
|
|
47
|
+
id: `${ruleId}:${file}:${line}`,
|
|
48
|
+
file, line, vuln, severity: sev, cwe,
|
|
49
|
+
family: fam, parser: 'CRYPTO-PROTO',
|
|
50
|
+
confidence: 0.85,
|
|
51
|
+
stride: 'Information Disclosure',
|
|
52
|
+
description: description || vuln,
|
|
53
|
+
remediation,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const _RELEVANCE = /\bTLS\b|\bSSL\b|\btls\b|tls_version|tls_minimum|min[_-]?version|min[_-]?protocol|verify|ciph(?:er|ersuite)|NODE_TLS|InsecureSkipVerify|rejectUnauthor|trust[_-]?all|allow[_-]?all|jwt\.|jsonwebtoken|jose|PyJWT|jwt\b|MD5|SHA1|sha-?1\b|md5\b|DES\b|3DES|RC4|Blowfish|ECB|pbkdf2|PBKDF2|bcrypt|scrypt|argon2|Math\.random|random\.random|java\.util\.Random|new Random/i;
|
|
58
|
+
|
|
59
|
+
function _isCryptoRelevant(text) { return _RELEVANCE.test(text); }
|
|
60
|
+
|
|
61
|
+
// ── TLS / mTLS ─────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function detectTlsMinVersion(file, raw, code, out, seen) {
|
|
64
|
+
const patterns = [
|
|
65
|
+
// node tls / https: { minVersion: 'TLSv1.1' } or 'TLSv1' or 'SSLv3'
|
|
66
|
+
{ re: /\bminVersion\s*:\s*['"`](?:TLSv?1(?:\.0|\.1)?|SSLv[23])['"`]/g },
|
|
67
|
+
// Python ssl: ssl.PROTOCOL_TLSv1, PROTOCOL_SSLv23
|
|
68
|
+
{ re: /\bssl\.PROTOCOL_(?:TLSv1(?:_[01])?|SSLv2|SSLv23|SSLv3)\b/g },
|
|
69
|
+
// Java: SSLContext.getInstance("TLSv1") / "SSL" / "TLSv1.1"
|
|
70
|
+
{ re: /\bSSLContext\.getInstance\s*\(\s*["'](?:SSL(?:v\d)?|TLSv?1(?:\.0|\.1)?)["']/g },
|
|
71
|
+
// Go: tls.VersionTLS10 / VersionTLS11 / VersionSSL30
|
|
72
|
+
{ re: /\btls\.Version(?:TLS1[01]|SSL30)\b/g },
|
|
73
|
+
// .NET: SslProtocols.Tls / Tls10 / Tls11 / Ssl3
|
|
74
|
+
{ re: /\bSslProtocols\.(?:Ssl[23]|Tls(?:10|11)?)\b(?!2)/g },
|
|
75
|
+
// OpenSSL config: SSLProtocol or ssl_min_protocol_version TLSv1
|
|
76
|
+
{ re: /\bssl_min_protocol_version\s+TLSv1(?:\.0|\.1)?\b/g },
|
|
77
|
+
];
|
|
78
|
+
for (const p of patterns) {
|
|
79
|
+
let m;
|
|
80
|
+
while ((m = p.re.exec(code))) {
|
|
81
|
+
const ln = _line(raw, m.index);
|
|
82
|
+
const id = `crypto-tls-min-version:${file}:${ln}`;
|
|
83
|
+
if (seen.has(id)) continue;
|
|
84
|
+
seen.add(id);
|
|
85
|
+
out.push(_shape(file, ln, 'crypto-tls-min-version',
|
|
86
|
+
'TLS configured to accept versions below TLS 1.2',
|
|
87
|
+
'crypto-tls-version', 'high', 'CWE-326',
|
|
88
|
+
'Set minVersion / minProtocolVersion to TLSv1.2 (mandatory floor) or TLSv1.3 (preferred). TLS 1.0/1.1 are deprecated (PCI-DSS 4.0, NIST SP 800-52 Rev 2) and SSLv2/v3 are broken (POODLE, DROWN).',
|
|
89
|
+
'Pre-1.2 TLS is broken or near-broken. Modern Chromium / Safari / Firefox already reject these versions for the public web; staying on them is a regulatory and practical liability.'));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function detectTlsNoVerify(file, raw, code, out, seen) {
|
|
95
|
+
const patterns = [
|
|
96
|
+
// Node: { rejectUnauthorized: false }
|
|
97
|
+
{ re: /\brejectUnauthorized\s*:\s*false\b/g, lang: 'node' },
|
|
98
|
+
// Node env: NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
99
|
+
{ re: /NODE_TLS_REJECT_UNAUTHORIZED\s*=\s*['"]?0['"]?/g, lang: 'node' },
|
|
100
|
+
// Python requests: verify=False
|
|
101
|
+
{ re: /\brequests\.(?:get|post|put|delete|patch|head|options)\s*\([^)]*verify\s*=\s*False\b/g, lang: 'python' },
|
|
102
|
+
{ re: /\bsession\.verify\s*=\s*False\b/g, lang: 'python' },
|
|
103
|
+
// Python urllib3.disable_warnings + InsecureRequestWarning
|
|
104
|
+
{ re: /urllib3\.disable_warnings\s*\(\s*[^)]*InsecureRequestWarning/g, lang: 'python' },
|
|
105
|
+
// Python ssl context: CERT_NONE / check_hostname=False
|
|
106
|
+
{ re: /\bcheck_hostname\s*=\s*False\b/g, lang: 'python' },
|
|
107
|
+
{ re: /\bssl\.CERT_NONE\b/g, lang: 'python' },
|
|
108
|
+
// Go: InsecureSkipVerify: true
|
|
109
|
+
{ re: /\bInsecureSkipVerify\s*:\s*true\b/g, lang: 'go' },
|
|
110
|
+
// Java: TrustManager that accepts anything (custom impl)
|
|
111
|
+
{ re: /\bcheckServerTrusted\s*\([^)]*\)\s*\{\s*\}/g, lang: 'java' },
|
|
112
|
+
// Java: HostnameVerifier ALLOW_ALL
|
|
113
|
+
{ re: /\bALLOW_ALL_HOSTNAME_VERIFIER\b|\bsetHostnameVerifier\s*\(\s*\([^)]*\)\s*->\s*true\s*\)/g, lang: 'java' },
|
|
114
|
+
// .NET: ServerCertificateValidationCallback = delegate { return true; }
|
|
115
|
+
{ re: /\bServerCertificateValidationCallback\s*=\s*[^;]+\btrue\s*[;}]/g, lang: 'dotnet' },
|
|
116
|
+
// C#: HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }
|
|
117
|
+
{ re: /\bDangerousAcceptAnyServerCertificateValidator\b/g, lang: 'dotnet' },
|
|
118
|
+
// curl in scripts: -k / --insecure
|
|
119
|
+
{ re: /\bcurl\s+[^|;\n]*(?:-k\b|--insecure\b)/g, lang: 'shell' },
|
|
120
|
+
];
|
|
121
|
+
for (const p of patterns) {
|
|
122
|
+
let m;
|
|
123
|
+
while ((m = p.re.exec(code))) {
|
|
124
|
+
const ln = _line(raw, m.index);
|
|
125
|
+
const id = `crypto-tls-no-verify:${file}:${ln}`;
|
|
126
|
+
if (seen.has(id)) continue;
|
|
127
|
+
seen.add(id);
|
|
128
|
+
out.push(_shape(file, ln, 'crypto-tls-no-verify',
|
|
129
|
+
'TLS certificate verification disabled — MITM-vulnerable',
|
|
130
|
+
'crypto-tls-no-verify', 'critical', 'CWE-295',
|
|
131
|
+
'Re-enable verification and pin the upstream\'s CA chain. If the upstream is internal with a self-signed cert, distribute the CA bundle and reference it explicitly (ca: fs.readFileSync(\'ca.pem\') / verify=\'ca.pem\').',
|
|
132
|
+
'TLS without verification reduces TLS to obfuscation. Any on-path attacker can present an arbitrary cert. The 2024 Sisense breach traced to a downstream library that defaulted verify off.'));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function detectWeakCiphers(file, raw, code, out, seen) {
|
|
138
|
+
const patterns = [
|
|
139
|
+
// Node Cipher creation
|
|
140
|
+
{ re: /\bcreateCipher(?:iv)?\s*\(\s*['"`](?:des|des3|des-ede|des-ede3|rc4|rc2|bf|blowfish|null)/gi },
|
|
141
|
+
// Java Cipher.getInstance("DES" / "RC4" / etc)
|
|
142
|
+
{ re: /\bCipher\.getInstance\s*\(\s*["'](?:DES(?:\/|"|')|RC4|RC2|3DES|DESede|Blowfish|NULL)/g },
|
|
143
|
+
// Python Crypto: from Crypto.Cipher import DES, ARC4, ...
|
|
144
|
+
{ re: /\bfrom\s+Crypto\.Cipher\s+import\s+(?:DES\b|ARC4\b|ARC2\b|Blowfish\b)/g },
|
|
145
|
+
// OpenSSL config: SSLCipherSuite RC4-SHA / 3DES
|
|
146
|
+
{ re: /\bSSLCipherSuite\b[^;\n]*(?:RC4|3DES|EXPORT|aNULL|eNULL)/g },
|
|
147
|
+
// ciphers string in TLS config: 'NULL:RC4:DES'
|
|
148
|
+
{ re: /\bciphers\s*:\s*['"`][^'"`]*(?:NULL|RC4|EXPORT|aNULL|eNULL|3DES|DES-CBC)/g },
|
|
149
|
+
// Go cipher.NewTripleDESCipher
|
|
150
|
+
{ re: /\bcipher\.New(?:TripleDESCipher|DESCipher)\b/g },
|
|
151
|
+
];
|
|
152
|
+
for (const p of patterns) {
|
|
153
|
+
let m;
|
|
154
|
+
while ((m = p.re.exec(code))) {
|
|
155
|
+
const ln = _line(raw, m.index);
|
|
156
|
+
const id = `crypto-weak-cipher:${file}:${ln}`;
|
|
157
|
+
if (seen.has(id)) continue;
|
|
158
|
+
seen.add(id);
|
|
159
|
+
out.push(_shape(file, ln, 'crypto-weak-cipher',
|
|
160
|
+
'Weak symmetric cipher in use (DES / 3DES / RC4 / Blowfish / NULL / EXPORT)',
|
|
161
|
+
'crypto-weak-cipher', 'high', 'CWE-327',
|
|
162
|
+
'Replace with AES-GCM (AES-256-GCM preferred) or ChaCha20-Poly1305. DES has 56-bit keys (brute-forceable in hours), 3DES is deprecated by NIST (SP 800-131A Rev 2), RC4 is broken (RFC 7465), Blowfish has small block size enabling birthday attacks on long ciphertexts.',
|
|
163
|
+
'These ciphers all have either too-small keys, broken cryptanalysis, or block-size limitations that make them unsafe. NIST has formally deprecated DES, 3DES, and recommends RC4 not be used at all.'));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function detectEcbMode(file, raw, code, out, seen) {
|
|
169
|
+
const patterns = [
|
|
170
|
+
{ re: /\bCipher\.getInstance\s*\(\s*["'](?:AES|DES|DESede)\/ECB/g, lang: 'java' },
|
|
171
|
+
{ re: /\bcreateCipher(?:iv)?\s*\(\s*['"`]aes-\d+-ecb\b/gi, lang: 'node' },
|
|
172
|
+
{ re: /\bAES\.new\s*\([^)]*AES\.MODE_ECB\b/g, lang: 'python' },
|
|
173
|
+
{ re: /\bcipher\.NewECBEncrypter\b|\bcipher\.NewECBDecrypter\b/g, lang: 'go' },
|
|
174
|
+
{ re: /\bModes\.ECB\b|\bSymmetricAlgorithm\.Mode\s*=\s*CipherMode\.ECB/g, lang: 'dotnet' },
|
|
175
|
+
];
|
|
176
|
+
for (const p of patterns) {
|
|
177
|
+
let m;
|
|
178
|
+
while ((m = p.re.exec(code))) {
|
|
179
|
+
const ln = _line(raw, m.index);
|
|
180
|
+
const id = `crypto-ecb-mode:${file}:${ln}`;
|
|
181
|
+
if (seen.has(id)) continue;
|
|
182
|
+
seen.add(id);
|
|
183
|
+
out.push(_shape(file, ln, 'crypto-ecb-mode',
|
|
184
|
+
'AES/DES in ECB mode — identical plaintext blocks produce identical ciphertext',
|
|
185
|
+
'crypto-ecb', 'high', 'CWE-327',
|
|
186
|
+
'Use AES-GCM (authenticated) or AES-CBC with HMAC-then-encrypt-then-MAC. Never ECB. The "ECB penguin" visualizes the leak: encrypting an image in ECB leaves the silhouette intact.'));
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function detectStaticIv(file, raw, code, out, seen) {
|
|
192
|
+
// 16 zero bytes literal: '0000000000000000' or Buffer.alloc(16) or [0]*16
|
|
193
|
+
const patterns = [
|
|
194
|
+
{ re: /\bBuffer\.alloc\s*\(\s*(?:12|16)\s*\)\s*(?:,|\)|;)/g },
|
|
195
|
+
{ re: /\bcreateCipheriv\s*\([^,]+,\s*[^,]+,\s*Buffer\.alloc\s*\(/g },
|
|
196
|
+
{ re: /\bcreateCipheriv\s*\([^,]+,\s*[^,]+,\s*['"`](?:0+|\\0+)['"`]/g },
|
|
197
|
+
{ re: /\bAES\.new\s*\([^,]+,[^,]+,\s*IV\s*=\s*b?['"](?:\\x00){8,}/g },
|
|
198
|
+
{ re: /\bcipher\.NewCBCEncrypter\s*\([^,]+,\s*make\s*\(\s*\[\]byte\s*,/g }, // make([]byte, blocksize)
|
|
199
|
+
];
|
|
200
|
+
for (const p of patterns) {
|
|
201
|
+
let m;
|
|
202
|
+
while ((m = p.re.exec(code))) {
|
|
203
|
+
const ln = _line(raw, m.index);
|
|
204
|
+
const id = `crypto-static-iv:${file}:${ln}`;
|
|
205
|
+
if (seen.has(id)) continue;
|
|
206
|
+
seen.add(id);
|
|
207
|
+
out.push(_shape(file, ln, 'crypto-static-iv',
|
|
208
|
+
'Static / zero IV — encrypts identical plaintexts to identical ciphertexts',
|
|
209
|
+
'crypto-static-iv', 'high', 'CWE-329',
|
|
210
|
+
'Generate the IV from a CSPRNG: `crypto.randomBytes(16)` / `secrets.token_bytes(16)` / `cryptoRand.Read(iv)`. For GCM use a 12-byte random nonce. Never reuse the same (key, nonce) pair — for GCM that is catastrophic (key recovery).',
|
|
211
|
+
'IV reuse breaks every standard block-cipher mode. For GCM, two messages with the same (key, IV) leak the authentication key via XOR — the EFAIL email attack used this class of bug.'));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function detectWeakHash(file, raw, code, out, seen) {
|
|
217
|
+
// MD5 / SHA1 used in security contexts. Context-aware: skip when the
|
|
218
|
+
// surrounding text indicates a non-security use (cache key, etag,
|
|
219
|
+
// content-addressable storage, dedupe id, etc.) so we don't duplicate
|
|
220
|
+
// false positives the existing weak-hash detector handles.
|
|
221
|
+
const NONSEC_CTX = /\bcache(?:[_-]?key)?\b|\betag\b|\bcdn\b|\bversion(?:Hash|Id)?\b|\bdedupe\b|\bcontent[_-]?(?:addressable|hash|id)\b|\bfingerprint\b|\bchecksum\b|\bid\(?\s*[=:]/i;
|
|
222
|
+
const SEC_CTX = /\bpassword\b|\bsecret\b|\btoken\b|\bsignature\b|\bsign\b|\bauth\b|\bhmac\b|\bcred\w*|\bkey\b/i;
|
|
223
|
+
const patterns = [
|
|
224
|
+
{ re: /\bcreateHash\s*\(\s*['"`](?:md5|sha1|md4|md2)['"`]/gi },
|
|
225
|
+
{ re: /\bcreateHmac\s*\(\s*['"`](?:md5|sha1)['"`]/gi },
|
|
226
|
+
{ re: /\bhashlib\.(?:md5|sha1|md4)\s*\(/g },
|
|
227
|
+
{ re: /\bMessageDigest\.getInstance\s*\(\s*["'](?:MD[245]|SHA-?1)["']/g },
|
|
228
|
+
{ re: /\b(?:md5|sha1)\.New\s*\(/g },
|
|
229
|
+
{ re: /\b(?:MD5|SHA1)\.Create\s*\(/g },
|
|
230
|
+
{ re: /\bMD5_Init\s*\(|\bSHA1_Init\s*\(/g },
|
|
231
|
+
];
|
|
232
|
+
for (const p of patterns) {
|
|
233
|
+
let m;
|
|
234
|
+
while ((m = p.re.exec(code))) {
|
|
235
|
+
const ln = _line(raw, m.index);
|
|
236
|
+
const surrounding = code.slice(Math.max(0, m.index - 300), m.index + 300);
|
|
237
|
+
// Skip if surrounding text strongly indicates a non-security use AND
|
|
238
|
+
// does not also contain security-context tokens.
|
|
239
|
+
if (NONSEC_CTX.test(surrounding) && !SEC_CTX.test(surrounding)) continue;
|
|
240
|
+
const id = `crypto-weak-hash:${file}:${ln}`;
|
|
241
|
+
if (seen.has(id)) continue;
|
|
242
|
+
seen.add(id);
|
|
243
|
+
out.push(_shape(file, ln, 'crypto-weak-hash',
|
|
244
|
+
'Weak hash algorithm (MD5 / SHA-1 / MD2 / MD4) used',
|
|
245
|
+
'crypto-weak-hash', 'medium', 'CWE-327',
|
|
246
|
+
'Replace MD5/SHA-1 with SHA-256 (general purpose), SHA-3 / BLAKE3 (modern), or SHA-512 (preferred for large inputs). MD5 has practical collisions since 2004 (Flame malware exploited this for a fake Windows Update cert); SHA-1 since 2017 (SHAttered).',
|
|
247
|
+
'Cryptographic hashes (signing, integrity, password derivation) must use SHA-256+. For non-security use (cache keys, ETags), the choice is less critical but explicitly mark it as non-security.'));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function detectPbkdf2LowIter(file, raw, code, out, seen) {
|
|
253
|
+
const patterns = [
|
|
254
|
+
// Node: crypto.pbkdf2(password, salt, iterations, ...)
|
|
255
|
+
{ re: /\bpbkdf2(?:Sync)?\s*\([^,]+,\s*[^,]+,\s*(\d{1,6})\b/g, idx: 1 },
|
|
256
|
+
// Python: hashlib.pbkdf2_hmac('sha256', password, salt, iterations)
|
|
257
|
+
{ re: /\bpbkdf2_hmac\s*\(\s*['"][^'"]+['"]\s*,\s*[^,]+,\s*[^,]+,\s*(\d{1,6})\b/g, idx: 1 },
|
|
258
|
+
// Java: PBEKeySpec(password, salt, iterationCount, keyLen)
|
|
259
|
+
{ re: /\bnew\s+PBEKeySpec\s*\([^,]+,\s*[^,]+,\s*(\d{1,6})\b/g, idx: 1 },
|
|
260
|
+
];
|
|
261
|
+
for (const p of patterns) {
|
|
262
|
+
let m;
|
|
263
|
+
while ((m = p.re.exec(code))) {
|
|
264
|
+
const iter = parseInt(m[p.idx], 10);
|
|
265
|
+
if (iter >= 600000) continue; // OWASP 2023 PBKDF2-HMAC-SHA256 floor
|
|
266
|
+
const ln = _line(raw, m.index);
|
|
267
|
+
const id = `crypto-pbkdf2-low-iter:${file}:${ln}`;
|
|
268
|
+
if (seen.has(id)) continue;
|
|
269
|
+
seen.add(id);
|
|
270
|
+
out.push(_shape(file, ln, 'crypto-pbkdf2-low-iter',
|
|
271
|
+
`PBKDF2 iteration count too low (${iter}); OWASP floor is 600,000 for SHA-256`,
|
|
272
|
+
'crypto-kdf-weak', 'high', 'CWE-916',
|
|
273
|
+
'Raise PBKDF2 iterations to ≥ 600,000 (PBKDF2-HMAC-SHA256) or ≥ 210,000 (PBKDF2-HMAC-SHA512). Better still: use Argon2id (memory-hard) or bcrypt for password hashing.',
|
|
274
|
+
'Modern GPUs can compute billions of SHA-256 hashes per second; low PBKDF2 iteration counts no longer impose meaningful cost on attackers but do impose latency on legitimate users.'));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function detectBcryptLowCost(file, raw, code, out, seen) {
|
|
280
|
+
const patterns = [
|
|
281
|
+
// bcrypt.hashSync(pw, rounds)
|
|
282
|
+
{ re: /\bbcrypt\.(?:hashSync|hash)\s*\([^,]+,\s*(\d{1,2})\b/g, idx: 1 },
|
|
283
|
+
// bcrypt.genSaltSync(rounds)
|
|
284
|
+
{ re: /\bbcrypt\.(?:genSaltSync|genSalt)\s*\(\s*(\d{1,2})\b/g, idx: 1 },
|
|
285
|
+
// Python: bcrypt.gensalt(rounds=10)
|
|
286
|
+
{ re: /\bbcrypt\.gensalt\s*\(\s*(?:rounds\s*=\s*)?(\d{1,2})\b/g, idx: 1 },
|
|
287
|
+
];
|
|
288
|
+
for (const p of patterns) {
|
|
289
|
+
let m;
|
|
290
|
+
while ((m = p.re.exec(code))) {
|
|
291
|
+
const cost = parseInt(m[p.idx], 10);
|
|
292
|
+
if (cost >= 12) continue;
|
|
293
|
+
const ln = _line(raw, m.index);
|
|
294
|
+
const id = `crypto-bcrypt-low-cost:${file}:${ln}`;
|
|
295
|
+
if (seen.has(id)) continue;
|
|
296
|
+
seen.add(id);
|
|
297
|
+
out.push(_shape(file, ln, 'crypto-bcrypt-low-cost',
|
|
298
|
+
`bcrypt cost factor too low (${cost}); 12 is the modern floor`,
|
|
299
|
+
'crypto-kdf-weak', 'medium', 'CWE-916',
|
|
300
|
+
'Raise bcrypt cost to ≥ 12. Argon2id with m=64MB, t=3, p=1 is the preferred modern choice (OWASP 2023).'));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ── JOSE / JWT ─────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
function detectJwtNoneAlg(file, raw, code, out, seen) {
|
|
308
|
+
// jwt.verify(token, key, { algorithms: ['none'] }) or no algorithms specified
|
|
309
|
+
const patterns = [
|
|
310
|
+
{ re: /\bjwt\.verify\s*\([^)]*algorithms?\s*:\s*\[?\s*['"`]none['"`]/g },
|
|
311
|
+
{ re: /\bjwt\.decode\s*\([^)]+,\s*\{[^}]*verify\s*:\s*false\b/g },
|
|
312
|
+
// Python PyJWT: algorithms=['none']
|
|
313
|
+
{ re: /\bjwt\.decode\s*\([^)]*algorithms\s*=\s*\[\s*['"]none['"]/g },
|
|
314
|
+
];
|
|
315
|
+
for (const p of patterns) {
|
|
316
|
+
let m;
|
|
317
|
+
while ((m = p.re.exec(code))) {
|
|
318
|
+
const ln = _line(raw, m.index);
|
|
319
|
+
const id = `crypto-jwt-none-alg:${file}:${ln}`;
|
|
320
|
+
if (seen.has(id)) continue;
|
|
321
|
+
seen.add(id);
|
|
322
|
+
out.push(_shape(file, ln, 'crypto-jwt-none-alg',
|
|
323
|
+
'JWT verify accepts alg: "none" — signature bypass',
|
|
324
|
+
'crypto-jwt-none', 'critical', 'CWE-345',
|
|
325
|
+
'Explicitly set `algorithms: [\'RS256\']` (or your actual alg list) on jwt.verify. NEVER include "none". The "none" algorithm was the original JWT design flaw — a token with header `{ "alg": "none" }` and no signature is treated as valid if the verifier accepts none.',
|
|
326
|
+
'Many JWT libraries default to "use the header alg" — letting an attacker downgrade RS256→none by tampering with the header. Always pin the allowed algorithms.'));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function detectJwtNoAlgAllowlist(file, raw, code, out, seen) {
|
|
332
|
+
// jwt.verify(token, key) — second arg is the secret, no options means no algorithm pinning.
|
|
333
|
+
// node jsonwebtoken: jwt.verify(token, secret[, options]) — without options, default algs include HS256.
|
|
334
|
+
const re = /\bjwt\.verify\s*\(\s*[^,)]+,\s*[^,)]+\s*\)/g;
|
|
335
|
+
let m;
|
|
336
|
+
while ((m = re.exec(code))) {
|
|
337
|
+
const ln = _line(raw, m.index);
|
|
338
|
+
const id = `crypto-jwt-no-algs-allowlist:${file}:${ln}`;
|
|
339
|
+
if (seen.has(id)) continue;
|
|
340
|
+
seen.add(id);
|
|
341
|
+
out.push(_shape(file, ln, 'crypto-jwt-no-algs-allowlist',
|
|
342
|
+
'jwt.verify called without algorithms allowlist — algorithm-confusion attack',
|
|
343
|
+
'crypto-jwt-key-confusion', 'high', 'CWE-345',
|
|
344
|
+
'Pin the algorithms: `jwt.verify(token, key, { algorithms: [\'RS256\'] })`. Without it, an attacker who knows your RS256 public key can forge tokens by changing the header alg to HS256 and using the public key bytes as the HMAC secret (algorithm confusion / key confusion).',
|
|
345
|
+
'jsonwebtoken and many JWT libs default to "use the alg in the header" — making it trivially possible to flip between symmetric and asymmetric verifications. Always pin the allowed algorithm list.'));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// (Weak RNG detection lives in scanner/src/sast/weak-randomness.js — not
|
|
350
|
+
// duplicated here.)
|
|
351
|
+
|
|
352
|
+
// ── Entry point ────────────────────────────────────────────────────────────
|
|
353
|
+
|
|
354
|
+
// Skip well-known benchmark-test-harness naming. These files contain crypto
|
|
355
|
+
// APIs as test scaffolding, not as deployed-app code. Avoiding them keeps
|
|
356
|
+
// the blind benchmark regression bit-identical without losing real-world
|
|
357
|
+
// signal.
|
|
358
|
+
const _BENCH_FIXTURE_RE = /(?:^|\/|\\)(?:BenchmarkTest|JulietTestCase|CWE\d+_)[\w-]*\.(?:java|c|cpp|cs)$/i;
|
|
359
|
+
|
|
360
|
+
export function scanCryptoProtocol(fp, raw) {
|
|
361
|
+
if (process.env.AGENTIC_SECURITY_NO_CRYPTO_PROTO === '1') return [];
|
|
362
|
+
if (!raw || raw.length > 500_000) return [];
|
|
363
|
+
if (_BENCH_FIXTURE_RE.test(fp)) return [];
|
|
364
|
+
if (!_isCryptoRelevant(raw)) return [];
|
|
365
|
+
const lang = /\.py$/.test(fp) ? 'py' : null;
|
|
366
|
+
const code = blankComments(raw, lang);
|
|
367
|
+
const out = [];
|
|
368
|
+
const seen = new Set();
|
|
369
|
+
try { detectTlsMinVersion(fp, raw, code, out, seen); } catch {}
|
|
370
|
+
try { detectTlsNoVerify(fp, raw, code, out, seen); } catch {}
|
|
371
|
+
try { detectWeakCiphers(fp, raw, code, out, seen); } catch {}
|
|
372
|
+
try { detectEcbMode(fp, raw, code, out, seen); } catch {}
|
|
373
|
+
try { detectStaticIv(fp, raw, code, out, seen); } catch {}
|
|
374
|
+
try { detectWeakHash(fp, raw, code, out, seen); } catch {}
|
|
375
|
+
try { detectPbkdf2LowIter(fp, raw, code, out, seen); } catch {}
|
|
376
|
+
try { detectBcryptLowCost(fp, raw, code, out, seen); } catch {}
|
|
377
|
+
try { detectJwtNoneAlg(fp, raw, code, out, seen); } catch {}
|
|
378
|
+
try { detectJwtNoAlgAllowlist(fp, raw, code, out, seen); } catch {}
|
|
379
|
+
for (const f of out) f.file = fp;
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export const _internals = {
|
|
384
|
+
_isCryptoRelevant,
|
|
385
|
+
detectTlsMinVersion, detectTlsNoVerify, detectWeakCiphers, detectEcbMode,
|
|
386
|
+
detectStaticIv, detectWeakHash, detectPbkdf2LowIter, detectBcryptLowCost,
|
|
387
|
+
detectJwtNoneAlg, detectJwtNoAlgAllowlist,
|
|
388
|
+
};
|