@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
@@ -0,0 +1,146 @@
1
+ // SARD_80_F1 W5.41/W5.42 — cross-file literal-resolution SUPPRESSION for
2
+ // this codebase's Java taint-independent structural detectors
3
+ // (java-structural.js's SQLi/cmdi rules, java-bench-extras.js's CWE-601
4
+ // open-redirect rule).
5
+ //
6
+ // W5.40 root-caused java-structural.js's own remaining CWE-89 false-
7
+ // positive bucket (175 of 186 fps, on the real corpus) to Juliet's Flow
8
+ // Variant 51+ idiom: "data passed as an argument from one method to
9
+ // another, in a DIFFERENT class" — confirmed via a direct fetch of the
10
+ // public mirror's own `CWE89_SQL_Injection__database_executeQuery_51a.java`
11
+ // / `_51b.java` pair. File `_51a`'s `goodG2B()` calls
12
+ // `(new ..._51b()).goodG2BSink("foo")` with a hardcoded literal; the sink
13
+ // itself lives in `_51b`'s `goodG2BSink(String data)`, where `data` is a
14
+ // bare method PARAMETER with no same-file assignment at all. Every real
15
+ // caller of `goodG2BSink` passes a literal — but that caller lives in a
16
+ // SIBLING FILE, invisible to the detector's own same-file-only literal-
17
+ // tracking helper. W5.42 found the SAME idiom manifests identically in
18
+ // java-bench-extras.js's independent CWE-601 `response.sendRedirect`
19
+ // detector (a separate, independently-tuned copy of the same "nearest
20
+ // assignment is a literal" heuristic).
21
+ //
22
+ // This module is a SEPARATE, project-wide post-loop pass — NOT a change to
23
+ // either detector's per-file call signature — deliberately mirroring
24
+ // `csharp-cross-file.js`'s own W5.31 design of a standalone project-wide
25
+ // pass rather than threading extra context through the per-file detector
26
+ // cascade. `engine.js`'s `_runFileCascade` is explicitly documented as
27
+ // self-contained so it can run inside a worker thread for deep-mode scans;
28
+ // threading sibling-file content through that interface would require
29
+ // verifying both the sync AND worker code paths carry it correctly, a
30
+ // substantially larger and riskier change than this suppression-only pass,
31
+ // which only ever needs read access to the whole project's file contents —
32
+ // already available at the SAME post-loop stage `scanCsharpCrossFile`
33
+ // itself runs at in engine.js.
34
+ //
35
+ // Unlike csharp-cross-file.js (which ADDS genuinely new findings from
36
+ // cross-file taint), this module only ever SUPPRESSES an already-emitted
37
+ // finding: it re-derives the exact same "is the concatenated/redirected
38
+ // variable provably a literal" question each detector already asks, using
39
+ // the SAME regexes and the SAME literal-tracking mechanism (imported, not
40
+ // duplicated, to avoid drift), extended with an optional sibling-file
41
+ // fallback — see `java-structural.js`'s own `_resolveParamLiteralViaAllCallSites`
42
+ // header comment for why the fallback ALSO requires matching the sink's own
43
+ // declared class name, not just its bare method name: Juliet reuses the
44
+ // SAME generic method names (bad/badSink/goodG2B/goodG2BSink/…) identically
45
+ // across thousands of otherwise-unrelated flow-variant files living in the
46
+ // SAME directory, and blind scrambling preserves that collision by design
47
+ // (the same original word always hashes to the same opaque token,
48
+ // everywhere) — confirmed directly against the real corpus during W5.41's
49
+ // own verification (a bare-name search across one directory's ~980 sibling
50
+ // files matched hundreds of unrelated classes' own same-named methods,
51
+ // causing an unrelated caller elsewhere in the directory to make the check
52
+ // fail closed even for the intended pair).
53
+ //
54
+ // It never invents a new detection capability and never touches any other
55
+ // detector's findings.
56
+ //
57
+ // Scope, deliberately conservative and matching every other interprocedural
58
+ // mechanism in this codebase: SAME DIRECTORY ONLY (Juliet's own convention
59
+ // — a flow variant's split-file siblings always live together), and a real,
60
+ // non-literal call site ANYWHERE (same file or any sibling, for the SAME
61
+ // class) still fails the check closed, exactly like the same-file-only
62
+ // version already does — this can only ever suppress a finding that a
63
+ // same-file-only reading of every real caller would ALSO have deemed safe,
64
+ // not guess from partial information.
65
+ import { blankComments } from './_comment-strip.js';
66
+ import { deadBranchRanges, isLineInDeadRange } from './java-ast-folding.js';
67
+ import { RE as STRUCTURAL_RE, _trailingIdentIsLiteral } from './java-structural.js';
68
+ import { SEND_REDIRECT_RE, _nearestAssignIsLiteral } from './java-bench-extras.js';
69
+
70
+ function _parseProject(fileContents) {
71
+ const javaFiles = Object.entries(fileContents || {}).filter(([p, raw]) => /\.java$/i.test(p) && raw);
72
+ const byDir = new Map();
73
+ const parsedByFile = new Map(); // file -> { raw, code, deadRanges }
74
+ for (const [file, raw] of javaFiles) {
75
+ if (raw.length > 500_000) continue;
76
+ const dir = file.slice(0, file.lastIndexOf('/'));
77
+ if (!byDir.has(dir)) byDir.set(dir, []);
78
+ byDir.get(dir).push(file);
79
+ let code;
80
+ try { code = blankComments(raw); } catch { continue; }
81
+ let ranges = [];
82
+ try { ranges = deadBranchRanges(raw); } catch { /* parse error → no AST info, same fallback as the detectors themselves use */ }
83
+ parsedByFile.set(file, { raw, code, deadRanges: ranges });
84
+ }
85
+ return { javaFiles, byDir, parsedByFile };
86
+ }
87
+
88
+ function _siblingsOf(file, byDir, parsedByFile) {
89
+ const dir = file.slice(0, file.lastIndexOf('/'));
90
+ return (byDir.get(dir) || [])
91
+ .filter((f) => f !== file)
92
+ .map((f) => parsedByFile.get(f))
93
+ .filter(Boolean)
94
+ .map((s) => ({ code: s.code, deadRanges: s.deadRanges }));
95
+ }
96
+
97
+ export function computeJavaStructuralCrossFileSuppressions(fileContents) {
98
+ const drop = new Set();
99
+ const { javaFiles, byDir, parsedByFile } = _parseProject(fileContents);
100
+ if (javaFiles.length < 2) return drop;
101
+
102
+ for (const [file] of javaFiles) {
103
+ const self = parsedByFile.get(file);
104
+ if (!self) continue;
105
+ const siblingFiles = _siblingsOf(file, byDir, parsedByFile);
106
+ if (!siblingFiles.length) continue;
107
+
108
+ // java-structural.js's own SQLi/cmdi structural detectors.
109
+ for (const re of Object.values(STRUCTURAL_RE)) {
110
+ const r = new RegExp(re.source, re.flags);
111
+ let m;
112
+ while ((m = r.exec(self.code))) {
113
+ const varName = m[1];
114
+ if (!varName) continue;
115
+ if (self.deadRanges.length) {
116
+ const line0 = self.code.substring(0, m.index).split('\n').length;
117
+ // A sink already inside a dead range never produced a finding in
118
+ // the first place — nothing to suppress; skip re-deriving it here.
119
+ if (isLineInDeadRange(line0, self.deadRanges)) continue;
120
+ }
121
+ const resolved = _trailingIdentIsLiteral(self.code, varName, m.index, self.deadRanges, 0, siblingFiles);
122
+ if (resolved) {
123
+ const line = self.code.substring(0, m.index).split('\n').length;
124
+ drop.add(`${file}:${line}`);
125
+ }
126
+ }
127
+ }
128
+
129
+ // java-bench-extras.js's own CWE-601 response.sendRedirect detector.
130
+ const redirectRe = new RegExp(SEND_REDIRECT_RE.source, SEND_REDIRECT_RE.flags);
131
+ let rm;
132
+ while ((rm = redirectRe.exec(self.code))) {
133
+ const arg = (rm[1] || '').trim();
134
+ if (!/^[A-Za-z_]\w*$/.test(arg)) continue; // only a bare identifier is a literal-tracking candidate
135
+ if (self.deadRanges.length) {
136
+ const line0 = self.code.substring(0, rm.index).split('\n').length;
137
+ if (isLineInDeadRange(line0, self.deadRanges)) continue;
138
+ }
139
+ if (_nearestAssignIsLiteral(self.code, arg, rm.index, self.deadRanges, 0, siblingFiles)) {
140
+ const line = self.code.substring(0, rm.index).split('\n').length;
141
+ drop.add(`${file}:${line}`);
142
+ }
143
+ }
144
+ }
145
+ return drop;
146
+ }