circle-ir 3.168.0 → 3.173.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 (40) hide show
  1. package/dist/analysis/config-loader.d.ts.map +1 -1
  2. package/dist/analysis/config-loader.js +79 -0
  3. package/dist/analysis/config-loader.js.map +1 -1
  4. package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
  5. package/dist/analysis/passes/cross-file-pass.js +3 -2
  6. package/dist/analysis/passes/cross-file-pass.js.map +1 -1
  7. package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
  8. package/dist/analysis/passes/interprocedural-pass.js +3 -2
  9. package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
  10. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  11. package/dist/analysis/passes/sink-filter-pass.js +2 -1
  12. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  13. package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
  14. package/dist/analysis/passes/taint-propagation-pass.js +29 -15
  15. package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
  16. package/dist/analysis/sanitizer-index.d.ts +34 -0
  17. package/dist/analysis/sanitizer-index.d.ts.map +1 -0
  18. package/dist/analysis/sanitizer-index.js +48 -0
  19. package/dist/analysis/sanitizer-index.js.map +1 -0
  20. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  21. package/dist/analysis/taint-matcher.js +93 -45
  22. package/dist/analysis/taint-matcher.js.map +1 -1
  23. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  24. package/dist/analysis/taint-propagation.js +5 -2
  25. package/dist/analysis/taint-propagation.js.map +1 -1
  26. package/dist/analyzer.d.ts.map +1 -1
  27. package/dist/analyzer.js +51 -2
  28. package/dist/analyzer.js.map +1 -1
  29. package/dist/browser/circle-ir.js +257 -94
  30. package/dist/core/circle-ir-core.cjs +193 -72
  31. package/dist/core/circle-ir-core.js +193 -72
  32. package/dist/core/extractors/cfg.d.ts +11 -1
  33. package/dist/core/extractors/cfg.d.ts.map +1 -1
  34. package/dist/core/extractors/cfg.js +18 -9
  35. package/dist/core/extractors/cfg.js.map +1 -1
  36. package/dist/core/extractors/imports.d.ts +9 -1
  37. package/dist/core/extractors/imports.d.ts.map +1 -1
  38. package/dist/core/extractors/imports.js +29 -22
  39. package/dist/core/extractors/imports.js.map +1 -1
  40. package/package.json +1 -1
@@ -7497,33 +7497,33 @@ function detectLanguage2(tree) {
7497
7497
  if (pythonScore > jsScore && pythonScore > javaScore) return "python";
7498
7498
  return jsScore > javaScore ? "javascript" : "java";
7499
7499
  }
