@clear-capabilities/agentic-security-scanner 0.151.3 → 0.153.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 (51) hide show
  1. package/CHANGELOG.md +125 -0
  2. package/dist/1336.index.js +16010 -0
  3. package/dist/agentic-security.mjs +80 -80
  4. package/dist/agentic-security.mjs.sha256 +1 -1
  5. package/package.json +7 -6
  6. package/src/dataflow/CLAUDE.md +1 -1
  7. package/src/dataflow/backward.js +104 -7
  8. package/src/dataflow/catalog.js +802 -13
  9. package/src/dataflow/cross-service-taint.js +154 -11
  10. package/src/dataflow/engine.js +875 -90
  11. package/src/dataflow/exploit-prover.js +111 -23
  12. package/src/dataflow/incremental.js +13 -0
  13. package/src/dataflow/index.js +29 -6
  14. package/src/dataflow/path-feasibility.js +86 -9
  15. package/src/dataflow/string-domain.js +168 -0
  16. package/src/dataflow/summaries.js +69 -8
  17. package/src/engine.js +118 -9
  18. package/src/ir/CLAUDE.md +2 -1
  19. package/src/ir/balanced-call.js +33 -1
  20. package/src/ir/callgraph.js +26 -2
  21. package/src/ir/class-hierarchy.js +261 -7
  22. package/src/ir/csharp-ir.js +97 -8
  23. package/src/ir/index.js +21 -1
  24. package/src/ir/parser-cs.js +706 -24
  25. package/src/ir/parser-java.js +498 -17
  26. package/src/ir/parser-js.js +22 -0
  27. package/src/ir/parser-php.js +446 -43
  28. package/src/ir/parser-rust.js +1338 -0
  29. package/src/ir/php-include-merge.js +190 -0
  30. package/src/lineage/sink-registry.js +18 -0
  31. package/src/lineage/source-registry.js +23 -0
  32. package/src/posture/csharp-analysis.js +341 -43
  33. package/src/posture/finding-defaults.js +5 -1
  34. package/src/posture/fix-coverage.js +1 -0
  35. package/src/posture/proof-coverage.js +1 -0
  36. package/src/report/index.js +12 -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
  51. package/src/dataflow/smt-feasibility.js +0 -189
