@clear-capabilities/agentic-security-scanner 0.77.0 → 0.79.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.
Files changed (69) hide show
  1. package/bin/.agentic-security/findings.json +1907 -0
  2. package/bin/.agentic-security/last-scan.json +1907 -0
  3. package/bin/.agentic-security/last-scan.json.sig +1 -0
  4. package/bin/.agentic-security/scan-history.json +166 -0
  5. package/bin/.agentic-security/streak.json +20 -0
  6. package/bin/agentic-security.js +55 -9
  7. package/dist/178.index.js +1 -1
  8. package/dist/384.index.js +1 -1
  9. package/dist/476.index.js +5 -5
  10. package/dist/637.index.js +1 -1
  11. package/dist/700.index.js +138 -0
  12. package/dist/718.index.js +159 -0
  13. package/dist/824.index.js +126 -0
  14. package/dist/838.index.js +1 -1
  15. package/dist/985.index.js +5 -0
  16. package/dist/agentic-security.mjs +32 -32
  17. package/dist/agentic-security.mjs.sha256 +1 -1
  18. package/package.json +4 -4
  19. package/src/dataflow/async-sequencing.js +16 -7
  20. package/src/dataflow/builtin-summaries.js +131 -0
  21. package/src/dataflow/catalog.js +107 -0
  22. package/src/dataflow/cross-repo.js +75 -1
  23. package/src/dataflow/engine.js +181 -8
  24. package/src/dataflow/implicit-flow.js +24 -6
  25. package/src/dataflow/stub-aware-filter.js +69 -11
  26. package/src/dataflow/summaries.js +28 -3
  27. package/src/engine-parallel.js +70 -0
  28. package/src/engine.js +270 -19
  29. package/src/integrations/index.js +2 -1
  30. package/src/ir/callgraph.js +27 -7
  31. package/src/ir/index.js +22 -1
  32. package/src/ir/parser-go.js +403 -0
  33. package/src/ir/parser-js.js +2 -0
  34. package/src/ir/parser-php.js +330 -0
  35. package/src/ir/parser-py.helper.py +137 -11
  36. package/src/ir/parser-rb.js +309 -0
  37. package/src/llm-validator/index.js +7 -5
  38. package/src/mcp/audit.js +5 -0
  39. package/src/posture/calibration-drift.js +2 -1
  40. package/src/posture/calibration.js +16 -1
  41. package/src/posture/fix-history.js +8 -2
  42. package/src/posture/profile.js +4 -5
  43. package/src/posture/rule-overrides.js +2 -3
  44. package/src/posture/rule-pack-signing.js +2 -3
  45. package/src/posture/rule-synthesis.js +5 -6
  46. package/src/posture/security-trend.js +4 -7
  47. package/src/posture/state-dir.js +124 -0
  48. package/src/posture/streak.js +3 -0
  49. package/src/posture/suppressions.js +5 -8
  50. package/src/posture/triage.js +16 -5
  51. package/src/posture/validator-metrics.js +3 -6
  52. package/src/report/index.js +23 -2
  53. package/src/sast/cache-poisoning.js +77 -0
  54. package/src/sast/comparison-safety.js +73 -0
  55. package/src/sast/db-taint.js +78 -0
  56. package/src/sast/graphql.js +127 -0
  57. package/src/sast/llm-stored-prompt.js +57 -0
  58. package/src/sast/mutation-xss.js +43 -0
  59. package/src/sast/nosql-injection.js +5 -0
  60. package/src/sast/null-byte-injection.js +76 -0
  61. package/src/sast/redos-nfa.js +338 -0
  62. package/src/sast/rust.js +26 -0
  63. package/src/sast/sensitive-data-logging.js +73 -0
  64. package/src/sast/weak-password-hash.js +77 -0
  65. package/src/sast/weak-randomness.js +100 -0
  66. package/src/sca/binary-metadata.js +124 -0
  67. package/src/sca/llm-function-extract.js +107 -0
  68. package/src/sca/py-package-functions.js +118 -0
  69. package/src/sca/vendor-detect.js +144 -0
