@clear-capabilities/agentic-security-scanner 0.76.1 → 0.78.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 +320 -9
- package/bin/.agentic-security/last-scan.json +320 -9
- package/bin/.agentic-security/last-scan.json.sig +1 -1
- package/bin/.agentic-security/scan-history.json +17 -377
- package/bin/.agentic-security/streak.json +11 -16
- package/bin/agentic-security.js +33 -2
- package/dist/178.index.js +1 -1
- package/dist/384.index.js +1 -1
- package/dist/637.index.js +1 -1
- package/dist/718.index.js +106 -0
- package/dist/824.index.js +126 -0
- package/dist/838.index.js +1 -1
- package/dist/agentic-security.mjs +32 -32
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +7 -7
- package/src/.agentic-security/findings.json +5731 -3933
- package/src/.agentic-security/last-scan.json +5731 -3933
- package/src/.agentic-security/last-scan.json.sig +1 -1
- package/src/.agentic-security/scan-history.json +2533 -887
- package/src/.agentic-security/streak.json +11 -16
- package/src/dataflow/.agentic-security/findings.json +52 -24
- package/src/dataflow/.agentic-security/last-scan.json +52 -24
- package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
- package/src/dataflow/.agentic-security/scan-history.json +101 -134
- package/src/dataflow/.agentic-security/streak.json +8 -10
- package/src/dataflow/async-sequencing.js +16 -7
- package/src/dataflow/builtin-summaries.js +131 -0
- package/src/dataflow/catalog.js +107 -0
- package/src/dataflow/cross-repo.js +75 -1
- package/src/dataflow/engine.js +129 -0
- package/src/dataflow/implicit-flow.js +24 -6
- package/src/dataflow/stub-aware-filter.js +69 -11
- package/src/dataflow/summaries.js +28 -3
- package/src/engine-parallel.js +70 -0
- package/src/engine.js +165 -15
- package/src/ir/.agentic-security/findings.json +757 -16
- package/src/ir/.agentic-security/last-scan.json +757 -16
- package/src/ir/.agentic-security/last-scan.json.sig +1 -1
- package/src/ir/.agentic-security/scan-history.json +545 -138
- package/src/ir/.agentic-security/streak.json +11 -13
- package/src/ir/index.js +22 -1
- package/src/ir/parser-go.js +403 -0
- package/src/ir/parser-js.js +2 -0
- package/src/ir/parser-php.js +330 -0
- package/src/ir/parser-py.helper.py +137 -11
- package/src/ir/parser-rb.js +309 -0
- package/src/posture/.agentic-security/findings.json +407 -84
- package/src/posture/.agentic-security/last-scan.json +407 -84
- package/src/posture/.agentic-security/last-scan.json.sig +1 -1
- package/src/posture/.agentic-security/scan-history.json +16 -4923
- package/src/posture/.agentic-security/streak.json +10 -14
- package/src/posture/calibration.js +14 -0
- package/src/posture/triage.js +13 -0
- package/src/report/.agentic-security/findings.json +6 -5
- package/src/report/.agentic-security/last-scan.json +6 -5
- package/src/report/.agentic-security/last-scan.json.sig +1 -1
- package/src/report/.agentic-security/scan-history.json +3 -300
- package/src/report/.agentic-security/streak.json +7 -8
- package/src/report/index.js +23 -2
- package/src/sast/.agentic-security/findings.json +195 -56
- package/src/sast/.agentic-security/last-scan.json +195 -56
- package/src/sast/.agentic-security/last-scan.json.sig +1 -1
- package/src/sast/.agentic-security/scan-history.json +14 -394
- package/src/sast/.agentic-security/streak.json +10 -13
- package/src/sast/cache-poisoning.js +77 -0
- package/src/sast/comparison-safety.js +73 -0
- package/src/sast/db-taint.js +54 -0
- package/src/sast/graphql.js +127 -0
- package/src/sast/llm-stored-prompt.js +57 -0
- package/src/sast/mutation-xss.js +43 -0
- package/src/sast/nosql-injection.js +5 -0
- package/src/sast/null-byte-injection.js +76 -0
- package/src/sast/redos-nfa.js +338 -0
- package/src/sast/sensitive-data-logging.js +73 -0
- package/src/sast/weak-password-hash.js +77 -0
- package/src/sast/weak-randomness.js +100 -0
- package/src/sca/.agentic-security/findings.json +502 -11
- package/src/sca/.agentic-security/last-scan.json +502 -11
- package/src/sca/.agentic-security/last-scan.json.sig +1 -1
- package/src/sca/.agentic-security/scan-history.json +19 -1
- package/src/sca/.agentic-security/streak.json +6 -6
- package/src/sca/llm-function-extract.js +107 -0
- package/src/sca/vendor-detect.js +91 -0
- package/dist/218.index.js +0 -793
- package/dist/601.index.js +0 -1038
- package/dist/634.index.js +0 -1892
- package/src/integrations/.agentic-security/findings.json +0 -1504
- package/src/integrations/.agentic-security/last-scan.json +0 -1504
- package/src/integrations/.agentic-security/scan-history.json +0 -40
- package/src/integrations/.agentic-security/streak.json +0 -21
- package/src/llm-validator/.agentic-security/findings.json +0 -1891
- package/src/llm-validator/.agentic-security/last-scan.json +0 -1891
- package/src/llm-validator/.agentic-security/last-scan.json.sig +0 -1
- package/src/llm-validator/.agentic-security/scan-history.json +0 -168
- package/src/llm-validator/.agentic-security/streak.json +0 -20
- package/src/lsp/.agentic-security/findings.json +0 -28
- package/src/lsp/.agentic-security/last-scan.json +0 -28
- package/src/lsp/.agentic-security/scan-history.json +0 -79
- package/src/lsp/.agentic-security/streak.json +0 -22
- package/src/mcp/.agentic-security/findings.json +0 -8403
- package/src/mcp/.agentic-security/last-scan.json +0 -8403
- package/src/mcp/.agentic-security/last-scan.json.sig +0 -1
- package/src/mcp/.agentic-security/scan-history.json +0 -1182
- package/src/mcp/.agentic-security/streak.json +0 -22
- package/src/sast/bench-shape/.agentic-security/findings.json +0 -28
- package/src/sast/bench-shape/.agentic-security/last-scan.json +0 -28
- package/src/sast/bench-shape/.agentic-security/scan-history.json +0 -24
- package/src/sast/bench-shape/.agentic-security/streak.json +0 -22
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// ReDoS NFA analyzer — detects catastrophic backtracking in regex patterns.
|
|
2
|
+
//
|
|
3
|
+
// Builds a simplified NFA from a regex body string and detects superlinear
|
|
4
|
+
// ambiguity: two distinct paths through a quantifier cycle that accept the
|
|
5
|
+
// same character. This is the core condition for exponential backtracking.
|
|
6
|
+
//
|
|
7
|
+
// Scope: character classes, alternation, quantifiers (+*?{n,m}), groups,
|
|
8
|
+
// escapes, anchors. Unknown constructs → treated as safe (opaque atom).
|
|
9
|
+
// Body-length cap: 500 chars → skip (too complex for static analysis).
|
|
10
|
+
//
|
|
11
|
+
// Also exports extractors for Python re.compile() and Java Pattern.compile().
|
|
12
|
+
|
|
13
|
+
const MAX_BODY_LEN = 500;
|
|
14
|
+
|
|
15
|
+
// ── Regex parser ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
function parseRegex(body) {
|
|
18
|
+
let pos = 0;
|
|
19
|
+
const src = body;
|
|
20
|
+
|
|
21
|
+
function peek() { return pos < src.length ? src[pos] : null; }
|
|
22
|
+
function advance() { return src[pos++]; }
|
|
23
|
+
|
|
24
|
+
function parseAlternation() {
|
|
25
|
+
const branches = [parseConcat()];
|
|
26
|
+
while (peek() === '|') {
|
|
27
|
+
advance();
|
|
28
|
+
branches.push(parseConcat());
|
|
29
|
+
}
|
|
30
|
+
return branches.length === 1 ? branches[0] : { type: 'alt', branches };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseConcat() {
|
|
34
|
+
const items = [];
|
|
35
|
+
while (pos < src.length && peek() !== ')' && peek() !== '|') {
|
|
36
|
+
items.push(parseQuantified());
|
|
37
|
+
}
|
|
38
|
+
return items.length === 1 ? items[0] : { type: 'concat', items };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function parseQuantified() {
|
|
42
|
+
let atom = parseAtom();
|
|
43
|
+
if (!atom) return { type: 'literal', ch: '' };
|
|
44
|
+
while (pos < src.length) {
|
|
45
|
+
const c = peek();
|
|
46
|
+
if (c === '*') { advance(); atom = { type: 'star', child: atom }; }
|
|
47
|
+
else if (c === '+') { advance(); atom = { type: 'plus', child: atom }; }
|
|
48
|
+
else if (c === '?') { advance(); atom = { type: 'opt', child: atom }; }
|
|
49
|
+
else if (c === '{') {
|
|
50
|
+
const saved = pos;
|
|
51
|
+
advance();
|
|
52
|
+
let numStr = '';
|
|
53
|
+
while (pos < src.length && /[\d,]/.test(peek())) numStr += advance();
|
|
54
|
+
if (peek() === '}') {
|
|
55
|
+
advance();
|
|
56
|
+
const parts = numStr.split(',');
|
|
57
|
+
const max = parts.length > 1 ? (parts[1] ? parseInt(parts[1]) : Infinity) : parseInt(parts[0]);
|
|
58
|
+
if (max > 1 || max === Infinity) {
|
|
59
|
+
atom = { type: 'star', child: atom };
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
pos = saved;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
} else break;
|
|
66
|
+
if (peek() === '?') advance(); // lazy modifier
|
|
67
|
+
}
|
|
68
|
+
return atom;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseAtom() {
|
|
72
|
+
const c = peek();
|
|
73
|
+
if (c === null || c === ')' || c === '|') return null;
|
|
74
|
+
if (c === '(') {
|
|
75
|
+
advance();
|
|
76
|
+
if (peek() === '?') {
|
|
77
|
+
advance();
|
|
78
|
+
// Non-capturing group or lookahead — skip modifier chars
|
|
79
|
+
while (pos < src.length && peek() !== ':' && peek() !== ')' && /[imsx<!=P]/.test(peek())) advance();
|
|
80
|
+
if (peek() === ':' || peek() === ')') {
|
|
81
|
+
if (peek() === ':') advance();
|
|
82
|
+
if (peek() === ')') { advance(); return { type: 'literal', ch: '' }; }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const inner = parseAlternation();
|
|
86
|
+
if (peek() === ')') advance();
|
|
87
|
+
return { type: 'group', child: inner };
|
|
88
|
+
}
|
|
89
|
+
if (c === '[') return parseCharClass();
|
|
90
|
+
if (c === '\\') {
|
|
91
|
+
advance();
|
|
92
|
+
const esc = advance();
|
|
93
|
+
if (!esc) return { type: 'literal', ch: '\\' };
|
|
94
|
+
if (esc === 'd') return { type: 'class', chars: '0123456789' };
|
|
95
|
+
if (esc === 'w') return { type: 'class', chars: 'azAZ09_' };
|
|
96
|
+
if (esc === 's') return { type: 'class', chars: ' \t\n\r' };
|
|
97
|
+
if (esc === 'D' || esc === 'W' || esc === 'S') return { type: 'class', chars: 'ANY' };
|
|
98
|
+
if (esc === 'b' || esc === 'B') return { type: 'literal', ch: '' }; // anchor
|
|
99
|
+
return { type: 'literal', ch: esc };
|
|
100
|
+
}
|
|
101
|
+
if (c === '.') { advance(); return { type: 'class', chars: 'ANY' }; }
|
|
102
|
+
if (c === '^' || c === '$') { advance(); return { type: 'literal', ch: '' }; }
|
|
103
|
+
advance();
|
|
104
|
+
return { type: 'literal', ch: c };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseCharClass() {
|
|
108
|
+
advance(); // [
|
|
109
|
+
let chars = '';
|
|
110
|
+
let negated = false;
|
|
111
|
+
if (peek() === '^') { negated = true; advance(); }
|
|
112
|
+
if (peek() === ']') { chars += advance(); }
|
|
113
|
+
while (pos < src.length && peek() !== ']') {
|
|
114
|
+
if (peek() === '\\') {
|
|
115
|
+
advance();
|
|
116
|
+
const esc = advance();
|
|
117
|
+
if (esc === 'd') chars += '0123456789';
|
|
118
|
+
else if (esc === 'w') chars += 'azAZ09_';
|
|
119
|
+
else if (esc === 's') chars += ' \t\n\r';
|
|
120
|
+
else chars += (esc || '');
|
|
121
|
+
} else {
|
|
122
|
+
chars += advance();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (peek() === ']') advance();
|
|
126
|
+
return { type: 'class', chars: negated ? 'ANY' : chars };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const tree = parseAlternation();
|
|
131
|
+
return { ok: true, tree };
|
|
132
|
+
} catch {
|
|
133
|
+
return { ok: false, tree: null };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Ambiguity detection ─────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
function classOverlaps(a, b) {
|
|
140
|
+
if (a === 'ANY' || b === 'ANY') return true;
|
|
141
|
+
for (const ch of a) {
|
|
142
|
+
if (b.includes(ch)) return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function collectFirstChars(node) {
|
|
148
|
+
if (!node) return [];
|
|
149
|
+
switch (node.type) {
|
|
150
|
+
case 'literal':
|
|
151
|
+
return node.ch ? [node.ch] : [];
|
|
152
|
+
case 'class':
|
|
153
|
+
return [node.chars];
|
|
154
|
+
case 'group':
|
|
155
|
+
return collectFirstChars(node.child);
|
|
156
|
+
case 'concat':
|
|
157
|
+
for (const item of (node.items || [])) {
|
|
158
|
+
const fc = collectFirstChars(item);
|
|
159
|
+
if (fc.length) return fc;
|
|
160
|
+
if (!canBeEmpty(item)) return fc;
|
|
161
|
+
}
|
|
162
|
+
return [];
|
|
163
|
+
case 'alt':
|
|
164
|
+
return (node.branches || []).flatMap(collectFirstChars);
|
|
165
|
+
case 'star':
|
|
166
|
+
case 'plus':
|
|
167
|
+
case 'opt':
|
|
168
|
+
return collectFirstChars(node.child);
|
|
169
|
+
default:
|
|
170
|
+
return [];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function canBeEmpty(node) {
|
|
175
|
+
if (!node) return true;
|
|
176
|
+
switch (node.type) {
|
|
177
|
+
case 'literal': return !node.ch;
|
|
178
|
+
case 'class': return false;
|
|
179
|
+
case 'group': return canBeEmpty(node.child);
|
|
180
|
+
case 'concat': return (node.items || []).every(canBeEmpty);
|
|
181
|
+
case 'alt': return (node.branches || []).some(canBeEmpty);
|
|
182
|
+
case 'star': case 'opt': return true;
|
|
183
|
+
case 'plus': return canBeEmpty(node.child);
|
|
184
|
+
default: return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function detectSuperlinear(tree) {
|
|
189
|
+
if (!tree) return { unsafe: false };
|
|
190
|
+
const reasons = [];
|
|
191
|
+
_walk(tree, reasons, 0);
|
|
192
|
+
return reasons.length ? { unsafe: true, reason: reasons[0] } : { unsafe: false };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function _walk(node, reasons, quantifierDepth) {
|
|
196
|
+
if (!node || reasons.length) return;
|
|
197
|
+
switch (node.type) {
|
|
198
|
+
case 'star':
|
|
199
|
+
case 'plus': {
|
|
200
|
+
if (quantifierDepth > 0) {
|
|
201
|
+
reasons.push('nested quantifier');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
_walk(node.child, reasons, quantifierDepth + 1);
|
|
205
|
+
// Unwrap group to check inner structure
|
|
206
|
+
const inner = node.child && node.child.type === 'group' ? node.child.child : node.child;
|
|
207
|
+
if (inner && inner.type === 'alt') {
|
|
208
|
+
const branches = inner.branches || [];
|
|
209
|
+
for (let i = 0; i < branches.length; i++) {
|
|
210
|
+
const fc_i = collectFirstChars(branches[i]);
|
|
211
|
+
for (let j = i + 1; j < branches.length; j++) {
|
|
212
|
+
const fc_j = collectFirstChars(branches[j]);
|
|
213
|
+
for (const a of fc_i) {
|
|
214
|
+
for (const b of fc_j) {
|
|
215
|
+
if (classOverlaps(a, b)) {
|
|
216
|
+
reasons.push('alternation ambiguity under quantifier');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (inner && inner.type === 'concat') {
|
|
225
|
+
const items = inner.items || [];
|
|
226
|
+
if (items.length >= 2) {
|
|
227
|
+
const first = collectFirstChars(items[0]);
|
|
228
|
+
for (let k = 1; k < items.length; k++) {
|
|
229
|
+
// Check if all items before k can be empty (nullable prefix)
|
|
230
|
+
const prefixNullable = items.slice(0, k).every(canBeEmpty);
|
|
231
|
+
const prevNullable = canBeEmpty(items[k - 1]) || items[k - 1].type === 'star' || items[k - 1].type === 'opt';
|
|
232
|
+
if (prevNullable || prefixNullable) {
|
|
233
|
+
const fc_k = collectFirstChars(items[k]);
|
|
234
|
+
for (const a of first) {
|
|
235
|
+
for (const b of fc_k) {
|
|
236
|
+
if (classOverlaps(a, b)) {
|
|
237
|
+
reasons.push('overlapping nullable prefix in quantifier');
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
case 'opt':
|
|
249
|
+
_walk(node.child, reasons, quantifierDepth);
|
|
250
|
+
break;
|
|
251
|
+
case 'group':
|
|
252
|
+
_walk(node.child, reasons, quantifierDepth);
|
|
253
|
+
break;
|
|
254
|
+
case 'concat':
|
|
255
|
+
for (const item of (node.items || [])) _walk(item, reasons, quantifierDepth);
|
|
256
|
+
break;
|
|
257
|
+
case 'alt':
|
|
258
|
+
for (const b of (node.branches || [])) _walk(b, reasons, quantifierDepth);
|
|
259
|
+
break;
|
|
260
|
+
default:
|
|
261
|
+
break;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
export function isUnsafeRegex(body) {
|
|
268
|
+
if (!body || typeof body !== 'string') return { unsafe: false };
|
|
269
|
+
if (body.length > MAX_BODY_LEN) return { unsafe: false };
|
|
270
|
+
if (!/[*+{]/.test(body)) return { unsafe: false };
|
|
271
|
+
const parsed = parseRegex(body);
|
|
272
|
+
if (!parsed.ok) return { unsafe: false };
|
|
273
|
+
return detectSuperlinear(parsed.tree);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function extractJsRegexBodies(code) {
|
|
277
|
+
const out = [];
|
|
278
|
+
// Regex literals: /pattern/flags
|
|
279
|
+
for (const m of code.matchAll(/\/([^/\n]+)\/[gimsuy]*/g)) {
|
|
280
|
+
out.push({ body: m[1], line: code.slice(0, m.index).split('\n').length });
|
|
281
|
+
}
|
|
282
|
+
// new RegExp("pattern")
|
|
283
|
+
for (const m of code.matchAll(/new\s+RegExp\s*\(\s*['"]([^'"]+)['"]/g)) {
|
|
284
|
+
out.push({ body: m[1], line: code.slice(0, m.index).split('\n').length });
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function extractPyRegexBodies(code) {
|
|
290
|
+
const out = [];
|
|
291
|
+
for (const m of code.matchAll(/\bre\.(?:compile|match|search|sub|findall|fullmatch)\s*\(\s*r?['"]((?:\\.|[^'"\n])+)['"]/g)) {
|
|
292
|
+
out.push({ body: m[1], line: code.slice(0, m.index).split('\n').length });
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function extractJavaRegexBodies(code) {
|
|
298
|
+
const out = [];
|
|
299
|
+
for (const m of code.matchAll(/\bPattern\.compile\s*\(\s*"((?:\\.|[^"\n])+)"/g)) {
|
|
300
|
+
out.push({ body: m[1].replace(/\\\\/g, '\\'), line: code.slice(0, m.index).split('\n').length });
|
|
301
|
+
}
|
|
302
|
+
for (const m of code.matchAll(/\.matches\s*\(\s*"((?:\\.|[^"\n])+)"/g)) {
|
|
303
|
+
out.push({ body: m[1].replace(/\\\\/g, '\\'), line: code.slice(0, m.index).split('\n').length });
|
|
304
|
+
}
|
|
305
|
+
return out;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function scanRegexReDoS(file, raw) {
|
|
309
|
+
if (!file || !raw || typeof raw !== 'string') return [];
|
|
310
|
+
if (raw.length > 500_000) return [];
|
|
311
|
+
const findings = [];
|
|
312
|
+
let bodies = [];
|
|
313
|
+
if (/\.(?:js|jsx|ts|tsx|mjs|cjs)$/i.test(file)) bodies = extractJsRegexBodies(raw);
|
|
314
|
+
else if (/\.py$/i.test(file)) bodies = extractPyRegexBodies(raw);
|
|
315
|
+
else if (/\.java$/i.test(file)) bodies = extractJavaRegexBodies(raw);
|
|
316
|
+
else return [];
|
|
317
|
+
|
|
318
|
+
for (const { body, line } of bodies) {
|
|
319
|
+
const result = isUnsafeRegex(body);
|
|
320
|
+
if (result.unsafe) {
|
|
321
|
+
findings.push({
|
|
322
|
+
id: `redos-nfa:${file}:${line}`,
|
|
323
|
+
file,
|
|
324
|
+
line,
|
|
325
|
+
vuln: 'ReDoS — Catastrophic Backtracking (NFA analysis)',
|
|
326
|
+
severity: 'high',
|
|
327
|
+
family: 'redos',
|
|
328
|
+
cwe: 'CWE-1333',
|
|
329
|
+
parser: 'NFA',
|
|
330
|
+
confidence: 0.85,
|
|
331
|
+
description: `Regex pattern has ${result.reason}. A crafted input can cause exponential backtracking, consuming 100% CPU.`,
|
|
332
|
+
remediation: 'Rewrite the regex to avoid nested quantifiers and overlapping alternation. Consider using the re2 library for guaranteed linear-time matching.',
|
|
333
|
+
snippet: `/${body.slice(0, 60)}${body.length > 60 ? '...' : ''}/`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return findings;
|
|
338
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Sensitive data logging detector.
|
|
2
|
+
//
|
|
3
|
+
// Flags PII-named variables sent to logging sinks without sanitization.
|
|
4
|
+
// Covers JS (console/winston/pino/bunyan), Python (logging/print),
|
|
5
|
+
// Go (log/fmt.Printf).
|
|
6
|
+
|
|
7
|
+
const PII_CONTEXT = /\b(email|ssn|password|phone|dob|date_of_birth|credit_card|card_number|social_security|passport|medical_record|ip_address|first_name|last_name|address|zip_code|bank_account)\b/i;
|
|
8
|
+
|
|
9
|
+
function _line(raw, idx) { return raw.slice(0, idx).split('\n').length; }
|
|
10
|
+
|
|
11
|
+
const LANG_SINKS = {
|
|
12
|
+
js: {
|
|
13
|
+
ext: /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i,
|
|
14
|
+
sinks: [
|
|
15
|
+
/\bconsole\.(?:log|warn|error|info|debug|trace)\s*\(/g,
|
|
16
|
+
/\blogger\.(?:log|info|warn|error|debug|trace|fatal)\s*\(/g,
|
|
17
|
+
/\b(?:winston|pino|bunyan|log4js)(?:\.\w+)*\.(?:info|warn|error|debug|log)\s*\(/g,
|
|
18
|
+
],
|
|
19
|
+
},
|
|
20
|
+
py: {
|
|
21
|
+
ext: /\.py$/i,
|
|
22
|
+
sinks: [
|
|
23
|
+
/\blogging\.(?:info|warning|error|debug|critical)\s*\(/g,
|
|
24
|
+
/\blogger\.(?:info|warning|error|debug|critical)\s*\(/g,
|
|
25
|
+
/\bprint\s*\(/g,
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
go: {
|
|
29
|
+
ext: /\.go$/i,
|
|
30
|
+
sinks: [
|
|
31
|
+
/\blog\.(?:Printf|Println|Print|Fatalf|Fatal)\s*\(/g,
|
|
32
|
+
/\bfmt\.(?:Printf|Println|Print|Fprintf)\s*\(/g,
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function scanSensitiveDataLogging(fp, raw) {
|
|
38
|
+
if (!fp || !raw || typeof raw !== 'string') return [];
|
|
39
|
+
if (raw.length > 500_000) return [];
|
|
40
|
+
|
|
41
|
+
const findings = [];
|
|
42
|
+
let lang = null;
|
|
43
|
+
for (const v of Object.values(LANG_SINKS)) {
|
|
44
|
+
if (v.ext.test(fp)) { lang = v; break; }
|
|
45
|
+
}
|
|
46
|
+
if (!lang) return [];
|
|
47
|
+
|
|
48
|
+
for (const sinkRe of lang.sinks) {
|
|
49
|
+
sinkRe.lastIndex = 0;
|
|
50
|
+
for (const m of raw.matchAll(sinkRe)) {
|
|
51
|
+
const lineNum = _line(raw, m.index);
|
|
52
|
+
const lineEnd = raw.indexOf('\n', m.index);
|
|
53
|
+
const lineText = raw.slice(m.index, lineEnd > 0 ? lineEnd : m.index + 200);
|
|
54
|
+
if (!PII_CONTEXT.test(lineText)) continue;
|
|
55
|
+
// Skip if the line contains redaction/masking
|
|
56
|
+
if (/\b(?:redact|mask|sanitize|censor|scrub|\*{3,}|\.{3}|slice\s*\(\s*0\s*,\s*\d\s*\))\b/i.test(lineText)) continue;
|
|
57
|
+
findings.push({
|
|
58
|
+
id: `sensitive-log:${fp}:${lineNum}`,
|
|
59
|
+
file: fp, line: lineNum,
|
|
60
|
+
vuln: 'Sensitive Data Logged — PII-named variable sent to logger without sanitization',
|
|
61
|
+
severity: 'medium',
|
|
62
|
+
family: 'sensitive-data-logging',
|
|
63
|
+
cwe: 'CWE-532',
|
|
64
|
+
parser: 'PII-LOG',
|
|
65
|
+
confidence: 0.70,
|
|
66
|
+
description: 'A variable with a PII-related name (email, password, ssn, etc.) is logged without redaction. Log aggregators, crash reporters, and stdout can expose this data.',
|
|
67
|
+
remediation: 'Redact sensitive fields before logging: logger.info("Login", { email: email.slice(0, 3) + "***" }). Never log passwords, SSNs, or credit card numbers.',
|
|
68
|
+
snippet: lineText.trim().slice(0, 100),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// Weak password hashing detector.
|
|
2
|
+
//
|
|
3
|
+
// Context-gated: only fires when the hash input variable is named
|
|
4
|
+
// password/secret/credential or the enclosing function is password-related.
|
|
5
|
+
// Detects MD5/SHA1 without salt for password storage.
|
|
6
|
+
|
|
7
|
+
const PASSWORD_CONTEXT = /\b(password|passwd|pwd|secret|credential|passphrase|pass_hash|user_pass)\b/i;
|
|
8
|
+
const PASSWORD_FUNC = /\b(hashPassword|encryptPassword|checkPassword|verifyPassword|createHash|setPassword|validatePassword|hash_password|check_password)\b/i;
|
|
9
|
+
|
|
10
|
+
function _line(raw, idx) { return raw.slice(0, idx).split('\n').length; }
|
|
11
|
+
|
|
12
|
+
const PATTERNS = {
|
|
13
|
+
js: {
|
|
14
|
+
ext: /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i,
|
|
15
|
+
rules: [
|
|
16
|
+
{ re: /\bcreateHash\s*\(\s*['"](?:md5|sha1|sha-1|md4)['"]\s*\)[\s\S]{0,60}\.update\s*\(\s*(\w+)/g, label: 'createHash with weak algorithm' },
|
|
17
|
+
{ re: /\bmd5\s*\(\s*(\w+)/g, label: 'md5() function call' },
|
|
18
|
+
{ re: /\bsha1\s*\(\s*(\w+)/g, label: 'sha1() function call' },
|
|
19
|
+
],
|
|
20
|
+
},
|
|
21
|
+
py: {
|
|
22
|
+
ext: /\.py$/i,
|
|
23
|
+
rules: [
|
|
24
|
+
{ re: /\bhashlib\.(?:md5|sha1)\s*\(\s*(\w+)/g, label: 'hashlib.md5/sha1' },
|
|
25
|
+
{ re: /\bhashlib\.new\s*\(\s*['"](?:md5|sha1)['"]\s*\)[\s\S]{0,40}\.update\s*\(\s*(\w+)/g, label: 'hashlib.new with weak algorithm' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
go: {
|
|
29
|
+
ext: /\.go$/i,
|
|
30
|
+
rules: [
|
|
31
|
+
{ re: /\bmd5\.(?:Sum|New)\s*\(\s*(?:\[\]byte\s*\(\s*)?(\w+)/g, label: 'md5.Sum/New' },
|
|
32
|
+
{ re: /\bsha1\.(?:Sum|New)\s*\(\s*(?:\[\]byte\s*\(\s*)?(\w+)/g, label: 'sha1.Sum/New' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function scanWeakPasswordHash(fp, raw) {
|
|
38
|
+
if (!fp || !raw || typeof raw !== 'string') return [];
|
|
39
|
+
if (raw.length > 500_000) return [];
|
|
40
|
+
|
|
41
|
+
const findings = [];
|
|
42
|
+
let lang = null;
|
|
43
|
+
for (const v of Object.values(PATTERNS)) {
|
|
44
|
+
if (v.ext.test(fp)) { lang = v; break; }
|
|
45
|
+
}
|
|
46
|
+
if (!lang) return [];
|
|
47
|
+
|
|
48
|
+
for (const { re, label } of lang.rules) {
|
|
49
|
+
re.lastIndex = 0;
|
|
50
|
+
for (const m of raw.matchAll(re)) {
|
|
51
|
+
const inputVar = m[1] || '';
|
|
52
|
+
const line = _line(raw, m.index);
|
|
53
|
+
// Check context: password-named variable or password-related function
|
|
54
|
+
const funcStart = raw.lastIndexOf('\n', Math.max(0, m.index - 500));
|
|
55
|
+
const context = raw.slice(funcStart, m.index + m[0].length + 100);
|
|
56
|
+
if (!PASSWORD_CONTEXT.test(inputVar) && !PASSWORD_CONTEXT.test(context) && !PASSWORD_FUNC.test(context)) continue;
|
|
57
|
+
// Check for salt within 10 lines before
|
|
58
|
+
const before = raw.slice(Math.max(0, m.index - 400), m.index);
|
|
59
|
+
const hasSalt = /\b(salt|randomBytes|urandom|os\.urandom|crypto\.randomBytes|bcrypt|argon2|scrypt|pbkdf2)\b/i.test(before);
|
|
60
|
+
if (hasSalt) continue;
|
|
61
|
+
|
|
62
|
+
findings.push({
|
|
63
|
+
id: `weak-pw-hash:${fp}:${line}`,
|
|
64
|
+
file: fp, line,
|
|
65
|
+
vuln: `Weak Password Hashing — ${label} for password without salt`,
|
|
66
|
+
severity: 'critical',
|
|
67
|
+
family: 'weak-password-hash',
|
|
68
|
+
cwe: 'CWE-916',
|
|
69
|
+
parser: 'WEAK-PW-HASH',
|
|
70
|
+
confidence: 0.80,
|
|
71
|
+
description: `${label} used on a password-context variable without salt. MD5/SHA1 are fast hashes trivially reversed via rainbow tables. Unsalted hashes are cracked in seconds.`,
|
|
72
|
+
remediation: 'Use bcrypt (cost ≥ 12), argon2id, or scrypt. Never use MD5/SHA1/SHA256 for password storage.',
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return findings;
|
|
77
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Cross-language insecure randomness detector.
|
|
2
|
+
//
|
|
3
|
+
// Flags usage of non-cryptographic PRNGs when the result is assigned to a
|
|
4
|
+
// security-sensitive variable (token, session, nonce, key, secret, etc.).
|
|
5
|
+
//
|
|
6
|
+
// Coverage:
|
|
7
|
+
// JS/TS: Math.random()
|
|
8
|
+
// Python: random.random(), random.randint(), random.choice(), random.uniform()
|
|
9
|
+
// Go: rand.Intn(), rand.Int(), rand.Float64(), rand.Int31(), rand.Int63()
|
|
10
|
+
// Ruby: rand(), Random.rand, Random.new.rand
|
|
11
|
+
// PHP: rand(), mt_rand(), array_rand(), shuffle()
|
|
12
|
+
|
|
13
|
+
const SECURITY_CONTEXT = /\b(token|session|nonce|key|secret|password|otp|csrf|salt|code|pin|auth|reset|verify|captcha|challenge|ticket)\b/i;
|
|
14
|
+
|
|
15
|
+
function _line(raw, idx) {
|
|
16
|
+
return raw.slice(0, idx).split('\n').length;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const LANG_PATTERNS = {
|
|
20
|
+
js: {
|
|
21
|
+
ext: /\.(?:js|jsx|ts|tsx|mjs|cjs)$/i,
|
|
22
|
+
patterns: [
|
|
23
|
+
{ re: /\bMath\.random\s*\(\s*\)/g, label: 'Math.random()' },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
py: {
|
|
27
|
+
ext: /\.py$/i,
|
|
28
|
+
patterns: [
|
|
29
|
+
{ re: /\brandom\.(?:random|randint|choice|uniform|randrange|sample|getrandbits)\s*\(/g, label: 'random module (non-crypto)' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
go: {
|
|
33
|
+
ext: /\.go$/i,
|
|
34
|
+
patterns: [
|
|
35
|
+
{ re: /\brand\.(?:Intn|Int|Float64|Float32|Int31|Int63|Int31n|Int63n|Uint32|Uint64)\s*\(/g, label: 'math/rand (non-crypto)' },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
rb: {
|
|
39
|
+
ext: /\.rb$/i,
|
|
40
|
+
patterns: [
|
|
41
|
+
{ re: /\b(?:rand\s*\(|Random\.(?:rand|new\.rand)\s*\()/g, label: 'Kernel.rand / Random.rand' },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
php: {
|
|
45
|
+
ext: /\.(?:php|phtml)$/i,
|
|
46
|
+
patterns: [
|
|
47
|
+
{ re: /\b(?:rand|mt_rand|array_rand)\s*\(/g, label: 'rand() / mt_rand()' },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function scanWeakRandomness(fp, raw) {
|
|
53
|
+
if (!fp || !raw || typeof raw !== 'string') return [];
|
|
54
|
+
if (raw.length > 500_000) return [];
|
|
55
|
+
|
|
56
|
+
const findings = [];
|
|
57
|
+
let lang = null;
|
|
58
|
+
for (const [k, v] of Object.entries(LANG_PATTERNS)) {
|
|
59
|
+
if (v.ext.test(fp)) { lang = v; break; }
|
|
60
|
+
}
|
|
61
|
+
if (!lang) return [];
|
|
62
|
+
|
|
63
|
+
for (const { re, label } of lang.patterns) {
|
|
64
|
+
re.lastIndex = 0;
|
|
65
|
+
for (const m of raw.matchAll(re)) {
|
|
66
|
+
const line = _line(raw, m.index);
|
|
67
|
+
const lineStart = raw.lastIndexOf('\n', m.index) + 1;
|
|
68
|
+
const lineEnd = raw.indexOf('\n', m.index);
|
|
69
|
+
const lineText = raw.slice(lineStart, lineEnd > 0 ? lineEnd : raw.length);
|
|
70
|
+
if (!SECURITY_CONTEXT.test(lineText)) {
|
|
71
|
+
const prevLineStart = raw.lastIndexOf('\n', lineStart - 2) + 1;
|
|
72
|
+
const prevLine = raw.slice(prevLineStart, lineStart - 1);
|
|
73
|
+
if (!SECURITY_CONTEXT.test(prevLine)) continue;
|
|
74
|
+
}
|
|
75
|
+
findings.push({
|
|
76
|
+
id: `weak-rng:${fp}:${line}`,
|
|
77
|
+
file: fp,
|
|
78
|
+
line,
|
|
79
|
+
vuln: `Insecure Randomness — ${label} used for security-sensitive value`,
|
|
80
|
+
severity: 'high',
|
|
81
|
+
family: 'weak-rng',
|
|
82
|
+
cwe: 'CWE-330',
|
|
83
|
+
parser: 'WEAK-RNG',
|
|
84
|
+
confidence: 0.80,
|
|
85
|
+
description: `${label} is not cryptographically secure. An attacker can predict the output and forge tokens, bypass OTP, or guess session identifiers.`,
|
|
86
|
+
remediation: _remediation(fp),
|
|
87
|
+
snippet: lineText.trim().slice(0, 80),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return findings;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _remediation(fp) {
|
|
95
|
+
if (/\.py$/i.test(fp)) return 'Use secrets.token_hex(32), secrets.token_urlsafe(32), or secrets.randbelow(n).';
|
|
96
|
+
if (/\.go$/i.test(fp)) return 'Use crypto/rand: n, _ := rand.Int(rand.Reader, big.NewInt(999999)).';
|
|
97
|
+
if (/\.rb$/i.test(fp)) return 'Use SecureRandom.hex(32) or SecureRandom.uuid.';
|
|
98
|
+
if (/\.(?:php|phtml)$/i.test(fp)) return 'Use random_bytes(32) or random_int(0, $max).';
|
|
99
|
+
return 'Use crypto.randomBytes(32).toString("hex") or crypto.getRandomValues().';
|
|
100
|
+
}
|