@ship-safe/cli 1.1.3 → 1.1.7
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 +65 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4210,7 +4210,8 @@ var NEXTJS_RULES = [
|
|
|
4210
4210
|
{ regex: "export\\s+(?:async\\s+)?function\\s+(?:GET|POST|PUT|DELETE|PATCH)\\s*\\(", type: "match" }
|
|
4211
4211
|
],
|
|
4212
4212
|
excludePatterns: [
|
|
4213
|
-
{ regex: "(?:auth|session|token|clerk|getAuth|currentUser|requireAuth|protect|webhook|public|health)", type: "context_line" }
|
|
4213
|
+
{ regex: "(?:auth|session|token|clerk|getAuth|currentUser|requireAuth|protect|webhook|public|health)", type: "context_line" },
|
|
4214
|
+
{ regex: "(?:webhook|callback|health|cron|stripe|checkout|unsubscribe|contact|export|badge|cli\\/)", type: "file_path" }
|
|
4214
4215
|
],
|
|
4215
4216
|
fix: { description: "Add authentication to your API route. Use middleware or check the session at the start of each handler.", suggestion: "const { userId } = auth();\nif (!userId) return new Response('Unauthorized', { status: 401 });" }
|
|
4216
4217
|
},
|
|
@@ -4714,7 +4715,10 @@ var SECRET_RULES = [
|
|
|
4714
4715
|
owasp: "A07:2021",
|
|
4715
4716
|
languages: ["*"],
|
|
4716
4717
|
patterns: [{ regex: `(?:password|passwd|pwd)\\s*[:=]\\s*["']([^\\s"']{8,})["']`, type: "match" }],
|
|
4717
|
-
excludePatterns: [
|
|
4718
|
+
excludePatterns: [
|
|
4719
|
+
{ regex: "(?:example|test|fake|dummy|placeholder|password123|changeme|process\\.env|import\\.meta\\.env|type|interface|schema|validation|zod|yup)", type: "context_line" },
|
|
4720
|
+
{ regex: "(?:admin123|admin|default|sample|secret123|qwerty|letmein|welcome|monkey|dragon|master|1234|abcd)", type: "context_line" }
|
|
4721
|
+
],
|
|
4718
4722
|
fix: { description: "Remove hardcoded passwords. Use environment variables or a secrets manager." }
|
|
4719
4723
|
},
|
|
4720
4724
|
{
|
|
@@ -4893,6 +4897,10 @@ var XSS_RULES = [
|
|
|
4893
4897
|
owasp: "A03:2021",
|
|
4894
4898
|
languages: ["javascript", "typescript"],
|
|
4895
4899
|
patterns: [{ regex: "dangerouslySetInnerHTML", type: "match" }],
|
|
4900
|
+
excludePatterns: [
|
|
4901
|
+
{ regex: "(?:json-ld|jsonLd|structured-data|schema\\.org|ld\\+json|localStorage|theme|nonce|suppressHydration|DOMPurify|sanitize)", type: "context_line" },
|
|
4902
|
+
{ regex: "(?:json-ld|jsonld|structured-data|layout\\.tsx|layout\\.jsx|_document\\.tsx|_document\\.jsx)", type: "file_path" }
|
|
4903
|
+
],
|
|
4896
4904
|
fix: { description: "Avoid dangerouslySetInnerHTML. If you must use it, sanitize the HTML with DOMPurify or a similar library first." }
|
|
4897
4905
|
},
|
|
4898
4906
|
{
|
|
@@ -4905,6 +4913,10 @@ var XSS_RULES = [
|
|
|
4905
4913
|
owasp: "A03:2021",
|
|
4906
4914
|
languages: ["javascript", "typescript"],
|
|
4907
4915
|
patterns: [{ regex: "document\\.write\\s*\\(", type: "match" }],
|
|
4916
|
+
excludePatterns: [
|
|
4917
|
+
{ regex: "(?:pdf|print|export|report|window\\.open)", type: "context_line" },
|
|
4918
|
+
{ regex: "(?:pdf|print|export)", type: "file_path" }
|
|
4919
|
+
],
|
|
4908
4920
|
fix: { description: "Replace document.write() with DOM manipulation methods like createElement() and appendChild()." }
|
|
4909
4921
|
},
|
|
4910
4922
|
{
|
|
@@ -5034,6 +5046,10 @@ var PII_RULES = [
|
|
|
5034
5046
|
owasp: "A09:2021",
|
|
5035
5047
|
languages: ["javascript", "typescript"],
|
|
5036
5048
|
patterns: [{ regex: "console\\.log\\s*\\(.*(?:email|user\\.email|userEmail)", type: "match" }],
|
|
5049
|
+
excludePatterns: [
|
|
5050
|
+
{ regex: "(?:chalk|ora|spinner|ink|inquirer|readline|prompt|display|print|show)", type: "context_line" },
|
|
5051
|
+
{ regex: "(?:cli\\/|commands\\/|bin\\/)", type: "file_path" }
|
|
5052
|
+
],
|
|
5037
5053
|
fix: { description: "Avoid logging email addresses or other PII. If needed for debugging, mask the email." }
|
|
5038
5054
|
},
|
|
5039
5055
|
{
|
|
@@ -5046,6 +5062,10 @@ var PII_RULES = [
|
|
|
5046
5062
|
owasp: "A09:2021",
|
|
5047
5063
|
languages: ["javascript", "typescript"],
|
|
5048
5064
|
patterns: [{ regex: "console\\.(?:log|info|debug|warn)\\s*\\(.*(?:password|token|secret|creditCard|ssn|socialSecurity)", type: "match" }],
|
|
5065
|
+
excludePatterns: [
|
|
5066
|
+
{ regex: "(?:not set|missing|undefined|required|invalid|expired|failed|error|skipping)", type: "context_line" },
|
|
5067
|
+
{ regex: "(?:cli\\/|commands\\/|bin\\/)", type: "file_path" }
|
|
5068
|
+
],
|
|
5049
5069
|
fix: { description: "Remove console logging of sensitive data before deploying to production." }
|
|
5050
5070
|
},
|
|
5051
5071
|
{
|
|
@@ -5091,7 +5111,10 @@ var AUTHZ_RULES = [
|
|
|
5091
5111
|
owasp: "A01:2021",
|
|
5092
5112
|
languages: ["javascript", "typescript"],
|
|
5093
5113
|
patterns: [{ regex: `(?:isAdmin|role\\s*===?\\s*["']admin|user\\.role|hasPermission|isAuthorized)\\s*(?:\\)|&&|;|\\?)`, type: "match" }],
|
|
5094
|
-
excludePatterns: [
|
|
5114
|
+
excludePatterns: [
|
|
5115
|
+
{ regex: "(?:middleware|server|api|route\\.ts|route\\.js|\\bGET\\b|\\bPOST\\b|\\bPUT\\b|\\bDELETE\\b)", type: "file_path" },
|
|
5116
|
+
{ regex: "(?:useQuery|useConvex|useMutation|convex|trpc|graphql|useSWR|useSession|getServerSession|className|disabled|hidden|opacity|hidden|display|render|show|visible)", type: "context_line" }
|
|
5117
|
+
],
|
|
5095
5118
|
fix: { description: "Always enforce admin/role checks on the server side (API routes or middleware), not just in the frontend. Client-side checks are for UI only." }
|
|
5096
5119
|
},
|
|
5097
5120
|
{
|
|
@@ -5561,7 +5584,10 @@ var HEADERS_RULES = [
|
|
|
5561
5584
|
{ regex: "(?:login|signin|sign-in|signup|sign-up|register|reset-password|forgot-password|verify-email).*(?:POST|post|handler|action)", type: "match" },
|
|
5562
5585
|
{ regex: "(?:POST|post|handler|action).*(?:login|signin|sign-in|signup|sign-up|register|reset-password|forgot-password)", type: "match" }
|
|
5563
5586
|
],
|
|
5564
|
-
excludePatterns: [
|
|
5587
|
+
excludePatterns: [
|
|
5588
|
+
{ regex: "(?:rateLimit|rateLimiter|throttle|limiter|upstash|slowDown|brute|attempts)", type: "context_line" },
|
|
5589
|
+
{ regex: "(?:cli\\/|commands\\/|bin\\/|desktop\\/|electron\\/)", type: "file_path" }
|
|
5590
|
+
],
|
|
5565
5591
|
fix: { description: "Add rate limiting to all authentication endpoints. Limit login attempts per IP and per account to prevent brute force attacks.", suggestion: "const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 })" }
|
|
5566
5592
|
},
|
|
5567
5593
|
{
|
|
@@ -5574,10 +5600,12 @@ var HEADERS_RULES = [
|
|
|
5574
5600
|
owasp: "A10:2021",
|
|
5575
5601
|
languages: ["javascript", "typescript"],
|
|
5576
5602
|
patterns: [
|
|
5577
|
-
{ regex: "
|
|
5603
|
+
{ regex: "\\bfetch\\s*\\(\\s*(?:req\\.(?:body|query)|params|input|url|body|userUrl|targetUrl)", type: "match" },
|
|
5578
5604
|
{ regex: "axios\\.(?:get|post|request)\\s*\\(\\s*(?:req\\.(?:body|query)|params|input|url|body|userUrl)", type: "match" }
|
|
5579
5605
|
],
|
|
5580
|
-
excludePatterns: [
|
|
5606
|
+
excludePatterns: [
|
|
5607
|
+
{ regex: "(?:169\\.254|metadata|validateUrl|isValidUrl|blockInternal|isInternalIp|allowedDomains|whitelist|resolveAndValidate|safeFetch|pinnedUrl|ssrf)", type: "context_line" }
|
|
5608
|
+
],
|
|
5581
5609
|
fix: { description: "Block requests to cloud metadata IPs (169.254.169.254, fd00::, 10.x, 172.16-31.x, 192.168.x) before fetching user-provided URLs.", suggestion: "if (isInternalIP(url)) throw new Error('Internal URLs not allowed')" }
|
|
5582
5610
|
},
|
|
5583
5611
|
{
|
|
@@ -5590,10 +5618,10 @@ var HEADERS_RULES = [
|
|
|
5590
5618
|
owasp: "A10:2021",
|
|
5591
5619
|
languages: ["javascript", "typescript"],
|
|
5592
5620
|
patterns: [
|
|
5593
|
-
{ regex: "
|
|
5621
|
+
{ regex: "\\bfetch\\s*\\(\\s*(?:req|request|ctx)\\.(?:body|query|params)\\.\\w+\\s*\\)", type: "match" },
|
|
5594
5622
|
{ regex: "new\\s+URL\\s*\\(\\s*(?:req|request|ctx)\\.(?:body|query|params)\\.(?:url|target|link|href)", type: "match" }
|
|
5595
5623
|
],
|
|
5596
|
-
excludePatterns: [{ regex: "(?:isInternalIp|blockPrivate|127\\.0\\.0|10\\.|172\\.16|192\\.168|validateUrl|allowedHosts|dnsResolve)", type: "context_line" }],
|
|
5624
|
+
excludePatterns: [{ regex: "(?:isInternalIp|blockPrivate|127\\.0\\.0|10\\.|172\\.16|192\\.168|validateUrl|allowedHosts|dnsResolve|resolveAndValidate|safeFetch|pinnedUrl|ssrf)", type: "context_line" }],
|
|
5597
5625
|
fix: { description: "Validate user URLs by resolving DNS and checking the IP is not in a private range before making the request." }
|
|
5598
5626
|
}
|
|
5599
5627
|
];
|
|
@@ -5716,7 +5744,10 @@ var CLIENT_RULES = [
|
|
|
5716
5744
|
{ regex: "(?:res|response)\\.(?:json|send|status)\\s*\\([^)]*(?:error\\.stack|error\\.message|err\\.stack|err\\.message|e\\.stack)", type: "match" },
|
|
5717
5745
|
{ regex: "NextResponse\\.json\\s*\\(\\s*\\{[^}]*(?:error\\.stack|error\\.message|err\\.stack|err\\.message)", type: "match" }
|
|
5718
5746
|
],
|
|
5719
|
-
excludePatterns: [
|
|
5747
|
+
excludePatterns: [
|
|
5748
|
+
{ regex: "(?:development|dev|NODE_ENV|process\\.env)", type: "context_line" },
|
|
5749
|
+
{ regex: "(?:startsWith|includes|===|!==|Quota|limit|Unauthorized|Invalid)", type: "context_line" }
|
|
5750
|
+
],
|
|
5720
5751
|
fix: { description: "Return a generic error message to users (e.g. 'Something went wrong'). Log the full error server-side with console.error() for debugging. Never send stack traces or internal error messages in API responses." }
|
|
5721
5752
|
},
|
|
5722
5753
|
{
|
|
@@ -5749,7 +5780,7 @@ var CLIENT_RULES = [
|
|
|
5749
5780
|
],
|
|
5750
5781
|
excludePatterns: [
|
|
5751
5782
|
{ regex: "(?:csrf|xsrf|_token|csrfToken|validateToken|SameSite|clerk|auth\\(\\)|getAuth|currentUser|getSession)", type: "context_line" },
|
|
5752
|
-
{ regex: "(?:api/webhook|api/stripe|api/clerk|api/cron|api/internal)", type: "file_path" }
|
|
5783
|
+
{ regex: "(?:api/webhook|api/stripe|api/clerk|api/cron|api/internal|api/cli|api/checkout|api/contact|api/unsubscribe)", type: "file_path" }
|
|
5753
5784
|
],
|
|
5754
5785
|
fix: { description: "Add CSRF protection: use SameSite=Strict cookies, verify a CSRF token header, or use a framework that handles this automatically (e.g. Next.js Server Actions have built-in CSRF protection)." }
|
|
5755
5786
|
}
|
|
@@ -5819,10 +5850,30 @@ function extractSnippet(content, line, contextLines = 5) {
|
|
|
5819
5850
|
return `${marker} ${lineNum} | ${l}`;
|
|
5820
5851
|
}).join("\n");
|
|
5821
5852
|
}
|
|
5853
|
+
function isCommentLine(line) {
|
|
5854
|
+
const trimmed = line.trim();
|
|
5855
|
+
return trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("/*") || trimmed.startsWith("* ") || trimmed.startsWith("*/") || trimmed === "*" || trimmed.startsWith("<!--");
|
|
5856
|
+
}
|
|
5857
|
+
function isInsideStringLiteral(line, pattern) {
|
|
5858
|
+
const match = pattern.exec(line);
|
|
5859
|
+
if (!match) return false;
|
|
5860
|
+
const idx = match.index;
|
|
5861
|
+
pattern.lastIndex = 0;
|
|
5862
|
+
let backtickCount = 0;
|
|
5863
|
+
for (let i = 0; i < idx; i++) {
|
|
5864
|
+
if (line[i] === "`" && (i === 0 || line[i - 1] !== "\\")) backtickCount++;
|
|
5865
|
+
}
|
|
5866
|
+
return backtickCount % 2 === 1;
|
|
5867
|
+
}
|
|
5822
5868
|
function matchRule(rule, file) {
|
|
5823
5869
|
if (!rule.languages.includes("*") && !rule.languages.includes(file.language)) {
|
|
5824
5870
|
return [];
|
|
5825
5871
|
}
|
|
5872
|
+
const isSecretRule = rule.id.startsWith("secrets/");
|
|
5873
|
+
if (!isSecretRule) {
|
|
5874
|
+
const docPaths = /(?:\/docs\/|\/examples\/|\/fixtures\/|__tests__\/fixtures)/i;
|
|
5875
|
+
if (docPaths.test(file.path)) return [];
|
|
5876
|
+
}
|
|
5826
5877
|
const findings = [];
|
|
5827
5878
|
const lines = file.content.split("\n");
|
|
5828
5879
|
for (const pattern of rule.patterns) {
|
|
@@ -5832,6 +5883,8 @@ function matchRule(rule, file) {
|
|
|
5832
5883
|
const line = lines[i];
|
|
5833
5884
|
if (!regex.test(line)) continue;
|
|
5834
5885
|
regex.lastIndex = 0;
|
|
5886
|
+
if (!isSecretRule && isCommentLine(line)) continue;
|
|
5887
|
+
if (!isSecretRule && isInsideStringLiteral(line, new RegExp(pattern.regex, "gi"))) continue;
|
|
5835
5888
|
if (rule.excludePatterns?.length) {
|
|
5836
5889
|
const excluded = rule.excludePatterns.some((ep) => {
|
|
5837
5890
|
const exRegex = new RegExp(ep.regex, "i");
|
|
@@ -7475,10 +7528,10 @@ import { homedir as homedir2 } from "os";
|
|
|
7475
7528
|
import { join as join3 } from "path";
|
|
7476
7529
|
var HISTORY_DIR = join3(homedir2(), ".shipsafe", "scans");
|
|
7477
7530
|
function fingerprint(f) {
|
|
7478
|
-
return createHash2("
|
|
7531
|
+
return createHash2("sha256").update(`${f.ruleId}:${f.file}`).digest("hex");
|
|
7479
7532
|
}
|
|
7480
7533
|
function historyPath(scanPath) {
|
|
7481
|
-
const hash = createHash2("
|
|
7534
|
+
const hash = createHash2("sha256").update(scanPath).digest("hex");
|
|
7482
7535
|
return join3(HISTORY_DIR, `${hash}.json`);
|
|
7483
7536
|
}
|
|
7484
7537
|
function diffWithPrevious(scanPath, currentFindings) {
|