xploitscan 1.0.7 → 1.0.9

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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  **Security scanner for AI-generated code.** Find vulnerabilities before attackers do.
7
7
 
8
- Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code. 131 security rules. Plain-English results. Copy-paste fixes.
8
+ Built for developers shipping code via Cursor, Lovable, Bolt, Replit, and Claude Code. 151 security rules. Plain-English results. Copy-paste fixes.
9
9
 
10
10
  ## Quick Start
11
11
 
@@ -17,7 +17,7 @@ No install, no config, no account required. Your code stays 100% local.
17
17
 
18
18
  ## What It Catches
19
19
 
20
- 131 rules across 15+ categories:
20
+ 151 rules across 15+ categories:
21
21
 
22
22
  | Category | Examples | Rules |
23
23
  |----------|---------|-------|
@@ -130,7 +130,7 @@ Scan via the web at [xploitscan.com](https://xploitscan.com):
130
130
  - SOC2/ISO27001 compliance mapping
131
131
  - Slack and Discord webhook notifications
132
132
 
133
- **Free**: 5 scans/day, 30 core rules. **Pro** ($29/mo): unlimited scans, all 131 rules, and all dashboard features. **Team** ($99/mo): everything in Pro plus 5 seats, shared scan history, RBAC, and portfolio reports. Annual plans save 20%.
133
+ **Free**: 5 scans/day, 30 core rules. **Pro** ($29/mo): unlimited scans, all 151 rules, and all dashboard features. **Team** ($99/mo): everything in Pro plus 5 seats, shared scan history, RBAC, and portfolio reports. Annual plans save 20%.
134
134
 
135
135
  ## Supported Languages
136
136
 
package/dist/index.js CHANGED
@@ -335,15 +335,20 @@ var hardcodedSecrets = {
335
335
  // Database URLs with credentials
336
336
  /(?:postgres|mysql|mongodb(?:\+srv)?):\/\/[^:]+:[^@]+@[^/\s"'`]+/gi
337
337
  ];
338
+ const fixMessages = [
339
+ "Move this API key to an environment variable. If this key has been committed, rotate it immediately \u2014 it may have already been scraped by bots.",
340
+ "AWS access key detected \u2014 may grant full account access (EC2, S3, IAM, billing). Rotate immediately in AWS Console \u2192 IAM \u2192 Security Credentials. Use IAM roles or environment variables instead.",
341
+ "Stripe key detected. sk_live_ keys can process real charges, issue refunds, and access customer payment data. sk_test_ keys are lower risk but should still not be committed. Rotate in Stripe Dashboard \u2192 Developers \u2192 API Keys.",
342
+ "Supabase key detected. Service role keys bypass Row Level Security and grant full database read/write access. Move to a server-side environment variable immediately.",
343
+ "OpenAI API key detected \u2014 grants full API access and can incur charges. Rotate at platform.openai.com \u2192 API Keys.",
344
+ "Hardcoded token or password detected. Move to an environment variable and rotate the credential if it has been committed to version control.",
345
+ "Private key found in source code. If this has been committed to version control, consider the key compromised \u2014 generate a new key pair and revoke the old one.",
346
+ "Database credentials in connection string. An attacker with this URL has full database access. Move to an environment variable, restrict network access, and rotate the password."
347
+ ];
338
348
  const matches = [];
339
- for (const pattern of patterns) {
340
- const rawMatches = findMatches(
341
- content,
342
- pattern,
343
- hardcodedSecrets,
344
- filePath,
345
- () => "Move this secret to an environment variable and add it to .env (not committed to git). Use .env.example to document the required variables."
346
- );
349
+ for (let pi = 0; pi < patterns.length; pi++) {
350
+ const pattern = patterns[pi];
351
+ const rawMatches = findMatches(content, pattern, hardcodedSecrets, filePath, () => fixMessages[pi]);
347
352
  for (const rm3 of rawMatches) {
348
353
  const lineText = content.split("\n")[rm3.line - 1] || "";
349
354
  const trimmed = lineText.trimStart();
@@ -393,7 +398,12 @@ var missingAuthMiddleware = {
393
398
  // Next.js API routes
394
399
  /export\s+(?:async\s+)?function\s+(?:GET|POST|PUT|PATCH|DELETE)\s*\(/gi
395
400
  ];
396
- if (filePath.includes("test") || filePath.includes("spec") || filePath.includes("mock")) return [];
401
+ if (isTestFile(filePath)) return [];
402
+ if (filePath.match(/\.(md|txt|rst|html|css|json|yaml|yml)$/)) return [];
403
+ const isAuthRoute = /\/auth\/|\/login|\/signup|\/register|\/logout|\/password\/|\/forgot|\/reset/i.test(filePath);
404
+ if (isAuthRoute) return [];
405
+ const isWebhookRoute = /\/webhook/i.test(filePath);
406
+ if (isWebhookRoute) return [];
397
407
  const authPatterns = [
398
408
  /auth/i,
399
409
  /session/i,
@@ -404,6 +414,8 @@ var missingAuthMiddleware = {
404
414
  /currentUser/i,
405
415
  /isAuthenticated/i,
406
416
  /requireAuth/i,
417
+ /requireUser/i,
418
+ /requireUserForApi/i,
407
419
  /clerk/i,
408
420
  /supabase\.auth/i,
409
421
  /getServerSession/i,
@@ -415,7 +427,10 @@ var missingAuthMiddleware = {
415
427
  /withAuth/i,
416
428
  /passport/i,
417
429
  /firebase\.auth/i,
418
- /cognito/i
430
+ /cognito/i,
431
+ /verifyCronSecret/i,
432
+ /verifySecret/i,
433
+ /checkApiKey/i
419
434
  ];
420
435
  const hasAuth = authPatterns.some((p) => p.test(content));
421
436
  if (hasAuth) return [];
@@ -477,18 +492,33 @@ var stripeWebhookUnprotected = {
477
492
  category: "Payment Security",
478
493
  description: "Stripe webhook endpoints without signature verification allow attackers to fake payment events.",
479
494
  check(content, filePath) {
480
- if (!/stripe|webhook/i.test(content)) return [];
481
- const hasWebhookRoute = /webhook/i.test(filePath) || /(?:post|handler).*webhook/i.test(content);
482
- if (!hasWebhookRoute) return [];
483
- const hasVerification = /constructEvent|verifyHeader|stripe-signature|webhook_secret/i.test(content);
484
- if (hasVerification) return [];
485
- return findMatches(
486
- content,
487
- /webhook/gi,
488
- stripeWebhookUnprotected,
489
- filePath,
490
- () => "Verify the Stripe webhook signature using stripe.webhooks.constructEvent(body, sig, webhookSecret) to prevent forged payment events."
491
- );
495
+ if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
496
+ if (isTestFile(filePath)) return [];
497
+ if (!/stripe/i.test(content)) return [];
498
+ const hasStripeWebhookHandler = /(?:stripe.*webhook|webhook.*stripe)/i.test(content) || /(?:checkout\.session\.completed|invoice\.paid|payment_intent|customer\.subscription)/i.test(content);
499
+ if (!hasStripeWebhookHandler) return [];
500
+ if (/constructEvent|verifyHeader|stripe[_-]?signature|webhook[_-]?secret|STRIPE_WEBHOOK_SECRET/i.test(content)) return [];
501
+ const handlerPatterns = [
502
+ // POST handler that processes Stripe events
503
+ /export\s+(?:async\s+)?function\s+POST\s*\(/g,
504
+ // Express-style Stripe webhook route
505
+ /\.(post|all)\s*\(\s*["'`][^"'`]*(?:stripe|webhook)[^"'`]*["'`]/gi,
506
+ // Event type checking without prior verification
507
+ /(?:event\.type|req\.body\.type)\s*===?\s*["'`](?:checkout|invoice|payment|customer)\./g
508
+ ];
509
+ const matches = [];
510
+ for (const pattern of handlerPatterns) {
511
+ matches.push(
512
+ ...findMatches(
513
+ content,
514
+ pattern,
515
+ stripeWebhookUnprotected,
516
+ filePath,
517
+ () => "Verify the Stripe webhook signature using stripe.webhooks.constructEvent(body, sig, webhookSecret) to prevent forged payment events."
518
+ )
519
+ );
520
+ }
521
+ return matches;
492
522
  }
493
523
  };
494
524
  var sqlInjection = {
@@ -1114,16 +1144,31 @@ var dangerousInnerHTML = {
1114
1144
  description: "Using dangerouslySetInnerHTML without sanitization (DOMPurify) enables XSS attacks. User-controlled content injected as raw HTML can execute arbitrary JavaScript.",
1115
1145
  check(content, filePath) {
1116
1146
  if (!filePath.match(/\.(jsx|tsx)$/)) return [];
1147
+ if (isTestFile(filePath)) return [];
1117
1148
  if (!/dangerouslySetInnerHTML/i.test(content)) return [];
1118
1149
  const hasSanitize = /DOMPurify|sanitize|purify|xss|sanitizeHtml|isomorphic-dompurify/i.test(content);
1119
1150
  if (hasSanitize) return [];
1120
- return findMatches(
1121
- content,
1122
- /dangerouslySetInnerHTML\s*=\s*\{\s*\{/g,
1123
- dangerousInnerHTML,
1124
- filePath,
1125
- () => "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify"
1126
- );
1151
+ const findings = [];
1152
+ const re = /dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:\s*([^}]+)\}/g;
1153
+ let m;
1154
+ while ((m = re.exec(content)) !== null) {
1155
+ if (isCommentLine(content, m.index)) continue;
1156
+ const value = m[1].trim();
1157
+ if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
1158
+ if (/^["'`][^$]*["'`]$/.test(value)) continue;
1159
+ const lineNum = content.substring(0, m.index).split("\n").length;
1160
+ findings.push({
1161
+ rule: "VC063",
1162
+ title: dangerousInnerHTML.title,
1163
+ severity: "critical",
1164
+ category: "Injection",
1165
+ file: filePath,
1166
+ line: lineNum,
1167
+ snippet: getSnippet(content, lineNum),
1168
+ fix: "Sanitize HTML before using dangerouslySetInnerHTML: dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(content) }}. Install: npm install dompurify"
1169
+ });
1170
+ }
1171
+ return findings;
1127
1172
  }
1128
1173
  };
1129
1174
  function detectFramework(files) {
@@ -1144,51 +1189,52 @@ function detectFramework(files) {
1144
1189
  if (frameworks.size === 0) frameworks.add("unknown");
1145
1190
  return [...frameworks];
1146
1191
  }
1147
- function calculateGrade(findings, totalFiles) {
1192
+ function calculateGrade(findings, _totalFiles) {
1148
1193
  if (findings.length === 0) {
1149
- return { grade: "A+", score: 100, summary: "No security issues detected. Excellent!" };
1194
+ return { grade: "A+", score: 100, summary: "No security issues detected. Excellent." };
1150
1195
  }
1151
- let deductions = 0;
1196
+ let critical = 0, high = 0, medium = 0, low = 0;
1152
1197
  for (const f of findings) {
1153
- switch (f.severity) {
1154
- case "critical":
1155
- deductions += 15;
1156
- break;
1157
- case "high":
1158
- deductions += 8;
1159
- break;
1160
- case "medium":
1161
- deductions += 4;
1162
- break;
1163
- case "low":
1164
- deductions += 1;
1165
- break;
1166
- }
1167
- }
1168
- const sizeBuffer = Math.min(Math.log2(Math.max(totalFiles, 1)) * 2, 15);
1169
- const score = Math.max(0, Math.min(100, 100 - deductions + sizeBuffer));
1198
+ if (f.severity === "critical") critical++;
1199
+ else if (f.severity === "high") high++;
1200
+ else if (f.severity === "medium") medium++;
1201
+ else if (f.severity === "low") low++;
1202
+ }
1203
+ const deductions = critical * 15 + high * 7 + medium * 3 + low * 1;
1204
+ const rawScore = Math.max(0, 100 - deductions);
1170
1205
  let grade;
1206
+ if (rawScore >= 97) grade = "A+";
1207
+ else if (rawScore >= 90) grade = "A";
1208
+ else if (rawScore >= 80) grade = "B";
1209
+ else if (rawScore >= 70) grade = "C";
1210
+ else if (rawScore >= 60) grade = "D";
1211
+ else grade = "F";
1212
+ const capGrade = (cap) => {
1213
+ const order = ["A+", "A", "B", "C", "D", "F"];
1214
+ return order.indexOf(grade) < order.indexOf(cap) ? cap : grade;
1215
+ };
1216
+ if (critical >= 1) grade = capGrade("D");
1217
+ else if (high >= 3) grade = capGrade("D");
1218
+ else if (high >= 1) grade = capGrade("B");
1219
+ else if (medium >= 5) grade = capGrade("B");
1220
+ else if (medium >= 1) grade = capGrade("A");
1171
1221
  let summary;
1172
- if (score >= 95) {
1173
- grade = "A+";
1174
- summary = "Excellent security posture with minimal issues.";
1175
- } else if (score >= 85) {
1176
- grade = "A";
1177
- summary = "Strong security with a few minor concerns.";
1178
- } else if (score >= 70) {
1179
- grade = "B";
1180
- summary = "Good security but some issues need attention.";
1181
- } else if (score >= 55) {
1182
- grade = "C";
1183
- summary = "Fair security \u2014 several vulnerabilities should be fixed.";
1184
- } else if (score >= 35) {
1185
- grade = "D";
1186
- summary = "Poor security \u2014 critical issues require immediate attention.";
1222
+ if (critical > 0) {
1223
+ summary = `${critical} critical ${critical === 1 ? "vulnerability" : "vulnerabilities"} require immediate attention.`;
1224
+ } else if (high >= 3) {
1225
+ summary = `${high} high-severity issues require urgent attention.`;
1226
+ } else if (high > 0) {
1227
+ summary = `${high} high-severity ${high === 1 ? "issue needs" : "issues need"} attention.`;
1228
+ } else if (medium >= 5) {
1229
+ summary = `${medium} medium-severity issues to address.`;
1230
+ } else if (medium > 0) {
1231
+ summary = `Clean of critical and high issues. ${medium} medium-severity ${medium === 1 ? "issue" : "issues"} to review.`;
1232
+ } else if (low > 0) {
1233
+ summary = `Clean of critical, high, and medium issues. ${low} low-severity best-practice ${low === 1 ? "note" : "notes"}.`;
1187
1234
  } else {
1188
- grade = "F";
1189
- summary = "Failing \u2014 serious vulnerabilities present. Fix critical issues immediately.";
1235
+ summary = "No security issues detected.";
1190
1236
  }
1191
- return { grade, score: Math.round(score), summary };
1237
+ return { grade, score: rawScore, summary };
1192
1238
  }
1193
1239
  var complianceMap = {
1194
1240
  VC001: { owasp: "A07:2021", cwe: "CWE-798" },
@@ -1321,7 +1367,29 @@ var complianceMap = {
1321
1367
  VC128: { owasp: "A05:2021", cwe: "CWE-444" },
1322
1368
  VC129: { owasp: "A02:2021", cwe: "CWE-311" },
1323
1369
  VC130: { owasp: "A07:2021", cwe: "CWE-307" },
1324
- VC131: { owasp: "A06:2021", cwe: "CWE-1104" }
1370
+ VC131: { owasp: "A06:2021", cwe: "CWE-1104" },
1371
+ // VC132–VC145: Service-specific API key detection
1372
+ VC132: { owasp: "A07:2021", cwe: "CWE-798" },
1373
+ VC133: { owasp: "A07:2021", cwe: "CWE-798" },
1374
+ VC134: { owasp: "A07:2021", cwe: "CWE-798" },
1375
+ VC135: { owasp: "A07:2021", cwe: "CWE-798" },
1376
+ VC136: { owasp: "A07:2021", cwe: "CWE-798" },
1377
+ VC137: { owasp: "A07:2021", cwe: "CWE-798" },
1378
+ VC138: { owasp: "A07:2021", cwe: "CWE-798" },
1379
+ VC139: { owasp: "A07:2021", cwe: "CWE-798" },
1380
+ VC140: { owasp: "A07:2021", cwe: "CWE-798" },
1381
+ VC141: { owasp: "A07:2021", cwe: "CWE-798" },
1382
+ VC142: { owasp: "A07:2021", cwe: "CWE-798" },
1383
+ VC143: { owasp: "A07:2021", cwe: "CWE-798" },
1384
+ VC144: { owasp: "A07:2021", cwe: "CWE-798" },
1385
+ VC145: { owasp: "A07:2021", cwe: "CWE-798" },
1386
+ // VC146–VC151: Secret exposure path analysis
1387
+ VC146: { owasp: "A07:2021", cwe: "CWE-598" },
1388
+ VC147: { owasp: "A09:2021", cwe: "CWE-532" },
1389
+ VC148: { owasp: "A09:2021", cwe: "CWE-209" },
1390
+ VC149: { owasp: "A07:2021", cwe: "CWE-798" },
1391
+ VC150: { owasp: "A07:2021", cwe: "CWE-615" },
1392
+ VC151: { owasp: "A07:2021", cwe: "CWE-214" }
1325
1393
  };
1326
1394
  var consoleLogProduction = {
1327
1395
  id: "VC097",
@@ -2468,6 +2536,8 @@ function scanEntropy(files) {
2468
2536
  for (const { path: filePath, content } of files) {
2469
2537
  if (SKIP_FILES.test(filePath)) continue;
2470
2538
  if (SKIP_FILENAMES.test(filePath)) continue;
2539
+ const basename = filePath.split("/").pop() || "";
2540
+ if (SKIP_FILENAMES.test(basename)) continue;
2471
2541
  if (filePath.includes("node_modules")) continue;
2472
2542
  if (filePath.includes(".min.")) continue;
2473
2543
  if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;
@@ -3442,9 +3512,9 @@ async function scanCommand(directory, options) {
3442
3512
  if (tier === "free" && !isSilent) {
3443
3513
  console.log("");
3444
3514
  if (userPlan === "anonymous") {
3445
- console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Log in to unlock all 131 rules \u2192") + chalk2.bold(" xploitscan auth login"));
3515
+ console.log(chalk2.gray(" Scanned with 30 free rules.") + chalk2.cyan(" Log in to unlock all 151 rules \u2192") + chalk2.bold(" xploitscan auth login"));
3446
3516
  } else {
3447
- console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all 131 rules \u2192") + chalk2.bold(" xploitscan upgrade"));
3517
+ console.log(chalk2.gray(" Scanned with 30 rules.") + chalk2.cyan(" Upgrade to Pro for all 151 rules \u2192") + chalk2.bold(" xploitscan upgrade"));
3448
3518
  }
3449
3519
  console.log("");
3450
3520
  }
@@ -3820,7 +3890,7 @@ async function cursorInstallCommand(opts = {}) {
3820
3890
  var program = new Command();
3821
3891
  program.name("xploitscan").description(
3822
3892
  "AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
3823
- ).version("1.0.7");
3893
+ ).version("1.0.8");
3824
3894
  program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option("-f, --format <format>", "Output format: terminal, json, sarif", "terminal").option("-v, --verbose", "Show detailed output", false).option("--diff [base]", "Scan only files changed vs base branch (default: main)").option("-w, --watch", "Watch for file changes and re-scan automatically", false).action(async (directory, opts) => {
3825
3895
  await scanCommand(directory, {
3826
3896
  directory,