@clear-capabilities/agentic-security-scanner 0.151.2 → 0.152.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 (50) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/1905.index.js +30 -2
  3. package/dist/3499.index.js +20 -1
  4. package/dist/agentic-security.mjs +80 -80
  5. package/dist/agentic-security.mjs.sha256 +1 -1
  6. package/package.json +8 -4
  7. package/src/dataflow/CLAUDE.md +1 -1
  8. package/src/dataflow/catalog.js +842 -17
  9. package/src/dataflow/engine.js +865 -90
  10. package/src/dataflow/incremental.js +13 -0
  11. package/src/dataflow/index.js +18 -5
  12. package/src/dataflow/path-feasibility.js +86 -9
  13. package/src/dataflow/string-domain.js +132 -0
  14. package/src/dataflow/summaries.js +69 -8
  15. package/src/discovery/THREAT_MODEL.md +122 -0
  16. package/src/discovery/disprove.js +10 -1
  17. package/src/discovery/lenses.js +20 -1
  18. package/src/engine.js +108 -3
  19. package/src/ir/CLAUDE.md +2 -1
  20. package/src/ir/balanced-call.js +33 -1
  21. package/src/ir/callgraph.js +26 -2
  22. package/src/ir/class-hierarchy.js +119 -7
  23. package/src/ir/csharp-ir.js +97 -8
  24. package/src/ir/index.js +21 -1
  25. package/src/ir/parser-cs.js +706 -24
  26. package/src/ir/parser-java.js +527 -24
  27. package/src/ir/parser-js.js +22 -0
  28. package/src/ir/parser-php.js +446 -43
  29. package/src/ir/parser-rust.js +1338 -0
  30. package/src/ir/php-include-merge.js +190 -0
  31. package/src/lineage/sink-registry.js +18 -0
  32. package/src/lineage/source-registry.js +23 -0
  33. package/src/posture/csharp-analysis.js +341 -43
  34. package/src/posture/finding-defaults.js +5 -1
  35. package/src/posture/fix-coverage.js +1 -0
  36. package/src/posture/proof-coverage.js +1 -0
  37. package/src/sast/CLAUDE.md +1 -1
  38. package/src/sast/authz.js +144 -1
  39. package/src/sast/crypto-protocol.js +65 -7
  40. package/src/sast/csharp-cross-file.js +125 -0
  41. package/src/sast/csharp-structural.js +78 -2
  42. package/src/sast/csharp.js +574 -42
  43. package/src/sast/java-ast-folding.js +268 -11
  44. package/src/sast/java-bench-extras.js +530 -2
  45. package/src/sast/java-deserialization.js +31 -2
  46. package/src/sast/java-structural-cross-file.js +146 -0
  47. package/src/sast/java-structural.js +499 -6
  48. package/src/sast/ldap-injection.js +133 -3
  49. package/src/sast/rust.js +57 -0
  50. package/src/sast/xss-reflected-multilang.js +252 -3
