circle-ir 3.57.0 → 3.58.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 +55 -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 +500 -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.
@@ -22110,25 +22457,42 @@ function findBashTaintSources(sourceCode, dfg) {
22110
22457
  const sources = [];
22111
22458
  const lines = sourceCode.split("\n");
22112
22459
  const definedVars = new Set(dfg.defs.filter((d) => d.kind === "local").map((d) => d.variable));
22460
+ const fnHeaderRe = /^\s*(?:function\s+)?[A-Za-z_][\w-]*\s*\(\s*\)\s*\{?\s*$|^\s*function\s+[A-Za-z_][\w-]*\s*\{?\s*$/;
22461
+ let braceDepth = 0;
22113
22462
  for (let i2 = 0; i2 < lines.length; i2++) {
22114
22463
  const line = lines[i2];
22115
22464
  const trimmed = line.trim();
22116
22465
  const lineNumber = i2 + 1;
22117
22466
  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
- });
22467
+ const insideFunction = braceDepth > 0;
22468
+ if (!insideFunction) {
22469
+ const positionalRe = /\$([1-9@*])|\$\{([1-9@*])\}/g;
22470
+ let m;
22471
+ while ((m = positionalRe.exec(line)) !== null) {
22472
+ const param = m[1] ?? m[2];
22473
+ const alreadyExists = sources.some((s) => s.line === lineNumber && s.variable === param);
22474
+ if (!alreadyExists) {
22475
+ sources.push({
22476
+ type: "io_input",
22477
+ location: `positional parameter $${param}`,
22478
+ severity: "high",
22479
+ line: lineNumber,
22480
+ confidence: 1,
22481
+ variable: param
22482
+ });
22483
+ }
22484
+ }
22485
+ }
22486
+ if (fnHeaderRe.test(line) || /^\s*[A-Za-z_][\w-]*\s*\(\s*\)\s*\{/.test(line)) {
22487
+ const openBracesOnLine = (line.match(/\{/g) ?? []).length;
22488
+ const closeBracesOnLine = (line.match(/\}/g) ?? []).length;
22489
+ braceDepth += openBracesOnLine - closeBracesOnLine;
22490
+ } else {
22491
+ if (braceDepth > 0) {
22492
+ const openBracesOnLine = (line.match(/\{/g) ?? []).length;
22493
+ const closeBracesOnLine = (line.match(/\}/g) ?? []).length;
22494
+ braceDepth += openBracesOnLine - closeBracesOnLine;
22495
+ if (braceDepth < 0) braceDepth = 0;
22132
22496
  }
22133
22497
  }
22134
22498
  const cmdSubAssign = trimmed.match(/^(\w+)=\$\((\w+)\s/);
@@ -22550,10 +22914,14 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
22550
22914
  const callsAtSink = callsByLine.get(sink.line) ?? [];
22551
22915
  const isInSynchronizedBlock = synchronizedLines?.has(sink.line) ?? false;
22552
22916
  const relevantCalls = sink.method ? callsAtSink.filter((c) => c.method_name === sink.method) : callsAtSink;
22917
+ const trustArgPositions = language !== "bash" && language !== "shell";
22553
22918
  for (const call of relevantCalls) {
22554
22919
  let allArgsAreClean = true;
22920
+ let dangerousArgCount = 0;
22555
22921
  const methodName = call.in_method;
22556
22922
  for (const arg of call.arguments) {
22923
+ if (trustArgPositions && sink.argPositions && sink.argPositions.length > 0 && !sink.argPositions.includes(arg.position)) continue;
22924
+ dangerousArgCount++;
22557
22925
  if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null) continue;
22558
22926
  if (arg.variable && !arg.expression?.includes("[")) {
22559
22927
  const varName = arg.variable;
@@ -22576,7 +22944,7 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
22576
22944
  allArgsAreClean = false;
22577
22945
  }
22578
22946
  }
22579
- if (allArgsAreClean && call.arguments.length > 0) return false;
22947
+ if (allArgsAreClean && dangerousArgCount > 0) return false;
22580
22948
  }
22581
22949
  return true;
22582
22950
  });
@@ -22668,7 +23036,7 @@ var TaintPropagationPass = class {
22668
23036
  flows.push(f);
22669
23037
  }
22670
23038
  }
22671
- const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines) ?? [];
23039
+ const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code) ?? [];
22672
23040
  for (const f of collectionFlows) {
22673
23041
  if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) continue;
22674
23042
  const flowForCheck = {
@@ -22687,13 +23055,13 @@ var TaintPropagationPass = class {
22687
23055
  if (isFP) continue;
22688
23056
  flows.push(f);
22689
23057
  }
22690
- const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines) ?? [];
23058
+ const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
22691
23059
  for (const f of paramFlows) {
22692
23060
  if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line)) {
22693
23061
  flows.push(f);
22694
23062
  }
22695
23063
  }
