@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.
Files changed (108) hide show
  1. package/bin/.agentic-security/findings.json +320 -9
  2. package/bin/.agentic-security/last-scan.json +320 -9
  3. package/bin/.agentic-security/last-scan.json.sig +1 -1
  4. package/bin/.agentic-security/scan-history.json +17 -377
  5. package/bin/.agentic-security/streak.json +11 -16
  6. package/bin/agentic-security.js +33 -2
  7. package/dist/178.index.js +1 -1
  8. package/dist/384.index.js +1 -1
  9. package/dist/637.index.js +1 -1
  10. package/dist/718.index.js +106 -0
  11. package/dist/824.index.js +126 -0
  12. package/dist/838.index.js +1 -1
  13. package/dist/agentic-security.mjs +32 -32
  14. package/dist/agentic-security.mjs.sha256 +1 -1
  15. package/package.json +7 -7
  16. package/src/.agentic-security/findings.json +5731 -3933
  17. package/src/.agentic-security/last-scan.json +5731 -3933
  18. package/src/.agentic-security/last-scan.json.sig +1 -1
  19. package/src/.agentic-security/scan-history.json +2533 -887
  20. package/src/.agentic-security/streak.json +11 -16
  21. package/src/dataflow/.agentic-security/findings.json +52 -24
  22. package/src/dataflow/.agentic-security/last-scan.json +52 -24
  23. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -1
  24. package/src/dataflow/.agentic-security/scan-history.json +101 -134
  25. package/src/dataflow/.agentic-security/streak.json +8 -10
  26. package/src/dataflow/async-sequencing.js +16 -7
  27. package/src/dataflow/builtin-summaries.js +131 -0
  28. package/src/dataflow/catalog.js +107 -0
  29. package/src/dataflow/cross-repo.js +75 -1
  30. package/src/dataflow/engine.js +129 -0
  31. package/src/dataflow/implicit-flow.js +24 -6
  32. package/src/dataflow/stub-aware-filter.js +69 -11
  33. package/src/dataflow/summaries.js +28 -3
  34. package/src/engine-parallel.js +70 -0
  35. package/src/engine.js +165 -15
  36. package/src/ir/.agentic-security/findings.json +757 -16
  37. package/src/ir/.agentic-security/last-scan.json +757 -16
  38. package/src/ir/.agentic-security/last-scan.json.sig +1 -1
  39. package/src/ir/.agentic-security/scan-history.json +545 -138
  40. package/src/ir/.agentic-security/streak.json +11 -13
  41. package/src/ir/index.js +22 -1
  42. package/src/ir/parser-go.js +403 -0
  43. package/src/ir/parser-js.js +2 -0
  44. package/src/ir/parser-php.js +330 -0
  45. package/src/ir/parser-py.helper.py +137 -11
  46. package/src/ir/parser-rb.js +309 -0
  47. package/src/posture/.agentic-security/findings.json +407 -84
  48. package/src/posture/.agentic-security/last-scan.json +407 -84
  49. package/src/posture/.agentic-security/last-scan.json.sig +1 -1
  50. package/src/posture/.agentic-security/scan-history.json +16 -4923
  51. package/src/posture/.agentic-security/streak.json +10 -14
  52. package/src/posture/calibration.js +14 -0
  53. package/src/posture/triage.js +13 -0
  54. package/src/report/.agentic-security/findings.json +6 -5
  55. package/src/report/.agentic-security/last-scan.json +6 -5
  56. package/src/report/.agentic-security/last-scan.json.sig +1 -1
  57. package/src/report/.agentic-security/scan-history.json +3 -300
  58. package/src/report/.agentic-security/streak.json +7 -8
  59. package/src/report/index.js +23 -2
  60. package/src/sast/.agentic-security/findings.json +195 -56
  61. package/src/sast/.agentic-security/last-scan.json +195 -56
  62. package/src/sast/.agentic-security/last-scan.json.sig +1 -1
  63. package/src/sast/.agentic-security/scan-history.json +14 -394
  64. package/src/sast/.agentic-security/streak.json +10 -13
  65. package/src/sast/cache-poisoning.js +77 -0
  66. package/src/sast/comparison-safety.js +73 -0
  67. package/src/sast/db-taint.js +54 -0
  68. package/src/sast/graphql.js +127 -0
  69. package/src/sast/llm-stored-prompt.js +57 -0
  70. package/src/sast/mutation-xss.js +43 -0
  71. package/src/sast/nosql-injection.js +5 -0
  72. package/src/sast/null-byte-injection.js +76 -0
  73. package/src/sast/redos-nfa.js +338 -0
  74. package/src/sast/sensitive-data-logging.js +73 -0
  75. package/src/sast/weak-password-hash.js +77 -0
  76. package/src/sast/weak-randomness.js +100 -0
  77. package/src/sca/.agentic-security/findings.json +502 -11
  78. package/src/sca/.agentic-security/last-scan.json +502 -11
  79. package/src/sca/.agentic-security/last-scan.json.sig +1 -1
  80. package/src/sca/.agentic-security/scan-history.json +19 -1
  81. package/src/sca/.agentic-security/streak.json +6 -6
  82. package/src/sca/llm-function-extract.js +107 -0
  83. package/src/sca/vendor-detect.js +91 -0
  84. package/dist/218.index.js +0 -793
  85. package/dist/601.index.js +0 -1038
  86. package/dist/634.index.js +0 -1892
  87. package/src/integrations/.agentic-security/findings.json +0 -1504
  88. package/src/integrations/.agentic-security/last-scan.json +0 -1504
  89. package/src/integrations/.agentic-security/scan-history.json +0 -40
  90. package/src/integrations/.agentic-security/streak.json +0 -21
  91. package/src/llm-validator/.agentic-security/findings.json +0 -1891
  92. package/src/llm-validator/.agentic-security/last-scan.json +0 -1891
  93. package/src/llm-validator/.agentic-security/last-scan.json.sig +0 -1
  94. package/src/llm-validator/.agentic-security/scan-history.json +0 -168
  95. package/src/llm-validator/.agentic-security/streak.json +0 -20
  96. package/src/lsp/.agentic-security/findings.json +0 -28
  97. package/src/lsp/.agentic-security/last-scan.json +0 -28
  98. package/src/lsp/.agentic-security/scan-history.json +0 -79
  99. package/src/lsp/.agentic-security/streak.json +0 -22
  100. package/src/mcp/.agentic-security/findings.json +0 -8403
  101. package/src/mcp/.agentic-security/last-scan.json +0 -8403
  102. package/src/mcp/.agentic-security/last-scan.json.sig +0 -1
  103. package/src/mcp/.agentic-security/scan-history.json +0 -1182
  104. package/src/mcp/.agentic-security/streak.json +0 -22
  105. package/src/sast/bench-shape/.agentic-security/findings.json +0 -28
  106. package/src/sast/bench-shape/.agentic-security/last-scan.json +0 -28
  107. package/src/sast/bench-shape/.agentic-security/scan-history.json +0 -24
  108. package/src/sast/bench-shape/.agentic-security/streak.json +0 -22
