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.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +79 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/cross-file-pass.d.ts.map +1 -1
- package/dist/analysis/passes/cross-file-pass.js +3 -2
- package/dist/analysis/passes/cross-file-pass.js.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.d.ts.map +1 -1
- package/dist/analysis/passes/interprocedural-pass.js +3 -2
- package/dist/analysis/passes/interprocedural-pass.js.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
- package/dist/analysis/passes/sink-filter-pass.js +2 -1
- package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +29 -15
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/sanitizer-index.d.ts +34 -0
- package/dist/analysis/sanitizer-index.d.ts.map +1 -0
- package/dist/analysis/sanitizer-index.js +48 -0
- package/dist/analysis/sanitizer-index.js.map +1 -0
- package/dist/analysis/taint-matcher.d.ts.map +1 -1
- package/dist/analysis/taint-matcher.js +93 -45
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +5 -2
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +51 -2
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +257 -94
- package/dist/core/circle-ir-core.cjs +193 -72
- package/dist/core/circle-ir-core.js +193 -72
- package/dist/core/extractors/cfg.d.ts +11 -1
- package/dist/core/extractors/cfg.d.ts.map +1 -1
- package/dist/core/extractors/cfg.js +18 -9
- package/dist/core/extractors/cfg.js.map +1 -1
- package/dist/core/extractors/imports.d.ts +9 -1
- package/dist/core/extractors/imports.d.ts.map +1 -1
- package/dist/core/extractors/imports.js +29 -22
- package/dist/core/extractors/imports.js.map +1 -1
- 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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
...
|
|
8207
|
-
...
|
|
8208
|
-
...
|
|
8209
|
-
...
|
|
8210
|
-
...
|
|
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
|
-
...
|
|
8233
|
-
...
|
|
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
|
-
|
|
14004
|
-
|
|
14005
|
-
|
|
14006
|
-
|
|
14007
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 ?? {};
|