@@ -47,6 +47,10 @@ const TYPE_MODIFIERS = new Set(['readonly', 'const', 'static', 'public', 'privat
47
47
 
48
48
  const BUILTIN_TYPES = new Set(['void', 'bool', 'byte', 'sbyte', 'char', 'short', 'ushort', 'int', 'uint', 'long', 'ulong', 'float', 'double', 'decimal', 'string', 'object', 'var', 'dynamic']);
49
49
 
50
+ // SARD_80_F1 W4.C11 — compound assignment operators, tokenized as single
51
+ // 'op' tokens by csharp-tokenizer.js but previously never checked for here.
52
+ const CS_COMPOUND_ASSIGN_OPS = new Set(['+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '<<=', '>>=', '??=']);
53
+
50
54
  function isType(tok) {
51
55
  if (!tok) return false;
52
56
  if (tok.kind === 'kw' && BUILTIN_TYPES.has(tok.value)) return true;
@@ -268,7 +272,7 @@ function readMethodHeader(tokens, i, attachedAttrs) {
268
272
  }
269
273
 
270
274
  function walkMethodBody(method) {
271
- const out = { calls: [], ctors: [], assignments: [], decls: [], strings: [] };
275
+ const out = { calls: [], ctors: [], assignments: [], decls: [], strings: [], returns: [] };
272
276
  const tokens = method.bodyTokens;
273
277
  for (let i = 0; i < tokens.length; i++) {
274
278
  const t = tokens[i];
@@ -277,6 +281,45 @@ function walkMethodBody(method) {
277
281
  out.strings.push({ kind: t.kind, value: t.value || tokenText([t]), line: t.line, parts: t.parts || null });
278
282
  continue;
279
283
  }
284
+ // `return <expr>;` (bare `return;` produces no expr and is skipped —
285
+ // interprocedural return-taint resolution has nothing to check there).
286
+ // SARD_80_F1 W3.x: gives `analyzeMethodFlow` visibility into what a
287
+ // same-file callee actually returns, so a call-shaped assignment RHS
288
+ // (`data = GoodG2BSource(req, resp);`) can be resolved against the
289
+ // callee's OWN return taint instead of blindly matching every
290
+ // identifier NAME in the call text (which wrongly taints on `req`/
291
+ // `resp` merely being passed as arguments — see the taintMap loop
292
+ // below and its `_asBareCall`/`resolveCalleeReturnTaint` usage).
293
+ if (t.kind === 'kw' && t.value === 'return') {
294
+ let j = i + 1;
295
+ let depth = 0;
296
+ while (j < tokens.length && (depth > 0 || tokens[j].kind !== 'semi')) {
297
+ const tk = tokens[j];
298
+ if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
299
+ else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
300
+ // SARD_80_F1 W3.x — an unbalanced closer (depth already 0) closes
301
+ // an ENCLOSING construct we didn't open (e.g. this `return` sits
302
+ // inside a `using (...)`/`if (...)` clause with no `;` of its
303
+ // own before the surrounding `)`), not part of our own
304
+ // expression — stop here rather than scanning past it into
305
+ // whatever follows. See the identical fix on the decl/assign/
306
+ // field loops below for the full incident writeup.
307
+ if (depth === 0) break;
308
+ depth--;
309
+ }
310
+ j++;
311
+ }
312
+ const exprTokens = tokens.slice(i + 1, j);
313
+ if (exprTokens.length) {
314
+ out.returns.push({ line: t.line, exprTokens, exprText: tokenText(exprTokens) });
315
+ }
316
+ // Deliberately do NOT skip past the expression tokens (`i = j`): the
317
+ // main loop's own ident/call/assignment branches below still need to
318
+ // walk them, so a statement-level call inside the return
319
+ // (`return string.Format(...)`) is still recorded in `out.calls`
320
+ // exactly as it was before this branch existed.
321
+ continue;
322
+ }
280
323
  // new Type(args)
281
324
  if (t.kind === 'kw' && t.value === 'new') {
282
325
  const tr = readType(tokens, i + 1);
@@ -301,8 +344,27 @@ function walkMethodBody(method) {
301
344
  let j = tr.next + 2;
302
345
  let depth = 0;
303
346
  while (j < tokens.length && (depth > 0 || tokens[j].kind !== 'semi')) {
304
- if (tokens[j].kind === 'lparen' || tokens[j].kind === 'lbrace' || tokens[j].kind === 'lbracket') depth++;
305
- if (tokens[j].kind === 'rparen' || tokens[j].kind === 'rbrace' || tokens[j].kind === 'rbracket') depth--;
347
+ const tk = tokens[j];
348
+ if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
349
+ else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
350
+ // SARD_80_F1 W3.x — a declaration with no `;` of its own,
351
+ // sitting inside an enclosing `using (Type x = expr)` / `for
352
+ // (Type x = expr; …)` clause: the FIRST `)` we see here is
353
+ // that outer construct's own close, not one WE opened (depth
354
+ // is already 0), so stop before consuming it. Previously this
355
+ // fell through to `depth--` unconditionally, going negative
356
+ // and then scanning straight through into the following
357
+ // block body looking for a `;` that could be arbitrarily far
358
+ // away — corrupting this decl's rhsText with everything up to
359
+ // that stray semicolon AND silently absorbing whatever real
360
+ // statement lived in between (found via SARD_80_F1 W4.C14:
361
+ // `using (StreamReader sr = new StreamReader("f")) { data =
362
+ // sr.ReadLine(); }` swallowed the `data = sr.ReadLine()`
363
+ // assignment into `sr`'s own rhsText, deleting it from
364
+ // `out.assignments` entirely).
365
+ if (depth === 0) break;
366
+ depth--;
367
+ }
306
368
  j++;
307
369
  }
308
370
  const rhs = tokens.slice(tr.next + 2, j);
@@ -329,12 +391,31 @@ function walkMethodBody(method) {
329
391
  targetParts.push(tokens[j + 1].value);
330
392
  j += 2;
331
393
  }
332
- if (tokens[j] && tokens[j].kind === 'op' && tokens[j].value === '=') {
394
+ // SARD_80_F1 W4.C11 — compound assignment (`+=`, `-=`, …) was
395
+ // previously invisible here (only bare `=` matched), so a statement
396
+ // like `cmd.CommandText += "..." + tainted + "...";` — Juliet's own
397
+ // universal convention for building CommandText — produced NO
398
+ // assignment record at all, silently dropping it from this
399
+ // detector's taint model. This model (`analyzeMethodFlow`, see
400
+ // csharp-analysis.js) has no un-taint step — a taintMap entry is
401
+ // only ever set true, never cleared — so treating the compound RHS
402
+ // exactly like a plain assign's RHS is already correct: if the
403
+ // target was tainted before, that status is never touched (no
404
+ // clearing branch exists); if this RHS is tainted, it becomes
405
+ // tainted now, same as a `+=` should.
406
+ if (tokens[j] && tokens[j].kind === 'op' &&
407
+ (tokens[j].value === '=' || CS_COMPOUND_ASSIGN_OPS.has(tokens[j].value))) {
333
408
  let k = j + 1;
334
409
  let depth = 0;
335
410
  while (k < tokens.length && (depth > 0 || tokens[k].kind !== 'semi')) {
336
- if (tokens[k].kind === 'lparen' || tokens[k].kind === 'lbrace' || tokens[k].kind === 'lbracket') depth++;
337
- if (tokens[k].kind === 'rparen' || tokens[k].kind === 'rbrace' || tokens[k].kind === 'rbracket') depth--;
411
+ const tk = tokens[k];
412
+ if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
413
+ else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
414
+ // SARD_80_F1 W3.x — see the decl "typed init" loop above for the
415
+ // full incident writeup; same enclosing-construct boundary fix.
416
+ if (depth === 0) break;
417
+ depth--;
418
+ }
338
419
  k++;
339
420
  }
340
421
  const rhs = tokens.slice(j + 1, k);
@@ -419,6 +500,7 @@ function walkMethodBody(method) {
419
500
  method.ctors = out.ctors;
420
501
  method.assignments = out.assignments;
421
502
  method.decls = out.decls;
503
+ method.returns = out.returns;
422
504
  method.strings = out.strings;
423
505
  return out;
424
506
  }
@@ -555,8 +637,15 @@ export function buildCSharpIR(source) {
555
637
  if (tokens[f] && tokens[f].kind === 'op' && tokens[f].value === '=') {
556
638
  let fj = f + 1, depth = 0;
557
639
  while (fj < tokens.length && (depth > 0 || tokens[fj].kind !== 'semi')) {
558
- if (tokens[fj].kind === 'lparen' || tokens[fj].kind === 'lbrace' || tokens[fj].kind === 'lbracket') depth++;
559
- if (tokens[fj].kind === 'rparen' || tokens[fj].kind === 'rbrace' || tokens[fj].kind === 'rbracket') depth--;
640
+ const tk = tokens[fj];
641
+ if (tk.kind === 'lparen' || tk.kind === 'lbrace' || tk.kind === 'lbracket') depth++;
642
+ else if (tk.kind === 'rparen' || tk.kind === 'rbrace' || tk.kind === 'rbracket') {
643
+ // SARD_80_F1 W3.x — see the decl "typed init" loop's
644
+ // header comment for the full incident writeup; same
645
+ // enclosing-construct boundary fix.
646
+ if (depth === 0) break;
647
+ depth--;
648
+ }
560
649
  fj++;
561
650
  }
562
651
  ir.decls.push({ name: tokens[tr.next].value, type: tr.type, rhsTokens: tokens.slice(f + 1, fj), rhsText: tokenText(tokens.slice(f + 1, fj)), line: tk.line, isVar: false, isField: true });
package/src/ir/index.js CHANGED
@@ -18,7 +18,9 @@ import {
18
18
  import { parseJavaFile } from './parser-java.js';
19
19
  import { parseGoFile } from './parser-go.js';
20
20
  import { parsePhpFile } from './parser-php.js';
21
+ import { mergePhpIncludes } from './php-include-merge.js';
21
22
  import { parseRubyFile } from './parser-rb.js';
23
+ import { parseRustFile } from './parser-rust.js';
22
24
  import { parseCppFile, cppExtRe } from './parser-cpp.js';
23
25
  import { buildCallGraph } from './callgraph.js';
24
26
  import { buildClassHierarchy } from './class-hierarchy.js';
@@ -164,6 +166,9 @@ export function buildProjectIR(fileContents) {
164
166
  } else if (/\.rb$/i.test(file)) {
165
167
  const ir = parseRubyFile(file, code);
166
168
  if (ir) perFile[file] = ir;
169
+ } else if (/\.rs$/i.test(file)) {
170
+ const ir = parseRustFile(file, code);
171
+ if (ir) perFile[file] = ir;
167
172
  } else if (cppExtRe().test(file)) {
168
173
  const ir = parseCppFile(file, code);
169
174
  if (ir) perFile[file] = ir;
@@ -175,6 +180,12 @@ export function buildProjectIR(fileContents) {
175
180
  if (ir && ir.file) perFile[ir.file] = ir;
176
181
  }
177
182
  }
183
+ // SARD_80_F1 W5.7 — merge PHP include/require targets' top-level
184
+ // assignments into the includer's own scope before anything else touches
185
+ // the CFGs (SSA numbering included), since PHP's `include` genuinely
186
+ // executes in the caller's variable namespace. See php-include-merge.js's
187
+ // module header for the full rationale and scope.
188
+ try { mergePhpIncludes(perFile); } catch {}
178
189
  if (isSSAEnabled()) {
179
190
  for (const ir of Object.values(perFile)) {
180
191
  for (const fn of (ir.functions || [])) {
@@ -226,6 +237,9 @@ export async function buildProjectIRAsync(fileContents) {
226
237
  } else if (/\.rb$/i.test(file)) {
227
238
  const ir = parseRubyFile(file, code);
228
239
  if (ir) perFile[file] = ir;
240
+ } else if (/\.rs$/i.test(file)) {
241
+ const ir = parseRustFile(file, code);
242
+ if (ir) perFile[file] = ir;
229
243
  } else if (cppExtRe().test(file)) {
230
244
  const ir = parseCppFile(file, code);
231
245
  if (ir) perFile[file] = ir;
@@ -237,6 +251,12 @@ export async function buildProjectIRAsync(fileContents) {
237
251
  if (ir && ir.file) perFile[ir.file] = ir;
238
252
  }
239
253
  }
254
+ // SARD_80_F1 W5.7 — merge PHP include/require targets' top-level
255
+ // assignments into the includer's own scope before anything else touches
256
+ // the CFGs (SSA numbering included), since PHP's `include` genuinely
257
+ // executes in the caller's variable namespace. See php-include-merge.js's
258
+ // module header for the full rationale and scope.
259
+ try { mergePhpIncludes(perFile); } catch {}
240
260
  if (isSSAEnabled()) {
241
261
  for (const ir of Object.values(perFile)) {
242
262
  for (const fn of (ir.functions || [])) {
@@ -269,4 +289,4 @@ export function parsePythonFile(file, code) {
269
289
  return parsePythonFileRegex(file, code);
270
290
  }
271
291
 
272
- export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, parseGoFile, parsePhpFile, parseRubyFile, parseCppFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable, pythonParserDegradation, resetPythonParserDegradation };
292
+ export { parseJsFile, parseJavaFile, parseCSharpFile, parseKotlinFile, parseGoFile, parsePhpFile, parseRubyFile, parseRustFile, parseCppFile, buildCallGraph, buildClassHierarchy, computeSSA, isSSAEnabled, probePythonAvailable, pythonParserDegradation, resetPythonParserDegradation };