22696
- const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, ctx.code, ctx.language) ?? [];
23064
+ const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language) ?? [];
22697
23065
  for (const f of exprScanFlows) {
22698
23066
  if (flows.some(
22699
23067
  (x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
@@ -22714,10 +23082,21 @@ var TaintPropagationPass = class {
22714
23082
  if (isFP) continue;
22715
23083
  flows.push(f);
22716
23084
  }
22717
- return { flows };
23085
+ const sanitizedNames = constProp.sanitizedVars;
23086
+ const finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
23087
+ if (f.path.length === 0) return true;
23088
+ const sourceVar = f.path[0].variable;
23089
+ if (!sourceVar) return true;
23090
+ if (sanitizedNames.has(sourceVar)) return false;
23091
+ for (const s of sanitizedNames) {
23092
+ if (s.endsWith(`:${sourceVar}`)) return false;
23093
+ }
23094
+ return true;
23095
+ });
23096
+ return { flows: finalFlows };
22718
23097
  }
22719
23098
  };
22720
- function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines) {
23099
+ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLines, code) {
22721
23100
  const flows = [];
22722
23101
  const callsByLine = /* @__PURE__ */ new Map();
22723
23102
  for (const call of calls) {
@@ -22739,6 +23118,9 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
22739
23118
  if (taintedVars.has(varName) || taintedVars.has(scopedName)) {
22740
23119
  const source = sources[0];
22741
23120
  if (source) {
23121
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, varName, source.line, sink.line)) {
23122
+ continue;
23123
+ }
22742
23124
  flows.push({
22743
23125
  source_line: source.line,
22744
23126
  sink_line: sink.line,
@@ -22773,6 +23155,9 @@ function detectCollectionFlows(calls, sources, sinks, taintedVars, unreachableLi
22773
23155
  if (taintedVars.has(collectionVar) || taintedVars.has(scopedCollection)) {
22774
23156
  const source = sources[0];
22775
23157
  if (source) {
23158
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, collectionVar, source.line, sink.line)) {
23159
+ continue;
23160
+ }
22776
23161
  flows.push({
22777
23162
  source_line: source.line,
22778
23163
  sink_line: sink.line,
@@ -22842,7 +23227,7 @@ function detectArrayElementFlows(calls, sources, sinks, taintedArrayElements, un
22842
23227
  }
22843
23228
  return flows;
22844
23229
  }
22845
- function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines) {
23230
+ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines, tainted, code) {
22846
23231
  const flows = [];
22847
23232
  const paramSourcesByMethod = /* @__PURE__ */ new Map();
22848
23233
  for (const source of sources) {
@@ -22884,6 +23269,9 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
22884
23269
  if (paramSource) {
22885
23270
  const exists = flows.some((f) => f.source_line === paramSource.line && f.sink_line === sink.line);
22886
23271
  if (!exists) {
23272
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, arg.variable, paramSource.line, sink.line)) {
23273
+ continue;
23274
+ }
22887
23275
  flows.push({
22888
23276
  source_line: paramSource.line,
22889
23277
  sink_line: sink.line,
@@ -22905,7 +23293,27 @@ function detectParameterSinkFlows(types, calls, sources, sinks, unreachableLines
22905
23293
  void types;
22906
23294
  return flows;
22907
23295
  }
22908
- function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, code, language) {
23296
+ function isReassignedToLiteralBetween(code, variable, srcLine, sinkLine) {
23297
+ if (!variable || sinkLine - srcLine < 2) return false;
23298
+ if (!/^[A-Za-z_][\w]*$/.test(variable)) return false;
23299
+ const lines = code.split("\n");
23300
+ const lo = Math.max(0, srcLine);
23301
+ const hi = Math.min(lines.length, sinkLine - 1);
23302
+ const strLit = `(?:"[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|'[^'\\\\]*(?:\\\\.[^'\\\\]*)*'|\`[^\`\\\\]*(?:\\\\.[^\`\\\\]*)*\`)`;
23303
+ const reNaked = new RegExp(
23304
+ `^\\s*${variable}\\s*(?::?=)\\s*${strLit}\\s*;?\\s*$`
23305
+ );
23306
+ const reGuarded = new RegExp(
23307
+ `^\\s*if\\b.*\\b${variable}\\s*=\\s*${strLit}\\s*;?\\s*$`
23308
+ );
23309
+ for (let i2 = lo; i2 < hi; i2++) {
23310
+ const line = lines[i2];
23311
+ if (!line) continue;
23312
+ if (reNaked.test(line) || reGuarded.test(line)) return true;
23313
+ }
23314
+ return false;
23315
+ }
23316
+ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachableLines, tainted, code, language) {
22909
23317
  const flows = [];
22910
23318
  const sourcesWithVar = sources.filter(
22911
23319
  (s) => typeof s.variable === "string" && s.variable.length > 0
@@ -22957,9 +23365,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
22957
23365
  if (!rhsMatch) continue;
22958
23366
  const rhs = rhsMatch[1];
22959
23367
  for (const san of lineSans) {
22960
- const sanMatch = san.method.match(/^(?:(\w+)\.)?(\w+)\(\)$/);
23368
+ const sanMatch = san.method.match(/(\w+)\(\)$/);
22961
23369
  if (!sanMatch) continue;
22962
- const sanName = sanMatch[1] ? `${sanMatch[1]}.${sanMatch[2]}` : sanMatch[2];
23370
+ const sanName = sanMatch[1];
22963
23371
  if (!rhs.includes(`${sanName}(`)) continue;
22964
23372
  let set = aliasSanitizedFor.get(varName);
22965
23373
  if (!set) {
@@ -23023,6 +23431,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
23023
23431
  if (aliasSanitizedFor.get(source.variable)?.has(sink.type)) {
23024
23432
  break;
23025
23433
  }
23434
+ if (typeof code === "string" && isReassignedToLiteralBetween(code, source.variable, source.line, sink.line)) {
23435
+ break;
23436
+ }
23026
23437
  flows.push({
23027
23438
  source_line: source.line,
23028
23439
  sink_line: sink.line,
@@ -23053,6 +23464,9 @@ function detectExpressionScanFlows(calls, sources, sinks, sanitizers, unreachabl
23053
23464
  if (!colocSources || colocSources.length === 0) continue;
23054
23465
  for (const source of colocSources) {
23055
23466
  if (!canSourceReachSink(source.type, sink.type)) continue;
23467
+ if (source.type === "file_input" && sink.type === "path_traversal" && sink.method && source.location.includes(`${sink.method}(`)) {
23468
+ continue;
23469
+ }
23056
23470
  if (flows.some(
23057
23471
  (f) => f.source_line === source.line && f.sink_line === sink.line && f.sink_type === sink.type
23058
23472
  )) continue;
@@ -26530,6 +26944,28 @@ var JS_ROUTE_METHODS = /* @__PURE__ */ new Set([
26530
26944
  "head",
26531
26945
  "options"
26532
26946
  ]);
26947
+ var SECURITY_MIDDLEWARE_METHODS = /* @__PURE__ */ new Set([
26948
+ // Node helmet (and sub-modules)
26949
+ "helmet",
26950
+ "frameguard",
26951
+ "contentSecurityPolicy",
26952
+ "hsts",
26953
+ "noSniff",
26954
+ "xssFilter",
26955
+ "referrerPolicy",
26956
+ "permittedCrossDomainPolicies",
26957
+ "dnsPrefetchControl",
26958
+ // Spring HttpSecurity builder chain
26959
+ "frameOptions",
26960
+ "headers",
26961
+ "httpStrictTransportSecurity",
26962
+ "contentTypeOptions",
26963
+ "xssProtection",
26964
+ // Flask / Python
26965
+ "Talisman",
26966
+ "Secure"
26967
+ ]);
26968
+ var SECURITY_MIDDLEWARE_ANNOTATIONS_RE = /\b(EnableWebSecurity|SecurityFilterChain|after_request|before_request)\b/;
26533
26969
  var SecurityHeadersPass = class {
26534
26970
  name = "security-headers";
26535
26971
  category = "security";
@@ -26556,12 +26992,14 @@ var SecurityHeadersPass = class {
26556
26992
  list.push(call);
26557
26993
  }
26558
26994
  const hasHandler = detectHandler(graph, calls);
26995
+ const hasGlobalMiddleware = detectGlobalSecurityMiddleware(graph, calls);
26559
26996
  for (const rule of this.rules) {
26560
26997
  const headerKey = rule.header.toLowerCase();
26561
26998
  const writes = writtenHeaders.get(headerKey) ?? [];
26562
26999
  if (rule.kind === "missing") {
26563
27000
  if (writes.length > 0) continue;
26564
27001
  if (rule.requiresHandler !== false && !hasHandler) continue;
27002
+ if (hasGlobalMiddleware) continue;
26565
27003
  ctx.addFinding({
26566
27004
  id: `${rule.rule_id}-${file}`,
26567
27005
  pass: this.name,
@@ -26715,6 +27153,23 @@ function detectHandler(graph, calls) {
26715
27153
  }
26716
27154
  return false;
26717
27155
  }
27156
+ function detectGlobalSecurityMiddleware(graph, calls) {
27157
+ for (const call of calls) {
27158
+ if (SECURITY_MIDDLEWARE_METHODS.has(call.method_name)) return true;
27159
+ if (call.method_name === "use" && call.arguments.length > 0) {
27160
+ const firstArg = call.arguments[0].expression ?? "";
27161
+ if (/\b(helmet|Talisman|secure)\b/.test(firstArg)) return true;
27162
+ }
27163
+ }
27164
+ for (const type of graph.ir.types) {
27165
+ if (type.annotations.some((a) => SECURITY_MIDDLEWARE_ANNOTATIONS_RE.test(a))) return true;
27166
+ for (const method of type.methods) {
27167
+ if (method.annotations.some((a) => SECURITY_MIDDLEWARE_ANNOTATIONS_RE.test(a))) return true;
27168
+ if (/^security[A-Za-z]*FilterChain$/i.test(method.name)) return true;
27169
+ }
27170
+ }
27171
+ return false;
27172
+ }
26718
27173
 
26719
27174
  // src/analysis/passes/scan-secrets-pass.ts
26720
27175
  var TEST_PATH_RE3 = /(?:^|[\\/])(?:test|tests|spec|specs|__tests?__|__mocks?__|fixtures?|testdata)(?:[\\/]|$)/i;