circle-ir 3.4.0 → 3.6.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.
@@ -5470,6 +5470,9 @@ function extractCalls(tree, cache, language) {
5470
5470
  const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
5471
5471
  const isPython = detectedLanguage === "python";
5472
5472
  const isRust = detectedLanguage === "rust";
5473
+ if (detectedLanguage === "bash") {
5474
+ return extractBashCalls(tree, cache);
5475
+ }
5473
5476
  if (isRust) {
5474
5477
  return extractRustCalls(tree, cache);
5475
5478
  }
@@ -6159,6 +6162,88 @@ function inferTypeFromReceiverName(receiver) {
6159
6162
  const lowerReceiver = receiver.toLowerCase();
6160
6163
  return patterns[lowerReceiver] ?? null;
6161
6164
  }
6165
+ function extractBashCalls(tree, cache) {
6166
+ const calls = [];
6167
+ const commands = getNodesFromCache(tree.rootNode, "command", cache);
6168
+ for (const cmd of commands) {
6169
+ const callInfo = extractBashCommandInfo(cmd);
6170
+ if (callInfo) {
6171
+ calls.push(callInfo);
6172
+ }
6173
+ }
6174
+ return calls;
6175
+ }
6176
+ function extractBashCommandInfo(node) {
6177
+ const nameNode = node.childForFieldName("name");
6178
+ if (!nameNode) return null;
6179
+ const commandName = getNodeText(nameNode);
6180
+ if (!commandName) return null;
6181
+ const args2 = [];
6182
+ let position = 0;
6183
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6184
+ const child = node.child(i2);
6185
+ if (!child) continue;
6186
+ if (child === nameNode) continue;
6187
+ if (child.type.includes("redirect") || child.type === "heredoc_body" || child.type === "file_descriptor") {
6188
+ continue;
6189
+ }
6190
+ const expression = getNodeText(child);
6191
+ if (!expression.trim()) continue;
6192
+ const variable = extractBashVariableRef(child);
6193
+ args2.push({
6194
+ position: position++,
6195
+ expression,
6196
+ variable,
6197
+ literal: null
6198
+ });
6199
+ }
6200
+ const inMethod = findBashEnclosingFunction(node);
6201
+ return {
6202
+ method_name: commandName,
6203
+ receiver: null,
6204
+ arguments: args2,
6205
+ location: {
6206
+ line: node.startPosition.row + 1,
6207
+ column: node.startPosition.column
6208
+ },
6209
+ in_method: inMethod,
6210
+ resolved: false,
6211
+ resolution: { status: "external_method" }
6212
+ };
6213
+ }
6214
+ function extractBashVariableRef(node) {
6215
+ const type = node.type;
6216
+ if (type === "simple_expansion" || type === "expansion") {
6217
+ return getNodeText(node).replace(/^\$\{?/, "").replace(/\}$/, "");
6218
+ }
6219
+ if (type === "string" || type === "concatenation") {
6220
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6221
+ const child = node.child(i2);
6222
+ if (!child) continue;
6223
+ if (child.type === "simple_expansion" || child.type === "expansion") {
6224
+ return getNodeText(child).replace(/^\$\{?/, "").replace(/\}$/, "");
6225
+ }
6226
+ }
6227
+ }
6228
+ if (type === "word") {
6229
+ const text = getNodeText(node);
6230
+ if (text.startsWith("$")) {
6231
+ return text.slice(1).replace(/^\{/, "").replace(/\}$/, "");
6232
+ }
6233
+ }
6234
+ return null;
6235
+ }
6236
+ function findBashEnclosingFunction(node) {
6237
+ let current = node.parent;
6238
+ while (current) {
6239
+ if (current.type === "function_definition") {
6240
+ const nameNode = current.childForFieldName("name");
6241
+ return nameNode ? getNodeText(nameNode) : null;
6242
+ }
6243
+ current = current.parent;
6244
+ }
6245
+ return null;
6246
+ }
6162
6247
  function extractPythonCalls(tree, cache) {
6163
6248
  const calls = [];
6164
6249
  const context = buildPythonResolutionContext(tree, cache);
@@ -13896,6 +13981,9 @@ function getLanguageRegistry() {
13896
13981
  function registerLanguage(plugin) {
13897
13982
  getLanguageRegistry().register(plugin);
13898
13983
  }
13984
+ function getLanguagePlugin(language) {
13985
+ return getLanguageRegistry().get(language);
13986
+ }
13899
13987
 
13900
13988
  // src/languages/plugins/base.ts
13901
13989
  var BaseLanguagePlugin = class {
@@ -16033,12 +16121,241 @@ var RustPlugin = class extends BaseLanguagePlugin {
16033
16121
  }
16034
16122
  };
16035
16123
 
16124
+ // src/languages/plugins/bash.ts
16125
+ var BashPlugin = class extends BaseLanguagePlugin {
16126
+ id = "bash";
16127
+ name = "Bash/Shell";
16128
+ extensions = [".sh", ".bash", ".zsh", ".ksh"];
16129
+ wasmPath = "tree-sitter-bash.wasm";
16130
+ nodeTypes = {
16131
+ // Type declarations — shell has no OOP types
16132
+ classDeclaration: [],
16133
+ interfaceDeclaration: [],
16134
+ enumDeclaration: [],
16135
+ functionDeclaration: ["function_definition"],
16136
+ methodDeclaration: ["function_definition"],
16137
+ // Expressions — commands are treated as calls
16138
+ methodCall: ["command"],
16139
+ functionCall: ["command"],
16140
+ assignment: ["variable_assignment"],
16141
+ variableDeclaration: ["variable_assignment", "declaration_command"],
16142
+ // Parameters and arguments — positional args are child words
16143
+ parameter: [],
16144
+ argument: [],
16145
+ // Annotations/decorators — none in shell
16146
+ annotation: [],
16147
+ decorator: [],
16148
+ // Imports — shell uses `source` / `.` but no formal import system
16149
+ importStatement: [],
16150
+ // Control flow
16151
+ ifStatement: ["if_statement"],
16152
+ forStatement: ["for_statement", "c_style_for_statement"],
16153
+ whileStatement: ["while_statement"],
16154
+ tryStatement: [],
16155
+ returnStatement: []
16156
+ };
16157
+ /**
16158
+ * Shell scripts don't have a formal framework concept.
16159
+ */
16160
+ detectFramework(_context) {
16161
+ return void 0;
16162
+ }
16163
+ /**
16164
+ * Bash taint source patterns.
16165
+ * In shell, tainted data enters via `read` (stdin).
16166
+ * curl/wget are excluded as sources (see comment in implementation).
16167
+ */
16168
+ getBuiltinSources() {
16169
+ return [
16170
+ // read built-in reads user input from stdin.
16171
+ // curl/wget are intentionally excluded: they're also registered as sinks (SSRF),
16172
+ // and without DFG tracking of $() command substitution, including them as sources
16173
+ // would generate false positives for safe curl calls.
16174
+ {
16175
+ method: "read",
16176
+ type: "io_input",
16177
+ severity: "high",
16178
+ confidence: 0.9,
16179
+ returnTainted: true
16180
+ }
16181
+ ];
16182
+ }
16183
+ /**
16184
+ * Bash taint sink patterns.
16185
+ * Key sinks: eval (CWE-94), bash/sh -c (CWE-78), DB clients (CWE-89),
16186
+ * file operations (CWE-22), SSRF via curl/wget (CWE-918).
16187
+ */
16188
+ getBuiltinSinks() {
16189
+ return [
16190
+ // Code / command injection via eval
16191
+ {
16192
+ method: "eval",
16193
+ type: "code_injection",
16194
+ cwe: "CWE-94",
16195
+ severity: "critical",
16196
+ argPositions: [0]
16197
+ },
16198
+ // Command injection: spawning a sub-shell with -c flag
16199
+ {
16200
+ method: "bash",
16201
+ type: "command_injection",
16202
+ cwe: "CWE-78",
16203
+ severity: "critical",
16204
+ argPositions: [1]
16205
+ },
16206
+ {
16207
+ method: "sh",
16208
+ type: "command_injection",
16209
+ cwe: "CWE-78",
16210
+ severity: "critical",
16211
+ argPositions: [1]
16212
+ },
16213
+ {
16214
+ method: "zsh",
16215
+ type: "command_injection",
16216
+ cwe: "CWE-78",
16217
+ severity: "critical",
16218
+ argPositions: [1]
16219
+ },
16220
+ {
16221
+ method: "ksh",
16222
+ type: "command_injection",
16223
+ cwe: "CWE-78",
16224
+ severity: "critical",
16225
+ argPositions: [1]
16226
+ },
16227
+ // SQL injection via DB CLI clients (first arg is query/expression)
16228
+ {
16229
+ method: "mysql",
16230
+ type: "sql_injection",
16231
+ cwe: "CWE-89",
16232
+ severity: "critical",
16233
+ argPositions: [1]
16234
+ },
16235
+ {
16236
+ method: "psql",
16237
+ type: "sql_injection",
16238
+ cwe: "CWE-89",
16239
+ severity: "critical",
16240
+ argPositions: [1]
16241
+ },
16242
+ {
16243
+ method: "sqlite3",
16244
+ type: "sql_injection",
16245
+ cwe: "CWE-89",
16246
+ severity: "critical",
16247
+ argPositions: [1]
16248
+ },
16249
+ // Path traversal via file operations (first arg is path)
16250
+ {
16251
+ method: "cat",
16252
+ type: "path_traversal",
16253
+ cwe: "CWE-22",
16254
+ severity: "high",
16255
+ argPositions: [0]
16256
+ },
16257
+ {
16258
+ method: "rm",
16259
+ type: "path_traversal",
16260
+ cwe: "CWE-22",
16261
+ severity: "high",
16262
+ argPositions: [0]
16263
+ },
16264
+ {
16265
+ method: "cp",
16266
+ type: "path_traversal",
16267
+ cwe: "CWE-22",
16268
+ severity: "high",
16269
+ argPositions: [0]
16270
+ },
16271
+ {
16272
+ method: "mv",
16273
+ type: "path_traversal",
16274
+ cwe: "CWE-22",
16275
+ severity: "high",
16276
+ argPositions: [0]
16277
+ },
16278
+ {
16279
+ method: "chmod",
16280
+ type: "path_traversal",
16281
+ cwe: "CWE-22",
16282
+ severity: "medium",
16283
+ argPositions: [1]
16284
+ },
16285
+ {
16286
+ method: "chown",
16287
+ type: "path_traversal",
16288
+ cwe: "CWE-22",
16289
+ severity: "medium",
16290
+ argPositions: [1]
16291
+ },
16292
+ // SSRF — curl/wget with externally-controlled URL
16293
+ {
16294
+ method: "curl",
16295
+ type: "ssrf",
16296
+ cwe: "CWE-918",
16297
+ severity: "high",
16298
+ argPositions: [0]
16299
+ },
16300
+ {
16301
+ method: "wget",
16302
+ type: "ssrf",
16303
+ cwe: "CWE-918",
16304
+ severity: "high",
16305
+ argPositions: [0]
16306
+ }
16307
+ ];
16308
+ }
16309
+ /**
16310
+ * Shell has no OOP receiver types.
16311
+ */
16312
+ getReceiverType(_node, _context) {
16313
+ return void 0;
16314
+ }
16315
+ /**
16316
+ * Bash string literals: quoted strings and raw ($'...') strings.
16317
+ */
16318
+ isStringLiteral(node) {
16319
+ return node.type === "string" || node.type === "raw_string" || node.type === "ansi_c_string";
16320
+ }
16321
+ /**
16322
+ * Extract string value from bash string literal, stripping quotes.
16323
+ */
16324
+ getStringValue(node) {
16325
+ if (!this.isStringLiteral(node)) return void 0;
16326
+ const text = node.text;
16327
+ if (node.type === "raw_string") {
16328
+ return text.slice(1, -1);
16329
+ }
16330
+ if (node.type === "ansi_c_string") {
16331
+ return text.slice(2, -1);
16332
+ }
16333
+ const match = text.match(/^"(.*)"$/s);
16334
+ if (match) return match[1];
16335
+ return text;
16336
+ }
16337
+ // Extraction methods — delegate to base extractors via generic walker
16338
+ extractTypes(_context) {
16339
+ return [];
16340
+ }
16341
+ extractCalls(_context) {
16342
+ return [];
16343
+ }
16344
+ extractImports(_context) {
16345
+ return [];
16346
+ }
16347
+ extractPackage(_context) {
16348
+ return void 0;
16349
+ }
16350
+ };
16351
+
16036
16352
  // src/languages/plugins/index.ts
16037
16353
  function registerBuiltinPlugins() {
16038
16354
  registerLanguage(new JavaPlugin());
16039
16355
  registerLanguage(new JavaScriptPlugin());
16040
16356
  registerLanguage(new PythonPlugin());
16041
16357
  registerLanguage(new RustPlugin());
16358
+ registerLanguage(new BashPlugin());
16042
16359
  }
16043
16360
 
16044
16361
  // src/utils/logger.ts
@@ -16367,6 +16684,18 @@ async function analyze(code, filePath, language, options = {}) {
16367
16684
  "member_expression",
16368
16685
  "assignment_expression"
16369
16686
  ]);
16687
+ } else if (language === "bash") {
16688
+ nodeTypesToCollect = /* @__PURE__ */ new Set([
16689
+ // Bash AST nodes
16690
+ "command",
16691
+ "function_definition",
16692
+ "variable_assignment",
16693
+ "declaration_command",
16694
+ "if_statement",
16695
+ "for_statement",
16696
+ "c_style_for_statement",
16697
+ "while_statement"
16698
+ ]);
16370
16699
  } else {
16371
16700
  nodeTypesToCollect = /* @__PURE__ */ new Set([
16372
16701
  // Java AST nodes
@@ -16397,7 +16726,41 @@ async function analyze(code, filePath, language, options = {}) {
16397
16726
  }
16398
16727
  }
16399
16728
  }
16400
- const baseConfig = options.taintConfig ?? getDefaultConfig();
16729
+ let baseConfig = options.taintConfig ?? getDefaultConfig();
16730
+ if (!options.taintConfig) {
16731
+ const plugin = getLanguagePlugin(language);
16732
+ if (plugin) {
16733
+ const pluginSources = plugin.getBuiltinSources();
16734
+ const pluginSinks = plugin.getBuiltinSinks();
16735
+ if (pluginSources.length > 0 || pluginSinks.length > 0) {
16736
+ baseConfig = {
16737
+ ...baseConfig,
16738
+ sources: [
16739
+ ...baseConfig.sources,
16740
+ ...pluginSources.map((s) => ({
16741
+ method: s.method,
16742
+ class: s.class,
16743
+ annotation: s.annotation,
16744
+ type: s.type,
16745
+ severity: s.severity,
16746
+ return_tainted: s.returnTainted ?? false
16747
+ }))
16748
+ ],
16749
+ sinks: [
16750
+ ...baseConfig.sinks,
16751
+ ...pluginSinks.map((s) => ({
16752
+ method: s.method,
16753
+ class: s.class,
16754
+ type: s.type,
16755
+ cwe: s.cwe,
16756
+ severity: s.severity,
16757
+ arg_positions: s.argPositions
16758
+ }))
16759
+ ]
16760
+ };
16761
+ }
16762
+ }
16763
+ }
16401
16764
  const preliminaryTaint = analyzeTaint(calls, types, baseConfig);
16402
16765
  const taintedParameters = [];
16403
16766
  for (const source of preliminaryTaint.sources) {
@@ -5546,6 +5546,9 @@ function extractCalls(tree, cache, language) {
5546
5546
  const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
5547
5547
  const isPython = detectedLanguage === "python";
5548
5548
  const isRust = detectedLanguage === "rust";
5549
+ if (detectedLanguage === "bash") {
5550
+ return extractBashCalls(tree, cache);
5551
+ }
5549
5552
  if (isRust) {
5550
5553
  return extractRustCalls(tree, cache);
5551
5554
  }
@@ -6235,6 +6238,88 @@ function inferTypeFromReceiverName(receiver) {
6235
6238
  const lowerReceiver = receiver.toLowerCase();
6236
6239
  return patterns[lowerReceiver] ?? null;
6237
6240
  }
6241
+ function extractBashCalls(tree, cache) {
6242
+ const calls = [];
6243
+ const commands = getNodesFromCache(tree.rootNode, "command", cache);
6244
+ for (const cmd of commands) {
6245
+ const callInfo = extractBashCommandInfo(cmd);
6246
+ if (callInfo) {
6247
+ calls.push(callInfo);
6248
+ }
6249
+ }
6250
+ return calls;
6251
+ }
6252
+ function extractBashCommandInfo(node) {
6253
+ const nameNode = node.childForFieldName("name");
6254
+ if (!nameNode) return null;
6255
+ const commandName = getNodeText(nameNode);
6256
+ if (!commandName) return null;
6257
+ const args2 = [];
6258
+ let position = 0;
6259
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6260
+ const child = node.child(i2);
6261
+ if (!child) continue;
6262
+ if (child === nameNode) continue;
6263
+ if (child.type.includes("redirect") || child.type === "heredoc_body" || child.type === "file_descriptor") {
6264
+ continue;
6265
+ }
6266
+ const expression = getNodeText(child);
6267
+ if (!expression.trim()) continue;
6268
+ const variable = extractBashVariableRef(child);
6269
+ args2.push({
6270
+ position: position++,
6271
+ expression,
6272
+ variable,
6273
+ literal: null
6274
+ });
6275
+ }
6276
+ const inMethod = findBashEnclosingFunction(node);
6277
+ return {
6278
+ method_name: commandName,
6279
+ receiver: null,
6280
+ arguments: args2,
6281
+ location: {
6282
+ line: node.startPosition.row + 1,
6283
+ column: node.startPosition.column
6284
+ },
6285
+ in_method: inMethod,
6286
+ resolved: false,
6287
+ resolution: { status: "external_method" }
6288
+ };
6289
+ }
6290
+ function extractBashVariableRef(node) {
6291
+ const type = node.type;
6292
+ if (type === "simple_expansion" || type === "expansion") {
6293
+ return getNodeText(node).replace(/^\$\{?/, "").replace(/\}$/, "");
6294
+ }
6295
+ if (type === "string" || type === "concatenation") {
6296
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6297
+ const child = node.child(i2);
6298
+ if (!child) continue;
6299
+ if (child.type === "simple_expansion" || child.type === "expansion") {
6300
+ return getNodeText(child).replace(/^\$\{?/, "").replace(/\}$/, "");
6301
+ }
6302
+ }
6303
+ }
6304
+ if (type === "word") {
6305
+ const text = getNodeText(node);
6306
+ if (text.startsWith("$")) {
6307
+ return text.slice(1).replace(/^\{/, "").replace(/\}$/, "");
6308
+ }
6309
+ }
6310
+ return null;
6311
+ }
6312
+ function findBashEnclosingFunction(node) {
6313
+ let current = node.parent;
6314
+ while (current) {
6315
+ if (current.type === "function_definition") {
6316
+ const nameNode = current.childForFieldName("name");
6317
+ return nameNode ? getNodeText(nameNode) : null;
6318
+ }
6319
+ current = current.parent;
6320
+ }
6321
+ return null;
6322
+ }
6238
6323
  function extractPythonCalls(tree, cache) {
6239
6324
  const calls = [];
6240
6325
  const context = buildPythonResolutionContext(tree, cache);
@@ -5481,6 +5481,9 @@ function extractCalls(tree, cache, language) {
5481
5481
  const isJavaScript = detectedLanguage === "javascript" || detectedLanguage === "typescript";
5482
5482
  const isPython = detectedLanguage === "python";
5483
5483
  const isRust = detectedLanguage === "rust";
5484
+ if (detectedLanguage === "bash") {
5485
+ return extractBashCalls(tree, cache);
5486
+ }
5484
5487
  if (isRust) {
5485
5488
  return extractRustCalls(tree, cache);
5486
5489
  }
@@ -6170,6 +6173,88 @@ function inferTypeFromReceiverName(receiver) {
6170
6173
  const lowerReceiver = receiver.toLowerCase();
6171
6174
  return patterns[lowerReceiver] ?? null;
6172
6175
  }
6176
+ function extractBashCalls(tree, cache) {
6177
+ const calls = [];
6178
+ const commands = getNodesFromCache(tree.rootNode, "command", cache);
6179
+ for (const cmd of commands) {
6180
+ const callInfo = extractBashCommandInfo(cmd);
6181
+ if (callInfo) {
6182
+ calls.push(callInfo);
6183
+ }
6184
+ }
6185
+ return calls;
6186
+ }
6187
+ function extractBashCommandInfo(node) {
6188
+ const nameNode = node.childForFieldName("name");
6189
+ if (!nameNode) return null;
6190
+ const commandName = getNodeText(nameNode);
6191
+ if (!commandName) return null;
6192
+ const args2 = [];
6193
+ let position = 0;
6194
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6195
+ const child = node.child(i2);
6196
+ if (!child) continue;
6197
+ if (child === nameNode) continue;
6198
+ if (child.type.includes("redirect") || child.type === "heredoc_body" || child.type === "file_descriptor") {
6199
+ continue;
6200
+ }
6201
+ const expression = getNodeText(child);
6202
+ if (!expression.trim()) continue;
6203
+ const variable = extractBashVariableRef(child);
6204
+ args2.push({
6205
+ position: position++,
6206
+ expression,
6207
+ variable,
6208
+ literal: null
6209
+ });
6210
+ }
6211
+ const inMethod = findBashEnclosingFunction(node);
6212
+ return {
6213
+ method_name: commandName,
6214
+ receiver: null,
6215
+ arguments: args2,
6216
+ location: {
6217
+ line: node.startPosition.row + 1,
6218
+ column: node.startPosition.column
6219
+ },
6220
+ in_method: inMethod,
6221
+ resolved: false,
6222
+ resolution: { status: "external_method" }
6223
+ };
6224
+ }
6225
+ function extractBashVariableRef(node) {
6226
+ const type = node.type;
6227
+ if (type === "simple_expansion" || type === "expansion") {
6228
+ return getNodeText(node).replace(/^\$\{?/, "").replace(/\}$/, "");
6229
+ }
6230
+ if (type === "string" || type === "concatenation") {
6231
+ for (let i2 = 0; i2 < node.childCount; i2++) {
6232
+ const child = node.child(i2);
6233
+ if (!child) continue;
6234
+ if (child.type === "simple_expansion" || child.type === "expansion") {
6235
+ return getNodeText(child).replace(/^\$\{?/, "").replace(/\}$/, "");
6236
+ }
6237
+ }
6238
+ }
6239
+ if (type === "word") {
6240
+ const text = getNodeText(node);
6241
+ if (text.startsWith("$")) {
6242
+ return text.slice(1).replace(/^\{/, "").replace(/\}$/, "");
6243
+ }
6244
+ }
6245
+ return null;
6246
+ }
6247
+ function findBashEnclosingFunction(node) {
6248
+ let current = node.parent;
6249
+ while (current) {
6250
+ if (current.type === "function_definition") {
6251
+ const nameNode = current.childForFieldName("name");
6252
+ return nameNode ? getNodeText(nameNode) : null;
6253
+ }
6254
+ current = current.parent;
6255
+ }
6256
+ return null;
6257
+ }
6173
6258
  function extractPythonCalls(tree, cache) {
6174
6259
  const calls = [];
6175
6260
  const context = buildPythonResolutionContext(tree, cache);