seaworthycode 1.2.20 → 1.2.22

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,
@@ -1083,6 +1102,7 @@ var copy = {
1083
1102
  // Remediation hints
1084
1103
  "remediation.security.hardcoded-secrets": "Move this value to an environment variable and reference it via process.env.",
1085
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.`,
1086
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.",
1087
1107
  "remediation.security.auth-missing-endpoints": "Apply authentication middleware to this route handler. Use requireAuth, passport, JWT verification, or session-based auth.",
1088
1108
  "remediation.security.cors-too-permissive": "Restrict Access-Control-Allow-Origin to your specific frontend domains rather than using a wildcard.",
@@ -1783,6 +1803,39 @@ function pathsUnderTarget(topLevel, targetDir, repoRelativePath) {
1783
1803
  function isInsideGitRepo(dir) {
1784
1804
  return gitTopLevel(dir) !== null;
1785
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
+ }
1786
1839
  function hasFileInGitHistory(dir, pattern) {
1787
1840
  const topLevel = gitTopLevel(dir);
1788
1841
  if (!topLevel) {
@@ -4362,6 +4415,13 @@ function lineMatch(line, lineIndex, pattern) {
4362
4415
  function hasShellValidation(content) {
4363
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);
4364
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
+ }
4365
4425
  function shellLineHasTimeout(line) {
4366
4426
  return /^\s*timeout\s+\S+/.test(line) || /(?:^|\s)(?:--max-time|--connect-timeout|--timeout|-m)(?:\s|=|$)/.test(line);
4367
4427
  }
@@ -4449,23 +4509,24 @@ registry.register({
4449
4509
  severity: "high",
4450
4510
  minTier: "free",
4451
4511
  async run(ctx) {
4452
- const hasEnv = hasFileInGitHistory(
4512
+ const envMatch = findFileInGitHistory(
4453
4513
  ctx.targetDir,
4454
4514
  /(?:^|\/)\.env(?:$|\.(?!example$).+)/
4455
4515
  );
4456
4516
  const hasSettings = await hasCommittedSettingsWithSecrets(ctx.targetDir);
4457
4517
  const hasJavaConfig = await hasCommittedJavaConfigWithSecrets(ctx.targetDir);
4458
- if (!hasEnv && !hasSettings && !hasJavaConfig) return [];
4518
+ if (!envMatch.found && !hasSettings && !hasJavaConfig) return [];
4459
4519
  const findings = [];
4460
- if (hasEnv) {
4520
+ if (envMatch.found) {
4461
4521
  findings.push({
4462
4522
  id: "committed-env-1",
4463
4523
  checkId: "security.committed-env",
4464
4524
  category: "security",
4465
4525
  severity: "high",
4466
- 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),
4467
4527
  confidence: "high",
4468
- remediation: getCopy("remediation.security.committed-env")
4528
+ remediation: getCopy("remediation.security.committed-env"),
4529
+ properties: { commit: envMatch.commit, refs: envMatch.refs }
4469
4530
  });
4470
4531
  }
4471
4532
  if (hasSettings) {
@@ -4490,7 +4551,7 @@ registry.register({
4490
4551
  remediation: getCopy("remediation.security.committed-env")
4491
4552
  });
4492
4553
  }
4493
- if (hasEnv) {
4554
+ if (envMatch.found) {
4494
4555
  const sourceFindings = await scanBashLines(ctx, {
4495
4556
  checkId: "security.committed-env",
4496
4557
  category: "security",
@@ -5105,7 +5166,7 @@ registry.register({
5105
5166
  }
5106
5167
  if (lockfiles.length === 0) {
5107
5168
  try {
5108
- const output = execSync2("npm audit --json", {
5169
+ const output = execSync2("npm audit --json --omit=dev", {
5109
5170
  cwd: ctx.targetDir,
5110
5171
  stdio: "pipe",
5111
5172
  encoding: "utf-8",
@@ -5128,7 +5189,7 @@ registry.register({
5128
5189
  for (const lockfile of lockfiles) {
5129
5190
  const lockfileDir = join8(ctx.targetDir, dirname2(lockfile));
5130
5191
  try {
5131
- const output = execSync2("npm audit --json", {
5192
+ const output = execSync2("npm audit --json --omit=dev", {
5132
5193
  cwd: lockfileDir,
5133
5194
  stdio: "pipe",
5134
5195
  encoding: "utf-8",
@@ -6985,7 +7046,10 @@ async function scanBashNoInputValidation(ctx) {
6985
7046
  remediation: getCopy("remediation.resilience.no-input-validation")
6986
7047
  }, (line, content, index) => {
6987
7048
  if (hasShellValidation(content)) return null;
6988
- 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;
6989
7053
  }).then((findings) => findings.slice(0, 1));
6990
7054
  }
6991
7055
  registry.register({