circle-ir 4.13.2 → 4.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +3 -3
  2. package/configs/sinks/golang.json +20 -0
  3. package/dist/analysis/config-loader.d.ts.map +1 -1
  4. package/dist/analysis/config-loader.js +7 -2
  5. package/dist/analysis/config-loader.js.map +1 -1
  6. package/dist/analysis/interprocedural.js +1 -1
  7. package/dist/analysis/interprocedural.js.map +1 -1
  8. package/dist/analysis/passes/library-profile-source-gate-pass.d.ts +19 -18
  9. package/dist/analysis/passes/library-profile-source-gate-pass.d.ts.map +1 -1
  10. package/dist/analysis/passes/library-profile-source-gate-pass.js +16 -15
  11. package/dist/analysis/passes/library-profile-source-gate-pass.js.map +1 -1
  12. package/dist/analysis/passes/scan-secrets-pass.d.ts +2 -2
  13. package/dist/analysis/passes/scan-secrets-pass.d.ts.map +1 -1
  14. package/dist/analysis/passes/scan-secrets-pass.js +23 -7
  15. package/dist/analysis/passes/scan-secrets-pass.js.map +1 -1
  16. package/dist/analysis/passes/sink-filter-pass.d.ts.map +1 -1
  17. package/dist/analysis/passes/sink-filter-pass.js +47 -14
  18. package/dist/analysis/passes/sink-filter-pass.js.map +1 -1
  19. package/dist/analysis/taint-matcher.d.ts.map +1 -1
  20. package/dist/analysis/taint-matcher.js +49 -9
  21. package/dist/analysis/taint-matcher.js.map +1 -1
  22. package/dist/analysis/taint-propagation.d.ts +0 -6
  23. package/dist/analysis/taint-propagation.d.ts.map +1 -1
  24. package/dist/analysis/taint-propagation.js +12 -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 +40 -11
  28. package/dist/analyzer.js.map +1 -1
  29. package/dist/browser/circle-ir.js +193 -31
  30. package/dist/core/circle-ir-core.cjs +75 -16
  31. package/dist/core/circle-ir-core.js +75 -16
  32. package/dist/core/extractors/calls.js +18 -1
  33. package/dist/core/extractors/calls.js.map +1 -1
  34. package/dist/core/extractors/dfg.js +38 -8
  35. package/dist/core/extractors/dfg.js.map +1 -1
  36. package/dist/languages/plugins/go.d.ts.map +1 -1
  37. package/dist/languages/plugins/go.js +40 -0
  38. package/dist/languages/plugins/go.js.map +1 -1
  39. package/package.json +1 -1
@@ -6184,6 +6184,13 @@ function csharpBareName(node) {
6184
6184
  }
6185
6185
  return getNodeText(node);
6186
6186
  }
