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 +101 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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(
|
|
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
|
-
|
|
6038
|
-
if (
|
|
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
|
|
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
|
|
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 (
|
|
6124
|
-
|
|
6125
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 = [
|
|
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: "+"
|
|
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: "+"
|
|
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: "+"
|
|
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: "+"
|
|
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: "+"
|
|
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: "+"
|
|
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: "+"
|
|
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: "+"
|
|
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
|