cognium-dev 3.83.0 → 3.85.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 +410 -6
  2. package/package.json +2 -2
package/dist/cli.js CHANGED
@@ -12047,6 +12047,61 @@ function isSafeGoExecCommandCall(call, pattern, language) {
12047
12047
  return false;
12048
12048
  return true;
12049
12049
  }
12050
+ function isSafeRustCommandCall(call, pattern, language) {
12051
+ if (language !== "rust")
12052
+ return false;
12053
+ if (pattern.type !== "command_injection")
12054
+ return false;
12055
+ if (pattern.class !== undefined && pattern.class !== "Command")
12056
+ return false;
12057
+ const SHELL_PROGRAMS = new Set([
12058
+ "sh",
12059
+ "bash",
12060
+ "zsh",
12061
+ "dash",
12062
+ "ash",
12063
+ "ksh",
12064
+ "cmd",
12065
+ "cmd.exe",
12066
+ "powershell",
12067
+ "pwsh",
12068
+ "powershell.exe",
12069
+ "pwsh.exe"
12070
+ ]);
12071
+ const PROGRAM_RE = /\bCommand\s*::\s*new\s*\(\s*(?:r?"([^"]*)"|'([^']*)')/;
12072
+ const extractProgram = (text) => {
12073
+ const m = PROGRAM_RE.exec(text);
12074
+ if (!m)
12075
+ return null;
12076
+ const lit = m[1] ?? m[2] ?? "";
12077
+ return lit.split("/").pop() ?? lit;
12078
+ };
12079
+ if (pattern.method === "new") {
12080
+ const programArg = call.arguments.find((a) => a.position === 0);
12081
+ if (!programArg)
12082
+ return false;
12083
+ let program;
12084
+ if (programArg.literal !== null && programArg.literal !== undefined) {
12085
+ program = String(programArg.literal).split("/").pop() ?? String(programArg.literal);
12086
+ } else {
12087
+ const expr = (programArg.expression ?? "").trim();
12088
+ if (!(expr.startsWith('"') || expr.startsWith("'"))) {
12089
+ return false;
12090
+ }
12091
+ const stripped = expr.slice(1, -1);
12092
+ program = stripped.split("/").pop() ?? stripped;
12093
+ }
12094
+ return !SHELL_PROGRAMS.has(program);
12095
+ }
12096
+ if (pattern.method === "arg" || pattern.method === "args" || pattern.method === "spawn" || pattern.method === "output") {
12097
+ const receiverText = call.receiver ?? "";
12098
+ const program = extractProgram(receiverText);
12099
+ if (program === null)
12100
+ return false;
12101
+ return !SHELL_PROGRAMS.has(program);
12102
+ }
12103
+ return false;
12104
+ }
12050
12105
  var CLASS_LITERAL_RE = /^(?:[A-Za-z_][\w]*\.)*[A-Z][\w]*(?:\[\])*\.class$/;
12051
12106
  function argIsClassLiteral(call, position) {
12052
12107
  const arg = call.arguments.find((a) => a.position === position);
@@ -12071,6 +12126,9 @@ function findSinks(calls, patterns, typeHierarchy, language, sourceLines) {
12071
12126
  if (isSafeGoExecCommandCall(call, pattern, language)) {
12072
12127
  continue;
12073
12128
  }
12129
+ if (isSafeRustCommandCall(call, pattern, language)) {
12130
+ continue;
12131
+ }
12074
12132
  if (pattern.safe_if_class_literal_at !== undefined && argIsClassLiteral(call, pattern.safe_if_class_literal_at)) {
12075
12133
  continue;
12076
12134
  }
@@ -21307,6 +21365,14 @@ class LanguageSourcesPass {
21307
21365
  additionalSanitizers.push(...findGoMapAllowlistGuardSanitizers(code));
21308
21366
  additionalSanitizers.push(...findGoHtmlTemplateImportSanitizers(code));
21309
21367
  }
21368
+ if (language === "python") {
21369
+ additionalSanitizers.push(...findPythonNetlocAllowlistGuardSanitizers(code));
21370
+ additionalSanitizers.push(...findPythonRangeCheckGuardSanitizers(code));
21371
+ }
21372
+ if (language === "rust") {
21373
+ additionalSanitizers.push(...findRustSetAllowlistGuardSanitizers(code));
21374
+ additionalSanitizers.push(...findRustCanonicalizeGuardSanitizers(code));
21375
+ }
21310
21376
  attachSourceLineCode(additionalSources, additionalSinks, code);
21311
21377
  return { additionalSources, additionalSinks, additionalSanitizers, pyTaintedVars, pySanitizedVars, jsTaintedVars };
21312
21378
  }
@@ -22421,6 +22487,210 @@ function findGoHtmlTemplateImportSanitizers(code) {
22421
22487
  }
22422
22488
  return sanitizers;
22423
22489
  }
