circle-ir 3.57.0 → 3.59.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 (39) hide show
  1. package/configs/sinks/golang.json +61 -0
  2. package/configs/sinks/nodejs.json +11 -6
  3. package/configs/sinks/python.json +24 -0
  4. package/configs/sinks/rust.json +30 -0
  5. package/configs/sinks/sql.yaml +53 -0
  6. package/dist/analysis/config-loader.d.ts.map +1 -1
  7. package/dist/analysis/config-loader.js +57 -9
  8. package/dist/analysis/config-loader.js.map +1 -1
  9. package/dist/analysis/constant-propagation/patterns.d.ts.map +1 -1
  10. package/dist/analysis/constant-propagation/patterns.js +12 -0
  11. package/dist/analysis/constant-propagation/patterns.js.map +1 -1
  12. package/dist/analysis/constant-propagation/propagator.d.ts +62 -0
  13. package/dist/analysis/constant-propagation/propagator.d.ts.map +1 -1
  14. package/dist/analysis/constant-propagation/propagator.js +275 -7
  15. package/dist/analysis/constant-propagation/propagator.js.map +1 -1
  16. package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
  17. package/dist/analysis/passes/language-sources-pass.js +226 -14
  18. package/dist/analysis/passes/language-sources-pass.js.map +1 -1
  19. package/dist/analysis/passes/security-headers-pass.d.ts.map +1 -1
  20. package/dist/analysis/passes/security-headers-pass.js +93 -0
  21. package/dist/analysis/passes/security-headers-pass.js.map +1 -1
  22. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  23. package/dist/analysis/passes/sink-filter-pass.js +16 -1
  24. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  25. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  26. package/dist/analysis/passes/taint-propagation-pass.js +153 -9
  27. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  28. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  29. package/dist/analysis/taint-matcher.js +116 -2
  30. package/dist/analysis/taint-matcher.js.map +1 -1
  31. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  32. package/dist/analysis/taint-propagation.js +25 -1
  33. package/dist/analysis/taint-propagation.js.map +1 -1
  34. package/dist/browser/circle-ir.js +610 -45
  35. package/dist/core/circle-ir-core.cjs +368 -21
  36. package/dist/core/circle-ir-core.js +368 -21
  37. package/dist/types/config.d.ts +7 -0
  38. package/dist/types/config.d.ts.map +1 -1
  39. package/package.json +1 -1
