seaworthycode 1.2.19 → 1.2.21

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/index.js CHANGED
@@ -471,6 +471,9 @@ var CHECK_PRECEDENCE = {
471
471
  "configuration.default-credentials": 1
472
472
  };
473
473
  var CROSS_DEDUPE_CHECK_IDS = new Set(Object.keys(CHECK_PRECEDENCE));
474
+ var SUPERSESSION_RULES = [
475
+ { winner: "security.taint-xss", loser: "security.xss-surface" }
476
+ ];
474
477
  function crossCheckKey(finding) {
475
478
  return `${finding.file ?? ""}:${finding.line ?? 0}`;
476
479
  }
@@ -511,7 +514,23 @@ function dedupeCrossCheck(findings) {
511
514
  }
512
515
  mergedCrossFindings.push(winner);
513
516
  }
514
- return [...otherFindings, ...mergedCrossFindings];
517
+ let result = [...otherFindings, ...mergedCrossFindings];
518
+ if (SUPERSESSION_RULES.length > 0) {
519
+ const supersededKeys = /* @__PURE__ */ new Set();
520
+ for (const rule of SUPERSESSION_RULES) {
521
+ for (const f of result) {
522
+ if (f.checkId === rule.winner && f.file != null && f.line != null) {
523
+ supersededKeys.add(`${rule.loser}:${f.file}:${f.line}`);
524
+ }
525
+ }
526
+ }
527
+ if (supersededKeys.size > 0) {
528
+ result = result.filter(
529
+ (f) => f.file == null || f.line == null || !supersededKeys.has(`${f.checkId}:${f.file}:${f.line}`)
530
+ );
531
+ }
532
+ }
533
+ return result;
515
534
  }
