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 +3 -3
- package/dist/index.js +141 -71
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
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
|
|
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 (
|
|
340
|
-
const
|
|
341
|
-
|
|
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 (
|
|
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 (
|
|
481
|
-
|
|
482
|
-
if (
|
|
483
|
-
const
|
|
484
|
-
if (
|
|
485
|
-
return
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
()
|
|
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
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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,
|
|
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
|
|
1196
|
+
let critical = 0, high = 0, medium = 0, low = 0;
|
|
1152
1197
|
for (const f of findings) {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
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 (
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
summary =
|
|
1178
|
-
} else if (
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
summary =
|
|
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
|
-
|
|
1189
|
-
summary = "Failing \u2014 serious vulnerabilities present. Fix critical issues immediately.";
|
|
1235
|
+
summary = "No security issues detected.";
|
|
1190
1236
|
}
|
|
1191
|
-
return { grade, score:
|
|
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
|
|
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
|
|
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.
|
|
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,
|