seaworthycode 1.2.12 → 1.2.16

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
@@ -411,7 +411,8 @@ async function crawl(options) {
411
411
  path: fullPath,
412
412
  relativePath: relPath,
413
413
  content: () => fs.readFile(fullPath, "utf-8"),
414
- language: inferLanguage(entry.name)
414
+ language: inferLanguage(entry.name),
415
+ size: stat2.size
415
416
  });
416
417
  } catch {
417
418
  }
@@ -431,7 +432,8 @@ async function crawl(options) {
431
432
  path: fullPath,
432
433
  relativePath: relPath,
433
434
  content: () => fs.readFile(fullPath, "utf-8"),
434
- language: inferLanguage(entry.name)
435
+ language: inferLanguage(entry.name),
436
+ size: stat2.size
435
437
  });
436
438
  } catch {
437
439
  }
@@ -789,6 +791,7 @@ var copy = {
789
791
  "report.cta.hasFindings": `Fix these before you ship. ${SITE_URL}/docs`,
790
792
  "report.cta.critical": `\u26A0 Critical issues found \u2014 patch before you deploy. ${SITE_URL}/docs`,
791
793
  "report.proUpsellNamed": (names, count) => `\u2693 Pro unlocks ${count} more check${count === 1 ? "" : "s"} (${names}). ${SITE_URL}`,
794
+ "report.timedOutChecks": (names, count) => `\u26A0 ${count} check${count === 1 ? "" : "s"} timed out and were skipped (${names}). Use SEAWORTHY_CHECK_TIMEOUT_MS to extend the limit.`,
792
795
  "report.preScanNotice": (count) => `\u2693 Pro unlocks ${count} additional checks. Upgrade at ${SITE_URL}`,
793
796
  "report.llmPrivacyWarning": "Pro checks will send code context to your LLM provider via your API key. No data is sent to Seaworthy servers.",
794
797
  "report.confidenceHigh": "high confidence",
@@ -1601,6 +1604,16 @@ async function runWithConcurrencyCollect(items, fn, concurrency) {
1601
1604
  await Promise.all(workers);
1602
1605
  return results;
1603
1606
  }
1607
+ var DEFAULT_CHECK_TIMEOUT_MS = 5 * 60 * 1e3;
1608
+ var SLOW_CHECK_THRESHOLD_RATIO = 0.5;
1609
+ function getCheckTimeoutMs() {
1610
+ const env2 = process.env.SEAWORTHY_CHECK_TIMEOUT_MS;
1611
+ if (env2 !== void 0) {
1612
+ const n = parseInt(env2, 10);
1613
+ if (!isNaN(n) && n > 0) return n;
1614
+ }
1615
+ return DEFAULT_CHECK_TIMEOUT_MS;
1616
+ }
1604
1617
  var ScanRunner = class {
1605
1618
  async run(options) {
1606
1619
  const { concurrency = 4 } = options;
@@ -1617,7 +1630,9 @@ var ScanRunner = class {
1617
1630
  files,
1618
1631
  meta,
1619
1632
  taintCache: /* @__PURE__ */ new Map(),
1620
- astCache: /* @__PURE__ */ new Map(),
1633
+ // astCache is intentionally omitted here — each check gets its own
1634
+ // per-check cache below so content strings are GC'd when the check ends.
1635
+ astCache: void 0,
1621
1636
  bashAstCache: /* @__PURE__ */ new Map()
1622
1637
  };
1623
1638
  const results = [];
@@ -1628,15 +1643,55 @@ var ScanRunner = class {
1628
1643
  }
1629
1644
  const categoryCompleted = /* @__PURE__ */ new Map();
1630
1645
  let completedCount = 0;
1646
+ const timedOutChecks = [];
1647
+ const checkTimeoutMs = getCheckTimeoutMs();
1631
1648
  await runWithConcurrency(
1632
1649
  checks,
1633
1650
  async (check2) => {
1651
+ if (options.debug) {
1652
+ process.stderr.write(`[seaworthy] check.start ${check2.id}
1653
+ `);
1654
+ }
1655
+ const startMs = Date.now();
1656
+ let timedOut = false;
1657
+ let softTimer;
1658
+ let hardTimer;
1659
+ const checkCtx = { ...ctx, astCache: /* @__PURE__ */ new Map() };
1634
1660
  try {
1635
- const findings2 = await check2.run(ctx);
1636
- checkResults.set(check2.id, findings2);
1661
+ const findings2 = await new Promise((resolve22, reject) => {
1662
+ softTimer = setTimeout(() => {
1663
+ process.stderr.write(
1664
+ `[seaworthy] check.slow ${check2.id} \u2014 still running after ${Math.round(checkTimeoutMs * SLOW_CHECK_THRESHOLD_RATIO / 1e3)}s
1665
+ `
1666
+ );
1667
+ }, checkTimeoutMs * SLOW_CHECK_THRESHOLD_RATIO);
1668
+ hardTimer = setTimeout(() => {
1669
+ timedOut = true;
1670
+ process.stderr.write(
1671
+ `[seaworthy] check.timeout ${check2.id} \u2014 aborting after ${Math.round(checkTimeoutMs / 1e3)}s
1672
+ `
1673
+ );
1674
+ resolve22([]);
1675
+ }, checkTimeoutMs);
1676
+ check2.run(checkCtx).then(resolve22, reject);
1677
+ });
1678
+ clearTimeout(softTimer);
1679
+ clearTimeout(hardTimer);
1680
+ if (timedOut) {
1681
+ timedOutChecks.push({ id: check2.id, name: check2.name, category: check2.category });
1682
+ checkResults.set(check2.id, []);
1683
+ } else {
1684
+ checkResults.set(check2.id, findings2);
1685
+ }
1637
1686
  } catch (err) {
1687
+ clearTimeout(softTimer);
1688
+ clearTimeout(hardTimer);
1638
1689
  console.error(`Check "${check2.id}" failed: ${String(err)}`);
1639
1690
  }
1691
+ if (options.debug) {
1692
+ process.stderr.write(`[seaworthy] check.done ${check2.id} ${Date.now() - startMs}ms
1693
+ `);
1694
+ }
1640
1695
  options.onCheckComplete?.(++completedCount, checks.length, check2.name);
1641
1696
  if (options.onCategoryComplete) {
1642
1697
  const catDone = (categoryCompleted.get(check2.category) ?? 0) + 1;
@@ -1685,7 +1740,8 @@ var ScanRunner = class {
1685
1740
  return {
1686
1741
  findings,
1687
1742
  scannedFileCount: files.length,
1688
- skippedChecks
1743
+ skippedChecks,
1744
+ timedOutChecks
1689
1745
  };
1690
1746
  }
1691
1747
  };
@@ -2486,7 +2542,7 @@ function renderCollapsedGroup(group, showIds) {
2486
2542
  }
2487
2543
  var TerminalReporter = class {
2488
2544
  generate(input) {
2489
- const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount, skippedChecks = [], showIds = false, all = false } = input;
2545
+ const { findings: rawFindings, targetDir, tier, licensedTo, scannedFileCount, skippedChecks = [], timedOutChecks = [], showIds = false, all = false } = input;
2490
2546
  const findings = dedupeDegradedFindings(rawFindings);
2491
2547
  if (findings.length === 0) {
2492
2548
  const lines2 = [];
@@ -2500,6 +2556,10 @@ var TerminalReporter = class {
2500
2556
  lines2.push("");
2501
2557
  lines2.push(text.link(buildProUpsell(skippedChecks)));
2502
2558
  }
2559
+ if (timedOutChecks.length > 0) {
2560
+ lines2.push("");
2561
+ lines2.push(text.warning(buildTimedOutNote(timedOutChecks)));
2562
+ }
2503
2563
  return lines2.join("\n");
2504
2564
  }
2505
2565
  const lines = [];
@@ -2549,9 +2609,18 @@ var TerminalReporter = class {
2549
2609
  if (skippedChecks.length > 0) {
2550
2610
  lines.push(text.link(buildProUpsell(skippedChecks)));
2551
2611
  }
2612
+ if (timedOutChecks.length > 0) {
2613
+ lines.push(text.warning(buildTimedOutNote(timedOutChecks)));
2614
+ }
2552
2615
  return lines.join("\n");
2553
2616
  }
2554
2617
  };
2618
+ function buildTimedOutNote(timedOutChecks) {
2619
+ const count = timedOutChecks.length;
2620
+ const names = timedOutChecks.slice(0, 2).map((c) => c.name.toLowerCase()).join(", ");
2621
+ const suffix = count > 2 ? ", \u2026" : "";
2622
+ return getCopy("report.timedOutChecks", names + suffix, count);
2623
+ }
2555
2624
  function buildProUpsell(skippedChecks) {
2556
2625
  const count = skippedChecks.length;
2557
2626
  const names = skippedChecks.slice(0, 2).map((c) => c.name.toLowerCase()).join(", ");
@@ -5908,6 +5977,14 @@ function classifyError(err) {
5908
5977
  return "llm.unexpected";
5909
5978
  }
5910
5979
  var LLM_TIMEOUT_MS = 9e4;
5980
+ function getMaxFilesPerLlmCheck() {
5981
+ const env2 = process.env.SEAWORTHY_LLM_MAX_FILES;
5982
+ if (env2 !== void 0) {
5983
+ const n = parseInt(env2, 10);
5984
+ if (!isNaN(n) && n > 0) return n;
5985
+ }
5986
+ return 150;
5987
+ }
5911
5988
  function withLLMTimeout(promise, ms) {
5912
5989
  return new Promise((resolve22, reject) => {
5913
5990
  const timerId = setTimeout(() => {
@@ -5951,21 +6028,14 @@ function downgradeSeverity(severity) {
5951
6028
  if (idx < 0 || idx >= SEVERITY_ORDER3.length - 1) return severity;
5952
6029
  return SEVERITY_ORDER3[idx + 1];
5953
6030
  }
5954
- function estimateTokens(text22) {
5955
- return Math.ceil(text22.length / 4);
5956
- }
5957
- async function filterFilesByBudget(files, budget) {
6031
+ function filterFilesByBudget(files, budget) {
5958
6032
  if (budget === void 0 || budget === Infinity) {
5959
6033
  return files;
5960
6034
  }
5961
- const result = [];
5962
- for (const file of files) {
5963
- const content = await file.content();
5964
- if (estimateTokens(content) <= budget) {
5965
- result.push(file);
5966
- }
5967
- }
5968
- return result;
6035
+ return files.filter((file) => {
6036
+ const tokens2 = file.size !== void 0 ? Math.ceil(file.size / 4) : budget;
6037
+ return tokens2 <= budget;
6038
+ });
5969
6039
  }
5970
6040
  async function buildCacheKey(ctx, template, files) {
5971
6041
  if (!ctx.llm) return void 0;
@@ -6142,14 +6212,21 @@ async function runLLMCheck(ctx, template) {
6142
6212
  return [];
6143
6213
  }
6144
6214
  const proEligibleFiles = ctx.files.filter((f) => isProCheckEligible(f, template));
6145
- const cacheKey = await buildCacheKey(ctx, template, proEligibleFiles);
6215
+ let eligibleFiles = filterFilesByBudget(proEligibleFiles, template.tokenBudget);
6216
+ const maxFiles = getMaxFilesPerLlmCheck();
6217
+ if (eligibleFiles.length > maxFiles) {
6218
+ console.error(
6219
+ `[seaworthy] ${template.checkId}: ${eligibleFiles.length} eligible files \u2014 capping LLM prompt to ${maxFiles}`
6220
+ );
6221
+ eligibleFiles = eligibleFiles.slice(0, maxFiles);
6222
+ }
6223
+ const cacheKey = await buildCacheKey(ctx, template, eligibleFiles);
6146
6224
  if (cacheKey) {
6147
6225
  const cached = readCache(cacheKey);
6148
6226
  if (cached !== null) {
6149
6227
  return cached;
6150
6228
  }
6151
6229
  }
6152
- const eligibleFiles = await filterFilesByBudget(proEligibleFiles, template.tokenBudget);
6153
6230
  if (eligibleFiles.length === 0) {
6154
6231
  let mechFindings = await runMechanicalCheck(ctx, template);
6155
6232
  if (template.projectScoped) {
@@ -6318,7 +6395,7 @@ registry.register({
6318
6395
  return runLLMCheck(ctx, promptTemplate2);
6319
6396
  }
6320
6397
  });
6321
- var PROMPT_VERSION3 = "1.0.0";
6398
+ var PROMPT_VERSION3 = "1.1.0";
6322
6399
  var fallbackPatterns3 = [
6323
6400
  {
6324
6401
  regex: /['"`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\s+.*\$\{/i,
@@ -6332,7 +6409,22 @@ var fallbackPatterns3 = [
6332
6409
  }
6333
6410
  ];
6334
6411
  async function buildPrompt3(sourceFiles) {
6335
- const parts = ["Check for SQL injection vulnerabilities. Look for raw string concatenation or template literals used to build SQL queries. Flag any use of string interpolation in .query(), .raw(), .execute(), or raw SQL strings."];
6412
+ const parts = [
6413
+ `Check for SQL injection: flag ONLY where user-controlled input reaches a SQL query WITHOUT parameterization.
6414
+
6415
+ SAFE \u2014 do NOT flag:
6416
+ - Queries using ? or $N placeholders with a separate args array: db.Query("SELECT * FROM t WHERE id = ?", id)
6417
+ - GORM .Raw()/.Exec() with ? bindings, even when the SQL string contains || (PostgreSQL concat operator inside the string): db.Exec("ST_GeomFromText('POINT(' || ? || ')', 4326)", lat, lng)
6418
+ - String concatenation where ALL operands are string literals (compile-time constants, not variables)
6419
+ - Variables sourced from application config or startup env, not from HTTP requests or user payloads
6420
+
6421
+ UNSAFE \u2014 DO flag:
6422
+ - Template literals / f-strings injecting request values: db.query(\`SELECT * FROM t WHERE id = \${req.params.id}\`)
6423
+ - String concatenation where a variable traces to req.body, req.query, FormValue(), r.URL.Query(), $_GET, $_POST, or similar external input
6424
+ - fmt.Sprintf building a SQL string with user-supplied arguments (not just format specifiers over internal values)
6425
+
6426
+ Only flag if you can trace the injected value to an external input source (HTTP request, message payload, file upload).`
6427
+ ];
6336
6428
  for (const file of sourceFiles) {
6337
6429
  const content = await file.content();
6338
6430
  parts.push(`--- ${file.relativePath} ---
@@ -6374,7 +6466,8 @@ var tsCheck4 = createTreeSitterCheck({
6374
6466
  (#match? @method "^(query|raw|execute)$"))
6375
6467
  arguments: (arguments
6376
6468
  (binary_expression
6377
- operator: "+"))) @match
6469
+ operator: "+"
6470
+ right: (_) @rhs))) @match
6378
6471
 
6379
6472
  (call_expression
6380
6473
  function: (member_expression
@@ -6404,7 +6497,8 @@ var tsCheck4 = createTreeSitterCheck({
6404
6497
  (#match? @method "^(query|raw|execute)$"))
6405
6498
  arguments: (arguments
6406
6499
  (binary_expression
6407
- operator: "+"))) @match
6500
+ operator: "+"
6501
+ right: (_) @rhs))) @match
6408
6502
 
6409
6503
  (call_expression
6410
6504
  function: (member_expression
@@ -6434,7 +6528,8 @@ var tsCheck4 = createTreeSitterCheck({
6434
6528
  (#match? @method "^(query|raw|execute)$"))
6435
6529
  arguments: (arguments
6436
6530
  (binary_expression
6437
- operator: "+"))) @match
6531
+ operator: "+"
6532
+ right: (_) @rhs))) @match
6438
6533
 
6439
6534
  (call_expression
6440
6535
  function: (member_expression
@@ -6464,7 +6559,8 @@ var tsCheck4 = createTreeSitterCheck({
6464
6559
  (#match? @method "^(query|execute|raw)$"))
6465
6560
  arguments: (argument_list
6466
6561
  (binary_operator
6467
- operator: "+"))) @match
6562
+ operator: "+"
6563
+ right: (_) @rhs))) @match
6468
6564
 
6469
6565
  (call
6470
6566
  function: (attribute
@@ -6493,7 +6589,8 @@ var tsCheck4 = createTreeSitterCheck({
6493
6589
  (#match? @method "^(Query|Exec|QueryRow)$"))
6494
6590
  arguments: (argument_list
6495
6591
  (binary_expression
6496
- operator: "+"))) @match
6592
+ operator: "+"
6593
+ right: (_) @rhs))) @match
6497
6594
 
6498
6595
  (call_expression
6499
6596
  function: (selector_expression
@@ -6542,14 +6639,16 @@ var tsCheck4 = createTreeSitterCheck({
6542
6639
  (#match? @method "^execute.*$")
6543
6640
  arguments: (argument_list
6544
6641
  (binary_expression
6545
- operator: "+"))) @match
6642
+ operator: "+"
6643
+ right: (_) @rhs))) @match
6546
6644
 
6547
6645
  (method_invocation
6548
6646
  name: (identifier) @method
6549
6647
  (#eq? @method "prepareStatement")
6550
6648
  arguments: (argument_list
6551
6649
  (binary_expression
6552
- operator: "+"))) @match
6650
+ operator: "+"
6651
+ right: (_) @rhs))) @match
6553
6652
 
6554
6653
  (method_invocation
6555
6654
  object: (identifier) @obj
@@ -6558,7 +6657,8 @@ var tsCheck4 = createTreeSitterCheck({
6558
6657
  (#match? @method "^(query|update)$")
6559
6658
  arguments: (argument_list
6560
6659
  (binary_expression
6561
- operator: "+"))) @match
6660
+ operator: "+"
6661
+ right: (_) @rhs))) @match
6562
6662
  `,
6563
6663
  ruby: `
6564
6664
  (call
@@ -6581,6 +6681,13 @@ var tsCheck4 = createTreeSitterCheck({
6581
6681
  remediationKey: "remediation.security.sql-injection-surface",
6582
6682
  messageFactory: (captures) => {
6583
6683
  return getCopy("checks.sql-injection-surface.message", captures.method ?? "?");
6684
+ },
6685
+ matchFilter: (captures) => {
6686
+ if (!captures.rhs) return true;
6687
+ const rhs = captures.rhs.trim();
6688
+ if (/^["'`]/.test(rhs)) return false;
6689
+ if (/^\[\]byte\s*\(["']/.test(rhs)) return false;
6690
+ return true;
6584
6691
  }
6585
6692
  });
6586
6693
  var llmCheck = {
@@ -8822,52 +8929,6 @@ var QUERIES6 = {
8822
8929
  arguments: (argument_list
8823
8930
  (_) @data)) @match
8824
8931
  `,
8825
- go: `
8826
- (call_expression
8827
- function: (selector_expression
8828
- operand: (identifier) @obj
8829
- field: (field_identifier) @field
8830
- (#eq? @obj "json")
8831
- (#eq? @field "Unmarshal"))
8832
- arguments: (argument_list
8833
- [
8834
- (identifier) @data
8835
- (call_expression) @data
8836
- (selector_expression) @data
8837
- (index_expression) @data
8838
- (binary_expression) @data
8839
- ]
8840
- (_))) @match
8841
-
8842
- (call_expression
8843
- function: (selector_expression
8844
- field: (field_identifier) @field
8845
- (#eq? @field "Decode"))
8846
- arguments: (argument_list
8847
- [
8848
- (identifier) @data
8849
- (call_expression) @data
8850
- (selector_expression) @data
8851
- (index_expression) @data
8852
- (binary_expression) @data
8853
- ])) @match
8854
-
8855
- (call_expression
8856
- function: (selector_expression
8857
- operand: (identifier) @obj
8858
- field: (field_identifier) @field
8859
- (#eq? @obj "xml")
8860
- (#eq? @field "Unmarshal"))
8861
- arguments: (argument_list
8862
- [
8863
- (identifier) @data
8864
- (call_expression) @data
8865
- (selector_expression) @data
8866
- (index_expression) @data
8867
- (binary_expression) @data
8868
- ]
8869
- (_))) @match
8870
- `,
8871
8932
  java: `
8872
8933
  (method_invocation
8873
8934
  name: (identifier) @method
@@ -16142,6 +16203,7 @@ async function executeScan(args, ctx) {
16142
16203
  licensedTo: ctx.licensedTo ?? "free tier",
16143
16204
  scannedFileCount: result.scannedFileCount,
16144
16205
  skippedChecks: result.skippedChecks,
16206
+ timedOutChecks: result.timedOutChecks,
16145
16207
  showIds: args.showIds ?? args.debug,
16146
16208
  all: args.all
16147
16209
  };