seaworthycode 1.2.13 → 1.2.17

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
@@ -412,7 +412,8 @@ async function crawl(options) {
412
412
  path: fullPath,
413
413
  relativePath: relPath,
414
414
  content: () => cachedContent ??= fs.readFile(fullPath, "utf-8"),
415
- language: inferLanguage(entry.name)
415
+ language: inferLanguage(entry.name),
416
+ size: stat2.size
416
417
  });
417
418
  } catch {
418
419
  }
@@ -433,7 +434,8 @@ async function crawl(options) {
433
434
  path: fullPath,
434
435
  relativePath: relPath,
435
436
  content: () => cachedContent ??= fs.readFile(fullPath, "utf-8"),
436
- language: inferLanguage(entry.name)
437
+ language: inferLanguage(entry.name),
438
+ size: stat2.size
437
439
  });
438
440
  } catch {
439
441
  }
@@ -1630,7 +1632,9 @@ var ScanRunner = class {
1630
1632
  files,
1631
1633
  meta,
1632
1634
  taintCache: /* @__PURE__ */ new Map(),
1633
- astCache: /* @__PURE__ */ new Map(),
1635
+ // astCache is intentionally omitted here — each check gets its own
1636
+ // per-check cache below so content strings are GC'd when the check ends.
1637
+ astCache: void 0,
1634
1638
  bashAstCache: /* @__PURE__ */ new Map()
1635
1639
  };
1636
1640
  const results = [];
