@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.
Files changed (2) hide show
  1. package/dist/index.js +65 -12
  2. 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: [{ regex: "(?:example|test|fake|dummy|placeholder|password123|changeme|process\\.env|import\\.meta\\.env|type|interface|schema|validation|zod|yup)", type: "context_line" }],
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: [{ regex: "(?:middleware|server|api|route\\.ts|route\\.js|\\bGET\\b|\\bPOST\\b|\\bPUT\\b|\\bDELETE\\b)", type: "file_path" }],
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: [{ regex: "(?:rateLimit|rateLimiter|throttle|limiter|upstash|slowDown|brute|attempts)", type: "context_line" }],
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: "fetch\\s*\\(\\s*(?:req\\.(?:body|query)|params|input|url|body|userUrl|targetUrl)", type: "match" },
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: [{ regex: "(?:169\\.254|metadata|validateUrl|isValidUrl|blockInternal|isInternalIp|allowedDomains|whitelist)", type: "context_line" }],
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: "fetch\\s*\\(\\s*(?:req|request|ctx)\\.(?:body|query|params)\\.\\w+\\s*\\)", type: "match" },
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: [{ regex: "(?:development|dev|NODE_ENV|process\\.env)", type: "context_line" }],
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("md5").update(`${f.ruleId}:${f.file}`).digest("hex");
7531
+ return createHash2("sha256").update(`${f.ruleId}:${f.file}`).digest("hex");
7479
7532
  }
7480
7533
  function historyPath(scanPath) {
7481
- const hash = createHash2("md5").update(scanPath).digest("hex");
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-safe/cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.7",
4
4
  "description": "Security scanner for AI-generated code — find vulnerabilities before you ship",
5
5
  "type": "module",
6
6
  "license": "MIT",