@clear-capabilities/agentic-security-scanner 0.79.0 → 0.84.1

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 (122) hide show
  1. package/dist/178.index.js +1 -1
  2. package/dist/333.index.js +283 -0
  3. package/dist/384.index.js +1 -1
  4. package/dist/637.index.js +1 -1
  5. package/dist/838.index.js +1 -1
  6. package/dist/839.index.js +170 -0
  7. package/dist/985.index.js +140 -1
  8. package/dist/agentic-security.mjs +10 -10
  9. package/dist/agentic-security.mjs.sha256 +1 -1
  10. package/package.json +7 -5
  11. package/src/.agentic-security/findings.json +117732 -0
  12. package/src/.agentic-security/last-scan.json +117732 -0
  13. package/src/.agentic-security/last-scan.json.sig +1 -0
  14. package/src/.agentic-security/scan-history.json +12946 -0
  15. package/src/.agentic-security/streak.json +21 -0
  16. package/src/dataflow/.agentic-security/findings.json +6086 -0
  17. package/src/dataflow/.agentic-security/last-scan.json +6086 -0
  18. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  19. package/src/dataflow/.agentic-security/scan-history.json +250 -0
  20. package/src/dataflow/.agentic-security/streak.json +21 -0
  21. package/src/dataflow/cross-service-taint.js +201 -0
  22. package/src/dataflow/formal-verify.js +204 -0
  23. package/src/dataflow/ifds-precise.js +222 -0
  24. package/src/dataflow/k2-summary-cache.js +153 -0
  25. package/src/dataflow/lib-taint-summaries.js +198 -0
  26. package/src/dataflow/privacy-taint.js +205 -0
  27. package/src/dataflow/smt-feasibility.js +189 -0
  28. package/src/engine.js +825 -127
  29. package/src/ir/.agentic-security/findings.json +4011 -0
  30. package/src/ir/.agentic-security/last-scan.json +4011 -0
  31. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  32. package/src/ir/.agentic-security/scan-history.json +193 -0
  33. package/src/ir/.agentic-security/streak.json +20 -0
  34. package/src/ir/cpp-preprocessor.js +142 -0
  35. package/src/ir/csharp-ir.js +604 -0
  36. package/src/ir/universal-ir.js +403 -0
  37. package/src/mcp/.agentic-security/findings.json +8632 -0
  38. package/src/mcp/.agentic-security/last-scan.json +8632 -0
  39. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  40. package/src/mcp/.agentic-security/scan-history.json +331 -0
  41. package/src/mcp/.agentic-security/streak.json +20 -0
  42. package/src/mcp/tools.js +140 -1
  43. package/src/posture/.agentic-security/findings.json +77181 -0
  44. package/src/posture/.agentic-security/last-scan.json +77181 -0
  45. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  46. package/src/posture/.agentic-security/scan-history.json +8904 -0
  47. package/src/posture/.agentic-security/streak.json +21 -0
  48. package/src/posture/api-contract.js +193 -0
  49. package/src/posture/attack-taxonomy.js +227 -0
  50. package/src/posture/auditor-walkthrough.js +252 -0
  51. package/src/posture/claude-authorship.js +197 -0
  52. package/src/posture/compliance-frameworks/.agentic-security/findings.json +80 -0
  53. package/src/posture/compliance-frameworks/.agentic-security/last-scan.json +80 -0
  54. package/src/posture/compliance-frameworks/.agentic-security/last-scan.json.sig +1 -0
  55. package/src/posture/compliance-frameworks/.agentic-security/scan-history.json +90 -0
  56. package/src/posture/compliance-frameworks/.agentic-security/streak.json +22 -0
  57. package/src/posture/compliance-frameworks/ccpa.json +32 -0
  58. package/src/posture/compliance-frameworks/eu-ai-act.json +51 -0
  59. package/src/posture/compliance-frameworks/gdpr.json +45 -0
  60. package/src/posture/compliance-frameworks/hipaa-security-rule.json +56 -0
  61. package/src/posture/compliance-frameworks/nist-ai-600-1.json +51 -0
  62. package/src/posture/compliance-frameworks/nist-csf-2.json +73 -0
  63. package/src/posture/compliance-frameworks/owasp-asvs-5.json +79 -0
  64. package/src/posture/compliance-frameworks/owasp-llm-top-10.json +69 -0
  65. package/src/posture/compliance-policy.js +218 -0
  66. package/src/posture/composite-risk.js +122 -0
  67. package/src/posture/cross-repo-memory.js +180 -0
  68. package/src/posture/csharp-analysis.js +330 -0
  69. package/src/posture/dep-add-guard.js +197 -0
  70. package/src/posture/exploit-bundle.js +210 -0
  71. package/src/posture/federated-learning.js +172 -0
  72. package/src/posture/findings-memory.js +152 -0
  73. package/src/posture/fix-style-mirror.js +118 -0
  74. package/src/posture/git-history.js +141 -0
  75. package/src/posture/intent-context.js +175 -0
  76. package/src/posture/license-attributions.js +94 -0
  77. package/src/posture/license-graph.js +238 -0
  78. package/src/posture/model-rescan.js +76 -0
  79. package/src/posture/pattern-propagation.js +39 -0
  80. package/src/posture/pqc-migration-plan.js +158 -0
  81. package/src/posture/pr-augment.js +234 -0
  82. package/src/posture/reachability-filter.js +33 -2
  83. package/src/posture/realtime-cve-monitor.js +214 -0
  84. package/src/posture/risk-dollars.js +158 -0
  85. package/src/posture/runtime-correlation.js +174 -0
  86. package/src/posture/sbom-diff.js +171 -0
  87. package/src/posture/sca-policy.js +235 -0
  88. package/src/posture/sca-upgrade.js +259 -0
  89. package/src/posture/threat-model-auto.js +268 -0
  90. package/src/posture/threat-model-grounding.js +169 -0
  91. package/src/posture/time-to-fix.js +129 -0
  92. package/src/posture/triage-learning.js +170 -0
  93. package/src/posture/triage-memory.js +151 -0
  94. package/src/posture/triage.js +40 -1
  95. package/src/posture/watch-mode.js +171 -0
  96. package/src/posture/workflow-installer.js +231 -0
  97. package/src/sast/.agentic-security/findings.json +6154 -0
  98. package/src/sast/.agentic-security/last-scan.json +6154 -0
  99. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  100. package/src/sast/.agentic-security/scan-history.json +941 -0
  101. package/src/sast/.agentic-security/streak.json +22 -0
  102. package/src/sast/_secret-entropy.js +145 -0
  103. package/src/sast/cloud-iam.js +312 -0
  104. package/src/sast/cpp.js +138 -4
  105. package/src/sast/crypto-protocol.js +388 -0
  106. package/src/sast/csharp-tokenizer.js +392 -0
  107. package/src/sast/csharp.js +924 -138
  108. package/src/sast/dapp-frontend.js +200 -0
  109. package/src/sast/k8s-admission.js +271 -0
  110. package/src/sast/llm-app.js +272 -0
  111. package/src/sast/ml-supply-chain.js +259 -0
  112. package/src/sast/mobile.js +224 -0
  113. package/src/sast/post-quantum-crypto.js +348 -0
  114. package/src/sast/web3-advanced.js +375 -0
  115. package/src/sca/.agentic-security/findings.json +7460 -0
  116. package/src/sca/.agentic-security/last-scan.json +7460 -0
  117. package/src/sca/.agentic-security/last-scan.json.sig +1 -0
  118. package/src/sca/.agentic-security/scan-history.json +113 -0
  119. package/src/sca/.agentic-security/streak.json +21 -0
  120. package/src/sca/CLAUDE.md +161 -0
  121. package/src/sca/binary-metadata.js +37 -15
  122. package/src/sca/sigstore-verify.js +215 -0
