seaworthycode 1.1.1 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/index.js +269 -59
  2. package/dist/index.js.map +1 -1
  3. package/package.json +16 -12
  4. package/rules/external/configuration/insecure-cookie-flags.test.yaml +114 -0
  5. package/rules/external/configuration/insecure-cookie-flags.yaml +77 -0
  6. package/rules/external/configuration/missing-ssl-verification.test.yaml +130 -0
  7. package/rules/external/configuration/missing-ssl-verification.yaml +123 -0
  8. package/rules/external/data-exposure/debug-artifacts.test.yaml +143 -0
  9. package/rules/external/data-exposure/debug-artifacts.yaml +57 -0
  10. package/rules/external/data-exposure/hardcoded-ip.test.yaml +131 -0
  11. package/rules/external/data-exposure/hardcoded-ip.yaml +67 -0
  12. package/rules/external/data-exposure/localstorage-sensitive-data.test.yaml +58 -0
  13. package/rules/external/data-exposure/localstorage-sensitive-data.yaml +60 -0
  14. package/rules/external/dependencies/deprecated-functions.test.yaml +92 -0
  15. package/rules/external/dependencies/deprecated-functions.yaml +66 -0
  16. package/rules/external/ops/hardcoded-port.test.yaml +141 -0
  17. package/rules/external/ops/hardcoded-port.yaml +91 -0
  18. package/rules/external/ops/insecure-file-permissions.test.yaml +111 -0
  19. package/rules/external/ops/insecure-file-permissions.yaml +91 -0
  20. package/rules/external/resilience/missing-memory-limit.test.yaml +54 -0
  21. package/rules/external/resilience/missing-memory-limit.yaml +59 -0
  22. package/rules/external/resilience/missing-timeout-config.test.yaml +179 -0
  23. package/rules/external/resilience/missing-timeout-config.yaml +124 -0
  24. package/rules/external/security/broken-crypto-algorithm.test.yaml +104 -0
  25. package/rules/external/security/broken-crypto-algorithm.yaml +79 -0
  26. package/rules/external/security/http-method-override.test.yaml +40 -0
  27. package/rules/external/security/http-method-override.yaml +35 -0
  28. package/rules/external/security/http-no-tls.test.yaml +115 -0
  29. package/rules/external/security/http-no-tls.yaml +59 -0
  30. package/rules/external/security/path-traversal.test.yaml +188 -0
  31. package/rules/external/security/path-traversal.yaml +210 -0
  32. package/rules/external/security/unsafe-innerhtml.test.yaml +72 -0
  33. package/rules/external/security/unsafe-innerhtml.yaml +46 -0
  34. package/rules/external/security/untrusted-deserialization.test.yaml +79 -0
  35. package/rules/external/security/untrusted-deserialization.yaml +48 -0
  36. package/rules/external/security/weak-encryption-mode.test.yaml +126 -0
  37. package/rules/external/security/weak-encryption-mode.yaml +104 -0
  38. package/rules/external/security/weak-hash-algorithm.test.yaml +121 -0
  39. package/rules/external/security/weak-hash-algorithm.yaml +87 -0
  40. package/rules/external/security/xxe-parsing.test.yaml +76 -0
  41. package/rules/external/security/xxe-parsing.yaml +63 -0
  42. package/rules/internal/security/dangerous-eval.test.yaml +126 -0
  43. package/rules/internal/security/dangerous-eval.yaml +134 -0
  44. package/rules/internal/security/no-document-write.test.yaml +33 -0
  45. package/rules/internal/security/no-document-write.yaml +40 -0
  46. package/rules/internal/taint/MATRIX.md +16 -0
  47. package/rules/internal/taint/bash.test.yaml +84 -0
  48. package/rules/internal/taint/bash.yaml +166 -0
  49. package/rules/internal/taint/dvwa.test.yaml +217 -0
  50. package/rules/internal/taint/go.test.yaml +207 -0
  51. package/rules/internal/taint/go.yaml +325 -0
  52. package/rules/internal/taint/java.test.yaml +158 -0
  53. package/rules/internal/taint/java.yaml +318 -0
  54. package/rules/internal/taint/javascript.test.yaml +374 -0
  55. package/rules/internal/taint/javascript.yaml +439 -0
  56. package/rules/internal/taint/php.test.yaml +134 -0
  57. package/rules/internal/taint/php.yaml +331 -0
  58. package/rules/internal/taint/python.test.yaml +427 -0
  59. package/rules/internal/taint/python.yaml +636 -0
  60. package/rules/internal/taint/ruby.test.yaml +148 -0
  61. package/rules/internal/taint/ruby.yaml +233 -0
  62. package/rules/internal/taint/rust.test.yaml +585 -0
  63. package/rules/internal/taint/rust.yaml +483 -0
  64. package/rules/internal/taint/tsx.test.yaml +140 -0
  65. package/rules/internal/taint/tsx.yaml +51 -0
  66. package/rules/internal/taint/typescript.test.yaml +393 -0
  67. package/rules/internal/taint/typescript.yaml +660 -0
  68. package/wasm/tree-sitter-sql.wasm +0 -0
package/dist/index.js CHANGED
@@ -9,8 +9,10 @@ import { cac } from "cac";
9
9
  import { createRequire as createRequire2 } from "module";