@@ -0,0 +1,309 @@
1
+ // Ruby IR frontend.
2
+ //
3
+ // Regex-based, follows the parser-cs.js / parser-go.js pattern. Focused on
4
+ // Rails params, ActiveRecord, Kernel methods surface area.
5
+ //
6
+ // What we model:
7
+ // - def / def self. method declarations
8
+ // - var = expr assignments
9
+ // - method calls: obj.method(args) and method(args)
10
+ // - return
11
+ // - each/map/select blocks as loop-header
12
+ //
13
+ // What we do NOT model:
14
+ // - blocks / procs / lambdas as first-class values
15
+ // - metaprogramming (define_method, method_missing)
16
+ // - module_function / protected / private method visibility scoping
17
+ // - control flow (if/unless/while/until/case) — body is straight-line
18
+ //
19
+ // Ruby body extraction: count def/class/module/do/if/unless/while/until/
20
+ // for/case/begin as openers and `end` as closers. Return null on balance
21
+ // failure (heredocs, multi-line strings can confuse the regex parser).
22
+
23
+ import * as crypto from 'node:crypto';
24
+
25
+ const DEF_RE = /(?:^|\n)\s*def\s+(?:self\.)?(\w+[?!=]?)\s*(?:\(([^)]*)\))?/g;
26
+
27
+ function _extractRubyBody(src, defEnd) {
28
+ let depth = 1;
29
+ let i = defEnd;
30
+ let inStr = null;
31
+ let escape = false;
32
+ const openers = /\b(?:def|class|module|do|if|unless|while|until|for|case|begin)\b/;
33
+ while (i < src.length && depth > 0) {
34
+ const c = src[i];
35
+ if (escape) { escape = false; i++; continue; }
36
+ if (inStr) {
37
+ if (c === '\\') { escape = true; i++; continue; }
38
+ if (c === inStr) inStr = null;
39
+ i++; continue;
40
+ }
41
+ if (c === '"' || c === '\'') { inStr = c; i++; continue; }
42
+ if (c === '#') {
43
+ while (i < src.length && src[i] !== '\n') i++;
44
+ continue;
45
+ }
46
+ // Check for keyword boundaries
47
+ if (/[a-z]/i.test(c)) {
48
+ let word = '';
49
+ const start = i;
50
+ while (i < src.length && /\w/.test(src[i])) { word += src[i]; i++; }
51
+ if (word === 'end' && (start === 0 || /[^.\w]/.test(src[start - 1] || ' '))) {
52
+ depth--;
53
+ } else if (openers.test(word) && (start === 0 || /[^.\w]/.test(src[start - 1] || ' '))) {
54
+ // Only count as opener if not preceded by . (e.g., x.if would be wrong but rare)
55
+ depth++;
56
+ }
57
+ continue;
58
+ }
59
+ i++;
60
+ }
61
+ if (depth !== 0) return null;
62
+ // `end` keyword ends at position `i`; body is between defEnd and the start of `end`
63
+ return { body: src.slice(defEnd, i - 3).trimEnd(), end: i };
64
+ }
65
+
66
+ const _RB_OPENERS = /^(?:if|unless|while|until|for|case|begin|do)\b/;
67
+ const _RB_BLOCK_KW = /\b(?:def|class|module|if|unless|while|until|for|case|begin|do)\b/;
68
+
69
+ function _splitStatements(body) {
70
+ const lines = body.split('\n');
71
+ const out = [];
72
+ let buf = '';
73
+ let depth = 0;
74
+ for (const rawLine of lines) {
75
+ const line = rawLine.trim();
76
+ if (!line || line.startsWith('#')) continue;
77
+ if (depth === 0 && _RB_OPENERS.test(line)) {
78
+ if (buf.trim()) out.push(buf.trim());
79
+ buf = line + '\n';
80
+ for (const m of line.matchAll(/\b(?:if|unless|while|until|for|case|begin|do|def|class|module)\b/g)) depth++;
81
+ if (/\bend\b/.test(line)) depth--;
82
+ if (depth <= 0) { depth = 0; out.push(buf.trim()); buf = ''; }
83
+ continue;
84
+ }
85
+ if (depth > 0) {
86
+ buf += line + '\n';
87
+ for (const m of line.matchAll(/\b(?:if|unless|while|until|for|case|begin|do|def|class|module)\b/g)) depth++;
88
+ const endMatches = line.match(/\bend\b/g);
89
+ if (endMatches) depth -= endMatches.length;
90
+ if (depth <= 0) { depth = 0; out.push(buf.trim()); buf = ''; }
91
+ continue;
92
+ }
93
+ out.push(line);
94
+ }
95
+ if (buf.trim()) out.push(buf.trim());
96
+ return out;
97
+ }
98
+
99
+ function _lowerExpr(text) {
100
+ const s = String(text || '').trim();
101
+ if (!s) return { kind: 'unknown' };
102
+ // String interpolation before plain literal check
103
+ if (/^".*#\{/.test(s)) {
104
+ const parts = [];
105
+ for (const m of s.matchAll(/#\{([^}]+)\}/g)) parts.push(_lowerExpr(m[1]));
106
+ if (parts.length) return { kind: 'tpl', parts };
107
+ }
108
+ if (/^['"]/.test(s)) return { kind: 'literal', value: s };
109
+ if (/^\d/.test(s)) return { kind: 'literal', value: s };
110
+ if (/^(true|false|nil)\b/.test(s)) return { kind: 'literal', value: s };
111
+ // Symbol
112
+ if (/^:\w+/.test(s)) return { kind: 'literal', value: s };
113
+ // Call: obj.method(args) or method(args)
114
+ const callMatch = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
115
+ if (callMatch) {
116
+ return { kind: 'call', callee: callMatch[1], args: _splitTopLevelCommas(callMatch[2]).map(_lowerExpr) };
117
+ }
118
+ // Method call without parens is very common in Ruby but hard to detect
119
+ // reliably with regex. We handle the explicit-paren form above.
120
+ // Dotted member: obj.prop
121
+ if (/^[A-Za-z_]\w*(?:\.\w+)+$/.test(s)) {
122
+ const parts = s.split('.');
123
+ let cur = { kind: 'ident', name: parts[0] };
124
+ for (let i = 1; i < parts.length; i++) cur = { kind: 'member', object: cur, prop: parts[i] };
125
+ return cur;
126
+ }
127
+ // Hash access: params[:key]
128
+ if (/^[A-Za-z_]\w*\[/.test(s)) {
129
+ const lb = s.indexOf('[');
130
+ const base = s.slice(0, lb);
131
+ return { kind: 'member', object: { kind: 'ident', name: base }, prop: '[]' };
132
+ }
133
+ // Simple ident
134
+ if (/^[A-Za-z_@]\w*$/.test(s)) return { kind: 'ident', name: s };
135
+ // Concat with +
136
+ if (s.includes('+')) {
137
+ const parts = s.split('+').map(p => _lowerExpr(p.trim()));
138
+ return { kind: 'tpl', parts };
139
+ }
140
+ return { kind: 'unknown' };
141
+ }
142
+
143
+ function _splitTopLevelCommas(s) {
144
+ const out = [];
145
+ let buf = '';
146
+ let depth = 0;
147
+ let inStr = null;
148
+ for (let i = 0; i < s.length; i++) {
149
+ const c = s[i];
150
+ if (inStr) {
151
+ buf += c;
152
+ if (c === '\\') { i++; buf += s[i] || ''; continue; }
153
+ if (c === inStr) inStr = null;
154
+ continue;
155
+ }
156
+ if (c === '"' || c === '\'') { inStr = c; buf += c; continue; }
157
+ if (c === '(' || c === '{' || c === '[') depth++;
158
+ if (c === ')' || c === '}' || c === ']') depth--;
159
+ if (c === ',' && depth === 0) { out.push(buf.trim()); buf = ''; continue; }
160
+ buf += c;
161
+ }
162
+ if (buf.trim()) out.push(buf.trim());
163
+ return out;
164
+ }
165
+
166
+ function _lowerStmt(stmt, line) {
167
+ const s = stmt.trim();
168
+ if (!s || s.startsWith('#')) return null;
169
+ if (/^return\b/.test(s)) {
170
+ const rest = s.replace(/^return\s*/, '').trim();
171
+ return { kind: 'return', line, value: rest ? _lowerExpr(rest) : null };
172
+ }
173
+ if (/^raise\b/.test(s)) {
174
+ return { kind: 'throw', line, value: _lowerExpr(s.replace(/^raise\s*/, '')) };
175
+ }
176
+ // Assignment: var = expr
177
+ const assign = s.match(/^(@?\w+)\s*=\s*(.+)$/s);
178
+ if (assign && !/^={2}/.test(assign[2])) {
179
+ return { kind: 'assign', line, target: assign[1], source: _lowerExpr(assign[2]) };
180
+ }
181
+ // Statement-form call with parens
182
+ const call = s.match(/^([\w.]+)\s*\((.*)\)\s*$/s);
183
+ if (call) {
184
+ return { kind: 'call', line, callee: call[1], args: _splitTopLevelCommas(call[2]).map(_lowerExpr) };
185
+ }
186
+ // Statement-form call without parens (common Ruby idiom): redirect_to expr
187
+ const bareCall = s.match(/^([a-z_]\w*)\s+(.+)$/s);
188
+ if (bareCall && /^[a-z_]/.test(bareCall[1]) && !/^(?:if|unless|while|until|for|case|when|elsif|else|end|return|raise|require|include|extend|attr_\w+)$/.test(bareCall[1])) {
189
+ return { kind: 'call', line, callee: bareCall[1], args: [_lowerExpr(bareCall[2])] };
190
+ }
191
+ return null;
192
+ }
193
+
194
+ function _lineAt(src, idx) {
195
+ let line = 1;
196
+ for (let i = 0; i < idx && i < src.length; i++) if (src[i] === '\n') line++;
197
+ return line;
198
+ }
199
+
200
+ function _qid(file, name, line, body) {
201
+ const sha = crypto.createHash('sha256').update(body).digest('hex').slice(0, 8);
202
+ return `${file}::${name}@${line}#${sha}`;
203
+ }
204
+
205
+ let _nid = 0;
206
+ function _nextId() { return `rn${++_nid}`; }
207
+
208
+ function _addNode(nodes, node) {
209
+ const id = _nextId();
210
+ node.succ = node.succ || [];
211
+ node.pred = node.pred || [];
212
+ nodes[id] = node;
213
+ return id;
214
+ }
215
+
216
+ function _linkNodes(nodes, src, dst) {
217
+ if (!nodes[src] || !nodes[dst]) return;
218
+ if (!nodes[src].succ.includes(dst)) nodes[src].succ.push(dst);
219
+ if (!nodes[dst].pred.includes(src)) nodes[dst].pred.push(src);
220
+ }
221
+
222
+ function _extractRubyBlockBody(compound) {
223
+ const lines = compound.split('\n');
224
+ if (lines.length < 2) return '';
225
+ return lines.slice(1, -1).join('\n');
226
+ }
227
+
228
+ function _buildCfg(bodyText, nodes, prevId, startLine) {
229
+ const stmts = _splitStatements(bodyText);
230
+ let prev = prevId;
231
+ let line = startLine;
232
+ for (const stmt of stmts) {
233
+ const s = stmt.trim();
234
+ if (!s || s.startsWith('#')) { line++; continue; }
235
+
236
+ const ifMatch = s.match(/^(if|unless)\s+(.+)$/m);
237
+ if (ifMatch && /\bend\b\s*$/.test(s)) {
238
+ const condText = ifMatch[2].trim();
239
+ const innerBody = _extractRubyBlockBody(s);
240
+ const ifNode = _addNode(nodes, { kind: 'if', cond: _lowerExpr(condText), line });
241
+ _linkNodes(nodes, prev, ifNode);
242
+ const join = _addNode(nodes, { kind: 'noop', line });
243
+ const thenTail = _buildCfg(innerBody, nodes, ifNode, line + 1);
244
+ _linkNodes(nodes, thenTail, join);
245
+ _linkNodes(nodes, ifNode, join);
246
+ prev = join;
247
+ line += (s.match(/\n/g) || []).length + 1;
248
+ continue;
249
+ }
250
+
251
+ const whileMatch = s.match(/^(while|until)\s+(.+)$/m);
252
+ if (whileMatch && /\bend\b\s*$/.test(s)) {
253
+ const innerBody = _extractRubyBlockBody(s);
254
+ const header = _addNode(nodes, { kind: 'loop-header', line });
255
+ _linkNodes(nodes, prev, header);
256
+ const bodyTail = _buildCfg(innerBody, nodes, header, line + 1);
257
+ _linkNodes(nodes, bodyTail, header);
258
+ const join = _addNode(nodes, { kind: 'noop', line });
259
+ _linkNodes(nodes, header, join);
260
+ prev = join;
261
+ line += (s.match(/\n/g) || []).length + 1;
262
+ continue;
263
+ }
264
+
265
+ const node = _lowerStmt(s, line);
266
+ if (!node) { line += (s.match(/\n/g) || []).length + 1; continue; }
267
+ const id = _addNode(nodes, node);
268
+ _linkNodes(nodes, prev, id);
269
+ prev = id;
270
+ line += (s.match(/\n/g) || []).length + 1;
271
+ }
272
+ return prev;
273
+ }
274
+
275
+ export function parseRubyFile(file, code) {
276
+ if (!file || typeof code !== 'string') return null;
277
+ if (!/\.rb$/i.test(file)) return null;
278
+ if (code.length > 1_000_000) return null;
279
+
280
+ const functions = [];
281
+ DEF_RE.lastIndex = 0;
282
+ _nid = 0;
283
+ let m;
284
+ while ((m = DEF_RE.exec(code)) !== null) {
285
+ const name = m[1];
286
+ const paramsText = m[2] || '';
287
+ const params = paramsText.split(',').map(p => {
288
+ const t = p.trim().replace(/\s*=\s*.*$/, '').replace(/^[*&]+/, '');
289
+ return t && /^\w+$/.test(t) ? t : null;
290
+ }).filter(Boolean);
291
+ const defLineEnd = code.indexOf('\n', m.index + m[0].length);
292
+ if (defLineEnd < 0) continue;
293
+ const extracted = _extractRubyBody(code, defLineEnd + 1);
294
+ if (!extracted) continue;
295
+ const startLine = _lineAt(code, m.index);
296
+ const nodes = {};
297
+ const entry = _addNode(nodes, { kind: 'entry', line: startLine });
298
+ const exit = _addNode(nodes, { kind: 'exit', line: startLine });
299
+ const tail = _buildCfg(extracted.body, nodes, entry, startLine + 1);
300
+ _linkNodes(nodes, tail, exit);
301
+ functions.push({
302
+ qid: _qid(file, name, startLine, extracted.body),
303
+ name, line: startLine, params, file,
304
+ cfg: { entry, exit, nodes },
305
+ });
306
+ DEF_RE.lastIndex = extracted.end;
307
+ }
308
+ return functions.length ? { file, functions, topLevel: null } : null;
309
+ }