6187
+ function csharpCreatedSimpleType(creation) {
6188
+ const typeNode = creation.childForFieldName("type");
6189
+ if (!typeNode) return null;
6190
+ const bare = csharpBareName(typeNode).replace(/<[^>]*>/g, "");
6191
+ const simple = bare.split(".").pop()?.trim() ?? "";
6192
+ return simple && simple !== "var" ? simple : null;
6193
+ }
6187
6194
  function extractCSharpCalls(tree, cache) {
6188
6195
  const calls = [];
6189
6196
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -6192,11 +6199,15 @@ function extractCSharpCalls(tree, cache) {
6192
6199
  const fn = inv.childForFieldName("function");
6193
6200
  let methodName = "unknown";
6194
6201
  let receiver = null;
6202
+ let receiverTypeFromCreation = null;
6195
6203
  if (fn?.type === "member_access_expression") {
6196
6204
  const nameNode = fn.childForFieldName("name");
6197
6205
  const exprNode = fn.childForFieldName("expression");
6198
6206
  methodName = nameNode ? csharpBareName(nameNode) : "unknown";
6199
6207
  receiver = exprNode ? getNodeText(exprNode) : null;
6208
+ if (exprNode?.type === "object_creation_expression") {
6209
+ receiverTypeFromCreation = csharpCreatedSimpleType(exprNode);
6210
+ }
6200
6211
  } else if (fn) {
6201
6212
  methodName = csharpBareName(fn);
6202
6213
  }
@@ -6214,7 +6225,7 @@ function extractCSharpCalls(tree, cache) {
6214
6225
  calls.push({
6215
6226
  method_name: methodName,
6216
6227
  receiver,
6217
- receiver_type: receiver ? typeMap.get(receiver) ?? null : null,
6228
+ receiver_type: receiverTypeFromCreation ?? (receiver ? typeMap.get(receiver) ?? null : null),
6218
6229
  receiver_type_fqn: null,
6219
6230
  arguments: args2,
6220
6231
  location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
@@ -10185,6 +10196,36 @@ function extractFieldDefs(body2, startId) {
10185
10196
  function extractJSParameterDefs(params, startId) {
10186
10197
  const defs = [];
10187
10198
  let id = startId;
10199
+ const addPatternDefs = (patternNode, line) => {
10200
+ if (patternNode.type === "identifier" || patternNode.type === "shorthand_property_identifier_pattern") {
10201
+ defs.push({
10202
+ id: id++,
10203
+ variable: getNodeText(patternNode),
10204
+ line,
10205
+ kind: "param"
10206
+ });
10207
+ return;
10208
+ }
10209
+ if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
10210
+ const nameNode = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
10211
+ if (!nameNode) return;
10212
+ addPatternDefs(nameNode, line);
10213
+ return;
10214
+ }
10215
+ if (patternNode.type === "assignment_pattern") {
10216
+ const leftNode = patternNode.childForFieldName("left");
10217
+ if (leftNode) addPatternDefs(leftNode, line);
10218
+ return;
10219
+ }
10220
+ if (patternNode.type === "object_pattern" || patternNode.type === "array_pattern") {
10221
+ const destructDefs = extractDestructuringDefs(patternNode, id);
10222
+ for (const def of destructDefs) {
10223
+ def.kind = "param";
10224
+ defs.push(def);
10225
+ id++;
10226
+ }
10227
+ }
10228
+ };
10188
10229
  for (let i2 = 0; i2 < params.childCount; i2++) {
10189
10230
  const param = params.child(i2);
10190
10231
  if (!param) continue;
@@ -10232,14 +10273,7 @@ function extractJSParameterDefs(params, startId) {
10232
10273
  }
10233
10274
  } else if (param.type === "required_parameter" || param.type === "optional_parameter") {
10234
10275
  const patternNode = param.childForFieldName("pattern");
10235
- if (patternNode && patternNode.type === "identifier") {
10236
- defs.push({
10237
- id: id++,
10238
- variable: getNodeText(patternNode),
10239
- line: param.startPosition.row + 1,
10240
- kind: "param"
10241
- });
10242
- }
10276
+ if (patternNode) addPatternDefs(patternNode, param.startPosition.row + 1);
10243
10277
  }
10244
10278
  }
10245
10279
  return defs;
@@ -11995,6 +12029,9 @@ var DEFAULT_SOURCES = [
11995
12029
  // entry.getName() returns a path that may contain ../ — flowing into File()/FileOutputStream()
11996
12030
  // is a classic Zip-Slip vulnerability.
11997
12031
  { method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
12032
+ // JarEntry extends ZipEntry. A declared JarEntry does not match the ZipEntry
12033
+ // row, and a receiver not named `entry` misses the name heuristic. cognium-dev#620.
12034
+ { method: "getName", class: "JarEntry", type: "file_input", severity: "high", return_tainted: true },
11998
12035
  { method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
11999
12036
  { method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
12000
12037
  { method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
@@ -12751,8 +12788,10 @@ var DEFAULT_SINKS = [
12751
12788
  { method: "checkout", class: "SCM", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12752
12789
  { method: "retrieve", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12753
12790
  { method: "add", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12754
- // Jenkins CPS Flow Execution (constructor)
12755
- { method: "CpsFlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12791
+ // Jenkins CPS compiles the script argument. That is code injection (CWE-94),
12792
+ // the same class as GroovyShell.evaluate. Registered as command_injection it
12793
+ // was dropped by the CWE-78 constructor allowlist (#129). cognium-dev#619.
12794
+ { method: "CpsFlowExecution", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
12756
12795
  { method: "CpsFlowDefinition", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12757
12796
  { method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12758
12797
  // ActiveMQ control commands
@@ -15750,7 +15789,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
15750
15789
  if (skipMethods.includes(method.name)) continue;
15751
15790
  for (const param of method.parameters) {
15752
15791
  const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
15753
- const isTaintable = hasCSharpBindingAttr ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
15792
+ const isDestructureBinding = param.name.startsWith("{") || param.name.startsWith("[");
15793
+ const isTaintable = hasCSharpBindingAttr ? true : isDestructureBinding ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
15754
15794
  if (isTaintable) {
15755
15795
  const paramLine = param.line ?? method.start_line;
15756
15796
  sources.push({
@@ -15980,12 +16020,14 @@ function isInterproceduralTaintableType(typeName, language) {
15980
16020
  }
15981
16021
  function isParameterizedQueryCall(call, pattern) {
15982
16022
  if (pattern.type !== "sql_injection") return false;
15983
- const queryArg = call.arguments.find((a) => a.position === 0);
16023
+ const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
16024
+ const queryArg = call.arguments.find((a) => a.position === queryPos);
15984
16025
  if (queryArg) {
15985
16026
  const queryText = queryArg.literal ?? queryArg.expression ?? "";
15986
16027
  const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
15987
16028
  const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
15988
- if (hasPlaceholders && !hasConcatenation && call.arguments.length >= 2) {
16029
+ const hasBoundArg = queryPos === 0 ? call.arguments.length >= 2 : call.arguments.some((a) => a.position > queryPos);
16030
+ if (hasPlaceholders && !hasConcatenation && hasBoundArg) {
15989
16031
  return true;
15990
16032
  }
15991
16033
  }
@@ -16635,6 +16677,14 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
16635
16677
  }
16636
16678
  return false;
16637
16679
  }
16680
+ function isArgumentlessJsResponseWriter(call, pattern, language) {
16681
+ if (pattern.type !== "xss" || pattern.class !== "Response") return false;
16682
+ if (language !== "javascript" && language !== "typescript") return false;
16683
+ const positions = pattern.arg_positions;
16684
+ if (!positions || positions.length === 0) return false;
16685
+ const argc = call.arguments?.length ?? 0;
16686
+ return positions.every((pos) => pos >= argc);
16687
+ }
16638
16688
  function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
16639
16689
  const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
16640
16690
  const sinkMap = /* @__PURE__ */ new Map();
@@ -16695,6 +16745,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
16695
16745
  continue;
16696
16746
  }
16697
16747
  }
16748
+ if (isArgumentlessJsResponseWriter(call, pattern, language)) {
16749
+ continue;
16750
+ }
16698
16751
  if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
16699
16752
  continue;
16700
16753
  }
@@ -19226,6 +19279,9 @@ function buildSanitizersByLine(sanitizers) {
19226
19279
  }
19227
19280
  return out2;
19228
19281
  }
19282
+ function concatOutsideStrings(expression) {
19283
+ return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
19284
+ }
19229
19285
  function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
19230
19286
  let graph;
19231
19287
  let sources;
@@ -19257,6 +19313,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
19257
19313
  }
19258
19314
  const taintedVars = [];
19259
19315
  const flows = [];
19316
+ const javaConcat = graph.ir.meta.language === "java";
19260
19317
  const reachableSinks = /* @__PURE__ */ new Map();
19261
19318
  const defsByLine = graph.defsByLine;
19262
19319
  const usesByLine = graph.usesByLine;
@@ -19293,12 +19350,14 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
19293
19350
  continue;
19294
19351
  }
19295
19352
  }
19296
- const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
19353
+ const expr = typeof arg.expression === "string" ? arg.expression : "";
19354
+ const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
19355
+ const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
19297
19356
  {
19298
19357
  for (const use of candidateUses) {
19299
19358
  if (use.def_id !== null) {
19300
19359
  if (allTaintedDefIds.has(use.def_id)) {
19301
- if (!arg.variable) {
19360
+ if (!arg.variable || compoundConcat) {
19302
19361
  if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
19303
19362
  const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
19304
19363
  if (!re.test(arg.expression)) continue;
@@ -19961,6 +20020,8 @@ function analyzeInterprocedural(graphOrTypes, callsOrSources, dfgOrSinks, source
19961
20020
  "QueryRowContext",
19962
20021
  "Exec",
19963
20022
  "ExecContext",
20023
+ "Prepare",
20024
+ "PrepareContext",
19964
20025
  // Go html/template escape helpers — explicit safe utilities.
19965
20026
  // Belt-and-suspenders with the sanitizer config so external_taint_escape
19966
20027
  // is suppressed regardless of class-prefix matching. (cognium-dev #102 FP-27)
@@ -26230,6 +26291,16 @@ var VuePlugin = class extends BaseLanguagePlugin {
26230
26291
  };
26231
26292
 
26232
26293
  // src/languages/plugins/go.ts
26294
+ function sqlSinks(cls, methods) {
26295
+ return methods.map(([method, argPositions]) => ({
26296
+ method,
26297
+ class: cls,
26298
+ type: "sql_injection",
26299
+ cwe: "CWE-89",
26300
+ severity: "critical",
26301
+ argPositions
26302
+ }));
26303
+ }
26233
26304
  var GoPlugin = class extends BaseLanguagePlugin {
26234
26305
  id = "go";
26235
26306
  name = "Go";
@@ -26451,6 +26522,35 @@ var GoPlugin = class extends BaseLanguagePlugin {
26451
26522
  severity: "critical",
26452
26523
  argPositions: [0]
26453
26524
  },
26525
+ // database/sql context and transaction forms (#583). The query is
26526
+ // argument 0 for the plain forms and argument 1 for the *Context
26527
+ // forms (argument 0 is the context). Conn is the same shape as DB.
26528
+ ...sqlSinks("DB", [
26529
+ ["QueryContext", [1]],
26530
+ ["QueryRowContext", [1]],
26531
+ ["ExecContext", [1]],
26532
+ ["Prepare", [0]],
26533
+ ["PrepareContext", [1]]
26534
+ ]),
26535
+ ...sqlSinks("Tx", [
26536
+ ["Exec", [0]],
26537
+ ["QueryRow", [0]],
26538
+ ["QueryContext", [1]],
26539
+ ["QueryRowContext", [1]],
26540
+ ["ExecContext", [1]],
26541
+ ["Prepare", [0]],
26542
+ ["PrepareContext", [1]]
26543
+ ]),
26544
+ ...sqlSinks("Conn", [
26545
+ ["Query", [0]],
26546
+ ["QueryRow", [0]],
26547
+ ["Exec", [0]],
26548
+ ["QueryContext", [1]],
26549
+ ["QueryRowContext", [1]],
26550
+ ["ExecContext", [1]],
26551
+ ["Prepare", [0]],
26552
+ ["PrepareContext", [1]]
26553
+ ]),
26454
26554
  // Command Injection — argPositions intentionally empty so all variadic
26455
26555
  // positions are scanned. `exec.Command("sh", "-c", taintedCmd)` puts the
26456
26556
  // injection at arg[2]; `exec.Command(taintedName, args...)` puts it at
@@ -37274,6 +37374,20 @@ function evaluateSimpleExpression(expr, symbols) {
37274
37374
  if (result !== null && !isNaN(result)) return String(Math.floor(result));
37275
37375
  return expr;
37276
37376
  }
37377
+ var ARGUMENT_IDENT_SKIP = /* @__PURE__ */ new Set(["new", "true", "false", "null", "this", "super"]);
37378
+ function isStringConcat(expression) {
37379
+ return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
37380
+ }
37381
+ function argumentIdentifiers(expression) {
37382
+ const stripped = expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ");
37383
+ const found = stripped.match(/[A-Za-z_$][A-Za-z0-9_$]*/g) ?? [];
37384
+ const out2 = [];
37385
+ for (const id of found) {
37386
+ if (ARGUMENT_IDENT_SKIP.has(id)) continue;
37387
+ if (!out2.includes(id)) out2.push(id);
37388
+ }
37389
+ return out2;
37390
+ }
37277
37391
  function isStringLiteralExpression(expr) {
37278
37392
  const trimmed = expr.trim();
37279
37393
  if (trimmed.length < 2) return false;
@@ -37345,20 +37459,27 @@ function filterCleanVariableSinks(sinks, calls, taintedVars, symbols, dfg, sanit
37345
37459
  dangerousArgCount++;
37346
37460
  if (language === "bash" && arg.expression === call.method_name && !arg.variable && arg.literal == null) continue;
37347
37461
  if (arg.variable && !arg.expression?.includes("[")) {
37348
- const varName = arg.variable;
37349
- const scopedName = methodName ? `${methodName}:${varName}` : varName;
37350
- if (fieldNames.has(varName) && !isInSynchronizedBlock) {
37351
- allArgsAreClean = false;
37352
- continue;
37353
- }
37354
- if (sanitizedVars?.has(scopedName) || sanitizedVars?.has(varName)) continue;
37355
- if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
37356
- allArgsAreClean = false;
37357
- continue;
37462
+ const expr = arg.expression ?? "";
37463
+ const concatNames = language === "java" && isStringConcat(expr) ? argumentIdentifiers(expr) : [];
37464
+ const names = concatNames.length > 0 ? concatNames : [arg.variable];
37465
+ let argClean = true;
37466
+ for (const varName of names) {
37467
+ const scopedName = methodName ? `${methodName}:${varName}` : varName;
37468
+ if (fieldNames.has(varName) && !isInSynchronizedBlock) {
37469
+ argClean = false;
37470
+ break;
37471
+ }
37472
+ if (sanitizedVars?.has(scopedName) || sanitizedVars?.has(varName)) continue;
37473
+ if (taintedVars.has(scopedName) || taintedVars.has(varName)) {
37474
+ argClean = false;
37475
+ break;
37476
+ }
37477
+ const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
37478
+ if (symbolValue && symbolValue.type !== "unknown") continue;
37479
+ argClean = false;
37480
+ break;
37358
37481
  }
37359
- const symbolValue = symbols.get(scopedName) ?? symbols.get(varName);
37360
- if (symbolValue && symbolValue.type !== "unknown") continue;
37361
- allArgsAreClean = false;
37482
+ if (!argClean) allArgsAreClean = false;
37362
37483
  } else {
37363
37484
  if (arg.literal != null) continue;
37364
37485
  if (arg.expression && !arg.variable && isStringLiteralExpression(arg.expression)) continue;
@@ -44259,7 +44380,7 @@ function isGeneratedFile(file) {
44259
44380
  var PROVIDER_PATTERNS = [
44260
44381
  {
44261
44382
  name: "AWS access key",
44262
- regex: /\bAKIA[0-9A-Z]{16}\b/,
44383
+ regex: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
44263
44384
  severity: "critical",
44264
44385
  level: "error",
44265
44386
  fix: "Rotate the AWS access key immediately and move it to an environment variable or AWS Secrets Manager."
@@ -44334,6 +44455,22 @@ var PROVIDER_PATTERNS = [
44334
44455
  level: "warning",
44335
44456
  fix: "Publishable keys are not secret but should still not be checked in to back-end source files; verify front-end vs back-end context."
44336
44457
  },
44458
+ // Test-mode secret and restricted keys still authorize the test account.
44459
+ // Same length floor as the live forms. Severity is high, not critical (#576).
44460
+ {
44461
+ name: "Stripe test secret key",
44462
+ regex: /\bsk_test_[A-Za-z0-9]{24,}\b/,
44463
+ severity: "high",
44464
+ level: "warning",
44465
+ fix: "Rotate the Stripe test secret key in the Stripe Dashboard and load it from a secrets manager."
44466
+ },
44467
+ {
44468
+ name: "Stripe test restricted key",
44469
+ regex: /\brk_test_[A-Za-z0-9]{24,}\b/,
44470
+ severity: "high",
44471
+ level: "warning",
44472
+ fix: "Rotate the Stripe test restricted key in the Stripe Dashboard and load it from a secrets manager."
44473
+ },
44337
44474
  {
44338
44475
  name: "OpenAI API key",
44339
44476
  regex: /\bsk-[A-Za-z0-9]{48}\b/,
@@ -44633,7 +44770,7 @@ var ScanSecretsPass = class {
44633
44770
  for (const pattern of PROVIDER_PATTERNS) {
44634
44771
  const m = pattern.regex.exec(lineText);
44635
44772
  if (!m) continue;
44636
- if ((pattern.name === "Stripe live restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
44773
+ if ((pattern.name === "Stripe live restricted key" || pattern.name === "Stripe test secret key" || pattern.name === "Stripe test restricted key" || pattern.name === "GitLab personal access token") && PLACEHOLDER_RE.test(m[0])) {
44637
44774
  continue;
44638
44775
  }
44639
44776
  if (pattern.name === "PEM private key" && !pemHasInlineBody(lines, i2)) {
@@ -49402,6 +49539,22 @@ function longestLineLength(code) {
49402
49539
  return max;
49403
49540
  }
49404
49541
  var MAX_ANALYZABLE_LINE_LENGTH = 1e4;
49542
+ function neutralizeOverlongLines(code) {
49543
+ if (longestLineLength(code) <= MAX_ANALYZABLE_LINE_LENGTH) {
49544
+ return { code, dropped: 0 };
49545
+ }
49546
+ let out2 = "";
49547
+ let start2 = 0;
49548
+ let dropped = 0;
49549
+ for (let i2 = 0; i2 <= code.length; i2++) {
49550
+ if (i2 !== code.length && code.charCodeAt(i2) !== 10) continue;
49551
+ if (i2 - start2 > MAX_ANALYZABLE_LINE_LENGTH) dropped++;
49552
+ else if (i2 > start2) out2 += code.slice(start2, i2);
49553
+ if (i2 < code.length) out2 += "\n";
49554
+ start2 = i2 + 1;
49555
+ }
49556
+ return { code: out2, dropped };
49557
+ }
49405
49558
  async function analyze(code, filePath, language, options = {}) {
49406
49559
  if (!initialized) {
49407
49560
  await initAnalyzer(options);
@@ -49420,6 +49573,15 @@ async function analyze(code, filePath, language, options = {}) {
49420
49573
  parseGrammar = "tsx";
49421
49574
  }
49422
49575
  }
49576
+ const neutralized = neutralizeOverlongLines(code);
49577
+ if (neutralized.dropped > 0) {
49578
+ logger.warn("Neutralized over-long lines before analysis", {
49579
+ filePath,
49580
+ language,
49581
+ droppedLines: neutralized.dropped
49582
+ });
49583
+ code = neutralized.code;
49584
+ }
49423
49585
  logger.debug("Analyzing file", { filePath, language, parseGrammar, codeLength: code.length });
49424
49586
  const phaseTimingEnabled = globalThis.__circleIrPassTiming === true;
49425
49587
  const phaseStart = () => phaseTimingEnabled ? Date.now() : 0;
@@ -6230,6 +6230,13 @@ function csharpBareName(node) {
6230
6230
  }
6231
6231
  return getNodeText(node);
6232
6232
  }
6233
+ function csharpCreatedSimpleType(creation) {
6234
+ const typeNode = creation.childForFieldName("type");
6235
+ if (!typeNode) return null;
6236
+ const bare = csharpBareName(typeNode).replace(/<[^>]*>/g, "");
6237
+ const simple = bare.split(".").pop()?.trim() ?? "";
6238
+ return simple && simple !== "var" ? simple : null;
6239
+ }
6233
6240
  function extractCSharpCalls(tree, cache) {
6234
6241
  const calls = [];
6235
6242
  const typeMap = buildCSharpReceiverTypeMap(tree, cache);
@@ -6238,11 +6245,15 @@ function extractCSharpCalls(tree, cache) {
6238
6245
  const fn = inv.childForFieldName("function");
6239
6246
  let methodName = "unknown";
6240
6247
  let receiver = null;
6248
+ let receiverTypeFromCreation = null;
6241
6249
  if (fn?.type === "member_access_expression") {
6242
6250
  const nameNode = fn.childForFieldName("name");
6243
6251
  const exprNode = fn.childForFieldName("expression");
6244
6252
  methodName = nameNode ? csharpBareName(nameNode) : "unknown";
6245
6253
  receiver = exprNode ? getNodeText(exprNode) : null;
6254
+ if (exprNode?.type === "object_creation_expression") {
6255
+ receiverTypeFromCreation = csharpCreatedSimpleType(exprNode);
6256
+ }
6246
6257
  } else if (fn) {
6247
6258
  methodName = csharpBareName(fn);
6248
6259
  }
@@ -6260,7 +6271,7 @@ function extractCSharpCalls(tree, cache) {
6260
6271
  calls.push({
6261
6272
  method_name: methodName,
6262
6273
  receiver,
6263
- receiver_type: receiver ? typeMap.get(receiver) ?? null : null,
6274
+ receiver_type: receiverTypeFromCreation ?? (receiver ? typeMap.get(receiver) ?? null : null),
6264
6275
  receiver_type_fqn: null,
6265
6276
  arguments: args2,
6266
6277
  location: { line: inv.startPosition.row + 1, column: inv.startPosition.column },
@@ -10231,6 +10242,36 @@ function extractFieldDefs(body2, startId) {
10231
10242
  function extractJSParameterDefs(params, startId) {
10232
10243
  const defs = [];
10233
10244
  let id = startId;
10245
+ const addPatternDefs = (patternNode, line) => {
10246
+ if (patternNode.type === "identifier" || patternNode.type === "shorthand_property_identifier_pattern") {
10247
+ defs.push({
10248
+ id: id++,
10249
+ variable: getNodeText(patternNode),
10250
+ line,
10251
+ kind: "param"
10252
+ });
10253
+ return;
10254
+ }
10255
+ if (patternNode.type === "rest_pattern" || patternNode.type === "rest_element") {
10256
+ const nameNode = patternNode.namedChildCount > 0 ? patternNode.namedChild(0) : null;
10257
+ if (!nameNode) return;
10258
+ addPatternDefs(nameNode, line);
10259
+ return;
10260
+ }
10261
+ if (patternNode.type === "assignment_pattern") {
10262
+ const leftNode = patternNode.childForFieldName("left");
10263
+ if (leftNode) addPatternDefs(leftNode, line);
10264
+ return;
10265
+ }
10266
+ if (patternNode.type === "object_pattern" || patternNode.type === "array_pattern") {
10267
+ const destructDefs = extractDestructuringDefs(patternNode, id);
10268
+ for (const def of destructDefs) {
10269
+ def.kind = "param";
10270
+ defs.push(def);
10271
+ id++;
10272
+ }
10273
+ }
10274
+ };
10234
10275
  for (let i2 = 0; i2 < params.childCount; i2++) {
10235
10276
  const param = params.child(i2);
10236
10277
  if (!param) continue;
@@ -10278,14 +10319,7 @@ function extractJSParameterDefs(params, startId) {
10278
10319
  }
10279
10320
  } else if (param.type === "required_parameter" || param.type === "optional_parameter") {
10280
10321
  const patternNode = param.childForFieldName("pattern");
10281
- if (patternNode && patternNode.type === "identifier") {
10282
- defs.push({
10283
- id: id++,
10284
- variable: getNodeText(patternNode),
10285
- line: param.startPosition.row + 1,
10286
- kind: "param"
10287
- });
10288
- }
10322
+ if (patternNode) addPatternDefs(patternNode, param.startPosition.row + 1);
10289
10323
  }
10290
10324
  }
10291
10325
  return defs;
@@ -11414,6 +11448,9 @@ var DEFAULT_SOURCES = [
11414
11448
  // entry.getName() returns a path that may contain ../ — flowing into File()/FileOutputStream()
11415
11449
  // is a classic Zip-Slip vulnerability.
11416
11450
  { method: "getName", class: "ZipEntry", type: "file_input", severity: "high", return_tainted: true },
11451
+ // JarEntry extends ZipEntry. A declared JarEntry does not match the ZipEntry
11452
+ // row, and a receiver not named `entry` misses the name heuristic. cognium-dev#620.
11453
+ { method: "getName", class: "JarEntry", type: "file_input", severity: "high", return_tainted: true },
11417
11454
  { method: "getName", class: "ZipArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
11418
11455
  { method: "getName", class: "TarArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
11419
11456
  { method: "getName", class: "ArchiveEntry", type: "file_input", severity: "high", return_tainted: true },
@@ -12170,8 +12207,10 @@ var DEFAULT_SINKS = [
12170
12207
  { method: "checkout", class: "SCM", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12171
12208
  { method: "retrieve", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12172
12209
  { method: "add", class: "LibraryAdder", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12173
- // Jenkins CPS Flow Execution (constructor)
12174
- { method: "CpsFlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12210
+ // Jenkins CPS compiles the script argument. That is code injection (CWE-94),
12211
+ // the same class as GroovyShell.evaluate. Registered as command_injection it
12212
+ // was dropped by the CWE-78 constructor allowlist (#129). cognium-dev#619.
12213
+ { method: "CpsFlowExecution", class: "constructor", type: "code_injection", cwe: "CWE-94", severity: "critical", arg_positions: [0] },
12175
12214
  { method: "CpsFlowDefinition", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12176
12215
  { method: "FlowExecution", class: "constructor", type: "command_injection", cwe: "CWE-78", severity: "critical", arg_positions: [0] },
12177
12216
  // ActiveMQ control commands
@@ -15050,7 +15089,8 @@ function findSources(calls, types, patterns, sourceLines, language) {
15050
15089
  if (skipMethods.includes(method.name)) continue;
15051
15090
  for (const param of method.parameters) {
15052
15091
  const hasCSharpBindingAttr = language === "csharp" && param.annotations.some((a) => CSHARP_BINDING_ATTRS.has(a));
15053
- const isTaintable = hasCSharpBindingAttr ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
15092
+ const isDestructureBinding = param.name.startsWith("{") || param.name.startsWith("[");
15093
+ const isTaintable = hasCSharpBindingAttr ? true : isDestructureBinding ? true : param.type ? isInterproceduralTaintableType(param.type, language) : true;
15054
15094
  if (isTaintable) {
15055
15095
  const paramLine = param.line ?? method.start_line;
15056
15096
  sources.push({
@@ -15280,12 +15320,14 @@ function isInterproceduralTaintableType(typeName, language) {
15280
15320
  }
15281
15321
  function isParameterizedQueryCall(call, pattern) {
15282
15322
  if (pattern.type !== "sql_injection") return false;
15283
- const queryArg = call.arguments.find((a) => a.position === 0);
15323
+ const queryPos = pattern.arg_positions.length > 0 ? Math.min(...pattern.arg_positions) : 0;
15324
+ const queryArg = call.arguments.find((a) => a.position === queryPos);
15284
15325
  if (queryArg) {
15285
15326
  const queryText = queryArg.literal ?? queryArg.expression ?? "";
15286
15327
  const hasPlaceholders = /(\?(?:\s|,|$|\))|\$\d+|:\w+|%s)/.test(queryText);
15287
15328
  const hasConcatenation = /\+\s*[^+]/.test(queryText) || queryText.includes("${");
15288
- if (hasPlaceholders && !hasConcatenation && call.arguments.length >= 2) {
15329
+ const hasBoundArg = queryPos === 0 ? call.arguments.length >= 2 : call.arguments.some((a) => a.position > queryPos);
15330
+ if (hasPlaceholders && !hasConcatenation && hasBoundArg) {
15289
15331
  return true;
15290
15332
  }
15291
15333
  }
@@ -15935,6 +15977,14 @@ function isSafeJinjaRenderCall(call, pattern, language, sourceLines) {
15935
15977
  }
15936
15978
  return false;
15937
15979
  }
15980
+ function isArgumentlessJsResponseWriter(call, pattern, language) {
15981
+ if (pattern.type !== "xss" || pattern.class !== "Response") return false;
15982
+ if (language !== "javascript" && language !== "typescript") return false;
15983
+ const positions = pattern.arg_positions;
15984
+ if (!positions || positions.length === 0) return false;
15985
+ const argc = call.arguments?.length ?? 0;
15986
+ return positions.every((pos) => pos >= argc);
15987
+ }
15938
15988
  function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types) {
15939
15989
  const patternsForLanguage = language === void 0 ? patterns : patterns.filter((p) => sinkPatternAppliesTo(p, language));
15940
15990
  const sinkMap = /* @__PURE__ */ new Map();
@@ -15995,6 +16045,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines, types)
15995
16045
  continue;
15996
16046
  }
15997
16047
  }
16048
+ if (isArgumentlessJsResponseWriter(call, pattern, language)) {
16049
+ continue;
16050
+ }
15998
16051
  if ((pattern.type === "command_injection" || pattern.type === "sql_injection") && call.method_name === "exec" && (language === "javascript" || language === "typescript") && isRegexReceiver(call.receiver, sourceLines)) {
15999
16052
  continue;
16000
16053
  }
@@ -17191,6 +17244,9 @@ function buildSanitizersByLine(sanitizers) {
17191
17244
  }
17192
17245
  return out2;
17193
17246
  }
17247
+ function concatOutsideStrings(expression) {
17248
+ return expression.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, " ").includes("+");
17249
+ }
17194
17250
  function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanitizers, sanitizersArg) {
17195
17251
  let graph;
17196
17252
  let sources;
@@ -17222,6 +17278,7 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17222
17278
  }
17223
17279
  const taintedVars = [];
17224
17280
  const flows = [];
17281
+ const javaConcat = graph.ir.meta.language === "java";
17225
17282
  const reachableSinks = /* @__PURE__ */ new Map();
17226
17283
  const defsByLine = graph.defsByLine;
17227
17284
  const usesByLine = graph.usesByLine;
@@ -17258,12 +17315,14 @@ function propagateTaint(graphOrDfg, callsOrSources, sourcesOrSinks, sinksOrSanit
17258
17315
  continue;
17259
17316
  }
17260
17317
  }
17261
- const candidateUses = arg.variable ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
17318
+ const expr = typeof arg.expression === "string" ? arg.expression : "";
17319
+ const compoundConcat = javaConcat && !!arg.variable && expr !== arg.variable && concatOutsideStrings(expr);
17320
+ const candidateUses = arg.variable && !compoundConcat ? usesAtSink.filter((u) => u.variable === arg.variable) : usesAtSink;
17262
17321
  {
17263
17322
  for (const use of candidateUses) {
17264
17323
  if (use.def_id !== null) {
17265
17324
  if (allTaintedDefIds.has(use.def_id)) {
17266
- if (!arg.variable) {
17325
+ if (!arg.variable || compoundConcat) {
17267
17326
  if (typeof arg.expression !== "string" || arg.expression.length === 0) continue;
17268
17327
  const re = new RegExp(`(?:^|[^A-Za-z0-9_$])${use.variable.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:[^A-Za-z0-9_$]|$)`);
17269
17328
  if (!re.test(arg.expression)) continue;