10
10
 
11
11
  // src/cli-action.ts
12
- import { stat } from "fs/promises";
12
+ import { stat, access as access2 } from "fs/promises";
13
13
  import { resolve as resolve3 } from "path";
14
+ import { homedir as homedir3 } from "os";
15
+ import { join as join15 } from "path";
14
16
 
15
17
  // ../core/dist/index.js
16
18
  import { readFileSync } from "fs";
@@ -296,8 +298,8 @@ async function peekFileHead(filePath) {
296
298
  break;
297
299
  }
298
300
  }
299
- const text2 = buf.toString("utf-8", 0, bytesRead).toLowerCase();
300
- const isGenerated = GENERATED_MARKERS.some((marker) => text2.includes(marker));
301
+ const text22 = buf.toString("utf-8", 0, bytesRead).toLowerCase();
302
+ const isGenerated = GENERATED_MARKERS.some((marker) => text22.includes(marker));
301
303
  return { isBinary, isGenerated };
302
304
  } finally {
303
305
  await handle.close();
@@ -513,7 +515,9 @@ function sortFindings(findings) {
513
515
  const fileA = a.file ?? "";
514
516
  const fileB = b.file ?? "";
515
517
  if (fileA !== fileB) return fileA.localeCompare(fileB);
516
- return (a.line ?? 0) - (b.line ?? 0);
518
+ const lineDiff = (a.line ?? 0) - (b.line ?? 0);
519
+ if (lineDiff !== 0) return lineDiff;
520
+ return a.checkId.localeCompare(b.checkId);
517
521
  });
518
522
  }
