circle-ir 3.167.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 +23 -2
  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 +319 -95
  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
  }
@@ -14899,6 +15006,64 @@ function formatCallCode(call) {
14899
15006
  return `${call.method_name}(${args2})`;
14900
15007
  }
14901
15008
 
15009
+ // src/analysis/non-executable-lines.ts
15010
+ function isNonExecutableSourceLine(sourceCode, line, language) {
15011
+ if (!sourceCode || line < 1) return false;
15012
+ const lines = sourceCode.split("\n");
15013
+ if (line > lines.length) return false;
15014
+ const raw = lines[line - 1] ?? "";
15015
+ const trimmed = raw.trim();
15016
+ if (trimmed === "") return true;
15017
+ const lang = language.toLowerCase();
15018
+ if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*/") || trimmed.startsWith("*") || trimmed === "/**") {
15019
+ return true;
15020
+ }
15021
+ switch (lang) {
15022
+ case "java":
15023
+ case "javascript":
15024
+ case "typescript":
15025
+ case "go":
15026
+ case "rust":
15027
+ return isNonExecutableCurly(trimmed);
15028
+ case "python":
15029
+ return isNonExecutablePython(trimmed);
15030
+ default:
15031
+ return false;
15032
+ }
15033
+ }
15034
+ function isNonExecutableCurly(trimmed) {
15035
+ if (/^(import|package|use|from)\s/.test(trimmed)) return true;
15036
+ if (/^@[A-Za-z_][\w.]*(?:\s*\([^)]*\))?\s*$/.test(trimmed)) return true;
15037
+ if (/^(?:(?:public|private|protected|internal)\s+)?(?:static\s+)?(?:final\s+|readonly\s+|const\s+)(?:[A-Za-z_][\w<>.\[\]]*\s+)?[A-Za-z_]\w*(?:\s*:\s*[^=]+)?\s*=\s*(?:["'`][^"'`]*["'`]|-?\d+(?:\.\d+)?)\s*;?\s*$/.test(
15038
+ trimmed
15039
+ )) {
15040
+ return true;
15041
+ }
15042
+ if (/^(?:const|let|var|static)\s+[A-Za-z_]\w*(?:\s*:\s*[^=]+)?\s*=\s*(?:["'`][^"'`]*["'`]|-?\d+(?:\.\d+)?)\s*;?\s*$/.test(
15043
+ trimmed
15044
+ )) {
15045
+ return true;
15046
+ }
15047
+ if (/^(?:(?:public|private|protected)\s+)?(?:static\s+)?(?:final\s+)[A-Za-z_][\w<>.\[\]]*\s+[A-Za-z_]\w*\s*;\s*$/.test(
15048
+ trimmed
15049
+ )) {
15050
+ return true;
15051
+ }
15052
+ return false;
15053
+ }
15054
+ function isNonExecutablePython(trimmed) {
15055
+ if (/^(import|from)\s/.test(trimmed)) return true;
15056
+ if (trimmed.startsWith("#")) return true;
15057
+ if (trimmed === '"""' || trimmed === "'''") return true;
15058
+ if (/^@[A-Za-z_][\w.]*(?:\s*\([^)]*\))?\s*$/.test(trimmed)) return true;
15059
+ if (/^[A-Z_][A-Z0-9_]*\s*(?::\s*[^=]+)?\s*=\s*(?:["'][^"']*["']|-?\d+(?:\.\d+)?)\s*$/.test(
15060
+ trimmed
15061
+ )) {
15062
+ return true;
15063
+ }
15064
+ return false;
15065
+ }
15066
+
14902
15067
  // src/analysis/findings.ts
14903
15068
  function canSourceReachSink(sourceType, sinkType) {
14904
15069
  const sourceToSinkMapping = {
@@ -16293,6 +16458,20 @@ function walkBackwardDefs(startDefId, chainsByToDef, defById, options = {}) {
16293
16458
  return { visited, lines, hopCapReached };
16294
16459
  }
16295
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
+
16296
16475
  // src/analysis/taint-propagation.ts
16297
16476
  function buildSanitizersByLine(sanitizers) {
16298
16477
  const out2 = /* @__PURE__ */ new Map();
@@ -16529,7 +16708,7 @@ function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
16529
16708
  if (sanitizersAtTarget && sanitizersAtTarget.length > 0) {
16530
16709
  for (const san of sanitizersAtTarget) {
16531
16710
  if (isKnownSinkType) {
16532
- if (san.sanitizes.includes(sinkType)) {
16711
+ if (sanitizerCoversSink(san, sinkType)) {
16533
16712
  return { sanitized: true, sanitizer: san };
16534
16713
  }
16535
16714
  } else if (san.sanitizes.length > 0) {
@@ -16551,7 +16730,7 @@ function checkSanitized(fromLine, toLine, sinkType, sanitizersByLine, ctx) {
16551
16730
  if (!sansAtLine || sansAtLine.length === 0) continue;
16552
16731
  for (const san of sansAtLine) {
16553
16732
  if (isKnownSinkType) {
16554
- if (san.sanitizes.includes(sinkType)) {
16733
+ if (sanitizerCoversSink(san, sinkType)) {
16555
16734
  return { sanitized: true, sanitizer: san };
16556
16735
  }
16557
16736
  } else if (san.sanitizes.length > 0) {
@@ -31158,7 +31337,10 @@ var SinkFilterPass = class {
31158
31337
  const taintMatcher = ctx.getResult("taint-matcher");
31159
31338
  const constProp = ctx.getResult("constant-propagation");
31160
31339
  const langSources = ctx.getResult("language-sources");
31161
- const sources = [...taintMatcher.sources, ...langSources.additionalSources];
31340
+ const mergedSources = [...taintMatcher.sources, ...langSources.additionalSources];
31341
+ const sources = ctx.code && ctx.language ? mergedSources.filter(
31342
+ (s) => !isNonExecutableSourceLine(ctx.code, s.line, ctx.language)
31343
+ ) : mergedSources;
31162
31344
  const sinks = [...taintMatcher.sinks];
31163
31345
  for (const s of langSources.additionalSinks) {
31164
31346
  if (!sinks.some((x) => x.line === s.line && x.cwe === s.cwe && x.type === s.type)) {
@@ -32133,7 +32315,7 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
32133
32315
  const lineSanitizers = sanitizersByLine.get(sink.line);
32134
32316
  if (!lineSanitizers || lineSanitizers.length === 0) return true;
32135
32317
  for (const san of lineSanitizers) {
32136
- if (san.sanitizes.includes(sink.type)) {
32318
+ if (sanitizerCoversSink(san, sink.type)) {
32137
32319
  const lineCalls = callsByLine.get(sink.line) ?? [];
32138
32320
  for (const call of lineCalls) {
32139
32321
  for (const arg of call.arguments) {
@@ -32626,15 +32808,23 @@ var TaintPropagationPass = class {
32626
32808
  confidence: flow.confidence,
32627
32809
  sanitized: flow.sanitized
32628
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
+ };
32629
32821
  const arrayFlows = detectArrayElementFlows(calls, sources, sinks, constProp.taintedArrayElements, constProp.unreachableLines, types) ?? [];
32630
32822
  for (const f of arrayFlows) {
32631
- if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
32632
- flows.push(f);
32633
- }
32823
+ pushIfNew(f);
32634
32824
  }
32635
32825
  const collectionFlows = detectCollectionFlows(calls, sources, sinks, constProp.tainted, constProp.unreachableLines, ctx.code, types) ?? [];
32636
32826
  for (const f of collectionFlows) {
32637
- 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;
32638
32828
  const flowForCheck = {
32639
32829
  source: { line: f.source_line },
32640
32830
  sink: { line: f.sink_line },
@@ -32649,19 +32839,15 @@ var TaintPropagationPass = class {
32649
32839
  }
32650
32840
  }
32651
32841
  if (isFP) continue;
32652
- flows.push(f);
32842
+ pushIfNew(f);
32653
32843
  }
32654
32844
  const paramFlows = detectParameterSinkFlows(types, calls, sources, sinks, constProp.unreachableLines, constProp.tainted, ctx.code) ?? [];
32655
32845
  for (const f of paramFlows) {
32656
- if (!flows.some((x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type)) {
32657
- flows.push(f);
32658
- }
32846
+ pushIfNew(f);
32659
32847
  }
32660
32848
  const exprScanFlows = detectExpressionScanFlows(calls, sources, sinks, sanitizers, constProp.unreachableLines, constProp.tainted, ctx.code, ctx.language) ?? [];
32661
32849
  for (const f of exprScanFlows) {
32662
- if (flows.some(
32663
- (x) => x.source_line === f.source_line && x.sink_line === f.sink_line && x.sink_type === f.sink_type
32664
- )) continue;
32850
+ if (flowKeys.has(flowKey(f))) continue;
32665
32851
  const flowForCheck = {
32666
32852
  source: { line: f.source_line },
32667
32853
  sink: { line: f.sink_line },
@@ -32676,7 +32862,7 @@ var TaintPropagationPass = class {
32676
32862
  }
32677
32863
  }
32678
32864
  if (isFP) continue;
32679
- flows.push(f);
32865
+ pushIfNew(f);
32680
32866
  }
32681
32867
  const sanitizedNames = constProp.sanitizedVars;
32682
32868
  let finalFlows = sanitizedNames.size === 0 ? flows : flows.filter((f) => {
@@ -32704,7 +32890,7 @@ var TaintPropagationPass = class {
32704
32890
  const sansAtLine = sanitizersByLine.get(line);
32705
32891
  if (!sansAtLine) continue;
32706
32892
  for (const san of sansAtLine) {
32707
- if (san.sanitizes.includes(f.sink_type)) {
32893
+ if (sanitizerCoversSink(san, f.sink_type)) {
32708
32894
  return false;
32709
32895
  }
32710
32896
  }
@@ -32714,7 +32900,7 @@ var TaintPropagationPass = class {
32714
32900
  const sansAtSink = sanitizersByLine.get(f.sink_line);
32715
32901
  if (sansAtSink && sansAtSink.length > 0) {
32716
32902
  for (const san of sansAtSink) {
32717
- if (san.sanitizes.includes(f.sink_type)) {
32903
+ if (sanitizerCoversSink(san, f.sink_type)) {
32718
32904
  return false;
32719
32905
  }
32720
32906
  }
@@ -32738,7 +32924,7 @@ var TaintPropagationPass = class {
32738
32924
  const sansAtLine = sanitizersByLine.get(line);
32739
32925
  if (!sansAtLine || sansAtLine.length === 0) continue;
32740
32926
  for (const san of sansAtLine) {
32741
- if (san.sanitizes.includes(f.sink_type)) {
32927
+ if (sanitizerCoversSink(san, f.sink_type)) {
32742
32928
  return false;
32743
32929
  }
32744
32930
  }
@@ -33703,7 +33889,7 @@ var InterproceduralPass = class {
33703
33889
  const sansAtLine = sanitizersByLine.get(line);
33704
33890
  if (!sansAtLine) continue;
33705
33891
  for (const san of sansAtLine) {
33706
- if (san.sanitizes.includes(f.sink_type)) {
33892
+ if (sanitizerCoversSink(san, f.sink_type)) {
33707
33893
  sanitizedSinkKeys.add(`${f.sink_line}:${f.sink_type}`);
33708
33894
  return false;
33709
33895
  }
@@ -33714,7 +33900,7 @@ var InterproceduralPass = class {
33714
33900
  const sansAtSink = sanitizersByLine.get(f.sink_line);
33715
33901
  if (!sansAtSink || sansAtSink.length === 0) return true;
33716
33902
  for (const san of sansAtSink) {
33717
- if (san.sanitizes.includes(f.sink_type)) {
33903
+ if (sanitizerCoversSink(san, f.sink_type)) {
33718
33904
  return false;
33719
33905
  }
33720
33906
  }
@@ -42125,7 +42311,13 @@ function getNodeTypesForLanguage(language) {
42125
42311
  "jsx_self_closing_element",
42126
42312
  "jsx_opening_element",
42127
42313
  "jsx_attribute",
42128
- "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"
42129
42321
  ]);
42130
42322
  case "bash":
42131
42323
  return /* @__PURE__ */ new Set([
@@ -42179,7 +42371,9 @@ function getNodeTypesForLanguage(language) {
42179
42371
  "field_declaration",
42180
42372
  "import_declaration",
42181
42373
  "interface_declaration",
42182
- "enum_declaration"
42374
+ "enum_declaration",
42375
+ "package_declaration",
42376
+ "local_variable_declaration"
42183
42377
  ]);
42184
42378
  }
42185
42379
  }
@@ -42203,10 +42397,20 @@ async function analyze(code, filePath, language, options = {}) {
42203
42397
  }
42204
42398
  }
42205
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();
42206
42407
  const tree = await parse(code, parseGrammar);
42408
+ phaseEnd("parse", tParse);
42207
42409
  try {
42208
42410
  logger.trace("Parsed AST", { rootNodeType: tree.rootNode.type });
42411
+ const tParseStatus = phaseStart();
42209
42412
  const parseStatus = extractParseStatus(tree);
42413
+ phaseEnd("extractParseStatus", tParseStatus);
42210
42414
  if (parseStatus.has_errors) {
42211
42415
  logger.warn("Partial parse \u2014 IR may be incomplete", {
42212
42416
  filePath,
@@ -42215,18 +42419,37 @@ async function analyze(code, filePath, language, options = {}) {
42215
42419
  firstErrorLine: parseStatus.error_locations[0]?.line
42216
42420
  });
42217
42421
  }
42422
+ const tCollect = phaseStart();
42218
42423
  const nodeCache = collectAllNodes(tree.rootNode, getNodeTypesForLanguage(language));
42424
+ phaseEnd("collectAllNodes", tCollect);
42425
+ const tMeta = phaseStart();
42219
42426
  const meta = extractMeta(code, tree, filePath, language);
42220
42427
  if (options.projectProfile !== void 0) {
42221
42428
  meta.projectProfile = makeProfileResolver(options.projectProfile)(filePath);
42222
42429
  }
42430
+ phaseEnd("extractMeta", tMeta);
42431
+ const tTypes = phaseStart();
42223
42432
  const types = extractTypes(tree, nodeCache, language);
42433
+ phaseEnd("extractTypes", tTypes);
42434
+ const tCalls = phaseStart();
42224
42435
  const calls = extractCalls(tree, nodeCache, language);
42225
- 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();
42226
42441
  const exports = extractExports(types);
42227
- 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();
42228
42447
  const dfg = buildDFG(tree, nodeCache, language);
42448
+ phaseEnd("buildDFG", tDFG);
42449
+ const tRuntime = phaseStart();
42229
42450
  const runtimeRegistrations = extractRuntimeRegistrations(tree, nodeCache, language, imports);
42451
+ phaseEnd("extractRuntimeRegistrations", tRuntime);
42452
+ const tGraph = phaseStart();
42230
42453
  const graph = new CodeGraph({
42231
42454
  meta,
42232
42455
  types,
@@ -42239,6 +42462,7 @@ async function analyze(code, filePath, language, options = {}) {
42239
42462
  unresolved: [],
42240
42463
  enriched: {}
42241
42464
  });
42465
+ phaseEnd("buildCodeGraph", tGraph);
42242
42466
  const config = options.taintConfig ?? getDefaultConfig();
42243
42467
  const disabledPasses = new Set(options.disabledPasses ?? []);
42244
42468
  const passOpts = options.passOptions ?? {};