@@ -1654,6 +1658,7 @@ var ScanRunner = class {
1654
1658
  let timedOut = false;
1655
1659
  let softTimer;
1656
1660
  let hardTimer;
1661
+ const checkCtx = { ...ctx, astCache: /* @__PURE__ */ new Map() };
1657
1662
  try {
1658
1663
  const findings2 = await new Promise((resolve22, reject) => {
1659
1664
  softTimer = setTimeout(() => {
@@ -1670,7 +1675,7 @@ var ScanRunner = class {
1670
1675
  );
1671
1676
  resolve22([]);
1672
1677
  }, checkTimeoutMs);
1673
- check2.run(ctx).then(resolve22, reject);
1678
+ check2.run(checkCtx).then(resolve22, reject);
1674
1679
  });
1675
1680
  clearTimeout(softTimer);
1676
1681
  clearTimeout(hardTimer);
@@ -5982,6 +5987,14 @@ function getMaxFilesPerLlmCheck() {
5982
5987
  }
5983
5988
  return 150;
5984
5989
  }
5990
+ function getMechanicalMaxFiles() {
5991
+ const env2 = process.env.SEAWORTHY_MECHANICAL_MAX_FILES;
5992
+ if (env2 !== void 0) {
5993
+ const n = parseInt(env2, 10);
5994
+ if (!isNaN(n) && n > 0) return n;
5995
+ }
5996
+ return 2e3;
5997
+ }
5985
5998
  function withLLMTimeout(promise, ms) {
5986
5999
  return new Promise((resolve22, reject) => {
5987
6000
  const timerId = setTimeout(() => {
@@ -6034,8 +6047,14 @@ async function filterFilesByBudget(files, budget) {
6034
6047
  }
6035
6048
  const result = [];
6036
6049
  for (const file of files) {
6037
- const content = await file.content();
6038
- if (estimateTokens(content) <= budget) {
6050
+ let tokens2;
6051
+ if (file.size !== void 0) {
6052
+ tokens2 = Math.ceil(file.size / 4);
6053
+ } else {
6054
+ const content = await file.content();
6055
+ tokens2 = estimateTokens(content);
6056
+ }
6057
+ if (tokens2 <= budget) {
6039
6058
  result.push(file);
6040
6059
  }
6041
6060
  }
@@ -6090,7 +6109,9 @@ function capProjectScopedFindings(findings) {
6090
6109
  }
6091
6110
  async function runProjectScopedMechanical(eligibleFiles, template) {
6092
6111
  if (eligibleFiles.length === 0) return [];
6093
- const contents = await Promise.all(eligibleFiles.map((f) => f.content()));
6112
+ const maxFiles = getMechanicalMaxFiles();
6113
+ const scanFiles = eligibleFiles.length > maxFiles ? eligibleFiles.slice(0, maxFiles) : eligibleFiles;
6114
+ const contents = await Promise.all(scanFiles.map((f) => f.content()));
6094
6115
  const combined = contents.join("\n");
6095
6116
  let shouldFlag = false;
6096
6117
  const pattern = template.fallbackPatterns[0];
@@ -6117,13 +6138,23 @@ async function runProjectAbsenceChecks(eligibleFiles, template) {
6117
6138
  if (absencePatterns.length === 0 || eligibleFiles.length === 0) {
6118
6139
  return [];
6119
6140
  }
6120
- const contents = await Promise.all(eligibleFiles.map((f) => f.content()));
6141
+ const maxFiles = getMechanicalMaxFiles();
6142
+ const scanFiles = eligibleFiles.length > maxFiles ? eligibleFiles.slice(0, maxFiles) : eligibleFiles;
6121
6143
  const anchorFile = pickProjectAnchorFile(eligibleFiles);
6144
+ const foundIndices = /* @__PURE__ */ new Set();
6145
+ for (const file of scanFiles) {
6146
+ if (foundIndices.size === absencePatterns.length) break;
6147
+ const content = await file.content();
6148
+ for (let i = 0; i < absencePatterns.length; i++) {
6149
+ if (!foundIndices.has(i) && absencePatterns[i].regex.test(content)) {
6150
+ foundIndices.add(i);
6151
+ }
6152
+ }
6153
+ }
6122
6154
  const results = [];
6123
- for (const pattern of absencePatterns) {
6124
- const foundAnywhere = contents.some((content) => pattern.regex.test(content));
6125
- if (!foundAnywhere) {
6126
- results.push(makeMechanicalFinding(template, pattern, anchorFile, 1));
6155
+ for (let i = 0; i < absencePatterns.length; i++) {
6156
+ if (!foundIndices.has(i)) {
6157
+ results.push(makeMechanicalFinding(template, absencePatterns[i], anchorFile, 1));
6127
6158
  }
6128
6159
  }
6129
6160
  return results;
@@ -6135,8 +6166,16 @@ async function runMechanicalCheck(ctx, template) {
6135
6166
  }
6136
6167
  const perFilePatterns = template.fallbackPatterns.filter((p) => !p.absenceBased);
6137
6168
  const projectResults = await runProjectAbsenceChecks(eligibleFiles, template);
6169
+ const maxMechanicalFiles = getMechanicalMaxFiles();
6170
+ let cappedFiles = eligibleFiles;
6171
+ if (cappedFiles.length > maxMechanicalFiles) {
6172
+ console.error(
6173
+ `[seaworthy] ${template.checkId}: ${cappedFiles.length} eligible files \u2014 capping mechanical scan to ${maxMechanicalFiles}`
6174
+ );
6175
+ cappedFiles = cappedFiles.slice(0, maxMechanicalFiles);
6176
+ }
6138
6177
  const fileResults = perFilePatterns.length === 0 ? [] : await runWithConcurrencyCollect(
6139
- eligibleFiles,
6178
+ cappedFiles,
6140
6179
  async (file) => {
6141
6180
  const content = await file.content();
6142
6181
  const lines = content.split("\n");
@@ -6216,14 +6255,21 @@ async function runLLMCheck(ctx, template) {
6216
6255
  return [];
6217
6256
  }
6218
6257
  const proEligibleFiles = ctx.files.filter((f) => isProCheckEligible(f, template));
6219
- const cacheKey = await buildCacheKey(ctx, template, proEligibleFiles);
6258
+ let eligibleFiles = await filterFilesByBudget(proEligibleFiles, template.tokenBudget);
6259
+ const maxFiles = getMaxFilesPerLlmCheck();
6260
+ if (eligibleFiles.length > maxFiles) {
6261
+ console.error(
6262
+ `[seaworthy] ${template.checkId}: ${eligibleFiles.length} eligible files \u2014 capping LLM prompt to ${maxFiles}`
6263
+ );
6264
+ eligibleFiles = eligibleFiles.slice(0, maxFiles);
6265
+ }
6266
+ const cacheKey = await buildCacheKey(ctx, template, eligibleFiles);
6220
6267
  if (cacheKey) {
6221
6268
  const cached = readCache(cacheKey);
6222
6269
  if (cached !== null) {
6223
6270
  return cached;
6224
6271
  }
6225
6272
  }
6226
- let eligibleFiles = await filterFilesByBudget(proEligibleFiles, template.tokenBudget);
6227
6273
  if (eligibleFiles.length === 0) {
6228
6274
  let mechFindings = await runMechanicalCheck(ctx, template);
6229
6275
  if (template.projectScoped) {
@@ -6239,13 +6285,6 @@ async function runLLMCheck(ctx, template) {
6239
6285
  }
6240
6286
  return withRemediation;
6241
6287
  }
6242
- const maxFiles = getMaxFilesPerLlmCheck();
6243
- if (eligibleFiles.length > maxFiles) {
6244
- console.error(
6245
- `[seaworthy] ${template.checkId}: ${eligibleFiles.length} eligible files \u2014 capping LLM prompt to ${maxFiles}`
6246
- );
6247
- eligibleFiles = eligibleFiles.slice(0, maxFiles);
6248
- }
6249
6288
  const prompt = `${PROMPT_PREAMBLE}
6250
6289
 
6251
6290
  ${await template.buildPrompt(eligibleFiles)}`;
@@ -6399,7 +6438,7 @@ registry.register({
6399
6438
  return runLLMCheck(ctx, promptTemplate2);
6400
6439
  }
6401
6440
  });
6402
- var PROMPT_VERSION3 = "1.0.0";
6441
+ var PROMPT_VERSION3 = "1.1.0";
6403
6442
  var fallbackPatterns3 = [
6404
6443
  {
6405
6444
  regex: /['"`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\s+.*\$\{/i,
@@ -6413,7 +6452,22 @@ var fallbackPatterns3 = [
6413
6452
  }
6414
6453
  ];
6415
6454
  async function buildPrompt3(sourceFiles) {
6416
- 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."];
6455
+ const parts = [
6456
+ `Check for SQL injection: flag ONLY where user-controlled input reaches a SQL query WITHOUT parameterization.
6457
+
6458
+ SAFE \u2014 do NOT flag:
6459
+ - Queries using ? or $N placeholders with a separate args array: db.Query("SELECT * FROM t WHERE id = ?", id)
6460
+ - 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)
6461
+ - String concatenation where ALL operands are string literals (compile-time constants, not variables)
6462
+ - Variables sourced from application config or startup env, not from HTTP requests or user payloads
6463
+
6464
+ UNSAFE \u2014 DO flag:
6465
+ - Template literals / f-strings injecting request values: db.query(\`SELECT * FROM t WHERE id = \${req.params.id}\`)
6466
+ - String concatenation where a variable traces to req.body, req.query, FormValue(), r.URL.Query(), $_GET, $_POST, or similar external input
6467
+ - fmt.Sprintf building a SQL string with user-supplied arguments (not just format specifiers over internal values)
6468
+
6469
+ Only flag if you can trace the injected value to an external input source (HTTP request, message payload, file upload).`
6470
+ ];
6417
6471
  for (const file of sourceFiles) {
6418
6472
  const content = await file.content();
6419
6473
  parts.push(`--- ${file.relativePath} ---
@@ -6455,7 +6509,8 @@ var tsCheck4 = createTreeSitterCheck({
6455
6509
  (#match? @method "^(query|raw|execute)$"))
6456
6510
  arguments: (arguments
6457
6511
  (binary_expression
6458
- operator: "+"))) @match
6512
+ operator: "+"
6513
+ right: (_) @rhs))) @match
6459
6514
 
6460
6515
  (call_expression
6461
6516
  function: (member_expression
@@ -6485,7 +6540,8 @@ var tsCheck4 = createTreeSitterCheck({
6485
6540
  (#match? @method "^(query|raw|execute)$"))
6486
6541
  arguments: (arguments
6487
6542
  (binary_expression
6488
- operator: "+"))) @match
6543
+ operator: "+"
6544
+ right: (_) @rhs))) @match
6489
6545
 
6490
6546
  (call_expression
6491
6547
  function: (member_expression
@@ -6515,7 +6571,8 @@ var tsCheck4 = createTreeSitterCheck({
6515
6571
  (#match? @method "^(query|raw|execute)$"))
6516
6572
  arguments: (arguments
6517
6573
  (binary_expression
6518
- operator: "+"))) @match
6574
+ operator: "+"
6575
+ right: (_) @rhs))) @match
6519
6576
 
6520
6577
  (call_expression
6521
6578
  function: (member_expression
@@ -6545,7 +6602,8 @@ var tsCheck4 = createTreeSitterCheck({
6545
6602
  (#match? @method "^(query|execute|raw)$"))
6546
6603
  arguments: (argument_list
6547
6604
  (binary_operator
6548
- operator: "+"))) @match
6605
+ operator: "+"
6606
+ right: (_) @rhs))) @match
6549
6607
 
6550
6608
  (call
6551
6609
  function: (attribute
@@ -6574,7 +6632,8 @@ var tsCheck4 = createTreeSitterCheck({
6574
6632
  (#match? @method "^(Query|Exec|QueryRow)$"))
6575
6633
  arguments: (argument_list
6576
6634
  (binary_expression
6577
- operator: "+"))) @match
6635
+ operator: "+"
6636
+ right: (_) @rhs))) @match
6578
6637
 
6579
6638
  (call_expression
6580
6639
  function: (selector_expression
@@ -6623,14 +6682,16 @@ var tsCheck4 = createTreeSitterCheck({
6623
6682
  (#match? @method "^execute.*$")
6624
6683
  arguments: (argument_list
6625
6684
  (binary_expression
6626
- operator: "+"))) @match
6685
+ operator: "+"
6686
+ right: (_) @rhs))) @match
6627
6687
 
6628
6688
  (method_invocation
6629
6689
  name: (identifier) @method
6630
6690
  (#eq? @method "prepareStatement")
6631
6691
  arguments: (argument_list
6632
6692
  (binary_expression
6633
- operator: "+"))) @match
6693
+ operator: "+"
6694
+ right: (_) @rhs))) @match
6634
6695
 
6635
6696
  (method_invocation
6636
6697
  object: (identifier) @obj
@@ -6639,7 +6700,8 @@ var tsCheck4 = createTreeSitterCheck({
6639
6700
  (#match? @method "^(query|update)$")
6640
6701
  arguments: (argument_list
6641
6702
  (binary_expression
6642
- operator: "+"))) @match
6703
+ operator: "+"
6704
+ right: (_) @rhs))) @match
6643
6705
  `,
6644
6706
  ruby: `
6645
6707
  (call
@@ -6662,6 +6724,13 @@ var tsCheck4 = createTreeSitterCheck({
6662
6724
  remediationKey: "remediation.security.sql-injection-surface",
6663
6725
  messageFactory: (captures) => {
6664
6726
  return getCopy("checks.sql-injection-surface.message", captures.method ?? "?");
6727
+ },
6728
+ matchFilter: (captures) => {
6729
+ if (!captures.rhs) return true;
6730
+ const rhs = captures.rhs.trim();
6731
+ if (/^["'`]/.test(rhs)) return false;
6732
+ if (/^\[\]byte\s*\(["']/.test(rhs)) return false;
6733
+ return true;
6665
6734
  }
6666
6735
  });
6667
6736
  var llmCheck = {
@@ -8903,52 +8972,6 @@ var QUERIES6 = {
8903
8972
  arguments: (argument_list
8904
8973
  (_) @data)) @match
8905
8974
  `,
8906
- go: `
8907
- (call_expression
8908
- function: (selector_expression
8909
- operand: (identifier) @obj
8910
- field: (field_identifier) @field
8911
- (#eq? @obj "json")
8912
- (#eq? @field "Unmarshal"))
8913
- arguments: (argument_list
8914
- [
8915
- (identifier) @data
8916
- (call_expression) @data
8917
- (selector_expression) @data
8918
- (index_expression) @data
8919
- (binary_expression) @data
8920
- ]
8921
- (_))) @match
8922
-
8923
- (call_expression
8924
- function: (selector_expression
8925
- field: (field_identifier) @field
8926
- (#eq? @field "Decode"))
8927
- arguments: (argument_list
8928
- [
8929
- (identifier) @data
8930
- (call_expression) @data
8931
- (selector_expression) @data
8932
- (index_expression) @data
8933
- (binary_expression) @data
8934
- ])) @match
8935
-
8936
- (call_expression
8937
- function: (selector_expression
8938
- operand: (identifier) @obj
8939
- field: (field_identifier) @field
8940
- (#eq? @obj "xml")
8941
- (#eq? @field "Unmarshal"))
8942
- arguments: (argument_list
8943
- [
8944
- (identifier) @data
8945
- (call_expression) @data
8946
- (selector_expression) @data
8947
- (index_expression) @data
8948
- (binary_expression) @data
8949
- ]
8950
- (_))) @match
8951
- `,
8952
8975
  java: `
8953
8976
  (method_invocation
8954
8977
  name: (identifier) @method