@kevinrabun/judges 3.20.14 → 3.21.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 (54) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/ast/taint-tracker.d.ts +3 -1
  3. package/dist/ast/taint-tracker.d.ts.map +1 -1
  4. package/dist/ast/taint-tracker.js +523 -12
  5. package/dist/ast/taint-tracker.js.map +1 -1
  6. package/dist/evaluators/authentication.d.ts +2 -2
  7. package/dist/evaluators/authentication.d.ts.map +1 -1
  8. package/dist/evaluators/authentication.js +26 -2
  9. package/dist/evaluators/authentication.js.map +1 -1
  10. package/dist/evaluators/cybersecurity.d.ts +2 -2
  11. package/dist/evaluators/cybersecurity.d.ts.map +1 -1
  12. package/dist/evaluators/cybersecurity.js +58 -5
  13. package/dist/evaluators/cybersecurity.js.map +1 -1
  14. package/dist/evaluators/framework-safety.d.ts.map +1 -1
  15. package/dist/evaluators/framework-safety.js +855 -365
  16. package/dist/evaluators/framework-safety.js.map +1 -1
  17. package/dist/evaluators/index.d.ts.map +1 -1
  18. package/dist/evaluators/index.js +5 -1
  19. package/dist/evaluators/index.js.map +1 -1
  20. package/dist/evaluators/performance.d.ts +2 -2
  21. package/dist/evaluators/performance.d.ts.map +1 -1
  22. package/dist/evaluators/performance.js +33 -4
  23. package/dist/evaluators/performance.js.map +1 -1
  24. package/dist/evaluators/project.d.ts.map +1 -1
  25. package/dist/evaluators/project.js +200 -0
  26. package/dist/evaluators/project.js.map +1 -1
  27. package/dist/evaluators/shared.d.ts +31 -3
  28. package/dist/evaluators/shared.d.ts.map +1 -1
  29. package/dist/evaluators/shared.js +145 -11
  30. package/dist/evaluators/shared.js.map +1 -1
  31. package/dist/language-patterns.d.ts +136 -0
  32. package/dist/language-patterns.d.ts.map +1 -1
  33. package/dist/language-patterns.js +155 -1
  34. package/dist/language-patterns.js.map +1 -1
  35. package/dist/patches/index.d.ts.map +1 -1
  36. package/dist/patches/index.js +210 -0
  37. package/dist/patches/index.js.map +1 -1
  38. package/dist/tools/register-fix.d.ts +6 -0
  39. package/dist/tools/register-fix.d.ts.map +1 -0
  40. package/dist/tools/register-fix.js +153 -0
  41. package/dist/tools/register-fix.js.map +1 -0
  42. package/dist/tools/register-workspace.d.ts +3 -0
  43. package/dist/tools/register-workspace.d.ts.map +1 -0
  44. package/dist/tools/register-workspace.js +215 -0
  45. package/dist/tools/register-workspace.js.map +1 -0
  46. package/dist/tools/register.d.ts +1 -1
  47. package/dist/tools/register.d.ts.map +1 -1
  48. package/dist/tools/register.js +5 -1
  49. package/dist/tools/register.js.map +1 -1
  50. package/dist/tools/schemas.d.ts +2 -2
  51. package/dist/types.d.ts +18 -2
  52. package/dist/types.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/server.json +18 -2
package/CHANGELOG.md CHANGED
@@ -2,6 +2,41 @@
2
2
 
3
3
  All notable changes to **@kevinrabun/judges** are documented here.
4
4
 
5
+ ## [3.21.0] — 2026-03-05
6
+
7
+ ### Added — P0: GitHub Action CI/CD
8
+ - **PR inline review comments** — New `pr-review` input in `action.yml` posts findings as inline PR review comments with severity badges, auto-fix hints, and judge attribution
9
+ - **Diff-only mode** — New `diff-only` input restricts analysis to changed files using `git diff`, dramatically reducing CI noise on large repos
10
+ - **Baseline filtering** — New `baseline-file` input suppresses known findings via a baseline JSON, surfacing only new issues in PRs
11
+ - **Improved step summary** — GitHub Actions summary now includes findings table, score badge, and must-fix gate status
12
+
13
+ ### Added — P1: Core Engine Enhancements
14
+ - **AST context in more evaluators** — `AnalyzeContext` interface pipes tree-sitter AST data into cybersecurity (scope-aware taint), performance (async/complexity detection), and authentication (decorator/import awareness) evaluators
15
+ - **`fix_code` MCP tool** — New tool evaluates code and auto-applies all available patches, returning fixed code + summary of remaining findings
16
+ - **Multi-language framework evaluators** — Extended `framework-safety.ts` from JS/TS-only to 8 frameworks: Django (6 rules), Flask (4), FastAPI (1), Spring Boot (6), ASP.NET Core (6), Go/Gin/Echo/Fiber (5)
17
+
18
+ ### Added — P2: Depth & Tooling
19
+ - **20+ new auto-fix patches** — Added patches for Python (7), Go (2), Java (5), C# (4), Rust (2) covering SQL injection, command injection, weak hashing, empty catch, and more
20
+ - **VS Code findings panel** — TreeView-based panel with sort-by-severity/judge, filter controls, go-to-line navigation, and 7 new commands (`judges.showFindingsPanel`, `judges.sortBySeverity`, etc.)
21
+ - **Cross-file type/state tracking** — Three new project-level detectors: `detectSharedMutableState()`, `detectTypeSafetyGaps()`, `detectScatteredEnvAccess()` in `project.ts`
22
+ - **Taint tracker language depth** — Expanded from 5 to 9 language-specific pattern sets with `LanguagePatternSet` interface; each set defines sources, sinks, sanitizers, assign patterns, and guard conditions
23
+
24
+ ### Added — P3: Breadth & Polish
25
+ - **PHP/Ruby/Kotlin/Swift language support** — Added 4 new languages to `LangFamily`, expanded all ~35 pattern constants in `language-patterns.ts`, added 4 complete taint tracker pattern sets (PHP: 7 sources/11 sinks/11 sanitizers, Ruby: 9/11/10, Kotlin: 9/8/8, Swift: 8/9/6)
26
+ - **Performance & snapshot tests** — 3 new test suites: performance budgets (tribunal <5s, per-judge <500ms, evaluateDiff <3s, large-block <15s), rule coverage stability (≥30 judges, 100-600 findings, required families, severity distribution), multi-language pattern coverage (8 tests for PHP/Ruby/Kotlin/Swift)
27
+ - **Framework version awareness** — `detectFrameworkVersions()` extracts versions from 14 manifest/config patterns; `getVersionConfidenceAdjustment()` applies version-specific confidence rules for Django 4+, Spring 3+, Next.js 13+/14+, Express 5+, Rails 6+/7+, Laravel 9+, ASP.NET 8+; integrated into `applyFrameworkAwareness()`
28
+ - **MCP workspace & streaming tools** — 3 new MCP tools: `list_files` (recursive directory listing with skip-dirs), `read_file` (content reading with line-range slicing), `evaluate_with_progress` (progressive judge-by-judge reporting with count updates)
29
+
30
+ ### Changed
31
+ - **MCP tool count** — 10 → 13 tools registered in `server.json`
32
+ - **`applyFrameworkAwareness()` rewritten** — Now combines framework mitigation with version-aware confidence adjustments and stacked provenance notes
33
+ - **`register.ts` modular architecture** — Now orchestrates 4 registration modules: evaluation, workflow, fix, workspace
34
+
35
+ ### Tests
36
+ - 19 new performance/snapshot/multi-language tests in `judges.test.ts`
37
+ - 19 new framework version awareness tests in `subsystems.test.ts`
38
+ - 1006 tests in judges.test.ts, 392 tests in subsystems.test.ts — all passing
39
+
5
40
  ## [3.20.14] — 2026-03-04