@@ -0,0 +1,604 @@
1
+ // C# IR — a CST-ish intermediate representation produced from the token
2
+ // stream, sufficient for security detectors without requiring a full
3
+ // parse (no overload resolution, no generics inference, no semantic model).
4
+ //
5
+ // Why hand-rolled rather than tree-sitter-c-sharp? The scanner's design
6
+ // principle is "no runtime cloud calls / no new runtime deps unless lazy."
7
+ // tree-sitter native bindings drag in node-gyp; WASM is bundlable but adds
8
+ // a 1.5 MB blob. For Juliet C# + idiomatic ASP.NET code, a focused
9
+ // hand-rolled CST gets us most of the way at zero dependency cost.
10
+ //
11
+ // IR shape (what each node carries):
12
+ //
13
+ // File: { usings: string[], namespaces: Namespace[], classes: ClassDecl[],
14
+ // methods: MethodDecl[], // top-level (rare; usually inside class)
15
+ // calls: CallExpr[], // every method call site
16
+ // ctors: NewExpr[], // every `new TypeName(...)` site
17
+ // assignments: Assign[], // every `x = ...` or `x.Member = ...`
18
+ // decls: VarDecl[], // every typed local + field
19
+ // attrs: AttributeUse[], // every [Foo(...)] use
20
+ // strings: StringLit[], // every string/verbatim/interp literal
21
+ // }
22
+ //
23
+ // ClassDecl: { name, attrs: AttributeUse[], line, endLine, methods: [], fields: [] }
24
+ // MethodDecl: { name, returnType, params: [{name,type}], attrs: [], line, endLine,
25
+ // bodyTokens, calls: [], decls: [], assignments: [], strings: [] }
26
+ // CallExpr: { receiver: string|null, method: string, args: ArgExpr[],
27
+ // line, scope: MethodDecl|null }
28
+ // NewExpr: { type: string, args: ArgExpr[], line, scope }
29
+ // Assign: { target: string, isMember: bool, memberPath: string|null,
30
+ // rhsTokens, line, scope }
31
+ // VarDecl: { name, type, rhsTokens|null, line, scope, isVar: bool }
32
+ // AttributeUse: { name, argsRaw: string, line, attachedTo: 'class'|'method'|'param'|'field' }
33
+ //
34
+ // ArgExpr: { tokens, // raw tokens between commas
35
+ // text, // simple textual concatenation for matching
36
+ // idents // identifier names referenced }
37
+ //
38
+ // The IR is intentionally shallow: a method's body is a flat list of CallExpr /
39
+ // Assign / VarDecl, not a tree. Detectors that need control-flow analysis
40
+ // (the existing taint engine) consume IR.calls / IR.assignments and rebuild
41
+ // their own data structures.
42
+
43
+ import { tokenize, identsIn } from '../sast/csharp-tokenizer.js';
44
+
45
+ // Type modifiers that appear before a type name in a declaration.
46
+ const TYPE_MODIFIERS = new Set(['readonly', 'const', 'static', 'public', 'private', 'protected', 'internal', 'override', 'virtual', 'abstract', 'sealed', 'partial', 'async', 'new', 'unsafe', 'extern', 'ref', 'out', 'in', 'params']);
47
+
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
+
50
+ function isType(tok) {
51
+ if (!tok) return false;
52
+ if (tok.kind === 'kw' && BUILTIN_TYPES.has(tok.value)) return true;
53
+ if (tok.kind === 'ident') return /^[A-Z]/.test(tok.value) || tok.value === 'var';
54
+ return false;
55
+ }
56
+
57
+ function readType(tokens, i) {
58
+ // Greedy type reader: handles `Foo`, `Foo.Bar.Baz`, `List<int>`, `Foo[]`,
59
+ // `Foo?`, `Foo.Bar<X,Y>`, `Span<byte>[]`. Returns { type, next } or null.
60
+ const start = i;
61
+ if (!isType(tokens[i])) return null;
62
+ let buf = tokens[i].value;
63
+ i++;
64
+ // Dotted-namespace qualified name
65
+ while (tokens[i] && tokens[i].kind === 'dot' && tokens[i + 1] && tokens[i + 1].kind === 'ident') {
66
+ buf += '.' + tokens[i + 1].value;
67
+ i += 2;
68
+ }
69
+ // Generic <X[,Y]>
70
+ if (tokens[i] && tokens[i].kind === 'op' && tokens[i].value === '<') {
71
+ let depth = 1;
72
+ let g = '<';
73
+ i++;
74
+ while (tokens[i] && depth > 0) {
75
+ const t = tokens[i];
76
+ if (t.kind === 'op' && t.value === '<') depth++;
77
+ if (t.kind === 'op' && t.value === '>') depth--;
78
+ if (t.kind === 'op' && t.value === '>>') { depth -= 2; g += '>>'; i++; continue; }
79
+ g += (t.value || '');
80
+ i++;
81
+ if (depth === 0) break;
82
+ }
83
+ buf += g;
84
+ }
85
+ // Array brackets [] or [,]
86
+ while (tokens[i] && tokens[i].kind === 'lbracket') {
87
+ let j = i + 1, ok = true, depth = 1;
88
+ while (tokens[j] && depth > 0) {
89
+ if (tokens[j].kind === 'lbracket') depth++;
90
+ else if (tokens[j].kind === 'rbracket') depth--;
91
+ else if (tokens[j].kind !== 'comma' && tokens[j].kind !== 'op') { ok = false; break; }
92
+ j++;
93
+ }
94
+ if (!ok) break;
95
+ buf += tokens.slice(i, j).map(t => t.value || '').join('');
96
+ i = j;
97
+ }
98
+ // Nullable ?
99
+ if (tokens[i] && tokens[i].kind === 'op' && tokens[i].value === '?') {
100
+ buf += '?'; i++;
101
+ }
102
+ return { type: buf, next: i, startIdx: start };
103
+ }
104
+
105
+ function tokenText(tokens) {
106
+ // Re-render a token slice into a plain string for `text` matching.
107
+ // String literal contents become their literal value; interpolations are
108
+ // expanded as `"…{expr}…"` so detectors can grep for `${var}` shapes.
109
+ // Inserts a space between adjacent "word" tokens (idents + keywords) so
110
+ // `new SqlCommand` doesn't render as `newSqlCommand` and break downstream
111
+ // regex matching.
112
+ const parts = [];
113
+ let prevWasWord = false;
114
+ for (const t of tokens || []) {
115
+ if (!t || t.kind === 'eof') continue;
116
+ const isWord = (t.kind === 'ident' || t.kind === 'kw');
117
+ if (isWord && prevWasWord) parts.push(' ');
118
+ if (t.kind === 'string') parts.push(`"${t.value}"`);
119
+ else if (t.kind === 'verbatim') parts.push(`@"${t.value}"`);
120
+ else if (t.kind === 'interp') {
121
+ parts.push('"');
122
+ for (const p of t.parts || []) {
123
+ if (p.kind === 'lit') parts.push(p.text);
124
+ else if (p.kind === 'expr') parts.push('{' + p.text + '}');
125
+ }
126
+ parts.push('"');
127
+ }
128
+ else if (t.kind === 'char') parts.push(`'${t.value}'`);
129
+ else parts.push(t.value || '');
130
+ prevWasWord = isWord;
131
+ }
132
+ return parts.join('');
133
+ }
134
+
135
+ function splitArgsByComma(tokens) {
136
+ // Split a token slice on top-level commas (depth-aware on (), [], <>, {}).
137
+ const out = []; let cur = []; let depth = 0;
138
+ for (const t of tokens) {
139
+ if (t.kind === 'lparen' || t.kind === 'lbrace' || t.kind === 'lbracket' || (t.kind === 'op' && t.value === '<')) depth++;
140
+ if (t.kind === 'rparen' || t.kind === 'rbrace' || t.kind === 'rbracket' || (t.kind === 'op' && t.value === '>')) depth--;
141
+ if (depth === 0 && t.kind === 'comma') { out.push(cur); cur = []; continue; }
142
+ cur.push(t);
143
+ }
144
+ if (cur.length) out.push(cur);
145
+ return out;
146
+ }
147
+
148
+ function makeArgExpr(tokens) {
149
+ return { tokens, text: tokenText(tokens), idents: identsIn(tokens) };
150
+ }
151
+
152
+ // Walk balanced delimiters and return the index of the matching close.
153
+ function matchClose(tokens, openIdx, openKind, closeKind) {
154
+ let depth = 0;
155
+ for (let i = openIdx; i < tokens.length; i++) {
156
+ if (tokens[i].kind === openKind) depth++;
157
+ else if (tokens[i].kind === closeKind) {
158
+ depth--;
159
+ if (depth === 0) return i;
160
+ }
161
+ }
162
+ return -1;
163
+ }
164
+
165
+ // Read an attribute use `[Name(args)]` starting at attr-open.
166
+ function readAttribute(tokens, i, attachedTo) {
167
+ const open = i;
168
+ const close = matchClose(tokens, i, 'attr-open', 'attr-close');
169
+ if (close === -1) return null;
170
+ // Inside: ident(.ident)* (args)? — possibly multiple attributes comma-separated
171
+ // e.g. [HttpGet("/x"), Authorize] — we record only the first for simplicity.
172
+ let j = open + 1;
173
+ const nameTokens = [];
174
+ while (j < close && (tokens[j].kind === 'ident' || tokens[j].kind === 'dot')) {
175
+ nameTokens.push(tokens[j].value);
176
+ j++;
177
+ }
178
+ const name = nameTokens.join('');
179
+ let argsRaw = '';
180
+ if (tokens[j] && tokens[j].kind === 'lparen') {
181
+ const aClose = matchClose(tokens, j, 'lparen', 'rparen');
182
+ if (aClose !== -1) {
183
+ argsRaw = tokenText(tokens.slice(j + 1, aClose));
184
+ j = aClose + 1;
185
+ }
186
+ }
187
+ return { attr: { name, argsRaw, line: tokens[open].line, attachedTo }, next: close + 1 };
188
+ }
189
+
190
+ // Read a method header: `[attrs]* modifiers* returnType Name(params) (`{`|`;`|`=>`)
191
+ // Returns { method, bodyStart, bodyEnd, next } or null.
192
+ function readMethodHeader(tokens, i, attachedAttrs) {
193
+ // Skip modifiers
194
+ let j = i;
195
+ const modifiers = [];
196
+ while (tokens[j] && tokens[j].kind === 'kw' && TYPE_MODIFIERS.has(tokens[j].value)) {
197
+ modifiers.push(tokens[j].value);
198
+ j++;
199
+ }
200
+ // Return type
201
+ const tr = readType(tokens, j);
202
+ if (!tr) return null;
203
+ j = tr.next;
204
+ // Method name
205
+ if (!tokens[j] || tokens[j].kind !== 'ident') return null;
206
+ const name = tokens[j].value;
207
+ j++;
208
+ // Generic <T> on method
209
+ if (tokens[j] && tokens[j].kind === 'op' && tokens[j].value === '<') {
210
+ const close = (function findGenericClose(){
211
+ let d = 1, k = j + 1;
212
+ while (tokens[k] && d > 0) {
213
+ if (tokens[k].kind === 'op' && tokens[k].value === '<') d++;
214
+ if (tokens[k].kind === 'op' && tokens[k].value === '>') d--;
215
+ if (tokens[k].kind === 'op' && tokens[k].value === '>>') d -= 2;
216
+ k++; if (d === 0) return k - 1;
217
+ }
218
+ return -1;
219
+ })();
220
+ if (close !== -1) j = close + 1;
221
+ }
222
+ // Params
223
+ if (!tokens[j] || tokens[j].kind !== 'lparen') return null;
224
+ const paramOpen = j;
225
+ const paramClose = matchClose(tokens, j, 'lparen', 'rparen');
226
+ if (paramClose === -1) return null;
227
+ const paramTokens = tokens.slice(paramOpen + 1, paramClose);
228
+ const params = [];
229
+ for (const argTokens of splitArgsByComma(paramTokens)) {
230
+ let k = 0;
231
+ while (argTokens[k] && argTokens[k].kind === 'attr-open') {
232
+ const ac = matchClose(argTokens, k, 'attr-open', 'attr-close');
233
+ k = ac === -1 ? argTokens.length : ac + 1;
234
+ }
235
+ while (argTokens[k] && argTokens[k].kind === 'kw' && TYPE_MODIFIERS.has(argTokens[k].value)) k++;
236
+ const tr2 = readType(argTokens, k);
237
+ if (!tr2) continue;
238
+ k = tr2.next;
239
+ if (argTokens[k] && argTokens[k].kind === 'ident') {
240
+ params.push({ type: tr2.type, name: argTokens[k].value });
241
+ }
242
+ }
243
+ j = paramClose + 1;
244
+ // Optional: where T : ...
245
+ while (tokens[j] && tokens[j].kind === 'kw' && tokens[j].value === 'where') {
246
+ while (tokens[j] && tokens[j].kind !== 'lbrace' && tokens[j].kind !== 'arrow' && tokens[j].kind !== 'semi') j++;
247
+ }
248
+ // Body
249
+ if (!tokens[j]) return null;
250
+ if (tokens[j].kind === 'semi') {
251
+ // Abstract / interface method — no body to walk.
252
+ return { method: { name, returnType: tr.type, params, attrs: attachedAttrs, line: tokens[i].line, endLine: tokens[j].line, modifiers, bodyTokens: [] }, next: j + 1 };
253
+ }
254
+ if (tokens[j].kind === 'arrow') {
255
+ // Expression-bodied member: => expr;
256
+ let k = j + 1;
257
+ while (tokens[k] && tokens[k].kind !== 'semi') k++;
258
+ const body = tokens.slice(j + 1, k);
259
+ return { method: { name, returnType: tr.type, params, attrs: attachedAttrs, line: tokens[i].line, endLine: tokens[k]?.line || tokens[j].line, modifiers, bodyTokens: body }, next: k + 1 };
260
+ }
261
+ if (tokens[j].kind === 'lbrace') {
262
+ const bClose = matchClose(tokens, j, 'lbrace', 'rbrace');
263
+ if (bClose === -1) return null;
264
+ const body = tokens.slice(j + 1, bClose);
265
+ return { method: { name, returnType: tr.type, params, attrs: attachedAttrs, line: tokens[i].line, endLine: tokens[bClose].line, modifiers, bodyTokens: body }, next: bClose + 1 };
266
+ }
267
+ return null;
268
+ }
269
+
270
+ function walkMethodBody(method) {
271
+ const out = { calls: [], ctors: [], assignments: [], decls: [], strings: [] };
272
+ const tokens = method.bodyTokens;
273
+ for (let i = 0; i < tokens.length; i++) {
274
+ const t = tokens[i];
275
+ // String literals
276
+ if (t.kind === 'string' || t.kind === 'verbatim' || t.kind === 'interp') {
277
+ out.strings.push({ kind: t.kind, value: t.value || tokenText([t]), line: t.line, parts: t.parts || null });
278
+ continue;
279
+ }
280
+ // new Type(args)
281
+ if (t.kind === 'kw' && t.value === 'new') {
282
+ const tr = readType(tokens, i + 1);
283
+ if (tr && tokens[tr.next] && tokens[tr.next].kind === 'lparen') {
284
+ const open = tr.next;
285
+ const close = matchClose(tokens, open, 'lparen', 'rparen');
286
+ if (close !== -1) {
287
+ const args = splitArgsByComma(tokens.slice(open + 1, close)).map(makeArgExpr);
288
+ out.ctors.push({ type: tr.type, args, line: t.line });
289
+ i = close;
290
+ continue;
291
+ }
292
+ }
293
+ }
294
+ // Variable declaration: TypeOrVar identifier (= ...)? ;
295
+ if (isType(t)) {
296
+ const tr = readType(tokens, i);
297
+ if (tr && tokens[tr.next] && tokens[tr.next].kind === 'ident' && tokens[tr.next + 1]) {
298
+ const after = tokens[tr.next + 1];
299
+ if (after.kind === 'op' && after.value === '=') {
300
+ // typed init
301
+ let j = tr.next + 2;
302
+ let depth = 0;
303
+ 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--;
306
+ j++;
307
+ }
308
+ const rhs = tokens.slice(tr.next + 2, j);
309
+ out.decls.push({ name: tokens[tr.next].value, type: tr.type, rhsTokens: rhs, rhsText: tokenText(rhs), line: t.line, isVar: tr.type === 'var' });
310
+ i = j;
311
+ continue;
312
+ }
313
+ if (after.kind === 'semi' || after.kind === 'comma') {
314
+ out.decls.push({ name: tokens[tr.next].value, type: tr.type, rhsTokens: null, rhsText: '', line: t.line, isVar: tr.type === 'var' });
315
+ i = tr.next + 1;
316
+ continue;
317
+ }
318
+ }
319
+ }
320
+ // Assignment / call: <ident-or-builtin-type-kw>(.member)* (= ...)? ( ( … ) )?
321
+ // Built-in type keywords like `string` are valid receivers for static
322
+ // methods (`string.Format`, `int.TryParse`, etc.), so we accept them here.
323
+ if (t.kind === 'ident' || (t.kind === 'kw' && BUILTIN_TYPES.has(t.value) && t.value !== 'void' && t.value !== 'var' && t.value !== 'dynamic')) {
324
+ // Collect dotted target
325
+ let j = i;
326
+ const targetParts = [tokens[j].value];
327
+ j++;
328
+ while (tokens[j] && tokens[j].kind === 'dot' && tokens[j + 1] && tokens[j + 1].kind === 'ident') {
329
+ targetParts.push(tokens[j + 1].value);
330
+ j += 2;
331
+ }
332
+ if (tokens[j] && tokens[j].kind === 'op' && tokens[j].value === '=') {
333
+ let k = j + 1;
334
+ let depth = 0;
335
+ 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--;
338
+ k++;
339
+ }
340
+ const rhs = tokens.slice(j + 1, k);
341
+ out.assignments.push({
342
+ target: targetParts[0],
343
+ isMember: targetParts.length > 1,
344
+ memberPath: targetParts.length > 1 ? targetParts.slice(1).join('.') : null,
345
+ fullTarget: targetParts.join('.'),
346
+ rhsTokens: rhs, rhsText: tokenText(rhs),
347
+ line: t.line,
348
+ });
349
+ i = k;
350
+ continue;
351
+ }
352
+ // Call: ident(.ident)* ( args )
353
+ if (tokens[j] && tokens[j].kind === 'lparen') {
354
+ const open = j;
355
+ const close = matchClose(tokens, open, 'lparen', 'rparen');
356
+ if (close !== -1) {
357
+ const args = splitArgsByComma(tokens.slice(open + 1, close)).map(makeArgExpr);
358
+ out.calls.push({
359
+ receiver: targetParts.length > 1 ? targetParts.slice(0, -1).join('.') : null,
360
+ method: targetParts[targetParts.length - 1],
361
+ args, line: t.line,
362
+ fullPath: targetParts.join('.'),
363
+ });
364
+ i = close;
365
+ continue;
366
+ }
367
+ }
368
+ }
369
+ }
370
+ // Second pass: walk inside the rhs of every decl and assignment, looking
371
+ // for embedded calls and ctors. C# expressions can carry calls anywhere
372
+ // (`var x = Foo(Bar() + Baz())`), and the first pass only collected
373
+ // top-level statement-level calls. We don't try to track receiver
374
+ // typing for these nested calls — detectors that need that look up the
375
+ // receiver in flow.typeMap separately.
376
+ function _scanNested(tokens, line) {
377
+ for (let k = 0; k < tokens.length; k++) {
378
+ const tk = tokens[k];
379
+ if (tk.kind === 'kw' && tk.value === 'new') {
380
+ const tr = readType(tokens, k + 1);
381
+ if (tr && tokens[tr.next] && tokens[tr.next].kind === 'lparen') {
382
+ const open = tr.next;
383
+ const close = matchClose(tokens, open, 'lparen', 'rparen');
384
+ if (close !== -1) {
385
+ out.ctors.push({ type: tr.type, args: splitArgsByComma(tokens.slice(open + 1, close)).map(makeArgExpr), line: tk.line || line });
386
+ }
387
+ }
388
+ continue;
389
+ }
390
+ if (tk.kind === 'ident' || (tk.kind === 'kw' && BUILTIN_TYPES.has(tk.value) && tk.value !== 'void' && tk.value !== 'var' && tk.value !== 'dynamic')) {
391
+ let j = k;
392
+ const parts = [tk.value];
393
+ j++;
394
+ while (tokens[j] && tokens[j].kind === 'dot' && tokens[j + 1] && tokens[j + 1].kind === 'ident') {
395
+ parts.push(tokens[j + 1].value);
396
+ j += 2;
397
+ }
398
+ if (tokens[j] && tokens[j].kind === 'lparen') {
399
+ const open = j;
400
+ const close = matchClose(tokens, open, 'lparen', 'rparen');
401
+ if (close !== -1) {
402
+ out.calls.push({
403
+ receiver: parts.length > 1 ? parts.slice(0, -1).join('.') : null,
404
+ method: parts[parts.length - 1],
405
+ args: splitArgsByComma(tokens.slice(open + 1, close)).map(makeArgExpr),
406
+ line: tk.line || line,
407
+ fullPath: parts.join('.'),
408
+ });
409
+ // Recurse into args so nested calls inside arguments are also caught.
410
+ _scanNested(tokens.slice(open + 1, close), tk.line || line);
411
+ }
412
+ }
413
+ }
414
+ }
415
+ }
416
+ for (const d of out.decls) if (d.rhsTokens) _scanNested(d.rhsTokens, d.line);
417
+ for (const a of out.assignments) if (a.rhsTokens) _scanNested(a.rhsTokens, a.line);
418
+ method.calls = out.calls;
419
+ method.ctors = out.ctors;
420
+ method.assignments = out.assignments;
421
+ method.decls = out.decls;
422
+ method.strings = out.strings;
423
+ return out;
424
+ }
425
+
426
+ // Build the full IR for a C# source file.
427
+ export function buildCSharpIR(source) {
428
+ const tokens = tokenize(source);
429
+ const ir = {
430
+ tokens,
431
+ usings: [], namespaces: [], classes: [], methods: [],
432
+ calls: [], ctors: [], assignments: [], decls: [], attrs: [], strings: [],
433
+ };
434
+ let pendingAttrs = [];
435
+
436
+ let i = 0;
437
+ while (i < tokens.length) {
438
+ const t = tokens[i];
439
+ if (!t || t.kind === 'eof') break;
440
+
441
+ // using directive
442
+ if (t.kind === 'kw' && t.value === 'using') {
443
+ let j = i + 1;
444
+ while (tokens[j] && tokens[j].kind !== 'semi' && tokens[j].kind !== 'lparen') j++;
445
+ const parts = tokens.slice(i + 1, j).map(t => t.value).filter(Boolean).join('');
446
+ if (parts) ir.usings.push(parts);
447
+ i = j + 1;
448
+ continue;
449
+ }
450
+
451
+ // namespace declaration
452
+ if (t.kind === 'kw' && t.value === 'namespace') {
453
+ let j = i + 1;
454
+ const nameParts = [];
455
+ while (tokens[j] && (tokens[j].kind === 'ident' || tokens[j].kind === 'dot')) {
456
+ nameParts.push(tokens[j].value); j++;
457
+ }
458
+ ir.namespaces.push({ name: nameParts.join(''), line: t.line });
459
+ i = j;
460
+ continue;
461
+ }
462
+
463
+ // Attribute
464
+ if (t.kind === 'attr-open') {
465
+ const a = readAttribute(tokens, i, 'unknown');
466
+ if (a) {
467
+ pendingAttrs.push(a.attr);
468
+ ir.attrs.push(a.attr);
469
+ i = a.next;
470
+ continue;
471
+ }
472
+ }
473
+
474
+ // class / struct / interface / record
475
+ if (t.kind === 'kw' && (t.value === 'class' || t.value === 'struct' || t.value === 'interface' || t.value === 'record')) {
476
+ const nameTok = tokens[i + 1];
477
+ const cls = { name: nameTok && nameTok.kind === 'ident' ? nameTok.value : '<anon>', attrs: pendingAttrs, line: t.line, methods: [], baseTypes: [] };
478
+ pendingAttrs = [];
479
+ // Extract base types: `class X : Foo, Bar<int> { … }`. Walk after the
480
+ // class name to a ':' and then collect comma-separated type tokens
481
+ // until '{'. We don't model `where T : …` constraint clauses here;
482
+ // they're stripped during method-body skipping.
483
+ let nameEnd = i + 2;
484
+ // Skip generic on class
485
+ if (tokens[nameEnd] && tokens[nameEnd].kind === 'op' && tokens[nameEnd].value === '<') {
486
+ let d = 1; let k = nameEnd + 1;
487
+ while (tokens[k] && d > 0) {
488
+ if (tokens[k].kind === 'op' && tokens[k].value === '<') d++;
489
+ if (tokens[k].kind === 'op' && tokens[k].value === '>') d--;
490
+ if (tokens[k].kind === 'op' && tokens[k].value === '>>') d -= 2;
491
+ k++; if (d === 0) break;
492
+ }
493
+ nameEnd = k;
494
+ }
495
+ if (tokens[nameEnd] && tokens[nameEnd].kind === 'op' && tokens[nameEnd].value === ':') {
496
+ let k = nameEnd + 1;
497
+ let curBase = '';
498
+ while (tokens[k] && tokens[k].kind !== 'lbrace' && !(tokens[k].kind === 'kw' && tokens[k].value === 'where')) {
499
+ if (tokens[k].kind === 'comma') { if (curBase.trim()) cls.baseTypes.push(curBase.trim()); curBase = ''; k++; continue; }
500
+ if (tokens[k].kind === 'ident' || tokens[k].kind === 'dot' || tokens[k].kind === 'kw' || tokens[k].kind === 'op') curBase += tokens[k].value;
501
+ k++;
502
+ }
503
+ if (curBase.trim()) cls.baseTypes.push(curBase.trim());
504
+ }
505
+ // Find class body
506
+ let j = i + 2;
507
+ while (tokens[j] && tokens[j].kind !== 'lbrace' && tokens[j].kind !== 'semi') j++;
508
+ if (!tokens[j] || tokens[j].kind !== 'lbrace') { ir.classes.push(cls); i = j + 1; continue; }
509
+ const classClose = matchClose(tokens, j, 'lbrace', 'rbrace');
510
+ cls.endLine = (classClose !== -1) ? tokens[classClose].line : t.line;
511
+ // Walk members
512
+ let k = j + 1;
513
+ let memberAttrs = [];
514
+ while (k < classClose) {
515
+ const tk = tokens[k];
516
+ if (!tk) break;
517
+ if (tk.kind === 'attr-open') {
518
+ const a = readAttribute(tokens, k, 'unknown');
519
+ if (a) { memberAttrs.push(a.attr); ir.attrs.push(a.attr); k = a.next; continue; }
520
+ }
521
+ // Skip nested classes/structs — they get their own pass via the outer loop pattern.
522
+ if (tk.kind === 'kw' && (tk.value === 'class' || tk.value === 'struct' || tk.value === 'interface' || tk.value === 'record')) {
523
+ // Find the nested type's closing brace and skip it.
524
+ let nb = k + 1;
525
+ while (tokens[nb] && tokens[nb].kind !== 'lbrace' && tokens[nb].kind !== 'semi') nb++;
526
+ if (tokens[nb]?.kind === 'lbrace') {
527
+ const ncClose = matchClose(tokens, nb, 'lbrace', 'rbrace');
528
+ k = ncClose === -1 ? classClose : ncClose + 1;
529
+ } else { k = nb + 1; }
530
+ memberAttrs = [];
531
+ continue;
532
+ }
533
+ const mh = readMethodHeader(tokens, k, memberAttrs);
534
+ if (mh) {
535
+ memberAttrs.forEach(a => { a.attachedTo = 'method'; });
536
+ // Walk the method body to fill calls / decls / assignments / strings.
537
+ walkMethodBody(mh.method);
538
+ cls.methods.push(mh.method);
539
+ ir.methods.push(mh.method);
540
+ ir.calls.push(...mh.method.calls);
541
+ ir.ctors.push(...mh.method.ctors);
542
+ ir.assignments.push(...mh.method.assignments);
543
+ ir.decls.push(...mh.method.decls);
544
+ ir.strings.push(...mh.method.strings);
545
+ memberAttrs = [];
546
+ k = mh.next;
547
+ continue;
548
+ }
549
+ // Field declaration at class level: TypeOrVar Name (=expr)?;
550
+ if (isType(tk)) {
551
+ const tr = readType(tokens, k);
552
+ if (tr && tokens[tr.next] && tokens[tr.next].kind === 'ident') {
553
+ let f = tr.next + 1;
554
+ // optional = init
555
+ if (tokens[f] && tokens[f].kind === 'op' && tokens[f].value === '=') {
556
+ let fj = f + 1, depth = 0;
557
+ 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--;
560
+ fj++;
561
+ }
562
+ 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 });
563
+ k = fj + 1;
564
+ memberAttrs = [];
565
+ continue;
566
+ }
567
+ if (tokens[f] && tokens[f].kind === 'semi') {
568
+ ir.decls.push({ name: tokens[tr.next].value, type: tr.type, rhsTokens: null, rhsText: '', line: tk.line, isVar: false, isField: true });
569
+ k = f + 1;
570
+ memberAttrs = [];
571
+ continue;
572
+ }
573
+ // Property: TypeName Name { get; set; } — skip the body.
574
+ if (tokens[f] && tokens[f].kind === 'lbrace') {
575
+ const pClose = matchClose(tokens, f, 'lbrace', 'rbrace');
576
+ ir.decls.push({ name: tokens[tr.next].value, type: tr.type, rhsTokens: null, rhsText: '', line: tk.line, isVar: false, isField: true, isProperty: true });
577
+ k = pClose === -1 ? classClose : pClose + 1;
578
+ memberAttrs = [];
579
+ continue;
580
+ }
581
+ }
582
+ }
583
+ k++;
584
+ }
585
+ ir.classes.push(cls);
586
+ i = classClose === -1 ? tokens.length : classClose + 1;
587
+ pendingAttrs = [];
588
+ continue;
589
+ }
590
+
591
+ i++;
592
+ }
593
+
594
+ // Cross-reference: which class/method does each call live in?
595
+ for (const cls of ir.classes) {
596
+ for (const m of cls.methods) {
597
+ for (const c of m.calls) c.scope = m;
598
+ for (const a of m.assignments) a.scope = m;
599
+ for (const d of m.decls) d.scope = m;
600
+ for (const x of m.ctors) x.scope = m;
601
+ }
602
+ }
603
+ return ir;
604
+ }