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
@@ -10179,11 +10179,14 @@ var DEFAULT_SOURCES = [
10179
10179
  // Rocket
10180
10180
  { method: "param", class: "Request", type: "http_param", severity: "high", return_tainted: true },
10181
10181
  { method: "cookies", class: "Request", type: "http_cookie", severity: "high", return_tainted: true },
10182
- // Axum extractors
10183
- { method: "Json", type: "http_body", severity: "high", return_tainted: true },
10184
- { method: "Query", type: "http_param", severity: "high", return_tainted: true },
10185
- { method: "Path", type: "http_path", severity: "high", return_tainted: true },
10186
- { method: "Form", type: "http_param", severity: "high", return_tainted: true },
10182
+ // Axum extractors — Rust-only. The simple names `Json`/`Query`/`Path`/`Form`
10183
+ // collide with stdlib types in other ecosystems (notably Python's
10184
+ // `pathlib.Path` constructor and `flask.Form`), so they MUST be
10185
+ // language-scoped to Rust to avoid spurious source matches.
10186
+ { method: "Json", type: "http_body", severity: "high", return_tainted: true, languages: ["rust"] },
10187
+ { method: "Query", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
10188
+ { method: "Path", type: "http_path", severity: "high", return_tainted: true, languages: ["rust"] },
10189
+ { method: "Form", type: "http_param", severity: "high", return_tainted: true, languages: ["rust"] },
10187
10190
  // Rust std library
10188
10191
  { method: "var", class: "env", type: "env_input", severity: "medium", return_tainted: true },
10189
10192
  { method: "var_os", class: "env", type: "env_input", severity: "medium", return_tainted: true },
@@ -10386,10 +10389,15 @@ var DEFAULT_SINKS = [
10386
10389
  { method: "PathResource", class: "constructor", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10387
10390
  // Additional resource/file patterns
10388
10391
  { method: "forFile", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10389
- { method: "resolve", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10390
- { method: "resolve", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10391
- { method: "resolveSibling", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10392
- { method: "relativize", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0] },
10392
+ // Java NIO `Path.resolve(other)` joining with an untrusted `other` can
10393
+ // escape the parent directory. Language-scoped to Java because the simple
10394
+ // name `resolve` collides with Python `pathlib.Path.resolve()`
10395
+ // (a canonicalization SANITIZER, no argument), JS `Promise.resolve(...)`,
10396
+ // and Rust `Path::canonicalize` variants. Sprint 9 #48.2.
10397
+ { method: "resolve", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
10398
+ { method: "resolve", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
10399
+ { method: "resolveSibling", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0], languages: ["java"] },
10400
+ { method: "relativize", class: "Path", type: "path_traversal", cwe: "CWE-22", severity: "medium", arg_positions: [0], languages: ["java"] },
10393
10401
  // Static file configuration
10394
10402
  { method: "staticFiles", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
10395
10403
  { method: "setRoot", type: "path_traversal", cwe: "CWE-22", severity: "high", arg_positions: [0] },
@@ -11546,6 +11554,16 @@ var DEFAULT_SANITIZERS = [
11546
11554
  // Returns just filename, strips path
11547
11555
  { method: "canonicalize", removes: ["path_traversal"] },
11548
11556
  // Resolves symlinks and normalizes
11557
+ // Go path sanitizers (#51) — filepath.Base strips directory components
11558
+ // (fully sanitizes), filepath.Clean / path.Clean normalize away ../ segments
11559
+ // (defense-in-depth — mirrors Java getCanonicalPath in this table; the
11560
+ // stricter Clean+HasPrefix guard recognition is tracked separately).
11561
+ // EvalSymlinks is the Go equivalent of Java's Path.toRealPath.
11562
+ { method: "Base", class: "filepath", removes: ["path_traversal"] },
11563
+ { method: "Base", class: "path", removes: ["path_traversal"] },
11564
+ { method: "Clean", class: "filepath", removes: ["path_traversal"] },
11565
+ { method: "Clean", class: "path", removes: ["path_traversal"] },
11566
+ { method: "EvalSymlinks", class: "filepath", removes: ["path_traversal"] },
11549
11567
  // Log Injection sanitizers
11550
11568
  { method: "replace", removes: ["log_injection"] },
11551
11569
  // Used to remove newlines/control chars
@@ -11640,6 +11658,8 @@ var DEFAULT_SANITIZERS = [
11640
11658
  { method: "abspath", class: "os.path", removes: ["path_traversal"] },
11641
11659
  { method: "realpath", class: "path", removes: ["path_traversal"] },
11642
11660
  { method: "abspath", class: "path", removes: ["path_traversal"] },
11661
+ // pathlib.Path.resolve() — canonicalizes path, resolves symlinks (Python 3)
11662
+ { method: "resolve", class: "Path", removes: ["path_traversal"] },
11643
11663
  // Python Type coercion
11644
11664
  { method: "int", removes: ["sql_injection", "command_injection", "xss"] },
11645
11665
  { method: "float", removes: ["sql_injection", "command_injection"] },
@@ -11672,8 +11692,36 @@ var DEFAULT_SANITIZERS = [
11672
11692
  { method: "encode_attribute", class: "html_escape", removes: ["xss"] },
11673
11693
  { method: "escape_html", removes: ["xss"] },
11674
11694
  // Rust Type coercion (parsing)
11675
- { method: "parse", removes: ["sql_injection", "command_injection", "xss"] }
11695
+ { method: "parse", removes: ["sql_injection", "command_injection", "xss"] },
11676
11696
  // str.parse::<i32>()
11697
+ // =========================================================================
11698
+ // Type-cast taint barriers (#57)
11699
+ // Numeric/UUID casts cannot carry a string-injection payload.
11700
+ // =========================================================================
11701
+ // Java numeric parse — Integer.parseInt, Long.parseLong, etc.
11702
+ { method: "parseInt", class: "Integer", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11703
+ { method: "parseLong", class: "Long", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11704
+ { method: "parseFloat", class: "Float", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11705
+ { method: "parseDouble", class: "Double", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11706
+ { method: "parseShort", class: "Short", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11707
+ { method: "parseByte", class: "Byte", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11708
+ // Java UUID parse — UUID.fromString rejects non-UUID strings
11709
+ { method: "fromString", class: "UUID", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11710
+ // JavaScript numeric coercion (Number/parseInt/parseFloat already covered above; add path_traversal/code_injection)
11711
+ { method: "BigInt", removes: ["sql_injection", "nosql_injection", "command_injection", "path_traversal", "code_injection"] },
11712
+ // Go numeric parse — strconv.Atoi, ParseInt, ParseFloat, ParseUint, ParseBool
11713
+ { method: "Atoi", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11714
+ { method: "ParseInt", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11715
+ { method: "ParseFloat", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11716
+ { method: "ParseUint", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11717
+ { method: "ParseBool", class: "strconv", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11718
+ // Go UUID parse
11719
+ { method: "Parse", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11720
+ { method: "MustParse", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11721
+ // Python — int/float already covered above; add bool + UUID/Decimal casts
11722
+ { method: "bool", removes: ["sql_injection", "command_injection", "xss", "code_injection"] },
11723
+ { method: "UUID", class: "uuid", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] },
11724
+ { method: "Decimal", class: "decimal", removes: ["sql_injection", "command_injection", "path_traversal", "code_injection"] }
11677
11725
  ];
11678
11726
  function getDefaultConfig() {
11679
11727
  return {
@@ -11703,7 +11751,7 @@ function analyzeTaint(calls, types, config = getDefaultConfig(), typeHierarchy,
11703
11751
  const sourceLines = code !== void 0 ? code.split("\n") : void 0;
11704
11752
  const sources = findSources(calls, types, config.sources, sourceLines, language);
11705
11753
  const sinks = findSinks(calls, config.sinks, typeHierarchy, language, sourceLines);
11706
- const sanitizers = findSanitizers(calls, types, config.sanitizers);
11754
+ const sanitizers = findSanitizers(calls, types, config.sanitizers, sourceLines);
11707
11755
  return { sources, sinks, sanitizers };
11708
11756
  }
11709
11757
  function attachSourceLineCode(sources, sinks, code) {
@@ -11723,6 +11771,9 @@ function findSources(calls, types, patterns, sourceLines, language) {
11723
11771
  const sources = [];
11724
11772
  for (const call of calls) {
11725
11773
  for (const pattern of patterns) {
11774
+ if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
11775
+ continue;
11776
+ }
11726
11777
  if (matchesSourcePattern(call, pattern)) {
11727
11778
  sources.push({
11728
11779
  type: pattern.type,
@@ -12359,6 +12410,15 @@ function receiverMightBeClass(receiver, className) {
12359
12410
  if (receiver === className) {
12360
12411
  return true;
12361
12412
  }
12413
+ if (receiver.endsWith(")")) {
12414
+ const ctorMatch = receiver.match(/^(\w+)\(/);
12415
+ if (ctorMatch) {
12416
+ const ctorName = ctorMatch[1];
12417
+ if (ctorName === className || ctorName.toLowerCase() === className.toLowerCase()) {
12418
+ return true;
12419
+ }
12420
+ }
12421
+ }
12362
12422
  if (receiver.includes("::")) {
12363
12423
  const scopePrefix = receiver.match(/^(\w+)::/);
12364
12424
  if (scopePrefix) {
@@ -12626,7 +12686,7 @@ function calculateSinkConfidence(call, pattern) {
12626
12686
  }
12627
12687
  return Math.min(confidence, 1);
12628
12688
  }
12629
- function findSanitizers(calls, types, patterns) {
12689
+ function findSanitizers(calls, types, patterns, sourceLines) {
12630
12690
  const sanitizers = [];
12631
12691
  const sanitizerMethods = /* @__PURE__ */ new Set();
12632
12692
  for (const type of types) {
@@ -12636,6 +12696,66 @@ function findSanitizers(calls, types, patterns) {
12636
12696
  }
12637
12697
  }
12638
12698
  }
12699
+ const wrapperSanitizers = /* @__PURE__ */ new Map();
12700
+ for (const type of types) {
12701
+ for (const method of type.methods) {
12702
+ const bodySize = method.end_line - method.start_line;
12703
+ if (bodySize < 0 || bodySize > 2) continue;
12704
+ const paramNames = new Set(method.parameters.map((p) => p.name));
12705
+ if (paramNames.size === 0) continue;
12706
+ const inside = [];
12707
+ for (const c of calls) {
12708
+ if (c.location.line < method.start_line || c.location.line > method.end_line) continue;
12709
+ if (c.method_name === method.name) continue;
12710
+ inside.push(c);
12711
+ }
12712
+ if (inside.length !== 1) continue;
12713
+ const innerCall = inside[0];
12714
+ let matched;
12715
+ for (const pattern of patterns) {
12716
+ if (matchesSanitizerPattern(innerCall, pattern)) {
12717
+ matched = pattern;
12718
+ break;
12719
+ }
12720
+ }
12721
+ if (!matched || !matched.removes || matched.removes.length === 0) continue;
12722
+ let argOk = false;
12723
+ for (const arg of innerCall.arguments) {
12724
+ if (arg.variable && paramNames.has(arg.variable)) {
12725
+ argOk = true;
12726
+ break;
12727
+ }
12728
+ }
12729
+ if (!argOk) continue;
12730
+ if (sourceLines) {
12731
+ const lineText = sourceLines[innerCall.location.line - 1] ?? "";
12732
+ const stripped = lineText.trim();
12733
+ const returnMatch = stripped.match(/^return\s+(?:await\s+)?(.*)$/);
12734
+ if (!returnMatch) continue;
12735
+ const after = returnMatch[1].replace(/;\s*$/, "").trimEnd();
12736
+ const callPrefix = innerCall.receiver ? `${innerCall.receiver}.${innerCall.method_name}(` : `${innerCall.method_name}(`;
12737
+ if (!after.startsWith(callPrefix)) continue;
12738
+ if (!after.endsWith(")")) continue;
12739
+ }
12740
+ const existing = wrapperSanitizers.get(method.name);
12741
+ if (existing) {
12742
+ const set = /* @__PURE__ */ new Set([...existing, ...matched.removes]);
12743
+ wrapperSanitizers.set(method.name, Array.from(set));
12744
+ } else {
12745
+ wrapperSanitizers.set(method.name, [...matched.removes]);
12746
+ }
12747
+ }
12748
+ }
12749
+ for (const call of calls) {
12750
+ const removes = wrapperSanitizers.get(call.method_name);
12751
+ if (!removes) continue;
12752
+ sanitizers.push({
12753
+ type: "derived_wrapper",
12754
+ method: formatSanitizerMethod(call),
12755
+ line: call.location.line,
12756
+ sanitizes: removes
12757
+ });
12758
+ }
12639
12759
  for (const call of calls) {
12640
12760
  if (sanitizerMethods.has(call.method_name)) {
12641
12761
  sanitizers.push({
@@ -12958,6 +13078,15 @@ var CodeGraph = class {
12958
13078
  };
12959
13079
 
12960
13080
  // src/analysis/taint-propagation.ts
13081
+ function buildSanitizersByLine(sanitizers) {
13082
+ const out2 = /* @__PURE__ */ new Map();
13083
+ for (const san of sanitizers) {
13084
+ const existing = out2.get(san.line);
13085
+ if (existing) existing.push(san);
13086
+ else out2.set(san.line, [san]);
13087
+ }
13088
+ return out2;
13089
+ }
12961
13090
  function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
12962
13091
  let graph;
12963
13092
  let sources;
@@ -12993,7 +13122,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
12993
13122
  const defsByLine = graph.defsByLine;
12994
13123
  const usesByLine = graph.usesByLine;
12995
13124
  const callsByLine = graph.callsByLine;
12996
- const sanitizersByLine = graph.sanitizersByLine;
13125
+ const sanitizersByLine = sanitizers.length > 0 ? buildSanitizersByLine(sanitizers) : graph.sanitizersByLine;
12997
13126
  const defById = graph.defById;
12998
13127
  const rawInitialTaint = findInitialTaint(sources, callsByLine, defsByLine);
12999
13128
  const initialTaint = rawInitialTaint.filter((tv) => {
@@ -13666,7 +13795,32 @@ var SANITIZER_METHODS = /* @__PURE__ */ new Set([
13666
13795
  "validatePath",
13667
13796
  "validateCityName",
13668
13797
  "validateInput",
13669
- "sanitizeInput"
13798
+ "sanitizeInput",
13799
+ // Type-cast barriers (#57) — numeric/boolean casts cannot carry a string
13800
+ // injection payload. Conservative whitelist; ambiguous names like `valueOf`,
13801
+ // `Parse`, `fromString` are intentionally excluded.
13802
+ // Java
13803
+ "parseInt",
13804
+ "parseLong",
13805
+ "parseFloat",
13806
+ "parseDouble",
13807
+ "parseShort",
13808
+ "parseByte",
13809
+ "fromString",
13810
+ // UUID.fromString — parses strict UUID format, rejects injection
13811
+ // JS/TS (parseInt/parseFloat covered above)
13812
+ "Number",
13813
+ "BigInt",
13814
+ // Go
13815
+ "Atoi",
13816
+ "ParseInt",
13817
+ "ParseFloat",
13818
+ "ParseUint",
13819
+ "ParseBool",
13820
+ // Python
13821
+ "int",
13822
+ "float",
13823
+ "bool"
13670
13824
  ]);
13671
13825
  var ANTI_SANITIZER_METHODS = /* @__PURE__ */ new Set([
13672
13826
  // URL decoding (reverses URL encoding)
@@ -13796,6 +13950,10 @@ var ConstantPropagator = class _ConstantPropagator {
13796
13950
  inConstructor = false;
13797
13951
  // Map constructor parameter names to their positions (0-indexed)
13798
13952
  constructorParamPositions = /* @__PURE__ */ new Map();
13953
+ // Sprint 9 #58.1 — names of `static final Pattern` fields whose compiled
13954
+ // regex is strict-anchored (provably matches a bounded character set).
13955
+ // Populated lazily on first access via `getSafePatternFields()`.
13956
+ safePatternFieldsCache = null;
13799
13957
  /**
13800
13958
  * Analyze source code and build constant propagation state.
13801
13959
  */
@@ -13829,6 +13987,7 @@ var ConstantPropagator = class _ConstantPropagator {
13829
13987
  this.currentClassName = null;
13830
13988
  this.inConstructor = false;
13831
13989
  this.constructorParamPositions.clear();
13990
+ this.safePatternFieldsCache = null;
13832
13991
  this.collectClassFields(tree.rootNode);
13833
13992
  for (const methodName of sanitizerMethods) {
13834
13993
  this.methodReturnsSanitized.add(methodName);
@@ -13838,6 +13997,7 @@ var ConstantPropagator = class _ConstantPropagator {
13838
13997
  (name2) => this.lookupSymbol(name2)
13839
13998
  );
13840
13999
  this.analyzeMethodReturns(tree.rootNode);
14000
+ this.seedPythonModuleConstants(tree.rootNode);
13841
14001
  this.visit(tree.rootNode);
13842
14002
  this.refineTaintFromConstants();
13843
14003
  const resultTainted = new Set(this.tainted);
@@ -14198,6 +14358,162 @@ var ConstantPropagator = class _ConstantPropagator {
14198
14358
  }
14199
14359
  }
14200
14360
  }
14361
+ /**
14362
+ * Sprint 9 #55 — seed the symbol table with Python module-level constant
14363
+ * assignments. Walks only direct children of the `module` root and adds
14364
+ * `IDENT = <primitive literal>` to `symbols` so `if IDENT:` guards inside
14365
+ * downstream functions can be folded to dead code.
14366
+ *
14367
+ * Recognized literal RHS kinds: `true`/`false` (booleans), integer/float
14368
+ * literals, string literals. The ExpressionEvaluator already understands
14369
+ * each via the same lookup callback; we just need the symbol present.
14370
+ */
14371
+ /**
14372
+ * Sprint 9 #55 — gate `field_declaration` folding to primitive literals.
14373
+ *
14374
+ * The deep-nesting regression (cognium-ai#88) constructs a Java
14375
+ * `static final String hyphenData = "a" + "b" + ... (10k segments)` at the
14376
+ * class level. `handleVariableDeclaration` would otherwise dispatch
14377
+ * `evaluateExpression` on the deeply nested binary AST and blow the V8
14378
+ * stack. The dead-code-by-const-guard pattern (`if (DEBUG)`) only requires
14379
+ * `boolean`/`integer`/`string` (single-literal) RHS folding, so restrict
14380
+ * to those node types.
14381
+ */
14382
+ fieldDeclHasPrimitiveLiteralValue(node) {
14383
+ const primitive = /* @__PURE__ */ new Set([
14384
+ // Java literal node types
14385
+ "true",
14386
+ "false",
14387
+ "null_literal",
14388
+ "decimal_integer_literal",
14389
+ "hex_integer_literal",
14390
+ "octal_integer_literal",
14391
+ "binary_integer_literal",
14392
+ "decimal_floating_point_literal",
14393
+ "hex_floating_point_literal",
14394
+ "character_literal",
14395
+ "string_literal",
14396
+ // JS/TS literal node types (defensive, in case other langs reuse it)
14397
+ "number",
14398
+ "string"
14399
+ ]);
14400
+ for (const child of node.children) {
14401
+ if (child.type !== "variable_declarator") continue;
14402
+ const value = child.childForFieldName("value");
14403
+ if (!value) continue;
14404
+ if (!primitive.has(value.type)) return false;
14405
+ }
14406
+ return true;
14407
+ }
14408
+ /**
14409
+ * Sprint 9 #58.1 — collect the set of class-level `Pattern` field names
14410
+ * whose compiled regex is strict-anchored, i.e. provably matches a
14411
+ * bounded character set with no wildcard escape. A subsequent
14412
+ * `if (!FIELD.matcher(var).matches()) throw ...;` guard then proves
14413
+ * `var` is sanitized after the if.
14414
+ *
14415
+ * Recognized initializer shapes (scanned via source-text regex to avoid
14416
+ * threading another AST walk):
14417
+ * `static final Pattern FIELD = Pattern.compile("regex");`
14418
+ *
14419
+ * Strict-anchored regex criteria:
14420
+ * - starts with `^` and ends with `$`
14421
+ * - after stripping `[...]` character classes, must not contain `.` or
14422
+ * `|` (a `.` could match anything; `|` admits an arbitrary alternative)
14423
+ */
14424
+ getSafePatternFields() {
14425
+ if (this.safePatternFieldsCache !== null) return this.safePatternFieldsCache;
14426
+ const set = /* @__PURE__ */ new Set();
14427
+ 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;
14428
+ let m;
14429
+ while ((m = re.exec(this.source)) !== null) {
14430
+ const name2 = m[1];
14431
+ const regex = m[2];
14432
+ if (this.isStrictAnchoredRegex(regex)) set.add(name2);
14433
+ }
14434
+ this.safePatternFieldsCache = set;
14435
+ return set;
14436
+ }
14437
+ isStrictAnchoredRegex(re) {
14438
+ if (!re.startsWith("^") || !re.endsWith("$")) return false;
14439
+ const stripped = re.replace(/\[(?:[^\]\\]|\\.)*\]/g, "");
14440
+ const cleaned = stripped.replace(/\\./g, "");
14441
+ if (cleaned.includes(".")) return false;
14442
+ if (cleaned.includes("|")) return false;
14443
+ return true;
14444
+ }
14445
+ /**
14446
+ * Sprint 9 #58.1 — detect the regex-allowlist guard pattern.
14447
+ *
14448
+ * if (!SAFE_NAME.matcher(var).matches()) { throw ...; }
14449
+ *
14450
+ * Returns the guarded variable name if the pattern matches AND
14451
+ * `SAFE_NAME` is a recognized strict-anchored Pattern field, otherwise
14452
+ * null. Caller drops the variable from `tainted` after the if-block.
14453
+ */
14454
+ detectRegexAllowlistGuard(condition, consequence) {
14455
+ if (!consequence) return null;
14456
+ let condText = getNodeText2(condition, this.source).replace(/\s+/g, "");
14457
+ while (condText.startsWith("(") && condText.endsWith(")")) {
14458
+ const inner = condText.slice(1, -1);
14459
+ let depth = 0;
14460
+ let balanced = true;
14461
+ for (let i2 = 0; i2 < inner.length; i2++) {
14462
+ if (inner[i2] === "(") depth++;
14463
+ else if (inner[i2] === ")") depth--;
14464
+ if (depth < 0) {
14465
+ balanced = false;
14466
+ break;
14467
+ }
14468
+ }
14469
+ if (!balanced || depth !== 0) break;
14470
+ condText = inner;
14471
+ }
14472
+ const m = condText.match(/^!(\w+)\.matcher\((\w+)\)\.matches\(\)$/);
14473
+ if (!m) return null;
14474
+ const patternName = m[1];
14475
+ const varName = m[2];
14476
+ if (!this.getSafePatternFields().has(patternName)) return null;
14477
+ if (!this.consequenceContainsThrow(consequence)) return null;
14478
+ return varName;
14479
+ }
14480
+ consequenceContainsThrow(node) {
14481
+ if (node.type === "throw_statement") return true;
14482
+ const stack = [node];
14483
+ while (stack.length > 0) {
14484
+ const n = stack.pop();
14485
+ if (!n) continue;
14486
+ if (n.type === "throw_statement") return true;
14487
+ if (n.type === "if_statement" || n.type === "switch_statement") continue;
14488
+ for (const c of n.children) stack.push(c);
14489
+ }
14490
+ return false;
14491
+ }
14492
+ seedPythonModuleConstants(root) {
14493
+ if (root.type !== "module") return;
14494
+ for (const child of root.children) {
14495
+ const target = child.type === "assignment" ? child : child.type === "expression_statement" && child.children.length > 0 ? child.children[0] : null;
14496
+ if (!target || target.type !== "assignment") continue;
14497
+ const left = target.childForFieldName("left");
14498
+ const right = target.childForFieldName("right");
14499
+ if (!left || !right) continue;
14500
+ if (left.type !== "identifier") continue;
14501
+ const allowed = /* @__PURE__ */ new Set([
14502
+ "true",
14503
+ "false",
14504
+ "none",
14505
+ "integer",
14506
+ "float",
14507
+ "string"
14508
+ ]);
14509
+ if (!allowed.has(right.type)) continue;
14510
+ const name2 = getNodeText2(left, this.source);
14511
+ if (!name2) continue;
14512
+ const value = this.evaluateExpression(right);
14513
+ if (!isKnown(value)) continue;
14514
+ this.symbols.set(name2, value);
14515
+ }
14516
+ }
14201
14517
  findAllMethods(node) {
14202
14518
  const methods = [];
14203
14519
  const stack = [node];
@@ -14292,6 +14608,11 @@ var ConstantPropagator = class _ConstantPropagator {
14292
14608
  case "local_variable_declaration":
14293
14609
  this.handleVariableDeclaration(node);
14294
14610
  return false;
14611
+ case "field_declaration":
14612
+ if (this.fieldDeclHasPrimitiveLiteralValue(node)) {
14613
+ this.handleVariableDeclaration(node);
14614
+ }
14615
+ return false;
14295
14616
  case "assignment_expression":
14296
14617
  this.handleAssignment(node);
14297
14618
  return false;
@@ -14782,6 +15103,16 @@ var ConstantPropagator = class _ConstantPropagator {
14782
15103
  }
14783
15104
  this.inConditionalBranch = wasInConditional;
14784
15105
  this.tainted = /* @__PURE__ */ new Set([...taintedBefore, ...taintedAfterThen, ...taintedAfterElse]);
15106
+ const guardedVar = this.detectRegexAllowlistGuard(condition, consequence);
15107
+ if (guardedVar) {
15108
+ this.tainted.delete(guardedVar);
15109
+ this.sanitizedVars.add(guardedVar);
15110
+ const scoped = this.getScopedName(guardedVar);
15111
+ if (scoped !== guardedVar) {
15112
+ this.tainted.delete(scoped);
15113
+ this.sanitizedVars.add(scoped);
15114
+ }
15115
+ }
14785
15116
  }
14786
15117
  }
14787
15118
  /**
@@ -14972,17 +15303,33 @@ var ConstantPropagator = class _ConstantPropagator {
14972
15303
  /**
14973
15304
  * Check if an expression is a call to a sanitizer method.
14974
15305
  * This includes both built-in sanitizers and @sanitizer annotated methods.
15306
+ * Handles Java (`method_invocation`), Go/JS/TS (`call_expression`), and
15307
+ * Python (`call`) AST shapes.
14975
15308
  */
14976
15309
  isSanitizerMethodCall(node) {
14977
- if (node.type !== "method_invocation") {
14978
- return false;
15310
+ const methodName = this.extractCallName(node);
15311
+ if (!methodName) return false;
15312
+ return SANITIZER_METHODS.has(methodName) || this.methodReturnsSanitized.has(methodName);
15313
+ }
15314
+ /**
15315
+ * Extract the trailing method/function name from any call node shape:
15316
+ * Java `method_invocation` — name field
15317
+ * Go/JS `call_expression` — function field (identifier or selector/member)
15318
+ * Python `call` — function field (identifier or attribute)
15319
+ */
15320
+ extractCallName(node) {
15321
+ let fnNode = null;
15322
+ if (node.type === "method_invocation") {
15323
+ fnNode = node.childForFieldName("name");
15324
+ } else if (node.type === "call_expression" || node.type === "call") {
15325
+ fnNode = node.childForFieldName("function");
14979
15326
  }
14980
- const nameNode = node.childForFieldName("name");
14981
- if (!nameNode) {
14982
- return false;
15327
+ if (!fnNode) return null;
15328
+ if (fnNode.type === "selector_expression" || fnNode.type === "member_expression" || fnNode.type === "attribute") {
15329
+ const tail = fnNode.childForFieldName("field") || fnNode.childForFieldName("property") || fnNode.childForFieldName("attribute");
15330
+ if (tail) return getNodeText2(tail, this.source);
14983
15331
  }
14984
- const methodName = getNodeText2(nameNode, this.source);
14985
- return SANITIZER_METHODS.has(methodName) || this.methodReturnsSanitized.has(methodName);
15332
+ return getNodeText2(fnNode, this.source);
14986
15333
  }
14987
15334
  /**
14988
15335
  * Check if an expression is a call to an anti-sanitizer method.
@@ -17,6 +17,13 @@ export interface SourcePattern {
17
17
  return_tainted?: boolean;
18
18
  param_tainted?: boolean;
19
19
  property_tainted?: boolean;
20
+ /**
21
+ * Restrict the pattern to specific source languages. When omitted, the
22
+ * pattern matches regardless of language. Use this for sources whose
23
+ * method name collides across language ecosystems (e.g. Rust Axum's
24
+ * `Path<T>` extractor vs Python's `pathlib.Path` constructor).
25
+ */
26
+ languages?: SupportedLanguage[];
20
27
  note?: string;
21
28
  }
22
29
  export interface SinkConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAMhG,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAE5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAIhB,UAAU,CAAC,EAAE,MAAM,CAAC;IAIpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IAGnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAChC;;;;;;;;;OASG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,UAAU,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAMD;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,KAAK,EAAE,UAAU,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,EAAE,QAAQ,CAAC;IACnB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,IAAI,EAAE,SAAS,GAAG,YAAY,GAAG,cAAc,CAAC;IAChD;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/types/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,QAAQ,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAMhG,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAE5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAIhB,UAAU,CAAC,EAAE,MAAM,CAAC;IAIpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC;IAGnB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;;OAKG;IACH,SAAS,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAEhC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,gBAAgB,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,QAAQ,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAChC;;;;;;;;;OASG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,QAAQ,EAAE,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAMD,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,UAAU,EAAE,gBAAgB,EAAE,CAAC;CAChC;AAMD;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,GAAG,EAAE,MAAM,CAAC;IACZ,0DAA0D;IAC1D,KAAK,EAAE,UAAU,CAAC;IAClB,+DAA+D;IAC/D,QAAQ,EAAE,QAAQ,CAAC;IACnB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,IAAI,EAAE,SAAS,GAAG,YAAY,GAAG,cAAc,CAAC;IAChD;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,IAAI,CAAC,EAAE,MAAM,CAAC;CACf"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circle-ir",
3
- "version": "3.57.0",
3
+ "version": "3.58.0",
4
4
  "description": "High-performance Static Application Security Testing (SAST) library for detecting security vulnerabilities through taint analysis",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",