516
535
  var SEVERITY_ORDER = {
517
536
  critical: 0,
@@ -641,6 +660,14 @@ function filterByMinConfidence(findings, minConfidence) {
641
660
  if (!minConfidence) return findings;
642
661
  return findings.filter((f) => meetsMinConfidence(f.confidence, minConfidence));
643
662
  }
663
+ var SEVERITY_LEVELS = ["critical", "high", "medium", "low", "info"];
664
+ function meetsMinSeverity(findingSeverity, minSeverity) {
665
+ return SEVERITY_LEVELS.indexOf(findingSeverity) <= SEVERITY_LEVELS.indexOf(minSeverity);
666
+ }
667
+ function filterByMinSeverity(findings, minSeverity) {
668
+ if (!minSeverity) return findings;
669
+ return findings.filter((f) => meetsMinSeverity(f.severity, minSeverity));
670
+ }
644
671
  var SITE_URL = process.env.SEAWORTHY_SITE_URL ?? "https://seaworthycode.com";
645
672
  var whyCopy = {
646
673
  "checks.no-document-write.why": "Cross-site scripting (XSS) allows attackers to inject malicious client-side scripts. This can lead to session hijacking, cookie theft, or unauthorized actions on behalf of users (CWE-79).",
@@ -1060,6 +1087,8 @@ var copy = {
1060
1087
  "checks.verbose-errors.description": "Error responses include stack traces or internal details, leaking implementation information to clients.",
1061
1088
  "checks.pii-in-responses.name": "PII in API responses",
1062
1089
  "checks.pii-in-responses.description": "API responses include personally identifiable information without proper filtering.",
1090
+ "checks.pii-in-responses.message": (match) => `PII field detected in response model: ${match}`,
1091
+ "checks.pii-in-responses.degraded": (path, language) => `Could not fully analyse ${path} (${language}) for PII fields \u2014 install tree-sitter grammar for complete coverage.`,
1063
1092
  "checks.no-response-filtering.name": "No response filtering",
1064
1093
  "checks.no-response-filtering.description": "API responses return full database rows without filtering sensitive or unnecessary fields.",
1065
1094
  "checks.missing-lockfile.name": "Missing lockfile",
@@ -1073,6 +1102,7 @@ var copy = {
1073
1102
  // Remediation hints
1074
1103
  "remediation.security.hardcoded-secrets": "Move this value to an environment variable and reference it via process.env.",
1075
1104
  "remediation.security.dangerous-eval": "Avoid eval(), exec(), and new Function(). Use safer alternatives like JSON.parse for data parsing, or refactor to eliminate dynamic code execution.",
1105
+ "checks.committed-env.env-message": (commit, refs) => `A .env file (other than .env.example) is tracked in git history (commit ${commit.slice(0, 7)}${refs ? `, refs: ${refs}` : ""}). Rotate any exposed secrets immediately.`,
1076
1106
  "remediation.security.committed-env": "Add .env to .gitignore. If the file is already tracked, use git rm --cached to untrack it without deleting, or rotate any exposed secrets.",
1077
1107
  "remediation.security.auth-missing-endpoints": "Apply authentication middleware to this route handler. Use requireAuth, passport, JWT verification, or session-based auth.",
1078
1108
  "remediation.security.cors-too-permissive": "Restrict Access-Control-Allow-Origin to your specific frontend domains rather than using a wildcard.",
@@ -1728,6 +1758,7 @@ var ScanRunner = class {
1728
1758
  dedupeDegradedFindings(dedupeCrossCheck(dedupeFindings(results)))
1729
1759
  );
1730
1760
  findings = filterByMinConfidence(findings, options.minConfidence);
1761
+ findings = filterByMinSeverity(findings, options.minSeverity);
1731
1762
  if (options.baseline) {
1732
1763
  if (options.baseline === "save") {
1733
1764
  const baselinePath = resolveBaselinePath(options.targetDir);
@@ -1772,6 +1803,39 @@ function pathsUnderTarget(topLevel, targetDir, repoRelativePath) {
1772
1803
  function isInsideGitRepo(dir) {
1773
1804
  return gitTopLevel(dir) !== null;
1774
1805
  }
1806
+ function findFileInGitHistory(dir, pattern) {
1807
+ const topLevel = gitTopLevel(dir);
1808
+ if (!topLevel) {
1809
+ return { found: false };
1810
+ }
1811
+ try {
1812
+ const output = execSync(
1813
+ 'git log --all --format="%H|%D" --name-only --diff-filter=A',
1814
+ {
1815
+ cwd: dir,
1816
+ stdio: "pipe",
1817
+ encoding: "utf-8",
1818
+ maxBuffer: 10 * 1024 * 1024
1819
+ }
1820
+ );
1821
+ let currentCommit = "";
1822
+ let currentRefs = "";
1823
+ for (const line of output.split(/\r?\n/)) {
1824
+ if (line.length >= 40 && line[40] === "|" && /^[0-9a-f]{40}$/.test(line.slice(0, 40))) {
1825
+ currentCommit = line.slice(0, 40);
1826
+ currentRefs = line.slice(41).trim();
1827
+ continue;
1828
+ }
1829
+ if (!line.length) continue;
1830
+ if (pathsUnderTarget(topLevel, dir, line) && pattern.test(line)) {
1831
+ return { found: true, commit: currentCommit, refs: currentRefs };
1832
+ }
1833
+ }
1834
+ return { found: false };
1835
+ } catch {
1836
+ return { found: false };
1837
+ }
1838
+ }
1775
1839
  function hasFileInGitHistory(dir, pattern) {
1776
1840
  const topLevel = gitTopLevel(dir);
1777
1841
  if (!topLevel) {
@@ -4351,6 +4415,13 @@ function lineMatch(line, lineIndex, pattern) {
4351
4415
  function hasShellValidation(content) {
4352
4416
  return /\[\[\s*"?\$\d+"?\s*=~\s*\^/.test(content) || /\bcase\s+(?:"?\$\d+"?|"?\$\{[1-9][0-9]*(?::-[^}]*)?\}"?)\s+in\b/.test(content) || /\[\[\s+-[nz]\s+"?\$\d+"?\s*\]\]/.test(content);
4353
4417
  }
4418
+ function isInsideSingleQuotes(line, charIndex) {
4419
+ let inside = false;
4420
+ for (let i = 0; i < charIndex; i++) {
4421
+ if (line[i] === "'") inside = !inside;
4422
+ }
4423
+ return inside;
4424
+ }
4354
4425
  function shellLineHasTimeout(line) {
4355
4426
  return /^\s*timeout\s+\S+/.test(line) || /(?:^|\s)(?:--max-time|--connect-timeout|--timeout|-m)(?:\s|=|$)/.test(line);
4356
4427
  }
@@ -4438,23 +4509,24 @@ registry.register({
4438
4509
  severity: "high",
4439
4510
  minTier: "free",
4440
4511
  async run(ctx) {
4441
- const hasEnv = hasFileInGitHistory(
4512
+ const envMatch = findFileInGitHistory(
4442
4513
  ctx.targetDir,
4443
4514
  /(?:^|\/)\.env(?:$|\.(?!example$).+)/
4444
4515
  );
4445
4516
  const hasSettings = await hasCommittedSettingsWithSecrets(ctx.targetDir);
4446
4517
  const hasJavaConfig = await hasCommittedJavaConfigWithSecrets(ctx.targetDir);
4447
- if (!hasEnv && !hasSettings && !hasJavaConfig) return [];
4518
+ if (!envMatch.found && !hasSettings && !hasJavaConfig) return [];
4448
4519
  const findings = [];
4449
- if (hasEnv) {
4520
+ if (envMatch.found) {
4450
4521
  findings.push({
4451
4522
  id: "committed-env-1",
4452
4523
  checkId: "security.committed-env",
4453
4524
  category: "security",
4454
4525
  severity: "high",
4455
- message: "A .env file (other than .env.example) is tracked in git history. Environment files should never be committed.",
4526
+ message: getCopy("checks.committed-env.env-message", envMatch.commit, envMatch.refs),
4456
4527
  confidence: "high",
4457
- remediation: getCopy("remediation.security.committed-env")
4528
+ remediation: getCopy("remediation.security.committed-env"),
4529
+ properties: { commit: envMatch.commit, refs: envMatch.refs }
4458
4530
  });
4459
4531
  }
4460
4532
  if (hasSettings) {
@@ -4479,7 +4551,7 @@ registry.register({
4479
4551
  remediation: getCopy("remediation.security.committed-env")
4480
4552
  });
4481
4553
  }
4482
- if (hasEnv) {
4554
+ if (envMatch.found) {
4483
4555
  const sourceFindings = await scanBashLines(ctx, {
4484
4556
  checkId: "security.committed-env",
4485
4557
  category: "security",
@@ -5094,7 +5166,7 @@ registry.register({
5094
5166
  }
5095
5167
  if (lockfiles.length === 0) {
5096
5168
  try {
5097
- const output = execSync2("npm audit --json", {
5169
+ const output = execSync2("npm audit --json --omit=dev", {
5098
5170
  cwd: ctx.targetDir,
5099
5171
  stdio: "pipe",
5100
5172
  encoding: "utf-8",
@@ -5117,7 +5189,7 @@ registry.register({
5117
5189
  for (const lockfile of lockfiles) {
5118
5190
  const lockfileDir = join8(ctx.targetDir, dirname2(lockfile));
5119
5191
  try {
5120
- const output = execSync2("npm audit --json", {
5192
+ const output = execSync2("npm audit --json --omit=dev", {
5121
5193
  cwd: lockfileDir,
5122
5194
  stdio: "pipe",
5123
5195
  encoding: "utf-8",
@@ -6204,6 +6276,10 @@ async function runMechanicalCheck(ctx, template) {
6204
6276
  }
6205
6277
  for (let lineNum = 0; lineNum < lines.length; lineNum++) {
6206
6278
  const line = lines[lineNum];
6279
+ if (pattern.skipCommentLines) {
6280
+ const trimmed = line.trimStart();
6281
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*")) continue;
6282
+ }
6207
6283
  if (pattern.regex.test(line)) {
6208
6284
  results.push(
6209
6285
  makeMechanicalFinding(template, pattern, file.relativePath, lineNum + 1)
@@ -6970,7 +7046,10 @@ async function scanBashNoInputValidation(ctx) {
6970
7046
  remediation: getCopy("remediation.resilience.no-input-validation")
6971
7047
  }, (line, content, index) => {
6972
7048
  if (hasShellValidation(content)) return null;
6973
- return lineMatch(line, index, /\$\d\b|\$\{[1-9][0-9]*\}/);
7049
+ const match = lineMatch(line, index, /\$\d\b|\$\{[1-9][0-9]*\}/);
7050
+ if (!match) return null;
7051
+ if (isInsideSingleQuotes(line, match.column - 1)) return null;
7052
+ return match;
6974
7053
  }).then((findings) => findings.slice(0, 1));
6975
7054
  }
6976
7055
  registry.register({
@@ -7097,12 +7176,13 @@ registry.register({
7097
7176
  return runLLMCheck(ctx, promptTemplate8);
7098
7177
  }
7099
7178
  });
7100
- var PROMPT_VERSION9 = "1.2.0";
7179
+ var PROMPT_VERSION9 = "1.3.0";
7101
7180
  var fallbackPatterns9 = [
7102
7181
  {
7103
7182
  regex: /(?:fetch|axios\.(?:get|post|put|delete|patch|request)|requests\.(?:get|post|put|patch|delete|head)|aiohttp\.\w*[Ss]ession|httpx\.(?:get|post|put|patch|delete|Client))\s*\(/i,
7104
7183
  message: "Outbound HTTP call found \u2014 verify timeout is configured",
7105
- severity: "medium"
7184
+ severity: "medium",
7185
+ skipCommentLines: true
7106
7186
  }
7107
7187
  ];
7108
7188
  async function buildPrompt9(sourceFiles) {
@@ -7376,12 +7456,13 @@ registry.register({
7376
7456
  return runLLMCheck(ctx, promptTemplate12);
7377
7457
  }
7378
7458
  });
7379
- var PROMPT_VERSION13 = "1.1.0";
7459
+ var PROMPT_VERSION13 = "1.2.0";
7380
7460
  var fallbackPatterns13 = [
7381
7461
  {
7382
7462
  regex: /console\.(?:log|error|warn)\b/i,
7383
7463
  message: "Console logging used \u2014 structured logging recommended",
7384
- severity: "low"
7464
+ severity: "low",
7465
+ skipCommentLines: true
7385
7466
  }
7386
7467
  ];
7387
7468
  async function buildPrompt13(sourceFiles) {
@@ -7518,16 +7599,18 @@ registry.register({
7518
7599
  return runLLMCheck(ctx, promptTemplate15);
7519
7600
  }
7520
7601
  });
7521
- var PROMPT_VERSION16 = "1.0.0";
7602
+ var PROMPT_VERSION16 = "1.1.0";
7522
7603
  var fallbackPatterns16 = [
7523
7604
  {
7524
- regex: /(?:admin:admin|password\s*[:=]\s*['"`]password|changeme|default)/i,
7605
+ regex: /(?:admin:admin|\b\w*(?:password|passwd|pwd|pass|secret|token|credential)\w*\s*[:=]\s*['"`](?:admin|password|123456|changeme|letmein|secret|test|postgres|root|default|1234|12345|qwerty|pass|guest)['"`])/i,
7525
7606
  message: "Default credentials possibly in use",
7526
7607
  severity: "high"
7527
7608
  }
7528
7609
  ];
7529
7610
  async function buildPrompt16(sourceFiles) {
7530
- const parts = ["Check if default credentials are unchanged. Look for default usernames/passwords (admin/admin, root/root, guest/guest), placeholder credentials (changeme, password), or default connection strings that have not been customised."];
7611
+ const parts = [
7612
+ "Check if default credentials are unchanged. Look for default usernames/passwords (admin/admin, root/root, guest/guest), placeholder credentials (changeme, password), or default connection strings that have not been customised.\n\nDo NOT flag: (1) programming language control-flow keywords such as switch/case/default in Go, JavaScript, Java, Rust, or PHP \u2014 these are not credentials. (2) Comments that describe or explain a default value in prose \u2014 the comment itself is not a credential assignment. Only flag actual credential assignments where a variable whose name suggests it holds a password, token, or secret is set to a known weak literal value."
7613
+ ];
7531
7614
  for (const file of sourceFiles) {
7532
7615
  const content = await file.content();
7533
7616
  parts.push(`--- ${file.relativePath} ---
@@ -7574,7 +7657,13 @@ function isDefaultCredential(password2) {
7574
7657
  "secret",
7575
7658
  "test",
7576
7659
  "postgres",
7577
- "root"
7660
+ "root",
7661
+ "default",
7662
+ "pass",
7663
+ "1234",
7664
+ "12345",
7665
+ "qwerty",
7666
+ "guest"
7578
7667
  ]);
7579
7668
  return defaults.has(normalized);
7580
7669
  }
@@ -7706,6 +7795,115 @@ async function scanBashDefaultCredentials(ctx) {
7706
7795
  return lineMatch(line, index, /(?:^|\s)(?:export\s+)?[A-Z0-9_]*(?:PASS|PASSWORD|PWD|SECRET|TOKEN)[A-Z0-9_]*=(["'])([^"']{1,64})\1/i);
7707
7796
  });
7708
7797
  }
7798
+ var CREDENTIAL_NAME_REGEX = /(?:password|passwd|pwd|pass|secret|token|credential|cred)/i;
7799
+ var DEFAULT_CRED_TS_QUERIES = {
7800
+ javascript: `
7801
+ (lexical_declaration
7802
+ (variable_declarator
7803
+ name: (identifier) @name
7804
+ value: (string) @value)) @match
7805
+
7806
+ (assignment_expression
7807
+ left: (identifier) @name
7808
+ right: (string) @value) @match
7809
+
7810
+ (pair
7811
+ key: (property_identifier) @name
7812
+ value: (string) @value) @match
7813
+ `,
7814
+ typescript: `
7815
+ (lexical_declaration
7816
+ (variable_declarator
7817
+ name: (identifier) @name
7818
+ value: (string) @value)) @match
7819
+
7820
+ (assignment_expression
7821
+ left: (identifier) @name
7822
+ right: (string) @value) @match
7823
+
7824
+ (pair
7825
+ key: (property_identifier) @name
7826
+ value: (string) @value) @match
7827
+ `,
7828
+ tsx: `
7829
+ (lexical_declaration
7830
+ (variable_declarator
7831
+ name: (identifier) @name
7832
+ value: (string) @value)) @match
7833
+
7834
+ (assignment_expression
7835
+ left: (identifier) @name
7836
+ right: (string) @value) @match
7837
+
7838
+ (pair
7839
+ key: (property_identifier) @name
7840
+ value: (string) @value) @match
7841
+ `,
7842
+ go: `
7843
+ (short_var_declaration
7844
+ left: (expression_list (identifier) @name)
7845
+ right: (expression_list (_) @value)) @match
7846
+
7847
+ (var_declaration
7848
+ (var_spec
7849
+ name: (identifier) @name
7850
+ value: (_) @value)) @match
7851
+ `,
7852
+ python: `
7853
+ (assignment
7854
+ left: (identifier) @name
7855
+ right: (string) @value) @match
7856
+ `,
7857
+ ruby: `
7858
+ (assignment left: (identifier) @name right: (string) @value) @match
7859
+
7860
+ (pair key: (_) @name value: (string) @value) @match
7861
+ `,
7862
+ java: `
7863
+ (local_variable_declaration
7864
+ declarator: (variable_declarator
7865
+ name: (identifier) @name
7866
+ value: (string_literal) @value)) @match
7867
+
7868
+ (field_declaration
7869
+ declarator: (variable_declarator
7870
+ name: (identifier) @name
7871
+ value: (string_literal) @value)) @match
7872
+ `,
7873
+ rust: `
7874
+ (let_declaration
7875
+ pattern: (identifier) @name
7876
+ value: (string_literal) @value) @match
7877
+ `
7878
+ };
7879
+ var astCheck = createTreeSitterCheck({
7880
+ id: CHECK_ID8,
7881
+ nameKey: "checks.default-credentials.name",
7882
+ descriptionKey: "checks.default-credentials.description",
7883
+ category: "configuration",
7884
+ severity: "high",
7885
+ minTier: "pro",
7886
+ queries: DEFAULT_CRED_TS_QUERIES,
7887
+ messageKey: "checks.default-credentials.message",
7888
+ degradedMessageKey: "checks.default-credentials.degraded",
7889
+ remediationKey: "remediation.configuration.default-credentials",
7890
+ fileFilter: (file) => !isMechanicalCheckExcludedPath(file.relativePath),
7891
+ matchFilter: (captures) => {
7892
+ const name = captures.name ?? "";
7893
+ const value = captures.value ?? "";
7894
+ if (!CREDENTIAL_NAME_REGEX.test(name)) return false;
7895
+ const trimmed = value.trim();
7896
+ if (!/^['"`].*['"`]$/.test(trimmed)) return false;
7897
+ const cleanValue = trimmed.replace(/^['"`]|['"`]$/g, "");
7898
+ if (looksLikeHash(cleanValue)) return false;
7899
+ return isDefaultCredential(cleanValue);
7900
+ },
7901
+ messageFactory: (captures) => {
7902
+ const name = captures.name ?? "unknown";
7903
+ const cleanValue = (captures.value ?? "").trim().replace(/^['"`]|['"`]$/g, "");
7904
+ return getCopy("checks.default-credentials.message", `${name} = "${cleanValue}"`);
7905
+ }
7906
+ });
7709
7907
  var sqlCheck = {
7710
7908
  id: CHECK_ID8,
7711
7909
  name: getCopy("checks.default-credentials.name"),
@@ -7714,12 +7912,14 @@ var sqlCheck = {
7714
7912
  severity: "high",
7715
7913
  minTier: "pro",
7716
7914
  async run(ctx) {
7717
- const [sqlFindings, bashFindings, llmFindings] = await Promise.all([
7915
+ const [sqlFindings, bashFindings, llmFindings, astFindings] = await Promise.all([
7718
7916
  scanSqlDefaultCredentials(ctx),
7719
7917
  scanBashDefaultCredentials(ctx),
7720
- runLLMCheck(ctx, promptTemplate16)
7918
+ runLLMCheck(ctx, promptTemplate16),
7919
+ astCheck.run(ctx)
7721
7920
  ]);
7722
- return [...sqlFindings, ...bashFindings, ...llmFindings];
7921
+ const nonAstFindings = [...sqlFindings, ...bashFindings, ...llmFindings];
7922
+ return mergeFindings(astFindings, nonAstFindings);
7723
7923
  }
7724
7924
  };
7725
7925
  registry.register(sqlCheck);
@@ -7781,16 +7981,17 @@ registry.register({
7781
7981
  return [...bashFindings, ...llmFindings];
7782
7982
  }
7783
7983
  });
7784
- var PROMPT_VERSION18 = "1.0.0";
7984
+ var PROMPT_VERSION18 = "1.1.0";
7785
7985
  var fallbackPatterns18 = [
7786
7986
  {
7787
7987
  regex: /\b(?:ssn|social_security|date_of_birth|dob|passport|national_id)\b/i,
7788
7988
  message: "Potentially sensitive field name found \u2014 verify it is not exposed in API responses",
7789
- severity: "high"
7989
+ severity: "high",
7990
+ skipCommentLines: true
7790
7991
  }
7791
7992
  ];
7792
7993
  async function buildPrompt18(sourceFiles) {
7793
- const parts = ["Check if PII or sensitive fields are included in API responses. Look for response serialization that may include SSN, date of birth, passport numbers, or other personally identifiable information that should be filtered. In Python, flag Django REST Framework serializers or FastAPI response models that include PII fields without exclude/exclude_unset configuration."];
7994
+ const parts = ["Check if PII or sensitive fields are included in API responses. Look for response serialization that may include SSN, date of birth, passport numbers, or other personally identifiable information that should be filtered. In Python, flag Django REST Framework serializers or FastAPI response models that include PII fields without exclude/exclude_unset configuration.\n\nDo NOT flag field names that appear only in comments, test fixtures, or documentation strings. Only flag fields that are directly included in serialised API responses or response model definitions."];
7794
7995
  for (const file of sourceFiles) {
7795
7996
  const content = await file.content();
7796
7997
  parts.push(`--- ${file.relativePath} ---
@@ -7808,15 +8009,63 @@ var promptTemplate18 = {
7808
8009
  parseResponse: parseResponse18,
7809
8010
  fallbackPatterns: fallbackPatterns18
7810
8011
  };
8012
+ var CHECK_ID9 = "data-exposure.pii-in-responses";
8013
+ var PII_FIELD_REGEX = /\b(?:ssn|social.?security|date.?of.?birth|dob|passport|national.?id)\b/i;
8014
+ var PII_TS_QUERIES = {
8015
+ typescript: `
8016
+ (property_signature name: (property_identifier) @name) @match
8017
+ (pair key: (property_identifier) @name) @match
8018
+ (public_field_definition name: (property_identifier) @name) @match
8019
+ `,
8020
+ javascript: `
8021
+ (pair key: (property_identifier) @name) @match
8022
+ (method_definition name: (property_identifier) @name) @match
8023
+ `,
8024
+ tsx: `
8025
+ (property_signature name: (property_identifier) @name) @match
8026
+ (pair key: (property_identifier) @name) @match
8027
+ `,
8028
+ go: `
8029
+ (field_declaration name: (field_identifier) @name) @match
8030
+ `,
8031
+ python: `
8032
+ (assignment left: (identifier) @name) @match
8033
+ `,
8034
+ ruby: `
8035
+ (assignment left: (identifier) @name right: (_)) @match
8036
+ `,
8037
+ java: `
8038
+ (field_declaration declarator: (variable_declarator name: (identifier) @name)) @match
8039
+ `
8040
+ };
8041
+ var piiAstCheck = createTreeSitterCheck({
8042
+ id: CHECK_ID9,
8043
+ nameKey: "checks.pii-in-responses.name",
8044
+ descriptionKey: "checks.pii-in-responses.description",
8045
+ category: "data-exposure",
8046
+ severity: "high",
8047
+ minTier: "pro",
8048
+ queries: PII_TS_QUERIES,
8049
+ messageKey: "checks.pii-in-responses.message",
8050
+ degradedMessageKey: "checks.pii-in-responses.degraded",
8051
+ remediationKey: "remediation.data-exposure.pii-in-responses",
8052
+ fileFilter: (file) => !isMechanicalCheckExcludedPath(file.relativePath),
8053
+ matchFilter: (captures) => PII_FIELD_REGEX.test(captures.name ?? ""),
8054
+ messageFactory: (captures) => getCopy("checks.pii-in-responses.message", captures.name ?? "unknown")
8055
+ });
7811
8056
  registry.register({
7812
- id: "data-exposure.pii-in-responses",
7813
- name: "PII in API responses",
7814
- description: "PII or sensitive fields in API responses",
8057
+ id: CHECK_ID9,
8058
+ name: getCopy("checks.pii-in-responses.name"),
8059
+ description: getCopy("checks.pii-in-responses.description"),
7815
8060
  category: "data-exposure",
7816
8061
  severity: "high",
7817
8062
  minTier: "pro",
7818
8063
  async run(ctx) {
7819
- return runLLMCheck(ctx, promptTemplate18);
8064
+ const [llmFindings, astFindings] = await Promise.all([
8065
+ runLLMCheck(ctx, promptTemplate18),
8066
+ piiAstCheck.run(ctx)
8067
+ ]);
8068
+ return mergeFindings(astFindings, llmFindings);
7820
8069
  }
7821
8070
  });
7822
8071
  var PROMPT_VERSION19 = "1.0.0";
@@ -7862,7 +8111,7 @@ registry.register({
7862
8111
  return runLLMCheck(ctx, promptTemplate19);
7863
8112
  }
7864
8113
  });
7865
- var CHECK_ID9 = "data-exposure.html-form-pii-exposure";
8114
+ var CHECK_ID10 = "data-exposure.html-form-pii-exposure";
7866
8115
  var REMEDIATION12 = getCopy("remediation.data-exposure.html-form-pii-exposure");
7867
8116
  var INPUT_PATTERN = /<input\b[^>]*?>/gi;
7868
8117
  var AUTOCOMPLETE_PATTERN = /\bautocomplete\s*=\s*["']([^"']+)["']/i;
@@ -7889,8 +8138,8 @@ function lineAndColumn4(content, index) {
7889
8138
  function pushFinding2(findings, filePath, content, index, matchText) {
7890
8139
  const { line, column } = lineAndColumn4(content, index);
7891
8140
  findings.push({
7892
- id: `${CHECK_ID9}:${filePath}:${line}:${column}`,
7893
- checkId: CHECK_ID9,
8141
+ id: `${CHECK_ID10}:${filePath}:${line}:${column}`,
8142
+ checkId: CHECK_ID10,
7894
8143
  category: "data-exposure",
7895
8144
  severity: "medium",
7896
8145
  message: getCopy("checks.html-form-pii-exposure.message", matchText),
@@ -7916,7 +8165,7 @@ function hasSensitiveField(tagText) {
7916
8165
  return SENSITIVE_TYPES.test(type.trim()) || SENSITIVE_NAME.test(name);
7917
8166
  }
7918
8167
  registry.register({
7919
- id: CHECK_ID9,
8168
+ id: CHECK_ID10,
7920
8169
  name: getCopy("checks.html-form-pii-exposure.name"),
7921
8170
  description: getCopy("checks.html-form-pii-exposure.description"),
7922
8171
  category: "data-exposure",
@@ -7945,7 +8194,7 @@ registry.register({
7945
8194
  return findings;
7946
8195
  }
7947
8196
  });
7948
- var CHECK_ID10 = "security.xss-surface";
8197
+ var CHECK_ID11 = "security.xss-surface";
7949
8198
  var CGI_PATH_HINT = new RegExp("(^|[/_-])cgi([/_.-]|$)", "i");
7950
8199
  function isLikelyCgiBashScript(file) {
7951
8200
  return CGI_PATH_HINT.test(file.relativePath);
@@ -8224,7 +8473,7 @@ var QUERIES = {
8224
8473
  };
8225
8474
  registry.register(
8226
8475
  createTreeSitterCheck({
8227
- id: CHECK_ID10,
8476
+ id: CHECK_ID11,
8228
8477
  nameKey: "checks.xss-surface.name",
8229
8478
  descriptionKey: "checks.xss-surface.description",
8230
8479
  category: "security",
@@ -8239,7 +8488,7 @@ registry.register(
8239
8488
  fileFilter: (file) => file.language !== "bash" || isLikelyCgiBashScript(file)
8240
8489
  })
8241
8490
  );
8242
- var CHECK_ID11 = "security.os-command-injection";
8491
+ var CHECK_ID12 = "security.os-command-injection";
8243
8492
  function isStaticShellLikeLiteral(text22) {
8244
8493
  const t = text22.trim();
8245
8494
  if (t.length < 2) return false;
@@ -8369,7 +8618,7 @@ var QUERIES2 = {
8369
8618
  };
8370
8619
  registry.register(
8371
8620
  createTreeSitterCheck({
8372
- id: CHECK_ID11,
8621
+ id: CHECK_ID12,
8373
8622
  nameKey: "checks.os-command-injection.name",
8374
8623
  descriptionKey: "checks.os-command-injection.description",
8375
8624
  category: "security",
@@ -8386,7 +8635,7 @@ registry.register(
8386
8635
  }
8387
8636
  })
8388
8637
  );
8389
- var CHECK_ID12 = "security.shell-arbitrary-evaluation";
8638
+ var CHECK_ID13 = "security.shell-arbitrary-evaluation";
8390
8639
  var QUERY = `
8391
8640
  (command
8392
8641
  name: (command_name (word) @cmd)
@@ -8400,7 +8649,7 @@ var QUERY = `
8400
8649
  `;
8401
8650
  registry.register(
8402
8651
  createTreeSitterCheck({
8403
- id: CHECK_ID12,
8652
+ id: CHECK_ID13,
8404
8653
  nameKey: "checks.shell-arbitrary-evaluation.name",
8405
8654
  descriptionKey: "checks.shell-arbitrary-evaluation.description",
8406
8655
  category: "security",
@@ -8412,7 +8661,7 @@ registry.register(
8412
8661
  remediationKey: "remediation.security.shell-arbitrary-evaluation"
8413
8662
  })
8414
8663
  );
8415
- var CHECK_ID13 = "security.shell-unescaped-path-deletion";
8664
+ var CHECK_ID14 = "security.shell-unescaped-path-deletion";
8416
8665
  var QUERY2 = `
8417
8666
  (command
8418
8667
  name: (command_name (word) @cmd)
@@ -8442,7 +8691,7 @@ var QUERY2 = `
8442
8691
  `;
8443
8692
  registry.register(
8444
8693
  createTreeSitterCheck({
8445
- id: CHECK_ID13,
8694
+ id: CHECK_ID14,
8446
8695
  nameKey: "checks.shell-unescaped-path-deletion.name",
8447
8696
  descriptionKey: "checks.shell-unescaped-path-deletion.description",
8448
8697
  category: "security",
@@ -8454,7 +8703,7 @@ registry.register(
8454
8703
  remediationKey: "remediation.security.shell-unescaped-path-deletion"
8455
8704
  })
8456
8705
  );
8457
- var CHECK_ID14 = "security.file-inclusion";
8706
+ var CHECK_ID15 = "security.file-inclusion";
8458
8707
  var QUERIES3 = {
8459
8708
  php: `
8460
8709
  (include_expression
@@ -8484,7 +8733,7 @@ var QUERIES3 = {
8484
8733
  };
8485
8734
  registry.register(
8486
8735
  createTreeSitterCheck({
8487
- id: CHECK_ID14,
8736
+ id: CHECK_ID15,
8488
8737
  nameKey: "checks.file-inclusion.name",
8489
8738
  descriptionKey: "checks.file-inclusion.description",
8490
8739
  category: "security",
@@ -8496,7 +8745,7 @@ registry.register(
8496
8745
  remediationKey: "remediation.security.file-inclusion"
8497
8746
  })
8498
8747
  );
8499
- var CHECK_ID15 = "security.open-redirect";
8748
+ var CHECK_ID16 = "security.open-redirect";
8500
8749
  var QUERIES4 = {
8501
8750
  php: `
8502
8751
  (function_call_expression
@@ -8666,7 +8915,7 @@ var QUERIES4 = {
8666
8915
  };
8667
8916
  registry.register(
8668
8917
  createTreeSitterCheck({
8669
- id: CHECK_ID15,
8918
+ id: CHECK_ID16,
8670
8919
  nameKey: "checks.open-redirect.name",
8671
8920
  descriptionKey: "checks.open-redirect.description",
8672
8921
  category: "security",
@@ -8678,7 +8927,7 @@ registry.register(
8678
8927
  remediationKey: "remediation.security.open-redirect"
8679
8928
  })
8680
8929
  );
8681
- var CHECK_ID16 = "security.unsafe-file-upload";
8930
+ var CHECK_ID17 = "security.unsafe-file-upload";
8682
8931
  var QUERIES5 = {
8683
8932
  javascript: `
8684
8933
  (call_expression
@@ -8850,7 +9099,7 @@ var QUERIES5 = {
8850
9099
  };
8851
9100
  registry.register(
8852
9101
  createTreeSitterCheck({
8853
- id: CHECK_ID16,
9102
+ id: CHECK_ID17,
8854
9103
  nameKey: "checks.unsafe-file-upload.name",
8855
9104
  descriptionKey: "checks.unsafe-file-upload.description",
8856
9105
  category: "security",
@@ -8866,7 +9115,7 @@ registry.register(
8866
9115
  }
8867
9116
  })
8868
9117
  );
8869
- var CHECK_ID17 = "security.unsafe-deserialization";
9118
+ var CHECK_ID18 = "security.unsafe-deserialization";
8870
9119
  var QUERIES6 = {
8871
9120
  javascript: `
8872
9121
  (call_expression
@@ -9003,7 +9252,7 @@ var QUERIES6 = {
9003
9252
  };
9004
9253
  registry.register(
9005
9254
  createTreeSitterCheck({
9006
- id: CHECK_ID17,
9255
+ id: CHECK_ID18,
9007
9256
  nameKey: "checks.unsafe-deserialization.name",
9008
9257
  descriptionKey: "checks.unsafe-deserialization.description",
9009
9258
  category: "security",
@@ -9021,7 +9270,7 @@ registry.register(
9021
9270
  }
9022
9271
  })
9023
9272
  );
9024
- var CHECK_ID18 = "security.hardcoded-passwords";
9273
+ var CHECK_ID19 = "security.hardcoded-passwords";
9025
9274
  var PASSWORD_NAME_REGEX = /^(\$)?(password|pwd|passwd|secret|pass)$/i;
9026
9275
  var REMEDIATION13 = getCopy("remediation.security.hardcoded-passwords");
9027
9276
  var HTML_CSS_PASSWORD_PATTERNS = [
@@ -9102,7 +9351,7 @@ var QUERIES7 = {
9102
9351
  dockerfile: "((identifier) @name) @match"
9103
9352
  };
9104
9353
  var regexCheck4 = {
9105
- id: CHECK_ID18,
9354
+ id: CHECK_ID19,
9106
9355
  name: getCopy("checks.hardcoded-passwords.name"),
9107
9356
  description: getCopy("checks.hardcoded-passwords.description"),
9108
9357
  category: "security",
@@ -9127,8 +9376,8 @@ var regexCheck4 = {
9127
9376
  const value = match[2] ?? match[1] ?? "";
9128
9377
  if (match[2] === void 0 && line.toLowerCase().includes('type="password"')) {
9129
9378
  findings.push({
9130
- id: `${CHECK_ID18}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
9131
- checkId: CHECK_ID18,
9379
+ id: `${CHECK_ID19}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
9380
+ checkId: CHECK_ID19,
9132
9381
  category: "security",
9133
9382
  severity: "critical",
9134
9383
  message: getCopy("checks.hardcoded-passwords.message", "password"),
@@ -9143,8 +9392,8 @@ var regexCheck4 = {
9143
9392
  if (!PASSWORD_NAME_REGEX.test(name)) continue;
9144
9393
  if ((value ?? "").trim().length === 0) continue;
9145
9394
  findings.push({
9146
- id: `${CHECK_ID18}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
9147
- checkId: CHECK_ID18,
9395
+ id: `${CHECK_ID19}:${file.relativePath}:${lineNum + 1}:${(match.index ?? 0) + 1}:${matchIdx}`,
9396
+ checkId: CHECK_ID19,
9148
9397
  category: "security",
9149
9398
  severity: "critical",
9150
9399
  message: getCopy("checks.hardcoded-passwords.message", name),
@@ -9162,7 +9411,7 @@ var regexCheck4 = {
9162
9411
  }
9163
9412
  };
9164
9413
  var treeSitterCheck = createTreeSitterCheck({
9165
- id: CHECK_ID18,
9414
+ id: CHECK_ID19,
9166
9415
  nameKey: "checks.hardcoded-passwords.name",
9167
9416
  descriptionKey: "checks.hardcoded-passwords.description",
9168
9417
  category: "security",
@@ -9358,7 +9607,7 @@ function isFullHtmlDocument(content) {
9358
9607
  function hasHeadElement(content) {
9359
9608
  return /<head\b/i.test(content);
9360
9609
  }
9361
- var CHECK_ID19 = "security.html-missing-csp";
9610
+ var CHECK_ID20 = "security.html-missing-csp";
9362
9611
  var REMEDIATION14 = getCopy("remediation.security.html-missing-csp");
9363
9612
  var META_CSP_PATTERN = /<meta\b[^>]*\bhttp-equiv\s*=\s*["']content-security-policy["'][^>]*>/gi;
9364
9613
  var CSP_CONTENT_PATTERN = /\bcontent\s*=\s*(["'])([\s\S]*?)\1/i;
@@ -9366,7 +9615,7 @@ function firstHtmlAnchor(files) {
9366
9615
  return files.filter((file) => file.language === "html" && !isMechanicalCheckExcludedPath(file.relativePath)).map((file) => file.relativePath).sort()[0];
9367
9616
  }
9368
9617
  registry.register({
9369
- id: CHECK_ID19,
9618
+ id: CHECK_ID20,
9370
9619
  name: getCopy("checks.html-missing-csp.name"),
9371
9620
  description: getCopy("checks.html-missing-csp.description"),
9372
9621
  category: "security",
@@ -9396,8 +9645,8 @@ registry.register({
9396
9645
  if (/default-src\s+\*|unsafe-inline|unsafe-eval/i.test(cspValue)) {
9397
9646
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
9398
9647
  findings.push({
9399
- id: `${CHECK_ID19}:${file.relativePath}:${line}:${column}`,
9400
- checkId: CHECK_ID19,
9648
+ id: `${CHECK_ID20}:${file.relativePath}:${line}:${column}`,
9649
+ checkId: CHECK_ID20,
9401
9650
  category: "security",
9402
9651
  severity: "medium",
9403
9652
  message: getCopy("checks.html-missing-csp.message", "weak Content Security Policy"),
@@ -9414,8 +9663,8 @@ registry.register({
9414
9663
  }
9415
9664
  if (findings.length === 0 && sawFullHtmlDoc && !sawValidMetaCsp) {
9416
9665
  findings.push({
9417
- id: `${CHECK_ID19}:${anchor}:1:1`,
9418
- checkId: CHECK_ID19,
9666
+ id: `${CHECK_ID20}:${anchor}:1:1`,
9667
+ checkId: CHECK_ID20,
9419
9668
  category: "security",
9420
9669
  severity: "medium",
9421
9670
  message: getCopy("checks.html-missing-csp.message", "no Content Security Policy found"),
@@ -9429,10 +9678,10 @@ registry.register({
9429
9678
  return findings;
9430
9679
  }
9431
9680
  });
9432
- var CHECK_ID20 = "security.html-static-host-headers";
9681
+ var CHECK_ID21 = "security.html-static-host-headers";
9433
9682
  var REMEDIATION15 = getCopy("remediation.security.html-static-host-headers");
9434
9683
  registry.register({
9435
- id: CHECK_ID20,
9684
+ id: CHECK_ID21,
9436
9685
  name: getCopy("checks.html-static-host-headers.name"),
9437
9686
  description: getCopy("checks.html-static-host-headers.description"),
9438
9687
  category: "security",
@@ -9452,8 +9701,8 @@ registry.register({
9452
9701
  if (!anchor) return [];
9453
9702
  return [
9454
9703
  {
9455
- id: `${CHECK_ID20}:${anchor}:1:1`,
9456
- checkId: CHECK_ID20,
9704
+ id: `${CHECK_ID21}:${anchor}:1:1`,
9705
+ checkId: CHECK_ID21,
9457
9706
  category: "security",
9458
9707
  severity: "medium",
9459
9708
  message: getCopy("checks.html-static-host-headers.message", missing.join(", ")),
@@ -9466,7 +9715,7 @@ registry.register({
9466
9715
  ];
9467
9716
  }
9468
9717
  });
9469
- var CHECK_ID21 = "security.html-inline-unsafe-script";
9718
+ var CHECK_ID22 = "security.html-inline-unsafe-script";
9470
9719
  var REMEDIATION16 = getCopy("remediation.security.html-inline-unsafe-script");
9471
9720
  var SCRIPT_BLOCK_PATTERN2 = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
9472
9721
  var EVENT_HANDLER_PATTERN = /\son[a-z]+\s*=\s*["']([^"']+)["']/gi;
@@ -9523,7 +9772,7 @@ function findEventHandlerMatches(content) {
9523
9772
  return results;
9524
9773
  }
9525
9774
  registry.register({
9526
- id: CHECK_ID21,
9775
+ id: CHECK_ID22,
9527
9776
  name: getCopy("checks.html-inline-unsafe-script.name"),
9528
9777
  description: getCopy("checks.html-inline-unsafe-script.description"),
9529
9778
  category: "security",
@@ -9538,8 +9787,8 @@ registry.register({
9538
9787
  for (const match of findScriptMatches(content)) {
9539
9788
  const { line, column } = lineAndColumn6(content, match.index);
9540
9789
  findings.push({
9541
- id: `${CHECK_ID21}:${file.relativePath}:${line}:${column}`,
9542
- checkId: CHECK_ID21,
9790
+ id: `${CHECK_ID22}:${file.relativePath}:${line}:${column}`,
9791
+ checkId: CHECK_ID22,
9543
9792
  category: "security",
9544
9793
  severity: "high",
9545
9794
  message: getCopy("checks.html-inline-unsafe-script.message", match.text),
@@ -9553,8 +9802,8 @@ registry.register({
9553
9802
  for (const match of findEventHandlerMatches(content)) {
9554
9803
  const { line, column } = lineAndColumn6(content, match.index);
9555
9804
  findings.push({
9556
- id: `${CHECK_ID21}:${file.relativePath}:${line}:${column}:event`,
9557
- checkId: CHECK_ID21,
9805
+ id: `${CHECK_ID22}:${file.relativePath}:${line}:${column}:event`,
9806
+ checkId: CHECK_ID22,
9558
9807
  category: "security",
9559
9808
  severity: "high",
9560
9809
  message: getCopy("checks.html-inline-unsafe-script.message", match.text),
@@ -9569,10 +9818,10 @@ registry.register({
9569
9818
  return findings;
9570
9819
  }
9571
9820
  });
9572
- var CHECK_ID22 = "security.html-referrer-leak";
9821
+ var CHECK_ID23 = "security.html-referrer-leak";
9573
9822
  var REMEDIATION17 = getCopy("remediation.security.html-referrer-leak");
9574
9823
  registry.register({
9575
- id: CHECK_ID22,
9824
+ id: CHECK_ID23,
9576
9825
  name: getCopy("checks.html-referrer-leak.name"),
9577
9826
  description: getCopy("checks.html-referrer-leak.description"),
9578
9827
  category: "security",
@@ -9588,8 +9837,8 @@ registry.register({
9588
9837
  if (target.hasRel) continue;
9589
9838
  const { line, column } = lineAndColumn5(content, target.index);
9590
9839
  findings.push({
9591
- id: `${CHECK_ID22}:${file.relativePath}:${line}:${column}`,
9592
- checkId: CHECK_ID22,
9840
+ id: `${CHECK_ID23}:${file.relativePath}:${line}:${column}`,
9841
+ checkId: CHECK_ID23,
9593
9842
  category: "security",
9594
9843
  severity: "medium",
9595
9844
  message: getCopy("checks.html-referrer-leak.message", target.text),
@@ -9604,7 +9853,7 @@ registry.register({
9604
9853
  return findings;
9605
9854
  }
9606
9855
  });
9607
- var CHECK_ID23 = "security.html-css-comments-secrets";
9856
+ var CHECK_ID24 = "security.html-css-comments-secrets";
9608
9857
  var REMEDIATION18 = getCopy("remediation.security.html-css-comments-secrets");
9609
9858
  var SECRET_MARKER = /(?:api[_-]?key|firebase[_-]?config|google[_-]?api|token|secret|password|private[_-]?key|access[_-]?key)/i;
9610
9859
  var SECRET_VALUE = /(?:[:=]\s*|["'`])([A-Za-z0-9+/=_-]{8,}|sk_[A-Za-z0-9/+=_-]{8,})/i;
@@ -9619,8 +9868,8 @@ function scanPattern(content, filePath, checkPrefix, pattern, messageText, findi
9619
9868
  const value = valueMatch?.[1] ?? text22;
9620
9869
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
9621
9870
  findings.push({
9622
- id: `${CHECK_ID23}:${filePath}:${line}:${column}:${checkPrefix}`,
9623
- checkId: CHECK_ID23,
9871
+ id: `${CHECK_ID24}:${filePath}:${line}:${column}:${checkPrefix}`,
9872
+ checkId: CHECK_ID24,
9624
9873
  category: "security",
9625
9874
  severity: "critical",
9626
9875
  message: getCopy("checks.html-css-comments-secrets.message", messageText),
@@ -9633,7 +9882,7 @@ function scanPattern(content, filePath, checkPrefix, pattern, messageText, findi
9633
9882
  }
9634
9883
  }
9635
9884
  registry.register({
9636
- id: CHECK_ID23,
9885
+ id: CHECK_ID24,
9637
9886
  name: getCopy("checks.html-css-comments-secrets.name"),
9638
9887
  description: getCopy("checks.html-css-comments-secrets.description"),
9639
9888
  category: "security",
@@ -9652,7 +9901,7 @@ registry.register({
9652
9901
  return findings;
9653
9902
  }
9654
9903
  });
9655
- var CHECK_ID24 = "security.css-insecure-import";
9904
+ var CHECK_ID25 = "security.css-insecure-import";
9656
9905
  var REMEDIATION19 = getCopy("remediation.security.css-insecure-import");
9657
9906
  var HTTP_URL_PATTERN = /^http:\/\//i;
9658
9907
  var IMPORT_PATTERN = /@import\s+(?:url\(\s*)?(["']?)([^"')\s;]+)\1\s*\)?\s*;/gi;
@@ -9665,8 +9914,8 @@ function lineText(content, index) {
9665
9914
  function pushFinding3(findings, filePath, content, matchText, index) {
9666
9915
  const { line, column } = lineAndColumn5(content, index);
9667
9916
  findings.push({
9668
- id: `${CHECK_ID24}:${filePath}:${line}:${column}`,
9669
- checkId: CHECK_ID24,
9917
+ id: `${CHECK_ID25}:${filePath}:${line}:${column}`,
9918
+ checkId: CHECK_ID25,
9670
9919
  category: "security",
9671
9920
  severity: "medium",
9672
9921
  message: getCopy("checks.css-insecure-import.message", matchText),
@@ -9678,7 +9927,7 @@ function pushFinding3(findings, filePath, content, matchText, index) {
9678
9927
  });
9679
9928
  }
9680
9929
  registry.register({
9681
- id: CHECK_ID24,
9930
+ id: CHECK_ID25,
9682
9931
  name: getCopy("checks.css-insecure-import.name"),
9683
9932
  description: getCopy("checks.css-insecure-import.description"),
9684
9933
  category: "security",
@@ -9705,7 +9954,7 @@ registry.register({
9705
9954
  return findings;
9706
9955
  }
9707
9956
  });
9708
- var CHECK_ID25 = "security.html-mixed-content";
9957
+ var CHECK_ID26 = "security.html-mixed-content";
9709
9958
  var REMEDIATION20 = getCopy("remediation.security.html-mixed-content");
9710
9959
  var TAG_RESOURCE_PATTERN = /<(script|link|iframe|img|form|object|embed)\b[^>]*?\b(src|href|action|data)\s*=\s*["'](http:\/\/[^"']+)["'][^>]*?>/gi;
9711
9960
  var STYLE_ATTR_PATTERN = /\bstyle\s*=\s*["']([\s\S]*?http:\/\/[\s\S]*?)["']/gi;
@@ -9714,8 +9963,8 @@ var CSS_URL_PATTERN = /url\(\s*(["']?)(http:\/\/[^"')]+)\1\s*\)/gi;
9714
9963
  function pushFinding4(findings, filePath, content, index, matchText) {
9715
9964
  const { line, column } = lineAndColumn5(content, index);
9716
9965
  findings.push({
9717
- id: `${CHECK_ID25}:${filePath}:${line}:${column}`,
9718
- checkId: CHECK_ID25,
9966
+ id: `${CHECK_ID26}:${filePath}:${line}:${column}`,
9967
+ checkId: CHECK_ID26,
9719
9968
  category: "security",
9720
9969
  severity: "medium",
9721
9970
  message: getCopy("checks.html-mixed-content.message", matchText),
@@ -9727,7 +9976,7 @@ function pushFinding4(findings, filePath, content, index, matchText) {
9727
9976
  });
9728
9977
  }
9729
9978
  registry.register({
9730
- id: CHECK_ID25,
9979
+ id: CHECK_ID26,
9731
9980
  name: getCopy("checks.html-mixed-content.name"),
9732
9981
  description: getCopy("checks.html-mixed-content.description"),
9733
9982
  category: "security",
@@ -9765,7 +10014,7 @@ registry.register({
9765
10014
  return findings;
9766
10015
  }
9767
10016
  });
9768
- var CHECK_ID26 = "security.html-external-asset-integrity";
10017
+ var CHECK_ID27 = "security.html-external-asset-integrity";
9769
10018
  var REMEDIATION21 = getCopy("remediation.security.html-external-asset-integrity");
9770
10019
  var SCRIPT_PATTERN = /<script\b[^>]*?\bsrc\s*=\s*["'](https:\/\/[^"']+)["'][^>]*?>/gi;
9771
10020
  var STYLESHEET_PATTERN = /<link\b(?=[^>]*\brel\s*=\s*["'][^"']*\bstylesheet\b[^"']*["'])[^>]*?\bhref\s*=\s*["'](https:\/\/[^"']+)["'][^>]*?>/gi;
@@ -9780,7 +10029,7 @@ function isLocalDevelopmentUrl(rawUrl) {
9780
10029
  }
9781
10030
  }
9782
10031
  registry.register({
9783
- id: CHECK_ID26,
10032
+ id: CHECK_ID27,
9784
10033
  name: getCopy("checks.html-external-asset-integrity.name"),
9785
10034
  description: getCopy("checks.html-external-asset-integrity.description"),
9786
10035
  category: "security",
@@ -9801,8 +10050,8 @@ registry.register({
9801
10050
  if (hasIntegrity && hasCrossorigin) continue;
9802
10051
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
9803
10052
  findings.push({
9804
- id: `${CHECK_ID26}:${file.relativePath}:${line}:${column}`,
9805
- checkId: CHECK_ID26,
10053
+ id: `${CHECK_ID27}:${file.relativePath}:${line}:${column}`,
10054
+ checkId: CHECK_ID27,
9806
10055
  category: "security",
9807
10056
  severity: "medium",
9808
10057
  message: getCopy("checks.html-external-asset-integrity.message", tagText),
@@ -9822,8 +10071,8 @@ registry.register({
9822
10071
  if (hasIntegrity && hasCrossorigin) continue;
9823
10072
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
9824
10073
  findings.push({
9825
- id: `${CHECK_ID26}:${file.relativePath}:${line}:${column}`,
9826
- checkId: CHECK_ID26,
10074
+ id: `${CHECK_ID27}:${file.relativePath}:${line}:${column}`,
10075
+ checkId: CHECK_ID27,
9827
10076
  category: "security",
9828
10077
  severity: "medium",
9829
10078
  message: getCopy("checks.html-external-asset-integrity.message", tagText),
@@ -9838,7 +10087,7 @@ registry.register({
9838
10087
  return findings;
9839
10088
  }
9840
10089
  });
9841
- var CHECK_ID27 = "security.html-insecure-form-action";
10090
+ var CHECK_ID28 = "security.html-insecure-form-action";
9842
10091
  var REMEDIATION22 = getCopy("remediation.security.html-insecure-form-action");
9843
10092
  var FORM_ACTION_PATTERN = /<form\b[^>]*?\baction\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))[^>]*?>/gi;
9844
10093
  var LOCAL_DEV_URL_PATTERN = /^(?:https?:)?\/\/(?:localhost|127\.0\.0\.1|\[::1\]|::1)(?::\d+)?(?:\/|$)/i;
@@ -9852,7 +10101,7 @@ function lineAndColumn7(content, index) {
9852
10101
  };
9853
10102
  }
9854
10103
  registry.register({
9855
- id: CHECK_ID27,
10104
+ id: CHECK_ID28,
9856
10105
  name: getCopy("checks.html-insecure-form-action.name"),
9857
10106
  description: getCopy("checks.html-insecure-form-action.description"),
9858
10107
  category: "security",
@@ -9870,8 +10119,8 @@ registry.register({
9870
10119
  if (LOCAL_DEV_URL_PATTERN.test(action)) continue;
9871
10120
  const { line, column } = lineAndColumn7(content, match.index ?? 0);
9872
10121
  findings.push({
9873
- id: `${CHECK_ID27}:${file.relativePath}:${line}:${column}`,
9874
- checkId: CHECK_ID27,
10122
+ id: `${CHECK_ID28}:${file.relativePath}:${line}:${column}`,
10123
+ checkId: CHECK_ID28,
9875
10124
  category: "security",
9876
10125
  severity: "medium",
9877
10126
  message: getCopy("checks.html-insecure-form-action.message", match[0] ?? action),
@@ -9886,7 +10135,7 @@ registry.register({
9886
10135
  return findings;
9887
10136
  }
9888
10137
  });
9889
- var CHECK_ID28 = "security.html-iframe-sandbox";
10138
+ var CHECK_ID29 = "security.html-iframe-sandbox";
9890
10139
  var REMEDIATION23 = getCopy("remediation.security.html-iframe-sandbox");
9891
10140
  var IFRAME_PATTERN = /<iframe\b[^>]*?>/gi;
9892
10141
  var SRC_PATTERN = /\bsrc\s*=\s*["']([^"']+)["']/i;
@@ -9901,7 +10150,7 @@ function hasSandboxAttribute(tagText) {
9901
10150
  return { present: true, value: match[1] ?? match[2] ?? match[3] ?? "" };
9902
10151
  }
9903
10152
  registry.register({
9904
- id: CHECK_ID28,
10153
+ id: CHECK_ID29,
9905
10154
  name: getCopy("checks.html-iframe-sandbox.name"),
9906
10155
  description: getCopy("checks.html-iframe-sandbox.description"),
9907
10156
  category: "security",
@@ -9924,8 +10173,8 @@ registry.register({
9924
10173
  if (sandbox.present && !hasBroadEscape) continue;
9925
10174
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
9926
10175
  findings.push({
9927
- id: `${CHECK_ID28}:${file.relativePath}:${line}:${column}`,
9928
- checkId: CHECK_ID28,
10176
+ id: `${CHECK_ID29}:${file.relativePath}:${line}:${column}`,
10177
+ checkId: CHECK_ID29,
9929
10178
  category: "security",
9930
10179
  severity: "medium",
9931
10180
  message: getCopy("checks.html-iframe-sandbox.message", tagText),
@@ -9940,7 +10189,7 @@ registry.register({
9940
10189
  return findings;
9941
10190
  }
9942
10191
  });
9943
- var CHECK_ID29 = "security.html-unsafe-embed";
10192
+ var CHECK_ID30 = "security.html-unsafe-embed";
9944
10193
  var REMEDIATION24 = getCopy("remediation.security.html-unsafe-embed");
9945
10194
  var OBJECT_PATTERN = /<object\b[^>]*?\bdata\s*=\s*["'](https:\/\/[^"']+)["'][^>]*?>/gi;
9946
10195
  var EMBED_PATTERN = /<embed\b[^>]*?\bsrc\s*=\s*["'](https:\/\/[^"']+)["'][^>]*?>/gi;
@@ -9961,8 +10210,8 @@ function getTypeValue(tagText) {
9961
10210
  function pushFindings(findings, filePath, content, index, tagText) {
9962
10211
  const { line, column } = lineAndColumn8(content, index);
9963
10212
  findings.push({
9964
- id: `${CHECK_ID29}:${filePath}:${line}:${column}`,
9965
- checkId: CHECK_ID29,
10213
+ id: `${CHECK_ID30}:${filePath}:${line}:${column}`,
10214
+ checkId: CHECK_ID30,
9966
10215
  category: "security",
9967
10216
  severity: "medium",
9968
10217
  message: getCopy("checks.html-unsafe-embed.message", tagText),
@@ -9974,7 +10223,7 @@ function pushFindings(findings, filePath, content, index, tagText) {
9974
10223
  });
9975
10224
  }
9976
10225
  registry.register({
9977
- id: CHECK_ID29,
10226
+ id: CHECK_ID30,
9978
10227
  name: getCopy("checks.html-unsafe-embed.name"),
9979
10228
  description: getCopy("checks.html-unsafe-embed.description"),
9980
10229
  category: "security",
@@ -10006,7 +10255,7 @@ registry.register({
10006
10255
  return findings;
10007
10256
  }
10008
10257
  });
10009
- var CHECK_ID30 = "security.html-meta-redirect";
10258
+ var CHECK_ID31 = "security.html-meta-redirect";
10010
10259
  var REMEDIATION25 = getCopy("remediation.security.html-meta-redirect");
10011
10260
  var META_REFRESH_PATTERN = /<meta\b[^>]*\bhttp-equiv\s*=\s*["']refresh["'][^>]*>/gi;
10012
10261
  var CONTENT_PATTERN = /\bcontent\s*=\s*["']([^"']+)["']/i;
@@ -10023,7 +10272,7 @@ function parseRefreshContent(value) {
10023
10272
  };
10024
10273
  }
10025
10274
  registry.register({
10026
- id: CHECK_ID30,
10275
+ id: CHECK_ID31,
10027
10276
  name: getCopy("checks.html-meta-redirect.name"),
10028
10277
  description: getCopy("checks.html-meta-redirect.description"),
10029
10278
  category: "security",
@@ -10046,8 +10295,8 @@ registry.register({
10046
10295
  if (LOCAL_DEV_URL_PATTERN4.test(parsed.url)) continue;
10047
10296
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
10048
10297
  findings.push({
10049
- id: `${CHECK_ID30}:${file.relativePath}:${line}:${column}`,
10050
- checkId: CHECK_ID30,
10298
+ id: `${CHECK_ID31}:${file.relativePath}:${line}:${column}`,
10299
+ checkId: CHECK_ID31,
10051
10300
  category: "security",
10052
10301
  severity: "medium",
10053
10302
  message: getCopy("checks.html-meta-redirect.message", tagText),
@@ -10062,7 +10311,7 @@ registry.register({
10062
10311
  return findings;
10063
10312
  }
10064
10313
  });
10065
- var CHECK_ID31 = "security.html-base-tag-hijack";
10314
+ var CHECK_ID32 = "security.html-base-tag-hijack";
10066
10315
  var REMEDIATION26 = getCopy("remediation.security.html-base-tag-hijack");
10067
10316
  var BASE_PATTERN = /<base\b[^>]*?>/gi;
10068
10317
  var HREF_PATTERN = /\bhref\s*=\s*["']([^"']+)["']/i;
@@ -10078,7 +10327,7 @@ function isOutsideHead(content, index) {
10078
10327
  return false;
10079
10328
  }
10080
10329
  registry.register({
10081
- id: CHECK_ID31,
10330
+ id: CHECK_ID32,
10082
10331
  name: getCopy("checks.html-base-tag-hijack.name"),
10083
10332
  description: getCopy("checks.html-base-tag-hijack.description"),
10084
10333
  category: "security",
@@ -10100,8 +10349,8 @@ registry.register({
10100
10349
  if (!externalHref && !targetBlank && !outsideHead) continue;
10101
10350
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
10102
10351
  findings.push({
10103
- id: `${CHECK_ID31}:${file.relativePath}:${line}:${column}`,
10104
- checkId: CHECK_ID31,
10352
+ id: `${CHECK_ID32}:${file.relativePath}:${line}:${column}`,
10353
+ checkId: CHECK_ID32,
10105
10354
  category: "security",
10106
10355
  severity: "medium",
10107
10356
  message: getCopy("checks.html-base-tag-hijack.message", tagText),
@@ -10116,12 +10365,12 @@ registry.register({
10116
10365
  return findings;
10117
10366
  }
10118
10367
  });
10119
- var CHECK_ID32 = "security.html-javascript-uri";
10368
+ var CHECK_ID33 = "security.html-javascript-uri";
10120
10369
  var REMEDIATION27 = getCopy("remediation.security.html-javascript-uri");
10121
10370
  var TAG_PATTERN = /<(a|form|iframe|object|embed)\b[^>]*?\b(href|action|src|data)\s*=\s*["'](javascript:[^"']+)["'][^>]*?>/gi;
10122
10371
  var VOID_PATTERN = /^javascript:\s*(?:void\s*\(?\s*0\s*\)?)(?:\s*;)?\s*$/i;
10123
10372
  registry.register({
10124
- id: CHECK_ID32,
10373
+ id: CHECK_ID33,
10125
10374
  name: getCopy("checks.html-javascript-uri.name"),
10126
10375
  description: getCopy("checks.html-javascript-uri.description"),
10127
10376
  category: "security",
@@ -10139,8 +10388,8 @@ registry.register({
10139
10388
  if (VOID_PATTERN.test(jsUrl)) continue;
10140
10389
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
10141
10390
  findings.push({
10142
- id: `${CHECK_ID32}:${file.relativePath}:${line}:${column}`,
10143
- checkId: CHECK_ID32,
10391
+ id: `${CHECK_ID33}:${file.relativePath}:${line}:${column}`,
10392
+ checkId: CHECK_ID33,
10144
10393
  category: "security",
10145
10394
  severity: "low",
10146
10395
  message: getCopy("checks.html-javascript-uri.message", tagText),
@@ -10155,7 +10404,7 @@ registry.register({
10155
10404
  return findings;
10156
10405
  }
10157
10406
  });
10158
- var CHECK_ID33 = "security.html-import-map-integrity";
10407
+ var CHECK_ID34 = "security.html-import-map-integrity";
10159
10408
  var REMEDIATION28 = getCopy("remediation.security.html-import-map-integrity");
10160
10409
  var IMPORTMAP_SCRIPT_PATTERN = /<script\b[^>]*\btype\s*=\s*["']importmap["'][^>]*>([\s\S]*?)<\/script>/gi;
10161
10410
  var IMPORTMAP_SRC_PATTERN = /<script\b[^>]*\btype\s*=\s*["']importmap["'][^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
@@ -10176,8 +10425,8 @@ function isExternalUrl(value) {
10176
10425
  function pushFinding5(findings, filePath, content, index, matchText) {
10177
10426
  const { line, column } = lineAndColumn9(content, index);
10178
10427
  findings.push({
10179
- id: `${CHECK_ID33}:${filePath}:${line}:${column}`,
10180
- checkId: CHECK_ID33,
10428
+ id: `${CHECK_ID34}:${filePath}:${line}:${column}`,
10429
+ checkId: CHECK_ID34,
10181
10430
  category: "security",
10182
10431
  severity: "medium",
10183
10432
  message: getCopy("checks.html-import-map-integrity.message", matchText),
@@ -10202,7 +10451,7 @@ function scanImportMapValue(findings, filePath, content, value, contextLabel, in
10202
10451
  }
10203
10452
  }
10204
10453
  registry.register({
10205
- id: CHECK_ID33,
10454
+ id: CHECK_ID34,
10206
10455
  name: getCopy("checks.html-import-map-integrity.name"),
10207
10456
  description: getCopy("checks.html-import-map-integrity.description"),
10208
10457
  category: "security",
@@ -10235,7 +10484,7 @@ registry.register({
10235
10484
  return findings;
10236
10485
  }
10237
10486
  });
10238
- var CHECK_ID34 = "security.css-data-exfiltration";
10487
+ var CHECK_ID35 = "security.css-data-exfiltration";
10239
10488
  var REMEDIATION29 = getCopy("remediation.security.css-data-exfiltration");
10240
10489
  var ATTR_SELECTOR_PATTERN = /\[[^\]]*(?:value|type|name|email|tel|address|cc-number|cc-exp)[^\]]*(?:\^=|\$=|\*=|~=|\|=)[^\]]*\]/i;
10241
10490
  var EXTERNAL_HTTP_URL_PATTERN = /url\(\s*(["']?)http:\/\/[^"')]+\1\s*\)/i;
@@ -10253,8 +10502,8 @@ function lineAndColumn10(content, index) {
10253
10502
  function pushFinding6(findings, filePath, content, index, matchText) {
10254
10503
  const { line, column } = lineAndColumn10(content, index);
10255
10504
  findings.push({
10256
- id: `${CHECK_ID34}:${filePath}:${line}:${column}`,
10257
- checkId: CHECK_ID34,
10505
+ id: `${CHECK_ID35}:${filePath}:${line}:${column}`,
10506
+ checkId: CHECK_ID35,
10258
10507
  category: "security",
10259
10508
  severity: "medium",
10260
10509
  message: getCopy("checks.css-data-exfiltration.message", matchText),
@@ -10275,7 +10524,7 @@ function scanCss(findings, filePath, scanContent, originalContent, offset = 0) {
10275
10524
  }
10276
10525
  }
10277
10526
  registry.register({
10278
- id: CHECK_ID34,
10527
+ id: CHECK_ID35,
10279
10528
  name: getCopy("checks.css-data-exfiltration.name"),
10280
10529
  description: getCopy("checks.css-data-exfiltration.description"),
10281
10530
  category: "security",
@@ -10307,7 +10556,7 @@ registry.register({
10307
10556
  return findings;
10308
10557
  }
10309
10558
  });
10310
- var CHECK_ID35 = "security.css-external-font-integrity";
10559
+ var CHECK_ID36 = "security.css-external-font-integrity";
10311
10560
  var REMEDIATION30 = getCopy("remediation.security.css-external-font-integrity");
10312
10561
  var FONT_FACE_PATTERN = /@font-face\b[\s\S]*?\}/gi;
10313
10562
  var URL_PATTERN = /url\(\s*(["']?)([^"')]+)\1\s*\)/gi;
@@ -10339,7 +10588,7 @@ function isPublicCdnFont(url) {
10339
10588
  }
10340
10589
  }
10341
10590
  registry.register({
10342
- id: CHECK_ID35,
10591
+ id: CHECK_ID36,
10343
10592
  name: getCopy("checks.css-external-font-integrity.name"),
10344
10593
  description: getCopy("checks.css-external-font-integrity.description"),
10345
10594
  category: "security",
@@ -10360,8 +10609,8 @@ registry.register({
10360
10609
  if (!isPublicCdnFont(url)) continue;
10361
10610
  const { line, column } = lineAndColumn11(content, face.index ?? 0);
10362
10611
  findings.push({
10363
- id: `${CHECK_ID35}:${file.relativePath}:${line}:${column}`,
10364
- checkId: CHECK_ID35,
10612
+ id: `${CHECK_ID36}:${file.relativePath}:${line}:${column}`,
10613
+ checkId: CHECK_ID36,
10365
10614
  category: "security",
10366
10615
  severity: "medium",
10367
10616
  message: getCopy("checks.css-external-font-integrity.message", match[0] ?? url),
@@ -10377,7 +10626,7 @@ registry.register({
10377
10626
  return findings;
10378
10627
  }
10379
10628
  });
10380
- var CHECK_ID36 = "security.insecure-random";
10629
+ var CHECK_ID37 = "security.insecure-random";
10381
10630
  var QUERIES8 = {
10382
10631
  javascript: `
10383
10632
  (call_expression
@@ -10446,7 +10695,7 @@ var QUERIES8 = {
10446
10695
  };
10447
10696
  registry.register(
10448
10697
  createTreeSitterCheck({
10449
- id: CHECK_ID36,
10698
+ id: CHECK_ID37,
10450
10699
  nameKey: "checks.insecure-random.name",
10451
10700
  descriptionKey: "checks.insecure-random.description",
10452
10701
  category: "security",
@@ -10458,7 +10707,7 @@ registry.register(
10458
10707
  remediationKey: "remediation.security.insecure-random"
10459
10708
  })
10460
10709
  );
10461
- var CHECK_ID37 = "security.weak-key-entropy";
10710
+ var CHECK_ID38 = "security.weak-key-entropy";
10462
10711
  var REMEDIATION31 = getCopy("remediation.security.weak-key-entropy");
10463
10712
  var RANDOM_BYTES_PATTERN = /randomBytes\s*\(\s*(\d+)\s*\)/g;
10464
10713
  var WEAK_KEY_CONTEXT = /(?:key|token|secret|id|license|password|session|csrf|nonce|salt|iv)/i;
@@ -10471,7 +10720,7 @@ function lineAndColumn12(content, index) {
10471
10720
  };
10472
10721
  }
10473
10722
  registry.register({
10474
- id: CHECK_ID37,
10723
+ id: CHECK_ID38,
10475
10724
  name: getCopy("checks.weak-key-entropy.name"),
10476
10725
  description: getCopy("checks.weak-key-entropy.description"),
10477
10726
  category: "security",
@@ -10493,8 +10742,8 @@ registry.register({
10493
10742
  if (!WEAK_KEY_CONTEXT.test(lineContent) && !WEAK_KEY_CONTEXT.test(content.slice(Math.max(0, matchIndex - 200), matchIndex))) continue;
10494
10743
  const { line, column } = lineAndColumn12(content, matchIndex);
10495
10744
  findings.push({
10496
- id: `${CHECK_ID37}:${file.relativePath}:${line}:${column}`,
10497
- checkId: CHECK_ID37,
10745
+ id: `${CHECK_ID38}:${file.relativePath}:${line}:${column}`,
10746
+ checkId: CHECK_ID38,
10498
10747
  category: "security",
10499
10748
  severity: "high",
10500
10749
  message: getCopy("checks.weak-key-entropy.message", match[0], String(byteCount)),
@@ -10509,7 +10758,7 @@ registry.register({
10509
10758
  return findings;
10510
10759
  }
10511
10760
  });
10512
- var CHECK_ID38 = "security.insecure-api-url-default";
10761
+ var CHECK_ID39 = "security.insecure-api-url-default";
10513
10762
  var REMEDIATION32 = getCopy("remediation.security.insecure-api-url-default");
10514
10763
  var HTTP_URL_DEFAULT_PATTERN = /(?:['"`])(https?:\/\/[^\s'"`]+)(?:['"`])/g;
10515
10764
  var API_CONTEXT = /(?:api[_-]?url|base[_-]?url|endpoint|server|host|webhook|resend|paddle|stripe|sendgrid|mailgun|postmark)/i;
@@ -10524,7 +10773,7 @@ function lineAndColumn13(content, index) {
10524
10773
  };
10525
10774
  }
10526
10775
  registry.register({
10527
- id: CHECK_ID38,
10776
+ id: CHECK_ID39,
10528
10777
  name: getCopy("checks.insecure-api-url-default.name"),
10529
10778
  description: getCopy("checks.insecure-api-url-default.description"),
10530
10779
  category: "security",
@@ -10549,8 +10798,8 @@ registry.register({
10549
10798
  if (!isEnvDefault && !API_CONTEXT.test(matchLine)) continue;
10550
10799
  const { line, column } = lineAndColumn13(content, matchIndex);
10551
10800
  findings.push({
10552
- id: `${CHECK_ID38}:${file.relativePath}:${line}:${column}`,
10553
- checkId: CHECK_ID38,
10801
+ id: `${CHECK_ID39}:${file.relativePath}:${line}:${column}`,
10802
+ checkId: CHECK_ID39,
10554
10803
  category: "security",
10555
10804
  severity: "high",
10556
10805
  message: getCopy("checks.insecure-api-url-default.message", url),
@@ -10565,7 +10814,7 @@ registry.register({
10565
10814
  return findings;
10566
10815
  }
10567
10816
  });
10568
- var CHECK_ID39 = "security.dynamic-sql-columns";
10817
+ var CHECK_ID40 = "security.dynamic-sql-columns";
10569
10818
  var REMEDIATION33 = getCopy("remediation.security.dynamic-sql-columns");
10570
10819
  var SQL_SET_INTERPOLATION = /\bSET\s+.*\$\{[^}]*\}/gis;
10571
10820
  var SQL_SET_TEMPLATE = /\bSET\s+.*`[^`]*\$\{[^}]*\}[^`]*`/gis;
@@ -10579,7 +10828,7 @@ function lineAndColumn14(content, index) {
10579
10828
  };
10580
10829
  }
10581
10830
  registry.register({
10582
- id: CHECK_ID39,
10831
+ id: CHECK_ID40,
10583
10832
  name: getCopy("checks.dynamic-sql-columns.name"),
10584
10833
  description: getCopy("checks.dynamic-sql-columns.description"),
10585
10834
  category: "security",
@@ -10600,11 +10849,11 @@ registry.register({
10600
10849
  for (const match of content.matchAll(regex)) {
10601
10850
  const matchIndex = match.index ?? 0;
10602
10851
  const { line, column } = lineAndColumn14(content, matchIndex);
10603
- const dedupKey = `${CHECK_ID39}:${file.relativePath}:${line}`;
10852
+ const dedupKey = `${CHECK_ID40}:${file.relativePath}:${line}`;
10604
10853
  if (findings.some((f) => f.id === dedupKey)) continue;
10605
10854
  findings.push({
10606
10855
  id: dedupKey,
10607
- checkId: CHECK_ID39,
10856
+ checkId: CHECK_ID40,
10608
10857
  category: "security",
10609
10858
  severity: "medium",
10610
10859
  message: getCopy("checks.dynamic-sql-columns.message", label),
@@ -10620,7 +10869,7 @@ registry.register({
10620
10869
  return findings;
10621
10870
  }
10622
10871
  });
10623
- var CHECK_ID40 = "security.weak-email-validation";
10872
+ var CHECK_ID41 = "security.weak-email-validation";
10624
10873
  var REMEDIATION34 = getCopy("remediation.security.weak-email-validation");
10625
10874
  var WEAK_EMAIL_REGEX_LINE = /\/\^[^/]*@[^/]*\\\.[^/]{0,4}\$\/[gimsuy]*/g;
10626
10875
  var LOOSE_REGEX_CLASS = /\[\^?\\?s@\]\+@\[\^?\\?s@\]\+\\\.\[\^?\\?s@\]\+/g;
@@ -10634,7 +10883,7 @@ function lineAndColumn15(content, index) {
10634
10883
  };
10635
10884
  }
10636
10885
  registry.register({
10637
- id: CHECK_ID40,
10886
+ id: CHECK_ID41,
10638
10887
  name: getCopy("checks.weak-email-validation.name"),
10639
10888
  description: getCopy("checks.weak-email-validation.description"),
10640
10889
  category: "security",
@@ -10655,11 +10904,11 @@ registry.register({
10655
10904
  for (const match of content.matchAll(regex)) {
10656
10905
  const matchIndex = match.index ?? 0;
10657
10906
  const { line, column } = lineAndColumn15(content, matchIndex);
10658
- const dedupKey = `${CHECK_ID40}:${file.relativePath}:${line}`;
10907
+ const dedupKey = `${CHECK_ID41}:${file.relativePath}:${line}`;
10659
10908
  if (findings.some((f) => f.id === dedupKey)) continue;
10660
10909
  findings.push({
10661
10910
  id: dedupKey,
10662
- checkId: CHECK_ID40,
10911
+ checkId: CHECK_ID41,
10663
10912
  category: "security",
10664
10913
  severity: "medium",
10665
10914
  message: getCopy("checks.weak-email-validation.message", `${match[0]} (${label})`),
@@ -10675,7 +10924,7 @@ registry.register({
10675
10924
  return findings;
10676
10925
  }
10677
10926
  });
10678
- var CHECK_ID41 = "security.idempotency-key-leak";
10927
+ var CHECK_ID42 = "security.idempotency-key-leak";
10679
10928
  var REMEDIATION35 = getCopy("remediation.security.idempotency-key-leak");
10680
10929
  var IDEMPOTENCY_CONTEXT = /idempotency[_-]?key|idempotency[_-]?token/i;
10681
10930
  var TEMPLATE_LITERAL_INTERPOLATION = new RegExp("`[^`]*\\$\\{[^}]+\\}[^`]*`", "g");
@@ -10689,7 +10938,7 @@ function lineAndColumn16(content, index) {
10689
10938
  };
10690
10939
  }
10691
10940
  registry.register({
10692
- id: CHECK_ID41,
10941
+ id: CHECK_ID42,
10693
10942
  name: getCopy("checks.idempotency-key-leak.name"),
10694
10943
  description: getCopy("checks.idempotency-key-leak.description"),
10695
10944
  category: "security",
@@ -10708,11 +10957,11 @@ registry.register({
10708
10957
  if (!STRUCTURED_ID_PATTERN.test(templateStr)) continue;
10709
10958
  const matchIndex = match.index ?? 0;
10710
10959
  const { line, column } = lineAndColumn16(content, matchIndex);
10711
- const dedupKey = `${CHECK_ID41}:${file.relativePath}:${line}`;
10960
+ const dedupKey = `${CHECK_ID42}:${file.relativePath}:${line}`;
10712
10961
  if (findings.some((f) => f.id === dedupKey)) continue;
10713
10962
  findings.push({
10714
10963
  id: dedupKey,
10715
- checkId: CHECK_ID41,
10964
+ checkId: CHECK_ID42,
10716
10965
  category: "security",
10717
10966
  severity: "medium",
10718
10967
  message: getCopy("checks.idempotency-key-leak.message", templateStr.slice(0, 80)),
@@ -10727,7 +10976,7 @@ registry.register({
10727
10976
  return findings;
10728
10977
  }
10729
10978
  });
10730
- var CHECK_ID42 = "data-exposure.plaintext-secret-in-email";
10979
+ var CHECK_ID43 = "data-exposure.plaintext-secret-in-email";
10731
10980
  var REMEDIATION36 = getCopy("remediation.data-exposure.plaintext-secret-in-email");
10732
10981
  var EMAIL_FILE_PATTERN = /email|mail|notify|notification/i;
10733
10982
  var SECRET_IN_TEXT_PATTERN = /(?:license[_-]?key|api[_-]?key|secret[_-]?key|token|password|auth[_-]?code|verification[_-]?code)\b/i;
@@ -10741,7 +10990,7 @@ function lineAndColumn17(content, index) {
10741
10990
  };
10742
10991
  }
10743
10992
  registry.register({
10744
- id: CHECK_ID42,
10993
+ id: CHECK_ID43,
10745
10994
  name: getCopy("checks.plaintext-secret-in-email.name"),
10746
10995
  description: getCopy("checks.plaintext-secret-in-email.description"),
10747
10996
  category: "data-exposure",
@@ -10763,8 +11012,8 @@ registry.register({
10763
11012
  const { line: lineNum, column } = lineAndColumn17(content, lineIndex);
10764
11013
  const secretMatch = SECRET_IN_TEXT_PATTERN.exec(lineText2);
10765
11014
  findings.push({
10766
- id: `${CHECK_ID42}:${file.relativePath}:${lineNum}:${column}`,
10767
- checkId: CHECK_ID42,
11015
+ id: `${CHECK_ID43}:${file.relativePath}:${lineNum}:${column}`,
11016
+ checkId: CHECK_ID43,
10768
11017
  category: "data-exposure",
10769
11018
  severity: "medium",
10770
11019
  message: getCopy("checks.plaintext-secret-in-email.message", secretMatch?.[0] ?? "secret"),
@@ -12974,7 +13223,7 @@ registry.register(
12974
13223
  attribution: "CWE-94"
12975
13224
  })
12976
13225
  );
12977
- var CHECK_ID43 = "security.missing-csrf-protection";
13226
+ var CHECK_ID44 = "security.missing-csrf-protection";
12978
13227
  var CSRF_MARKERS = /\bcsrf\b|xsrf|csrfmiddlewaretoken|double[\s-]submit[\s-]cookie|csurf\b|cookie-parser.*csrf|protect_from_forgery|verify_authenticity_token/i;
12979
13228
  var QUERIES9 = {
12980
13229
  php: `
@@ -13031,7 +13280,7 @@ async function hasProjectCsrfProtection(files) {
13031
13280
  return false;
13032
13281
  }
13033
13282
  var check = {
13034
- id: CHECK_ID43,
13283
+ id: CHECK_ID44,
13035
13284
  name: resolveCopy3("checks.missing-csrf-protection.name"),
13036
13285
  description: resolveCopy3("checks.missing-csrf-protection.description"),
13037
13286
  category: "security",
@@ -13071,8 +13320,8 @@ var check = {
13071
13320
  const sample = eligible.find((f) => f.language === lang);
13072
13321
  if (!sample) continue;
13073
13322
  findings.push({
13074
- id: `${CHECK_ID43}:${sample.relativePath}:degraded:${lang}`,
13075
- checkId: CHECK_ID43,
13323
+ id: `${CHECK_ID44}:${sample.relativePath}:degraded:${lang}`,
13324
+ checkId: CHECK_ID44,
13076
13325
  category: "security",
13077
13326
  severity: "info",
13078
13327
  message: resolveCopy3("checks.missing-csrf-protection.degraded", sample.relativePath, lang),
@@ -13093,8 +13342,8 @@ var check = {
13093
13342
  handlerCount > 1 ? `${handlerCount} state-changing handlers (e.g. ${exampleRoute})` : exampleRoute
13094
13343
  );
13095
13344
  const finding = {
13096
- id: `${CHECK_ID43}:project:${anchor.relativePath}:1`,
13097
- checkId: CHECK_ID43,
13345
+ id: `${CHECK_ID44}:project:${anchor.relativePath}:1`,
13346
+ checkId: CHECK_ID44,
13098
13347
  category: "security",
13099
13348
  severity: "medium",
13100
13349
  message,
@@ -13109,7 +13358,7 @@ var check = {
13109
13358
  }
13110
13359
  };
13111
13360
  registry.register(check);
13112
- var CHECK_ID44 = "security.weak-session-id";
13361
+ var CHECK_ID45 = "security.weak-session-id";
13113
13362
  var QUERIES10 = {
13114
13363
  php: `
13115
13364
  (function_call_expression
@@ -13196,7 +13445,7 @@ var QUERIES10 = {
13196
13445
  };
13197
13446
  registry.register(
13198
13447
  createTreeSitterCheck({
13199
- id: CHECK_ID44,
13448
+ id: CHECK_ID45,
13200
13449
  nameKey: "checks.weak-session-id.name",
13201
13450
  descriptionKey: "checks.weak-session-id.description",
13202
13451
  category: "security",
@@ -13214,7 +13463,7 @@ registry.register(
13214
13463
  }
13215
13464
  })
13216
13465
  );
13217
- var CHECK_ID45 = "security.rails-mass-assignment";
13466
+ var CHECK_ID46 = "security.rails-mass-assignment";
13218
13467
  var QUERIES11 = {
13219
13468
  ruby: `
13220
13469
  (call
@@ -13237,7 +13486,7 @@ var QUERIES11 = {
13237
13486
  };
13238
13487
  registry.register(
13239
13488
  createTreeSitterCheck({
13240
- id: CHECK_ID45,
13489
+ id: CHECK_ID46,
13241
13490
  nameKey: "checks.rails-mass-assignment.name",
13242
13491
  descriptionKey: "checks.rails-mass-assignment.description",
13243
13492
  category: "security",
@@ -13249,7 +13498,7 @@ registry.register(
13249
13498
  remediationKey: "remediation.security.rails-mass-assignment"
13250
13499
  })
13251
13500
  );
13252
- var CHECK_ID46 = "security.rails-insecure-session-config";
13501
+ var CHECK_ID47 = "security.rails-insecure-session-config";
13253
13502
  var REMEDIATION37 = getCopy("remediation.security.rails-insecure-session-config");
13254
13503
  var SESSION_STORE_PATH = /config\/initializers\/session_store\.rb$/;
13255
13504
  function hasSecureFlag(content) {
@@ -13262,7 +13511,7 @@ function hasSameSiteFlag(content) {
13262
13511
  return /\bsame_site:\s*:(strict|lax)\b/.test(content);
13263
13512
  }
13264
13513
  registry.register({
13265
- id: CHECK_ID46,
13514
+ id: CHECK_ID47,
13266
13515
  name: getCopy("checks.rails-insecure-session-config.name"),
13267
13516
  description: getCopy("checks.rails-insecure-session-config.description"),
13268
13517
  category: "security",
@@ -13282,8 +13531,8 @@ registry.register({
13282
13531
  if (!hasSameSiteFlag(content)) missing.push("same_site: :strict or :lax");
13283
13532
  if (missing.length > 0) {
13284
13533
  findings.push({
13285
- id: `${CHECK_ID46}:${file.relativePath}:1`,
13286
- checkId: CHECK_ID46,
13534
+ id: `${CHECK_ID47}:${file.relativePath}:1`,
13535
+ checkId: CHECK_ID47,
13287
13536
  category: "security",
13288
13537
  severity: "medium",
13289
13538
  message: getCopy(
@@ -13300,7 +13549,7 @@ registry.register({
13300
13549
  return findings;
13301
13550
  }
13302
13551
  });
13303
- var CHECK_ID47 = "security.xxe-surface";
13552
+ var CHECK_ID48 = "security.xxe-surface";
13304
13553
  var LIBXML_PARSE_QUERY = `
13305
13554
  (call_expression
13306
13555
  function: (member_expression
@@ -13347,7 +13596,7 @@ var QUERIES12 = {
13347
13596
  };
13348
13597
  registry.register(
13349
13598
  createTreeSitterCheck({
13350
- id: CHECK_ID47,
13599
+ id: CHECK_ID48,
13351
13600
  nameKey: "checks.xxe-surface.name",
13352
13601
  descriptionKey: "checks.xxe-surface.description",
13353
13602
  category: "security",
@@ -13546,12 +13795,13 @@ registry.register({
13546
13795
  return runLLMCheck(ctx, promptTemplate22);
13547
13796
  }
13548
13797
  });
13549
- var PROMPT_VERSION23 = "1.0.0";
13798
+ var PROMPT_VERSION23 = "1.1.0";
13550
13799
  var fallbackPatterns23 = [
13551
13800
  {
13552
13801
  regex: /while\s*\(\s*(?:true|1)\s*\)|while\s+True\s*:/i,
13553
13802
  message: "Infinite loop found \u2014 verify retry logic has a maximum attempt limit",
13554
- severity: "medium"
13803
+ severity: "medium",
13804
+ skipCommentLines: true
13555
13805
  }
13556
13806
  ];
13557
13807
  async function buildPrompt23(sourceFiles) {
@@ -13613,12 +13863,13 @@ registry.register({
13613
13863
  return [...bashFindings, ...llmFindings];
13614
13864
  }
13615
13865
  });
13616
- var PROMPT_VERSION24 = "1.1.0";
13866
+ var PROMPT_VERSION24 = "1.2.0";
13617
13867
  var fallbackPatterns24 = [
13618
13868
  {
13619
13869
  regex: /console\.(?:log|error|warn)\b/,
13620
13870
  message: "Unstructured logging detected \u2014 verify observability hooks are present",
13621
- severity: "medium"
13871
+ severity: "medium",
13872
+ skipCommentLines: true
13622
13873
  }
13623
13874
  ];
13624
13875
  async function buildPrompt24(sourceFiles) {
@@ -13841,12 +14092,13 @@ registry.register({
13841
14092
  return runLLMCheck(ctx, promptTemplate27);
13842
14093
  }
13843
14094
  });
13844
- var PROMPT_VERSION28 = "1.0.0";
14095
+ var PROMPT_VERSION28 = "1.1.0";
13845
14096
  var fallbackPatterns28 = [
13846
14097
  {
13847
14098
  regex: /\b(?:fingerprint|deviceId|visitorId|trackingId|correlationId|analyticsId)\b/i,
13848
14099
  message: "Derived identifier may link records across sessions or users",
13849
- severity: "medium"
14100
+ severity: "medium",
14101
+ skipCommentLines: true
13850
14102
  },
13851
14103
  {
13852
14104
  regex: /\bhash\s*\(\s*(?:email|phone|userId|id)\s*\)/i,
@@ -13880,15 +14132,64 @@ var promptTemplate28 = {
13880
14132
  parseResponse: parseResponse28,
13881
14133
  fallbackPatterns: fallbackPatterns28
13882
14134
  };
14135
+ var CHECK_ID49 = "data-exposure.derived-identifiers";
14136
+ var DERIVED_ID_REGEX = /\b(?:fingerprint|device.?id|visitor.?id|tracking.?id|correlation.?id|analytics.?id)\b/i;
14137
+ var DERIVED_TS_QUERIES = {
14138
+ javascript: `
14139
+ (lexical_declaration (variable_declarator name: (identifier) @name)) @match
14140
+ (pair key: (property_identifier) @name) @match
14141
+ (assignment_expression left: (identifier) @name) @match
14142
+ `,
14143
+ typescript: `
14144
+ (lexical_declaration (variable_declarator name: (identifier) @name)) @match
14145
+ (pair key: (property_identifier) @name) @match
14146
+ (property_signature name: (property_identifier) @name) @match
14147
+ `,
14148
+ tsx: `
14149
+ (lexical_declaration (variable_declarator name: (identifier) @name)) @match
14150
+ (pair key: (property_identifier) @name) @match
14151
+ (property_signature name: (property_identifier) @name) @match
14152
+ `,
14153
+ go: `
14154
+ (short_var_declaration left: (expression_list (identifier) @name)) @match
14155
+ (var_declaration (var_spec name: (identifier) @name)) @match
14156
+ `,
14157
+ python: `
14158
+ (assignment left: (identifier) @name) @match
14159
+ `,
14160
+ java: `
14161
+ (local_variable_declaration declarator: (variable_declarator name: (identifier) @name)) @match
14162
+ (field_declaration declarator: (variable_declarator name: (identifier) @name)) @match
14163
+ `
14164
+ };
14165
+ var derivedIdAstCheck = createTreeSitterCheck({
14166
+ id: CHECK_ID49,
14167
+ nameKey: "checks.derived-identifiers.name",
14168
+ descriptionKey: "checks.derived-identifiers.description",
14169
+ category: "data-exposure",
14170
+ severity: "medium",
14171
+ minTier: "pro",
14172
+ queries: DERIVED_TS_QUERIES,
14173
+ messageKey: "checks.derived-identifiers.message",
14174
+ degradedMessageKey: "checks.derived-identifiers.degraded",
14175
+ remediationKey: "remediation.data-exposure.derived-identifiers",
14176
+ fileFilter: (file) => !isMechanicalCheckExcludedPath(file.relativePath),
14177
+ matchFilter: (captures) => DERIVED_ID_REGEX.test(captures.name ?? ""),
14178
+ messageFactory: (captures) => getCopy("checks.derived-identifiers.message", captures.name ?? "unknown")
14179
+ });
13883
14180
  registry.register({
13884
- id: "data-exposure.derived-identifiers",
14181
+ id: CHECK_ID49,
13885
14182
  name: getCopy("checks.derived-identifiers.name"),
13886
14183
  description: getCopy("checks.derived-identifiers.description"),
13887
14184
  category: "data-exposure",
13888
14185
  severity: "medium",
13889
14186
  minTier: "pro",
13890
14187
  async run(ctx) {
13891
- return runLLMCheck(ctx, promptTemplate28);
14188
+ const [llmFindings, astFindings] = await Promise.all([
14189
+ runLLMCheck(ctx, promptTemplate28),
14190
+ derivedIdAstCheck.run(ctx)
14191
+ ]);
14192
+ return mergeFindings(astFindings, llmFindings);
13892
14193
  }
13893
14194
  });
13894
14195
  var PROMPT_VERSION29 = "1.0.0";
@@ -13954,12 +14255,12 @@ registry.register({
13954
14255
  return [...bashFindings, ...llmFindings];
13955
14256
  }
13956
14257
  });
13957
- var CHECK_ID48 = "security.permissive-grants";
14258
+ var CHECK_ID50 = "security.permissive-grants";
13958
14259
  var GRANT_PATTERN = /\bgrant\s+(?:all|all\s+privileges|select|insert|update|delete|usage|execute)\b[\s\S]*?\bto\s+(?:public|anon|anonymous|guest)\b/gi;
13959
14260
  var ALTER_DEFAULT_PRIVILEGES_PATTERN = /\balter\s+default\s+privileges\b[\s\S]*?\bgrant\s+[\s\S]*?\bto\s+(?:public|anon|anonymous|guest)\b/gi;
13960
14261
  var REMEDIATION38 = getCopy("remediation.security.permissive-grants");
13961
14262
  var permissiveGrantsCheck = {
13962
- id: CHECK_ID48,
14263
+ id: CHECK_ID50,
13963
14264
  name: getCopy("checks.permissive-grants.name"),
13964
14265
  description: getCopy("checks.permissive-grants.description"),
13965
14266
  category: "security",
@@ -13976,8 +14277,8 @@ var permissiveGrantsCheck = {
13976
14277
  const beforeMatch = content.slice(0, match.index);
13977
14278
  const lineNum = beforeMatch.split("\n").length;
13978
14279
  findings.push({
13979
- id: `${CHECK_ID48}:${file.relativePath}:${lineNum}:1`,
13980
- checkId: CHECK_ID48,
14280
+ id: `${CHECK_ID50}:${file.relativePath}:${lineNum}:1`,
14281
+ checkId: CHECK_ID50,
13981
14282
  category: "security",
13982
14283
  severity: "high",
13983
14284
  message: getCopy("checks.permissive-grants.message", match[0].trim()),
@@ -13994,8 +14295,8 @@ var permissiveGrantsCheck = {
13994
14295
  const beforeMatch = content.slice(0, altMatch.index);
13995
14296
  const lineNum = beforeMatch.split("\n").length;
13996
14297
  findings.push({
13997
- id: `${CHECK_ID48}:${file.relativePath}:${lineNum}:1`,
13998
- checkId: CHECK_ID48,
14298
+ id: `${CHECK_ID50}:${file.relativePath}:${lineNum}:1`,
14299
+ checkId: CHECK_ID50,
13999
14300
  category: "security",
14000
14301
  severity: "high",
14001
14302
  message: getCopy("checks.permissive-grants.message", altMatch[0].trim()),
@@ -14035,10 +14336,10 @@ function classifySqlDialect(content) {
14035
14336
  }
14036
14337
  return "unknown";
14037
14338
  }
14038
- var CHECK_ID49 = "security.sql-rls-static";
14339
+ var CHECK_ID51 = "security.sql-rls-static";
14039
14340
  var REMEDIATION39 = getCopy("remediation.security.sql-rls-static");
14040
14341
  var sqlRlsStaticCheck = {
14041
- id: CHECK_ID49,
14342
+ id: CHECK_ID51,
14042
14343
  name: getCopy("checks.sql-rls-static.name"),
14043
14344
  description: getCopy("checks.sql-rls-static.description"),
14044
14345
  category: "security",
@@ -14093,8 +14394,8 @@ var sqlRlsStaticCheck = {
14093
14394
  const isRlsEnabled = enabledTables.has(table.name);
14094
14395
  if (!isRlsEnabled) {
14095
14396
  findings.push({
14096
- id: `${CHECK_ID49}:${file.relativePath}:${table.line}:1:missing-rls`,
14097
- checkId: CHECK_ID49,
14397
+ id: `${CHECK_ID51}:${file.relativePath}:${table.line}:1:missing-rls`,
14398
+ checkId: CHECK_ID51,
14098
14399
  category: "security",
14099
14400
  severity: "high",
14100
14401
  message: getCopy("checks.sql-rls-static.message", `Table "${table.name}" contains tenant columns but Row-Level Security is not enabled.`),
@@ -14108,8 +14409,8 @@ var sqlRlsStaticCheck = {
14108
14409
  const isRlsForced = forcedTables.has(table.name);
14109
14410
  if (!isRlsForced) {
14110
14411
  findings.push({
14111
- id: `${CHECK_ID49}:${file.relativePath}:${table.line}:1:missing-force-rls`,
14112
- checkId: CHECK_ID49,
14412
+ id: `${CHECK_ID51}:${file.relativePath}:${table.line}:1:missing-force-rls`,
14413
+ checkId: CHECK_ID51,
14113
14414
  category: "security",
14114
14415
  severity: "high",
14115
14416
  message: getCopy("checks.sql-rls-static.message", `Table "${table.name}" has Row-Level Security enabled but lacks FORCE ROW LEVEL SECURITY, creating tenant bypass risks for table owners.`),
@@ -14133,8 +14434,8 @@ var sqlRlsStaticCheck = {
14133
14434
  const beforeMatch = content.slice(0, policyMatch.index);
14134
14435
  const lineNum = beforeMatch.split("\n").length;
14135
14436
  findings.push({
14136
- id: `${CHECK_ID49}:${file.relativePath}:${lineNum}:1:weak-policy`,
14137
- checkId: CHECK_ID49,
14437
+ id: `${CHECK_ID51}:${file.relativePath}:${lineNum}:1:weak-policy`,
14438
+ checkId: CHECK_ID51,
14138
14439
  category: "security",
14139
14440
  severity: "high",
14140
14441
  message: getCopy("checks.sql-rls-static.message", `Policy ${policyName} on table "${tableName}" uses an unrestrictive tautology (like true or 1=1).`),
@@ -14152,10 +14453,10 @@ var sqlRlsStaticCheck = {
14152
14453
  }
14153
14454
  };
14154
14455
  registry.register(sqlRlsStaticCheck);
14155
- var CHECK_ID50 = "security.sql-security-definer-search-path";
14456
+ var CHECK_ID52 = "security.sql-security-definer-search-path";
14156
14457
  var REMEDIATION40 = getCopy("remediation.security.sql-security-definer-search-path");
14157
14458
  var sqlSecurityDefinerSearchPathCheck = {
14158
- id: CHECK_ID50,
14459
+ id: CHECK_ID52,
14159
14460
  name: getCopy("checks.sql-security-definer-search-path.name"),
14160
14461
  description: getCopy("checks.sql-security-definer-search-path.description"),
14161
14462
  category: "security",
@@ -14181,8 +14482,8 @@ var sqlSecurityDefinerSearchPathCheck = {
14181
14482
  const beforeMatch = content.slice(0, match.index);
14182
14483
  const lineNum = beforeMatch.split("\n").length;
14183
14484
  findings.push({
14184
- id: `${CHECK_ID50}:${file.relativePath}:${lineNum}:1`,
14185
- checkId: CHECK_ID50,
14485
+ id: `${CHECK_ID52}:${file.relativePath}:${lineNum}:1`,
14486
+ checkId: CHECK_ID52,
14186
14487
  category: "security",
14187
14488
  severity: "high",
14188
14489
  message: getCopy("checks.sql-security-definer-search-path.message", `Function "${functionName}" is defined as SECURITY DEFINER but is missing a locked search_path.`),
@@ -14201,7 +14502,7 @@ var sqlSecurityDefinerSearchPathCheck = {
14201
14502
  }
14202
14503
  };
14203
14504
  registry.register(sqlSecurityDefinerSearchPathCheck);
14204
- var CHECK_ID51 = "resilience.missing-transaction-guard";
14505
+ var CHECK_ID53 = "resilience.missing-transaction-guard";
14205
14506
  var REMEDIATION41 = getCopy("remediation.resilience.missing-transaction-guard");
14206
14507
  var MUTATION_KEYWORDS = [
14207
14508
  /\binsert\s+into\b/i,
@@ -14214,7 +14515,7 @@ var MUTATION_KEYWORDS = [
14214
14515
  /\bcreate\s+table\s+\w+\s+as\b/i
14215
14516
  ];
14216
14517
  var missingTransactionGuardCheck = {
14217
- id: CHECK_ID51,
14518
+ id: CHECK_ID53,
14218
14519
  name: getCopy("checks.missing-transaction-guard.name"),
14219
14520
  description: getCopy("checks.missing-transaction-guard.description"),
14220
14521
  category: "resilience",
@@ -14244,8 +14545,8 @@ var missingTransactionGuardCheck = {
14244
14545
  }
14245
14546
  if (mutationCount > 5) {
14246
14547
  findings.push({
14247
- id: `${CHECK_ID51}:${file.relativePath}:1:1`,
14248
- checkId: CHECK_ID51,
14548
+ id: `${CHECK_ID53}:${file.relativePath}:1:1`,
14549
+ checkId: CHECK_ID53,
14249
14550
  category: "resilience",
14250
14551
  severity: "medium",
14251
14552
  message: getCopy("checks.missing-transaction-guard.message"),
@@ -14260,10 +14561,10 @@ var missingTransactionGuardCheck = {
14260
14561
  }
14261
14562
  };
14262
14563
  registry.register(missingTransactionGuardCheck);
14263
- var CHECK_ID52 = "resilience.destructive-sql-migration";
14564
+ var CHECK_ID54 = "resilience.destructive-sql-migration";
14264
14565
  var REMEDIATION42 = getCopy("remediation.resilience.destructive-sql-migration");
14265
14566
  var destructiveSqlMigrationCheck = {
14266
- id: CHECK_ID52,
14567
+ id: CHECK_ID54,
14267
14568
  name: getCopy("checks.destructive-sql-migration.name"),
14268
14569
  description: getCopy("checks.destructive-sql-migration.description"),
14269
14570
  category: "resilience",
@@ -14284,8 +14585,8 @@ var destructiveSqlMigrationCheck = {
14284
14585
  const highMatch = highRiskRegex.exec(line);
14285
14586
  if (highMatch) {
14286
14587
  findings.push({
14287
- id: `${CHECK_ID52}:${file.relativePath}:${lineNum + 1}:${(highMatch.index ?? 0) + 1}:high`,
14288
- checkId: CHECK_ID52,
14588
+ id: `${CHECK_ID54}:${file.relativePath}:${lineNum + 1}:${(highMatch.index ?? 0) + 1}:high`,
14589
+ checkId: CHECK_ID54,
14289
14590
  category: "resilience",
14290
14591
  severity: "high",
14291
14592
  message: getCopy("checks.destructive-sql-migration.message", highMatch[0]),
@@ -14298,8 +14599,8 @@ var destructiveSqlMigrationCheck = {
14298
14599
  const truncateMatch = truncateRegex.exec(line);
14299
14600
  if (truncateMatch) {
14300
14601
  findings.push({
14301
- id: `${CHECK_ID52}:${file.relativePath}:${lineNum + 1}:${(truncateMatch.index ?? 0) + 1}:high`,
14302
- checkId: CHECK_ID52,
14602
+ id: `${CHECK_ID54}:${file.relativePath}:${lineNum + 1}:${(truncateMatch.index ?? 0) + 1}:high`,
14603
+ checkId: CHECK_ID54,
14303
14604
  category: "resilience",
14304
14605
  severity: "high",
14305
14606
  message: getCopy("checks.destructive-sql-migration.message", truncateMatch[0]),
@@ -14315,8 +14616,8 @@ var destructiveSqlMigrationCheck = {
14315
14616
  const colMatch = dropColumnRegex.exec(line);
14316
14617
  if (colMatch) {
14317
14618
  findings.push({
14318
- id: `${CHECK_ID52}:${file.relativePath}:${lineNum + 1}:${(colMatch.index ?? 0) + 1}:col`,
14319
- checkId: CHECK_ID52,
14619
+ id: `${CHECK_ID54}:${file.relativePath}:${lineNum + 1}:${(colMatch.index ?? 0) + 1}:col`,
14620
+ checkId: CHECK_ID54,
14320
14621
  category: "resilience",
14321
14622
  severity: "medium",
14322
14623
  message: getCopy("checks.destructive-sql-migration.message", colMatch[0]),
@@ -14329,8 +14630,8 @@ var destructiveSqlMigrationCheck = {
14329
14630
  const policyMatch = dropPolicyRegex.exec(line);
14330
14631
  if (policyMatch) {
14331
14632
  findings.push({
14332
- id: `${CHECK_ID52}:${file.relativePath}:${lineNum + 1}:${(policyMatch.index ?? 0) + 1}:policy`,
14333
- checkId: CHECK_ID52,
14633
+ id: `${CHECK_ID54}:${file.relativePath}:${lineNum + 1}:${(policyMatch.index ?? 0) + 1}:policy`,
14634
+ checkId: CHECK_ID54,
14334
14635
  category: "resilience",
14335
14636
  severity: "medium",
14336
14637
  message: getCopy("checks.destructive-sql-migration.message", policyMatch[0]),
@@ -14343,8 +14644,8 @@ var destructiveSqlMigrationCheck = {
14343
14644
  const indexMatch = dropIndexRegex.exec(line);
14344
14645
  if (indexMatch) {
14345
14646
  findings.push({
14346
- id: `${CHECK_ID52}:${file.relativePath}:${lineNum + 1}:${(indexMatch.index ?? 0) + 1}:index`,
14347
- checkId: CHECK_ID52,
14647
+ id: `${CHECK_ID54}:${file.relativePath}:${lineNum + 1}:${(indexMatch.index ?? 0) + 1}:index`,
14648
+ checkId: CHECK_ID54,
14348
14649
  category: "resilience",
14349
14650
  severity: "medium",
14350
14651
  message: getCopy("checks.destructive-sql-migration.message", indexMatch[0]),
@@ -14360,7 +14661,7 @@ var destructiveSqlMigrationCheck = {
14360
14661
  }
14361
14662
  };
14362
14663
  registry.register(destructiveSqlMigrationCheck);
14363
- var CHECK_ID53 = "resilience.sql-broad-mutation";
14664
+ var CHECK_ID55 = "resilience.sql-broad-mutation";
14364
14665
  var REMEDIATION43 = getCopy("remediation.resilience.sql-broad-mutation");
14365
14666
  function splitSqlStatements(content) {
14366
14667
  const statements = [];
@@ -14398,7 +14699,7 @@ function splitSqlStatements(content) {
14398
14699
  return statements;
14399
14700
  }
14400
14701
  var sqlBroadMutationCheck = {
14401
- id: CHECK_ID53,
14702
+ id: CHECK_ID55,
14402
14703
  name: getCopy("checks.sql-broad-mutation.name"),
14403
14704
  description: getCopy("checks.sql-broad-mutation.description"),
14404
14705
  category: "resilience",
@@ -14444,8 +14745,8 @@ var sqlBroadMutationCheck = {
14444
14745
  const beforeStatement = content.slice(0, charOffset + leadingWhitespace);
14445
14746
  const lineNum = beforeStatement.split("\n").length;
14446
14747
  findings.push({
14447
- id: `${CHECK_ID53}:${file.relativePath}:${lineNum}:1`,
14448
- checkId: CHECK_ID53,
14748
+ id: `${CHECK_ID55}:${file.relativePath}:${lineNum}:1`,
14749
+ checkId: CHECK_ID55,
14449
14750
  category: "resilience",
14450
14751
  severity: "high",
14451
14752
  message: getCopy("checks.sql-broad-mutation.message", matchedKeyword),
@@ -14463,7 +14764,7 @@ var sqlBroadMutationCheck = {
14463
14764
  }
14464
14765
  };
14465
14766
  registry.register(sqlBroadMutationCheck);
14466
- var CHECK_ID54 = "data-exposure.unprotected-sensitive-sql-columns";
14767
+ var CHECK_ID56 = "data-exposure.unprotected-sensitive-sql-columns";
14467
14768
  var REMEDIATION44 = getCopy("remediation.data-exposure.unprotected-sensitive-sql-columns");
14468
14769
  var SENSITIVE_KEYWORDS = [
14469
14770
  "ssn",
@@ -14495,7 +14796,7 @@ var PROTECTED_SUFFIXES = [
14495
14796
  "_key_id"
14496
14797
  ];
14497
14798
  var unprotectedSensitiveSqlColumnsCheck = {
14498
- id: CHECK_ID54,
14799
+ id: CHECK_ID56,
14499
14800
  name: getCopy("checks.unprotected-sensitive-sql-columns.name"),
14500
14801
  description: getCopy("checks.unprotected-sensitive-sql-columns.description"),
14501
14802
  category: "data-exposure",
@@ -14525,8 +14826,8 @@ var unprotectedSensitiveSqlColumnsCheck = {
14525
14826
  const isProtected = PROTECTED_SUFFIXES.some((suf) => colName.endsWith(suf));
14526
14827
  if (isSensitive && isPlainText && !isProtected) {
14527
14828
  findings.push({
14528
- id: `${CHECK_ID54}:${file.relativePath}:${match.startLine}:${match.startColumn ?? 1}`,
14529
- checkId: CHECK_ID54,
14829
+ id: `${CHECK_ID56}:${file.relativePath}:${match.startLine}:${match.startColumn ?? 1}`,
14830
+ checkId: CHECK_ID56,
14530
14831
  category: "data-exposure",
14531
14832
  severity: "high",
14532
14833
  message: getCopy("checks.unprotected-sensitive-sql-columns.message", `Column "${colNameRaw}" with type "${colTypeRaw}"`),
@@ -14590,9 +14891,9 @@ registry.register({
14590
14891
  return runLLMCheck(ctx, promptTemplate30);
14591
14892
  }
14592
14893
  });
14593
- var CHECK_ID55 = "security.rust-unsafe-blocks";
14894
+ var CHECK_ID57 = "security.rust-unsafe-blocks";
14594
14895
  registry.register({
14595
- id: CHECK_ID55,
14896
+ id: CHECK_ID57,
14596
14897
  name: getCopy("checks.rust-unsafe-blocks.name"),
14597
14898
  description: getCopy("checks.rust-unsafe-blocks.description"),
14598
14899
  category: "security",
@@ -14679,8 +14980,8 @@ registry.register({
14679
14980
  }
14680
14981
  if (!foundSafety) {
14681
14982
  const finding = {
14682
- id: `${CHECK_ID55}:${file.relativePath}:${match.startLine}:${match.startColumn ?? 0}:${i}`,
14683
- checkId: CHECK_ID55,
14983
+ id: `${CHECK_ID57}:${file.relativePath}:${match.startLine}:${match.startColumn ?? 0}:${i}`,
14984
+ checkId: CHECK_ID57,
14684
14985
  category: "security",
14685
14986
  severity: "medium",
14686
14987
  message: getCopy("checks.rust-unsafe-blocks.message", match.nodeText),
@@ -14698,7 +14999,7 @@ registry.register({
14698
14999
  return findings;
14699
15000
  }
14700
15001
  });
14701
- var CHECK_ID56 = "resilience.rust-handler-panics";
15002
+ var CHECK_ID58 = "resilience.rust-handler-panics";
14702
15003
  var RUST_QUERY = `
14703
15004
  (call_expression
14704
15005
  function: (field_expression
@@ -14724,7 +15025,7 @@ function hasAsyncModifier(node) {
14724
15025
  return false;
14725
15026
  }
14726
15027
  registry.register({
14727
- id: CHECK_ID56,
15028
+ id: CHECK_ID58,
14728
15029
  name: getCopy("checks.rust-handler-panics.name"),
14729
15030
  description: getCopy("checks.rust-handler-panics.description"),
14730
15031
  category: "resilience",
@@ -14790,8 +15091,8 @@ registry.register({
14790
15091
  const isAsync = hasAsyncModifier(enclosingFunction);
14791
15092
  if (isAsync) {
14792
15093
  const finding = {
14793
- id: `${CHECK_ID56}:${file.relativePath}:${match.startLine}:${match.startColumn ?? 0}:${i}`,
14794
- checkId: CHECK_ID56,
15094
+ id: `${CHECK_ID58}:${file.relativePath}:${match.startLine}:${match.startColumn ?? 0}:${i}`,
15095
+ checkId: CHECK_ID58,
14795
15096
  category: "resilience",
14796
15097
  severity: "high",
14797
15098
  message: getCopy("checks.rust-handler-panics.message", match.nodeText),
@@ -16229,6 +16530,7 @@ async function executeScan(args, ctx) {
16229
16530
  debug: args.debug,
16230
16531
  baseline: args.baseline,
16231
16532
  minConfidence: args.minConfidence,
16533
+ minSeverity: args.minSeverity,
16232
16534
  onCheckComplete: (completed, total, checkName) => {
16233
16535
  heartbeat();
16234
16536
  if (showProgress) {
@@ -16608,6 +16910,7 @@ async function runCli(options) {
16608
16910
  debug: options.debug,
16609
16911
  baseline: options.baseline,
16610
16912
  minConfidence: options.minConfidence,
16913
+ minSeverity: options.minSeverity,
16611
16914
  showIds: options.showIds,
16612
16915
  all: options.all
16613
16916
  };
@@ -16786,12 +17089,13 @@ var require5 = createRequire4(import.meta.url);
16786
17089
  var { version } = require5("../package.json");
16787
17090
  var cli = cac("seaworthycode");
16788
17091
  cli.version(version);
16789
- cli.command("[path]", "Scan a directory for issues").option("--json", "Output JSON to stdout").option("--output <path>", "Write HTML report to file").option("--sarif <path>", "Write SARIF report to file").option("--sarif-stdout", "Write SARIF report to stdout").option("--fail-on-findings", "Exit with code 5 when findings are found").option("--tier <tier>", "Select tier (free, pro)").option("--provider <provider>", "LLM provider (anthropic, openai, kimi, moonshot, deepseek, gemini, minimax, ollama, openrouter)").option("--max-file-size <bytes>", "Skip files larger than this (default: 1 MB)").option("--show-ids", "Show check IDs alongside names in terminal output").option("--debug", "Enable debug logging").option("--baseline [ref]", "Compare against a baseline file or save current findings as baseline (--baseline save)").option("--min-confidence <level>", "Hide findings below this confidence (high, medium, low)").option("--all", "List every finding; do not collapse low-severity findings").action(async (targetDir, options) => {
17092
+ cli.command("[path]", "Scan a directory for issues").option("--json", "Output JSON to stdout").option("--output <path>", "Write HTML report to file").option("--sarif <path>", "Write SARIF report to file").option("--sarif-stdout", "Write SARIF report to stdout").option("--fail-on-findings", "Exit with code 5 when findings are found").option("--tier <tier>", "Select tier (free, pro)").option("--provider <provider>", "LLM provider (anthropic, openai, kimi, moonshot, deepseek, gemini, minimax, ollama, openrouter)").option("--max-file-size <bytes>", "Skip files larger than this (default: 1 MB)").option("--show-ids", "Show check IDs alongside names in terminal output").option("--debug", "Enable debug logging").option("--baseline [ref]", "Compare against a baseline file or save current findings as baseline (--baseline save)").option("--min-confidence <level>", "Hide findings below this confidence (high, medium, low)").option("--min-severity <level>", "Hide findings below this severity (critical, high, medium, low, info)").option("--all", "List every finding; do not collapse low-severity findings").action(async (targetDir, options) => {
16790
17093
  const maxFileSize = options.maxFileSize ? Number(options.maxFileSize) : void 0;
16791
17094
  const tier = options.tier;
16792
17095
  const baseline = options.baseline === "save" ? "save" : options.baseline;
16793
17096
  const minConfidence = options.minConfidence;
16794
- const result = await runCli({ targetDir, json: options.json, output: options.output, sarif: options.sarif, sarifStdout: options.sarifStdout, failOnFindings: options.failOnFindings, tier, provider: options.provider, maxFileSize, showIds: options.showIds, debug: options.debug, baseline, minConfidence, all: options.all });
17097
+ const minSeverity = options.minSeverity;
17098
+ const result = await runCli({ targetDir, json: options.json, output: options.output, sarif: options.sarif, sarifStdout: options.sarifStdout, failOnFindings: options.failOnFindings, tier, provider: options.provider, maxFileSize, showIds: options.showIds, debug: options.debug, baseline, minConfidence, minSeverity, all: options.all });
16795
17099
  if (result.updateMessage) {
16796
17100
  console.error(result.updateMessage);
16797
17101
  }
@@ -16859,6 +17163,7 @@ cli.help(() => {
16859
17163
  --max-file-size <n> Skip files larger than n bytes (default: 1 MB)
16860
17164
  --baseline [ref] Compare against baseline file or save (--baseline save)
16861
17165
  --min-confidence <level> Hide findings below confidence (high, medium, low)
17166
+ --min-severity <level> Hide findings below severity (critical, high, medium, low, info)
16862
17167
  --show-ids Show check IDs alongside names
16863
17168
  --all List every finding; do not collapse low-severity findings
16864
17169
  --debug Enable debug logging`