22490
+ function findPythonNetlocAllowlistGuardSanitizers(code) {
22491
+ const sanitizers = [];
22492
+ const lines = code.split(`
22493
+ `);
22494
+ const guardOpen = /^(\s*)if\s+.+?\s+not\s+in\s+([A-Za-z_][A-Za-z0-9_]*)\s*:\s*$/;
22495
+ const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
22496
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
22497
+ for (let i2 = 0;i2 < lines.length; i2++) {
22498
+ const m = guardOpen.exec(lines[i2]);
22499
+ if (!m)
22500
+ continue;
22501
+ const guardIndent = m[1].length;
22502
+ const allowName = m[2];
22503
+ if (!allowlistName.test(allowName))
22504
+ continue;
22505
+ let bodyHasTerminator = false;
22506
+ let blockEnd = -1;
22507
+ const maxScan = Math.min(lines.length, i2 + 26);
22508
+ for (let j = i2 + 1;j < maxScan; j++) {
22509
+ const line = lines[j];
22510
+ if (line.trim() === "")
22511
+ continue;
22512
+ const indent = line.length - line.trimStart().length;
22513
+ if (indent <= guardIndent) {
22514
+ blockEnd = j - 1;
22515
+ break;
22516
+ }
22517
+ if (terminator.test(line))
22518
+ bodyHasTerminator = true;
22519
+ }
22520
+ if (blockEnd === -1)
22521
+ blockEnd = Math.min(lines.length - 1, i2 + 25);
22522
+ if (!bodyHasTerminator)
22523
+ continue;
22524
+ for (let l = blockEnd + 2;l <= lines.length; l++) {
22525
+ sanitizers.push({
22526
+ type: "python_netloc_allowlist_guard",
22527
+ method: "if",
22528
+ line: l,
22529
+ sanitizes: [
22530
+ "open_redirect",
22531
+ "ssrf",
22532
+ "path_traversal",
22533
+ "external_taint_escape"
22534
+ ]
22535
+ });
22536
+ }
22537
+ }
22538
+ return sanitizers;
22539
+ }
22540
+ function findPythonRangeCheckGuardSanitizers(code) {
22541
+ const sanitizers = [];
22542
+ const lines = code.split(`
22543
+ `);
22544
+ const rangeGuard = /^(\s*)if\s+([A-Za-z_][A-Za-z0-9_]*)\s*[<>]=?\s*(?:\d+|-?\d+\.?\d*|[A-Z][A-Z0-9_]+)\s*(?:(?:or|and)\s+\2\s*[<>]=?\s*(?:\d+|-?\d+\.?\d*|[A-Z][A-Z0-9_]+)\s*)?:\s*$/;
22545
+ const terminator = /\b(return|raise|abort\s*\(|sys\.exit\s*\()/;
22546
+ for (let i2 = 0;i2 < lines.length; i2++) {
22547
+ const m = rangeGuard.exec(lines[i2]);
22548
+ if (!m)
22549
+ continue;
22550
+ const guardIndent = m[1].length;
22551
+ let bodyHasTerminator = false;
22552
+ let blockEnd = -1;
22553
+ const maxScan = Math.min(lines.length, i2 + 26);
22554
+ for (let j = i2 + 1;j < maxScan; j++) {
22555
+ const line = lines[j];
22556
+ if (line.trim() === "")
22557
+ continue;
22558
+ const indent = line.length - line.trimStart().length;
22559
+ if (indent <= guardIndent) {
22560
+ blockEnd = j - 1;
22561
+ break;
22562
+ }
22563
+ if (terminator.test(line))
22564
+ bodyHasTerminator = true;
22565
+ }
22566
+ if (blockEnd === -1)
22567
+ blockEnd = Math.min(lines.length - 1, i2 + 25);
22568
+ if (!bodyHasTerminator)
22569
+ continue;
22570
+ for (let l = blockEnd + 2;l <= lines.length; l++) {
22571
+ sanitizers.push({
22572
+ type: "python_range_check_guard",
22573
+ method: "if",
22574
+ line: l,
22575
+ sanitizes: ["xss", "external_taint_escape"]
22576
+ });
22577
+ }
22578
+ }
22579
+ return sanitizers;
22580
+ }
22581
+ function findRustSetAllowlistGuardSanitizers(code) {
22582
+ const sanitizers = [];
22583
+ const lines = code.split(`
22584
+ `);
22585
+ const guardOpen = /^\s*if\s+!\s*([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*(?:contains|contains_key)\s*\(/;
22586
+ const allowlistName = /^(?:[A-Z][A-Z0-9_]+|.*?(allowed|accepted|whitelist|permitted|valid|approved).*)$/i;
22587
+ const terminator = /\b(return|Err\s*\(|panic!\s*\(|HttpResponse::(?:Forbidden|BadRequest|Unauthorized))/;
22588
+ for (let i2 = 0;i2 < lines.length; i2++) {
22589
+ const m = guardOpen.exec(lines[i2]);
22590
+ if (!m)
22591
+ continue;
22592
+ const setName = m[1];
22593
+ if (!allowlistName.test(setName))
22594
+ continue;
22595
+ let depth = 0;
22596
+ for (const ch of lines[i2]) {
22597
+ if (ch === "{")
22598
+ depth++;
22599
+ else if (ch === "}")
22600
+ depth--;
22601
+ }
22602
+ if (depth <= 0)
22603
+ continue;
22604
+ let closeLine = -1;
22605
+ let bodyHasTerminator = false;
22606
+ const maxScan = Math.min(lines.length, i2 + 26);
22607
+ for (let j = i2 + 1;j < maxScan; j++) {
22608
+ const line = lines[j];
22609
+ if (terminator.test(line))
22610
+ bodyHasTerminator = true;
22611
+ for (const ch of line) {
22612
+ if (ch === "{")
22613
+ depth++;
22614
+ else if (ch === "}")
22615
+ depth--;
22616
+ }
22617
+ if (depth === 0) {
22618
+ closeLine = j;
22619
+ break;
22620
+ }
22621
+ }
22622
+ if (closeLine === -1 || !bodyHasTerminator)
22623
+ continue;
22624
+ for (let l = closeLine + 2;l <= lines.length; l++) {
22625
+ sanitizers.push({
22626
+ type: "rust_set_allowlist_guard",
22627
+ method: "if",
22628
+ line: l,
22629
+ sanitizes: [
22630
+ "ssrf",
22631
+ "open_redirect",
22632
+ "command_injection",
22633
+ "external_taint_escape"
22634
+ ]
22635
+ });
22636
+ }
22637
+ }
22638
+ return sanitizers;
22639
+ }
22640
+ function findRustCanonicalizeGuardSanitizers(code) {
22641
+ const sanitizers = [];
22642
+ const lines = code.split(`
22643
+ `);
22644
+ const guardOpen = /^\s*if\s+!\s*[A-Za-z_][\w?.()&]*\.starts_with\s*\(/;
22645
+ const terminator = /\b(return|Err\s*\(|panic!\s*\(|HttpResponse::(?:Forbidden|BadRequest|Unauthorized|NotFound))/;
22646
+ for (let i2 = 0;i2 < lines.length; i2++) {
22647
+ if (!guardOpen.test(lines[i2]))
22648
+ continue;
22649
+ let depth = 0;
22650
+ for (const ch of lines[i2]) {
22651
+ if (ch === "{")
22652
+ depth++;
22653
+ else if (ch === "}")
22654
+ depth--;
22655
+ }
22656
+ if (depth <= 0)
22657
+ continue;
22658
+ let closeLine = -1;
22659
+ let bodyHasTerminator = false;
22660
+ const maxScan = Math.min(lines.length, i2 + 26);
22661
+ for (let j = i2 + 1;j < maxScan; j++) {
22662
+ const line = lines[j];
22663
+ if (terminator.test(line))
22664
+ bodyHasTerminator = true;
22665
+ for (const ch of line) {
22666
+ if (ch === "{")
22667
+ depth++;
22668
+ else if (ch === "}")
22669
+ depth--;
22670
+ }
22671
+ if (depth === 0) {
22672
+ closeLine = j;
22673
+ break;
22674
+ }
22675
+ }
22676
+ if (closeLine === -1 || !bodyHasTerminator)
22677
+ continue;
22678
+ for (let l = closeLine + 2;l <= lines.length; l++) {
22679
+ sanitizers.push({
22680
+ type: "rust_canonicalize_guard",
22681
+ method: "if",
22682
+ line: l,
22683
+ sanitizes: [
22684
+ "path_traversal",
22685
+ "xss",
22686
+ "ssrf",
22687
+ "external_taint_escape"
22688
+ ]
22689
+ });
22690
+ }
22691
+ }
22692
+ return sanitizers;
22693
+ }
22424
22694
 
22425
22695
  // ../circle-ir/dist/analysis/passes/sink-filter-pass.js
22426
22696
  var JS_XSS_SANITIZERS = [
@@ -28687,6 +28957,11 @@ var TEST_FILENAME_RE = /(?:\.(?:test|spec)\.[cm]?[jt]sx?|_test\.go|_test\.py|Tes
28687
28957
  function isTestFile(file) {
28688
28958
  return TEST_PATH_RE3.test(file) || TEST_FILENAME_RE.test(file);
28689
28959
  }
28960
+ var GENERATED_PATH_RE = /(?:^|[\\/])(?:gen|generated|build[\\/]generated|src[\\/](?:main|test)[\\/]generated|target[\\/]generated-sources|target[\\/]generated-test-sources|node_modules[\\/]\.cache)(?:[\\/]|$)/i;
28961
+ var GENERATED_FILENAME_RE = /__[ch]\.java$|\.pb\.go$|_pb2\.py$|\.generated\.[cm]?[jt]sx?$/i;
28962
+ function isGeneratedFile(file) {
28963
+ return GENERATED_PATH_RE.test(file) || GENERATED_FILENAME_RE.test(file);
28964
+ }
28690
28965
  var PROVIDER_PATTERNS = [
28691
28966
  {
28692
28967
  name: "AWS access key",
@@ -28867,6 +29142,126 @@ function shannonEntropy(s) {
28867
29142
  return h;
28868
29143
  }
28869
29144
  var CREDENTIAL_NAME_RE = /(?:key|secret|token|password|passwd|credential|api[_-]?key)/i;
29145
+ function findAnnotationLineRanges(code) {
29146
+ const lines = code.split(`
29147
+ `);
29148
+ const inAnnotation = new Set;
29149
+ const OPEN_RE = /(?:@[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*\s*\(|#\[)/g;
29150
+ for (let i2 = 0;i2 < lines.length; i2++) {
29151
+ OPEN_RE.lastIndex = 0;
29152
+ let m;
29153
+ while ((m = OPEN_RE.exec(lines[i2])) !== null) {
29154
+ const isRustAttr = m[0].startsWith("#[");
29155
+ const openCh = isRustAttr ? "[" : "(";
29156
+ const closeCh = isRustAttr ? "]" : ")";
29157
+ let depth = 1;
29158
+ let li = i2;
29159
+ let col = m.index + m[0].length;
29160
+ let lineBudget = 200;
29161
+ inAnnotation.add(li + 1);
29162
+ while (depth > 0 && li < lines.length && lineBudget > 0) {
29163
+ const ln = lines[li];
29164
+ let inStr = null;
29165
+ while (col < ln.length && depth > 0) {
29166
+ const ch = ln[col];
29167
+ if (inStr !== null) {
29168
+ if (ch === "\\") {
29169
+ col += 2;
29170
+ continue;
29171
+ }
29172
+ if (ch === inStr)
29173
+ inStr = null;
29174
+ } else if (ch === '"' || ch === "'" || ch === "`") {
29175
+ inStr = ch;
29176
+ } else if (ch === openCh) {
29177
+ depth++;
29178
+ } else if (ch === closeCh) {
29179
+ depth--;
29180
+ }
29181
+ col++;
29182
+ }
29183
+ if (depth > 0) {
29184
+ li++;
29185
+ col = 0;
29186
+ lineBudget--;
29187
+ if (li < lines.length)
29188
+ inAnnotation.add(li + 1);
29189
+ }
29190
+ }
29191
+ }
29192
+ }
29193
+ return inAnnotation;
29194
+ }
29195
+ function findStringArrayLineRanges(code) {
29196
+ const lines = code.split(`
29197
+ `);
29198
+ const inArray = new Set;
29199
+ const OPEN_RE = /=\s*([{\[])/g;
29200
+ const STR_LITERAL_COUNT_RE = /(["'`])(?:\\.|(?!\1).)*\1/g;
29201
+ for (let i2 = 0;i2 < lines.length; i2++) {
29202
+ OPEN_RE.lastIndex = 0;
29203
+ let m;
29204
+ while ((m = OPEN_RE.exec(lines[i2])) !== null) {
29205
+ const openCh = m[1];
29206
+ const closeCh = openCh === "{" ? "}" : "]";
29207
+ let depth = 1;
29208
+ let li = i2;
29209
+ let col = m.index + m[0].length;
29210
+ let lineBudget = 500;
29211
+ const spanLines = [li + 1];
29212
+ let spanText = "";
29213
+ while (depth > 0 && li < lines.length && lineBudget > 0) {
29214
+ const ln = lines[li];
29215
+ let inStr = null;
29216
+ const start2 = col;
29217
+ while (col < ln.length && depth > 0) {
29218
+ const ch = ln[col];
29219
+ if (inStr !== null) {
29220
+ if (ch === "\\") {
29221
+ col += 2;
29222
+ continue;
29223
+ }
29224
+ if (ch === inStr)
29225
+ inStr = null;
29226
+ } else if (ch === '"' || ch === "'" || ch === "`") {
29227
+ inStr = ch;
29228
+ } else if (ch === openCh) {
29229
+ depth++;
29230
+ } else if (ch === closeCh) {
29231
+ depth--;
29232
+ }
29233
+ col++;
29234
+ }
29235
+ spanText += ln.substring(start2, col) + `
29236
+ `;
29237
+ if (depth > 0) {
29238
+ li++;
29239
+ col = 0;
29240
+ lineBudget--;
29241
+ if (li < lines.length)
29242
+ spanLines.push(li + 1);
29243
+ }
29244
+ }
29245
+ STR_LITERAL_COUNT_RE.lastIndex = 0;
29246
+ let strCount = 0;
29247
+ while (STR_LITERAL_COUNT_RE.exec(spanText) !== null) {
29248
+ strCount++;
29249
+ if (strCount >= 3)
29250
+ break;
29251
+ }
29252
+ if (strCount >= 3) {
29253
+ for (const ln of spanLines)
29254
+ inArray.add(ln);
29255
+ }
29256
+ }
29257
+ }
29258
+ return inArray;
29259
+ }
29260
+ var FIELD_ASSIGN_RE = /(?:^|[\s,(])([A-Za-z_$][\w$]*)\s*[:=]\s*["'`]/;
29261
+ function extractEnclosingFieldName(lineText) {
29262
+ const m = FIELD_ASSIGN_RE.exec(lineText);
29263
+ return m ? m[1] : null;
29264
+ }
28870
29265
  var TEST_CALL_RE = /\b(?:expect|assert|describe|it|test)\s*\(/;
28871
29266
  var COMMENT_EXAMPLE_RE = /(?:\/\/|#)\s*(?:example|sample|test|fixture)/i;
28872
29267
 
@@ -28875,7 +29270,7 @@ class ScanSecretsPass {
28875
29270
  category = "security";
28876
29271
  run(ctx) {
28877
29272
  const file = ctx.graph.ir.meta.file;
28878
- if (isTestFile(file)) {
29273
+ if (isTestFile(file) || isGeneratedFile(file)) {
28879
29274
  return { providerFindings: 0, entropyFindings: 0 };
28880
29275
  }
28881
29276
  const lines = ctx.code.split(`
@@ -28889,6 +29284,8 @@ class ScanSecretsPass {
28889
29284
  seen.add(`${f.line}:${f.rule_id}`);
28890
29285
  }
28891
29286
  }
29287
+ const annotationLines = findAnnotationLineRanges(ctx.code);
29288
+ const arrayLines = findStringArrayLineRanges(ctx.code);
28892
29289
  let providerFindings = 0;
28893
29290
  let entropyFindings = 0;
28894
29291
  for (let i2 = 0;i2 < lines.length; i2++) {
@@ -28955,12 +29352,18 @@ class ScanSecretsPass {
28955
29352
  continue;
28956
29353
  if (COMMENT_EXAMPLE_RE.test(lineText))
28957
29354
  continue;
29355
+ if (annotationLines.has(lineNum))
29356
+ continue;
29357
+ if (arrayLines.has(lineNum))
29358
+ continue;
28958
29359
  STRING_LITERAL_RE.lastIndex = 0;
28959
29360
  let match;
28960
29361
  while ((match = STRING_LITERAL_RE.exec(lineText)) !== null) {
28961
29362
  const value = match[2];
28962
29363
  if (!this.isCandidate(value))
28963
29364
  continue;
29365
+ if (value.length < 32)
29366
+ continue;
28964
29367
  if (!this.passesEntropyGate(value, lineText))
28965
29368
  continue;
28966
29369
  const key = `${lineNum}:hardcoded-credential-entropy`;
@@ -29007,11 +29410,12 @@ class ScanSecretsPass {
29007
29410
  return true;
29008
29411
  }
29009
29412
  passesEntropyGate(value, lineText) {
29413
+ const fieldName = extractEnclosingFieldName(lineText);
29414
+ if (fieldName === null || !CREDENTIAL_NAME_RE.test(fieldName))
29415
+ return false;
29010
29416
  const isHex = HEXISH_RE.test(value);
29011
- const boost = CREDENTIAL_NAME_RE.test(lineText) ? 0.2 : 0;
29012
- const threshold = isHex ? 3.5 - boost : 4.3 - boost;
29013
- const h = shannonEntropy(value);
29014
- return h >= threshold;
29417
+ const threshold = isHex ? 3.3 : 4.1;
29418
+ return shannonEntropy(value) >= threshold;
29015
29419
  }
29016
29420
  }
29017
29421
 
@@ -33586,7 +33990,7 @@ var colors = {
33586
33990
  };
33587
33991
 
33588
33992
  // src/version.ts
33589
- var version = "3.83.0";
33993
+ var version = "3.85.0";
33590
33994
 
33591
33995
  // src/formatters.ts
33592
33996
  var SINK_SEVERITY = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cognium-dev",
3
- "version": "3.83.0",
3
+ "version": "3.85.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",
@@ -65,7 +65,7 @@
65
65
  "registry": "https://registry.npmjs.org/"
66
66
  },
67
67
  "dependencies": {
68
- "circle-ir": "^3.83.0"
68
+ "circle-ir": "^3.85.0"
69
69
  },
70
70
  "devDependencies": {
71
71
  "@types/node": "^25.5.0",