cognium-dev 3.192.0 → 3.194.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +502 -14
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -9350,6 +9350,7 @@ var GO_KEYWORDS = new Set([
9350
9350
  function buildGoDFG(tree) {
9351
9351
  const defs = [];
9352
9352
  const uses = [];
9353
+ const extraChains = [];
9353
9354
  let defIdCounter = 1;
9354
9355
  let useIdCounter = 1;
9355
9356
  const scopeStack = [new Map];
@@ -9371,7 +9372,7 @@ function buildGoDFG(tree) {
9371
9372
  }
9372
9373
  const body2 = func2.childForFieldName("body");
9373
9374
  if (body2) {
9374
- processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter });
9375
+ processGoBlock(body2, defs, uses, scopeStack, { defId: defIdCounter, useId: useIdCounter }, extraChains);
9375
9376
  defIdCounter = defs.length + 1;
9376
9377
  useIdCounter = uses.length + 1;
9377
9378
  }
@@ -9387,6 +9388,15 @@ function buildGoDFG(tree) {
9387
9388
  }
9388
9389
  }
9389
9390
  const chains = computeChains(defs, uses);
9391
+ const seen = new Set(chains.map((c) => `${c.from_def}|${c.to_def}|${c.via}`));
9392
+ for (const ch of extraChains) {
9393
+ const key = `${ch.from_def}|${ch.to_def}|${ch.via}`;
9394
+ if (!seen.has(key)) {
9395
+ seen.add(key);
9396
+ chains.push(ch);
9397
+ }
9398
+ }
9399
+ chains.sort((a, b) => a.from_def - b.from_def || a.to_def - b.to_def);
9390
9400
  return { defs, uses, chains };
9391
9401
  }
9392
9402
  function extractGoParamDefs(params, defs, _startId, scopeStack) {
@@ -9415,28 +9425,51 @@ function extractGoParamDefs(params, defs, _startId, scopeStack) {
9415
9425
  }
9416
9426
  }
9417
9427
  }