7500
- function extractImports(tree, language) {
7500
+ function extractImports(tree, language, cache) {
7501
7501
  const effectiveLanguage = language ?? detectLanguage2(tree);
7502
7502
  const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
7503
7503
  const isPython = effectiveLanguage === "python";
7504
7504
  const isRust = effectiveLanguage === "rust";
7505
7505
  if (effectiveLanguage === "go") {
7506
- return extractGoImports(tree);
7506
+ return extractGoImports(tree, cache);
7507
7507
  }
7508
7508
  if (isRust) {
7509
- return extractRustImports(tree);
7509
+ return extractRustImports(tree, cache);
7510
7510
  }
7511
7511
  if (isPython) {
7512
- return extractPythonImports(tree);
7512
+ return extractPythonImports(tree, cache);
7513
7513
  }
7514
7514
  if (isJavaScript) {
7515
- return extractJavaScriptImports(tree);
7515
+ return extractJavaScriptImports(tree, cache);
7516
7516
  }
7517
- return extractJavaImports(tree);
7517
+ return extractJavaImports(tree, cache);
7518
7518
  }
7519
- function extractJavaScriptImports(tree) {
7519
+ function extractJavaScriptImports(tree, cache) {
7520
7520
  const imports = [];
7521
- const importStatements = findNodes(tree.rootNode, "import_statement");
7521
+ const importStatements = getNodesFromCache(tree.rootNode, "import_statement", cache);
7522
7522
  for (const importStmt of importStatements) {
7523
7523
  const importInfos = extractJSImportInfo(importStmt);
7524
7524
  imports.push(...importInfos);
7525
7525
  }
7526
- const exportStatements = findNodes(tree.rootNode, "export_statement");
7526
+ const exportStatements = getNodesFromCache(tree.rootNode, "export_statement", cache);
7527
7527
  for (const exportStmt of exportStatements) {
7528
7528
  const sourceNode = exportStmt.childForFieldName("source");
7529
7529
  if (!sourceNode) continue;
@@ -7537,13 +7537,13 @@ function extractJavaScriptImports(tree) {
7537
7537
  line_number: exportStmt.startPosition.row + 1
7538
7538
  });
7539
7539
  }
7540
- const requireCalls = findRequireCalls(tree);
7540
+ const requireCalls = findRequireCalls(tree, cache);
7541
7541
  imports.push(...requireCalls);
7542
7542
  return imports;
7543
7543
  }
7544
- function extractJavaImports(tree) {
7544
+ function extractJavaImports(tree, cache) {
7545
7545
  const imports = [];
7546
- const importDecls = findNodes(tree.rootNode, "import_declaration");
7546
+ const importDecls = getNodesFromCache(tree.rootNode, "import_declaration", cache);
7547
7547
  for (const importDecl of importDecls) {
7548
7548
  const importInfo = extractJavaImportInfo(importDecl);
7549
7549
  if (importInfo) {
@@ -7691,9 +7691,9 @@ function extractJSImportInfo(node) {
7691
7691
  }
7692
7692
  return imports;
7693
7693
  }
7694
- function findRequireCalls(tree) {
7694
+ function findRequireCalls(tree, cache) {
7695
7695
  const imports = [];
7696
- const callExpressions = findNodes(tree.rootNode, "call_expression");
7696
+ const callExpressions = getNodesFromCache(tree.rootNode, "call_expression", cache);
7697
7697
  for (const call of callExpressions) {
7698
7698
  const funcNode = call.childForFieldName("function");
7699
7699
  if (!funcNode || getNodeText(funcNode) !== "require") continue;
@@ -7807,14 +7807,14 @@ function parseImportPath(fullPath, isWildcard) {
7807
7807
  fromPackage: fullPath.substring(0, lastDot)
7808
7808
  };
7809
7809
  }
7810
- function extractPythonImports(tree) {
7810
+ function extractPythonImports(tree, cache) {
7811
7811
  const imports = [];
7812
- const importStatements = findNodes(tree.rootNode, "import_statement");
7812
+ const importStatements = getNodesFromCache(tree.rootNode, "import_statement", cache);
7813
7813
  for (const stmt of importStatements) {
7814
7814
  const importInfos = extractPythonImportStatement(stmt);
7815
7815
  imports.push(...importInfos);
7816
7816
  }
7817
- const importFromStatements = findNodes(tree.rootNode, "import_from_statement");
7817
+ const importFromStatements = getNodesFromCache(tree.rootNode, "import_from_statement", cache);
7818
7818
  for (const stmt of importFromStatements) {
7819
7819
  const importInfos = extractPythonFromImportStatement(stmt);
7820
7820
  imports.push(...importInfos);
@@ -7908,9 +7908,9 @@ function extractPythonFromImportStatement(node) {
7908
7908
  }
7909
7909
  return imports;
7910
7910
  }
7911
- function extractRustImports(tree) {
7911
+ function extractRustImports(tree, cache) {
7912
7912
  const imports = [];
7913
- const useDecls = findNodes(tree.rootNode, "use_declaration");
7913
+ const useDecls = getNodesFromCache(tree.rootNode, "use_declaration", cache);
7914
7914
  for (const useDecl of useDecls) {
7915
7915
  const useImports = extractRustUseDecl(useDecl);
7916
7916
  imports.push(...useImports);
@@ -8052,9 +8052,9 @@ function extractRustScopedUseList(node, lineNumber) {
8052
8052
  }
8053
8053
  return imports;
8054
8054
  }
8055
- function extractGoImports(tree) {
8055
+ function extractGoImports(tree, cache) {
8056
8056
  const imports = [];
8057
- const importDecls = findNodes(tree.rootNode, "import_declaration");
8057
+ const importDecls = getNodesFromCache(tree.rootNode, "import_declaration", cache);
8058
8058
  for (const decl of importDecls) {
8059
8059
  const singleSpec = findGoChildByType(decl, "import_spec");
8060
8060
  if (singleSpec) {
@@ -8189,7 +8189,7 @@ function detectLanguage3(tree) {
8189
8189
  }
8190
8190
  return jsScore > javaScore ? "javascript" : "java";
8191
8191
  }
8192
- function buildCFG(tree, language) {
8192
+ function buildCFG(tree, language, cache) {
8193
8193
  const effectiveLanguage = language ?? detectLanguage3(tree);
8194
8194
  const isJavaScript = effectiveLanguage === "javascript" || effectiveLanguage === "typescript" || effectiveLanguage === "tsx";
8195
8195
  const allBlocks = [];
@@ -8203,11 +8203,11 @@ function buildCFG(tree, language) {
8203
8203
  }
8204
8204
  if (isJavaScript) {
8205
8205
  const functions = [
8206
- ...findNodes(tree.rootNode, "function_declaration"),
8207
- ...findNodes(tree.rootNode, "arrow_function"),
8208
- ...findNodes(tree.rootNode, "method_definition"),
8209
- ...findNodes(tree.rootNode, "function"),
8210
- ...findNodes(tree.rootNode, "function_expression")
8206
+ ...getNodesFromCache(tree.rootNode, "function_declaration", cache),
8207
+ ...getNodesFromCache(tree.rootNode, "arrow_function", cache),
8208
+ ...getNodesFromCache(tree.rootNode, "method_definition", cache),
8209
+ ...getNodesFromCache(tree.rootNode, "function", cache),
8210
+ ...getNodesFromCache(tree.rootNode, "function_expression", cache)
8211
8211
  ];
8212
8212
  for (const func2 of functions) {
8213
8213
  const body2 = func2.childForFieldName("body");
@@ -8229,8 +8229,8 @@ function buildCFG(tree, language) {
8229
8229
  }
8230
8230
  } else {
8231
8231
  const methods = [
8232
- ...findNodes(tree.rootNode, "method_declaration"),
8233
- ...findNodes(tree.rootNode, "constructor_declaration")
8232
+ ...getNodesFromCache(tree.rootNode, "method_declaration", cache),
8233
+ ...getNodesFromCache(tree.rootNode, "constructor_declaration", cache)
8234
8234
  ];
8235
8235
  for (const method of methods) {
8236
8236
  const body2 = method.childForFieldName("body");
@@ -11134,7 +11134,86 @@ var DEFAULT_SOURCES = [
11134
11134
  { method: "recv", class: "TcpStream", type: "network_input", severity: "high", return_tainted: true },
11135
11135
  { method: "read", class: "TcpStream", type: "network_input", severity: "high", return_tainted: true },
11136
11136
  { method: "read_to_end", class: "TcpStream", type: "network_input", severity: "high", return_tainted: true },
11137
- { method: "read_to_string", class: "TcpStream", type: "network_input", severity: "high", return_tainted: true }
11137
+ { method: "read_to_string", class: "TcpStream", type: "network_input", severity: "high", return_tainted: true },
11138
+ // =========================================================================
11139
+ // Modern taint sources (cognium-dev #242 — 3.170.0)
11140
+ // GraphQL resolver args, gRPC request metadata, cache reads (second-order
11141
+ // taint), and JWT claims (unverified decode). These attacker-influenced
11142
+ // surfaces were previously silent — a resolver that concatenated an
11143
+ // `Args`-annotated field into a SQL string produced zero flows.
11144
+ // =========================================================================
11145
+ // --- GraphQL resolver argument sources (JS / TS — Apollo, TypeGraphQL, NestJS) ---
11146
+ // TypeGraphQL / NestJS annotate query/mutation/subscription methods; every
11147
+ // parameter on the annotated method carries attacker input from the GraphQL
11148
+ // POST body's `variables` field.
11149
+ { method_annotation: "Query", type: "http_body", severity: "high", languages: ["javascript", "typescript"] },
11150
+ { method_annotation: "Mutation", type: "http_body", severity: "high", languages: ["javascript", "typescript"] },
11151
+ { method_annotation: "Subscription", type: "http_body", severity: "high", languages: ["javascript", "typescript"] },
11152
+ { method_annotation: "FieldResolver", type: "http_body", severity: "high", languages: ["javascript", "typescript"] },
11153
+ { method_annotation: "ResolveField", type: "http_body", severity: "high", languages: ["javascript", "typescript"] },
11154
+ // Parameter-level: `@Arg('id') id: string`, `@Args('input') input: FooDto`.
11155
+ { annotation: "Arg", type: "http_param", severity: "high", param_tainted: true, languages: ["javascript", "typescript"] },
11156
+ { annotation: "Args", type: "http_param", severity: "high", param_tainted: true, languages: ["javascript", "typescript"] },
11157
+ // --- GraphQL resolver argument sources (Python — Strawberry, Graphene, Ariadne) ---
11158
+ { method_annotation: "strawberry.field", type: "http_body", severity: "high", languages: ["python"] },
11159
+ { method_annotation: "strawberry.mutation", type: "http_body", severity: "high", languages: ["python"] },
11160
+ { method_annotation: "strawberry.subscription", type: "http_body", severity: "high", languages: ["python"] },
11161
+ // --- GraphQL resolver argument sources (Java — Netflix DGS, SPQR, graphql-java-annotations) ---
11162
+ { method_annotation: "DgsQuery", type: "http_body", severity: "high", languages: ["java"] },
11163
+ { method_annotation: "DgsMutation", type: "http_body", severity: "high", languages: ["java"] },
11164
+ { method_annotation: "DgsSubscription", type: "http_body", severity: "high", languages: ["java"] },
11165
+ { method_annotation: "DgsData", type: "http_body", severity: "high", languages: ["java"] },
11166
+ { method_annotation: "GraphQLQuery", type: "http_body", severity: "high", languages: ["java"] },
11167
+ { method_annotation: "GraphQLMutation", type: "http_body", severity: "high", languages: ["java"] },
11168
+ { annotation: "InputArgument", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
11169
+ { annotation: "GraphQLArgument", type: "http_param", severity: "high", param_tainted: true, languages: ["java"] },
11170
+ // --- gRPC request metadata (Python — grpcio) ---
11171
+ // `context.invocation_metadata()` returns the caller-supplied metadata tuple.
11172
+ // No `class` filter — receiver names vary (`context`, `ctx`, `servicer_context`).
11173
+ { method: "invocation_metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["python"] },
11174
+ // --- gRPC request metadata (JS / TS — @grpc/grpc-js) ---
11175
+ // `call.metadata.get(key)` / `call.metadata.getMap()` inside a service handler.
11176
+ { method: "get", class: "Metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["javascript", "typescript"] },
11177
+ { method: "getMap", class: "Metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["javascript", "typescript"] },
11178
+ // --- gRPC request metadata (Go — google.golang.org/grpc/metadata) ---
11179
+ // `md, _ := metadata.FromIncomingContext(ctx)` pulls the caller's headers.
11180
+ { method: "FromIncomingContext", class: "metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["go"] },
11181
+ // --- gRPC request metadata (Java — io.grpc.Metadata) ---
11182
+ // Server-side interceptor receives `Metadata headers`; `headers.get(KEY)`
11183
+ // returns caller-supplied header values.
11184
+ { method: "get", class: "Metadata", type: "http_header", severity: "high", return_tainted: true, languages: ["java"] },
11185
+ // --- Cache reads (second-order taint — Redis / Memcached / Django cache) ---
11186
+ // The cache round-trip is a canonical second-order sink: whatever was
11187
+ // written previously (potentially attacker-controlled) resurfaces on read.
11188
+ // Severity 'medium' — cache contents are usually filtered by the writer but
11189
+ // the read side often forgets that guarantee. Class-scoped to `Redis`,
11190
+ // `Jedis`, `cache` to avoid colliding with generic `Map.get()`.
11191
+ { method: "get", class: "Redis", type: "db_input", severity: "medium", return_tainted: true, languages: ["python"] },
11192
+ { method: "hget", class: "Redis", type: "db_input", severity: "medium", return_tainted: true, languages: ["python"] },
11193
+ { method: "mget", class: "Redis", type: "db_input", severity: "medium", return_tainted: true, languages: ["python"] },
11194
+ { method: "lrange", class: "Redis", type: "db_input", severity: "medium", return_tainted: true, languages: ["python"] },
11195
+ { method: "get", class: "cache", type: "db_input", severity: "medium", return_tainted: true, languages: ["python"] },
11196
+ { method: "get_many", class: "cache", type: "db_input", severity: "medium", return_tainted: true, languages: ["python"] },
11197
+ { method: "get", class: "Redis", type: "db_input", severity: "medium", return_tainted: true, languages: ["javascript", "typescript"] },
11198
+ { method: "hget", class: "Redis", type: "db_input", severity: "medium", return_tainted: true, languages: ["javascript", "typescript"] },
11199
+ { method: "mget", class: "Redis", type: "db_input", severity: "medium", return_tainted: true, languages: ["javascript", "typescript"] },
11200
+ { method: "get", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
11201
+ { method: "hget", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
11202
+ { method: "mget", class: "Jedis", type: "db_input", severity: "medium", return_tainted: true, languages: ["java"] },
11203
+ // --- JWT claims (unverified decode — PyJWT / jose / jsonwebtoken / auth0 java-jwt / golang-jwt) ---
11204
+ // A JWT's payload is *always* attacker-authored. Even after verification
11205
+ // the *contents* of the claims (username, role, custom fields) are not
11206
+ // trusted for downstream flows into SQL, HTML, shell, etc. The `decode`
11207
+ // variants here return the parsed claims dictionary/object.
11208
+ { method: "decode", class: "jwt", type: "http_header", severity: "high", return_tainted: true, languages: ["python"] },
11209
+ { method: "get_unverified_claims", class: "jwt", type: "http_header", severity: "high", return_tainted: true, languages: ["python"] },
11210
+ { method: "get_unverified_header", class: "jwt", type: "http_header", severity: "high", return_tainted: true, languages: ["python"] },
11211
+ { method: "decode", class: "jwt", type: "http_header", severity: "high", return_tainted: true, languages: ["javascript", "typescript"] },
11212
+ { method: "decode", class: "jsonwebtoken", type: "http_header", severity: "high", return_tainted: true, languages: ["javascript", "typescript"] },
11213
+ { method: "decodeJwt", class: "jose", type: "http_header", severity: "high", return_tainted: true, languages: ["javascript", "typescript"] },
11214
+ { method: "decodeProtectedHeader", class: "jose", type: "http_header", severity: "high", return_tainted: true, languages: ["javascript", "typescript"] },
11215
+ { method: "decode", class: "JWT", type: "http_header", severity: "high", return_tainted: true, languages: ["java"] },
11216
+ { method: "ParseUnverified", class: "jwt", type: "http_header", severity: "high", return_tainted: true, languages: ["go"] }
11138
11217
  ];
11139
11218
  var DEFAULT_SINKS = [
11140
11219
  // SQL Injection (CWE-89)
@@ -13274,6 +13353,9 @@ function findSources(calls, types, patterns, sourceLines, language) {
13274
13353
  for (const param of method.parameters) {
13275
13354
  for (const pattern of patterns) {
13276
13355
  if (pattern.annotation && pattern.param_tainted) {
13356
+ if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
13357
+ continue;
13358
+ }
13277
13359
  if (matchesAnnotation(param.annotations, pattern.annotation)) {
13278
13360
  const paramLine = param.line ?? method.start_line;
13279
13361
  sources.push({
@@ -13294,6 +13376,9 @@ function findSources(calls, types, patterns, sourceLines, language) {
13294
13376
  for (const method of type.methods) {
13295
13377
  for (const pattern of patterns) {
13296
13378
  if (!pattern.method_annotation) continue;
13379
+ if (pattern.languages && pattern.languages.length > 0 && language !== void 0 && !pattern.languages.includes(language)) {
13380
+ continue;
13381
+ }
13297
13382
  if (!matchesAnnotation(method.annotations, pattern.method_annotation)) continue;
13298
13383
  for (const param of method.parameters) {
13299
13384
  const paramLine = param.line ?? method.start_line;
@@ -13558,7 +13643,7 @@ function isParameterizedQueryCall(call, pattern) {
13558
13643
  const secondArg = call.arguments.find((a) => a.position === 1);
13559
13644
  if (secondArg?.expression) {
13560
13645
  const expr = secondArg.expression.trim();
13561
- if (expr.startsWith("[")) {
13646
+ if (expr.startsWith("[") && !/^\[\s*\]$/.test(expr)) {
13562
13647
  return true;
13563
13648
  }
13564
13649
  }
@@ -13999,52 +14084,74 @@ function matchesSourcePattern(call, pattern) {
13999
14084
  }
14000
14085
  return false;
14001
14086
  }
14087
+ var JS_SOURCE_PATTERN_CACHE = /* @__PURE__ */ new WeakMap();
14088
+ var JS_FALLBACK_COMPILED = (() => {
14089
+ const bases = [
14090
+ { base: "req\\.params", sourceType: "http_param" },
14091
+ { base: "req\\.query", sourceType: "http_param" },
14092
+ { base: "req\\.body", sourceType: "http_body" },
14093
+ { base: "req\\.headers", sourceType: "http_header" },
14094
+ { base: "req\\.cookies", sourceType: "http_cookie" },
14095
+ { base: "req\\.url", sourceType: "http_path" },
14096
+ { base: "req\\.path", sourceType: "http_path" },
14097
+ { base: "req\\.originalUrl", sourceType: "http_path" },
14098
+ { base: "req\\.file", sourceType: "file_input" },
14099
+ { base: "req\\.files", sourceType: "file_input" },
14100
+ { base: "request\\.params", sourceType: "http_param" },
14101
+ { base: "request\\.query", sourceType: "http_param" },
14102
+ { base: "request\\.body", sourceType: "http_body" },
14103
+ { base: "request\\.headers", sourceType: "http_header" },
14104
+ { base: "process\\.env", sourceType: "env_input" },
14105
+ { base: "process\\.argv", sourceType: "io_input" },
14106
+ { base: "ctx\\.query", sourceType: "http_param" },
14107
+ { base: "ctx\\.params", sourceType: "http_param" },
14108
+ { base: "ctx\\.request", sourceType: "http_body" }
14109
+ ];
14110
+ const exact = [];
14111
+ const contained = [];
14112
+ for (const { base, sourceType } of bases) {
14113
+ exact.push({ pattern: new RegExp(`^${base}\\b`), sourceType });
14114
+ contained.push({ pattern: new RegExp(`\\b${base}\\b`), sourceType });
14115
+ }
14116
+ return { exact, contained };
14117
+ })();
14118
+ function compileJsSourcePatterns(sourcePatterns) {
14119
+ const cached = JS_SOURCE_PATTERN_CACHE.get(sourcePatterns);
14120
+ if (cached) return cached;
14121
+ const exact = [];
14122
+ const contained = [];
14123
+ for (const sp of sourcePatterns) {
14124
+ if (sp.property && sp.object && sp.property_tainted) {
14125
+ exact.push({
14126
+ pattern: new RegExp(`^${sp.object}\\.${sp.property}\\b`),
14127
+ sourceType: sp.type
14128
+ });
14129
+ contained.push({
14130
+ pattern: new RegExp(`\\b${sp.object}\\.${sp.property}\\b`),
14131
+ sourceType: sp.type
14132
+ });
14133
+ }
14134
+ }
14135
+ const compiled = { exact, contained };
14136
+ JS_SOURCE_PATTERN_CACHE.set(sourcePatterns, compiled);
14137
+ return compiled;
14138
+ }
14002
14139
  function isJavaScriptTaintedArgument(argExpression, sourcePatterns) {
14003
- const exactPatterns = [];
14004
- const containedPatterns = [];
14005
- if (sourcePatterns) {
14006
- for (const sp of sourcePatterns) {
14007
- if (sp.property && sp.object && sp.property_tainted) {
14008
- const exactRegex = new RegExp(`^${sp.object}\\.${sp.property}\\b`);
14009
- exactPatterns.push({ pattern: exactRegex, sourceType: sp.type });
14010
- const containedRegex = new RegExp(`\\b${sp.object}\\.${sp.property}\\b`);
14011
- containedPatterns.push({ pattern: containedRegex, sourceType: sp.type });
14012
- }
14013
- }
14014
- }
14015
- if (exactPatterns.length === 0) {
14016
- const basePatterns = [
14017
- { base: "req\\.params", sourceType: "http_param" },
14018
- { base: "req\\.query", sourceType: "http_param" },
14019
- { base: "req\\.body", sourceType: "http_body" },
14020
- { base: "req\\.headers", sourceType: "http_header" },
14021
- { base: "req\\.cookies", sourceType: "http_cookie" },
14022
- { base: "req\\.url", sourceType: "http_path" },
14023
- { base: "req\\.path", sourceType: "http_path" },
14024
- { base: "req\\.originalUrl", sourceType: "http_path" },
14025
- { base: "req\\.file", sourceType: "file_input" },
14026
- { base: "req\\.files", sourceType: "file_input" },
14027
- { base: "request\\.params", sourceType: "http_param" },
14028
- { base: "request\\.query", sourceType: "http_param" },
14029
- { base: "request\\.body", sourceType: "http_body" },
14030
- { base: "request\\.headers", sourceType: "http_header" },
14031
- { base: "process\\.env", sourceType: "env_input" },
14032
- { base: "process\\.argv", sourceType: "io_input" },
14033
- { base: "ctx\\.query", sourceType: "http_param" },
14034
- { base: "ctx\\.params", sourceType: "http_param" },
14035
- { base: "ctx\\.request", sourceType: "http_body" }
14036
- ];
14037
- for (const { base, sourceType } of basePatterns) {
14038
- exactPatterns.push({ pattern: new RegExp(`^${base}\\b`), sourceType });
14039
- containedPatterns.push({ pattern: new RegExp(`\\b${base}\\b`), sourceType });
14140
+ let compiled;
14141
+ if (sourcePatterns && sourcePatterns.length > 0) {
14142
+ compiled = compileJsSourcePatterns(sourcePatterns);
14143
+ if (compiled.exact.length === 0) {
14144
+ compiled = JS_FALLBACK_COMPILED;
14040
14145
  }
14146
+ } else {
14147
+ compiled = JS_FALLBACK_COMPILED;
14041
14148
  }
14042
- for (const { pattern, sourceType } of exactPatterns) {
14149
+ for (const { pattern, sourceType } of compiled.exact) {
14043
14150
  if (pattern.test(argExpression)) {
14044
14151
  return { isTainted: true, sourceType };
14045
14152
  }
14046
14153
  }
14047
- for (const { pattern, sourceType } of containedPatterns) {
14154
+ for (const { pattern, sourceType } of compiled.contained) {
14048
14155
  if (pattern.test(argExpression)) {
14049
14156
  return { isTainted: true, sourceType };
14050
14157
  }
@@ -16351,6 +16458,20 @@ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
16351
16458
  return { visited, lines, hopCapReached };
16352
16459
  }
16353
16460
 
16461
+ // src/analysis/sanitizer-index.ts
16462
+ var SANITIZER_SET_CACHE = /* @__PURE__ */ new WeakMap();
16463
+ function getSanitizesSet(san) {
16464
+ let s = SANITIZER_SET_CACHE.get(san);
16465
+ if (!s) {
16466
+ s = new Set(san.sanitizes);
16467
+ SANITIZER_SET_CACHE.set(san, s);
16468
+ }
16469
+ return s;
16470
+ }
16471
+ function sanitizerCoversSink(san, sinkType) {
16472
+ return getSanitizesSet(san).has(sinkType);
16473
+ }
16474
+
16354
16475
  // src/analysis/taint-propagation.ts
16355
16476
  function buildSanitizersByLine(sanitizers) {
16356
16477
  const out2 = /* @__PURE__ */ new Map();
@@ -16587,7 +16708,7 @@ function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
16587
16708
  if (sanitizersAtTarget && sanitizersAtTarget.length > 0) {
16588
16709
  for (const san of sanitizersAtTarget) {
16589
16710
  if (isKnownSinkType) {
16590
- if (san.sanitizes.includes(sinkType)) {
16711
+ if (sanitizerCoversSink(san, sinkType)) {
16591
16712
  return { sanitized: true, sanitizer: san };
16592
16713
  }
16593
16714
  } else if (san.sanitizes.length > 0) {
@@ -16609,7 +16730,7 @@ function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
16609
16730
  if (!sansAtLine || sansAtLine.length === 0) continue;
16610
16731
  for (const san of sansAtLine) {
16611
16732
  if (isKnownSinkType) {
16612
- if (san.sanitizes.includes(sinkType)) {
16733
+ if (sanitizerCoversSink(san, sinkType)) {
16613
16734
  return { sanitized: true, sanitizer: san };
16614
16735
  }
16615
16736
  } else if (san.sanitizes.length > 0) {
@@ -32194,7 +32315,7 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
32194
32315
  const lineSanitizers = sanitizersByLine.get(sink.line);
32195
32316
  if (!lineSanitizers || lineSanitizers.length === 0) return true;
32196
32317
  for (const san of lineSanitizers) {
32197
- if (san.sanitizes.includes(sink.type)) {
32318
+ if (sanitizerCoversSink(san, sink.type)) {
32198
32319
  const lineCalls = callsByLine.get(sink.line) ?? [];
32199
32320
  for (const call of lineCalls) {
32200
32321
  for (const arg of call.arguments) {
@@ -32687,15 +32808,23 @@ var TaintPropagationPass = class {
32687
32808
  confidence: flow.confidence,
32688
32809
  sanitized: flow.sanitized
32689
32810
  }));
32811
+ const flowKey = (x) => `${x.source_line}|${x.sink_line}|${x.sink_type}`;
32812
+ const flowKeys = /* @__PURE__ */ new Set();
32813
+ for (const x of flows) flowKeys.add(flowKey(x));
32814
+ const pushIfNew = (f) => {
32815
+ const k = flowKey(f);
32816
+ if (flowKeys.has(k)) return false;
32817
+ flowKeys.add(k);
32818
+ flows.push(f);
32819
+ return true;
32820
+ };
32690
32821
  const arrayFlows = detectArrayElementFlows(calls, sources, sinks, constProp.taintedArrayElements, constProp.unreachableLines, types) ?? [];
32691
32822
  for (const f of arrayFlows) {
32692
- if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
32693
- flows.push(f);
32694
- }
32823
+ pushIfNew(f);
32695
32824
  }
32696
32825
  const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
32697
32826
  for (const f of collectionFlows) {
32698
- if (flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) continue;
32827
+ if (flowKeys.has(flowKey(f))) continue;
32699
32828
  const flowForCheck = {
32700
32829
  source: { line: f.source_line },
32701
32830
  sink: { line: f.sink_line },
@@ -32710,19 +32839,15 @@ var TaintPropagationPass = class {
32710
32839
  }
32711
32840
  }
32712
32841
  if (isFP) continue;
32713
- flows.push(f);
32842
+ pushIfNew(f);
32714
32843
  }
32715
32844
  const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
32716
32845
  for (const f of paramFlows) {
32717
- if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
32718
- flows.push(f);
32719
- }
32846
+ pushIfNew(f);
32720
32847
  }
32721
32848
  const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language) ?? [];
32722
32849
  for (const f of exprScanFlows) {
32723
- if (flows.some(
32724
- (x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
32725
- )) continue;
32850
+ if (flowKeys.has(flowKey(f))) continue;
32726
32851
  const flowForCheck = {
32727
32852
  source: { line: f.source_line },
32728
32853
  sink: { line: f.sink_line },
@@ -32737,7 +32862,7 @@ var TaintPropagationPass = class {
32737
32862
  }
32738
32863
  }
32739
32864
  if (isFP) continue;
32740
- flows.push(f);
32865
+ pushIfNew(f);
32741
32866
  }
32742
32867
  const sanitizedNames = constProp.sanitizedVars;
32743
32868
  let finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
@@ -32765,7 +32890,7 @@ var TaintPropagationPass = class {
32765
32890
  const sansAtLine = sanitizersByLine.get(line);
32766
32891
  if (!sansAtLine) continue;
32767
32892
  for (const san of sansAtLine) {
32768
- if (san.sanitizes.includes(f.sink_type)) {
32893
+ if (sanitizerCoversSink(san, f.sink_type)) {
32769
32894
  return false;
32770
32895
  }
32771
32896
  }
@@ -32775,7 +32900,7 @@ var TaintPropagationPass = class {
32775
32900
  const sansAtSink = sanitizersByLine.get(f.sink_line);
32776
32901
  if (sansAtSink && sansAtSink.length > 0) {
32777
32902
  for (const san of sansAtSink) {
32778
- if (san.sanitizes.includes(f.sink_type)) {
32903
+ if (sanitizerCoversSink(san, f.sink_type)) {
32779
32904
  return false;
32780
32905
  }
32781
32906
  }
@@ -32799,7 +32924,7 @@ var TaintPropagationPass = class {
32799
32924
  const sansAtLine = sanitizersByLine.get(line);
32800
32925
  if (!sansAtLine || sansAtLine.length === 0) continue;
32801
32926
  for (const san of sansAtLine) {
32802
- if (san.sanitizes.includes(f.sink_type)) {
32927
+ if (sanitizerCoversSink(san, f.sink_type)) {
32803
32928
  return false;
32804
32929
  }
32805
32930
  }
@@ -33764,7 +33889,7 @@ var InterproceduralPass = class {
33764
33889
  const sansAtLine = sanitizersByLine.get(line);
33765
33890
  if (!sansAtLine) continue;
33766
33891
  for (const san of sansAtLine) {
33767
- if (san.sanitizes.includes(f.sink_type)) {
33892
+ if (sanitizerCoversSink(san, f.sink_type)) {
33768
33893
  sanitizedSinkKeys.add(`${f.sink_line}:${f.sink_type}`);
33769
33894
  return false;
33770
33895
  }
@@ -33775,7 +33900,7 @@ var InterproceduralPass = class {
33775
33900
  const sansAtSink = sanitizersByLine.get(f.sink_line);
33776
33901
  if (!sansAtSink || sansAtSink.length === 0) return true;
33777
33902
  for (const san of sansAtSink) {
33778
- if (san.sanitizes.includes(f.sink_type)) {
33903
+ if (sanitizerCoversSink(san, f.sink_type)) {
33779
33904
  return false;
33780
33905
  }
33781
33906
  }
@@ -42186,7 +42311,13 @@ function getNodeTypesForLanguage(language) {
42186
42311
  "jsx_self_closing_element",
42187
42312
  "jsx_opening_element",
42188
42313
  "jsx_attribute",
42189
- "jsx_expression"
42314
+ "jsx_expression",
42315
+ // buildCFG containers (3.172.0 T2-A) — the two below are legacy
42316
+ // tree-sitter-javascript node types for anonymous / expression
42317
+ // functions; declaring them here lets `collectAllNodes` populate
42318
+ // the cache in the same pass so `buildCFG` skips its per-type walk.
42319
+ "function",
42320
+ "function_expression"
42190
42321
  ]);
42191
42322
  case "bash":
42192
42323
  return /* @__PURE__ */ new Set([
@@ -42240,7 +42371,9 @@ function getNodeTypesForLanguage(language) {
42240
42371
  "field_declaration",
42241
42372
  "import_declaration",
42242
42373
  "interface_declaration",
42243
- "enum_declaration"
42374
+ "enum_declaration",
42375
+ "package_declaration",
42376
+ "local_variable_declaration"
42244
42377
  ]);
42245
42378
  }
42246
42379
  }
@@ -42264,10 +42397,20 @@ async function analyze(code, filePath, language, options = {}) {
42264
42397
  }
42265
42398
  }
42266
42399
  logger.debug("Analyzing file", { filePath, language, parseGrammar, codeLength: code.length });
42400
+ const phaseTimingEnabled = globalThis.__circleIrPassTiming === true;
42401
+ const phaseStart = () => phaseTimingEnabled ? Date.now() : 0;
42402
+ const phaseEnd = (label, t0) => {
42403
+ if (!phaseTimingEnabled) return;
42404
+ console.error(`[phase-timing] ${label} ${Date.now() - t0}ms file=${filePath}`);
42405
+ };
42406
+ const tParse = phaseStart();
42267
42407
  const tree = await parse(code, parseGrammar);
42408
+ phaseEnd("parse", tParse);
42268
42409
  try {
42269
42410
  logger.trace("Parsed AST", { rootNodeType: tree.rootNode.type });
42411
+ const tParseStatus = phaseStart();
42270
42412
  const parseStatus = extractParseStatus(tree);
42413
+ phaseEnd("extractParseStatus", tParseStatus);
42271
42414
  if (parseStatus.has_errors) {
42272
42415
  logger.warn("Partial parse \u2014 IR may be incomplete", {
42273
42416
  filePath,
@@ -42276,18 +42419,37 @@ async function analyze(code, filePath, language, options = {}) {
42276
42419
  firstErrorLine: parseStatus.error_locations[0]?.line
42277
42420
  });
42278
42421
  }
42422
+ const tCollect = phaseStart();
42279
42423
  const nodeCache = collectAllNodes(tree.rootNode, getNodeTypesForLanguage(language));
42424
+ phaseEnd("collectAllNodes", tCollect);
42425
+ const tMeta = phaseStart();
42280
42426
  const meta = extractMeta(code, tree, filePath, language);
42281
42427
  if (options.projectProfile !== void 0) {
42282
42428
  meta.projectProfile = makeProfileResolver(options.projectProfile)(filePath);
42283
42429
  }
42430
+ phaseEnd("extractMeta", tMeta);
42431
+ const tTypes = phaseStart();
42284
42432
  const types = extractTypes(tree, nodeCache, language);
42433
+ phaseEnd("extractTypes", tTypes);
42434
+ const tCalls = phaseStart();
42285
42435
  const calls = extractCalls(tree, nodeCache, language);
42286
- const imports = extractImports(tree, language);
42436
+ phaseEnd("extractCalls", tCalls);
42437
+ const tImports = phaseStart();
42438
+ const imports = extractImports(tree, language, nodeCache);
42439
+ phaseEnd("extractImports", tImports);
42440
+ const tExports = phaseStart();
42287
42441
  const exports = extractExports(types);
42288
- const cfg = buildCFG(tree, language);
42442
+ phaseEnd("extractExports", tExports);
42443
+ const tCFG = phaseStart();
42444
+ const cfg = buildCFG(tree, language, nodeCache);
42445
+ phaseEnd("buildCFG", tCFG);
42446
+ const tDFG = phaseStart();
42289
42447
  const dfg = buildDFG(tree, nodeCache, language);
42448
+ phaseEnd("buildDFG", tDFG);
42449
+ const tRuntime = phaseStart();
42290
42450
  const runtimeRegistrations = extractRuntimeRegistrations(tree, nodeCache, language, imports);
42451
+ phaseEnd("extractRuntimeRegistrations", tRuntime);
42452
+ const tGraph = phaseStart();
42291
42453
  const graph = new CodeGraph({
42292
42454
  meta,
42293
42455
  types,
@@ -42300,6 +42462,7 @@ async function analyze(code, filePath, language, options = {}) {
42300
42462
  unresolved: [],
42301
42463
  enriched: {}
42302
42464
  });
42465
+ phaseEnd("buildCodeGraph", tGraph);
42303
42466
  const config = options.taintConfig ?? getDefaultConfig();
42304
42467
  const disabledPasses = new Set(options.disabledPasses ?? []);
42305
42468
  const passOpts = options.passOptions ?? {};