seaworthycode 1.2.20 → 1.2.21
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 +74 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
|
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 (!
|
|
4518
|
+
if (!envMatch.found && !hasSettings && !hasJavaConfig) return [];
|
|
4459
4519
|
const findings = [];
|
|
4460
|
-
if (
|
|
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: "
|
|
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 (
|
|
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
|
-
|
|
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({
|