@@ -10863,11 +10863,14 @@ var DEFAULT_SOURCES = [
10863
10863
  // Rocket
10864
10864
  { method: "param", class: "Request", type: "http_param", severity: "high", return_tainted: true },
10865
10865
  { method: "cookies", class: "Request", type: "http_cookie", severity: "high", return_tainted: true },
10866
- // Axum extractors
10867
- { method: "Json", type: "http_body", severity: "high", return_tainted: true },
10868
- { method: "Query", type: "http_param", severity: "high", return_tainted: true },
10869
- { method: "Path", type: "http_path", severity: "high", return_tainted: true },
10870
- { method: "Form", type: "http_param", severity: "high", return_tainted: true },
10866
+ // Axum extractors — Rust-only. The simple names `Json`/`Query`/`Path`/`Form`
10867
+ // collide with stdlib types in other ecosystems (notably Python's
10868
+ // `pathlib.Path` constructor and `flask.Form`), so they MUST be
10869
+ // language-scoped to Rust to avoid spurious source matches.
10870
+ { method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
10871
+ { method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
10872
+ { method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
10873
+ { method: "Form", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
10871
10874
  // Rust std library
10872
10875
  { method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
10873
10876
  { method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
@@ -11070,10 +11073,15 @@ var DEFAULT_SINKS = [
11070
11073
  { method: "PathResource", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11071
11074
  // Additional resource/file patterns
11072
11075
  { method: "forFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11073
- { method: "resolve", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11074
- { method: "resolve", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11075
- { method: "resolveSibling", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11076
- { method: "relativize", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
11076
+ // Java NIO `Path.resolve(other)` joining with an untrusted `other` can
11077
+ // escape the parent directory. Language-scoped to Java because the simple
11078
+ // name `resolve` collides with Python `pathlib.Path.resolve()`
11079
+ // (a canonicalization SANITIZER, no argument), JS `Promise.resolve(...)`,
11080
+ // and Rust `Path::canonicalize` variants. Sprint 9 #48.2.
11081
+ { method: "resolve", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
11082
+ { method: "resolve", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
11083
+ { method: "resolveSibling", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
11084
+ { method: "relativize", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0], languages: ["java"] },
11077
11085
  // Static file configuration
11078
11086
  { method: "staticFiles", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
11079
11087
  { method: "setRoot", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
@@ -12230,6 +12238,16 @@ var DEFAULT_SANITIZERS = [
12230
12238
  // Returns just filename, strips path
12231
12239
  { method: "canonicalize", removes: ["path_traversal"] },
12232
12240
  // Resolves symlinks and normalizes
12241
+ // Go path sanitizers (#51) — filepath.Base strips directory components
12242
+ // (fully sanitizes), filepath.Clean / path.Clean normalize away ../ segments
12243
+ // (defense-in-depth — mirrors Java getCanonicalPath in this table; the
12244
+ // stricter Clean+HasPrefix guard recognition is tracked separately).
12245
+ // EvalSymlinks is the Go equivalent of Java's Path.toRealPath.
12246
+ { method: "Base", class: "filepath", removes: ["path_traversal"] },
12247
+ { method: "Base", class: "path", removes: ["path_traversal"] },
12248
+ { method: "Clean", class: "filepath", removes: ["path_traversal"] },
12249
+ { method: "Clean", class: "path", removes: ["path_traversal"] },
12250
+ { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal"] },
12233
12251
  // Log Injection sanitizers
12234
12252
  { method: "replace", removes: ["log_injection"] },
12235
12253
  // Used to remove newlines/control chars
@@ -12324,6 +12342,8 @@ var DEFAULT_SANITIZERS = [
12324
12342
  { method: "abspath", class: "os.path", removes: ["path_traversal"] },
12325
12343
  { method: "realpath", class: "path", removes: ["path_traversal"] },
12326
12344
  { method: "abspath", class: "path", removes: ["path_traversal"] },
12345
+ // pathlib.Path.resolve() — canonicalizes path, resolves symlinks (Python 3)
12346
+ { method: "resolve", class: "Path", removes: ["path_traversal"] },
12327
12347
  // Python Type coercion
12328
12348
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
12329
12349
  { method: "float", removes: ["sql_injection", "command_injection"] },
@@ -12356,8 +12376,36 @@ var DEFAULT_SANITIZERS = [
12356
12376
  { method: "encode_attribute", class: "html_escape", removes: ["xss"] },
12357
12377
  { method: "escape_html", removes: ["xss"] },
12358
12378
  // Rust Type coercion (parsing)
12359
- { method: "parse", removes: ["sql_injection", "command_injection", "xss"] }
12379
+ { method: "parse", removes: ["sql_injection", "command_injection", "xss"] },
12360
12380
  // str.parse::<i32>()
12381
+ // =========================================================================
12382
+ // Type-cast taint barriers (#57)
12383
+ // Numeric/UUID casts cannot carry a string-injection payload.
12384
+ // =========================================================================
12385
+ // Java numeric parse — Integer.parseInt, Long.parseLong, etc.
12386
+ { method: "parseInt", class: "Integer", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12387
+ { method: "parseLong", class: "Long", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12388
+ { method: "parseFloat", class: "Float", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12389
+ { method: "parseDouble", class: "Double", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12390
+ { method: "parseShort", class: "Short", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12391
+ { method: "parseByte", class: "Byte", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12392
+ // Java UUID parse — UUID.fromString rejects non-UUID strings
12393
+ { method: "fromString", class: "UUID", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12394
+ // JavaScript numeric coercion (Number/parseInt/parseFloat already covered above; add path_traversal/code_injection)
12395
+ { method: "BigInt", removes: ["sql_injection", "nosql_injection", "command_injection", "path_traversal", "code_injection"] },
12396
+ // Go numeric parse — strconv.Atoi, ParseInt, ParseFloat, ParseUint, ParseBool
12397
+ { method: "Atoi", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12398
+ { method: "ParseInt", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12399
+ { method: "ParseFloat", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12400
+ { method: "ParseUint", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12401
+ { method: "ParseBool", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12402
+ // Go UUID parse
12403
+ { method: "Parse", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12404
+ { method: "MustParse", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12405
+ // Python — int/float already covered above; add bool + UUID/Decimal casts
12406
+ { method: "bool", removes: ["sql_injection", "command_injection", "xss", "code_injection"] },
12407
+ { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
12408
+ { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
12361
12409
  ];
12362
12410
  function getDefaultConfig() {
12363
12411
  return {
@@ -12474,7 +12522,7 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
12474
12522
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
12475
12523
  const sources = findSources(calls, types, config.sources, sourceLines, language);
12476
12524
  const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
12477
- const sanitizers = findSanitizers(calls, types, config.sanitizers);
12525
+ const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
12478
12526
  return { sources, sinks, sanitizers };
12479
12527
  }
12480
12528
  function attachSourceLineCode(sources, sinks, code) {
@@ -12494,6 +12542,9 @@ function findSources(calls, types, patterns, sourceLines, language) {
12494
12542
  const sources = [];
12495
12543
  for (const call of calls) {
12496
12544
  for (const pattern of patterns) {
12545
+ if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
12546
+ continue;
12547
+ }
12497
12548
  if (matchesSourcePattern(call, pattern)) {
12498
12549
  sources.push({
12499
12550
  type: pattern.type,
@@ -13130,6 +13181,15 @@ function receiverMightBeClass(receiver, className) {
13130
13181
  if (receiver === className) {
13131
13182
  return true;
13132
13183
  }
13184
+ if (receiver.endsWith(")")) {
13185
+ const ctorMatch = receiver.match(/^(\w+)\(/);
13186
+ if (ctorMatch) {
13187
+ const ctorName = ctorMatch[1];
13188
+ if (ctorName === className || ctorName.toLowerCase() === className.toLowerCase()) {
13189
+ return true;
13190
+ }
13191
+ }
13192
+ }
13133
13193
  if (receiver.includes("::")) {
13134
13194
  const scopePrefix = receiver.match(/^(\w+)::/);
13135
13195
  if (scopePrefix) {
@@ -13397,7 +13457,7 @@ function calculateSinkConfidence(call, pattern) {
13397
13457
  }
13398
13458
  return Math.min(confidence, 1);
13399
13459
  }
13400
- function findSanitizers(calls, types, patterns) {
13460
+ function findSanitizers(calls, types, patterns, sourceLines) {
13401
13461
  const sanitizers = [];
13402
13462
  const sanitizerMethods = /* @__PURE__ */ new Set();
13403
13463
  for (const type of types) {
@@ -13407,6 +13467,66 @@ function findSanitizers(calls, types, patterns) {
13407
13467
  }
13408
13468
  }
13409
13469
  }
13470
+ const wrapperSanitizers = /* @__PURE__ */ new Map();
13471
+ for (const type of types) {
13472
+ for (const method of type.methods) {
13473
+ const bodySize = method.end_line - method.start_line;
13474
+ if (bodySize < 0 || bodySize > 2) continue;
13475
+ const paramNames = new Set(method.parameters.map((p) => p.name));
13476
+ if (paramNames.size === 0) continue;
13477
+ const inside = [];
13478
+ for (const c of calls) {
13479
+ if (c.location.line < method.start_line || c.location.line > method.end_line) continue;
13480
+ if (c.method_name === method.name) continue;
13481
+ inside.push(c);
13482
+ }
13483
+ if (inside.length !== 1) continue;
13484
+ const innerCall = inside[0];
13485
+ let matched;
13486
+ for (const pattern of patterns) {
13487
+ if (matchesSanitizerPattern(innerCall, pattern)) {
13488
+ matched = pattern;
13489
+ break;
13490
+ }
13491
+ }
13492
+ if (!matched || !matched.removes || matched.removes.length === 0) continue;
13493
+ let argOk = false;
13494
+ for (const arg of innerCall.arguments) {
13495
+ if (arg.variable && paramNames.has(arg.variable)) {
13496
+ argOk = true;
13497
+ break;
13498
+ }
13499
+ }
13500
+ if (!argOk) continue;
13501
+ if (sourceLines) {
13502
+ const lineText = sourceLines[innerCall.location.line - 1] ?? "";
13503
+ const stripped = lineText.trim();
13504
+ const returnMatch = stripped.match(/^return\s+(?:await\s+)?(.*)$/);
13505
+ if (!returnMatch) continue;
13506
+ const after = returnMatch[1].replace(/;\s*$/, "").trimEnd();
13507
+ const callPrefix = innerCall.receiver ? `${innerCall.receiver}.${innerCall.method_name}(` : `${innerCall.method_name}(`;
13508
+ if (!after.startsWith(callPrefix)) continue;
13509
+ if (!after.endsWith(")")) continue;
13510
+ }
13511
+ const existing = wrapperSanitizers.get(method.name);
13512
+ if (existing) {
13513
+ const set = /* @__PURE__ */ new Set([...existing, ...matched.removes]);
13514
+ wrapperSanitizers.set(method.name, Array.from(set));
13515
+ } else {
13516
+ wrapperSanitizers.set(method.name, [...matched.removes]);
13517
+ }
13518
+ }
13519
+ }
13520
+ for (const call of calls) {
13521
+ const removes = wrapperSanitizers.get(call.method_name);
13522
+ if (!removes) continue;
13523
+ sanitizers.push({
13524
+ type: "derived_wrapper",
13525
+ method: formatSanitizerMethod(call),
13526
+ line: call.location.line,
13527
+ sanitizes: removes
13528
+ });
13529
+ }
13410
13530
  for (const call of calls) {
13411
13531
  if (sanitizerMethods.has(call.method_name)) {
13412
13532
  sanitizers.push({
@@ -14676,6 +14796,15 @@ var AnalysisPipeline = class {
14676
14796
  };
14677
14797
 
14678
14798
  // src/analysis/taint-propagation.ts
14799
+ function buildSanitizersByLine(sanitizers) {
14800
+ const out2 = /* @__PURE__ */ new Map();
14801
+ for (const san of sanitizers) {
14802
+ const existing = out2.get(san.line);
14803
+ if (existing) existing.push(san);
14804
+ else out2.set(san.line, [san]);
14805
+ }
14806
+ return out2;
14807
+ }
14679
14808
  function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
14680
14809
  let graph;
14681
14810
  let sources;
@@ -14711,7 +14840,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
14711
14840
  const defsByLine = graph.defsByLine;
14712
14841
  const usesByLine = graph.usesByLine;
14713
14842
  const callsByLine = graph.callsByLine;
14714
- const sanitizersByLine = graph.sanitizersByLine;
14843
+ const sanitizersByLine = sanitizers.length > 0 ? buildSanitizersByLine(sanitizers) : graph.sanitizersByLine;
14715
14844
  const defById = graph.defById;
14716
14845
  const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine);
14717
14846
  const initialTaint = rawInitialTaint.filter((tv) => {
@@ -15826,7 +15955,32 @@ var SANITIZER_METHODS = /* @__PURE__ */ new Set([
15826
15955
  "validatePath",
15827
15956
  "validateCityName",
15828
15957
  "validateInput",
15829
- "sanitizeInput"
15958
+ "sanitizeInput",
15959
+ // Type-cast barriers (#57) — numeric/boolean casts cannot carry a string
15960
+ // injection payload. Conservative whitelist; ambiguous names like `valueOf`,
15961
+ // `Parse`, `fromString` are intentionally excluded.
15962
+ // Java
15963
+ "parseInt",
15964
+ "parseLong",
15965
+ "parseFloat",
15966
+ "parseDouble",
15967
+ "parseShort",
15968
+ "parseByte",
15969
+ "fromString",
15970
+ // UUID.fromString — parses strict UUID format, rejects injection
15971
+ // JS/TS (parseInt/parseFloat covered above)
15972
+ "Number",
15973
+ "BigInt",
15974
+ // Go
15975
+ "Atoi",
15976
+ "ParseInt",
15977
+ "ParseFloat",
15978
+ "ParseUint",
15979
+ "ParseBool",
15980
+ // Python
15981
+ "int",
15982
+ "float",
15983
+ "bool"
15830
15984
  ]);
15831
15985
  var ANTI_SANITIZER_METHODS = /* @__PURE__ */ new Set([
15832
15986
  // URL decoding (reverses URL encoding)
@@ -15956,6 +16110,10 @@ var ConstantPropagator = class _ConstantPropagator {
15956
16110
  inConstructor = false;
15957
16111
  // Map constructor parameter names to their positions (0-indexed)
15958
16112
  constructorParamPositions = /* @__PURE__ */ new Map();
16113
+ // Sprint 9 #58.1 — names of `static final Pattern` fields whose compiled
16114
+ // regex is strict-anchored (provably matches a bounded character set).
16115
+ // Populated lazily on first access via `getSafePatternFields()`.
16116
+ safePatternFieldsCache = null;
15959
16117
  /**
15960
16118
  * Analyze source code and build constant propagation state.
15961
16119
  */
@@ -15989,6 +16147,7 @@ var ConstantPropagator = class _ConstantPropagator {
15989
16147
  this.currentClassName = null;
15990
16148
  this.inConstructor = false;
15991
16149
  this.constructorParamPositions.clear();
16150
+ this.safePatternFieldsCache = null;
15992
16151
  this.collectClassFields(tree.rootNode);
15993
16152
  for (const methodName of sanitizerMethods) {
15994
16153
  this.methodReturnsSanitized.add(methodName);
@@ -15998,6 +16157,7 @@ var ConstantPropagator = class _ConstantPropagator {
15998
16157
  (name2) => this.lookupSymbol(name2)
15999
16158
  );
16000
16159
  this.analyzeMethodReturns(tree.rootNode);
16160
+ this.seedPythonModuleConstants(tree.rootNode);
16001
16161
  this.visit(tree.rootNode);
16002
16162
  this.refineTaintFromConstants();
16003
16163
  const resultTainted = new Set(this.tainted);
@@ -16358,6 +16518,162 @@ var ConstantPropagator = class _ConstantPropagator {
16358
16518
  }
16359
16519
  }
16360
16520
  }
16521
+ /**
16522
+ * Sprint 9 #55 — seed the symbol table with Python module-level constant
16523
+ * assignments. Walks only direct children of the `module` root and adds
16524
+ * `IDENT = <primitive literal>` to `symbols` so `if IDENT:` guards inside
16525
+ * downstream functions can be folded to dead code.
16526
+ *
16527
+ * Recognized literal RHS kinds: `true`/`false` (booleans), integer/float
16528
+ * literals, string literals. The ExpressionEvaluator already understands
16529
+ * each via the same lookup callback; we just need the symbol present.
16530
+ */
16531
+ /**
16532
+ * Sprint 9 #55 — gate `field_declaration` folding to primitive literals.
16533
+ *
16534
+ * The deep-nesting regression (cognium-ai#88) constructs a Java
16535
+ * `static final String hyphenData = "a" + "b" + ... (10k segments)` at the
16536
+ * class level. `handleVariableDeclaration` would otherwise dispatch
16537
+ * `evaluateExpression` on the deeply nested binary AST and blow the V8
16538
+ * stack. The dead-code-by-const-guard pattern (`if (DEBUG)`) only requires
16539
+ * `boolean`/`integer`/`string` (single-literal) RHS folding, so restrict
16540
+ * to those node types.
16541
+ */
16542
+ fieldDeclHasPrimitiveLiteralValue(node) {
16543
+ const primitive = /* @__PURE__ */ new Set([
16544
+ // Java literal node types
16545
+ "true",
16546
+ "false",
16547
+ "null_literal",
16548
+ "decimal_integer_literal",
16549
+ "hex_integer_literal",
16550
+ "octal_integer_literal",
16551
+ "binary_integer_literal",
16552
+ "decimal_floating_point_literal",
16553
+ "hex_floating_point_literal",
16554
+ "character_literal",
16555
+ "string_literal",
16556
+ // JS/TS literal node types (defensive, in case other langs reuse it)
16557
+ "number",
16558
+ "string"
16559
+ ]);
16560
+ for (const child of node.children) {
16561
+ if (child.type !== "variable_declarator") continue;
16562
+ const value = child.childForFieldName("value");
16563
+ if (!value) continue;
16564
+ if (!primitive.has(value.type)) return false;
16565
+ }
16566
+ return true;
16567
+ }
16568
+ /**
16569
+ * Sprint 9 #58.1 — collect the set of class-level `Pattern` field names
16570
+ * whose compiled regex is strict-anchored, i.e. provably matches a
16571
+ * bounded character set with no wildcard escape. A subsequent
16572
+ * `if (!FIELD.matcher(var).matches()) throw ...;` guard then proves
16573
+ * `var` is sanitized after the if.
16574
+ *
16575
+ * Recognized initializer shapes (scanned via source-text regex to avoid
16576
+ * threading another AST walk):
16577
+ * `static final Pattern FIELD = Pattern.compile("regex");`
16578
+ *
16579
+ * Strict-anchored regex criteria:
16580
+ * - starts with `^` and ends with `$`
16581
+ * - after stripping `[...]` character classes, must not contain `.` or
16582
+ * `|` (a `.` could match anything; `|` admits an arbitrary alternative)
16583
+ */
16584
+ getSafePatternFields() {
16585
+ if (this.safePatternFieldsCache !== null) return this.safePatternFieldsCache;
16586
+ const set = /* @__PURE__ */ new Set();
16587
+ const re = /\b(?:public\s+|private\s+|protected\s+)?(?:static\s+final|final\s+static)\s+(?:java\.util\.regex\.)?Pattern\s+(\w+)\s*=\s*(?:java\.util\.regex\.)?Pattern\s*\.\s*compile\s*\(\s*"((?:[^"\\]|\\.)*)"/g;
16588
+ let m;
16589
+ while ((m = re.exec(this.source)) !== null) {
16590
+ const name2 = m[1];
16591
+ const regex = m[2];
16592
+ if (this.isStrictAnchoredRegex(regex)) set.add(name2);
16593
+ }
16594
+ this.safePatternFieldsCache = set;
16595
+ return set;
16596
+ }
16597
+ isStrictAnchoredRegex(re) {
16598
+ if (!re.startsWith("^") || !re.endsWith("$")) return false;
16599
+ const stripped = re.replace(/\[(?:[^\]\\]|\\.)*\]/g, "");
16600
+ const cleaned = stripped.replace(/\\./g, "");
16601
+ if (cleaned.includes(".")) return false;
16602
+ if (cleaned.includes("|")) return false;
16603
+ return true;
16604
+ }
16605
+ /**
16606
+ * Sprint 9 #58.1 — detect the regex-allowlist guard pattern.
16607
+ *
16608
+ * if (!SAFE_NAME.matcher(var).matches()) { throw ...; }
16609
+ *
16610
+ * Returns the guarded variable name if the pattern matches AND
16611
+ * `SAFE_NAME` is a recognized strict-anchored Pattern field, otherwise
16612
+ * null. Caller drops the variable from `tainted` after the if-block.
16613
+ */
16614
+ detectRegexAllowlistGuard(condition, consequence) {
16615
+ if (!consequence) return null;
16616
+ let condText = getNodeText2(condition, this.source).replace(/\s+/g, "");
16617
+ while (condText.startsWith("(") && condText.endsWith(")")) {
16618
+ const inner = condText.slice(1, -1);
16619
+ let depth = 0;
16620
+ let balanced = true;
16621
+ for (let i2 = 0; i2 < inner.length; i2++) {
16622
+ if (inner[i2] === "(") depth++;
16623
+ else if (inner[i2] === ")") depth--;
16624
+ if (depth < 0) {
16625
+ balanced = false;
16626
+ break;
16627
+ }
16628
+ }
16629
+ if (!balanced || depth !== 0) break;
16630
+ condText = inner;
16631
+ }
16632
+ const m = condText.match(/^!(\w+)\.matcher\((\w+)\)\.matches\(\)$/);
16633
+ if (!m) return null;
16634
+ const patternName = m[1];
16635
+ const varName = m[2];
16636
+ if (!this.getSafePatternFields().has(patternName)) return null;
16637
+ if (!this.consequenceContainsThrow(consequence)) return null;
16638
+ return varName;
16639
+ }
16640
+ consequenceContainsThrow(node) {
16641
+ if (node.type === "throw_statement") return true;
16642
+ const stack = [node];
16643
+ while (stack.length > 0) {
16644
+ const n = stack.pop();
16645
+ if (!n) continue;
16646
+ if (n.type === "throw_statement") return true;
16647
+ if (n.type === "if_statement" || n.type === "switch_statement") continue;
16648
+ for (const c of n.children) stack.push(c);
16649
+ }
16650
+ return false;
16651
+ }
16652
+ seedPythonModuleConstants(root) {
16653
+ if (root.type !== "module") return;
16654
+ for (const child of root.children) {
16655
+ const target = child.type === "assignment" ? child : child.type === "expression_statement" && child.children.length > 0 ? child.children[0] : null;
16656
+ if (!target || target.type !== "assignment") continue;
16657
+ const left = target.childForFieldName("left");
16658
+ const right = target.childForFieldName("right");
16659
+ if (!left || !right) continue;
16660
+ if (left.type !== "identifier") continue;
16661
+ const allowed = /* @__PURE__ */ new Set([
16662
+ "true",
16663
+ "false",
16664
+ "none",
16665
+ "integer",
16666
+ "float",
16667
+ "string"
16668
+ ]);
16669
+ if (!allowed.has(right.type)) continue;
16670
+ const name2 = getNodeText2(left, this.source);
16671
+ if (!name2) continue;
16672
+ const value = this.evaluateExpression(right);
16673
+ if (!isKnown(value)) continue;
16674
+ this.symbols.set(name2, value);
16675
+ }
16676
+ }
16361
16677
  findAllMethods(node) {
16362
16678
  const methods = [];
16363
16679
  const stack = [node];
@@ -16452,6 +16768,11 @@ var ConstantPropagator = class _ConstantPropagator {
16452
16768
  case "local_variable_declaration":
16453
16769
  this.handleVariableDeclaration(node);
16454
16770
  return false;
16771
+ case "field_declaration":
16772
+ if (this.fieldDeclHasPrimitiveLiteralValue(node)) {
16773
+ this.handleVariableDeclaration(node);
16774
+ }
16775
+ return false;
16455
16776
  case "assignment_expression":
16456
16777
  this.handleAssignment(node);
16457
16778
  return false;
@@ -16942,6 +17263,16 @@ var ConstantPropagator = class _ConstantPropagator {
16942
17263
  }
16943
17264
  this.inConditionalBranch = wasInConditional;
16944
17265
  this.tainted = /* @__PURE__ */ new Set([...taintedBefore, ...taintedAfterThen, ...taintedAfterElse]);
17266
+ const guardedVar = this.detectRegexAllowlistGuard(condition, consequence);
17267
+ if (guardedVar) {
17268
+ this.tainted.delete(guardedVar);
17269
+ this.sanitizedVars.add(guardedVar);
17270
+ const scoped = this.getScopedName(guardedVar);
17271
+ if (scoped !== guardedVar) {
17272
+ this.tainted.delete(scoped);
17273
+ this.sanitizedVars.add(scoped);
17274
+ }
17275
+ }
16945
17276
  }
16946
17277
  }
16947
17278
  /**
@@ -17132,17 +17463,33 @@ var ConstantPropagator = class _ConstantPropagator {
17132
17463
  /**
17133
17464
  * Check if an expression is a call to a sanitizer method.
17134
17465
  * This includes both built-in sanitizers and @sanitizer annotated methods.
17466
+ * Handles Java (`method_invocation`), Go/JS/TS (`call_expression`), and
17467
+ * Python (`call`) AST shapes.
17135
17468
  */
17136
17469
  isSanitizerMethodCall(node) {
17137
- if (node.type !== "method_invocation") {
17138
- return false;
17470
+ const methodName = this.extractCallName(node);
17471
+ if (!methodName) return false;
17472
+ return SANITIZER_METHODS.has(methodName) || this.methodReturnsSanitized.has(methodName);
17473
+ }
17474
+ /**
17475
+ * Extract the trailing method/function name from any call node shape:
17476
+ * Java `method_invocation` — name field
17477
+ * Go/JS `call_expression` — function field (identifier or selector/member)
17478
+ * Python `call` — function field (identifier or attribute)
17479
+ */
17480
+ extractCallName(node) {
17481
+ let fnNode = null;
17482
+ if (node.type === "method_invocation") {
17483
+ fnNode = node.childForFieldName("name");
17484
+ } else if (node.type === "call_expression" || node.type === "call") {
17485
+ fnNode = node.childForFieldName("function");
17139
17486
  }
17140
- const nameNode = node.childForFieldName("name");
17141
- if (!nameNode) {
17142
- return false;
17487
+ if (!fnNode) return null;
17488
+ if (fnNode.type === "selector_expression" || fnNode.type === "member_expression" || fnNode.type === "attribute") {
17489
+ const tail = fnNode.childForFieldName("field") || fnNode.childForFieldName("property") || fnNode.childForFieldName("attribute");
17490
+ if (tail) return getNodeText2(tail, this.source);
17143
17491
  }
17144
- const methodName = getNodeText2(nameNode, this.source);
17145
- return SANITIZER_METHODS.has(methodName) || this.methodReturnsSanitized.has(methodName);
17492
+ return getNodeText2(fnNode, this.source);
17146
17493
  }
17147
17494
  /**
17148
17495
  * Check if an expression is a call to an anti-sanitizer method.
@@ -21698,6 +22045,7 @@ var LanguageSourcesPass = class {
21698
22045
  const additionalSources = [];
21699
22046
  const additionalSinks = [];
21700
22047
  additionalSources.push(...findGetterSources(types, constProp.instanceFieldTaint, code));
22048
+ additionalSources.push(...findOopFieldReadSources(types, code, language));
21701
22049
  additionalSources.push(...findJavaScriptAssignmentSources(code, language));
21702
22050
  const jsDOMSinks = findJavaScriptDOMSinks(code, language);
21703
22051
  for (const s of jsDOMSinks) {
@@ -21802,6 +22150,115 @@ function findGetterSources(types, instanceFieldTaint, _sourceCode) {
21802
22150
  }
21803
22151
  return sources;
21804
22152
  }
22153
+ function findOopFieldReadSources(types, sourceCode, language) {
22154
+ if (language !== "java" && language !== "python") return [];
22155
+ const sources = [];
22156
+ const lines = sourceCode.split("\n");
22157
+ const isPython = language === "python";
22158
+ const SELF = isPython ? "self" : "this";
22159
+ const javaHttpPattern = /\b(?:req|request|httpRequest|servletRequest|httpServletRequest)\.(?:getParameter|getParameterValues|getParameterMap|getHeader|getHeaders|getCookies|getQueryString|getPathInfo|getRequestURI|getRequestURL|getInputStream|getReader)\b/;
22160
+ const fieldAssignRe = new RegExp(`^\\s*${SELF}\\.([A-Za-z_]\\w*)\\s*=\\s*(.+?)(?:;\\s*)?$`);
22161
+ const commentPrefix = isPython ? "#" : "//";
22162
+ for (const type of types) {
22163
+ if (type.kind !== "class") continue;
22164
+ if (type.name === "<module>") continue;
22165
+ let ctor;
22166
+ for (const m of type.methods) {
22167
+ if (isPython) {
22168
+ if (m.name === "__init__") {
22169
+ ctor = m;
22170
+ break;
22171
+ }
22172
+ } else {
22173
+ if (m.name === type.name) {
22174
+ ctor = m;
22175
+ break;
22176
+ }
22177
+ }
22178
+ }
22179
+ if (!ctor) continue;
22180
+ const paramNames = /* @__PURE__ */ new Set();
22181
+ for (const p of ctor.parameters) {
22182
+ if (p.name === "self" || p.name === "this") continue;
22183
+ paramNames.add(p.name);
22184
+ }
22185
+ const fieldTaint = /* @__PURE__ */ new Map();
22186
+ const ctorStart = ctor.start_line;
22187
+ const ctorEnd = ctor.end_line;
22188
+ for (let i2 = ctorStart - 1; i2 < Math.min(ctorEnd, lines.length); i2++) {
22189
+ const line = lines[i2] ?? "";
22190
+ if (line.trim().startsWith(commentPrefix)) continue;
22191
+ const m = line.match(fieldAssignRe);
22192
+ if (!m) continue;
22193
+ const fieldName = m[1];
22194
+ const rhs = m[2].trim().replace(/;\s*$/, "");
22195
+ let sourceType = null;
22196
+ if (paramNames.has(rhs)) {
22197
+ sourceType = "interprocedural_param";
22198
+ } else if (!isPython && javaHttpPattern.test(rhs)) {
22199
+ sourceType = "http_param";
22200
+ } else if (isPython) {
22201
+ for (const { pattern, type: type2 } of PYTHON_TAINTED_PATTERNS2) {
22202
+ if (pattern.test(rhs)) {
22203
+ sourceType = type2;
22204
+ break;
22205
+ }
22206
+ }
22207
+ }
22208
+ if (sourceType) {
22209
+ fieldTaint.set(fieldName, { line: i2 + 1, type: sourceType });
22210
+ }
22211
+ }
22212
+ if (fieldTaint.size === 0) continue;
22213
+ for (const [fieldName, info2] of fieldTaint) {
22214
+ sources.push({
22215
+ type: info2.type,
22216
+ location: `${type.name}.${SELF}.${fieldName} (constructor-injected field, #78)`,
22217
+ severity: "high",
22218
+ line: info2.line,
22219
+ confidence: 0.85,
22220
+ variable: `${SELF}.${fieldName}`
22221
+ });
22222
+ }
22223
+ for (const m of type.methods) {
22224
+ if (m === ctor) continue;
22225
+ const nonSelfParams = m.parameters.filter((p) => p.name !== "self" && p.name !== "this");
22226
+ if (nonSelfParams.length !== 0) continue;
22227
+ const mStart = m.start_line;
22228
+ const mEnd = m.end_line;
22229
+ let returnedField = null;
22230
+ let returnStatementCount = 0;
22231
+ const returnRe = new RegExp(`\\breturn\\s+${SELF}\\.([A-Za-z_]\\w*)\\s*[;}]?`);
22232
+ for (let i2 = mStart - 1; i2 < Math.min(mEnd, lines.length); i2++) {
22233
+ const raw = lines[i2] ?? "";
22234
+ const trimmed = raw.trim();
22235
+ if (!trimmed) continue;
22236
+ if (trimmed.startsWith(commentPrefix)) continue;
22237
+ const rm = trimmed.match(returnRe);
22238
+ if (rm) {
22239
+ returnedField = rm[1];
22240
+ returnStatementCount++;
22241
+ } else if (/\breturn\b/.test(trimmed)) {
22242
+ returnStatementCount = 99;
22243
+ break;
22244
+ }
22245
+ }
22246
+ if (returnStatementCount === 1 && returnedField && fieldTaint.has(returnedField)) {
22247
+ const fieldInfo = fieldTaint.get(returnedField);
22248
+ const getterVar = isPython ? `${SELF}.${m.name}` : m.name;
22249
+ sources.push({
22250
+ type: fieldInfo.type,
22251
+ location: `${type.name}.${m.name} returns tainted field '${returnedField}' (#78)`,
22252
+ severity: "high",
22253
+ line: m.start_line,
22254
+ confidence: 0.85,
22255
+ variable: getterVar
22256
+ });
22257
+ }
22258
+ }
22259
+ }
22260
+ return sources;
22261
+ }
21805
22262
  function findJavaScriptAssignmentSources(sourceCode, language) {
21806
22263
  if (!["javascript", "typescript"].includes(language)) return [];
21807
22264
  const sources = [];
@@ -22110,25 +22567,42 @@ function findBashTaintSources(sourceCode, dfg) {
22110
22567
  const sources = [];
22111
22568
  const lines = sourceCode.split("\n");
22112
22569
  const definedVars = new Set(dfg.defs.filter((d) => d.kind === "local").map((d) => d.variable));
22570
+ const fnHeaderRe = /^\s*(?:function\s+)?[A-Za-z_][\w-]*\s*\(\s*\)\s*\{?\s*$|^\s*function\s+[A-Za-z_][\w-]*\s*\{?\s*$/;
22571
+ let braceDepth = 0;
22113
22572
  for (let i2 = 0; i2 < lines.length; i2++) {
22114
22573
  const line = lines[i2];
22115
22574
  const trimmed = line.trim();
22116
22575
  const lineNumber = i2 + 1;
22117
22576
  if (trimmed.startsWith("#")) continue;
22118
- const positionalRe = /\$([1-9@*])|\$\{([1-9@*])\}/g;
22119
- let m;
22120
- while ((m = positionalRe.exec(line)) !== null) {
22121
- const param = m[1] ?? m[2];
22122
- const alreadyExists = sources.some((s) => s.line === lineNumber && s.variable === param);
22123
- if (!alreadyExists) {
22124
- sources.push({
22125
- type: "io_input",
22126
- location: `positional parameter $${param}`,
22127
- severity: "high",
22128
- line: lineNumber,
22129
- confidence: 1,
22130
- variable: param
22131
- });
22577
+ const insideFunction = braceDepth > 0;
22578
+ if (!insideFunction) {
22579
+ const positionalRe = /\$([1-9@*])|\$\{([1-9@*])\}/g;
22580
+ let m;
22581
+ while ((m = positionalRe.exec(line)) !== null) {
22582
+ const param = m[1] ?? m[2];
22583
+ const alreadyExists = sources.some((s) => s.line === lineNumber && s.variable === param);
22584
+ if (!alreadyExists) {
22585
+ sources.push({
22586
+ type: "io_input",
22587
+ location: `positional parameter $${param}`,
22588
+ severity: "high",
22589
+ line: lineNumber,
22590
+ confidence: 1,
22591
+ variable: param
22592
+ });
22593
+ }
22594
+ }
22595
+ }
22596
+ if (fnHeaderRe.test(line) || /^\s*[A-Za-z_][\w-]*\s*\(\s*\)\s*\{/.test(line)) {
22597
+ const openBracesOnLine = (line.match(/\{/g) ?? []).length;
22598
+ const closeBracesOnLine = (line.match(/\}/g) ?? []).length;
22599
+ braceDepth += openBracesOnLine - closeBracesOnLine;
22600
+ } else {
22601
+ if (braceDepth > 0) {
22602
+ const openBracesOnLine = (line.match(/\{/g) ?? []).length;
22603
+ const closeBracesOnLine = (line.match(/\}/g) ?? []).length;
22604
+ braceDepth += openBracesOnLine - closeBracesOnLine;
22605
+ if (braceDepth < 0) braceDepth = 0;
22132
22606
  }
22133
22607
  }
22134
22608
  const cmdSubAssign = trimmed.match(/^(\w+)=\$\((\w+)\s/);
@@ -22550,10 +23024,14 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
22550
23024
  const callsAtSink = callsByLine.get(sink.line) ?? [];
22551
23025
  const isInSynchronizedBlock = synchronizedLines?.has(sink.line) ?? false;
22552
23026
  const relevantCalls = sink.method ? callsAtSink.filter((c) => c.method_name === sink.method) : callsAtSink;
23027
+ const trustArgPositions = language !== "bash" && language !== "shell";
22553
23028
  for (const call of relevantCalls) {
22554
23029
  let allArgsAreClean = true;
23030
+ let dangerousArgCount = 0;
22555
23031
  const methodName = call.in_method;
22556
23032
  for (const arg of call.arguments) {
23033
+ if (trustArgPositions && sink.argPositions && sink.argPositions.length > 0 && !sink.argPositions.includes(arg.position)) continue;
23034
+ dangerousArgCount++;
22557
23035
  if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null) continue;
22558
23036
  if (arg.variable && !arg.expression?.includes("[")) {
22559
23037
  const varName = arg.variable;
@@ -22576,7 +23054,7 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
22576
23054
  allArgsAreClean = false;
22577
23055
  }
22578
23056
  }
22579
- if (allArgsAreClean && call.arguments.length > 0) return false;
23057
+ if (allArgsAreClean && dangerousArgCount > 0) return false;
22580
23058
  }
22581
23059
  return true;
22582
23060
  });
@@ -22668,7 +23146,7 @@ var TaintPropagationPass = class {
22668
23146
  flows.push(f);
22669
23147
  }
22670
23148
  }
22671
- const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines) ?? [];
23149
+ const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code) ?? [];
22672
23150
  for (const f of collectionFlows) {
22673
23151
  if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) continue;
22674
23152
  const flowForCheck = {
@@ -22687,13 +23165,13 @@ var TaintPropagationPass = class {
22687
23165
  if (isFP) continue;
22688
23166
  flows.push(f);
22689
23167
  }
22690
- const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines) ?? [];
23168
+ const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
22691
23169
  for (const f of paramFlows) {
22692
23170
  if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
22693
23171
  flows.push(f);
22694
23172
  }
22695
23173
  }
22696
- const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
23174
+ const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language) ?? [];
22697
23175
  for (const f of exprScanFlows) {
22698
23176
  if (flows.some(
22699
23177
  (x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
@@ -22714,10 +23192,21 @@ var TaintPropagationPass = class {
22714
23192
  if (isFP) continue;
22715
23193
  flows.push(f);
22716
23194
  }
22717
- return { flows };
23195
+ const sanitizedNames = constProp.sanitizedVars;
23196
+ const finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
23197
+ if (f.path.length === 0) return true;
23198
+ const sourceVar = f.path[0].variable;
23199
+ if (!sourceVar) return true;
23200
+ if (sanitizedNames.has(sourceVar)) return false;
23201
+ for (const s of sanitizedNames) {
23202
+ if (s.endsWith(`:${sourceVar}`)) return false;
23203
+ }
23204
+ return true;
23205
+ });
23206
+ return { flows: finalFlows };
22718
23207
  }
22719
23208
  };
22720
- function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines) {
23209
+ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code) {
22721
23210
  const flows = [];
22722
23211
  const callsByLine = /* @__PURE__ */ new Map();
22723
23212
  for (const call of calls) {
@@ -22739,6 +23228,9 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
22739
23228
  if (taintedVars.has(varName) || taintedVars.has(scopedName)) {
22740
23229
  const source = sources[0];
22741
23230
  if (source) {
23231
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
23232
+ continue;
23233
+ }
22742
23234
  flows.push({
22743
23235
  source_line: source.line,
22744
23236
  sink_line: sink.line,
@@ -22773,6 +23265,9 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
22773
23265
  if (taintedVars.has(collectionVar) || taintedVars.has(scopedCollection)) {
22774
23266
  const source = sources[0];
22775
23267
  if (source) {
23268
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
23269
+ continue;
23270
+ }
22776
23271
  flows.push({
22777
23272
  source_line: source.line,
22778
23273
  sink_line: sink.line,
@@ -22842,7 +23337,7 @@ function detectArrayElementFlows(calls, sources, sinks, taintedArrayElements, un
22842
23337
  }
22843
23338
  return flows;
22844
23339
  }
22845
- function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines) {
23340
+ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines, tainted, code) {
22846
23341
  const flows = [];
22847
23342
  const paramSourcesByMethod = /* @__PURE__ */ new Map();
22848
23343
  for (const source of sources) {
@@ -22884,6 +23379,9 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
22884
23379
  if (paramSource) {
22885
23380
  const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
22886
23381
  if (!exists) {
23382
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
23383
+ continue;
23384
+ }
22887
23385
  flows.push({
22888
23386
  source_line: paramSource.line,
22889
23387
  sink_line: sink.line,
@@ -22905,7 +23403,27 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
22905
23403
  void types;
22906
23404
  return flows;
22907
23405
  }
22908
- function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, code, language) {
23406
+ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
23407
+ if (!variable || sinkLine - srcLine < 2) return false;
23408
+ if (!/^[A-Za-z_][\w]*$/.test(variable)) return false;
23409
+ const lines = code.split("\n");
23410
+ const lo = Math.max(0, srcLine);
23411
+ const hi = Math.min(lines.length, sinkLine - 1);
23412
+ const strLit = `(?:"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|\`[^\`\\\\]*(?:\\\\.[^\`\\\\]*)*\`)`;
23413
+ const reNaked = new RegExp(
23414
+ `^\\s*${variable}\\s*(?::?=)\\s*${strLit}\\s*;?\\s*$`
23415
+ );
23416
+ const reGuarded = new RegExp(
23417
+ `^\\s*if\\b.*\\b${variable}\\s*=\\s*${strLit}\\s*;?\\s*$`
23418
+ );
23419
+ for (let i2 = lo; i2 < hi; i2++) {
23420
+ const line = lines[i2];
23421
+ if (!line) continue;
23422
+ if (reNaked.test(line) || reGuarded.test(line)) return true;
23423
+ }
23424
+ return false;
23425
+ }
23426
+ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language) {
22909
23427
  const flows = [];
22910
23428
  const sourcesWithVar = sources.filter(
22911
23429
  (s) => typeof s.variable === "string" && s.variable.length > 0
@@ -22957,9 +23475,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
22957
23475
  if (!rhsMatch) continue;
22958
23476
  const rhs = rhsMatch[1];
22959
23477
  for (const san of lineSans) {
22960
- const sanMatch = san.method.match(/^(?:(\w+)\.)?(\w+)\(\)$/);
23478
+ const sanMatch = san.method.match(/(\w+)\(\)$/);
22961
23479
  if (!sanMatch) continue;
22962
- const sanName = sanMatch[1] ? `${sanMatch[1]}.${sanMatch[2]}` : sanMatch[2];
23480
+ const sanName = sanMatch[1];
22963
23481
  if (!rhs.includes(`${sanName}(`)) continue;
22964
23482
  let set = aliasSanitizedFor.get(varName);
22965
23483
  if (!set) {
@@ -23023,6 +23541,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
23023
23541
  if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
23024
23542
  break;
23025
23543
  }
23544
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
23545
+ break;
23546
+ }
23026
23547
  flows.push({
23027
23548
  source_line: source.line,
23028
23549
  sink_line: sink.line,
@@ -23053,6 +23574,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
23053
23574
  if (!colocSources || colocSources.length === 0) continue;
23054
23575
  for (const source of colocSources) {
23055
23576
  if (!canSourceReachSink(source.type, sink.type)) continue;
23577
+ if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
23578
+ continue;
23579
+ }
23056
23580
  if (flows.some(
23057
23581
  (f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
23058
23582
  )) continue;
@@ -26530,6 +27054,28 @@ var JS_ROUTE_METHODS = /* @__PURE__ */ new Set([
26530
27054
  "head",
26531
27055
  "options"
26532
27056
  ]);
27057
+ var SECURITY_MIDDLEWARE_METHODS = /* @__PURE__ */ new Set([
27058
+ // Node helmet (and sub-modules)
27059
+ "helmet",
27060
+ "frameguard",
27061
+ "contentSecurityPolicy",
27062
+ "hsts",
27063
+ "noSniff",
27064
+ "xssFilter",
27065
+ "referrerPolicy",
27066
+ "permittedCrossDomainPolicies",
27067
+ "dnsPrefetchControl",
27068
+ // Spring HttpSecurity builder chain
27069
+ "frameOptions",
27070
+ "headers",
27071
+ "httpStrictTransportSecurity",
27072
+ "contentTypeOptions",
27073
+ "xssProtection",
27074
+ // Flask / Python
27075
+ "Talisman",
27076
+ "Secure"
27077
+ ]);
27078
+ var SECURITY_MIDDLEWARE_ANNOTATIONS_RE = /\b(EnableWebSecurity|SecurityFilterChain|after_request|before_request)\b/;
26533
27079
  var SecurityHeadersPass = class {
26534
27080
  name = "security-headers";
26535
27081
  category = "security";
@@ -26556,12 +27102,14 @@ var SecurityHeadersPass = class {
26556
27102
  list.push(call);
26557
27103
  }
26558
27104
  const hasHandler = detectHandler(graph, calls);
27105
+ const hasGlobalMiddleware = detectGlobalSecurityMiddleware(graph, calls);
26559
27106
  for (const rule of this.rules) {
26560
27107
  const headerKey = rule.header.toLowerCase();
26561
27108
  const writes = writtenHeaders.get(headerKey) ?? [];
26562
27109
  if (rule.kind === "missing") {
26563
27110
  if (writes.length > 0) continue;
26564
27111
  if (rule.requiresHandler !== false && !hasHandler) continue;
27112
+ if (hasGlobalMiddleware) continue;
26565
27113
  ctx.addFinding({
26566
27114
  id: `${rule.rule_id}-${file}`,
26567
27115
  pass: this.name,
@@ -26715,6 +27263,23 @@ function detectHandler(graph, calls) {
26715
27263
  }
26716
27264
  return false;
26717
27265
  }
27266
+ function detectGlobalSecurityMiddleware(graph, calls) {
27267
+ for (const call of calls) {
27268
+ if (SECURITY_MIDDLEWARE_METHODS.has(call.method_name)) return true;
27269
+ if (call.method_name === "use" && call.arguments.length > 0) {
27270
+ const firstArg = call.arguments[0].expression ?? "";
27271
+ if (/\b(helmet|Talisman|secure)\b/.test(firstArg)) return true;
27272
+ }
27273
+ }
27274
+ for (const type of graph.ir.types) {
27275
+ if (type.annotations.some((a) => SECURITY_MIDDLEWARE_ANNOTATIONS_RE.test(a))) return true;
27276
+ for (const method of type.methods) {
27277
+ if (method.annotations.some((a) => SECURITY_MIDDLEWARE_ANNOTATIONS_RE.test(a))) return true;
27278
+ if (/^security[A-Za-z]*FilterChain$/i.test(method.name)) return true;
27279
+ }
27280
+ }
27281
+ return false;
27282
+ }
26718
27283
 
26719
27284
  // src/analysis/passes/scan-secrets-pass.ts
26720
27285
  var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;