@@ -0,0 +1,190 @@
1
+ // PHP cross-file `include`/`require` scope merging.
2
+ //
3
+ // SARD_80_F1 W5.7 — root-caused in W5.5: PHP's `include`/`require`/
4
+ // `include_once`/`require_once` was previously modeled PURELY as a CWE-98
5
+ // (LFI/RFI) SINK (`__php_include__` in parser-php.js, checked for a tainted
6
+ // PATH argument) — the included FILE'S OWN top-level variable assignments
7
+ // were never parsed and merged into the including file's scope. Real PHP
8
+ // semantics: an `include`d file's top-level code executes IN THE CALLER'S
9
+ // OWN VARIABLE SCOPE (unlike an ordinary function call, which gets its own
10
+ // local scope) — so a two-file split (`source.php`: `$tainted =
11
+ // $_GET['x'];` / `main.php`: `include "source.php"; sink($tainted);`) is
12
+ // genuinely, structurally invisible to a taint engine that treats `include`
13
+ // as an opaque sink call. Confirmed via the SARD PHP generator's own public
14
+ // source (`stivalet/PHP-Vuln-test-suite-generator`), which can emit any
15
+ // test case's source/sanitizer/construction component as its OWN separate
16
+ // file connected via exactly this shape.
17
+ //
18
+ // This module runs AFTER buildProjectIR's per-file parse loop (both the
19
+ // sync and async builders — PHP needs neither Java's async parser nor
20
+ // per-language ordering, so one shared post-pass covers both) and mutates
21
+ // each PHP file's `topLevel` function's CFG in place: for every
22
+ // `__php_include__` call site whose path argument is a LITERAL string that
23
+ // resolves to another file already in `perFile`, the included file's own
24
+ // top-level ASSIGN nodes are cloned and spliced into the includer's CFG
25
+ // immediately before the include call site.
26
+ //
27
+ // Deliberately scoped, matching W5.5's own stated caution:
28
+ // - Only LITERAL include paths are resolved. A dynamic/conditional path
29
+ // (`include $_GET['page'] . '.php';`) is left entirely alone — untainted
30
+ // for taint purposes) — over-approximating scope-merging content we
31
+ // genuinely can't rule out for a target we can't even name.
32
+ // - Control flow INSIDE the included file is flattened to a straight-line
33
+ // sequence of its own assign nodes (in CFG-order from its own entry),
34
+ // the same "over-approximate rather than model precisely" tradeoff this
35
+ // taint model already makes elsewhere (recall-preserving: a value that
36
+ // MIGHT be tainted along some path in the included file becomes
37
+ // unconditionally tainted after the splice, never LESS tainted than
38
+ // reality).
39
+ // - The include call node ITSELF is kept, unchanged, after the spliced
40
+ // chain — its own path-argument taint check (the pre-existing CWE-98
41
+ // sink) still runs exactly as before. This module is purely additive.
42
+ // - Cycle-guarded (a visited-file set per top-level merge) and depth-capped
43
+ // (`maxDepth`), so a mutual-include pair or a long chain can't recurse
44
+ // unboundedly.
45
+ // - Node IDs are per-file counters (reset to `pn1`, `pn2`, ... for every
46
+ // file parsed), so every cloned node gets a fresh, namespaced ID — never
47
+ // naively reusing the source file's own IDs, which would collide with
48
+ // the includer's own nodes or a second include of the same file.
49
+ //
50
+ // Known, accepted tradeoff: if the includer and the included file happen to
51
+ // use the SAME variable name for unrelated purposes, the splice can taint
52
+ // the includer's own pre-existing variable of that name. This mirrors the
53
+ // real PHP semantics being modeled (an actual `include` really does share
54
+ // the variable namespace), so it is not a modeling error — genuine
55
+ // real-world PHP code that reuses generic names across included files has
56
+ // exactly this exposure at runtime too.
57
+
58
+ import path from 'node:path';
59
+
60
+ let _mergeCounter = 0;
61
+
62
+ // Test-only reset so the namespace counter doesn't leak between test files.
63
+ export function _resetPhpIncludeMergeCounter() {
64
+ _mergeCounter = 0;
65
+ }
66
+
67
+ function _literalPathOf(argExpr) {
68
+ if (!argExpr || argExpr.kind !== 'literal' || typeof argExpr.value !== 'string') return null;
69
+ const v = argExpr.value;
70
+ const m = /^(["'])((?:[^\\]|\\.)*)\1$/.exec(v);
71
+ if (!m) return null;
72
+ // Unescape the handful of sequences PHP double-quoted strings support in
73
+ // a bare filename; single-quoted strings only ever need `\\` and `\'`.
74
+ return m[2].replace(/\\(.)/g, '$1');
75
+ }
76
+
77
+ // Candidate resolved paths for a literal include argument, relative to the
78
+ // includer's own directory (PHP's `include` resolves relative to the
79
+ // including SCRIPT'S directory by default, absent an include_path entry —
80
+ // exactly the SARD generator's own convention, a bare sibling filename).
81
+ function _resolveCandidates(includerFile, literalPath) {
82
+ if (!literalPath || path.posix.isAbsolute(literalPath)) return [];
83
+ const dir = path.posix.dirname(includerFile.replace(/\\/g, '/'));
84
+ const joined = path.posix.normalize(path.posix.join(dir, literalPath));
85
+ const candidates = [joined];
86
+ if (!/\.(?:php|phtml)$/i.test(joined)) candidates.push(`${joined}.php`);
87
+ return candidates;
88
+ }
89
+
90
+ // BFS node-id order from `cfg.entry`, so cloned assign nodes preserve the
91
+ // included file's own true execution order. Cycle-safe (a `seen` set) even
92
+ // though a straight-line module CFG should never actually cycle.
93
+ function _orderedNodeIds(cfg) {
94
+ const order = [];
95
+ const seen = new Set();
96
+ const queue = [cfg.entry];
97
+ while (queue.length) {
98
+ const id = queue.shift();
99
+ if (!id || seen.has(id) || !cfg.nodes[id]) continue;
100
+ seen.add(id);
101
+ order.push(id);
102
+ for (const s of cfg.nodes[id].succ || []) queue.push(s);
103
+ }
104
+ return order;
105
+ }
106
+
107
+ function _link(nodes, src, dst) {
108
+ if (!nodes[src] || !nodes[dst]) return;
109
+ if (!nodes[src].succ.includes(dst)) nodes[src].succ.push(dst);
110
+ if (!nodes[dst].pred.includes(src)) nodes[dst].pred.push(src);
111
+ }
112
+
113
+ function _topLevelFn(ir) {
114
+ if (!ir || !ir.topLevel) return null;
115
+ return (ir.functions || []).find((f) => f.qid === ir.topLevel) || null;
116
+ }
117
+
118
+ function _mergeIncludesInFunction(fn, currentFile, perFile, visited, depthLeft) {
119
+ if (!fn || !fn.cfg || depthLeft <= 0) return;
120
+ const cfg = fn.cfg;
121
+ // Snapshot node ids up front — the loop body adds new nodes to cfg.nodes,
122
+ // and those must not themselves be re-scanned for (nonexistent) includes.
123
+ const ids = Object.keys(cfg.nodes);
124
+ for (const id of ids) {
125
+ const node = cfg.nodes[id];
126
+ if (!node || node.kind !== 'call' || node.callee !== '__php_include__') continue;
127
+ const literalPath = _literalPathOf((node.args || [])[0]);
128
+ if (!literalPath) continue; // dynamic/non-literal include path — out of scope, never mistaint
129
+ const candidates = _resolveCandidates(currentFile, literalPath);
130
+ const targetFile = candidates.find((c) => perFile[c]);
131
+ if (!targetFile || visited.has(targetFile)) continue;
132
+ const targetFn = _topLevelFn(perFile[targetFile]);
133
+ if (!targetFn || !targetFn.cfg) continue;
134
+
135
+ // Recurse into the included file's OWN includes first, so a transitive
136
+ // chain (A includes B, B includes C) sees C's assignments merged into
137
+ // B before B's are cloned into A.
138
+ const nextVisited = new Set(visited);
139
+ nextVisited.add(targetFile);
140
+ _mergeIncludesInFunction(targetFn, targetFile, perFile, nextVisited, depthLeft - 1);
141
+
142
+ const srcOrder = _orderedNodeIds(targetFn.cfg).filter(
143
+ (nid) => targetFn.cfg.nodes[nid].kind === 'assign',
144
+ );
145
+ if (!srcOrder.length) continue;
146
+
147
+ _mergeCounter++;
148
+ const ns = `phpinc${_mergeCounter}_`;
149
+ const clones = srcOrder.map((nid) => {
150
+ const orig = targetFn.cfg.nodes[nid];
151
+ const cloneId = ns + nid;
152
+ cfg.nodes[cloneId] = {
153
+ kind: 'assign',
154
+ line: orig.line,
155
+ target: orig.target,
156
+ source: orig.source,
157
+ succ: [],
158
+ pred: [],
159
+ };
160
+ return cloneId;
161
+ });
162
+ for (let i = 0; i < clones.length - 1; i++) _link(cfg.nodes, clones[i], clones[i + 1]);
163
+
164
+ // Splice the clone chain in BEFORE the include call node: every current
165
+ // predecessor of the call node now points at the chain's first clone
166
+ // instead, and the chain's last clone points at the (unchanged) call
167
+ // node, whose own pred is reset to just that.
168
+ for (const p of [...node.pred]) {
169
+ const predNode = cfg.nodes[p];
170
+ if (!predNode) continue;
171
+ predNode.succ = predNode.succ.map((s) => (s === id ? clones[0] : s));
172
+ if (!cfg.nodes[clones[0]].pred.includes(p)) cfg.nodes[clones[0]].pred.push(p);
173
+ }
174
+ _link(cfg.nodes, clones[clones.length - 1], id);
175
+ node.pred = [clones[clones.length - 1]];
176
+ }
177
+ }
178
+
179
+ // Mutates every PHP file's `topLevel` function's CFG in place. Call once,
180
+ // after every file in the project has already been parsed into `perFile`
181
+ // (both `buildProjectIR` and `buildProjectIRAsync` share this one pass).
182
+ export function mergePhpIncludes(perFile, opts = {}) {
183
+ const maxDepth = opts.maxDepth ?? 4;
184
+ for (const [file, ir] of Object.entries(perFile || {})) {
185
+ if (!/\.(?:php|phtml)$/i.test(file)) continue;
186
+ const fn = _topLevelFn(ir);
187
+ if (!fn) continue;
188
+ _mergeIncludesInFunction(fn, file, perFile, new Set([file]), maxDepth);
189
+ }
190
+ }
@@ -138,6 +138,11 @@ export const CWE_MAP = Object.freeze({
138
138
  'CWE-78': Object.freeze({ category: null, status: 'unsupported', why: 'shell/process execution — no FR-201 category models process execution' }),
139
139
  'CWE-95': Object.freeze({ category: null, status: 'unsupported', why: 'code evaluation (eval/Function/exec/compile) — destination is an interpreter' }),
140
140
  'CWE-94': Object.freeze({ category: null, status: 'unsupported', why: 'code injection / template compilation — destination is an interpreter or template engine' }),
141
+ // PHP SARD catalog addition: include/require executes the included file
142
+ // as PHP code — same in-process-computation shape as CWE-94/CWE-1336
143
+ // above (the destination is the PHP interpreter itself), no FR-201
144
+ // egress category models it either.
145
+ 'CWE-98': Object.freeze({ category: null, status: 'unsupported', why: 'local/remote file inclusion — destination is the PHP interpreter executing the included file as code, not an FR-201 egress category' }),
141
146
  'CWE-1336': Object.freeze({ category: null, status: 'unsupported', why: 'SSTI — destination is a template engine, not a data destination' }),
142
147
  'CWE-502': Object.freeze({ category: null, status: 'unsupported', why: 'deserialization — destination is a deserializer' }),
143
148
  'CWE-611': Object.freeze({ category: null, status: 'unsupported', why: 'XXE — destination is an XML parser' }),
@@ -146,6 +151,19 @@ export const CWE_MAP = Object.freeze({
146
151
  'CWE-787': Object.freeze({ category: null, status: 'unsupported', why: 'out-of-bounds write — destination is raw memory' }),
147
152
  'CWE-1333': Object.freeze({ category: null, status: 'unsupported', why: 'ReDoS — destination is the regex engine' }),
148
153
  'CWE-114': Object.freeze({ category: null, status: 'unsupported', why: 'untrusted library load — destination is the dynamic loader' }),
154
+ // Rust catalog additions (SARD 80% F1 push).
155
+ 'CWE-117': Object.freeze({ category: 'log', status: 'modeled', why: 'log injection — the sink IS a log-writing call (log!/tracing! macros)' }),
156
+ 'CWE-770': Object.freeze({ category: null, status: 'unsupported', why: 'uncontrolled resource consumption — the sink is an in-process allocation, not an egress destination' }),
157
+ // SARD_80_F1_SCANNER_PRD.md Juliet Java audit: two CWEs newly added to
158
+ // CATALOG (Class.forName/Method.invoke reflection; String.format/printf
159
+ // uncontrolled format string). Both are in-process-computation
160
+ // destinations, same class as CWE-95/CWE-1333 above — no FR-201 egress
161
+ // category models either.
162
+ 'CWE-470': Object.freeze({ category: null, status: 'unsupported', why: 'unsafe reflection — destination is the class loader / reflection API, not an FR-201 egress category' }),
163
+ 'CWE-134': Object.freeze({ category: null, status: 'unsupported', why: 'uncontrolled format string — destination is the string formatter, not an FR-201 egress category' }),
164
+ // SARD 80% F1 push catalog additions (2026-09).
165
+ 'CWE-81': Object.freeze({ category: 'http-response', status: 'modeled', why: 'reflected script in an error-message response (e.g. response.sendError/StatusDescription) is a response-header/body writer, the same destination as ordinary reflected XSS' }),
166
+ 'CWE-91': Object.freeze({ category: null, status: 'unsupported', why: 'XML/XPath injection (SimpleXMLElement::xpath and equivalents) — destination is a query engine over an in-memory document, same reasoning as CWE-643' }),
149
167
  });
150
168
 
151
169
  // §5.2 — CWE-79's refinement key. FR-201 lumps "browser DOM or client
@@ -150,6 +150,10 @@ export const NO_PROVENANCE_OVERRIDES = Object.freeze({
150
150
  'java-request-getCookies': 'http-cookie',
151
151
  'java-request-getInputStream': 'http-body',
152
152
  'java-request-getReader': 'http-body',
153
+ // SARD_80_F1_SCANNER_PRD.md Juliet Java audit
154
+ 'java-request-getAttribute': 'http-query', // request-scoped attribute bag, same request-derived shape as getParameter
155
+ 'java-request-getQueryString': 'http-query',
156
+ 'java-cookie-getvalue': 'http-cookie',
153
157
  // Java / stdlib
154
158
  'java-system-getenv': 'env-value',
155
159
  'java-system-getProperty': 'env-value', // JVM system properties, env-adjacent
@@ -159,6 +163,18 @@ export const NO_PROVENANCE_OVERRIDES = Object.freeze({
159
163
  // vocabulary for exactly this shape.
160
164
  'java-resultset-getstring': 'database-read',
161
165
  'java-resultset-getobject': 'database-read',
166
+ // C# / stdlib console+stream reads and ADO.NET result-set reads — same
167
+ // two shapes as java-io-readline/java-resultset-getstring above, added by
168
+ // a separate SARD_80_F1_SCANNER_PRD.md catalog push. catalog.js
169
+ // deliberately declares no `provenance` field for these (an earlier
170
+ // draft used invented 'io-read'/'database' provenance strings that
171
+ // PROVENANCE_MAP has never recognized).
172
+ 'cs-console-readline': 'user-input',
173
+ 'cs-reader-readtoend': 'user-input',
174
+ 'cs-console-read': 'user-input',
175
+ 'cs-datareader-getstring': 'database-read',
176
+ 'cs-datareader-getvalue': 'database-read',
177
+ 'cs-datareader-getint32': 'database-read',
162
178
  // Java / Spring annotations
163
179
  'java-spring-requestparam': 'http-query',
164
180
  'java-spring-pathvariable': 'http-route',
@@ -228,6 +244,13 @@ export const NO_PROVENANCE_OVERRIDES = Object.freeze({
228
244
  'kt-request-param': 'http-query',
229
245
  'kt-request-header': 'http-header',
230
246
  'kt-ktor-parameters': 'http-query',
247
+ // Rust — the whole HttpRequest/Request object (actix/axum/hyper). No
248
+ // single SOURCE_CATEGORIES value names "the whole request"; http-body
249
+ // is the closest existing precedent (go-r-body/go-r-form already map a
250
+ // merged/whole-request-shaped read the same way).
251
+ 'rs-extract-httprequest': 'http-body',
252
+ 'rs-extract-request': 'http-body',
253
+ 'rs-closure-extract-request': 'http-body',
231
254
  });
232
255
 
233
256
  // ─────────────────────────────────────────────────────────────────────────