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.
- package/dist/cli.js +502 -14
- 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 (
|
|
15957
|
-
|
|
15958
|
-
|
|
15959
|
-
|
|
15960
|
-
|
|
15961
|
-
|
|
15962
|
-
|
|
15963
|
-
|
|
15964
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
69
|
+
"circle-ir": "^3.194.0"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@types/node": "^25.5.0",
|