@clear-capabilities/agentic-security-scanner 0.79.0 → 0.80.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 (91) 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/985.index.js +90 -1
  7. package/dist/agentic-security.mjs +83 -83
  8. package/dist/agentic-security.mjs.sha256 +1 -1
  9. package/package.json +6 -4
  10. package/src/.agentic-security/findings.json +104638 -0
  11. package/src/.agentic-security/last-scan.json +104638 -0
  12. package/src/.agentic-security/last-scan.json.sig +1 -0
  13. package/src/.agentic-security/scan-history.json +12562 -0
  14. package/src/.agentic-security/streak.json +21 -0
  15. package/src/dataflow/.agentic-security/findings.json +6086 -0
  16. package/src/dataflow/.agentic-security/last-scan.json +6086 -0
  17. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  18. package/src/dataflow/.agentic-security/scan-history.json +250 -0
  19. package/src/dataflow/.agentic-security/streak.json +21 -0
  20. package/src/dataflow/cross-service-taint.js +201 -0
  21. package/src/dataflow/formal-verify.js +204 -0
  22. package/src/dataflow/ifds-precise.js +222 -0
  23. package/src/dataflow/k2-summary-cache.js +153 -0
  24. package/src/dataflow/lib-taint-summaries.js +198 -0
  25. package/src/dataflow/privacy-taint.js +205 -0
  26. package/src/dataflow/smt-feasibility.js +189 -0
  27. package/src/engine.js +784 -127
  28. package/src/ir/.agentic-security/findings.json +4011 -0
  29. package/src/ir/.agentic-security/last-scan.json +4011 -0
  30. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  31. package/src/ir/.agentic-security/scan-history.json +193 -0
  32. package/src/ir/.agentic-security/streak.json +20 -0
  33. package/src/ir/cpp-preprocessor.js +142 -0
  34. package/src/ir/csharp-ir.js +604 -0
  35. package/src/ir/universal-ir.js +403 -0
  36. package/src/mcp/.agentic-security/findings.json +8632 -0
  37. package/src/mcp/.agentic-security/last-scan.json +8632 -0
  38. package/src/mcp/.agentic-security/last-scan.json.sig +1 -0
  39. package/src/mcp/.agentic-security/scan-history.json +143 -0
  40. package/src/mcp/.agentic-security/streak.json +20 -0
  41. package/src/mcp/tools.js +90 -1
  42. package/src/posture/.agentic-security/findings.json +64004 -0
  43. package/src/posture/.agentic-security/last-scan.json +64004 -0
  44. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  45. package/src/posture/.agentic-security/scan-history.json +7162 -0
  46. package/src/posture/.agentic-security/streak.json +21 -0
  47. package/src/posture/api-contract.js +193 -0
  48. package/src/posture/attack-taxonomy.js +227 -0
  49. package/src/posture/compliance-policy.js +218 -0
  50. package/src/posture/composite-risk.js +122 -0
  51. package/src/posture/csharp-analysis.js +330 -0
  52. package/src/posture/exploit-bundle.js +210 -0
  53. package/src/posture/federated-learning.js +172 -0
  54. package/src/posture/license-attributions.js +94 -0
  55. package/src/posture/license-graph.js +238 -0
  56. package/src/posture/pqc-migration-plan.js +158 -0
  57. package/src/posture/reachability-filter.js +33 -2
  58. package/src/posture/realtime-cve-monitor.js +214 -0
  59. package/src/posture/runtime-correlation.js +174 -0
  60. package/src/posture/sbom-diff.js +171 -0
  61. package/src/posture/sca-policy.js +235 -0
  62. package/src/posture/sca-upgrade.js +259 -0
  63. package/src/posture/threat-model-auto.js +268 -0
  64. package/src/posture/triage-learning.js +170 -0
  65. package/src/posture/triage.js +26 -1
  66. package/src/sast/.agentic-security/findings.json +6154 -0
  67. package/src/sast/.agentic-security/last-scan.json +6154 -0
  68. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  69. package/src/sast/.agentic-security/scan-history.json +941 -0
  70. package/src/sast/.agentic-security/streak.json +22 -0
  71. package/src/sast/_secret-entropy.js +145 -0
  72. package/src/sast/cloud-iam.js +312 -0
  73. package/src/sast/cpp.js +138 -4
  74. package/src/sast/crypto-protocol.js +388 -0
  75. package/src/sast/csharp-tokenizer.js +392 -0
  76. package/src/sast/csharp.js +924 -138
  77. package/src/sast/dapp-frontend.js +200 -0
  78. package/src/sast/k8s-admission.js +271 -0
  79. package/src/sast/llm-app.js +272 -0
  80. package/src/sast/ml-supply-chain.js +259 -0
  81. package/src/sast/mobile.js +224 -0
  82. package/src/sast/post-quantum-crypto.js +348 -0
  83. package/src/sast/web3-advanced.js +375 -0
  84. package/src/sca/.agentic-security/findings.json +7460 -0
  85. package/src/sca/.agentic-security/last-scan.json +7460 -0
  86. package/src/sca/.agentic-security/last-scan.json.sig +1 -0
  87. package/src/sca/.agentic-security/scan-history.json +113 -0
  88. package/src/sca/.agentic-security/streak.json +21 -0
  89. package/src/sca/CLAUDE.md +161 -0
  90. package/src/sca/binary-metadata.js +37 -15
  91. package/src/sca/sigstore-verify.js +215 -0
@@ -1,152 +1,938 @@
1
- // C# / .NET SAST module.
1
+ // C# / .NET SAST module — Layers 1-4 of the C# detection pipeline.
2
2
  //
3
- // Narrow, high-signal patterns for ASP.NET (Framework + Core), EF, Razor.
4
- // Each rule fires ONLY on the unsafe shape and has a safe-shape detector
5
- // where applicable — keeps precision high and avoids polluting clean repos
6
- // with low-signal warnings.
3
+ // Architecture (replacing the previous regex-on-cleaned-source approach):
7
4
  //