@@ -0,0 +1,403 @@
1
+ // Go IR frontend.
2
+ //
3
+ // Regex-based, follows the parser-cs.js / parser-kt.js pattern. Focused on
4
+ // net/http, gin, echo, chi, gorm, database/sql surface area.
5
+ //
6
+ // What we model:
7
+ // - func declarations: top-level, method receivers, closures (top-level only)
8
+ // - := short declarations and = assignments
9
+ // - method / function calls
10
+ // - return
11
+ // - if / for / switch as linear blocks (body treated as straight-line)
12
+ // - defer / go as call nodes
13
+ // - fmt.Sprintf as template literal
14
+ // - multi-return first-target tracking: a, err := f()
15
+ //
16
+ // What we do NOT model:
17
+ // - goroutine channel taint (send/receive)
18
+ // - interface dispatch (dynamic method resolution)
19
+ // - generics (type params)
20
+ // - select statements (treated as noop)
21
+ // - struct field assignments (x.Field = val) beyond simple dotted targets
22
+
23
+ import * as crypto from 'node:crypto';
24
+
25
+ const FUNC_RE = new RegExp(
26
+ '(?:^|[\\n;{}])\\s*func\\s+' +
27
+ '(?:\\(\\s*(\\w+)\\s+\\*?([A-Za-z_]\\w*)\\s*\\)\\s+)?' + // optional receiver (g1=name, g2=type)
28
+ '([A-Za-z_]\\w*)' + // func name (g3)
29
+ '\\s*\\(([^)]*)\\)' + // params (g4)
30
+ '(?:\\s*(?:\\([^)]*\\)|[A-Za-z_*\\[\\]\\w.,\\s]*))?' + // optional return type(s)
31
+ '\\s*\\{', 'g');
32
+
33
+ function _splitStatements(body) {
34
+ const out = [];
35
+ let buf = '';
36
+ let depth = 0;
37
+ let inStr = null;
38
+ let inRaw = false;
39
+ let escape = false;
40
+ for (let i = 0; i < body.length; i++) {
41
+ const c = body[i];
42
+ if (escape) { buf += c; escape = false; continue; }
43
+ if (inStr) {
44
+ buf += c;
45
+ if (c === '\\' && inStr === '"') { escape = true; continue; }
46
+ if (c === inStr) inStr = null;
47
+ continue;
48
+ }
49
+ if (inRaw) {
50
+ buf += c;
51
+ if (c === '`') inRaw = false;
52
+ continue;
53
+ }
54
+ if (c === '"' || c === '\'') { inStr = c; buf += c; continue; }
55
+ if (c === '`') { inRaw = true; buf += c; continue; }
56
+ if (c === '/' && body[i + 1] === '/') {
57
+ while (i < body.length && body[i] !== '\n') i++;
58
+ continue;
59
+ }
60
+ if (c === '{' || c === '(' || c === '[') depth++;
61
+ if (c === '}' || c === ')' || c === ']') depth--;
62
+ if ((c === '\n' || c === ';') && depth === 0) {
63
+ const t = buf.trim();
64
+ if (t) out.push(t);
65
+ buf = '';
66
+ continue;
67
+ }
68
+ buf += c;
69
+ }
70
+ if (buf.trim()) out.push(buf.trim());
71
+ return out;
72
+ }
73
+
74
+ function _lowerExpr(text) {
75
+ const s = String(text || '').trim();
76
+ if (!s) return { kind: 'unknown' };
77
+ if (/^fmt\.Sprintf\s*\(/.test(s)) {
78
+ const inner = s.slice(s.indexOf('(') + 1, s.lastIndexOf(')'));
79
+ const parts = _splitTopLevelCommas(inner).map(_lowerExpr);
80
+ return { kind: 'tpl', parts };
81
+ }
82
+ if (/^"/.test(s) || /^`/.test(s)) return { kind: 'literal', value: s };
83
+ if (/^\d/.test(s)) return { kind: 'literal', value: s };
84
+ if (/^(true|false|nil)\b/.test(s)) return { kind: 'literal', value: s };
85
+ // Call: foo.Bar(args) or Bar(args)
86
+ const callMatch = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
87
+ if (callMatch) {
88
+ const callee = callMatch[1];
89
+ const args = _splitTopLevelCommas(callMatch[2]).map(_lowerExpr);
90
+ return { kind: 'call', callee, args };
91
+ }
92
+ // String concat with +
93
+ if (s.includes('+') && /["'`]/.test(s)) {
94
+ const parts = _splitTopLevelPlus(s).map(_lowerExpr);
95
+ return { kind: 'tpl', parts };
96
+ }
97
+ // Member: a.b.c
98
+ if (/^[A-Za-z_][\w.]*$/.test(s)) {
99
+ const parts = s.split('.');
100
+ if (parts.length === 1) return { kind: 'ident', name: parts[0] };
101
+ let cur = { kind: 'ident', name: parts[0] };
102
+ for (let i = 1; i < parts.length; i++) cur = { kind: 'member', object: cur, prop: parts[i] };
103
+ return cur;
104
+ }
105
+ // Indexing: a[b] or a["key"]
106
+ if (/^[A-Za-z_][\w.]*\[/.test(s)) {
107
+ const lb = s.indexOf('[');
108
+ const base = s.slice(0, lb);
109
+ const parts = base.split('.');
110
+ let cur = { kind: 'ident', name: parts[0] };
111
+ for (let i = 1; i < parts.length; i++) cur = { kind: 'member', object: cur, prop: parts[i] };
112
+ return { kind: 'member', object: cur, prop: '[]' };
113
+ }
114
+ // Struct literal: Type{...}
115
+ if (/^[A-Za-z_]\w*\s*\{/.test(s)) {
116
+ return { kind: 'object', props: [] };
117
+ }
118
+ // Address-of / dereference
119
+ if (s.startsWith('&') || s.startsWith('*')) return _lowerExpr(s.slice(1));
120
+ return { kind: 'unknown' };
121
+ }
122
+
123
+ function _splitTopLevelCommas(s) {
124
+ const out = [];
125
+ let buf = '';
126
+ let depth = 0;
127
+ let inStr = null;
128
+ let inRaw = false;
129
+ for (let i = 0; i < s.length; i++) {
130
+ const c = s[i];
131
+ if (inStr) {
132
+ buf += c;
133
+ if (c === '\\' && inStr === '"') { i++; buf += s[i] || ''; continue; }
134
+ if (c === inStr) inStr = null;
135
+ continue;
136
+ }
137
+ if (inRaw) { buf += c; if (c === '`') inRaw = false; continue; }
138
+ if (c === '"') { inStr = c; buf += c; continue; }
139
+ if (c === '`') { inRaw = true; buf += c; continue; }
140
+ if (c === '(' || c === '{' || c === '[') depth++;
141
+ if (c === ')' || c === '}' || c === ']') depth--;
142
+ if (c === ',' && depth === 0) { out.push(buf.trim()); buf = ''; continue; }
143
+ buf += c;
144
+ }
145
+ if (buf.trim()) out.push(buf.trim());
146
+ return out;
147
+ }
148
+
149
+ function _splitTopLevelPlus(s) {
150
+ const out = [];
151
+ let buf = '';
152
+ let depth = 0;
153
+ let inStr = null;
154
+ for (let i = 0; i < s.length; i++) {
155
+ const c = s[i];
156
+ if (inStr) {
157
+ buf += c;
158
+ if (c === '\\' && inStr === '"') { i++; buf += s[i] || ''; continue; }
159
+ if (c === inStr) inStr = null;
160
+ continue;
161
+ }
162
+ if (c === '"' || c === '`') { inStr = c; buf += c; continue; }
163
+ if (c === '(' || c === '{' || c === '[') depth++;
164
+ if (c === ')' || c === '}' || c === ']') depth--;
165
+ if (c === '+' && depth === 0) { out.push(buf.trim()); buf = ''; continue; }
166
+ buf += c;
167
+ }
168
+ if (buf.trim()) out.push(buf.trim());
169
+ return out;
170
+ }
171
+
172
+ function _lowerStmt(stmt, line) {
173
+ const s = stmt.trim();
174
+ if (!s || s.startsWith('//')) return null;
175
+ // return
176
+ if (/^return\b/.test(s)) {
177
+ const rest = s.replace(/^return\s*/, '').trim();
178
+ // Multi-return: return a, b → take the first
179
+ const parts = _splitTopLevelCommas(rest);
180
+ const value = parts.length ? _lowerExpr(parts[0]) : null;
181
+ return { kind: 'return', line, value };
182
+ }
183
+ // defer / go: treat as call
184
+ if (/^(?:defer|go)\s+/.test(s)) {
185
+ const rest = s.replace(/^(?:defer|go)\s+/, '').trim();
186
+ const cm = rest.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
187
+ if (cm) {
188
+ return { kind: 'call', line, callee: cm[1], args: _splitTopLevelCommas(cm[2]).map(_lowerExpr) };
189
+ }
190
+ return { kind: 'noop', line };
191
+ }
192
+ // Short variable declaration: a, b := expr or a := expr
193
+ const shortDecl = s.match(/^(\w+(?:\s*,\s*\w+)*)\s*:=\s*(.+)$/s);
194
+ if (shortDecl) {
195
+ const targets = shortDecl[1].split(',').map(t => t.trim());
196
+ const rhs = shortDecl[2].trim();
197
+ if (targets.length === 1) {
198
+ return { kind: 'assign', line, target: targets[0], source: _lowerExpr(rhs) };
199
+ }
200
+ // Multi-return: a, err := f() → assign first target
201
+ return { kind: 'assign', line, target: targets[0], source: _lowerExpr(rhs) };
202
+ }
203
+ // Regular assignment: a = expr or a.b = expr
204
+ const assign = s.match(/^([A-Za-z_][\w.]*)\s*=\s*(.+)$/s);
205
+ if (assign) {
206
+ return { kind: 'assign', line, target: assign[1], source: _lowerExpr(assign[2]) };
207
+ }
208
+ // var declaration: var name Type = expr or var name = expr
209
+ const varDecl = s.match(/^var\s+(\w+)\s+(?:\w[\w.*[\]]*\s*)?=\s*(.+)$/s);
210
+ if (varDecl) {
211
+ return { kind: 'assign', line, target: varDecl[1], source: _lowerExpr(varDecl[2]) };
212
+ }
213
+ // Statement-form call: obj.Method(args) or Method(args)
214
+ const cm = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
215
+ if (cm) {
216
+ return { kind: 'call', line, callee: cm[1], args: _splitTopLevelCommas(cm[2]).map(_lowerExpr) };
217
+ }
218
+ return null;
219
+ }
220
+
221
+ function _extractBody(src, openBrace) {
222
+ let depth = 1;
223
+ let i = openBrace + 1;
224
+ let inStr = null;
225
+ let inRaw = false;
226
+ let escape = false;
227
+ while (i < src.length && depth > 0) {
228
+ const c = src[i];
229
+ if (escape) { escape = false; i++; continue; }
230
+ if (inStr) {
231
+ if (c === '\\' && inStr === '"') { escape = true; i++; continue; }
232
+ if (c === inStr) inStr = null;
233
+ i++; continue;
234
+ }
235
+ if (inRaw) { if (c === '`') inRaw = false; i++; continue; }
236
+ if (c === '"') { inStr = c; i++; continue; }
237
+ if (c === '`') { inRaw = true; i++; continue; }
238
+ if (c === '{') depth++;
239
+ else if (c === '}') depth--;
240
+ if (depth === 0) return { body: src.slice(openBrace + 1, i), end: i };
241
+ i++;
242
+ }
243
+ return null;
244
+ }
245
+
246
+ function _lineAt(src, idx) {
247
+ let line = 1;
248
+ for (let i = 0; i < idx && i < src.length; i++) if (src[i] === '\n') line++;
249
+ return line;
250
+ }
251
+
252
+ function _qid(file, name, line, body) {
253
+ const sha = crypto.createHash('sha256').update(body).digest('hex').slice(0, 8);
254
+ return `${file}::${name}@${line}#${sha}`;
255
+ }
256
+
257
+ function _parseGoParams(paramsText) {
258
+ if (!paramsText.trim()) return [];
259
+ const parts = _splitTopLevelCommas(paramsText);
260
+ const params = [];
261
+ for (const p of parts) {
262
+ const t = p.trim();
263
+ if (!t) continue;
264
+ // "name Type" or "name, name2 Type" or just "Type" (unnamed)
265
+ const tokens = t.split(/\s+/);
266
+ if (tokens.length >= 2) {
267
+ // Could be "name Type" or "name *Type" or "name ...Type"
268
+ const name = tokens[0].replace(/^\*/, '');
269
+ if (/^[a-z_]\w*$/i.test(name) && !/^(?:func|chan|map|interface|struct)$/.test(name)) {
270
+ params.push(name);
271
+ }
272
+ } else if (tokens.length === 1) {
273
+ // Single token — could be just a type (unnamed param) or a name
274
+ // In Go, if it looks like a lowercase identifier, it's likely a name
275
+ const t0 = tokens[0].replace(/^\*/, '').replace(/^\.\.\./, '');
276
+ if (/^[a-z_]\w*$/.test(t0) && !/^(?:int|string|bool|byte|rune|float32|float64|error|any|interface)$/.test(t0)) {
277
+ params.push(t0);
278
+ }
279
+ }
280
+ }
281
+ return params;
282
+ }
283
+
284
+ let _nid = 0;
285
+ function _nextId() { return `gn${++_nid}`; }
286
+
287
+ function _addNode(nodes, node) {
288
+ const id = _nextId();
289
+ node.succ = node.succ || [];
290
+ node.pred = node.pred || [];
291
+ nodes[id] = node;
292
+ return id;
293
+ }
294
+
295
+ function _link(nodes, src, dst) {
296
+ if (!nodes[src] || !nodes[dst]) return;
297
+ if (!nodes[src].succ.includes(dst)) nodes[src].succ.push(dst);
298
+ if (!nodes[dst].pred.includes(src)) nodes[dst].pred.push(src);
299
+ }
300
+
301
+ function _buildCfg(bodyText, nodes, prevId, startLine) {
302
+ const stmts = _splitStatements(bodyText);
303
+ let prev = prevId;
304
+ let line = startLine;
305
+ for (const stmt of stmts) {
306
+ const s = stmt.trim();
307
+ if (!s || s.startsWith('//')) { line++; continue; }
308
+
309
+ // if statement with brace body
310
+ const ifMatch = s.match(/^if\s+([\s\S]+?)\s*\{([\s\S]*)\}(?:\s*else\s*\{([\s\S]*)\})?\s*$/s) ||
311
+ s.match(/^if\s+([\s\S]+?)\s*\{([\s\S]*)\}\s*$/s);
312
+ if (ifMatch) {
313
+ const condText = ifMatch[1].replace(/;[^;]*$/, '').trim();
314
+ const thenBody = ifMatch[2];
315
+ const elseBody = ifMatch[3] || null;
316
+ const ifNode = _addNode(nodes, { kind: 'if', cond: _lowerExpr(condText), line });
317
+ _link(nodes, prev, ifNode);
318
+ const join = _addNode(nodes, { kind: 'noop', line });
319
+ const thenTail = _buildCfg(thenBody, nodes, ifNode, line + 1);
320
+ _link(nodes, thenTail, join);
321
+ if (elseBody) {
322
+ const elseTail = _buildCfg(elseBody, nodes, ifNode, line + 1);
323
+ _link(nodes, elseTail, join);
324
+ } else {
325
+ _link(nodes, ifNode, join);
326
+ }
327
+ prev = join;
328
+ line += (s.match(/\n/g) || []).length + 1;
329
+ continue;
330
+ }
331
+
332
+ // for loop with brace body
333
+ const forMatch = s.match(/^for\s+([\s\S]*?)\s*\{([\s\S]*)\}\s*$/s);
334
+ if (forMatch) {
335
+ const header = _addNode(nodes, { kind: 'loop-header', line });
336
+ _link(nodes, prev, header);
337
+ const loopBody = forMatch[2];
338
+ // for-range: extract loop variable assignment
339
+ const rangeMatch = forMatch[1].match(/^(\w+)(?:\s*,\s*(\w+))?\s*:=\s*range\s+(.+)$/s);
340
+ let bodyPrev = header;
341
+ if (rangeMatch) {
342
+ const loopVar = rangeMatch[2] || rangeMatch[1];
343
+ const iterExpr = rangeMatch[3];
344
+ const assignId = _addNode(nodes, { kind: 'assign', target: loopVar, source: _lowerExpr(iterExpr), line });
345
+ _link(nodes, header, assignId);
346
+ bodyPrev = assignId;
347
+ }
348
+ const bodyTail = _buildCfg(loopBody, nodes, bodyPrev, line + 1);
349
+ _link(nodes, bodyTail, header);
350
+ const join = _addNode(nodes, { kind: 'noop', line });
351
+ _link(nodes, header, join);
352
+ prev = join;
353
+ line += (s.match(/\n/g) || []).length + 1;
354
+ continue;
355
+ }
356
+
357
+ // Regular statement
358
+ const node = _lowerStmt(s, line);
359
+ if (!node) { line++; continue; }
360
+ const id = _addNode(nodes, node);
361
+ _link(nodes, prev, id);
362
+ prev = id;
363
+ line += (s.match(/\n/g) || []).length + 1;
364
+ }
365
+ return prev;
366
+ }
367
+
368
+ export function parseGoFile(file, code) {
369
+ if (!file || typeof code !== 'string') return null;
370
+ if (!/\.go$/i.test(file)) return null;
371
+ if (code.length > 1_000_000) return null;
372
+
373
+ const functions = [];
374
+ FUNC_RE.lastIndex = 0;
375
+ _nid = 0;
376
+ let m;
377
+ while ((m = FUNC_RE.exec(code)) !== null) {
378
+ const receiverName = m[1] || null;
379
+ const name = m[3];
380
+ const paramsText = m[4] || '';
381
+ const params = _parseGoParams(paramsText);
382
+ if (receiverName && !params.includes(receiverName)) {
383
+ params.unshift(receiverName);
384
+ }
385
+ const braceIdx = code.indexOf('{', m.index + m[0].length - 1);
386
+ if (braceIdx < 0) continue;
387
+ const extracted = _extractBody(code, braceIdx);
388
+ if (!extracted) continue;
389
+ const startLine = _lineAt(code, m.index);
390
+ const nodes = {};
391
+ const entry = _addNode(nodes, { kind: 'entry', line: startLine });
392
+ const exit = _addNode(nodes, { kind: 'exit', line: startLine });
393
+ const tail = _buildCfg(extracted.body, nodes, entry, startLine + 1);
394
+ _link(nodes, tail, exit);
395
+ functions.push({
396
+ qid: _qid(file, name, startLine, extracted.body),
397
+ name, line: startLine, params, file,
398
+ cfg: { entry, exit, nodes },
399
+ });
400
+ FUNC_RE.lastIndex = extracted.end + 1;
401
+ }
402
+ return functions.length ? { file, functions, topLevel: null } : null;
403
+ }
@@ -86,6 +86,7 @@ function exprOf(n) {
86
86
  };
87
87
  case 'ArrayExpression': return { kind: 'array', elements: (n.elements || []).map(exprOf) };
88
88
  case 'SpreadElement': return exprOf(n.argument);
89
+ case 'ThisExpression': return { kind: 'ident', name: '_this_' };
89
90
  default: return { kind: 'unknown' };
90
91
  }
91
92
  }
@@ -94,6 +95,7 @@ function exprOf(n) {
94
95
  function lhsPath(n) {
95
96
  if (!n) return null;
96
97
  if (n.type === 'Identifier') return n.name;
98
+ if (n.type === 'ThisExpression') return '_this_';
97
99
  if (n.type === 'MemberExpression') {
98
100
  const base = lhsPath(n.object);
99
101
  const prop = n.computed ? '*' : (n.property?.name || '*');