cognium-dev 3.195.0 → 3.198.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 +417 -4
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -24573,6 +24573,9 @@ class LanguageSourcesPass {
24573
24573
  additionalSanitizers.push(...findPythonRegexAllowlistWrapperSanitizers(code));
24574
24574
  additionalSanitizers.push(...findPythonSetMembershipXssGuardSanitizers(code));
24575
24575
  additionalSanitizers.push(...findPythonTraversalRejectGuardSanitizers(code));
24576
+ additionalSanitizers.push(...findPythonStringLiteralGuardSanitizers(code));
24577
+ additionalSanitizers.push(...findPythonDefaultSaxParserXxeSanitizers(code));
24578
+ additionalSanitizers.push(...findPythonUrlAllowlistRedirectSanitizers(code));
24576
24579
  additionalSanitizers.push(...findPythonDefusedXmlSanitizers(code));
24577
24580
  additionalSanitizers.push(...findPythonJinjaAutoescapeSanitizers(code));
24578
24581
  const pyMisconfigFindings = findPythonPatternFindings(code, graph.ir.meta.file);
@@ -25167,15 +25170,165 @@ function findPythonAssignmentSources(sourceCode, language) {
25167
25170
  }
25168
25171
  return sources;
25169
25172
  }
25173
+ function evaluatePythonConstExpression(expr, consts) {
25174
+ const COMPARISON = /(<=|>=|==|!=|<|>)/;
25175
+ const parts2 = expr.split(COMPARISON);
25176
+ if (parts2.length === 3) {
25177
+ const left = evaluatePythonConstExpression(parts2[0], consts);
25178
+ const right = evaluatePythonConstExpression(parts2[2], consts);
25179
+ if (typeof left !== "number" || typeof right !== "number")
25180
+ return;
25181
+ switch (parts2[1]) {
25182
+ case "<":
25183
+ return left < right;
25184
+ case ">":
25185
+ return left > right;
25186
+ case "<=":
25187
+ return left <= right;
25188
+ case ">=":
25189
+ return left >= right;
25190
+ case "==":
25191
+ return left === right;
25192
+ case "!=":
25193
+ return left !== right;
25194
+ default:
25195
+ return;
25196
+ }
25197
+ }
25198
+ if (parts2.length !== 1)
25199
+ return;
25200
+ const matched = expr.match(/\d+|[A-Za-z_][A-Za-z0-9_]*|\/\/|[-+*/%()]|\S/g);
25201
+ if (!matched)
25202
+ return;
25203
+ const tokens = matched;
25204
+ let pos = 0;
25205
+ const peek = () => tokens[pos];
25206
+ const parseFactor = () => {
25207
+ const tok = tokens[pos++];
25208
+ if (tok === undefined)
25209
+ return;
25210
+ if (tok === "(") {
25211
+ const inner = parseSum();
25212
+ if (tokens[pos++] !== ")")
25213
+ return;
25214
+ return inner;
25215
+ }
25216
+ if (tok === "-") {
25217
+ const operand = parseFactor();
25218
+ return operand === undefined ? undefined : -operand;
25219
+ }
25220
+ if (/^\d+$/.test(tok))
25221
+ return Number(tok);
25222
+ if (/^[A-Za-z_]/.test(tok))
25223
+ return consts.get(tok);
25224
+ return;
25225
+ };
25226
+ const parseTerm = () => {
25227
+ let value = parseFactor();
25228
+ if (value === undefined)
25229
+ return;
25230
+ while (peek() === "*" || peek() === "/" || peek() === "//" || peek() === "%") {
25231
+ const op = tokens[pos++];
25232
+ const rhs = parseFactor();
25233
+ if (rhs === undefined)
25234
+ return;
25235
+ if ((op === "/" || op === "//" || op === "%") && rhs === 0)
25236
+ return;
25237
+ if (op === "*")
25238
+ value *= rhs;
25239
+ else if (op === "/")
25240
+ value /= rhs;
25241
+ else if (op === "//")
25242
+ value = Math.floor(value / rhs);
25243
+ else
25244
+ value = (value % rhs + rhs) % rhs;
25245
+ }
25246
+ return value;
25247
+ };
25248
+ function parseSum() {
25249
+ let value = parseTerm();
25250
+ if (value === undefined)
25251
+ return;
25252
+ while (peek() === "+" || peek() === "-") {
25253
+ const op = tokens[pos++];
25254
+ const rhs = parseTerm();
25255
+ if (rhs === undefined)
25256
+ return;
25257
+ value = op === "+" ? value + rhs : value - rhs;
25258
+ }
25259
+ return value;
25260
+ }
25261
+ const result = parseSum();
25262
+ return pos === tokens.length ? result : undefined;
25263
+ }
25264
+ function normalizeListIndex(index, length) {
25265
+ const resolved = index < 0 ? length + index : index;
25266
+ return resolved >= 0 && resolved < length ? resolved : undefined;
25267
+ }
25268
+ function computeDeadPythonBranchLines(lines) {
25269
+ const dead = new Set;
25270
+ const consts = new Map;
25271
+ const indentOf = (line) => line.length - line.trimStart().length;
25272
+ for (let i2 = 0;i2 < lines.length; i2++) {
25273
+ const line = lines[i2];
25274
+ const assign = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=(?!=)\s*(.+)$/);
25275
+ if (assign) {
25276
+ const value = evaluatePythonConstExpression(assign[2].trim(), consts);
25277
+ if (typeof value === "number")
25278
+ consts.set(assign[1], value);
25279
+ else
25280
+ consts.delete(assign[1]);
25281
+ }
25282
+ const ifMatch = line.match(/^(\s*)if\s+(.+?)\s*:\s*$/);
25283
+ if (!ifMatch)
25284
+ continue;
25285
+ const indent = ifMatch[1].length;
25286
+ const decided = evaluatePythonConstExpression(ifMatch[2].trim(), consts);
25287
+ if (decided !== true && decided !== false)
25288
+ continue;
25289
+ const thenStart = i2 + 1;
25290
+ let cursor = thenStart;
25291
+ while (cursor < lines.length && (lines[cursor].trim() === "" || indentOf(lines[cursor]) > indent)) {
25292
+ cursor++;
25293
+ }
25294
+ const thenEnd = cursor - 1;
25295
+ let elseStart = -1;
25296
+ let elseEnd = -1;
25297
+ if (cursor < lines.length && indentOf(lines[cursor]) === indent) {
25298
+ if (/^\s*elif\b/.test(lines[cursor]))
25299
+ continue;
25300
+ if (/^\s*else\s*:\s*$/.test(lines[cursor])) {
25301
+ elseStart = cursor + 1;
25302
+ let k = elseStart;
25303
+ while (k < lines.length && (lines[k].trim() === "" || indentOf(lines[k]) > indent))
25304
+ k++;
25305
+ elseEnd = k - 1;
25306
+ }
25307
+ }
25308
+ if (decided === true) {
25309
+ for (let l = elseStart;l >= 0 && l <= elseEnd; l++)
25310
+ dead.add(l);
25311
+ } else {
25312
+ for (let l = thenStart;l <= thenEnd; l++)
25313
+ dead.add(l);
25314
+ }
25315
+ }
25316
+ return dead;
25317
+ }
25170
25318
  function buildPythonTaintedVars(sourceCode) {
25171
25319
  const tainted = new Map;
25172
25320
  const containerTainted = new Map;
25173
25321
  const lines = sourceCode.split(`
25174
25322
  `);
25323
+ const deadBranchLines = computeDeadPythonBranchLines(lines);
25324
+ const listElems = new Map;
25325
+ const intConsts = new Map;
25175
25326
  for (let i2 = 0;i2 < lines.length; i2++) {
25176
25327
  const line = lines[i2];
25177
25328
  if (line.trimStart().startsWith("#"))
25178
25329
  continue;
25330
+ if (deadBranchLines.has(i2))
25331
+ continue;
25179
25332
  const subscriptAssign = line.match(/^\s*([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]\s*=\s*(.+)$/u);
25180
25333
  if (subscriptAssign) {
25181
25334
  const [, container, , key, rhs2] = subscriptAssign;
@@ -25194,11 +25347,54 @@ function buildPythonTaintedVars(sourceCode) {
25194
25347
  }
25195
25348
  const containerAppendMatch = line.match(/^\s*([\p{L}\p{N}_]+)\.(append|extend|insert|add|push|put|appendleft)\s*\(\s*(.+?)\s*\)\s*$/u);
25196
25349
  if (containerAppendMatch) {
25197
- const [, receiver, , argExpr] = containerAppendMatch;
25350
+ const [, receiver, method, argExpr] = containerAppendMatch;
25198
25351
  const argIsTainted = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(argExpr));
25199
25352
  const argIsDirectSource = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(argExpr));
25200
25353
  if (argIsTainted || argIsDirectSource)
25201
25354
  tainted.set(receiver, tainted.get(receiver) ?? i2 + 1);
25355
+ const elems = listElems.get(receiver);
25356
+ if (elems) {
25357
+ if (method === "append") {
25358
+ elems.push(argIsTainted || argIsDirectSource);
25359
+ } else if (method === "insert") {
25360
+ const insertAt = argExpr.match(/^(-?\d+)\s*,\s*(.+)$/);
25361
+ if (insertAt) {
25362
+ const idx = normalizeListIndex(Number(insertAt[1]), elems.length);
25363
+ const valueExpr = insertAt[2];
25364
+ const valueTainted = [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(valueExpr)) || PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(valueExpr));
25365
+ if (idx !== undefined)
25366
+ elems.splice(idx, 0, valueTainted);
25367
+ else
25368
+ listElems.delete(receiver);
25369
+ } else {
25370
+ listElems.delete(receiver);
25371
+ }
25372
+ } else {
25373
+ listElems.delete(receiver);
25374
+ }
25375
+ }
25376
+ continue;
25377
+ }
25378
+ const popMatch = line.match(/^\s*(?:([\p{L}\p{N}_]+)\s*=\s*)?([\p{L}\p{N}_]+)\.pop\s*\(\s*(-?\d+)?\s*\)\s*$/u);
25379
+ const delMatch = line.match(/^\s*del\s+([\p{L}\p{N}_]+)\s*\[\s*(-?\d+)\s*\]\s*$/u);
25380
+ if (popMatch || delMatch) {
25381
+ const receiver = popMatch ? popMatch[2] : delMatch[1];
25382
+ const rawIndex = popMatch ? popMatch[3] !== undefined ? Number(popMatch[3]) : -1 : Number(delMatch[2]);
25383
+ const elems = listElems.get(receiver);
25384
+ if (elems) {
25385
+ const idx = normalizeListIndex(rawIndex, elems.length);
25386
+ if (idx === undefined)
25387
+ listElems.delete(receiver);
25388
+ else {
25389
+ const [removed] = elems.splice(idx, 1);
25390
+ if (popMatch && popMatch[1]) {
25391
+ if (removed)
25392
+ tainted.set(popMatch[1], i2 + 1);
25393
+ else
25394
+ tainted.delete(popMatch[1]);
25395
+ }
25396
+ }
25397
+ }
25202
25398
  continue;
25203
25399
  }
25204
25400
  const augAssign = line.match(/^\s*([\p{L}\p{N}_]+)\s*\+=\s*(.+)$/u);
@@ -25221,7 +25417,51 @@ function buildPythonTaintedVars(sourceCode) {
25221
25417
  const assignMatch = line.match(/^\s*([\p{L}\p{N}_]+)\s*=\s*(.+)$/u);
25222
25418
  if (!assignMatch)
25223
25419
  continue;
25224
- const [, lhs, rhs] = assignMatch;
25420
+ const [, lhs, rhsRaw] = assignMatch;
25421
+ const constValue = evaluatePythonConstExpression(rhsRaw.trim(), intConsts);
25422
+ if (typeof constValue === "number")
25423
+ intConsts.set(lhs, constValue);
25424
+ else
25425
+ intConsts.delete(lhs);
25426
+ let rhs = rhsRaw;
25427
+ const ternary = rhsRaw.match(/^(.+?)\s+if\s+(.+?)\s+else\s+(.+)$/);
25428
+ if (ternary) {
25429
+ const decided = evaluatePythonConstExpression(ternary[2].trim(), intConsts);
25430
+ if (decided === true)
25431
+ rhs = ternary[1].trim();
25432
+ else if (decided === false)
25433
+ rhs = ternary[3].trim();
25434
+ }
25435
+ const listLiteral = rhs.trim().match(/^\[\s*(.*?)\s*\]$/);
25436
+ if (listLiteral) {
25437
+ const inner = listLiteral[1];
25438
+ if (inner === "") {
25439
+ listElems.set(lhs, []);
25440
+ } else if (!inner.includes("[") && !inner.includes("(")) {
25441
+ listElems.set(lhs, inner.split(",").map((part) => {
25442
+ const expr = part.trim();
25443
+ return [...tainted.keys()].some((v) => new RegExp(`(?<![\\p{L}\\p{N}_])${v}(?![\\p{L}\\p{N}_])`, "u").test(expr)) || PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(expr));
25444
+ }));
25445
+ } else {
25446
+ listElems.delete(lhs);
25447
+ }
25448
+ } else {
25449
+ listElems.delete(lhs);
25450
+ }
25451
+ const indexRead = rhs.trim().match(/^([\p{L}\p{N}_]+)\s*\[\s*(-?\d+)\s*\]$/u);
25452
+ if (indexRead) {
25453
+ const elems = listElems.get(indexRead[1]);
25454
+ if (elems) {
25455
+ const idx = normalizeListIndex(Number(indexRead[2]), elems.length);
25456
+ if (idx !== undefined) {
25457
+ if (elems[idx])
25458
+ tainted.set(lhs, i2 + 1);
25459
+ else
25460
+ tainted.delete(lhs);
25461
+ continue;
25462
+ }
25463
+ }
25464
+ }
25225
25465
  const isDirectSource = PYTHON_TAINTED_PATTERNS2.some((p) => p.pattern.test(rhs));
25226
25466
  let propagatedFrom;
25227
25467
  const dictAccessMatch = rhs.trim().match(/^([\p{L}\p{N}_]+)\[(['"])([^'"]+)\2\]$/u);
@@ -26539,6 +26779,147 @@ function findPythonTraversalRejectGuardSanitizers(code) {
26539
26779
  }
26540
26780
  return sanitizers;
26541
26781
  }
26782
+ function findPythonStringLiteralGuardSanitizers(code) {
26783
+ const sanitizers = [];
26784
+ const lines = code.split(`
26785
+ `);
26786
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
26787
+ const Q = `(?:'(?:\\\\')'|"'"|'"'|"(?:\\\\")")`;
26788
+ for (let i2 = 0;i2 < lines.length; i2++) {
26789
+ const guard = lines[i2].match(new RegExp(`^(\\s*)if\\s+not\\s+([A-Za-z_][A-Za-z0-9_]*)\\.startswith\\(\\s*${Q}\\s*\\)` + `\\s+or\\s+not\\s+\\2\\.endswith\\(\\s*${Q}\\s*\\)` + `\\s+or\\s+${Q}\\s+in\\s+\\2\\[1:-1\\]\\s*:\\s*$`));
26790
+ if (!guard)
26791
+ continue;
26792
+ const guardIndent = guard[1].length;
26793
+ const guardedVar = guard[2];
26794
+ let bodyHasTerminator = false;
26795
+ let blockEnd = -1;
26796
+ const maxScan = Math.min(lines.length, i2 + 26);
26797
+ for (let j = i2 + 1;j < maxScan; j++) {
26798
+ const line = lines[j];
26799
+ if (line.trim() === "")
26800
+ continue;
26801
+ const indent = line.length - line.trimStart().length;
26802
+ if (indent <= guardIndent) {
26803
+ blockEnd = j - 1;
26804
+ break;
26805
+ }
26806
+ if (terminator.test(line))
26807
+ bodyHasTerminator = true;
26808
+ }
26809
+ if (blockEnd === -1)
26810
+ blockEnd = Math.min(lines.length - 1, i2 + 25);
26811
+ if (!bodyHasTerminator)
26812
+ continue;
26813
+ const varRe = new RegExp(`\\b${guardedVar}\\b`);
26814
+ for (let l = blockEnd + 2;l <= lines.length; l++) {
26815
+ if (!varRe.test(lines[l - 1]))
26816
+ continue;
26817
+ sanitizers.push({
26818
+ type: "python_string_literal_guard",
26819
+ method: "if",
26820
+ line: l,
26821
+ sanitizes: ["code_injection"]
26822
+ });
26823
+ }
26824
+ }
26825
+ return sanitizers;
26826
+ }
26827
+ function findPythonDefaultSaxParserXxeSanitizers(code) {
26828
+ const sanitizers = [];
26829
+ const lines = code.split(`
26830
+ `);
26831
+ const parserVars = new Set;
26832
+ for (const line of lines) {
26833
+ const made = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:xml\.)?sax\.make_parser\s*\(/);
26834
+ if (made)
26835
+ parserVars.add(made[1]);
26836
+ }
26837
+ if (parserVars.size === 0)
26838
+ return sanitizers;
26839
+ for (const line of lines) {
26840
+ for (const parser of [...parserVars]) {
26841
+ const enabled = new RegExp(`\\b${parser}\\.setFeature\\s*\\([^,]*external_(?:ges|pes)[^,]*,\\s*True\\s*\\)`);
26842
+ const resolver = new RegExp(`\\b${parser}\\.setEntityResolver\\s*\\(`);
26843
+ const rebound = new RegExp(`^\\s*${parser}\\s*=(?!=)`);
26844
+ if (enabled.test(line) || resolver.test(line))
26845
+ parserVars.delete(parser);
26846
+ else if (rebound.test(line) && !/make_parser\s*\(/.test(line))
26847
+ parserVars.delete(parser);
26848
+ }
26849
+ }
26850
+ if (parserVars.size === 0)
26851
+ return sanitizers;
26852
+ for (let i2 = 0;i2 < lines.length; i2++) {
26853
+ const line = lines[i2];
26854
+ const parseCall = /\b(?:minidom\.)?parseString\s*\(|\b(?:minidom\.)?parse\s*\(|\bsax\.parse(?:String)?\s*\(/;
26855
+ if (!parseCall.test(line))
26856
+ continue;
26857
+ const usesDefaultParser = [...parserVars].some((p) => new RegExp(`\\b${p}\\b`).test(line));
26858
+ if (!usesDefaultParser)
26859
+ continue;
26860
+ sanitizers.push({
26861
+ type: "python_default_sax_parser",
26862
+ method: "make_parser",
26863
+ line: i2 + 1,
26864
+ sanitizes: ["xxe"]
26865
+ });
26866
+ }
26867
+ return sanitizers;
26868
+ }
26869
+ function findPythonUrlAllowlistRedirectSanitizers(code) {
26870
+ const sanitizers = [];
26871
+ const lines = code.split(`
26872
+ `);
26873
+ const parsedFrom = new Map;
26874
+ for (const line of lines) {
26875
+ const m = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:urllib\.parse\.)?urlparse\s*\(\s*([A-Za-z_][A-Za-z0-9_]*)\s*\)/);
26876
+ if (m)
26877
+ parsedFrom.set(m[1], m[2]);
26878
+ }
26879
+ if (parsedFrom.size === 0)
26880
+ return sanitizers;
26881
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
26882
+ for (let i2 = 0;i2 < lines.length; i2++) {
26883
+ const guard = lines[i2].match(/^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\.netloc\s+not\s+in\s+[[({]/);
26884
+ if (!guard)
26885
+ continue;
26886
+ const guardIndent = guard[1].length;
26887
+ const rawVar = parsedFrom.get(guard[2]);
26888
+ if (!rawVar)
26889
+ continue;
26890
+ let bodyHasTerminator = false;
26891
+ let blockEnd = -1;
26892
+ const maxScan = Math.min(lines.length, i2 + 26);
26893
+ for (let j = i2 + 1;j < maxScan; j++) {
26894
+ const line = lines[j];
26895
+ if (line.trim() === "")
26896
+ continue;
26897
+ const indent = line.length - line.trimStart().length;
26898
+ if (indent <= guardIndent) {
26899
+ blockEnd = j - 1;
26900
+ break;
26901
+ }
26902
+ if (terminator.test(line))
26903
+ bodyHasTerminator = true;
26904
+ }
26905
+ if (blockEnd === -1)
26906
+ blockEnd = Math.min(lines.length - 1, i2 + 25);
26907
+ if (!bodyHasTerminator)
26908
+ continue;
26909
+ const varRe = new RegExp(`\\b${rawVar}\\b`);
26910
+ for (let l = blockEnd + 2;l <= lines.length; l++) {
26911
+ if (!varRe.test(lines[l - 1]))
26912
+ continue;
26913
+ sanitizers.push({
26914
+ type: "python_url_allowlist_guard",
26915
+ method: "if",
26916
+ line: l,
26917
+ sanitizes: ["open_redirect"]
26918
+ });
26919
+ }
26920
+ }
26921
+ return sanitizers;
26922
+ }
26542
26923
  function findPythonDefusedXmlSanitizers(code) {
26543
26924
  const sanitizers = [];
26544
26925
  const lines = code.split(`
@@ -31559,14 +31940,20 @@ class SinkFilterPass {
31559
31940
  if (sink.type !== "xpath_injection")
31560
31941
  return true;
31561
31942
  const sinkLineText = sourceLines[sink.line - 1] ?? "";
31562
- const taintedVarOnLine = [...pyTaintedVars.keys()].find((v) => new RegExp(`\\b${v}\\b`).test(sinkLineText));
31943
+ const taintedValueVars = [...pyTaintedVars.keys()].filter((v) => new RegExp(`(?<![\\w.])${v}\\b(?!\\s*=(?!=))`).test(sinkLineText));
31944
+ const taintedVarOnLine = taintedValueVars[0];
31563
31945
  const oopVarOnLine = [...oopFieldVars].find((v) => sinkLineText.includes(v));
31564
31946
  if (oopVarOnLine)
31565
31947
  return true;
31566
31948
  if (!taintedVarOnLine)
31567
31949
  return false;
31950
+ const neutralised = (v) => pySanitizedVars.has(v) || new RegExp(`\\.xpath\\s*\\([^)]*\\b\\w+\\s*=\\s*\\b${v}\\b`).test(sinkLineText) || isXPathQuoteEscapedOnLine(sinkLineText, v);
31951
+ if (taintedValueVars.every(neutralised))
31952
+ return false;
31568
31953
  if (pySanitizedVars.has(taintedVarOnLine))
31569
31954
  return false;
31955
+ if (isXPathQuoteEscapedOnLine(sinkLineText, taintedVarOnLine))
31956
+ return false;
31570
31957
  if (new RegExp(`\\.xpath\\s*\\([^)]*\\b\\w+\\s*=\\s*\\b${taintedVarOnLine}\\b`).test(sinkLineText))
31571
31958
  return false;
31572
31959
  return true;
@@ -32670,6 +33057,28 @@ function filterSanitizedSinks(sinks, sanitizers, calls) {
32670
33057
  return true;
32671
33058
  });
32672
33059
  }
33060
+ function isXPathQuoteEscapedOnLine(lineText, varName) {
33061
+ const STRING_LITERAL = `'(?:[^'\\\\]|\\\\.)*'|"(?:[^"\\\\]|\\\\.)*"`;
33062
+ const replaceCall = new RegExp(`^${varName}\\.replace\\s*\\(\\s*(${STRING_LITERAL})\\s*,\\s*(${STRING_LITERAL})\\s*\\)`);
33063
+ const decode = (literal) => literal.slice(1, -1).replace(/\\(.)/g, "$1");
33064
+ const occurrence = new RegExp(`(?<![\\w.])${varName}\\b`, "g");
33065
+ let match;
33066
+ let seen = 0;
33067
+ while ((match = occurrence.exec(lineText)) !== null) {
33068
+ seen++;
33069
+ const rest = lineText.slice(match.index);
33070
+ const call = replaceCall.exec(rest);
33071
+ if (!call)
33072
+ return false;
33073
+ const searched = decode(call[1]);
33074
+ const replacement = decode(call[2]);
33075
+ if (searched !== "'" && searched !== '"')
33076
+ return false;
33077
+ if (replacement.includes("'") || replacement.includes('"'))
33078
+ return false;
33079
+ }
33080
+ return seen > 0;
33081
+ }
32673
33082
 
32674
33083
  // ../circle-ir/dist/analysis/passes/sink-semantics-pass.js
32675
33084
  function buildRegistry(entries) {
@@ -44959,6 +45368,10 @@ async function analyze(code, filePath, language, options = {}) {
44959
45368
  return analyzeMarkupFile(code, filePath, options, language);
44960
45369
  }
44961
45370
  let parseGrammar = language;
45371
+ if (language === "tsx") {
45372
+ parseGrammar = "tsx";
45373
+ language = "typescript";
45374
+ }
44962
45375
  if (language === "javascript" || language === "typescript") {
44963
45376
  const lower = filePath.toLowerCase();
44964
45377
  if (lower.endsWith(".tsx") || lower.endsWith(".jsx")) {
@@ -46007,7 +46420,7 @@ var colors = {
46007
46420
  };
46008
46421
 
46009
46422
  // src/version.ts
46010
- var version = "3.195.0";
46423
+ var version = "3.198.0";
46011
46424
 
46012
46425
  // src/formatters.ts
46013
46426
  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.195.0",
3
+ "version": "3.198.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.195.0"
69
+ "circle-ir": "^3.198.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@types/node": "^25.5.0",