6
41
 
7
42
  ### Added
@@ -29,7 +29,9 @@ export type TaintSinkKind = "code-execution" | "command-exec" | "sql-query" | "x
29
29
  * dangerous sinks through variable assignments and string concatenation.
30
30
  *
31
31
  * For JS/TS, uses the TypeScript compiler AST for precise variable tracking.
32
- * For other languages, falls back to regex-based lightweight analysis.
32
+ * For Python, Java, Go, C#, and Rust: uses language-specific source/sink/
33
+ * sanitizer patterns for deeper analysis.
34
+ * For other languages, falls back to generic regex-based analysis.
33
35
  */
34
36
  export declare function analyzeTaintFlows(code: string, language: string): TaintFlow[];
35
37
  //# sourceMappingURL=taint-tracker.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"taint-tracker.d.ts","sourceRoot":"","sources":["../../src/ast/taint-tracker.ts"],"names":[],"mappings":"AAmBA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,0CAA0C;IAC1C,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,eAAe,CAAC;KACvB,CAAC;IACF,kDAAkD;IAClD,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,aAAa,CAAC;KACrB,CAAC;IACF,qDAAqD;IACrD,aAAa,EAAE,KAAK,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,eAAe,GACvB,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,WAAW,GACX,eAAe,CAAC;AAEpB,MAAM,MAAM,aAAa,GACrB,gBAAgB,GAChB,cAAc,GACd,WAAW,GACX,KAAK,GACL,gBAAgB,GAChB,UAAU,GACV,UAAU,GACV,iBAAiB,CAAC;AAoPtB;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,CAU7E"}
1
+ {"version":3,"file":"taint-tracker.d.ts","sourceRoot":"","sources":["../../src/ast/taint-tracker.ts"],"names":[],"mappings":"AAmBA;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,0CAA0C;IAC1C,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,eAAe,CAAC;KACvB,CAAC;IACF,kDAAkD;IAClD,IAAI,EAAE;QACJ,IAAI,EAAE,MAAM,CAAC;QACb,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,aAAa,CAAC;KACrB,CAAC;IACF,qDAAqD;IACrD,aAAa,EAAE,KAAK,CAAC;QACnB,IAAI,EAAE,MAAM,CAAC;QACb,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC,CAAC;IACH,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,MAAM,eAAe,GACvB,YAAY,GACZ,YAAY,GACZ,aAAa,GACb,WAAW,GACX,eAAe,CAAC;AAEpB,MAAM,MAAM,aAAa,GACrB,gBAAgB,GAChB,cAAc,GACd,WAAW,GACX,KAAK,GACL,gBAAgB,GAChB,UAAU,GACV,UAAU,GACV,iBAAiB,CAAC;AAguBtB;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,CAY7E"}
@@ -208,13 +208,480 @@ function getFnName(node) {
208
208
  }
209
209
  return undefined;
210
210
  }