519
523
  function dedupeDegradedFindings(findings) {
@@ -769,9 +773,13 @@ var copy = {
769
773
  "report.scannedFiles": (count) => `Scanned ${count} files.`,
770
774
  "report.summary": "Summary",
771
775
  "report.bySeverity": "By severity",
772
- "report.tierWatermark": (tier, licensedTo) => `Tier: ${tier} \u2014 Licensed to ${licensedTo}`,
773
- "report.preScanNotice": (count) => `${count} additional check(s) available on Pro. See ${SITE_URL} to upgrade.`,
774
- "report.postScanCta": (url) => `Learn more: ${url}`,
776
+ "report.tierWatermark": (licensedTo) => `\u2693 Pro \xB7 ${licensedTo}`,
777
+ "report.scanProgress.starting": (targetDir) => `\u2693 Scanning ${targetDir}...`,
778
+ "report.scanProgress.category": (label, count) => count > 0 ? ` \u2693 ${label} \u2014 ${count} issue(s)` : ` \u2693 ${label} \u2713`,
779
+ "report.cta.clean": `\u2713 All clear \u2014 your project is shipshape!`,
780
+ "report.cta.hasFindings": `Fix these before you ship. ${SITE_URL}/docs`,
781
+ "report.cta.critical": `\u26A0 Critical issues found \u2014 patch before you deploy. ${SITE_URL}/docs`,
782
+ "report.proUpsellNamed": (names, count) => `\u2693 Pro unlocks ${count} more check${count === 1 ? "" : "s"} (${names}). ${SITE_URL}`,
775
783
  "report.llmPrivacyWarning": "Pro checks will send code context to your LLM provider via your API key. No data is sent to Seaworthy servers.",
776
784
  "report.confidenceHigh": "high confidence",
777
785
  "report.confidenceMedium": "medium confidence",
@@ -1540,9 +1548,6 @@ var ScanRunner = class {
1540
1548
  const { files, meta } = await crawl({ targetDir: options.targetDir, maxFileSize: options.maxFileSize, debug: options.debug });
1541
1549
  const checks = registry.getForTier(options.tier);
1542
1550
  const skippedChecks = registry.getSkippedChecks(options.tier);
1543
- if (skippedChecks.length > 0) {
1544
- console.error(getCopy("report.preScanNotice", skippedChecks.length));
1545
- }
1546
1551
  if (options.llm) {
1547
1552
  console.error(getCopy("report.llmPrivacyWarning"));
1548
1553
  }
@@ -1556,20 +1561,36 @@ var ScanRunner = class {
1556
1561
  astCache: /* @__PURE__ */ new Map()
1557
1562
  };
1558
1563
  const results = [];
1559
- const mutex = [];
1564
+ const checkResults = /* @__PURE__ */ new Map();
1560
1565
  await runWithConcurrency(
1561
1566
  checks,
1562
1567
  async (check2) => {
1563
1568
  try {
1564
1569
  const findings2 = await check2.run(ctx);
1565
- mutex.push(...findings2);
1570
+ checkResults.set(check2.id, findings2);
1566
1571
  } catch (err) {
1567
1572
  console.error(`Check "${check2.id}" failed: ${String(err)}`);
1568
1573
  }
1569
1574
  },
1570
1575
  concurrency
1571
1576
  );
1572
- results.push(...mutex);
1577
+ for (const check2 of checks) {
1578
+ results.push(...checkResults.get(check2.id) ?? []);
1579
+ }
1580
+ if (options.onCategoryComplete) {
1581
+ const findingsPerCategory = /* @__PURE__ */ new Map();
1582
+ for (const check2 of checks) {
1583
+ const count = (checkResults.get(check2.id) ?? []).length;
1584
+ findingsPerCategory.set(check2.category, (findingsPerCategory.get(check2.category) ?? 0) + count);
1585
+ }
1586
+ const seen = /* @__PURE__ */ new Set();
1587
+ for (const check2 of checks) {
1588
+ if (!seen.has(check2.category)) {
1589
+ seen.add(check2.category);
1590
+ options.onCategoryComplete(check2.category, findingsPerCategory.get(check2.category) ?? 0);
1591
+ }
1592
+ }
1593
+ }
1573
1594
  if (ctx.astCache) {
1574
1595
  for (const promise of ctx.astCache.values()) {
1575
1596
  try {
@@ -1601,7 +1622,7 @@ var ScanRunner = class {
1601
1622
  return {
1602
1623
  findings,
1603
1624
  scannedFileCount: files.length,
1604
- skippedCheckCount: skippedChecks.length
1625
+ skippedChecks
1605
1626
  };
1606
1627
  }
1607
1628
  };
@@ -2351,15 +2372,31 @@ var text = {
2351
2372
  success: (s) => source_default.hex(tokens.success)(s)
2352
2373
  };
2353
2374
  var SEVERITY_ORDER2 = ["critical", "high", "medium", "low", "info"];
2375
+ function checkNameOf(checkId) {
2376
+ const short = checkId.replace(/^[^.]+\./, "");
2377
+ try {
2378
+ return getCopy(`checks.${short}.name`);
2379
+ } catch {
2380
+ return short;
2381
+ }
2382
+ }
2354
2383
  var TerminalReporter = class {
2355
2384
  generate(input) {
2356
- const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount } = input;
2385
+ const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount, skippedChecks = [], showIds = false } = input;
2357
2386
  const findings = dedupeDegradedFindings(rawFindings);
2358
2387
  if (findings.length === 0) {
2359
- return `${text.success(getCopy("report.noIssues"))}
2360
- ${text.meta(getCopy("report.scannedFiles", scannedFileCount))}
2361
-
2362
- ${text.link(getCopy("report.postScanCta", SITE_URL))}`;
2388
+ const lines2 = [];
2389
+ lines2.push(text.success(getCopy("report.cta.clean")));
2390
+ lines2.push(text.meta(getCopy("report.scannedFiles", scannedFileCount)));
2391
+ if (tier === "pro") {
2392
+ lines2.push("");
2393
+ lines2.push(text.meta(getCopy("report.tierWatermark", licensedTo)));
2394
+ }
2395
+ if (skippedChecks.length > 0) {
2396
+ lines2.push("");
2397
+ lines2.push(text.link(buildProUpsell(skippedChecks)));
2398
+ }
2399
+ return lines2.join("\n");
2363
2400
  }
2364
2401
  const lines = [];
2365
2402
  lines.push(text.heading(getCopy("report.header", targetDir, findings.length)));
@@ -2372,7 +2409,9 @@ ${text.link(getCopy("report.postScanCta", SITE_URL))}`;
2372
2409
  const rawAnalysisType = f.properties?.analysisType;
2373
2410
  const analysisTypeValue = rawAnalysisType === "pattern" || rawAnalysisType === "semantic" ? rawAnalysisType : void 0;
2374
2411
  const analysisType = analysisTypeValue ? text.meta(` [${getCopy(`reporting.analysisType.${analysisTypeValue}`)}]`) : "";
2375
- lines.push(`${severityLabel}${baselineLabel} ${f.checkId}${analysisType} \u2014 ${f.message}${loc}${confidenceLabel}`);
2412
+ const name = checkNameOf(f.checkId);
2413
+ const idSuffix = showIds ? text.meta(` \xB7 ${f.checkId}`) : "";
2414
+ lines.push(`${severityLabel}${baselineLabel} ${name}${idSuffix}${analysisType} \u2014 ${f.message}${loc}${confidenceLabel}`);
2376
2415
  if (f.remediation) {
2377
2416
  lines.push(` ${text.meta(`\u2192 ${f.remediation}`)}`);
2378
2417
  }
@@ -2390,11 +2429,23 @@ ${text.link(getCopy("report.postScanCta", SITE_URL))}`;
2390
2429
  }
2391
2430
  }
2392
2431
  lines.push("");
2393
- lines.push(text.meta(getCopy("report.tierWatermark", tier, licensedTo)));
2394
- lines.push(text.link(getCopy("report.postScanCta", SITE_URL)));
2432
+ const hasCriticalOrHigh = findings.some((f) => f.severity === "critical" || f.severity === "high");
2433
+ lines.push(text.link(hasCriticalOrHigh ? getCopy("report.cta.critical") : getCopy("report.cta.hasFindings")));
2434
+ if (tier === "pro") {
2435
+ lines.push(text.meta(getCopy("report.tierWatermark", licensedTo)));
2436
+ }
2437
+ if (skippedChecks.length > 0) {
2438
+ lines.push(text.link(buildProUpsell(skippedChecks)));
2439
+ }
2395
2440
  return lines.join("\n");
2396
2441
  }
2397
2442
  };
2443
+ function buildProUpsell(skippedChecks) {
2444
+ const count = skippedChecks.length;
2445
+ const names = skippedChecks.slice(0, 2).map((c) => c.name.toLowerCase()).join(", ");
2446
+ const suffix = count > 2 ? ", \u2026" : "";
2447
+ return getCopy("report.proUpsellNamed", names + suffix, count);
2448
+ }
2398
2449
  function textForSeverity(severity) {
2399
2450
  switch (severity) {
2400
2451
  case "critical":
@@ -5640,8 +5691,8 @@ function downgradeSeverity(severity) {
5640
5691
  if (idx < 0 || idx >= SEVERITY_ORDER3.length - 1) return severity;
5641
5692
  return SEVERITY_ORDER3[idx + 1];
5642
5693
  }
5643
- function estimateTokens(text2) {
5644
- return Math.ceil(text2.length / 4);
5694
+ function estimateTokens(text22) {
5695
+ return Math.ceil(text22.length / 4);
5645
5696
  }
5646
5697
  async function filterFilesByBudget(files, budget) {
5647
5698
  if (budget === void 0 || budget === Infinity) {
@@ -7087,8 +7138,8 @@ function unquoteString(value) {
7087
7138
  }
7088
7139
  return trimmed;
7089
7140
  }
7090
- function isDefaultCredential(password) {
7091
- const normalized = unquoteString(password).toLowerCase();
7141
+ function isDefaultCredential(password2) {
7142
+ const normalized = unquoteString(password2).toLowerCase();
7092
7143
  const defaults = /* @__PURE__ */ new Set([
7093
7144
  "admin",
7094
7145
  "password",
@@ -7190,8 +7241,8 @@ async function scanSqlDefaultCredentials(ctx) {
7190
7241
  let roleMatch = createRoleRegex.exec(content);
7191
7242
  while (roleMatch) {
7192
7243
  const roleName = roleMatch[1].replace(/["`[\]]/g, "");
7193
- const password = roleMatch[2];
7194
- if (!looksLikeHash(password) && isDefaultCredential(password)) {
7244
+ const password2 = roleMatch[2];
7245
+ if (!looksLikeHash(password2) && isDefaultCredential(password2)) {
7195
7246
  const beforeMatch = content.slice(0, roleMatch.index);
7196
7247
  const lineNum = beforeMatch.split("\n").length;
7197
7248
  findings.push({
@@ -7757,8 +7808,8 @@ registry.register(
7757
7808
  })
7758
7809
  );
7759
7810
  var CHECK_ID11 = "security.os-command-injection";
7760
- function isStaticShellLikeLiteral(text2) {
7761
- const t = text2.trim();
7811
+ function isStaticShellLikeLiteral(text22) {
7812
+ const t = text22.trim();
7762
7813
  if (t.length < 2) return false;
7763
7814
  if (t.startsWith('"') && t.endsWith('"') || t.startsWith("'") && t.endsWith("'")) return true;
7764
7815
  if (t.startsWith("`") && t.endsWith("`") && !/(?<!\\)\${/.test(t)) return true;
@@ -9176,10 +9227,10 @@ var CSS_COMMENT = /\/\*([\s\S]*?)\*\//g;
9176
9227
  var JSON_SCRIPT = /<script\b[^>]*type\s*=\s*["']application\/json["'][^>]*>([\s\S]*?)<\/script>/gi;
9177
9228
  function scanPattern(content, filePath, checkPrefix, pattern, messageText, findings) {
9178
9229
  for (const match of content.matchAll(pattern)) {
9179
- const text2 = match[1] ?? match[0] ?? "";
9180
- if (!SECRET_MARKER.test(text2)) continue;
9181
- const valueMatch = text2.match(SECRET_VALUE);
9182
- const value = valueMatch?.[1] ?? text2;
9230
+ const text22 = match[1] ?? match[0] ?? "";
9231
+ if (!SECRET_MARKER.test(text22)) continue;
9232
+ const valueMatch = text22.match(SECRET_VALUE);
9233
+ const value = valueMatch?.[1] ?? text22;
9183
9234
  const { line, column } = lineAndColumn5(content, match.index ?? 0);
9184
9235
  findings.push({
9185
9236
  id: `${CHECK_ID23}:${filePath}:${line}:${column}:${checkPrefix}`,
@@ -9830,11 +9881,11 @@ function pushFinding6(findings, filePath, content, index, matchText) {
9830
9881
  }
9831
9882
  function scanCss(findings, filePath, scanContent, originalContent, offset = 0) {
9832
9883
  for (const match of scanContent.matchAll(BRACE_RULE_PATTERN)) {
9833
- const text2 = match[0] ?? "";
9834
- if (!ATTR_SELECTOR_PATTERN.test(text2)) continue;
9835
- if (!EXTERNAL_HTTP_URL_PATTERN.test(text2)) continue;
9836
- const leadingWhitespace = text2.match(/^\s*/)?.[0] ?? "";
9837
- pushFinding6(findings, filePath, originalContent, (match.index ?? 0) + offset + leadingWhitespace.length, text2);
9884
+ const text22 = match[0] ?? "";
9885
+ if (!ATTR_SELECTOR_PATTERN.test(text22)) continue;
9886
+ if (!EXTERNAL_HTTP_URL_PATTERN.test(text22)) continue;
9887
+ const leadingWhitespace = text22.match(/^\s*/)?.[0] ?? "";
9888
+ pushFinding6(findings, filePath, originalContent, (match.index ?? 0) + offset + leadingWhitespace.length, text22);
9838
9889
  }
9839
9890
  }
9840
9891
  registry.register({
@@ -10185,7 +10236,7 @@ registry.register({
10185
10236
  });
10186
10237
  var CHECK_ID40 = "security.weak-email-validation";
10187
10238
  var REMEDIATION34 = getCopy("remediation.security.weak-email-validation");
10188
- var WEAK_EMAIL_REGEX_LINE = /\/\^[^\/]*@[^\/]*\\\.[^\/]{0,4}\$\/[gimsuy]*/g;
10239
+ var WEAK_EMAIL_REGEX_LINE = /\/\^[^/]*@[^/]*\\\.[^/]{0,4}\$\/[gimsuy]*/g;
10189
10240
  var LOOSE_REGEX_CLASS = /\[\^?\\?s@\]\+@\[\^?\\?s@\]\+\\\.\[\^?\\?s@\]\+/g;
10190
10241
  var EMAIL_CONTEXT = /email|e-?mail|validateEmail|isEmail|checkEmail|verifyEmail|deliverable/is;
10191
10242
  function lineAndColumn15(content, index) {
@@ -10241,7 +10292,7 @@ registry.register({
10241
10292
  var CHECK_ID41 = "security.idempotency-key-leak";
10242
10293
  var REMEDIATION35 = getCopy("remediation.security.idempotency-key-leak");
10243
10294
  var IDEMPOTENCY_CONTEXT = /idempotency[_-]?key|idempotency[_-]?token/i;
10244
- var TEMPLATE_LITERAL_INTERPOLATION = /\`[^`]*\$\{[^}]+\}[^`]*\`/g;
10295
+ var TEMPLATE_LITERAL_INTERPOLATION = new RegExp("`[^`]*\\$\\{[^}]+\\}[^`]*`", "g");
10245
10296
  var STRUCTURED_ID_PATTERN = /(?:lic(?:ense|ence)|sub(?:scription)?|order|cust(?:omer)?|user|tx|pay(?:ment)?|inv(?:oice)?)[-/]/i;
10246
10297
  function lineAndColumn16(content, index) {
10247
10298
  const prefix = content.slice(0, index);
@@ -10491,11 +10542,11 @@ function applyTaintToBashReadTarget(command, scopeRoot, s, cleanseFromId) {
10491
10542
  for (let i = 0; i < command.namedChildCount; i++) {
10492
10543
  const child = command.namedChild(i);
10493
10544
  if (child.type !== "word") continue;
10494
- const text2 = child.text;
10495
- if (text2 === "read" || text2.startsWith("-")) continue;
10545
+ const text22 = child.text;
10546
+ if (text22 === "read" || text22.startsWith("-")) continue;
10496
10547
  markTaint(s, child.id);
10497
10548
  if (cleanseFromId !== void 0) mergeCleanse(s, cleanseFromId, child.id);
10498
- taintVarUses(text2, scopeRoot, s);
10549
+ taintVarUses(text22, scopeRoot, s);
10499
10550
  }
10500
10551
  }
10501
10552
  function collectEntries(table) {
@@ -15092,7 +15143,23 @@ var copy3 = {
15092
15143
  "cli.help.usage": "Usage: seaworthy [path] [options]",
15093
15144
  "cli.help.options": "Options",
15094
15145
  "cli.help.learnMore": `Documentation: ${SITE_URL}/docs`,
15095
- "cli.scanning": "Scanning...",
15146
+ "cli.setup.intro": "Welcome to Seaworthy! Let's get you set up.",
15147
+ "cli.setup.licensePrompt": "License key (press Enter to skip and scan free)",
15148
+ "cli.setup.licenseHint": "Buy a key at seaworthycode.com \xB7 Enter skips to free tier",
15149
+ "cli.setup.providerPrompt": "LLM provider for Pro checks",
15150
+ "cli.setup.providerSkip": "Skip \u2014 no LLM checks",
15151
+ "cli.setup.apiKeyPrompt": "API key for",
15152
+ "cli.setup.modelPrompt": "Model (press Enter for default)",
15153
+ "cli.setup.baseUrlPrompt": "Custom API base URL (press Enter to use default)",
15154
+ "cli.setup.done": "Config saved. Run `seaworthy .` to scan your project.",
15155
+ "cli.setup.skipped": "Setup skipped. Running free-tier scan.",
15156
+ "cli.firstRun": "First time? Let's get you set up.",
15157
+ "cli.category.security": "Security",
15158
+ "cli.category.resilience": "Resilience",
15159
+ "cli.category.ops": "Ops Basics",
15160
+ "cli.category.dependencies": "Dependencies",
15161
+ "cli.category.configuration": "Configuration",
15162
+ "cli.category.data-exposure": "Data Exposure",
15096
15163
  "cli.reportWritten": (path) => `Report written to ${path}`,
15097
15164
  "cli.error.notADirectory": (path) => `Path is not a directory: ${path}`,
15098
15165
  "cli.error.notFound": (path) => `Path not found: ${path}`,
@@ -15104,6 +15171,7 @@ var copy3 = {
15104
15171
  "cli.sarif.stdout_conflict": "Flags --sarif-stdout and --json cannot be used together. Use --output to send JSON to a file.",
15105
15172
  "cli.error.llmRequired": "LLM API key required for Pro checks. Set it with seaworthy config or the SEAWORTHY_LLM_API_KEY environment variable.",
15106
15173
  "cli.warn.llmSkipped": "Pro checks skipped: no LLM API key configured. Set it with seaworthy config or the SEAWORTHY_LLM_API_KEY environment variable to enable semantic analysis.",
15174
+ "cli.warn.licenseInvalid": "Your license key is invalid or has been revoked. Running in free tier.\nTo re-enter your key, run: npx seaworthycode setup",
15107
15175
  "cli.error.config.missingKey": "Missing config key. Usage: seaworthy config <set|get> <key>",
15108
15176
  "cli.error.config.missingValue": "Missing config value. Usage: seaworthy config set <key> <value>",
15109
15177
  "cli.error.config.unknownKey": "Unknown config key. Valid keys: licenseKey, llmProvider, llmApiKey, llmModel, llmApiUrl",
@@ -15154,6 +15222,14 @@ function resolveScanTimeoutMs(tier) {
15154
15222
  }
15155
15223
  return tier === "free" ? FREE_SCAN_TIMEOUT_MS : PRO_SCAN_TIMEOUT_MS;
15156
15224
  }
15225
+ var CATEGORY_LABELS = {
15226
+ security: getCopy2("cli.category.security"),
15227
+ resilience: getCopy2("cli.category.resilience"),
15228
+ ops: getCopy2("cli.category.ops"),
15229
+ dependencies: getCopy2("cli.category.dependencies"),
15230
+ configuration: getCopy2("cli.category.configuration"),
15231
+ "data-exposure": getCopy2("cli.category.data-exposure")
15232
+ };
15157
15233
  function withTimeout(promise, ms, message) {
15158
15234
  return new Promise((resolve5, reject) => {
15159
15235
  const timer = setTimeout(() => reject(new Error(message)), ms);
@@ -15186,6 +15262,10 @@ async function executeScan(args, ctx) {
15186
15262
  if (args.llmApiKey && args.llmProvider && !process.env.SEAWORTHY_SKIP_LLM_CHECK) {
15187
15263
  llm = createProvider({ provider: args.llmProvider, apiKey: args.llmApiKey, model: args.llmModel, baseUrl: args.llmApiUrl });
15188
15264
  }
15265
+ const showProgress = args.format === "terminal" && !args.sarifStdout;
15266
+ if (showProgress) {
15267
+ process.stderr.write(getCopy("report.scanProgress.starting", args.targetDir) + "\n");
15268
+ }
15189
15269
  const runner = new ScanRunner();
15190
15270
  const scanTimeoutMs = resolveScanTimeoutMs(ctx.tier);
15191
15271
  const result = await withTimeout(
@@ -15196,7 +15276,11 @@ async function executeScan(args, ctx) {
15196
15276
  maxFileSize: args.maxFileSize,
15197
15277
  debug: args.debug,
15198
15278
  baseline: args.baseline,
15199
- minConfidence: args.minConfidence
15279
+ minConfidence: args.minConfidence,
15280
+ onCategoryComplete: showProgress ? (category, count) => {
15281
+ const label = CATEGORY_LABELS[category] ?? category;
15282
+ process.stderr.write(getCopy("report.scanProgress.category", label, count) + "\n");
15283
+ } : void 0
15200
15284
  }),
15201
15285
  scanTimeoutMs,
15202
15286
  getCopy("errors.scan.timed_out", Math.round(scanTimeoutMs / 1e3))
@@ -15206,7 +15290,9 @@ async function executeScan(args, ctx) {
15206
15290
  targetDir: args.targetDir,
15207
15291
  tier: ctx.tier,
15208
15292
  licensedTo: ctx.licensedTo ?? "free tier",
15209
- scannedFileCount: result.scannedFileCount
15293
+ scannedFileCount: result.scannedFileCount,
15294
+ skippedChecks: result.skippedChecks,
15295
+ showIds: args.showIds ?? args.debug
15210
15296
  };
15211
15297
  let primaryOutput;
15212
15298
  switch (args.format) {
@@ -15257,6 +15343,9 @@ var scanCommand = createCommand({
15257
15343
  handler: executeScan
15258
15344
  });
15259
15345
 
15346
+ // src/commands/setup.ts
15347
+ import { intro, outro, text as text2, password, select, isCancel } from "@clack/prompts";
15348
+
15260
15349
  // src/config/store.ts
15261
15350
  import { promises as fs9 } from "fs";
15262
15351
  import { homedir as homedir2 } from "os";
@@ -15283,6 +15372,91 @@ async function writeConfig(config) {
15283
15372
  await fs9.chmod(getConfigFile(), 384);
15284
15373
  }
15285
15374
 
15375
+ // src/commands/setup.ts
15376
+ var PROVIDERS_WITH_CUSTOM_URL = /* @__PURE__ */ new Set(["ollama", "openrouter"]);
15377
+ async function executeSetup() {
15378
+ intro(getCopy2("cli.setup.intro"));
15379
+ const config = await readConfig();
15380
+ const licenseKeyResult = await text2({
15381
+ message: getCopy2("cli.setup.licensePrompt"),
15382
+ placeholder: "SW-XXXXXX",
15383
+ defaultValue: config.licenseKey ?? "",
15384
+ validate: () => void 0
15385
+ });
15386
+ if (isCancel(licenseKeyResult)) {
15387
+ outro(getCopy2("cli.setup.skipped"));
15388
+ return { ok: true };
15389
+ }
15390
+ const licenseKey = String(licenseKeyResult).trim() || void 0;
15391
+ const providers = listPresetProviders();
15392
+ const providerOptions = [
15393
+ { value: "", label: getCopy2("cli.setup.providerSkip") },
15394
+ ...providers.map((p) => ({ value: p, label: p }))
15395
+ ];
15396
+ const providerResult = await select({
15397
+ message: getCopy2("cli.setup.providerPrompt"),
15398
+ options: providerOptions,
15399
+ initialValue: config.llmProvider ?? ""
15400
+ });
15401
+ if (isCancel(providerResult)) {
15402
+ outro(getCopy2("cli.setup.skipped"));
15403
+ return { ok: true };
15404
+ }
15405
+ const llmProvider = String(providerResult) || void 0;
15406
+ let llmApiKey = config.llmApiKey;
15407
+ let llmModel = config.llmModel;
15408
+ let llmApiUrl = config.llmApiUrl;
15409
+ if (llmProvider) {
15410
+ const apiKeyResult = await password({
15411
+ message: `${getCopy2("cli.setup.apiKeyPrompt")} ${llmProvider}`,
15412
+ validate: () => void 0
15413
+ });
15414
+ if (isCancel(apiKeyResult)) {
15415
+ outro(getCopy2("cli.setup.skipped"));
15416
+ return { ok: true };
15417
+ }
15418
+ llmApiKey = String(apiKeyResult).trim() || void 0;
15419
+ const preset = resolvePreset(llmProvider);
15420
+ const modelResult = await text2({
15421
+ message: getCopy2("cli.setup.modelPrompt"),
15422
+ placeholder: preset?.defaultModel ?? "",
15423
+ defaultValue: config.llmModel ?? "",
15424
+ validate: () => void 0
15425
+ });
15426
+ if (isCancel(modelResult)) {
15427
+ outro(getCopy2("cli.setup.skipped"));
15428
+ return { ok: true };
15429
+ }
15430
+ llmModel = String(modelResult).trim() || void 0;
15431
+ if (PROVIDERS_WITH_CUSTOM_URL.has(llmProvider)) {
15432
+ const urlResult = await text2({
15433
+ message: getCopy2("cli.setup.baseUrlPrompt"),
15434
+ placeholder: preset?.baseUrl ?? "",
15435
+ defaultValue: config.llmApiUrl ?? "",
15436
+ validate: () => void 0
15437
+ });
15438
+ if (isCancel(urlResult)) {
15439
+ outro(getCopy2("cli.setup.skipped"));
15440
+ return { ok: true };
15441
+ }
15442
+ llmApiUrl = String(urlResult).trim() || void 0;
15443
+ } else {
15444
+ llmApiUrl = void 0;
15445
+ }
15446
+ }
15447
+ await writeConfig({ licenseKey, llmProvider, llmApiKey, llmModel, llmApiUrl });
15448
+ outro(getCopy2("cli.setup.done"));
15449
+ return { ok: true };
15450
+ }
15451
+ var setupCommand = createCommand(
15452
+ {
15453
+ name: "setup",
15454
+ description: "Interactive first-run setup for license key and LLM provider",
15455
+ handler: async () => executeSetup()
15456
+ },
15457
+ { requireLicense: false }
15458
+ );
15459
+
15286
15460
  // src/auth/fs-credential-provider.ts
15287
15461
  var FsCredentialProvider = class {
15288
15462
  async getCredentials() {
@@ -15344,6 +15518,20 @@ async function checkUpdate() {
15344
15518
  }
15345
15519
 
15346
15520
  // src/cli-action.ts
15521
+ function getConfigFile2() {
15522
+ return join15(process.env.SEAWORTHY_CONFIG_DIR ?? join15(homedir3(), ".seaworthy"), "config");
15523
+ }
15524
+ async function isFirstRun(options) {
15525
+ if (options.json || options.output || options.sarif || options.sarifStdout || process.env.CI || process.env.SEAWORTHY_LICENSE_KEY || process.env.ANTHROPIC_API_KEY || process.env.OPENAI_API_KEY || !process.stdin.isTTY || !process.stdout.isTTY) {
15526
+ return false;
15527
+ }
15528
+ try {
15529
+ await access2(getConfigFile2());
15530
+ return false;
15531
+ } catch {
15532
+ return true;
15533
+ }
15534
+ }
15347
15535
  function resolveExitCode(code) {
15348
15536
  const normalized = code.replace(/^errors\./, "");
15349
15537
  if (["license.invalid", "license.expired"].includes(normalized)) return 1;
@@ -15401,14 +15589,23 @@ async function runCli(options) {
15401
15589
  error: getCopy2("cli.error.notFound", dir)
15402
15590
  };
15403
15591
  }
15592
+ if (await isFirstRun(options)) {
15593
+ console.error(getCopy2("cli.firstRun"));
15594
+ await setupCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
15595
+ }
15404
15596
  const provider = new FsCredentialProvider();
15405
15597
  const license = new LicenseClient({ baseUrl: LICENSE_SERVER_URL });
15406
15598
  const authResult = await resolveAuth(provider, license);
15407
15599
  if (!authResult.ok) {
15408
- const message = resolveErrorMessage(authResult.code);
15409
- return { code: resolveExitCode(authResult.code), error: message };
15600
+ const normalized = authResult.code.replace(/^errors\./, "");
15601
+ const softCodes = ["license.invalid", "license.expired"];
15602
+ if (!softCodes.includes(normalized)) {
15603
+ const message = resolveErrorMessage(authResult.code);
15604
+ return { code: resolveExitCode(authResult.code), error: message };
15605
+ }
15606
+ console.error(getCopy2("cli.warn.licenseInvalid"));
15410
15607
  }
15411
- const auth = authResult.value;
15608
+ const auth = authResult.ok ? authResult.value : { tier: "free", token: "", licensedTo: "free tier" };
15412
15609
  const allowedTiers = {
15413
15610
  free: ["free"],
15414
15611
  pro: ["free", "pro"]
@@ -15437,7 +15634,8 @@ async function runCli(options) {
15437
15634
  maxFileSize: options.maxFileSize,
15438
15635
  debug: options.debug,
15439
15636
  baseline: options.baseline,
15440
- minConfidence: options.minConfidence
15637
+ minConfidence: options.minConfidence,
15638
+ showIds: options.showIds
15441
15639
  };
15442
15640
  const updateStart = Date.now();
15443
15641
  const updatePromise = checkUpdate().catch(() => void 0);
@@ -15524,12 +15722,12 @@ var require5 = createRequire2(import.meta.url);
15524
15722
  var { version } = require5("../package.json");
15525
15723
  var cli = cac("seaworthy");
15526
15724
  cli.version(version);
15527
- 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, deepseek, gemini, minimax, ollama, openrouter)").option("--max-file-size <bytes>", "Skip files larger than this (default: 1 MB)").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)").action(async (targetDir, options) => {
15725
+ 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)").action(async (targetDir, options) => {
15528
15726
  const maxFileSize = options.maxFileSize ? Number(options.maxFileSize) : void 0;
15529
15727
  const tier = options.tier;
15530
15728
  const baseline = options.baseline === "save" ? "save" : options.baseline;
15531
15729
  const minConfidence = options.minConfidence;
15532
- 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, debug: options.debug, baseline, minConfidence });
15730
+ 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 });
15533
15731
  if (result.updateMessage) {
15534
15732
  console.error(result.updateMessage);
15535
15733
  }
@@ -15553,15 +15751,26 @@ cli.command("config <action> [key] [value]", "Manage configuration").action(asyn
15553
15751
  console.log(result.output);
15554
15752
  }
15555
15753
  });
15754
+ cli.command("setup", "Interactive setup for license key and LLM provider").action(async () => {
15755
+ const result = await setupCommand.handler({ action: "run" }, { tier: "free", licensedTo: "free tier" });
15756
+ if (!result.ok) {
15757
+ console.error(resolveErrorMessage(result.code));
15758
+ process.exit(2);
15759
+ }
15760
+ });
15556
15761
  cli.help(() => {
15557
15762
  return [
15558
15763
  {
15559
15764
  title: "Examples",
15560
- body: ` $ npx seaworthy ./my-project
15561
- $ npx seaworthy ./my-project --json
15562
- $ npx seaworthy ./my-project --output report.html
15563
- $ npx seaworthy ./my-project --sarif report.sarif
15564
- $ npx seaworthy --provider kimi ./my-project`
15765
+ body: ` $ npx seaworthycode ./my-project
15766
+ $ npx seaworthycode ./my-project --json
15767
+ $ npx seaworthycode ./my-project --output report.html
15768
+ $ npx seaworthycode ./my-project --sarif report.sarif
15769
+ $ npx seaworthycode --provider kimi ./my-project`
15770
+ },
15771
+ {
15772
+ title: "Commands",
15773
+ body: ` npx seaworthycode setup Interactive setup for license key and LLM provider`
15565
15774
  },
15566
15775
  {
15567
15776
  title: "Options",
@@ -15571,10 +15780,11 @@ cli.help(() => {
15571
15780
  --sarif-stdout ${getCopy2("cli.help.flag.sarifStdout")}
15572
15781
  --fail-on-findings Exit with code 5 when findings are found
15573
15782
  --tier <tier> Override tier (free, pro)
15574
- --provider <name> LLM provider (anthropic, openai, kimi, deepseek, gemini, minimax, ollama, openrouter)
15783
+ --provider <name> LLM provider (anthropic, openai, kimi, moonshot, deepseek, gemini, minimax, ollama, openrouter)
15575
15784
  --max-file-size <n> Skip files larger than n bytes (default: 1 MB)
15576
15785
  --baseline [ref] Compare against baseline file or save (--baseline save)
15577
15786
  --min-confidence <level> Hide findings below confidence (high, medium, low)
15787
+ --show-ids Show check IDs alongside names
15578
15788
  --debug Enable debug logging`
15579
15789
  },
15580
15790
  {