9418
- function processGoBlock(node, defs, uses, scopeStack, counters) {
9428
+ function processGoBlock(node, defs, uses, scopeStack, counters, extraChains = []) {
9429
+ const linkMultiLineRhs = (right, useStart, useEnd, defStart, defEnd) => {
9430
+ if (right.endPosition.row <= right.startPosition.row)
9431
+ return;
9432
+ for (let ui = useStart;ui < useEnd; ui++) {
9433
+ const u = uses[ui];
9434
+ if (!u || u.def_id === null)
9435
+ continue;
9436
+ for (let di = defStart;di < defEnd; di++) {
9437
+ const d = defs[di];
9438
+ if (d && d.kind === "local" && d.id !== u.def_id) {
9439
+ extraChains.push({ from_def: u.def_id, to_def: d.id, via: u.variable });
9440
+ }
9441
+ }
9442
+ }
9443
+ };
9419
9444
  walkTree(node, (child) => {
9420
9445
  if (child.type === "short_var_declaration") {
9421
9446
  const left = child.childForFieldName("left");
9422
9447
  const right = child.childForFieldName("right");
9448
+ const usesBefore = uses.length;
9423
9449
  if (right) {
9424
9450
  extractGoUses(right, uses, scopeStack);
9425
9451
  }
9452
+ const defsBefore = defs.length;
9426
9453
  if (left) {
9427
9454
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
9428
9455
  }
9456
+ if (right)
9457
+ linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
9429
9458
  } else if (child.type === "var_declaration") {
9430
9459
  processGoVarDecl(child, defs, scopeStack, counters);
9431
9460
  } else if (child.type === "assignment_statement") {
9432
9461
  const left = child.childForFieldName("left");
9433
9462
  const right = child.childForFieldName("right");
9463
+ const usesBefore = uses.length;
9434
9464
  if (right) {
9435
9465
  extractGoUses(right, uses, scopeStack);
9436
9466
  }
9467
+ const defsBefore = defs.length;
9437
9468
  if (left) {
9438
9469
  extractGoLhsDefs(left, defs, scopeStack, child.startPosition.row + 1);
9439
9470
  }
9471
+ if (right)
9472
+ linkMultiLineRhs(right, usesBefore, uses.length, defsBefore, defs.length);
9440
9473
  } else if (child.type === "for_statement") {
9441
9474
  const rangeClause = findChildByTypeGo(child, "range_clause");
9442
9475
  if (rangeClause) {
@@ -15947,22 +15980,36 @@ class CrossFileResolver {
15947
15980
  if (source.type === "interprocedural_param")
15948
15981
  continue;
15949
15982
  const sourceVar = source.variable ?? this.getLocalDefVarAt(ir, source.line);
15983
+ const reachable = sourceVar ? this.collectTaintReachable(ir, sourceVar, source.line, filePath) : undefined;
15950
15984
  for (const call of ir.calls) {
15951
15985
  if (call.location.line < source.line)
15952
15986
  continue;
15953
15987
  const resolved = this.resolveCall(call, filePath);
15954
15988
  if (!resolved || resolved.targetFile === filePath)
15955
15989
  continue;
15956
- if (sourceVar) {
15957
- const argMentions = call.arguments.some((arg) => {
15958
- if (arg.variable === sourceVar)
15959
- return true;
15960
- if (arg.expression && new RegExp(`\\b${sourceVar}\\b`).test(arg.expression))
15961
- return true;
15962
- return false;
15963
- });
15964
- if (!argMentions)
15990
+ if (reachable) {
15991
+ let matchedName;
15992
+ for (const arg of call.arguments) {
15993
+ if (arg.variable && reachable.names.has(arg.variable)) {
15994
+ matchedName = arg.variable;
15995
+ break;
15996
+ }
15997
+ if (arg.expression) {
15998
+ for (const v of reachable.names) {
15999
+ if (new RegExp(`\\b${v}\\b`).test(arg.expression)) {
16000
+ matchedName = v;
16001
+ break;
16002
+ }
16003
+ }
16004
+ if (matchedName)
16005
+ break;
16006
+ }
16007
+ }
16008
+ if (!matchedName)
16009
+ continue;
16010
+ if (this.taintClobbered(ir, call.location.line, matchedName, reachable.defIds)) {
15965
16011
  continue;
16012
+ }
15966
16013
  }
15967
16014
  const targetIR = this.fileIRs.get(resolved.targetFile);
15968
16015
  if (!targetIR || targetIR.taint.sinks.length === 0)
@@ -16023,7 +16070,8 @@ class CrossFileResolver {
16023
16070
  file: callerFile,
16024
16071
  line: src.line,
16025
16072
  type: src.type,
16026
- hopChain: [{ file: callerFile, line: src.line, method: method.name, kind: "source" }]
16073
+ hopChain: [{ file: callerFile, line: src.line, method: method.name, kind: "source" }],
16074
+ defIds: this.collectTaintReachable(callerIR, src.variable, src.line, callerFile).defIds
16027
16075
  });
16028
16076
  }
16029
16077
  const callsInMethod = callerIdx.callsByMethod.get(method) ?? [];
@@ -16052,7 +16100,8 @@ class CrossFileResolver {
16052
16100
  file: sourceFile,
16053
16101
  line: sourceLine,
16054
16102
  type: sourceType,
16055
- hopChain: baseChain
16103
+ hopChain: baseChain,
16104
+ defIds: this.forwardReachableDefs(callerIR, [def.id], callerFile)
16056
16105
  });
16057
16106
  }
16058
16107
  }
@@ -16065,6 +16114,9 @@ class CrossFileResolver {
16065
16114
  const matched = this.matchTaintedArg(arg, tainted);
16066
16115
  if (!matched)
16067
16116
  continue;
16117
+ if (this.taintClobbered(callerIR, call.location.line, matched.var, matched.origin.defIds)) {
16118
+ continue;
16119
+ }
16068
16120
  const calleeNode = methodIndex.get(resolved.targetMethod);
16069
16121
  if (!calleeNode)
16070
16122
  continue;
@@ -16398,6 +16450,62 @@ class CrossFileResolver {
16398
16450
  }
16399
16451
  return;
16400
16452
  }
16453
+ collectTaintReachable(ir, sourceVar, sourceLine, filePath) {
16454
+ const defs = ir.dfg.defs;
16455
+ let seedDefs = defs.filter((d) => d.variable === sourceVar && d.line === sourceLine);
16456
+ if (seedDefs.length === 0) {
16457
+ const named = defs.filter((d) => d.variable === sourceVar).sort((a, b) => a.line - b.line);
16458
+ if (named.length > 0)
16459
+ seedDefs = [named[0]];
16460
+ }
16461
+ const defIds = this.forwardReachableDefs(ir, seedDefs.map((d) => d.id), filePath);
16462
+ const names = new Set([sourceVar]);
16463
+ for (const d of defs) {
16464
+ if (d.variable && defIds.has(d.id))
16465
+ names.add(d.variable);
16466
+ }
16467
+ return { names, defIds };
16468
+ }
16469
+ forwardReachableDefs(ir, seedDefIds, filePath) {
16470
+ const defIds = new Set(seedDefIds);
16471
+ const chains = ir.dfg.chains;
16472
+ if (!chains || chains.length === 0 || seedDefIds.length === 0)
16473
+ return defIds;
16474
+ const lineByDefId = new Map;
16475
+ for (const d of ir.dfg.defs)
16476
+ lineByDefId.set(d.id, d.line);
16477
+ const crossFileCallLines = new Set;
16478
+ for (const call of ir.calls) {
16479
+ const resolved = this.resolveCall(call, filePath);
16480
+ if (resolved && resolved.targetFile !== filePath) {
16481
+ crossFileCallLines.add(call.location.line);
16482
+ }
16483
+ }
16484
+ const frontier = [...seedDefIds];
16485
+ const visited = new Set(frontier);
16486
+ while (frontier.length > 0) {
16487
+ const cur = frontier.shift();
16488
+ for (const ch of chains) {
16489
+ if (ch.from_def !== cur || visited.has(ch.to_def))
16490
+ continue;
16491
+ visited.add(ch.to_def);
16492
+ const toLine = lineByDefId.get(ch.to_def);
16493
+ if (toLine !== undefined && crossFileCallLines.has(toLine))
16494
+ continue;
16495
+ defIds.add(ch.to_def);
16496
+ frontier.push(ch.to_def);
16497
+ }
16498
+ }
16499
+ return defIds;
16500
+ }
16501
+ taintClobbered(ir, callLine, varName, taintDefIds) {
16502
+ if (!taintDefIds || taintDefIds.size === 0)
16503
+ return false;
16504
+ const use = ir.dfg.uses.find((u) => u.line === callLine && u.variable === varName && u.def_id !== null);
16505
+ if (!use || use.def_id === null)
16506
+ return false;
16507
+ return !taintDefIds.has(use.def_id);
16508
+ }
16401
16509
  findRealSourceLineInMethod(ir, method) {
16402
16510
  for (const src of ir.taint.sources) {
16403
16511
  if (src.type === "interprocedural_param")
@@ -33351,6 +33459,375 @@ class DeserializationSafetyGatePass {
33351
33459
  }
33352
33460
  }
33353
33461
 
33462
+ // ../circle-ir/dist/analysis/passes/prompt-injection-safety-gate-pass.js
33463
+ function isDelimiterLiteral(lit) {
33464
+ const s = lit.trim();
33465
+ if (s.length === 0)
33466
+ return true;
33467
+ return /^<\/?[\w.-]+\s*\/?>$/.test(s) || /^\[\/?[\w.-]+\]$/.test(s) || /^`{1,3}[\w-]*$/.test(s) || /^"""[\w-]*$/.test(s) || /^[#*=_~-]{2,}$/.test(s) || /^<{2,}[\w-]*$/.test(s) || /^[\w-]*>{2,}$/.test(s);
33468
+ }
33469
+ function scanValue(code, start2) {
33470
+ let depth = 0;
33471
+ let str = null;
33472
+ let out2 = "";
33473
+ for (let i2 = start2;i2 < code.length; i2++) {
33474
+ const c = code[i2];
33475
+ if (str) {
33476
+ out2 += c;
33477
+ if (c === str && code[i2 - 1] !== "\\")
33478
+ str = null;
33479
+ continue;
33480
+ }
33481
+ if (c === '"' || c === "'" || c === "`") {
33482
+ str = c;
33483
+ out2 += c;
33484
+ continue;
33485
+ }
33486
+ if (c === "(" || c === "[" || c === "{") {
33487
+ depth++;
33488
+ out2 += c;
33489
+ continue;
33490
+ }
33491
+ if (c === ")" || c === "]" || c === "}") {
33492
+ if (depth === 0)
33493
+ break;
33494
+ depth--;
33495
+ out2 += c;
33496
+ continue;
33497
+ }
33498
+ if (c === "," && depth === 0)
33499
+ break;
33500
+ out2 += c;
33501
+ }
33502
+ return out2.trim();
33503
+ }
33504
+ function stringLiterals(expr) {
33505
+ return [...expr.matchAll(/"([^"]*)"|'([^']*)'/g)].map((m) => m[1] ?? m[2] ?? "");
33506
+ }
33507
+ function hasIdentifier(expr) {
33508
+ return /[A-Za-z_$][\w$]*/.test(expr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " "));
33509
+ }
33510
+ function isPureLiteral(valueExpr) {
33511
+ const stripped = valueExpr.replace(/"[^"]*"|'[^']*'|`[^`]*`/g, " ");
33512
+ return !/[A-Za-z_$][\w$]*/.test(stripped);
33513
+ }
33514
+ function isInstructionConcat(valueExpr) {
33515
+ if (!valueExpr.includes("+"))
33516
+ return false;
33517
+ if (!hasIdentifier(valueExpr))
33518
+ return false;
33519
+ const lits = stringLiterals(valueExpr);
33520
+ if (lits.length === 0)
33521
+ return false;
33522
+ return !lits.every(isDelimiterLiteral);
33523
+ }
33524
+ function isMixedTemplate(v) {
33525
+ const fstr = v.match(/\bf(["'])((?:\\.|(?!\1).)*)\1/);
33526
+ if (fstr) {
33527
+ const body2 = fstr[2];
33528
+ if (/\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\{[^}]*\}/g, "")))
33529
+ return true;
33530
+ }
33531
+ const tmpl = v.match(/`([^`]*)`/);
33532
+ if (tmpl) {
33533
+ const body2 = tmpl[1];
33534
+ if (/\$\{[^}]+\}/.test(body2) && /\w/.test(body2.replace(/\$\{[^}]*\}/g, "")))
33535
+ return true;
33536
+ }
33537
+ return false;
33538
+ }
33539
+ function assignmentRe(varName, flags2 = "") {
33540
+ return new RegExp(`\\b${varName}\\s*(?::=|(?<![=<>!])=(?!=))\\s*`, flags2);
33541
+ }
33542
+ function resolveAssignmentRHS(varName, codeLines) {
33543
+ const re = assignmentRe(varName);
33544
+ for (const line of codeLines) {
33545
+ const m = line.match(re);
33546
+ if (m && m.index !== undefined) {
33547
+ return scanValue(line, m.index + m[0].length);
33548
+ }
33549
+ }
33550
+ return;
33551
+ }
33552
+ function classifyExpr(v) {
33553
+ if (isMixedTemplate(v))
33554
+ return "unsafe";
33555
+ if (v.includes("+")) {
33556
+ if (isInstructionConcat(v))
33557
+ return "unsafe";
33558
+ const lits = stringLiterals(v);
33559
+ if (lits.length > 0 && lits.every(isDelimiterLiteral) && hasIdentifier(v))
33560
+ return "safe";
33561
+ return "unknown";
33562
+ }
33563
+ return "unknown";
33564
+ }
33565
+ function classifyContentPlacement(val, codeLines) {
33566
+ const v = val.trim();
33567
+ if (/^[A-Za-z_$][\w$]*$/.test(v)) {
33568
+ const rhs = resolveAssignmentRHS(v, codeLines);
33569
+ if (rhs === undefined)
33570
+ return "unknown";
33571
+ return classifyExpr(rhs);
33572
+ }
33573
+ return classifyExpr(v);
33574
+ }
33575
+ function classifyPromptCall(callCode, codeLines) {
33576
+ const contentRe = /["']?[Cc]ontent["']?\s*[:=]\s*/g;
33577
+ let m;
33578
+ let sawDynamic = false;
33579
+ let allSafe = true;
33580
+ while ((m = contentRe.exec(callCode)) !== null) {
33581
+ const val = scanValue(callCode, m.index + m[0].length);
33582
+ if (val === "" || isPureLiteral(val))
33583
+ continue;
33584
+ sawDynamic = true;
33585
+ const ctx = callCode.slice(Math.max(0, m.index - 80), m.index);
33586
+ const roleM = ctx.match(/["']?[Rr]ole["']?\s*[:=]\s*["'](\w+)["'][^"']*$/);
33587
+ const role = roleM ? roleM[1].toLowerCase() : "user";
33588
+ if (role === "system" || role === "assistant")
33589
+ return "unsafe";
33590
+ const placement = classifyContentPlacement(val, codeLines);
33591
+ if (placement === "unsafe")
33592
+ return "unsafe";
33593
+ if (placement !== "safe")
33594
+ allSafe = false;
33595
+ }
33596
+ if (!sawDynamic)
33597
+ return "unknown";
33598
+ return allSafe ? "safe" : "unknown";
33599
+ }
33600
+ function scanRhsAcrossLines(text, start2) {
33601
+ let depth = 0;
33602
+ let str = null;
33603
+ let out2 = "";
33604
+ for (let i2 = start2;i2 < text.length; i2++) {
33605
+ const c = text[i2];
33606
+ if (str) {
33607
+ out2 += c;
33608
+ if (c === str && text[i2 - 1] !== "\\")
33609
+ str = null;
33610
+ continue;
33611
+ }
33612
+ if (c === '"' || c === "'" || c === "`") {
33613
+ str = c;
33614
+ out2 += c;
33615
+ continue;
33616
+ }
33617
+ if (c === "(" || c === "[" || c === "{") {
33618
+ depth++;
33619
+ out2 += c;
33620
+ continue;
33621
+ }
33622
+ if (c === ")" || c === "]" || c === "}") {
33623
+ if (depth === 0)
33624
+ break;
33625
+ depth--;
33626
+ out2 += c;
33627
+ continue;
33628
+ }
33629
+ if (c === `
33630
+ ` && depth === 0)
33631
+ break;
33632
+ out2 += c;
33633
+ }
33634
+ return out2.trim();
33635
+ }
33636
+ function resolveBuilderRhs(varName, joined) {
33637
+ const re = assignmentRe(varName, "g");
33638
+ const m = re.exec(joined);
33639
+ if (m)
33640
+ return scanRhsAcrossLines(joined, m.index + m[0].length);
33641
+ return;
33642
+ }
33643
+ function collectBuilderRegion(callCode, joined) {
33644
+ const parts2 = [];
33645
+ const seen = new Set;
33646
+ const stack = [...callCode.matchAll(/[A-Za-z_$][\w$]*/g)].map((x) => x[0]);
33647
+ let budget = 40;
33648
+ while (stack.length > 0 && budget-- > 0) {
33649
+ const v = stack.pop();
33650
+ if (seen.has(v))
33651
+ continue;
33652
+ seen.add(v);
33653
+ const rhs = resolveBuilderRhs(v, joined);
33654
+ if (rhs) {
33655
+ parts2.push(rhs);
33656
+ for (const mm of rhs.matchAll(/[A-Za-z_$][\w$]*/g))
33657
+ stack.push(mm[0]);
33658
+ }
33659
+ }
33660
+ return parts2.join(`
33661
+ `);
33662
+ }
33663
+ function classifyPromptSink(callCode, codeLines) {
33664
+ const direct = classifyPromptCall(callCode, codeLines);
33665
+ if (direct !== "unknown")
33666
+ return direct;
33667
+ const region = collectBuilderRegion(callCode, codeLines.join(`
33668
+ `));
33669
+ if (!region)
33670
+ return "unknown";
33671
+ return classifyPromptCall(region, codeLines);
33672
+ }
33673
+
33674
+ class PromptInjectionSafetyGatePass {
33675
+ name = "prompt-injection-safety-gate";
33676
+ category = "security";
33677
+ run(ctx) {
33678
+ const { graph, code } = ctx;
33679
+ const sinks = ctx.hasResult("sink-filter") ? ctx.getResult("sink-filter").sinks : graph.ir.taint.sinks;
33680
+ if (sinks.length === 0)
33681
+ return { droppedSafe: 0 };
33682
+ const codeLines = code.split(`
33683
+ `);
33684
+ let droppedSafe = 0;
33685
+ const kept = sinks.filter((sink) => {
33686
+ if (sink.type !== "prompt_injection")
33687
+ return true;
33688
+ const callCode = sink.code ?? codeLines[sink.line - 1] ?? "";
33689
+ if (!callCode)
33690
+ return true;
33691
+ if (classifyPromptSink(callCode, codeLines) === "safe") {
33692
+ droppedSafe++;
33693
+ return false;
33694
+ }
33695
+ return true;
33696
+ });
33697
+ if (droppedSafe > 0) {
33698
+ sinks.length = 0;
33699
+ sinks.push(...kept);
33700
+ }
33701
+ return { droppedSafe };
33702
+ }
33703
+ }
33704
+
33705
+ // ../circle-ir/dist/analysis/passes/speculative-prompt-param-source-pass.js
33706
+ function returnOrAssignRhs(text) {
33707
+ const t = text.trim();
33708
+ const ret = t.match(/^return\s+(.+?);?$/);
33709
+ if (ret)
33710
+ return ret[1];
33711
+ const asg = t.match(/^[A-Za-z_$][\w$.]*\s*(?::=|(?<![=<>!])=(?!=))\s*(.+?);?$/);
33712
+ if (asg)
33713
+ return asg[1];
33714
+ return;
33715
+ }
33716
+ function looksLikePromptText(lit) {
33717
+ const s = lit.trim();
33718
+ if (/\b(you are|you're|assistant|system prompt|follow (the )?(policy|instructions|rules)|act as|your task|instructions?\s*:|ignore (all )?previous|do not reveal|respond (to|with|as)|answer the (question|user)|you (must|should|will)|helpful (ai|assistant))\b/i.test(s)) {
33719
+ return true;
33720
+ }
33721
+ const words = s.split(/\s+/).filter(Boolean);
33722
+ return words.length >= 4 && s.length >= 20;
33723
+ }
33724
+ function literalsOf(expr) {
33725
+ return [...expr.matchAll(/"([^"]*)"|'([^']*)'|`([^`]*)`/g)].map((m) => m[1] ?? m[2] ?? m[3] ?? "");
33726
+ }
33727
+ function detectPromptConstructionFlows(codeLines, types) {
33728
+ const flows = [];
33729
+ const seen = new Set;
33730
+ for (const type of types) {
33731
+ for (const method of type.methods) {
33732
+ const params = (method.parameters ?? []).map((p) => ({ name: p.name, line: p.line ?? method.start_line })).filter((p) => p.name && p.name !== "_");
33733
+ if (params.length === 0)
33734
+ continue;
33735
+ for (let line = method.start_line;line <= method.end_line; line++) {
33736
+ const text = codeLines[line - 1] ?? "";
33737
+ const rhs = returnOrAssignRhs(text);
33738
+ if (!rhs || !isInstructionConcat(rhs))
33739
+ continue;
33740
+ if (!literalsOf(rhs).some(looksLikePromptText))
33741
+ continue;
33742
+ const param = params.find((p) => new RegExp(`\\b${p.name}\\b`).test(rhs));
33743
+ if (!param)
33744
+ continue;
33745
+ const key = `${param.line}|${line}`;
33746
+ if (seen.has(key))
33747
+ continue;
33748
+ seen.add(key);
33749
+ flows.push({
33750
+ source_line: param.line,
33751
+ sink_line: line,
33752
+ source_type: "http_body",
33753
+ sink_type: "prompt_injection",
33754
+ path: [
33755
+ { variable: param.name, line: param.line, type: "source" },
33756
+ { variable: param.name, line, type: "sink" }
33757
+ ],
33758
+ confidence: 1,
33759
+ sanitized: false
33760
+ });
33761
+ }
33762
+ }
33763
+ }
33764
+ return flows;
33765
+ }
33766
+ function looksLikeTextParam(type) {
33767
+ if (!type)
33768
+ return true;
33769
+ const t = type.trim();
33770
+ if (t.startsWith("*") || t.startsWith("&"))
33771
+ return false;
33772
+ if (/\b(Client|Context|Writer|Reader|Conn|DB|Logger|Handler|Server|Request|Response|Pool|Session|Engine|Service|Repository|Config)\b/.test(t)) {
33773
+ return false;
33774
+ }
33775
+ return /(?:^|\b)(string|str|String|text|any|object|interface\{\}|\[\]byte|\[\]string|List\[str\]|Optional\[str\])(?:\b|$)/i.test(t) || /^[A-Za-z_$][\w$<>[\], |]*$/.test(t);
33776
+ }
33777
+
33778
+ class SpeculativePromptParamSourcePass {
33779
+ enabled;
33780
+ name = "speculative-prompt-param-source";
33781
+ category = "security";
33782
+ constructor(enabled) {
33783
+ this.enabled = enabled;
33784
+ }
33785
+ run(ctx) {
33786
+ if (!this.enabled)
33787
+ return { added: 0 };
33788
+ if (!ctx.hasResult("sink-filter"))
33789
+ return { added: 0 };
33790
+ const sinkFilter = ctx.getResult("sink-filter");
33791
+ const { types } = ctx.graph.ir;
33792
+ const promptSinks = sinkFilter.sinks.filter((s) => s.type === "prompt_injection");
33793
+ if (promptSinks.length === 0)
33794
+ return { added: 0 };
33795
+ const existing = new Set(sinkFilter.sources.filter((s) => typeof s.variable === "string").map((s) => `${s.variable}:${s.line}`));
33796
+ const added = [];
33797
+ for (const type of types) {
33798
+ for (const method of type.methods) {
33799
+ const hasPromptSink = promptSinks.some((s) => s.line >= method.start_line && s.line <= method.end_line);
33800
+ if (!hasPromptSink)
33801
+ continue;
33802
+ for (const param of method.parameters) {
33803
+ if (!param.name || param.name === "_")
33804
+ continue;
33805
+ if (!looksLikeTextParam(param.type))
33806
+ continue;
33807
+ const line = param.line ?? method.start_line;
33808
+ const key = `${param.name}:${line}`;
33809
+ if (existing.has(key))
33810
+ continue;
33811
+ existing.add(key);
33812
+ added.push({
33813
+ type: "http_body",
33814
+ location: `speculative untrusted parameter '${param.name}' in ${method.name}`,
33815
+ severity: "high",
33816
+ line,
33817
+ confidence: 1,
33818
+ variable: param.name,
33819
+ in_method: method.name
33820
+ });
33821
+ }
33822
+ }
33823
+ }
33824
+ if (added.length > 0) {
33825
+ sinkFilter.sources.push(...added);
33826
+ }
33827
+ return { added: added.length };
33828
+ }
33829
+ }
33830
+
33354
33831
  // ../circle-ir/dist/analysis/passes/cli-main-reflection-suppress-pass.js
33355
33832
  var REFLECTION_SINK_METHODS = new Set([
33356
33833
  "forName",
@@ -45059,6 +45536,9 @@ async function analyze(code, filePath, language, options = {}) {
45059
45536
  pipeline.add(new SinkSemanticsPass);
45060
45537
  if (!disabledPasses.has("deserialization-safety-gate"))
45061
45538
  pipeline.add(new DeserializationSafetyGatePass(options.dependencyContext));
45539
+ if (!disabledPasses.has("prompt-injection-safety-gate"))
45540
+ pipeline.add(new PromptInjectionSafetyGatePass);
45541
+ pipeline.add(new SpeculativePromptParamSourcePass(options.speculativeParamSources === true));
45062
45542
  if (!disabledPasses.has("cli-main-reflection-suppress"))
45063
45543
  pipeline.add(new CliMainReflectionSuppressPass);
45064
45544
  if (!disabledPasses.has("library-profile-sink-gate"))
@@ -45193,6 +45673,14 @@ async function analyze(code, filePath, language, options = {}) {
45193
45673
  flows: interProc.additionalFlows,
45194
45674
  interprocedural: interProc.interprocedural
45195
45675
  };
45676
+ if (options.speculativeParamSources === true) {
45677
+ const existingFlowKeys = new Set((taint.flows ?? []).map((f) => `${f.source_line}|${f.sink_line}|${f.sink_type}`));
45678
+ const constructionFlows = detectPromptConstructionFlows(code.split(`
45679
+ `), types).filter((f) => !existingFlowKeys.has(`${f.source_line}|${f.sink_line}|${f.sink_type}`));
45680
+ if (constructionFlows.length > 0) {
45681
+ taint.flows = [...taint.flows ?? [], ...constructionFlows];
45682
+ }
45683
+ }
45196
45684
  if (taint.flows && taint.flows.length > 0 && taint.sinks.length > 0) {
45197
45685
  const sinkTagsByKey = new Map;
45198
45686
  for (const s of taint.sinks) {
@@ -46004,7 +46492,7 @@ var colors = {
46004
46492
  };
46005
46493
 
46006
46494
  // src/version.ts
46007
- var version = "3.192.0";
46495
+ var version = "3.194.0";
46008
46496
 
46009
46497
  // src/formatters.ts
46010
46498
  var LIBRARY_API_SURFACE_TAG2 = "library-api-surface:caller-responsibility";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.192.0",
3
+ "version": "3.194.0",
4
4
  "description": "Static Application Security Testing CLI for detecting security vulnerabilities via taint tracking",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@cognium/project-profile-detect": "^1.1.0",
69
- "circle-ir": "^3.192.0"
69
+ "circle-ir": "^3.194.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^25.5.0",