cognium-dev 3.193.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 +416 -3
  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) {
@@ -33426,6 +33459,375 @@ class DeserializationSafetyGatePass {
33426
33459
  }
33427
33460
  }
33428
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
+
33429
33831
  // ../circle-ir/dist/analysis/passes/cli-main-reflection-suppress-pass.js
33430
33832
  var REFLECTION_SINK_METHODS = new Set([
33431
33833
  "forName",
@@ -45134,6 +45536,9 @@ async function analyze(code, filePath, language, options = {}) {
45134
45536
  pipeline.add(new SinkSemanticsPass);
45135
45537
  if (!disabledPasses.has("deserialization-safety-gate"))
45136
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));
45137
45542
  if (!disabledPasses.has("cli-main-reflection-suppress"))
45138
45543
  pipeline.add(new CliMainReflectionSuppressPass);
45139
45544
  if (!disabledPasses.has("library-profile-sink-gate"))
@@ -45268,6 +45673,14 @@ async function analyze(code, filePath, language, options = {}) {
45268
45673
  flows: interProc.additionalFlows,
45269
45674
  interprocedural: interProc.interprocedural
45270
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
+ }
45271
45684
  if (taint.flows && taint.flows.length > 0 && taint.sinks.length > 0) {
45272
45685
  const sinkTagsByKey = new Map;
45273
45686
  for (const s of taint.sinks) {
@@ -46079,7 +46492,7 @@ var colors = {
46079
46492
  };
46080
46493
 
46081
46494
  // src/version.ts
46082
- var version = "3.193.0";
46495
+ var version = "3.194.0";
46083
46496
 
46084
46497
  // src/formatters.ts
46085
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.193.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.193.0"
69
+ "circle-ir": "^3.194.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^25.5.0",