211
+ const PYTHON_PATTERNS = {
212
+ sources: [
213
+ {
214
+ pattern: /\brequest\.(?:form|args|json|data|values|files|cookies|headers)\b(?:\[|\.get\s*\()/i,
215
+ kind: "http-param",
216
+ },
217
+ { pattern: /\brequest\.GET\b(?:\[|\.get\s*\()/i, kind: "http-param" },
218
+ { pattern: /\brequest\.POST\b(?:\[|\.get\s*\()/i, kind: "http-param" },
219
+ { pattern: /\brequest\.(?:query_params|query_string)\b/i, kind: "http-param" },
220
+ { pattern: /\bflask\.request\.\w+/i, kind: "http-param" },
221
+ { pattern: /\binput\s*\(/i, kind: "user-input" },
222
+ { pattern: /\bsys\.stdin\b/i, kind: "user-input" },
223
+ { pattern: /\bos\.environ\b(?:\[|\.get\s*\()/i, kind: "environment" },
224
+ { pattern: /\burlparse\s*\(|parse_qs\s*\(/i, kind: "url-param" },
225
+ { pattern: /\bopen\s*\(.*\)\.read/i, kind: "external-data" },
226
+ { pattern: /\brequests\.(?:get|post|put|delete)\s*\(/i, kind: "external-data" },
227
+ { pattern: /\bjson\.loads?\s*\(/i, kind: "external-data" },
228
+ ],
229
+ sinks: [
230
+ { pattern: /\bexec\s*\(/i, kind: "code-execution" },
231
+ { pattern: /\beval\s*\(/i, kind: "code-execution" },
232
+ { pattern: /\bcompile\s*\(.*\).*\bexec\b/i, kind: "code-execution" },
233
+ { pattern: /\bos\.system\s*\(/i, kind: "command-exec" },
234
+ { pattern: /\bos\.popen\s*\(/i, kind: "command-exec" },
235
+ {
236
+ pattern: /\bsubprocess\.(?:Popen|run|call|check_output|check_call|getoutput|getstatusoutput)\s*\(/i,
237
+ kind: "command-exec",
238
+ },
239
+ { pattern: /\bcursor\.execute\s*\(/i, kind: "sql-query" },
240
+ { pattern: /\b(?:connection|conn|db)\.execute\s*\(/i, kind: "sql-query" },
241
+ { pattern: /\braw\s*\(\s*["'`]?\s*(?:SELECT|INSERT|UPDATE|DELETE)\b/i, kind: "sql-query" },
242
+ { pattern: /\.(?:extra|raw)\s*\(/i, kind: "sql-query" },
243
+ { pattern: /\brender_template_string\s*\(/i, kind: "template" },
244
+ { pattern: /\bTemplate\s*\(.*\)\.render\s*\(/i, kind: "template" },
245
+ { pattern: /\bJinja2\.\w*\.from_string\s*\(/i, kind: "template" },
246
+ { pattern: /\bmarkup\s*\(.*\+/i, kind: "xss" },
247
+ { pattern: /\bopen\s*\(.*user|path|file|name/i, kind: "path-traversal" },
248
+ { pattern: /\bredirect\s*\(/i, kind: "redirect" },
249
+ { pattern: /\bpickle\.loads?\s*\(/i, kind: "deserialization" },
250
+ { pattern: /\byaml\.(?:load|unsafe_load)\s*\(/i, kind: "deserialization" },
251
+ { pattern: /\bmarshal\.loads?\s*\(/i, kind: "deserialization" },
252
+ ],
253
+ sanitizers: [
254
+ /\bbleach\.clean\s*\(/i,
255
+ /\bmarkup_safe\b/i,
256
+ /\bescape\s*\(/i,
257
+ /\bMarkup\b/i,
258
+ /\bquote\s*\(/i,
259
+ /\bshlex\.quote\s*\(/i,
260
+ /\bshellescape\s*\(/i,
261
+ /\bsanitize\w*\s*\(/i,
262
+ /\bvalidator\.\w+\s*\(/i,
263
+ /\bpydantic\b/i,
264
+ /\b%s\b.*\bexecute\s*\(/i, // parameterized query
265
+ /\bparamstyle\b/i,
266
+ /\bsqlalchemy\.text\s*\(/i,
267
+ ],
268
+ assignPattern: /^\s*(\w+)\s*(?::\s*\w[\w[\], |]*\s*)?=\s*(.+)/,
269
+ guards: [
270
+ /if[ \t]+(?:not[ \t]+)?isinstance\s*\(/i,
271
+ /if[ \t]+(?:not[ \t]+)?\w+\s*(?:is|==|!=)\s*None/i,
272
+ /raise[ \t]+(?:ValueError|TypeError|ValidationError)/i,
273
+ /assert[ \t]+isinstance\s*\(/i,
274
+ /\.validate\s*\(|\.is_valid\s*\(/i,
275
+ ],
276
+ };
277
+ const JAVA_PATTERNS = {
278
+ sources: [
279
+ { pattern: /\b(?:request|req|httpRequest)\.getParameter\s*\(/i, kind: "http-param" },
280
+ { pattern: /\brequest\.getAttribute\s*\(/i, kind: "http-param" },
281
+ { pattern: /\brequest\.getHeader\s*\(/i, kind: "http-param" },
282
+ { pattern: /\brequest\.getQueryString\s*\(/i, kind: "http-param" },
283
+ { pattern: /\brequest\.getInputStream\s*\(/i, kind: "http-param" },
284
+ { pattern: /\brequest\.getReader\s*\(/i, kind: "http-param" },
285
+ { pattern: /\brequest\.getCookies\s*\(/i, kind: "http-param" },
286
+ { pattern: /\b@RequestParam\b/i, kind: "http-param" },
287
+ { pattern: /\b@PathVariable\b/i, kind: "url-param" },
288
+ { pattern: /\b@RequestBody\b/i, kind: "http-param" },
289
+ { pattern: /\b@RequestHeader\b/i, kind: "http-param" },
290
+ { pattern: /\bSystem\.getenv\s*\(/i, kind: "environment" },
291
+ { pattern: /\bScanner\s*\(\s*System\.in\b/i, kind: "user-input" },
292
+ { pattern: /\bBufferedReader\b.*\bInputStreamReader\b.*\bSystem\.in\b/i, kind: "user-input" },
293
+ { pattern: /\bargs\[/i, kind: "user-input" },
294
+ { pattern: /\bnew\s+ObjectMapper\b.*\.read/i, kind: "external-data" },
295
+ { pattern: /\bURL\s*\(.*\)\.openStream\s*\(/i, kind: "external-data" },
296
+ ],
297
+ sinks: [
298
+ { pattern: /\bRuntime\.getRuntime\s*\(\)\.exec\s*\(/i, kind: "command-exec" },
299
+ { pattern: /\bProcessBuilder\b/i, kind: "command-exec" },
300
+ { pattern: /\bStatement\b.*\.(?:execute|executeQuery|executeUpdate)\s*\(/i, kind: "sql-query" },
301
+ { pattern: /\.(?:createQuery|createNativeQuery)\s*\(/i, kind: "sql-query" },
302
+ { pattern: /\bString\.format\s*\(.*(?:SELECT|INSERT|UPDATE|DELETE)\b/i, kind: "sql-query" },
303
+ { pattern: /\bScriptEngine\b.*\.eval\s*\(/i, kind: "code-execution" },
304
+ { pattern: /\bClass\.forName\s*\(/i, kind: "code-execution" },
305
+ { pattern: /\.newInstance\s*\(/i, kind: "code-execution" },
306
+ { pattern: /\bXStream\b.*\.fromXML\s*\(/i, kind: "deserialization" },
307
+ { pattern: /\bObjectInputStream\b.*\.readObject\s*\(/i, kind: "deserialization" },
308
+ { pattern: /\bnew\s+File\s*\(/i, kind: "path-traversal" },
309
+ { pattern: /\bFiles\.(?:read|write|copy|move|newInputStream)\s*\(/i, kind: "path-traversal" },
310
+ { pattern: /\bresponse\.sendRedirect\s*\(/i, kind: "redirect" },
311
+ { pattern: /\.(?:forward|include)\s*\(/i, kind: "redirect" },
312
+ { pattern: /\bVelocity\b.*\.evaluate\s*\(/i, kind: "template" },
313
+ { pattern: /\bFreemarkerConfiguration\b/i, kind: "template" },
314
+ ],
315
+ sanitizers: [
316
+ /\bPreparedStatement\b/i,
317
+ /\bEncoder\.encode\s*\(/i,
318
+ /\bOWASP\.\w+\.encode\s*\(/i,
319
+ /\bHtmlUtils\.htmlEscape\s*\(/i,
320
+ /\bStringEscapeUtils\.escape\w+\s*\(/i,
321
+ /\bPattern\.matches\s*\(/i,
322
+ /\b@Valid\b/i,
323
+ /\b@Validated\b/i,
324
+ /\bBindingResult\b/i,
325
+ /\bInputValidator\b/i,
326
+ /\bwhitelist\s*\(/i,
327
+ /\bSanitizers\.\w+\s*\(/i,
328
+ ],
329
+ assignPattern: /^\s*(?:(?:final|var|String|int|long|double|boolean|byte|short|float|char|Object|List|Map|Set|Integer|Long|Double|Boolean|Optional|HttpServletRequest)\s+)*(\w+)\s*=\s*(.+);/,
330
+ guards: [
331
+ /if[ \t]*\([ \t]*\w+[ \t]*==[ \t]*null/i,
332
+ /\bObjects\.requireNonNull\s*\(/i,
333
+ /\bOptional\.ofNullable\s*\(/i,
334
+ /\bif[ \t]*\([ \t]*!?\w+\.(?:isEmpty|isBlank|matches|startsWith)\s*\(/i,
335
+ /throw[ \t]+new[ \t]+(?:IllegalArgumentException|ValidationException)/i,
336
+ ],
337
+ };
338
+ const GO_PATTERNS = {
339
+ sources: [
340
+ { pattern: /\br\.(?:FormValue|PostFormValue)\s*\(/i, kind: "http-param" },
341
+ { pattern: /\br\.URL\.Query\s*\(\)/i, kind: "http-param" },
342
+ { pattern: /\br\.Header\.Get\s*\(/i, kind: "http-param" },
343
+ { pattern: /\br\.Body\b/i, kind: "http-param" },
344
+ { pattern: /\bc\.(?:Query|Param|PostForm|FormValue|GetHeader)\s*\(/i, kind: "http-param" },
345
+ { pattern: /\bc\.(?:BindJSON|ShouldBindJSON|Bind)\s*\(/i, kind: "http-param" },
346
+ { pattern: /\bos\.Getenv\s*\(/i, kind: "environment" },
347
+ { pattern: /\bos\.Args\b/i, kind: "user-input" },
348
+ { pattern: /\bflag\.(?:String|Int|Bool|Arg)\s*\(/i, kind: "user-input" },
349
+ { pattern: /\bbufio\.NewReader\s*\(\s*os\.Stdin\b/i, kind: "user-input" },
350
+ { pattern: /\bjson\.(?:Unmarshal|NewDecoder)\s*\(/i, kind: "external-data" },
351
+ { pattern: /\bhttp\.Get\s*\(/i, kind: "external-data" },
352
+ { pattern: /\bioutil\.ReadAll\s*\(/i, kind: "external-data" },
353
+ { pattern: /\bio\.ReadAll\s*\(/i, kind: "external-data" },
354
+ ],
355
+ sinks: [
356
+ { pattern: /\bexec\.Command\s*\(/i, kind: "command-exec" },
357
+ { pattern: /\bexec\.CommandContext\s*\(/i, kind: "command-exec" },
358
+ { pattern: /\bos\.(?:StartProcess|Exec)\s*\(/i, kind: "command-exec" },
359
+ { pattern: /\bdb\.(?:Query|Exec|QueryRow|QueryContext|ExecContext)\s*\(/i, kind: "sql-query" },
360
+ { pattern: /\bsql\.(?:Open|Query)\s*\(/i, kind: "sql-query" },
361
+ { pattern: /\bfmt\.Sprintf\s*\(.*(?:SELECT|INSERT|UPDATE|DELETE)\b/i, kind: "sql-query" },
362
+ { pattern: /\btemplate\.(?:New|Must)\s*\(.*\.Parse\s*\(/i, kind: "template" },
363
+ { pattern: /\bhtml\/template\b.*\.Execute\s*\(/i, kind: "template" },
364
+ { pattern: /\btext\/template\b.*\.Execute\s*\(/i, kind: "template" },
365
+ { pattern: /\bos\.(?:Open|Create|OpenFile|ReadFile|WriteFile)\s*\(/i, kind: "path-traversal" },
366
+ { pattern: /\bfilepath\.Join\s*\(.*\+/i, kind: "path-traversal" },
367
+ { pattern: /\bhttp\.Redirect\s*\(/i, kind: "redirect" },
368
+ { pattern: /\bgob\.NewDecoder\b.*\.Decode\s*\(/i, kind: "deserialization" },
369
+ { pattern: /\bencoding\/gob\b/i, kind: "deserialization" },
370
+ { pattern: /\byaml\.Unmarshal\s*\(/i, kind: "deserialization" },
371
+ ],
372
+ sanitizers: [
373
+ /\bhtml\.EscapeString\s*\(/i,
374
+ /\burl\.QueryEscape\s*\(/i,
375
+ /\burl\.PathEscape\s*\(/i,
376
+ /\btemplate\.HTMLEscapeString\s*\(/i,
377
+ /\bstrconv\.(?:Atoi|ParseInt|ParseFloat|ParseBool)\s*\(/i,
378
+ /\bregexp\.MustCompile\b.*\.(?:MatchString|FindString)\s*\(/i,
379
+ /\bfilepath\.Clean\s*\(/i,
380
+ /\bpath\.Clean\s*\(/i,
381
+ /\bsqlx?\.\w*Prepared\b/i,
382
+ /\bValidate\.\w+\s*\(/i,
383
+ ],
384
+ assignPattern: /^\s*(?:var\s+)?(\w+)\s*(?::=|=)\s*(.+)/,
385
+ guards: [
386
+ /if[ \t]+\w+[ \t]*(?:==|!=)[ \t]*nil/i,
387
+ /if[ \t]+err[ \t]*!=[ \t]*nil/i,
388
+ /if[ \t]+!?(?:strings\.Contains|strings\.HasPrefix|regexp)\b/i,
389
+ /if[ \t]+len\s*\(\w+\)[ \t]*(?:==|!=|<|>|<=|>=)/i,
390
+ ],
391
+ };
392
+ const CSHARP_PATTERNS = {
393
+ sources: [
394
+ { pattern: /\bRequest\.(?:Form|QueryString|Query|Params|Headers|Cookies)\b/i, kind: "http-param" },
395
+ { pattern: /\bRequest\.(?:Body|InputStream)\b/i, kind: "http-param" },
396
+ { pattern: /\b\[FromQuery\]/i, kind: "http-param" },
397
+ { pattern: /\b\[FromBody\]/i, kind: "http-param" },
398
+ { pattern: /\b\[FromForm\]/i, kind: "http-param" },
399
+ { pattern: /\b\[FromHeader\]/i, kind: "http-param" },
400
+ { pattern: /\b\[FromRoute\]/i, kind: "url-param" },
401
+ { pattern: /\bHttpContext\.Request\b/i, kind: "http-param" },
402
+ { pattern: /\bEnvironment\.GetEnvironmentVariable\s*\(/i, kind: "environment" },
403
+ { pattern: /\bConsole\.ReadLine\s*\(/i, kind: "user-input" },
404
+ { pattern: /\bargs\[/i, kind: "user-input" },
405
+ { pattern: /\bHttpClient\b.*\.(?:GetAsync|PostAsync|GetStringAsync)\s*\(/i, kind: "external-data" },
406
+ { pattern: /\bJsonSerializer\.Deserialize\s*\(/i, kind: "external-data" },
407
+ { pattern: /\bJsonConvert\.DeserializeObject\s*\(/i, kind: "external-data" },
408
+ ],
409
+ sinks: [
410
+ { pattern: /\bProcess\.Start\s*\(/i, kind: "command-exec" },
411
+ { pattern: /\bProcessStartInfo\b/i, kind: "command-exec" },
412
+ { pattern: /\bSqlCommand\b.*\.(?:ExecuteReader|ExecuteNonQuery|ExecuteScalar)\s*\(/i, kind: "sql-query" },
413
+ { pattern: /\bnew\s+SqlCommand\s*\(\s*(?:\$"|".*\+)/i, kind: "sql-query" },
414
+ { pattern: /\.(?:FromSqlRaw|ExecuteSqlRaw|SqlQuery)\s*\(/i, kind: "sql-query" },
415
+ { pattern: /\bstring\.Format\s*\(.*(?:SELECT|INSERT|UPDATE|DELETE)\b/i, kind: "sql-query" },
416
+ { pattern: /\bCSharpScript\.EvaluateAsync\s*\(/i, kind: "code-execution" },
417
+ { pattern: /\bAssembly\.Load\s*\(/i, kind: "code-execution" },
418
+ { pattern: /\bActivator\.CreateInstance\s*\(/i, kind: "code-execution" },
419
+ { pattern: /\bBinaryFormatter\b.*\.Deserialize\s*\(/i, kind: "deserialization" },
420
+ { pattern: /\bXmlSerializer\b.*\.Deserialize\s*\(/i, kind: "deserialization" },
421
+ { pattern: /\bFile\.(?:ReadAllText|ReadAllBytes|ReadAllLines|Open|OpenRead)\s*\(/i, kind: "path-traversal" },
422
+ { pattern: /\bPath\.Combine\s*\(.*\+/i, kind: "path-traversal" },
423
+ { pattern: /\bResponse\.Redirect\s*\(/i, kind: "redirect" },
424
+ { pattern: /\bRedirectToAction\s*\(/i, kind: "redirect" },
425
+ { pattern: /\b@Html\.Raw\s*\(/i, kind: "xss" },
426
+ { pattern: /\bHtmlHelper\b.*\.Raw\s*\(/i, kind: "xss" },
427
+ ],
428
+ sanitizers: [
429
+ /\bHtmlEncoder\.Default\.Encode\s*\(/i,
430
+ /\bWebUtility\.HtmlEncode\s*\(/i,
431
+ /\bUrlEncoder\.Default\.Encode\s*\(/i,
432
+ /\bAntiXssEncoder\.\w+\s*\(/i,
433
+ /\b\[ValidateAntiForgeryToken\]/i,
434
+ /\bModelState\.IsValid\b/i,
435
+ /\b\[Required\]/i,
436
+ /\b\[StringLength\b/i,
437
+ /\b\[RegularExpression\b/i,
438
+ /\bSqlParameter\b/i,
439
+ /\bParameterized\b/i,
440
+ /\bAddWithValue\s*\(/i,
441
+ /\bInputValidator\b/i,
442
+ ],
443
+ assignPattern: /^\s*(?:(?:var|string|int|long|double|bool|float|decimal|object|dynamic|char|byte|List|Dictionary|IEnumerable|Task)\s*(?:<[^>]+>\s*)?)?(\w+)\s*=\s*(.+);/,
444
+ guards: [
445
+ /if[ \t]*\([ \t]*\w+[ \t]*(?:==|!=)[ \t]*null/i,
446
+ /\bif[ \t]*\([ \t]*!?string\.IsNullOrEmpty\s*\(/i,
447
+ /\bif[ \t]*\([ \t]*!?string\.IsNullOrWhiteSpace\s*\(/i,
448
+ /\?\?[ \t]+throw\b/i,
449
+ /\bargument\w*Exception\b/i,
450
+ /\bModelState\.IsValid\b/i,
451
+ ],
452
+ };
453
+ const RUST_PATTERNS = {
454
+ sources: [
455
+ { pattern: /\b(?:web|actix_web)::(?:Query|Form|Json|Path)\b/i, kind: "http-param" },
456
+ { pattern: /\breq\.(?:body|param|query|header)\s*\(/i, kind: "http-param" },
457
+ { pattern: /\baxum::extract::(?:Query|Form|Json|Path)\b/i, kind: "http-param" },
458
+ { pattern: /\bstd::env::(?:var|args)\b/i, kind: "environment" },
459
+ { pattern: /\bstd::io::stdin\b/i, kind: "user-input" },
460
+ { pattern: /\bserde_json::from_str\s*\(/i, kind: "external-data" },
461
+ { pattern: /\breqwest::(?:get|Client)\b/i, kind: "external-data" },
462
+ ],
463
+ sinks: [
464
+ { pattern: /\bCommand::new\s*\(/i, kind: "command-exec" },
465
+ { pattern: /\bstd::process::Command\b/i, kind: "command-exec" },
466
+ { pattern: /\.(?:query|execute|query_as|query_scalar)\s*\(/i, kind: "sql-query" },
467
+ { pattern: /\bformat!\s*\(.*(?:SELECT|INSERT|UPDATE|DELETE)\b/i, kind: "sql-query" },
468
+ { pattern: /\bstd::fs::(?:read_to_string|read|write|File::open)\s*\(/i, kind: "path-traversal" },
469
+ { pattern: /\bFile::open\s*\(/i, kind: "path-traversal" },
470
+ { pattern: /\bserde_json::from_value\s*\(/i, kind: "deserialization" },
471
+ { pattern: /\bbincode::deserialize\s*\(/i, kind: "deserialization" },
472
+ { pattern: /\bRedirect::to\s*\(/i, kind: "redirect" },
473
+ ],
474
+ sanitizers: [
475
+ /\bhtml_escape\s*\(/i,
476
+ /\bammonia::clean\s*\(/i,
477
+ /\bencode_safe\s*\(/i,
478
+ /\bsqlx::query!\s*\(/i,
479
+ /\b\.bind\s*\(/i,
480
+ /\.parse::<(?:i32|i64|u32|u64|f64|usize|bool)>/i,
481
+ /\bvalidate\s*\(\)/i,
482
+ /\bPath::new\s*\(.*\)\.canonicalize\s*\(/i,
483
+ ],
484
+ assignPattern: /^\s*(?:let\s+(?:mut\s+)?)?(\w+)\s*(?::\s*[\w<>&, [\]]+\s*)?=\s*(.+);/,
485
+ guards: [
486
+ /\bmatch\s+\w+\s*\{/i,
487
+ /if[ \t]+let[ \t]+Some\b/i,
488
+ /\.(?:unwrap_or|unwrap_or_else|unwrap_or_default)\s*\(/i,
489
+ /\.is_(?:some|none|ok|err)\s*\(\)/i,
490
+ /\bensure!\s*\(/i,
491
+ /\banyhow::ensure!\s*\(/i,
492
+ ],
493
+ };
494
+ const PHP_PATTERNS = {
495
+ sources: [
496
+ { pattern: /\$_(?:GET|POST|REQUEST|COOKIE|SERVER|FILES)\[/i, kind: "http-param" },
497
+ { pattern: /\$request->(?:input|get|post|query|all)\s*\(/i, kind: "http-param" },
498
+ { pattern: /\$_ENV\[|getenv\s*\(/i, kind: "environment" },
499
+ { pattern: /\$argv\b|fgets\s*\(\s*STDIN\b/i, kind: "user-input" },
500
+ { pattern: /file_get_contents\s*\(\s*['"]php:\/\/input/i, kind: "http-param" },
501
+ { pattern: /json_decode\s*\(\s*file_get_contents/i, kind: "external-data" },
502
+ { pattern: /\$_SESSION\[/i, kind: "external-data" },
503
+ ],
504
+ sinks: [
505
+ { pattern: /\b(?:exec|system|passthru|shell_exec|popen|proc_open)\s*\(/i, kind: "command-exec" },
506
+ { pattern: /\beval\s*\(|preg_replace\b.*\/e/i, kind: "code-execution" },
507
+ { pattern: /\bmysqli?_query\s*\(/i, kind: "sql-query" },
508
+ { pattern: /\$(?:pdo|db|conn)->(?:query|exec)\s*\(/i, kind: "sql-query" },
509
+ { pattern: /->(?:where|whereRaw|selectRaw|orderByRaw)\s*\(/i, kind: "sql-query" },
510
+ { pattern: /\binclude\s*\(|\brequire\s*\(|include_once\s*\(|require_once\s*\(/i, kind: "path-traversal" },
511
+ { pattern: /\bfile_(?:get_contents|put_contents)\s*\(/i, kind: "path-traversal" },
512
+ { pattern: /\bfopen\s*\(/i, kind: "path-traversal" },
513
+ { pattern: /\bheader\s*\(\s*['"]Location:/i, kind: "redirect" },
514
+ { pattern: /\bunserialize\s*\(/i, kind: "deserialization" },
515
+ { pattern: /\becho\b|\bprint\b/i, kind: "xss" },
516
+ ],
517
+ sanitizers: [
518
+ /\bhtmlspecialchars\s*\(/i,
519
+ /\bhtmlentities\s*\(/i,
520
+ /\bstrip_tags\s*\(/i,
521
+ /\baddslashes\s*\(/i,
522
+ /\bmysqli?_real_escape_string\s*\(/i,
523
+ /\bPDO::quote\s*\(/i,
524
+ /->(?:prepare|bindParam|bindValue)\s*\(/i,
525
+ /\bintval\s*\(|\bfloatval\s*\(|\b\(int\)|\b\(float\)/i,
526
+ /\bfilter_(?:var|input)\s*\(/i,
527
+ /\bpreg_match\s*\(/i,
528
+ /\brealpath\s*\(|basename\s*\(/i,
529
+ ],
530
+ assignPattern: /^\s*\$(\w+)\s*=\s*(.+);/,
531
+ guards: [
532
+ /if[ \t]*\([ \t]*!?(?:isset|empty|is_null|is_numeric|is_string|is_array)\s*\(/i,
533
+ /if[ \t]*\([ \t]*!?\$\w+\s*(?:===?|!==?)\s*(?:null|false|''|"")\b/i,
534
+ /\bvalidate\s*\(/i,
535
+ /\bpreg_match\s*\(/i,
536
+ /\bfilter_(?:var|input)\s*\(/i,
537
+ ],
538
+ };
539
+ const RUBY_PATTERNS = {
540
+ sources: [
541
+ { pattern: /\bparams\[/i, kind: "http-param" },
542
+ { pattern: /\bparams\.(?:require|permit|fetch)\s*\(/i, kind: "http-param" },
543
+ { pattern: /\brequest\.(?:body|env|headers|params)\b/i, kind: "http-param" },
544
+ { pattern: /\bENV\[|ENV\.fetch\s*\(/i, kind: "environment" },
545
+ { pattern: /\bARGV\b|\bgets\b|\breadline\b/i, kind: "user-input" },
546
+ { pattern: /\bJSON\.parse\s*\(/i, kind: "external-data" },
547
+ { pattern: /\bNet::HTTP\b.*\.(?:get|post)\s*\(/i, kind: "external-data" },
548
+ { pattern: /\bsession\[/i, kind: "external-data" },
549
+ { pattern: /\bcookies\[/i, kind: "http-param" },
550
+ ],
551
+ sinks: [
552
+ { pattern: /\bsystem\s*\(|\bexec\s*\(|\b`[^`]*#\{/i, kind: "command-exec" },
553
+ { pattern: /\b%x\{|Kernel\.system\s*\(/i, kind: "command-exec" },
554
+ { pattern: /\beval\s*\(|instance_eval\s*\(|class_eval\s*\(/i, kind: "code-execution" },
555
+ { pattern: /\bsend\s*\(|public_send\s*\(/i, kind: "code-execution" },
556
+ { pattern: /\.(?:where|find_by_sql|execute|select)\s*\(\s*(?:"|'|%|#)/i, kind: "sql-query" },
557
+ { pattern: /\.connection\.execute\s*\(/i, kind: "sql-query" },
558
+ { pattern: /\bFile\.(?:open|read|write|delete)\s*\(/i, kind: "path-traversal" },
559
+ { pattern: /\bredirect_to\s*\(/i, kind: "redirect" },
560
+ { pattern: /\bMarshal\.load\s*\(|YAML\.load\s*\(/i, kind: "deserialization" },
561
+ { pattern: /\b\.html_safe\b/i, kind: "xss" },
562
+ { pattern: /\braw\s*\(/i, kind: "xss" },
563
+ ],
564
+ sanitizers: [
565
+ /\bERB::Util\.html_escape\s*\(/i,
566
+ /\bCGI\.escapeHTML\s*\(/i,
567
+ /\bsanitize\s*\(/i,
568
+ /\bparams\.(?:require|permit)\s*\(/i,
569
+ /\.to_i\b|\.to_f\b/i,
570
+ /\bActiveRecord::Base\.connection\.quote\s*\(/i,
571
+ /\.(?:where|find_by)\s*\(\s*\w+\s*:\s/i,
572
+ /\bMarshal\.safe_load\b|YAML\.safe_load\s*\(/i,
573
+ /\bRegexp\.match\s*\(/i,
574
+ /\bFile\.expand_path\b.*\.start_with\?\s*\(/i,
575
+ ],
576
+ assignPattern: /^\s*(\w+)\s*=\s*(.+)/,
577
+ guards: [
578
+ /\bunless\s+\w+\.(?:nil\?|blank\?|empty\?)\b/i,
579
+ /if[ \t]+\w+\.(?:present\?|valid\?)\b/i,
580
+ /\braise\s+\w+Error\b/i,
581
+ /\.(?:validates?|validate!)\s/i,
582
+ ],
583
+ };
584
+ const KOTLIN_PATTERNS = {
585
+ sources: [
586
+ { pattern: /\brequest\.(?:getParameter|getAttribute|getHeader)\s*\(/i, kind: "http-param" },
587
+ { pattern: /\b@RequestParam\b|\b@PathVariable\b|\b@RequestBody\b/i, kind: "http-param" },
588
+ { pattern: /\bcall\.receive\b/i, kind: "http-param" },
589
+ { pattern: /\bcall\.parameters\[/i, kind: "http-param" },
590
+ { pattern: /\bSystem\.getenv\s*\(/i, kind: "environment" },
591
+ { pattern: /\breadLine\s*\(\)|Scanner\s*\(\s*System\.`in`\)/i, kind: "user-input" },
592
+ { pattern: /\bargs\[/i, kind: "user-input" },
593
+ { pattern: /\bGson\(\)\.fromJson\s*\(/i, kind: "external-data" },
594
+ { pattern: /\bJson\.decodeFromString\s*\(/i, kind: "external-data" },
595
+ ],
596
+ sinks: [
597
+ { pattern: /\bRuntime\.getRuntime\(\)\.exec\s*\(/i, kind: "command-exec" },
598
+ { pattern: /\bProcessBuilder\s*\(/i, kind: "command-exec" },
599
+ { pattern: /\.(?:executeQuery|executeUpdate|createQuery|nativeQuery)\s*\(/i, kind: "sql-query" },
600
+ { pattern: /\bString\.format\s*\(.*(?:SELECT|INSERT|UPDATE|DELETE)\b/i, kind: "sql-query" },
601
+ { pattern: /\"\$\{?\w+\}?.*(?:SELECT|INSERT|UPDATE|DELETE)\b/i, kind: "sql-query" },
602
+ { pattern: /\bFile\s*\(\s*(?:\$|[^")]+\+)/i, kind: "path-traversal" },
603
+ { pattern: /\bScriptEngine\b.*\.eval\s*\(/i, kind: "code-execution" },
604
+ { pattern: /\bObjectInputStream\b.*\.readObject\s*\(/i, kind: "deserialization" },
605
+ ],
606
+ sanitizers: [
607
+ /\bPreparedStatement\b/i,
608
+ /\bEncoder\.encode\s*\(/i,
609
+ /\bHtmlUtils\.htmlEscape\s*\(/i,
610
+ /\bStringEscapeUtils\.escape\w+\s*\(/i,
611
+ /\b@Valid\b|\b@Validated\b/i,
612
+ /\brequire\s*\{|check\s*\{/i,
613
+ /\.(?:toIntOrNull|toLongOrNull|toDoubleOrNull)\s*\(/i,
614
+ /\bRegex\s*\(.*\)\.matches\s*\(/i,
615
+ ],
616
+ assignPattern: /^\s*(?:(?:val|var|private|internal)\s+)?(\w+)\s*(?::\s*[\w<>?, [\]]+\s*)?=\s*(.+)/,
617
+ guards: [
618
+ /if[ \t]*\([ \t]*\w+[ \t]*(?:==|!=)[ \t]*null\b/i,
619
+ /\?\.\s*let\s*\{/i,
620
+ /\brequire\s*\(/i,
621
+ /\bcheck\s*\(/i,
622
+ /if[ \t]*\([ \t]*!?\w+\.(?:isBlank|isEmpty|isNullOrBlank|isNullOrEmpty)\s*\(/i,
623
+ ],
624
+ };
625
+ const SWIFT_PATTERNS = {
626
+ sources: [
627
+ { pattern: /\breq\.(?:content|query|parameters)\b/i, kind: "http-param" },
628
+ { pattern: /\brequest\.(?:content|query|body)\b/i, kind: "http-param" },
629
+ { pattern: /\bURLComponents\b.*\.queryItems\b/i, kind: "url-param" },
630
+ { pattern: /\bProcessInfo\.processInfo\.environment\[/i, kind: "environment" },
631
+ { pattern: /\bCommandLine\.arguments\b/i, kind: "user-input" },
632
+ { pattern: /\breadLine\s*\(/i, kind: "user-input" },
633
+ { pattern: /\bJSONDecoder\(\)\.decode\s*\(/i, kind: "external-data" },
634
+ { pattern: /\bURLSession\b.*\.data\s*\(/i, kind: "external-data" },
635
+ ],
636
+ sinks: [
637
+ { pattern: /\bProcess\(\)\s*.*arguments/i, kind: "command-exec" },
638
+ { pattern: /\bNSTask\b/i, kind: "command-exec" },
639
+ { pattern: /\.(?:execute|prepare)\s*\(\s*(?:".*\\|".*\+)/i, kind: "sql-query" },
640
+ { pattern: /\bFileManager\b.*\.(?:contentsOfFile|createFile)\s*\(/i, kind: "path-traversal" },
641
+ { pattern: /\bURL\s*\(\s*fileURLWithPath:\s*(?:\w+\s*\+|"\\)/i, kind: "path-traversal" },
642
+ { pattern: /\bJSContext\b.*\.evaluateScript\s*\(/i, kind: "code-execution" },
643
+ { pattern: /\bNSExpression\s*\(/i, kind: "code-execution" },
644
+ { pattern: /\bNSKeyedUnarchiver\b.*\.unarchiveObject\s*\(/i, kind: "deserialization" },
645
+ { pattern: /\bResponse\.redirect\s*\(/i, kind: "redirect" },
646
+ ],
647
+ sanitizers: [
648
+ /\baddingPercentEncoding\s*\(/i,
649
+ /\.replacingOccurrences\s*\(of:.*with:/i,
650
+ /\bInt\s*\(|Double\s*\(|Float\s*\(/i,
651
+ /\bNSRegularExpression\b/i,
652
+ /\bguard\s+let\b/i,
653
+ /\b\.standardizedFileURL\b|\.resolvingSymlinksInPath\b/i,
654
+ ],
655
+ assignPattern: /^\s*(?:(?:let|var)\s+)?(\w+)\s*(?::\s*[\w<>?, [\]?!]+\s*)?=\s*(.+)/,
656
+ guards: [
657
+ /guard[ \t]+let\b/i,
658
+ /if[ \t]+let\b/i,
659
+ /guard[ \t]+!?\w+\.(?:isEmpty|isNil)\b/i,
660
+ /\bprecondition\s*\(/i,
661
+ /\bassert\s*\(/i,
662
+ ],
663
+ };
664
+ // Map normalized languages to their pattern sets
665
+ const LANGUAGE_PATTERN_MAP = {
666
+ python: PYTHON_PATTERNS,
667
+ java: JAVA_PATTERNS,
668
+ go: GO_PATTERNS,
669
+ csharp: CSHARP_PATTERNS,
670
+ rust: RUST_PATTERNS,
671
+ php: PHP_PATTERNS,
672
+ ruby: RUBY_PATTERNS,
673
+ kotlin: KOTLIN_PATTERNS,
674
+ swift: SWIFT_PATTERNS,
675
+ };
211
676
  // ─── Public API ──────────────────────────────────────────────────────────────
212
677
  /**
213
678
  * Analyze a source file for taint flows: paths from untrusted input to
214
679
  * dangerous sinks through variable assignments and string concatenation.
215
680
  *
216
681
  * For JS/TS, uses the TypeScript compiler AST for precise variable tracking.
217
- * For other languages, falls back to regex-based lightweight analysis.
682
+ * For Python, Java, Go, C#, and Rust: uses language-specific source/sink/
683
+ * sanitizer patterns for deeper analysis.
684
+ * For other languages, falls back to generic regex-based analysis.
218
685
  */
219
686
  export function analyzeTaintFlows(code, language) {
220
687
  const lang = normalizeLanguage(language);
@@ -222,8 +689,10 @@ export function analyzeTaintFlows(code, language) {
222
689
  case "javascript":
223
690
  case "typescript":
224
691
  return analyzeTypeScriptTaint(code, lang);
225
- default:
226
- return analyzeRegexTaint(code);
692
+ default: {
693
+ const langPatterns = LANGUAGE_PATTERN_MAP[lang];
694
+ return analyzeRegexTaint(code, langPatterns);
695
+ }
227
696
  }
228
697
  }
229
698
  // ─── TypeScript / JavaScript Taint Analysis ──────────────────────────────────
@@ -423,13 +892,55 @@ function analyzeTypeScriptTaint(code, language) {
423
892
  return deduplicateFlows(flows);
424
893
  }
425
894
  // ─── Regex-based Taint Analysis (non-JS/TS languages) ────────────────────────
426
- function analyzeRegexTaint(code) {
895
+ /**
896
+ * Language-aware sanitizer check: combines global sanitizers with
897
+ * language-specific ones when available.
898
+ */
899
+ function isLangSanitized(expression, langPatterns) {
900
+ if (isSanitized(expression))
901
+ return true;
902
+ if (langPatterns) {
903
+ for (const p of langPatterns.sanitizers) {
904
+ if (p.test(expression))
905
+ return true;
906
+ }
907
+ }
908
+ return false;
909
+ }
910
+ /**
911
+ * Language-aware guard clause detection: combines global guards with
912
+ * language-specific guard patterns.
913
+ */
914
+ function detectLangGuardClauses(varName, sourceLine, sinkLine, codeLines, langPatterns) {
915
+ const baseReduction = detectGuardClauses(varName, sourceLine, sinkLine, codeLines);
916
+ if (!langPatterns)
917
+ return baseReduction;
918
+ const start = Math.min(sourceLine, sinkLine) - 1;
919
+ const end = Math.max(sourceLine, sinkLine);
920
+ let extraGuards = 0;
921
+ for (let i = start; i < end && i < codeLines.length; i++) {
922
+ const line = codeLines[i];
923
+ if (!containsWordBoundary(line, varName))
924
+ continue;
925
+ for (const guard of langPatterns.guards) {
926
+ if (guard.test(line)) {
927
+ extraGuards++;
928
+ break;
929
+ }
930
+ }
931
+ }
932
+ return Math.min(baseReduction + extraGuards * 0.1, 0.35);
933
+ }
934
+ function analyzeRegexTaint(code, langPatterns) {
427
935
  const codeLines = code.split("\n");
428
936
  const flows = [];
429
937
  // Track tainted variable names
430
938
  const tainted = new Map();
431
- // Assignment pattern: variable = source_expression
432
- const assignPattern = /^\s*(?:(?:let|const|var|val|auto)\s+)?(\w+)\s*[:=]\s*(.+)/;
939
+ // Merge source and sink patterns: language-specific + global
940
+ const allSources = langPatterns ? [...langPatterns.sources, ...SOURCE_PATTERNS] : SOURCE_PATTERNS;
941
+ const allSinks = langPatterns ? [...langPatterns.sinks, ...SINK_PATTERNS] : SINK_PATTERNS;
942
+ // Use language-specific assignment pattern if available
943
+ const assignPattern = langPatterns?.assignPattern ?? /^\s*(?:(?:let|const|var|val|auto)\s+)?(\w+)\s*[:=]\s*(.+)/;
433
944
  for (let i = 0; i < codeLines.length; i++) {
434
945
  const line = codeLines[i];
435
946
  const lineNum = i + 1;
@@ -438,10 +949,10 @@ function analyzeRegexTaint(code) {
438
949
  if (assignMatch) {
439
950
  const [, varName, rhs] = assignMatch;
440
951
  // Skip sanitized assignments
441
- if (isSanitized(rhs))
952
+ if (isLangSanitized(rhs, langPatterns))
442
953
  continue;
443
954
  // Direct source
444
- for (const src of SOURCE_PATTERNS) {
955
+ for (const src of allSources) {
445
956
  if (src.pattern.test(rhs)) {
446
957
  tainted.set(varName, {
447
958
  sourceExpr: rhs.trim(),
@@ -462,16 +973,16 @@ function analyzeRegexTaint(code) {
462
973
  }
463
974
  }
464
975
  // Skip lines with sanitizers for sink checking
465
- if (isSanitized(line))
976
+ if (isLangSanitized(line, langPatterns))
466
977
  continue;
467
978
  // Check for sinks using tainted data
468
- for (const sink of SINK_PATTERNS) {
979
+ for (const sink of allSinks) {
469
980
  if (!sink.pattern.test(line))
470
981
  continue;
471
982
  // Check tainted variables (word-boundary aware)
472
983
  for (const [varName, info] of tainted) {
473
984
  if (containsWordBoundary(line, varName) && lineNum !== info.sourceLine) {
474
- const guardReduction = detectGuardClauses(varName, info.sourceLine, lineNum, codeLines);
985
+ const guardReduction = detectLangGuardClauses(varName, info.sourceLine, lineNum, codeLines, langPatterns);
475
986
  flows.push({
476
987
  source: {
477
988
  line: info.sourceLine,
@@ -486,7 +997,7 @@ function analyzeRegexTaint(code) {
486
997
  }
487
998
  }
488
999
  // Inline source→sink
489
- for (const src of SOURCE_PATTERNS) {
1000
+ for (const src of allSources) {
490
1001
  if (src.pattern.test(line)) {
491
1002
  const alreadyCaptured = flows.some((f) => f.sink.line === lineNum);
492
1003
  if (!alreadyCaptured) {