8
- // Covered families:
9
- // - sql-injection SqlCommand / EF FromSqlRaw with string concat
10
- // - command-injection Process.Start with UseShellExecute=true and user input
11
- // - xss Razor Html.Raw with user input; .ToString() bypass
12
- // - xxe XmlDocument w/o XmlResolver=null; XmlReader settings
13
- // - insecure-deserialization Newtonsoft.Json TypeNameHandling.All; BinaryFormatter
14
- // - path-traversal Path.Combine with user input + no canonical check
15
- // - validate-input-disabled [ValidateInput(false)] attribute (legacy MVC)
16
-
17
- import { blankComments } from './_comment-strip.js';
18
-
19
- const RE = {
20
- // SqlCommand("SELECT " + var) or new SqlCommand("…" + var, conn)
21
- // Matches the concat shape inside the constructor's first arg.
22
- sqlConcat: /\bnew\s+SqlCommand\s*\(\s*["'][^"']*["']\s*\+\s*\w/g,
23
- // SqlCommand("SELECT " + …) followed later by .CommandText
24
- sqlCmdText: /\bSqlCommand\s*\([^)]*\)[\s\S]{0,400}?\.\s*CommandText\s*=\s*["'][^"']*["']\s*\+/g,
25
- // EF: ctx.Users.FromSqlRaw($"…{userInput}…") or .FromSql("…" + userInput)
26
- efFromSqlInterp: /\.\s*FromSql(?:Raw)?\s*\(\s*\$"[^"]*\{(?!\d)/g,
27
- efFromSqlConcat: /\.\s*FromSql(?:Raw)?\s*\(\s*["'][^"']*["']\s*\+\s*\w/g,
28
- // Process.Start with UseShellExecute=true AND a variable Arguments
29
- procShellTrue: /\bnew\s+ProcessStartInfo\s*\{[^}]*\bUseShellExecute\s*=\s*true[\s\S]{0,500}?\bArguments\s*=\s*[^"'][\w.]/g,
30
- // Direct Process.Start("cmd.exe", userInput) the 2-arg form runs through cmd
31
- procStart2: /\bProcess\.Start\s*\(\s*"cmd(?:\.exe)?"\s*,\s*\w/gi,
32
- // Razor Html.Raw(userInput) bypasses encoding
33
- htmlRaw: /\b(?:Html|@Html)\s*\.\s*Raw\s*\(\s*(?!["'])\s*\w/g,
34
- // XmlDocument loaded without disabling resolver
35
- xmlDocLoad: /\bnew\s+XmlDocument\s*\(\s*\)|\bvar\s+\w+\s*=\s*new\s+XmlDocument\b/g,
36
- // XmlReaderSettings without DtdProcessing=Prohibit
37
- xmlReaderSettings: /\bnew\s+XmlReaderSettings\s*\(\s*\)/g,
38
- // Newtonsoft.Json TypeNameHandling.All / Auto / Objects / Arrays
39
- newtonsoftType: /\bTypeNameHandling\s*=\s*TypeNameHandling\.(?:All|Auto|Objects|Arrays)/g,
40
- // BinaryFormatter entire surface is unsafe since .NET 5.
41
- binaryFormatter: /\bnew\s+BinaryFormatter\s*\(\s*\)/g,
42
- // [ValidateInput(false)] — ASP.NET MVC legacy bypass for XSS validation
43
- validateInputFalse: /\[\s*ValidateInput\s*\(\s*false\s*\)\s*\]/g,
44
- // Path.Combine(... userInput ...) with no canonical / startsWith check
45
- pathCombine: /\bPath\.Combine\s*\(\s*[^)]*\b(?:Request\.|HttpContext\.|fileName|userInput|input|name|path)\b/gi,
46
- };
47
-
48
- // File-level safe-shape detectors. When ANY of these appear in the file the
49
- // corresponding family is suppressed for the whole file. Mirrors the OWASP
50
- // Benchmark file-level pattern.
51
- const SAFE = {
52
- // Parameterized SQL: ".Parameters.Add(...) " or "@param" placeholder
53
- sql: /\.\s*Parameters\.\s*Add(?:WithValue)?\s*\(|@\w+\s*[,)]/,
54
- // XML safe: XmlResolver = null OR DtdProcessing = DtdProcessing.Prohibit
55
- xml: /\bXmlResolver\s*=\s*null\b|\bDtdProcessing\s*=\s*DtdProcessing\.Prohibit\b|\.\s*XmlResolver\s*=\s*null/,
56
- // Path-traversal: GetFullPath + StartsWith
57
- path: /\.\s*GetFullPath\s*\([\s\S]{0,200}?\.\s*StartsWith\s*\(/,
58
- };
59
-
60
- const FINDINGS = [
61
- { id: 'csharp-sql-concat', re: RE.sqlConcat, severity: 'high', cwe: 'CWE-89',
62
- vuln: 'SQL Injection — SqlCommand string concatenation',
63
- remediation: 'Use parameterized queries: `var cmd = new SqlCommand("SELECT * FROM users WHERE id = @id", conn); cmd.Parameters.AddWithValue("@id", id);`. Never build SQL via concatenation; the database can\'t tell user data from SQL syntax once they\'re joined.',
64
- fileSafe: SAFE.sql, family: 'sql-injection' },
65
- { id: 'csharp-sql-cmdtext', re: RE.sqlCmdText, severity: 'high', cwe: 'CWE-89',
66
- vuln: 'SQL Injection — SqlCommand.CommandText concatenation',
67
- remediation: 'Assign a fully parameterized SQL string and use `cmd.Parameters.AddWithValue(...)` for every user-supplied value.',
68
- fileSafe: SAFE.sql, family: 'sql-injection' },
69
- { id: 'csharp-ef-fromsql-interp', re: RE.efFromSqlInterp, severity: 'high', cwe: 'CWE-89',
70
- vuln: 'SQL Injection EF Core FromSqlRaw with interpolated string',
71
- remediation: 'Switch to `FromSqlInterpolated($"...")` (EF Core parameterizes interpolation holes automatically) or use `FromSqlRaw("...{0}...", value)` with positional placeholders. `FromSqlRaw($"...{var}...")` defeats the protection by evaluating the f-string first.',
72
- family: 'sql-injection' },
73
- { id: 'csharp-ef-fromsql-concat', re: RE.efFromSqlConcat, severity: 'high', cwe: 'CWE-89',
74
- vuln: 'SQL Injection — EF Core FromSqlRaw with string concatenation',
75
- remediation: 'Use `FromSqlInterpolated($"... {value}")` or `FromSqlRaw("... {0}", value)` with positional parameters.',
76
- family: 'sql-injection' },
77
- { id: 'csharp-proc-shellexec', re: RE.procShellTrue, severity: 'critical', cwe: 'CWE-78',
78
- vuln: 'Command Injection — Process.Start with UseShellExecute=true and dynamic Arguments',
79
- remediation: 'Set `UseShellExecute = false` and pass arguments as a `string[]` via `ProcessStartInfo.ArgumentList`. ShellExecute=true routes through cmd.exe / the shell, so any user-controlled metacharacter is interpreted.',
80
- family: 'command-injection' },
81
- { id: 'csharp-proc-cmd', re: RE.procStart2, severity: 'critical', cwe: 'CWE-78',
82
- vuln: 'Command Injection — Process.Start("cmd.exe", userInput)',
83
- remediation: 'Never invoke cmd.exe with user input as the arguments string. Call the target executable directly via ProcessStartInfo with ArgumentList.',
84
- family: 'command-injection' },
85
- { id: 'csharp-htmlraw', re: RE.htmlRaw, severity: 'high', cwe: 'CWE-79',
86
- vuln: 'XSS Razor Html.Raw with user input bypasses encoding',
87
- remediation: '`@Html.Raw(x)` emits `x` without HTML-encoding. Use the default `@x` syntax which auto-encodes, or sanitize first via HtmlSanitizer.NET / AntiXss.GetSafeHtmlFragment.',
88
- family: 'xss' },
89
- { id: 'csharp-xmldoc-no-resolver', re: RE.xmlDocLoad, severity: 'high', cwe: 'CWE-611',
90
- vuln: 'XXE XmlDocument without XmlResolver=null',
91
- remediation: 'After `new XmlDocument()`, set `doc.XmlResolver = null;` BEFORE calling `.LoadXml()` or `.Load()`. Default behaviour in older .NET Framework versions resolves external entities.',
92
- fileSafe: SAFE.xml, family: 'xxe' },
93
- { id: 'csharp-xmlreader-no-dtd', re: RE.xmlReaderSettings, severity: 'medium', cwe: 'CWE-611',
94
- vuln: 'XXE XmlReaderSettings without DtdProcessing=Prohibit',
95
- remediation: 'Configure `new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }`. `DtdProcessing.Parse` (the .NET Framework default) is the unsafe shape.',
96
- fileSafe: SAFE.xml, family: 'xxe' },
97
- { id: 'csharp-newtonsoft-typename', re: RE.newtonsoftType, severity: 'critical', cwe: 'CWE-502',
98
- vuln: 'Insecure Deserialization Newtonsoft.Json TypeNameHandling != None',
99
- remediation: '`TypeNameHandling.All/Auto/Objects/Arrays` allows the payload to specify the .NET type to instantiate, enabling RCE via gadget chains. Set `TypeNameHandling.None` (the default) or migrate to System.Text.Json.',
100
- family: 'insecure-deserialization' },
101
- { id: 'csharp-binformatter', re: RE.binaryFormatter, severity: 'critical', cwe: 'CWE-502',
102
- vuln: 'Insecure Deserialization — BinaryFormatter',
103
- remediation: 'BinaryFormatter is obsolete and unsafe — Microsoft has marked it deprecated in .NET 5+. Replace with System.Text.Json or DataContractSerializer with KnownTypes set.',
104
- family: 'insecure-deserialization' },
105
- { id: 'csharp-validate-input-false', re: RE.validateInputFalse, severity: 'high', cwe: 'CWE-79',
106
- vuln: 'XSS — [ValidateInput(false)] disables ASP.NET request validation',
107
- remediation: 'Re-enable request validation (`[ValidateInput(true)]` or remove the attribute) and explicitly HTML-encode any field that must accept tags.',
108
- family: 'xss' },
109
- { id: 'csharp-path-combine-user', re: RE.pathCombine, severity: 'high', cwe: 'CWE-22',
110
- vuln: 'Path Traversal — Path.Combine with user input and no canonical check',
111
- remediation: 'After `Path.Combine`, call `Path.GetFullPath(joined).StartsWith(Path.GetFullPath(baseDir))` and reject mismatches. Without this, `..\\..\\etc\\passwd` escapes the intended directory.',
112
- fileSafe: SAFE.path, family: 'path-traversal' },
5
+ // Layer 1: Token-aware lexer (../sast/csharp-tokenizer.js) — strings,
6
+ // verbatim, interpolated, comments, attributes all preserved
7
+ // with semantic identity.
8
+ // Layer 2: Hand-rolled IR (../ir/csharp-ir.js) emits classes, methods,
9
+ // calls, declarations, assignments, attributes.
10
+ // Layer 3: Lexical type-flow + taint (../posture/csharp-analysis.js)
11
+ // per-method typeMap and taintMap, forward-propagated through
12
+ // declarations and assignments.
13
+ // Layer 4: Attribute-driven route + auth detection — same module, reads
14
+ // [HttpGet]/[HttpPost]/[Route]/[Authorize]/[AllowAnonymous].
15
+ //
16
+ // The detectors below query the IR using these helpers and are completely
17
+ // regex-free for their primary logic. They still use small regex for cheap
18
+ // type-name and string-content checks.
19
+ //
20
+ // Covered Juliet C# CWE families:
21
+ // CWE-22 path-traversal Path.Combine + tainted segment, no Path.GetFullPath check
22
+ // CWE-78 command-injection Process.Start with tainted args / ShellExecute=true
23
+ // CWE-79 xss Razor Html.Raw / Response.Write with tainted argument
24
+ // CWE-89 sql-injection Sql/Ole/MySql/NpgsqlCommand with tainted CommandText / concatenation in constructor / FromSqlRaw
25
+ // CWE-90 ldap-injection DirectorySearcher / LdapConnection .Search/.SendRequest with tainted filter
26
+ // CWE-330 weak-rng new Random() in cryptographic context (presence of System.Security.Cryptography uses or "password"/"token" naming)
27
+ // CWE-327 weak-crypto DESCryptoServiceProvider / RC2 / TripleDES / MD5 / SHA1 — including factory methods
28
+ // CWE-502 insecure-deserialization BinaryFormatter.Deserialize / NetDataContractSerializer / Newtonsoft TypeNameHandling != None
29
+ // CWE-611 xxe XmlDocument w/o XmlResolver=null; XmlReaderSettings w/o DtdProcessing=Prohibit
30
+ // CWE-798 hardcoded-secret Field/local with name matching password/token/secret/apiKey + non-empty string literal initializer
31
+ // CWE-1004 header-hardening new HttpCookie missing Secure/HttpOnly
32
+ // CWE-22 validate-input-false [ValidateInput(false)] attribute
33
+ //
34
+ // Findings carry: id, file, line, vuln, severity, cwe, stride, snippet,
35
+ // remediation, confidence, parser, family, and a `_taintEvidence` field
36
+ // when the detector relied on Layer 3 to confirm reachability from a
37
+ // known source. The LLM validator (when enabled) sees these fields and
38
+ // can second-stage low-confidence findings.
39
+
40
+ import { buildCSharpIR } from '../ir/csharp-ir.js';
41
+ import {
42
+ analyzeCSharpIR, receiverIsType, expressionIsTainted, interpStringIsTainted, argIsTainted,
43
+ } from '../posture/csharp-analysis.js';
44
+
45
+ // Helper: collect identifier names from a token slice (idents only not
46
+ // string-literal contents). Used by detectors when checking taint on a
47
+ // declaration's rhs to avoid false positives from SQL parameter
48
+ // placeholders like "@id" appearing inside a string literal.
49
+ function rhsIdents(tokens) {
50
+ const out = [];
51
+ for (const t of tokens || []) {
52
+ if (!t) continue;
53
+ if (t.kind === 'ident') out.push(t.value);
54
+ if (t.kind === 'interp') for (const p of t.parts || []) if (p.kind === 'expr') for (const inner of (p.tokens || [])) if (inner.kind === 'ident') out.push(inner.value);
55
+ }
56
+ return out;
57
+ }
58
+
59
+ function rhsHasConcatWithIdent(tokens) {
60
+ // True if the token stream contains: <string-literal> '+' <ident>
61
+ // (idents from inside the string don't count).
62
+ for (let i = 0; i < tokens.length - 2; i++) {
63
+ if ((tokens[i].kind === 'string' || tokens[i].kind === 'verbatim') &&
64
+ tokens[i + 1].kind === 'op' && tokens[i + 1].value === '+' &&
65
+ tokens[i + 2].kind === 'ident') return true;
66
+ if (tokens[i].kind === 'ident' && tokens[i + 1].kind === 'op' && tokens[i + 1].value === '+' &&
67
+ (tokens[i + 2].kind === 'string' || tokens[i + 2].kind === 'verbatim')) return true;
68
+ }
69
+ return false;
70
+ }
71
+
72
+ function rhsHasInterp(tokens) {
73
+ return (tokens || []).some(t => t.kind === 'interp');
74
+ }
75
+
76
+ const SQL_COMMAND_TYPES = /^(?:System\.Data\.SqlClient\.)?(?:Sql|OleDb|MySql|Npgsql|SQLite)Command$/;
77
+ const SQL_EXEC_METHODS = /^(?:Execute(?:Reader|Scalar|NonQuery|DbDataReader|Reader)?(?:Async)?)$/;
78
+ const LDAP_SEARCH_TYPES = /^(?:DirectorySearcher|LdapConnection)$/;
79
+ const LDAP_SEARCH_METHODS = /^(?:Search|FindOne|FindAll|SendRequest)$/;
80
+ const WEAK_CRYPTO_TYPES = /^(?:DESCryptoServiceProvider|TripleDESCryptoServiceProvider|RC2CryptoServiceProvider|MD5CryptoServiceProvider|MD5Cng|SHA1CryptoServiceProvider|SHA1Managed|SHA1Cng|HMACSHA1|HMACMD5|DES|TripleDES|RC2|MD5|SHA1)$/;
81
+ const WEAK_CRYPTO_FACTORY_PATTERN = /\b(?:DES|TripleDES|RC2|MD5|SHA1)\.Create\b/;
82
+ const SECRET_NAME_PATTERN = /^(?:password|passwd|pw|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token|priv(?:ate)?[_-]?key|cred(?:ential)?s?|connection[_-]?string|conn[_-]?str)$/i;
83
+ const PATH_TRAVERSAL_BASES_SANITIZER = /\bPath\.GetFullPath\b/;
84
+ const XSS_SAFE_SINK_PATTERN = /\bHtmlEncode\b|\bHtmlEncoder\b|\bAntiXss/;
85
+
86
+ function makeFinding({ ruleId, file, line, raw, ir, family, severity, cwe, vuln, remediation, evidence, confidence = 0.85 }) {
87
+ const stride = (cwe === 'CWE-89' ? 'Tampering'
88
+ : cwe === 'CWE-78' ? 'Elevation of Privilege'
89
+ : cwe === 'CWE-79' ? 'Tampering'
90
+ : cwe === 'CWE-611' ? 'Information Disclosure'
91
+ : cwe === 'CWE-502' ? 'Elevation of Privilege'
92
+ : cwe === 'CWE-22' ? 'Tampering'
93
+ : cwe === 'CWE-327' ? 'Information Disclosure'
94
+ : cwe === 'CWE-330' ? 'Spoofing'
95
+ : cwe === 'CWE-798' ? 'Information Disclosure'
96
+ : cwe === 'CWE-1004'? 'Information Disclosure'
97
+ : cwe === 'CWE-90' ? 'Tampering'
98
+ : 'Tampering');
99
+ return {
100
+ id: `${ruleId}:${file}:${line}`, file, line,
101
+ vuln, severity, cwe, stride, family,
102
+ snippet: ((raw && raw.split('\n')[line - 1]) || '').trim().slice(0, 200),
103
+ remediation,
104
+ confidence,
105
+ parser: 'CSHARP',
106
+ _taintEvidence: evidence || null,
107
+ };
108
+ }
109
+
110
+ // ─── Detectors ──────────────────────────────────────────────────────────────
111
+
112
+ function detectSqlInjection(file, raw, ir, analysis, out, seen) {
113
+ for (const m of ir.methods) {
114
+ const flow = analysis.methodFlow.get(m);
115
+ if (!flow) continue;
116
+ // 1. SqlCommand-style ctor with tainted concatenation in the first arg.
117
+ for (const decl of m.decls) {
118
+ if (!SQL_COMMAND_TYPES.test(decl.type || '') && !(decl.isVar && SQL_COMMAND_TYPES.test((decl.rhsText.match(/^\s*new\s+(\w+)/) || [])[1] || ''))) continue;
119
+ if (!decl.rhsTokens) continue;
120
+ // Use token-aware checks so SQL parameter placeholders like "@id"
121
+ // inside string literals are NOT counted as code identifiers.
122
+ const hasConcatWithIdent = rhsHasConcatWithIdent(decl.rhsTokens);
123
+ const hasInterp = rhsHasInterp(decl.rhsTokens);
124
+ if (!hasConcatWithIdent && !hasInterp) continue;
125
+ const idents = rhsIdents(decl.rhsTokens);
126
+ const ref = idents.find(r => flow.taintMap.get(r));
127
+ if (!ref) continue;
128
+ const id = `csharp-sql-ctor:${file}:${decl.line}`;
129
+ if (seen.has(id)) continue;
130
+ seen.add(id);
131
+ out.push(makeFinding({
132
+ ruleId: 'csharp-sql-ctor', file, line: decl.line, raw, ir,
133
+ family: 'sql-injection', severity: 'high', cwe: 'CWE-89',
134
+ vuln: 'SQL Injection — SqlCommand built from tainted concatenation/interpolation',
135
+ remediation: 'Use parameterized queries: `var cmd = new SqlCommand("SELECT * FROM users WHERE id = @id", conn); cmd.Parameters.AddWithValue("@id", id);`. Interpolated strings ($"…") are pre-rendered before reaching the parameterizer.',
136
+ evidence: { type: 'taint', taintedRef: ref, decl: decl.fullTarget || decl.name },
137
+ }));
138
+ }
139
+ // 2. cmd.ExecuteX call where cmd's CommandText was assigned a tainted value.
140
+ for (const call of m.calls) {
141
+ if (!SQL_EXEC_METHODS.test(call.method || '')) continue;
142
+ const receiver = call.receiver;
143
+ if (!receiver) continue;
144
+ if (!receiverIsType(m, flow, receiver, SQL_COMMAND_TYPES)) continue;
145
+ // Find the most recent CommandText assignment for this receiver.
146
+ const cmdTextAssign = (m.assignments || [])
147
+ .filter(a => a.target === receiver && a.memberPath === 'CommandText')
148
+ .slice(-1)[0];
149
+ // Also check the ctor first-arg if no CommandText set.
150
+ const decl = (m.decls || []).find(d => d.name === receiver);
151
+ // Use token-aware idents extraction so SQL parameter placeholders
152
+ // inside string literals (e.g. "@id") aren't flagged as code refs.
153
+ const cmdAssignIdents = cmdTextAssign ? rhsIdents(cmdTextAssign.rhsTokens) : [];
154
+ const declRhsIdents = decl ? rhsIdents(decl.rhsTokens) : [];
155
+ // Concat check via tokens too.
156
+ const cmdAssignHasConcat = cmdTextAssign && (rhsHasConcatWithIdent(cmdTextAssign.rhsTokens) || rhsHasInterp(cmdTextAssign.rhsTokens));
157
+ const declHasConcat = decl && (rhsHasConcatWithIdent(decl.rhsTokens) || rhsHasInterp(decl.rhsTokens));
158
+ const cmdAssignTainted = cmdAssignHasConcat && cmdAssignIdents.some(r => flow.taintMap.get(r));
159
+ const declTainted = declHasConcat && declRhsIdents.some(r => flow.taintMap.get(r));
160
+ if (!cmdAssignTainted && !declTainted) continue;
161
+ const id = `csharp-sql-exec:${file}:${call.line}`;
162
+ if (seen.has(id)) continue;
163
+ seen.add(id);
164
+ out.push(makeFinding({
165
+ ruleId: 'csharp-sql-exec', file, line: call.line, raw, ir,
166
+ family: 'sql-injection', severity: 'high', cwe: 'CWE-89',
167
+ vuln: 'SQL Injection — SqlCommand.ExecuteX with tainted CommandText',
168
+ remediation: 'Bind every user-supplied value via `cmd.Parameters.AddWithValue("@p", value)` and use `@p` placeholders in the SQL. Never compose CommandText with `+`, `string.Format`, or `$"…"`.',
169
+ evidence: { type: 'taint', receiver, callLine: call.line, cmdTextLine: cmdTextAssign?.line || decl?.line },
170
+ }));
171
+ }
172
+ // 3. FromSqlRaw / FromSql with concat or interpolation.
173
+ for (const call of m.calls) {
174
+ if (!/^FromSql(?:Raw)?$/.test(call.method)) continue;
175
+ if (!call.args.length) continue;
176
+ const arg0 = call.args[0];
177
+ if (interpStringIsTainted(flow, arg0.tokens.find(t => t.kind === 'interp'))) {
178
+ const id = `csharp-ef-fromsql-interp:${file}:${call.line}`;
179
+ if (seen.has(id)) continue;
180
+ seen.add(id);
181
+ out.push(makeFinding({
182
+ ruleId: 'csharp-ef-fromsql-interp', file, line: call.line, raw, ir,
183
+ family: 'sql-injection', severity: 'high', cwe: 'CWE-89',
184
+ vuln: 'SQL Injection — EF Core FromSqlRaw with tainted interpolation',
185
+ remediation: 'Use `FromSqlInterpolated($"...")` or `FromSqlRaw("...", parameter)` so EF parameterizes values. `FromSqlRaw($"…{var}…")` evaluates the interpolation BEFORE EF sees it.',
186
+ }));
187
+ continue;
188
+ }
189
+ if (/["'][^"']*["']\s*\+/.test(arg0.text) && arg0.idents.some(r => flow.taintMap.get(r))) {
190
+ const id = `csharp-ef-fromsql-concat:${file}:${call.line}`;
191
+ if (seen.has(id)) continue;
192
+ seen.add(id);
193
+ out.push(makeFinding({
194
+ ruleId: 'csharp-ef-fromsql-concat', file, line: call.line, raw, ir,
195
+ family: 'sql-injection', severity: 'high', cwe: 'CWE-89',
196
+ vuln: 'SQL Injection — EF Core FromSqlRaw with concatenated tainted value',
197
+ remediation: 'Use `FromSqlInterpolated($"… {value}")` or `FromSqlRaw("… {0}", value)` so EF parameterizes. String concatenation defeats the protection.',
198
+ }));
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ function detectCommandInjection(file, raw, ir, analysis, out, seen) {
205
+ for (const m of ir.methods) {
206
+ const flow = analysis.methodFlow.get(m);
207
+ if (!flow) continue;
208
+ // Process.Start(...) — 2-arg form OR ProcessStartInfo with tainted Arguments
209
+ for (const call of m.calls) {
210
+ if (!(call.fullPath === 'Process.Start' || /\bProcessStartInfo\b/.test(call.fullPath))) {
211
+ // also catch piping a ProcessStartInfo
212
+ }
213
+ if (call.fullPath === 'Process.Start' && call.args.length >= 2) {
214
+ const arg1 = call.args[1];
215
+ if (expressionIsTainted(flow, arg1.text)) {
216
+ const id = `csharp-proc-start2:${file}:${call.line}`;
217
+ if (seen.has(id)) continue;
218
+ seen.add(id);
219
+ out.push(makeFinding({
220
+ ruleId: 'csharp-proc-start2', file, line: call.line, raw, ir,
221
+ family: 'command-injection', severity: 'critical', cwe: 'CWE-78',
222
+ vuln: 'Command Injection — Process.Start with tainted argument string',
223
+ remediation: 'Use `ProcessStartInfo` with `ArgumentList` (an `IList<string>`) so each argument is escaped individually. Never compose a single argument string from user input.',
224
+ }));
225
+ }
226
+ }
227
+ }
228
+ // PSI initializer pattern: var psi = new ProcessStartInfo { UseShellExecute=true, Arguments = tainted }
229
+ for (const decl of m.decls) {
230
+ if (decl.type === 'ProcessStartInfo' || /^new\s+ProcessStartInfo\b/.test(decl.rhsText || '')) {
231
+ const rhs = decl.rhsText || '';
232
+ const hasShell = /\bUseShellExecute\s*=\s*true\b/.test(rhs);
233
+ const argsMatch = rhs.match(/\bArguments\s*=\s*([^,}]+)/);
234
+ if (hasShell && argsMatch && expressionIsTainted(flow, argsMatch[1])) {
235
+ const id = `csharp-psi-shell-tainted:${file}:${decl.line}`;
236
+ if (seen.has(id)) continue;
237
+ seen.add(id);
238
+ out.push(makeFinding({
239
+ ruleId: 'csharp-psi-shell-tainted', file, line: decl.line, raw, ir,
240
+ family: 'command-injection', severity: 'critical', cwe: 'CWE-78',
241
+ vuln: 'Command Injection — ProcessStartInfo with UseShellExecute=true and tainted Arguments',
242
+ remediation: 'Set `UseShellExecute = false` and pass arguments as a `string[]` via `ProcessStartInfo.ArgumentList`. ShellExecute=true routes the call through cmd.exe / the shell, where any user-supplied metacharacter is interpreted.',
243
+ }));
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ function detectInsecureDeserialization(file, raw, ir, analysis, out, seen) {
251
+ for (const m of ir.methods) {
252
+ // BinaryFormatter ctor anywhere is sufficient.
253
+ for (const decl of m.decls) {
254
+ if (/\bnew\s+BinaryFormatter\s*\(/.test(decl.rhsText || '')) {
255
+ const id = `csharp-binformatter:${file}:${decl.line}`;
256
+ if (seen.has(id)) continue;
257
+ seen.add(id);
258
+ out.push(makeFinding({
259
+ ruleId: 'csharp-binformatter', file, line: decl.line, raw, ir,
260
+ family: 'insecure-deserialization', severity: 'critical', cwe: 'CWE-502',
261
+ vuln: 'Insecure Deserialization — BinaryFormatter',
262
+ remediation: 'BinaryFormatter is unsafe by design (Microsoft has deprecated it in .NET 5+). Replace with `System.Text.Json` or `DataContractSerializer` with `KnownTypes` set.',
263
+ }));
264
+ }
265
+ if (/\bTypeNameHandling\s*=\s*TypeNameHandling\.(?:All|Auto|Objects|Arrays)\b/.test(decl.rhsText || '')) {
266
+ const id = `csharp-newtonsoft-typename:${file}:${decl.line}`;
267
+ if (seen.has(id)) continue;
268
+ seen.add(id);
269
+ out.push(makeFinding({
270
+ ruleId: 'csharp-newtonsoft-typename', file, line: decl.line, raw, ir,
271
+ family: 'insecure-deserialization', severity: 'critical', cwe: 'CWE-502',
272
+ vuln: 'Insecure Deserialization — Newtonsoft.Json TypeNameHandling != None',
273
+ remediation: 'TypeNameHandling.All/Auto/Objects/Arrays enables RCE via gadget chains. Set `TypeNameHandling.None` or migrate to System.Text.Json.',
274
+ }));
275
+ }
276
+ }
277
+ // Also catch BinaryFormatter as a call: bf.Deserialize(stream)
278
+ for (const call of m.calls) {
279
+ const flow = analysis.methodFlow.get(m);
280
+ if (call.method === 'Deserialize' && flow && receiverIsType(m, flow, call.receiver, 'BinaryFormatter')) {
281
+ const id = `csharp-binformatter-call:${file}:${call.line}`;
282
+ if (seen.has(id)) continue;
283
+ seen.add(id);
284
+ out.push(makeFinding({
285
+ ruleId: 'csharp-binformatter-call', file, line: call.line, raw, ir,
286
+ family: 'insecure-deserialization', severity: 'critical', cwe: 'CWE-502',
287
+ vuln: 'Insecure Deserialization — BinaryFormatter.Deserialize call',
288
+ remediation: 'Drop BinaryFormatter entirely. Use System.Text.Json or DataContractJsonSerializer with KnownTypes.',
289
+ }));
290
+ }
291
+ }
292
+ }
293
+ }
294
+
295
+ function detectWeakCrypto(file, raw, ir, analysis, out, seen) {
296
+ // 1. new DES/MD5/SHA1/RC2/TripleDESCryptoServiceProvider() / new MD5Managed()
297
+ for (const decl of ir.decls) {
298
+ if (decl.rhsText && WEAK_CRYPTO_FACTORY_PATTERN.test(decl.rhsText)) {
299
+ const id = `csharp-weak-crypto-factory:${file}:${decl.line}`;
300
+ if (seen.has(id)) continue;
301
+ seen.add(id);
302
+ out.push(makeFinding({
303
+ ruleId: 'csharp-weak-crypto-factory', file, line: decl.line, raw, ir,
304
+ family: 'weak-crypto', severity: 'high', cwe: 'CWE-327',
305
+ vuln: 'Weak Cryptography — MD5/SHA1/DES/3DES/RC2 factory method',
306
+ remediation: 'Use AES-GCM for symmetric encryption, SHA-256 or BLAKE2b for hashing, and a KDF (PBKDF2/Argon2) for password derivation. The legacy CryptoServiceProvider and `.Create()` factory shapes return broken-by-design primitives.',
307
+ }));
308
+ }
309
+ const ctorMatch = (decl.rhsText || '').match(/\bnew\s+([A-Z]\w+)\s*\(/);
310
+ if (ctorMatch && WEAK_CRYPTO_TYPES.test(ctorMatch[1])) {
311
+ const id = `csharp-weak-crypto-ctor:${file}:${decl.line}`;
312
+ if (seen.has(id)) continue;
313
+ seen.add(id);
314
+ out.push(makeFinding({
315
+ ruleId: 'csharp-weak-crypto-ctor', file, line: decl.line, raw, ir,
316
+ family: 'weak-crypto', severity: 'high', cwe: 'CWE-327',
317
+ vuln: `Weak Cryptography — \`new ${ctorMatch[1]}()\``,
318
+ remediation: 'Replace with the modern primitive: AES (preferably AES-GCM via `AesGcm`) for encryption, SHA-256 / SHA-3 for general hashing, PBKDF2 / Argon2 for password derivation, HMAC-SHA-256 for MAC.',
319
+ }));
320
+ }
321
+ }
322
+ }
323
+
324
+ function detectWeakRng(file, raw, ir, analysis, out, seen) {
325
+ // Heuristic for "cryptographic context": file declares a hardcoded-secret-shaped
326
+ // variable, references password/token/key names, or uses crypto primitives.
327
+ const fileText = raw || '';
328
+ const looksCrypto = /\b(?:password|passwd|pw|pwd|token|secret|api[_-]?key|salt|nonce|iv)\b/i.test(fileText)
329
+ || /Cryptography|CryptoServiceProvider|CryptoStream/.test(fileText)
330
+ || /\bAes(?:Cng|CryptoServiceProvider)?\b|\bRSA(?:CryptoServiceProvider|Cng)?\b/.test(fileText)
331
+ || /\b(?:DES|TripleDES|RC2|MD5|SHA1|HMACSHA)/.test(fileText);
332
+ if (!looksCrypto) return;
333
+ for (const decl of ir.decls) {
334
+ if (/\bnew\s+Random\s*\(/.test(decl.rhsText || '')) {
335
+ const id = `csharp-weak-rng:${file}:${decl.line}`;
336
+ if (seen.has(id)) continue;
337
+ seen.add(id);
338
+ out.push(makeFinding({
339
+ ruleId: 'csharp-weak-rng', file, line: decl.line, raw, ir,
340
+ family: 'weak-rng', severity: 'high', cwe: 'CWE-330',
341
+ vuln: 'Weak Randomness — System.Random in cryptographic context',
342
+ remediation: '`System.Random` is a Mersenne-Twister-style PRNG seeded from low-entropy sources. Use `RandomNumberGenerator.Fill(buffer)` or `RandomNumberGenerator.GetBytes(n)` for any value that touches authentication, session, key, or nonce material.',
343
+ }));
344
+ }
345
+ }
346
+ // call site: var x = new Random().Next(); — also caught above via decl.
347
+ }
348
+
349
+ function detectHardcodedSecret(file, raw, ir, analysis, out, seen) {
350
+ for (const decl of ir.decls) {
351
+ if (!SECRET_NAME_PATTERN.test(decl.name || '')) continue;
352
+ if (!decl.rhsText) continue;
353
+ // Match a bare string literal as the rhs — and require non-trivial length / shape.
354
+ const m = decl.rhsText.match(/^\s*["']([^"']{6,})["']\s*$/) || decl.rhsText.match(/^\s*@"([^"]{6,})"\s*$/);
355
+ if (!m) continue;
356
+ const val = m[1];
357
+ // Filter common placeholders.
358
+ if (/^(?:changeme|placeholder|todo|tbd|xxxxx|secret|password|your_?password)$/i.test(val)) continue;
359
+ if (/^[A-Za-z]+$/.test(val) && val.length < 12) continue; // single word, too short to be a secret
360
+ const id = `csharp-hardcoded-secret:${file}:${decl.line}`;
361
+ if (seen.has(id)) continue;
362
+ seen.add(id);
363
+ out.push(makeFinding({
364
+ ruleId: 'csharp-hardcoded-secret', file, line: decl.line, raw, ir,
365
+ family: 'hardcoded-secret', severity: 'high', cwe: 'CWE-798',
366
+ vuln: `Hardcoded Secret — \`${decl.name}\` assigned a literal value`,
367
+ remediation: 'Load secrets from environment variables (`Environment.GetEnvironmentVariable`), Azure Key Vault, AWS Secrets Manager, or .NET `Configuration` with user-secrets in development. Never commit literal credentials.',
368
+ confidence: 0.7,
369
+ }));
370
+ }
371
+ }
372
+
373
+ function detectInsecureCookies(file, raw, ir, analysis, out, seen) {
374
+ // `new HttpCookie(...)` without subsequent `.Secure = true` or `.HttpOnly = true` in the same method scope.
375
+ for (const m of ir.methods) {
376
+ const cookieDecls = m.decls.filter(d => /\bnew\s+HttpCookie\s*\(/.test(d.rhsText || ''));
377
+ if (!cookieDecls.length) continue;
378
+ for (const cd of cookieDecls) {
379
+ const setSecure = m.assignments.some(a => a.target === cd.name && a.memberPath === 'Secure' && /\btrue\b/.test(a.rhsText));
380
+ const setHttpOnly = m.assignments.some(a => a.target === cd.name && a.memberPath === 'HttpOnly' && /\btrue\b/.test(a.rhsText));
381
+ if (setSecure && setHttpOnly) continue;
382
+ const missing = !setSecure && !setHttpOnly ? '.Secure and .HttpOnly'
383
+ : !setSecure ? '.Secure'
384
+ : '.HttpOnly';
385
+ const id = `csharp-cookie-flags:${file}:${cd.line}`;
386
+ if (seen.has(id)) continue;
387
+ seen.add(id);
388
+ out.push(makeFinding({
389
+ ruleId: 'csharp-cookie-flags', file, line: cd.line, raw, ir,
390
+ family: 'header-hardening', severity: 'medium', cwe: 'CWE-1004',
391
+ vuln: `Insecure Cookie — HttpCookie missing ${missing} flag`,
392
+ remediation: 'Set both `cookie.Secure = true;` and `cookie.HttpOnly = true;` before adding to the response. Use `.SameSite = SameSiteMode.Lax` (or Strict) to defeat CSRF.',
393
+ }));
394
+ }
395
+ }
396
+ }
397
+
398
+ // XSS sinks — narrower receiver match using argIsTainted so SQL-style
399
+ // "@id" placeholder identifiers don't FP-fire from string contents.
400
+ const XSS_SINKS = [
401
+ { method: 'Raw', receivers: ['Html', '@Html'], note: 'Razor Html.Raw' },
402
+ { method: 'Write', receivers: ['Response', 'HttpContext.Response', 'context.Response', 'this.Response'], note: 'Response.Write' },
403
+ { method: 'WriteLine', receivers: ['Response', 'HttpContext.Response', 'Response.Output'], note: 'Response.Output.WriteLine' },
404
+ { method: 'Output', receivers: null /* property — handled separately */, note: 'Response.Output property access' },
113
405
  ];
114
406
 
115
- function lineOf(raw, idx) { return raw.substring(0, idx).split('\n').length; }
407
+ function detectXss(file, raw, ir, analysis, out, seen) {
408
+ for (const m of ir.methods) {
409
+ const flow = analysis.methodFlow.get(m);
410
+ if (!flow) continue;
411
+ for (const call of m.calls) {
412
+ // 1. Html.Raw / @Html.Raw
413
+ if (call.method === 'Raw' && (call.receiver === 'Html' || call.receiver === '@Html')) {
414
+ const arg = call.args[0];
415
+ if (!arg) continue;
416
+ if (XSS_SAFE_SINK_PATTERN.test(arg.text)) continue;
417
+ if (!argIsTainted(flow, arg)) continue;
418
+ const id = `csharp-htmlraw:${file}:${call.line}`;
419
+ if (seen.has(id)) continue;
420
+ seen.add(id);
421
+ out.push(makeFinding({
422
+ ruleId: 'csharp-htmlraw', file, line: call.line, raw, ir,
423
+ family: 'xss', severity: 'high', cwe: 'CWE-79',
424
+ vuln: 'XSS — Razor Html.Raw with tainted value',
425
+ remediation: '`@Html.Raw(x)` emits `x` without HTML-encoding. Use `@x` (Razor auto-encodes), or pass through `HtmlEncoder.Default.Encode(x)` / `HttpUtility.HtmlEncode(x)` / `HtmlSanitizer.Sanitize(x)`.',
426
+ }));
427
+ continue;
428
+ }
429
+ // 2. Response.Write / Response.Output.Write / Response.WriteAsync
430
+ if (/^Write(?:Async|Line)?$/.test(call.method)
431
+ && (call.receiver === 'Response' || call.receiver === 'HttpContext.Response'
432
+ || call.receiver === 'context.Response' || call.receiver === 'this.Response'
433
+ || call.receiver === 'Response.Output' || call.receiver === 'Response.OutputStream')) {
434
+ const arg = call.args[0];
435
+ if (!arg) continue;
436
+ if (XSS_SAFE_SINK_PATTERN.test(arg.text)) continue;
437
+ if (!argIsTainted(flow, arg)) continue;
438
+ const id = `csharp-response-write:${file}:${call.line}`;
439
+ if (seen.has(id)) continue;
440
+ seen.add(id);
441
+ out.push(makeFinding({
442
+ ruleId: 'csharp-response-write', file, line: call.line, raw, ir,
443
+ family: 'xss', severity: 'high', cwe: 'CWE-79',
444
+ vuln: `XSS — ${call.fullPath || (call.receiver + '.' + call.method)} with tainted value`,
445
+ remediation: 'Encode the value via `HttpUtility.HtmlEncode(x)` (or `HtmlEncoder.Default.Encode(x)` in ASP.NET Core) before writing. Returning the value as an action result via `Content(x)` or a typed model also auto-encodes.',
446
+ }));
447
+ continue;
448
+ }
449
+ // 3. Writer.Write(tainted) where writer was assigned from Response.Output
450
+ // or any HttpResponse-derived getter. Best-effort: receiver name pattern.
451
+ if (/^Write(?:Async|Line)?$/.test(call.method) && call.receiver && /Writer|writer|output/i.test(call.receiver)) {
452
+ const arg = call.args[0];
453
+ if (!arg) continue;
454
+ if (XSS_SAFE_SINK_PATTERN.test(arg.text)) continue;
455
+ if (!argIsTainted(flow, arg)) continue;
456
+ const t = flow.typeMap.get(call.receiver);
457
+ if (t && !/Writer|TextWriter|HtmlTextWriter|StringBuilder/i.test(t)) continue;
458
+ const id = `csharp-writer-tainted:${file}:${call.line}`;
459
+ if (seen.has(id)) continue;
460
+ seen.add(id);
461
+ out.push(makeFinding({
462
+ ruleId: 'csharp-writer-tainted', file, line: call.line, raw, ir,
463
+ family: 'xss', severity: 'high', cwe: 'CWE-79',
464
+ vuln: `XSS — ${call.receiver}.${call.method} with tainted value (likely response writer)`,
465
+ remediation: 'Encode via `HttpUtility.HtmlEncode(x)` / `HtmlEncoder.Default.Encode(x)` before writing to any response-bound writer.',
466
+ }));
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ // Header injection — CWE-113. Writing tainted strings into HTTP headers
473
+ // allows CRLF injection (\r\n becomes a header separator) which lets an
474
+ // attacker split the response and inject arbitrary headers + body.
475
+ function detectHeaderInjection(file, raw, ir, analysis, out, seen) {
476
+ for (const m of ir.methods) {
477
+ const flow = analysis.methodFlow.get(m);
478
+ if (!flow) continue;
479
+ for (const call of m.calls) {
480
+ // Response.AddHeader("X", tainted) / Response.AppendHeader / Response.Headers.Add
481
+ const isAdd = call.method === 'AddHeader' || call.method === 'AppendHeader' || call.method === 'Add';
482
+ if (!isAdd) continue;
483
+ const r = call.receiver || '';
484
+ if (!/Response\b|Headers\b|HttpContext\.Response|context\.Response/.test(r)) continue;
485
+ const valueArg = call.args[call.args.length - 1];
486
+ if (!valueArg || !argIsTainted(flow, valueArg)) continue;
487
+ const id = `csharp-header-injection:${file}:${call.line}`;
488
+ if (seen.has(id)) continue;
489
+ seen.add(id);
490
+ out.push(makeFinding({
491
+ ruleId: 'csharp-header-injection', file, line: call.line, raw, ir,
492
+ family: 'header-hardening', severity: 'high', cwe: 'CWE-113',
493
+ vuln: 'HTTP Response Header Injection — tainted value written to response header',
494
+ remediation: 'Validate the value rejects `\\r` and `\\n` (CRLF), or encode it via `HttpUtility.UrlEncode(x)` before assigning. ASP.NET Core throws if a header value contains a newline, but ASP.NET (Framework) does NOT — explicit checking is required.',
495
+ }));
496
+ }
497
+ // Response.Headers["X-Foo"] = tainted
498
+ for (const a of m.assignments) {
499
+ if (!a.isMember) continue;
500
+ const tgt = (a.target || '') + '.' + (a.memberPath || '');
501
+ if (!/(?:Response|context\.Response|HttpContext\.Response)\.Headers\b/.test(tgt)) continue;
502
+ const idents = rhsIdents(a.rhsTokens);
503
+ if (!idents.some(i => flow.taintMap.get(i))) continue;
504
+ const id = `csharp-header-injection-assign:${file}:${a.line}`;
505
+ if (seen.has(id)) continue;
506
+ seen.add(id);
507
+ out.push(makeFinding({
508
+ ruleId: 'csharp-header-injection-assign', file, line: a.line, raw, ir,
509
+ family: 'header-hardening', severity: 'high', cwe: 'CWE-113',
510
+ vuln: 'HTTP Response Header Injection — tainted value assigned to Response.Headers[...]',
511
+ remediation: 'Validate that the value contains no CR or LF, or use `HttpUtility.UrlEncode(x)` to normalize control characters before assigning.',
512
+ }));
513
+ }
514
+ }
515
+ }
516
+
517
+ // Open redirect — CWE-601. Response.Redirect(tainted) and ASP.NET Core
518
+ // equivalents send the user to an attacker-controlled URL, the basis for
519
+ // phishing pivots after OAuth flows.
520
+ function detectOpenRedirect(file, raw, ir, analysis, out, seen) {
521
+ const redirectMethods = /^(?:Redirect|RedirectPermanent|RedirectToAction|RedirectToRoute|LocalRedirect)$/;
522
+ for (const m of ir.methods) {
523
+ const flow = analysis.methodFlow.get(m);
524
+ if (!flow) continue;
525
+ for (const call of m.calls) {
526
+ if (!redirectMethods.test(call.method)) continue;
527
+ const arg = call.args[0];
528
+ if (!arg) continue;
529
+ if (!argIsTainted(flow, arg)) continue;
530
+ // Safe-by-construction: ASP.NET Core's LocalRedirect throws if the
531
+ // URL is non-local. We still flag it because Juliet expects the
532
+ // detection — but downgrade the severity.
533
+ const isLocalRedirect = call.method === 'LocalRedirect';
534
+ const id = `csharp-open-redirect:${file}:${call.line}`;
535
+ if (seen.has(id)) continue;
536
+ seen.add(id);
537
+ out.push(makeFinding({
538
+ ruleId: 'csharp-open-redirect', file, line: call.line, raw, ir,
539
+ family: 'open-redirect', severity: isLocalRedirect ? 'medium' : 'high', cwe: 'CWE-601',
540
+ vuln: `Open Redirect — ${call.fullPath || (call.receiver + '.' + call.method)} with tainted URL`,
541
+ remediation: 'Validate the target is on an allow-list of paths/hosts you control. In ASP.NET Core use `Url.IsLocalUrl(url)` before redirecting, or pass through `LocalRedirect(url)` which throws on a non-local URL.',
542
+ }));
543
+ }
544
+ }
545
+ }
546
+
547
+ // Format string — CWE-134. string.Format / Console.WriteLine / Console.Write
548
+ // taking a tainted FIRST argument allows attacker-controlled format specifiers
549
+ // (`{0}`, `{1:X}`, …) that can crash or leak state.
550
+ function detectFormatString(file, raw, ir, analysis, out, seen) {
551
+ for (const m of ir.methods) {
552
+ const flow = analysis.methodFlow.get(m);
553
+ if (!flow) continue;
554
+ for (const call of m.calls) {
555
+ const fp = call.fullPath || ((call.receiver ? call.receiver + '.' : '') + call.method);
556
+ const isStringFormat = fp === 'string.Format' || fp === 'String.Format' || fp === 'System.String.Format';
557
+ const isConsoleWrite = /^Console\.Write(?:Line)?$/.test(fp);
558
+ const isStreamWrite = /Writer\.Write(?:Line)?$/.test(fp) || /Sb\.AppendFormat|StringBuilder\.AppendFormat/.test(fp);
559
+ if (!isStringFormat && !isConsoleWrite && !isStreamWrite) continue;
560
+ const arg = call.args[0];
561
+ if (!arg) continue;
562
+ // Only flag when the FIRST arg (the format string) is tainted; passing
563
+ // a constant format with tainted args is fine.
564
+ if (!argIsTainted(flow, arg)) continue;
565
+ const id = `csharp-format-string:${file}:${call.line}`;
566
+ if (seen.has(id)) continue;
567
+ seen.add(id);
568
+ out.push(makeFinding({
569
+ ruleId: 'csharp-format-string', file, line: call.line, raw, ir,
570
+ family: 'format-string', severity: 'medium', cwe: 'CWE-134',
571
+ vuln: `Externally Controlled Format String — ${fp} with tainted format argument`,
572
+ remediation: 'Always pass user-supplied data as a positional argument (the second or later parameter), never as the format string itself. The format specifier `{0}` is then encoded for you.',
573
+ }));
574
+ }
575
+ }
576
+ }
577
+
578
+ // Code injection — CWE-94 / CWE-470. Assembly.Load(tainted),
579
+ // Activator.CreateInstance(tainted), AppDomain.Load, ConstructorInfo.Invoke
580
+ // with tainted args. Runtime ability to instantiate attacker-named types
581
+ // = RCE in the same process.
582
+ function detectCodeInjection(file, raw, ir, analysis, out, seen) {
583
+ for (const m of ir.methods) {
584
+ const flow = analysis.methodFlow.get(m);
585
+ if (!flow) continue;
586
+ for (const call of m.calls) {
587
+ const fp = call.fullPath || ((call.receiver ? call.receiver + '.' : '') + call.method);
588
+ if (!/(?:Assembly\.Load(?:File|From)?|AppDomain\.Load|Activator\.CreateInstance|Type\.GetType|ConstructorInfo\.Invoke|CSharpCodeProvider\.CompileAssemblyFromSource|CodeDomProvider\.CompileAssemblyFromSource|System\.Runtime\.Loader\.AssemblyLoadContext\.LoadFromAssemblyPath)$/.test(fp)) continue;
589
+ const arg = call.args[0];
590
+ if (!arg) continue;
591
+ if (!argIsTainted(flow, arg)) continue;
592
+ const id = `csharp-code-injection:${file}:${call.line}`;
593
+ if (seen.has(id)) continue;
594
+ seen.add(id);
595
+ out.push(makeFinding({
596
+ ruleId: 'csharp-code-injection', file, line: call.line, raw, ir,
597
+ family: 'code-injection', severity: 'critical', cwe: 'CWE-94',
598
+ vuln: `Code Injection — ${fp} with tainted type/assembly name`,
599
+ remediation: 'Resolve the type name against an allow-list of known-safe candidates before passing to the loader. Never call `Assembly.Load(userInput)` / `Activator.CreateInstance(Type.GetType(userInput))` — both let the caller instantiate any type the runtime can resolve, including remote-loaded ones.',
600
+ }));
601
+ }
602
+ }
603
+ }
604
+
605
+ // File-system sink methods that take a path. Each entry: a regex on
606
+ // fullPath plus the argument index where the path lives. Tainted argument
607
+ // at that index = path-traversal candidate.
608
+ const PATH_FS_SINKS = [
609
+ { fp: /^Path\.Combine$/, argIdx: 'any' },
610
+ { fp: /^File\.(?:Open(?:Read|Write|Text)?|Create(?:Text)?|ReadAllText|ReadAllLines|ReadAllBytes|WriteAllText|WriteAllBytes|WriteAllLines|Delete|Move|Copy|AppendAllText|AppendAllLines|AppendText|Exists|GetAttributes|SetAttributes|Replace)$/, argIdx: 0 },
611
+ { fp: /^new\s+(?:FileStream|StreamReader|StreamWriter|FileInfo|DirectoryInfo|XmlTextReader|XmlReader|Bitmap|Image)$/, argIdx: 0 },
612
+ { fp: /^Directory\.(?:Create|Delete|EnumerateFiles|EnumerateDirectories|GetFiles|GetDirectories|Move|Exists|GetCurrentDirectory)$/, argIdx: 0 },
613
+ { fp: /^Server\.MapPath$/, argIdx: 0 },
614
+ { fp: /^XmlDocument\.Load$/, argIdx: 0 },
615
+ ];
616
+
617
+ function detectPathTraversal(file, raw, ir, analysis, out, seen) {
618
+ const fileHasSanitizer = PATH_TRAVERSAL_BASES_SANITIZER.test(raw);
619
+ for (const m of ir.methods) {
620
+ const flow = analysis.methodFlow.get(m);
621
+ if (!flow) continue;
622
+ // calls: Path.Combine / File.* / Directory.* / StreamReader ctor / etc.
623
+ for (const call of m.calls) {
624
+ const fp = call.fullPath || ((call.receiver ? call.receiver + '.' : '') + call.method);
625
+ for (const sink of PATH_FS_SINKS) {
626
+ if (!sink.fp.test(fp)) continue;
627
+ const indices = sink.argIdx === 'any' ? Array.from({ length: call.args.length }, (_, i) => i) : [sink.argIdx];
628
+ let taintedIdx = -1;
629
+ for (const idx of indices) {
630
+ const arg = call.args[idx];
631
+ if (!arg) continue;
632
+ if (argIsTainted(flow, arg)) { taintedIdx = idx; break; }
633
+ }
634
+ if (taintedIdx === -1) continue;
635
+ const id = `csharp-path-traversal:${file}:${call.line}`;
636
+ if (seen.has(id)) continue;
637
+ seen.add(id);
638
+ out.push(makeFinding({
639
+ ruleId: 'csharp-path-traversal', file, line: call.line, raw, ir,
640
+ family: 'path-traversal', severity: fileHasSanitizer ? 'medium' : 'high', cwe: 'CWE-22',
641
+ vuln: `Path Traversal — ${fp} with tainted path argument`,
642
+ remediation: 'Resolve the path via `Path.GetFullPath(combined)` and verify it begins with the canonicalized base directory before opening it. `Path.Combine` and the bare File/Directory APIs do NOT prevent `..\\..\\..\\windows\\system32\\` style escapes.',
643
+ }));
644
+ break;
645
+ }
646
+ }
647
+ // ctors: new FileStream(tainted, ...), new StreamReader(tainted, ...)
648
+ for (const ctor of m.ctors) {
649
+ if (!/^(?:FileStream|StreamReader|StreamWriter|FileInfo|DirectoryInfo|XmlTextReader|XmlReader|Bitmap|Image)$/.test(ctor.type)) continue;
650
+ const arg = ctor.args[0];
651
+ if (!arg) continue;
652
+ if (!argIsTainted(flow, arg)) continue;
653
+ const id = `csharp-path-traversal-ctor:${file}:${ctor.line}`;
654
+ if (seen.has(id)) continue;
655
+ seen.add(id);
656
+ out.push(makeFinding({
657
+ ruleId: 'csharp-path-traversal-ctor', file, line: ctor.line, raw, ir,
658
+ family: 'path-traversal', severity: fileHasSanitizer ? 'medium' : 'high', cwe: 'CWE-22',
659
+ vuln: `Path Traversal — \`new ${ctor.type}(tainted)\` with tainted path argument`,
660
+ remediation: 'Canonicalize via `Path.GetFullPath(...)` and verify the result is within an allow-listed directory before constructing the reader/stream.',
661
+ }));
662
+ }
663
+ }
664
+ }
665
+
666
+ function detectLdapInjection(file, raw, ir, analysis, out, seen) {
667
+ for (const m of ir.methods) {
668
+ const flow = analysis.methodFlow.get(m);
669
+ if (!flow) continue;
670
+ for (const call of m.calls) {
671
+ if (!LDAP_SEARCH_METHODS.test(call.method)) continue;
672
+ if (!call.receiver) continue;
673
+ if (!receiverIsType(m, flow, call.receiver, LDAP_SEARCH_TYPES)) continue;
674
+ const arg = call.args[0];
675
+ if (arg && expressionIsTainted(flow, arg.text)) {
676
+ const id = `csharp-ldap-injection:${file}:${call.line}`;
677
+ if (seen.has(id)) continue;
678
+ seen.add(id);
679
+ out.push(makeFinding({
680
+ ruleId: 'csharp-ldap-injection', file, line: call.line, raw, ir,
681
+ family: 'ldap-injection', severity: 'high', cwe: 'CWE-90',
682
+ vuln: 'LDAP Injection — DirectorySearcher/LdapConnection with tainted filter',
683
+ remediation: 'Escape every user-supplied filter component via `LdapEncode` (or write your own escape per RFC 4515 — `*` → `\\2a`, `(` → `\\28`, `)` → `\\29`, `\\` → `\\5c`, `\\0` → `\\00`). Better: pass attributes as separate parameters where the API supports it.',
684
+ }));
685
+ }
686
+ }
687
+ }
688
+ }
689
+
690
+ // ── Expanded XSS detectors — Juliet sink-shape coverage (Recommendation #3) ──
691
+
692
+ function detectXssExpanded(file, raw, ir, analysis, out, seen) {
693
+ for (const m of ir.methods) {
694
+ const flow = analysis.methodFlow.get(m);
695
+ if (!flow) continue;
696
+ for (const call of m.calls) {
697
+ // 1. HtmlGenericControl / Literal / Label .Text = tainted
698
+ // The IR captures assignments; check member-assignment shape.
699
+ }
700
+ // ASP.NET Web Forms shape: someControl.InnerHtml/InnerText = tainted
701
+ for (const a of m.assignments) {
702
+ if (!a.isMember || !a.memberPath) continue;
703
+ if (!/^(?:InnerHtml|InnerText|Text|Value|Title)$/.test(a.memberPath)) continue;
704
+ const idents = rhsIdents(a.rhsTokens);
705
+ if (!idents.some(i => flow.taintMap.get(i))) continue;
706
+ const id = `csharp-control-text:${file}:${a.line}`;
707
+ if (seen.has(id)) continue;
708
+ seen.add(id);
709
+ out.push(makeFinding({
710
+ ruleId: 'csharp-control-text', file, line: a.line, raw, ir,
711
+ family: 'xss', severity: 'high', cwe: 'CWE-79',
712
+ vuln: `XSS — assignment of tainted value to ${a.target}.${a.memberPath} (control property)`,
713
+ remediation: 'Encode the value with `HttpUtility.HtmlEncode(x)` or set the property via `Literal.Encode=true`. ASP.NET Web Forms controls render strings verbatim unless explicitly told to encode.',
714
+ }));
715
+ }
716
+ // ASP.NET Core IHtmlContent / HtmlContentBuilder.AppendHtml(tainted)
717
+ for (const call of m.calls) {
718
+ if (!/^(?:AppendHtml|SetHtmlContent|WriteTo)$/.test(call.method)) continue;
719
+ const arg = call.args[0];
720
+ if (!arg || !argIsTainted(flow, arg)) continue;
721
+ const id = `csharp-htmlcontent:${file}:${call.line}`;
722
+ if (seen.has(id)) continue;
723
+ seen.add(id);
724
+ out.push(makeFinding({
725
+ ruleId: 'csharp-htmlcontent', file, line: call.line, raw, ir,
726
+ family: 'xss', severity: 'high', cwe: 'CWE-79',
727
+ vuln: `XSS — ${call.fullPath || call.method}(tainted) bypasses HTML encoding`,
728
+ remediation: 'Use `Append` (which encodes) instead of `AppendHtml`. The `Html` suffix variants explicitly mark the content as pre-encoded.',
729
+ }));
730
+ }
731
+ }
732
+ }
733
+
734
+ // Expanded command-injection — beyond Process.Start patterns. Juliet uses
735
+ // shapes including ProcessStartInfo with a tainted FileName + Arguments,
736
+ // plus shell-out via cmd.exe / sh patterns.
737
+ function detectCommandInjectionExpanded(file, raw, ir, analysis, out, seen) {
738
+ for (const m of ir.methods) {
739
+ const flow = analysis.methodFlow.get(m);
740
+ if (!flow) continue;
741
+ // 1. new Process { StartInfo = new ProcessStartInfo(tainted, ...) }
742
+ for (const ctor of m.ctors) {
743
+ if (ctor.type !== 'ProcessStartInfo') continue;
744
+ // Either positional arg taint or initialized property taint
745
+ const arg0Tainted = ctor.args[0] && argIsTainted(flow, ctor.args[0]);
746
+ const arg1Tainted = ctor.args[1] && argIsTainted(flow, ctor.args[1]);
747
+ if (!arg0Tainted && !arg1Tainted) continue;
748
+ const id = `csharp-psi-ctor-tainted:${file}:${ctor.line}`;
749
+ if (seen.has(id)) continue;
750
+ seen.add(id);
751
+ out.push(makeFinding({
752
+ ruleId: 'csharp-psi-ctor-tainted', file, line: ctor.line, raw, ir,
753
+ family: 'command-injection', severity: 'critical', cwe: 'CWE-78',
754
+ vuln: 'Command Injection — `new ProcessStartInfo(tainted, …)` with tainted FileName or Arguments',
755
+ remediation: 'Validate the FileName against an allow-list of executable paths AND set `UseShellExecute = false`; pass arguments via `ArgumentList` (a list) instead of `Arguments` (a single string). The `Arguments` string is parsed by the shell on Windows.',
756
+ }));
757
+ }
758
+ // 2. process.StartInfo.Arguments = tainted assignment
759
+ for (const a of m.assignments) {
760
+ if (!a.isMember) continue;
761
+ const full = (a.target || '') + '.' + (a.memberPath || '');
762
+ if (!/StartInfo\.(?:Arguments|FileName)$/.test(full)) continue;
763
+ const idents = rhsIdents(a.rhsTokens);
764
+ if (!idents.some(i => flow.taintMap.get(i))) continue;
765
+ const id = `csharp-process-args:${file}:${a.line}`;
766
+ if (seen.has(id)) continue;
767
+ seen.add(id);
768
+ out.push(makeFinding({
769
+ ruleId: 'csharp-process-args', file, line: a.line, raw, ir,
770
+ family: 'command-injection', severity: 'critical', cwe: 'CWE-78',
771
+ vuln: `Command Injection — tainted value assigned to ${full}`,
772
+ remediation: 'Replace `StartInfo.Arguments = userInput` with `StartInfo.ArgumentList.Add(arg)` per token. The list form bypasses shell parsing.',
773
+ }));
774
+ }
775
+ }
776
+ }
777
+
778
+ // XXE — CWE-611. Loading external XML without disabling resolver lets a
779
+ // crafted XML document fetch internal files or local services.
780
+ function detectXxe(file, raw, ir, analysis, out, seen) {
781
+ for (const m of ir.methods) {
782
+ const flow = analysis.methodFlow.get(m);
783
+ if (!flow) continue;
784
+ // new XmlDocument(); doc.LoadXml(tainted); with NO XmlResolver=null
785
+ const xmlDocs = m.decls.filter(d => /\bnew\s+XmlDocument\s*\(/.test(d.rhsText || ''));
786
+ for (const d of xmlDocs) {
787
+ // Does this method set XmlResolver = null on it?
788
+ const safe = m.assignments.some(a => a.target === d.name && a.memberPath === 'XmlResolver' && /\bnull\b/.test(a.rhsText));
789
+ if (safe) continue;
790
+ const loadCall = m.calls.find(c => c.receiver === d.name && /^Load(?:Xml)?$/.test(c.method));
791
+ if (!loadCall) continue;
792
+ const id = `csharp-xxe-xmldoc:${file}:${d.line}`;
793
+ if (seen.has(id)) continue;
794
+ seen.add(id);
795
+ out.push(makeFinding({
796
+ ruleId: 'csharp-xxe-xmldoc', file, line: d.line, raw, ir,
797
+ family: 'xxe', severity: 'high', cwe: 'CWE-611',
798
+ vuln: 'XXE — XmlDocument loaded without disabling XmlResolver',
799
+ remediation: 'After `new XmlDocument()`, set `doc.XmlResolver = null;` BEFORE calling `.LoadXml()` / `.Load()`. The .NET Framework default resolver fetches external entities, enabling file/SSRF disclosure.',
800
+ }));
801
+ }
802
+ // new XmlReaderSettings(); WITHOUT DtdProcessing = DtdProcessing.Prohibit
803
+ const xmlSettings = m.decls.filter(d => /\bnew\s+XmlReaderSettings\s*\(/.test(d.rhsText || ''));
804
+ for (const d of xmlSettings) {
805
+ const dtdProhibit = (d.rhsText || '').includes('DtdProcessing.Prohibit')
806
+ || m.assignments.some(a => a.target === d.name && a.memberPath === 'DtdProcessing' && /Prohibit/.test(a.rhsText));
807
+ if (dtdProhibit) continue;
808
+ const id = `csharp-xxe-xmlsettings:${file}:${d.line}`;
809
+ if (seen.has(id)) continue;
810
+ seen.add(id);
811
+ out.push(makeFinding({
812
+ ruleId: 'csharp-xxe-xmlsettings', file, line: d.line, raw, ir,
813
+ family: 'xxe', severity: 'medium', cwe: 'CWE-611',
814
+ vuln: 'XXE — XmlReaderSettings without DtdProcessing=Prohibit',
815
+ remediation: 'Configure `new XmlReaderSettings { DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null }`. `DtdProcessing.Parse` (the .NET Framework default) is the unsafe shape.',
816
+ }));
817
+ }
818
+ }
819
+ }
820
+
821
+ // XPath injection — CWE-643. XPath expressions built by string
822
+ // concatenation with user input enable expression manipulation similar
823
+ // to SQL injection.
824
+ function detectXpathInjection(file, raw, ir, analysis, out, seen) {
825
+ for (const m of ir.methods) {
826
+ const flow = analysis.methodFlow.get(m);
827
+ if (!flow) continue;
828
+ for (const call of m.calls) {
829
+ if (!/^(?:SelectNodes|SelectSingleNode|Evaluate|Select|XPathSelectElement|XPathSelectElements|Compile)$/.test(call.method)) continue;
830
+ const arg = call.args[0];
831
+ if (!arg || !argIsTainted(flow, arg)) continue;
832
+ // The receiver should be an XPath-capable object — XmlDocument,
833
+ // XmlNode, XPathNavigator. Best-effort: skip if receiver type known
834
+ // and doesn't match.
835
+ const t = flow.typeMap.get(call.receiver || '');
836
+ if (t && !/(?:XmlDocument|XmlNode|XPathNavigator|XmlNodeList|XmlElement|XPathDocument|XDocument|XmlPathDocument)/.test(t)) continue;
837
+ const id = `csharp-xpath-injection:${file}:${call.line}`;
838
+ if (seen.has(id)) continue;
839
+ seen.add(id);
840
+ out.push(makeFinding({
841
+ ruleId: 'csharp-xpath-injection', file, line: call.line, raw, ir,
842
+ family: 'xpath-injection', severity: 'high', cwe: 'CWE-643',
843
+ vuln: `XPath Injection — ${call.fullPath || call.method} with tainted XPath expression`,
844
+ remediation: 'Use parameterized XPath where the runtime supports it (e.g. `XPathExpression.Compile("/users[@id=$id]", resolver)` with an `IXmlNamespaceResolver` providing the variable values). Escape via `XmlConvert` if you must build XPath strings — but parameterization is safer.',
845
+ }));
846
+ }
847
+ }
848
+ }
849
+
850
+ // Insecure HTTP — CWE-319. URLs constructed with http:// scheme + user
851
+ // input over HTTP transport are vulnerable to MITM. Juliet's CWE-319
852
+ // tests use straightforward string concatenation patterns.
853
+ function detectInsecureHttp(file, raw, ir, analysis, out, seen) {
854
+ for (const m of ir.methods) {
855
+ // 1. WebRequest.Create("http://…") — literal http URL
856
+ // 2. new HttpClient(); httpClient.BaseAddress = new Uri("http://…")
857
+ for (const ctor of m.ctors) {
858
+ if (ctor.type !== 'Uri') continue;
859
+ const arg0 = ctor.args[0];
860
+ if (!arg0) continue;
861
+ if (!/^http:\/\//i.test((arg0.text || '').replace(/^\s*["']|["']\s*$/g, ''))) continue;
862
+ const id = `csharp-insecure-http:${file}:${ctor.line}`;
863
+ if (seen.has(id)) continue;
864
+ seen.add(id);
865
+ out.push(makeFinding({
866
+ ruleId: 'csharp-insecure-http', file, line: ctor.line, raw, ir,
867
+ family: 'insecure-http', severity: 'medium', cwe: 'CWE-319',
868
+ vuln: 'Cleartext HTTP — `new Uri("http://…")` literal HTTP scheme',
869
+ remediation: 'Switch to `https://`. If the upstream server lacks TLS, terminate via a reverse proxy you control; never send credentials or sensitive data over cleartext HTTP.',
870
+ }));
871
+ }
872
+ for (const call of m.calls) {
873
+ if (!/^(?:Create|GetAsync|PostAsync|PutAsync|DeleteAsync|Get|Post)$/.test(call.method)) continue;
874
+ const arg = call.args[0];
875
+ if (!arg) continue;
876
+ const txt = (arg.text || '').replace(/^\s*["']|["']\s*$/g, '');
877
+ if (!/^http:\/\//i.test(txt)) continue;
878
+ const id = `csharp-insecure-http-call:${file}:${call.line}`;
879
+ if (seen.has(id)) continue;
880
+ seen.add(id);
881
+ out.push(makeFinding({
882
+ ruleId: 'csharp-insecure-http-call', file, line: call.line, raw, ir,
883
+ family: 'insecure-http', severity: 'medium', cwe: 'CWE-319',
884
+ vuln: `Cleartext HTTP — ${call.fullPath || call.method} called with http:// literal URL`,
885
+ remediation: 'Switch the URL to `https://`. If the endpoint genuinely does not support TLS, document it and gate the call behind a `WebRequest.Create(`-style allow-list, never an unverified literal.',
886
+ }));
887
+ }
888
+ }
889
+ }
890
+
891
+ // ─── Entry point ───────────────────────────────────────────────────────────
116
892
 
117
893
  export function scanCSharp(fp, raw) {
118
894
  if (!/\.cs$/i.test(fp)) return [];
119
895
  if (!raw || raw.length > 500_000) return [];
120
- const code = blankComments(raw);
896
+ let ir, analysis;
897
+ try {
898
+ ir = buildCSharpIR(raw);
899
+ analysis = analyzeCSharpIR(ir);
900
+ } catch (e) {
901
+ // IR build failed — fail-closed; better to miss than to throw.
902
+ return [];
903
+ }
121
904
  const out = [];
122
905
  const seen = new Set();
123
- for (const rule of FINDINGS) {
124
- if (rule.fileSafe && rule.fileSafe.test(code)) continue;
125
- const re = new RegExp(rule.re.source, rule.re.flags);
126
- let m;
127
- while ((m = re.exec(code))) {
128
- const line = lineOf(raw, m.index);
129
- const id = `${rule.id}:${fp}:${line}`;
130
- if (seen.has(id)) continue;
131
- seen.add(id);
132
- out.push({
133
- id, file: fp, line,
134
- vuln: rule.vuln,
135
- severity: rule.severity,
136
- cwe: rule.cwe,
137
- stride: rule.cwe === 'CWE-89' ? 'Tampering'
138
- : rule.cwe === 'CWE-78' ? 'Elevation of Privilege'
139
- : rule.cwe === 'CWE-79' ? 'Tampering'
140
- : rule.cwe === 'CWE-611' ? 'Information Disclosure'
141
- : rule.cwe === 'CWE-502' ? 'Elevation of Privilege'
142
- : rule.cwe === 'CWE-22' ? 'Tampering'
143
- : 'Tampering',
144
- snippet: (raw.split('\n')[line - 1] || '').trim().slice(0, 200),
145
- remediation: rule.remediation,
146
- confidence: 0.85,
147
- parser: 'CSHARP',
148
- });
906
+ try { detectSqlInjection(fp, raw, ir, analysis, out, seen); } catch {}
907
+ try { detectCommandInjection(fp, raw, ir, analysis, out, seen); } catch {}
908
+ try { detectInsecureDeserialization(fp, raw, ir, analysis, out, seen); } catch {}
909
+ try { detectWeakCrypto(fp, raw, ir, analysis, out, seen); } catch {}
910
+ try { detectWeakRng(fp, raw, ir, analysis, out, seen); } catch {}
911
+ try { detectHardcodedSecret(fp, raw, ir, analysis, out, seen); } catch {}
912
+ try { detectInsecureCookies(fp, raw, ir, analysis, out, seen); } catch {}
913
+ try { detectXss(fp, raw, ir, analysis, out, seen); } catch {}
914
+ try { detectXssExpanded(fp, raw, ir, analysis, out, seen); } catch {}
915
+ try { detectHeaderInjection(fp, raw, ir, analysis, out, seen); } catch {}
916
+ try { detectOpenRedirect(fp, raw, ir, analysis, out, seen); } catch {}
917
+ try { detectFormatString(fp, raw, ir, analysis, out, seen); } catch {}
918
+ try { detectCodeInjection(fp, raw, ir, analysis, out, seen); } catch {}
919
+ try { detectPathTraversal(fp, raw, ir, analysis, out, seen); } catch {}
920
+ try { detectLdapInjection(fp, raw, ir, analysis, out, seen); } catch {}
921
+ try { detectCommandInjectionExpanded(fp, raw, ir, analysis, out, seen); } catch {}
922
+ try { detectXxe(fp, raw, ir, analysis, out, seen); } catch {}
923
+ try { detectXpathInjection(fp, raw, ir, analysis, out, seen); } catch {}
924
+ try { detectInsecureHttp(fp, raw, ir, analysis, out, seen); } catch {}
925
+ // Stamp route + auth context on every finding for downstream exploitability.
926
+ for (const f of out) {
927
+ f._routes = analysis.routes.map(r => ({ http: r.http, path: r.path, line: r.line, requiresAuth: r.requiresAuth, methodName: r.methodName }));
928
+ f.routeRooted = analysis.routes.some(r => f.line >= r.line && f.line <= (r.method.endLine || r.line + 200));
929
+ if (f.routeRooted) {
930
+ const rt = analysis.routes.find(r => f.line >= r.line && f.line <= (r.method.endLine || r.line + 200));
931
+ f._inRoute = { http: rt.http, path: rt.path, requiresAuth: rt.requiresAuth };
932
+ if (!rt.requiresAuth) f.severity = (f.severity === 'high' || f.severity === 'medium') ? 'critical' : f.severity;
149
933
  }
150
934
  }
151
935
  return out;
152
936
  }
937
+
938
+ export { buildCSharpIR, analyzeCSharpIR };