xploitscan 1.0.18 → 1.0.20
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 +141 -16
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -540,26 +540,42 @@ var sqlInjection = {
|
|
|
540
540
|
description: "String concatenation or template literals in SQL queries allow attackers to execute arbitrary database commands.",
|
|
541
541
|
check(content, filePath) {
|
|
542
542
|
const patterns = [
|
|
543
|
-
// Template
|
|
544
|
-
/
|
|
545
|
-
//
|
|
546
|
-
|
|
547
|
-
//
|
|
543
|
+
// Template literal passed as first arg to query/execute/raw/sql/
|
|
544
|
+
// queryRaw/queryRawUnsafe/execute. Examples that SHOULD fire:
|
|
545
|
+
// db.query(`SELECT ... ${x}`)
|
|
546
|
+
// prisma.$queryRawUnsafe(`... ${x}`)
|
|
547
|
+
// knex.raw(`... ${x}`)
|
|
548
|
+
/(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*`[^`]*\$\{/gi,
|
|
549
|
+
// String concatenation in SQL — now includes raw() and sql() and
|
|
550
|
+
// Drizzle's sql.raw() / Prisma's $queryRawUnsafe() variants. Previous
|
|
551
|
+
// version only covered query()/execute() and missed knex.raw("..." + x).
|
|
552
|
+
//
|
|
553
|
+
// The string literal part uses a proper "quoted string" regex that
|
|
554
|
+
// allows the opposite quote type inside (common in SQL — "WHERE x = 'a'").
|
|
555
|
+
// The older `[^"']*` class bailed out at the first inner quote and
|
|
556
|
+
// missed every realistic SQL-containing concat.
|
|
557
|
+
/(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\+/gi,
|
|
558
|
+
// Direct variable interpolation in a SQL verb context
|
|
548
559
|
/(?:SELECT|INSERT|UPDATE|DELETE|WHERE)\s+.*\$\{(?!.*parameterized)/gi
|
|
549
560
|
];
|
|
550
561
|
const matches = [];
|
|
551
562
|
const usesParams = /\?\s*,|\$\d+|:[\w]+|\bprepare\b|\bplaceholder\b/i.test(content);
|
|
552
563
|
if (usesParams) return [];
|
|
553
564
|
for (const pattern of patterns) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
() => "Use parameterized queries or prepared statements instead of string interpolation. Example: db.query('SELECT * FROM users WHERE id = ?', [userId])"
|
|
561
|
-
)
|
|
565
|
+
const raw = findMatches(
|
|
566
|
+
content,
|
|
567
|
+
pattern,
|
|
568
|
+
sqlInjection,
|
|
569
|
+
filePath,
|
|
570
|
+
() => "Use parameterized queries or prepared statements instead of string interpolation. Example: db.query('SELECT * FROM users WHERE id = ?', [userId])"
|
|
562
571
|
);
|
|
572
|
+
for (const m of raw) {
|
|
573
|
+
const lineText = content.split("\n")[m.line - 1] || "";
|
|
574
|
+
if (/\bPrisma\.sql\s*`/.test(lineText)) continue;
|
|
575
|
+
if (/\bsql\s*`/.test(lineText) && !/\bsql\.raw\s*\(/.test(lineText)) continue;
|
|
576
|
+
if (/\$queryRaw\s*\(\s*Prisma\.sql|\$executeRaw\s*\(\s*Prisma\.sql/.test(lineText)) continue;
|
|
577
|
+
matches.push(m);
|
|
578
|
+
}
|
|
563
579
|
}
|
|
564
580
|
return matches;
|
|
565
581
|
}
|
|
@@ -1246,7 +1262,7 @@ function calculateGrade(findings, _totalFiles) {
|
|
|
1246
1262
|
else if (medium >= 1) grade = capGrade("A");
|
|
1247
1263
|
let summary;
|
|
1248
1264
|
if (critical > 0) {
|
|
1249
|
-
summary = `${critical} critical ${critical === 1 ? "vulnerability" : "vulnerabilities"}
|
|
1265
|
+
summary = `${critical} critical ${critical === 1 ? "vulnerability requires" : "vulnerabilities require"} immediate attention.`;
|
|
1250
1266
|
} else if (high >= 3) {
|
|
1251
1267
|
summary = `${high} high-severity issues require urgent attention.`;
|
|
1252
1268
|
} else if (high > 0) {
|
|
@@ -3456,6 +3472,102 @@ Suggested fix: ${f.fix}` : `${f.title}: ${f.description}`;
|
|
|
3456
3472
|
console.log(JSON.stringify(sarif, null, 2));
|
|
3457
3473
|
}
|
|
3458
3474
|
|
|
3475
|
+
// src/reporters/siem.ts
|
|
3476
|
+
var ECS_SEVERITY = {
|
|
3477
|
+
// Elastic Common Schema uses 0–100 integer severity.
|
|
3478
|
+
critical: 90,
|
|
3479
|
+
high: 70,
|
|
3480
|
+
medium: 50,
|
|
3481
|
+
low: 30
|
|
3482
|
+
};
|
|
3483
|
+
var DATADOG_STATUS = {
|
|
3484
|
+
// Datadog Logs status field accepts emergency/alert/critical/error/warning/notice/info/debug.
|
|
3485
|
+
critical: "critical",
|
|
3486
|
+
high: "error",
|
|
3487
|
+
medium: "warning",
|
|
3488
|
+
low: "info"
|
|
3489
|
+
};
|
|
3490
|
+
function commonFields(result, finding) {
|
|
3491
|
+
return {
|
|
3492
|
+
rule: finding.rule,
|
|
3493
|
+
title: finding.title,
|
|
3494
|
+
description: finding.description,
|
|
3495
|
+
severity: finding.severity,
|
|
3496
|
+
file: finding.file,
|
|
3497
|
+
line: finding.line,
|
|
3498
|
+
category: finding.category,
|
|
3499
|
+
cwe: finding.cwe ?? null,
|
|
3500
|
+
owasp: finding.owasp ?? null,
|
|
3501
|
+
fix: finding.fix ?? null,
|
|
3502
|
+
scan_timestamp: result.timestamp,
|
|
3503
|
+
scan_directory: result.directory,
|
|
3504
|
+
files_scanned: result.filesScanned,
|
|
3505
|
+
scan_duration_ms: result.duration
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
function renderSplunkReport(result) {
|
|
3509
|
+
for (const finding of result.findings) {
|
|
3510
|
+
const event = {
|
|
3511
|
+
time: Math.floor(new Date(result.timestamp).getTime() / 1e3),
|
|
3512
|
+
sourcetype: "xploitscan:finding",
|
|
3513
|
+
source: "xploitscan",
|
|
3514
|
+
event: commonFields(result, finding)
|
|
3515
|
+
};
|
|
3516
|
+
console.log(JSON.stringify(event));
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
function renderElasticReport(result) {
|
|
3520
|
+
for (const finding of result.findings) {
|
|
3521
|
+
const event = {
|
|
3522
|
+
"@timestamp": result.timestamp,
|
|
3523
|
+
ecs: { version: "8.11.0" },
|
|
3524
|
+
event: {
|
|
3525
|
+
kind: "alert",
|
|
3526
|
+
category: ["vulnerability"],
|
|
3527
|
+
type: ["info"],
|
|
3528
|
+
dataset: "xploitscan.finding",
|
|
3529
|
+
module: "xploitscan",
|
|
3530
|
+
severity: ECS_SEVERITY[finding.severity] ?? 50,
|
|
3531
|
+
action: finding.rule
|
|
3532
|
+
},
|
|
3533
|
+
vulnerability: {
|
|
3534
|
+
id: finding.rule,
|
|
3535
|
+
category: [finding.category],
|
|
3536
|
+
description: finding.description,
|
|
3537
|
+
severity: finding.severity,
|
|
3538
|
+
classification: finding.owasp ?? "",
|
|
3539
|
+
reference: finding.cwe ? `https://cwe.mitre.org/data/definitions/${finding.cwe.replace(/[^\d]/g, "")}.html` : ""
|
|
3540
|
+
},
|
|
3541
|
+
file: { path: finding.file },
|
|
3542
|
+
log: { level: finding.severity },
|
|
3543
|
+
message: `${finding.rule}: ${finding.title}`,
|
|
3544
|
+
xploitscan: commonFields(result, finding)
|
|
3545
|
+
};
|
|
3546
|
+
console.log(JSON.stringify(event));
|
|
3547
|
+
}
|
|
3548
|
+
}
|
|
3549
|
+
function renderDatadogReport(result) {
|
|
3550
|
+
for (const finding of result.findings) {
|
|
3551
|
+
const event = {
|
|
3552
|
+
ddsource: "xploitscan",
|
|
3553
|
+
ddtags: [
|
|
3554
|
+
`severity:${finding.severity}`,
|
|
3555
|
+
`rule:${finding.rule}`,
|
|
3556
|
+
`category:${finding.category.toLowerCase().replace(/\s+/g, "_")}`,
|
|
3557
|
+
finding.owasp ? `owasp:${finding.owasp.replace(/\s+/g, "_")}` : "",
|
|
3558
|
+
finding.cwe ? `cwe:${finding.cwe}` : ""
|
|
3559
|
+
].filter(Boolean).join(","),
|
|
3560
|
+
service: "xploitscan",
|
|
3561
|
+
hostname: "xploitscan-cli",
|
|
3562
|
+
status: DATADOG_STATUS[finding.severity] ?? "info",
|
|
3563
|
+
message: `${finding.rule}: ${finding.title} \u2014 ${finding.description}`,
|
|
3564
|
+
timestamp: result.timestamp,
|
|
3565
|
+
xploitscan: commonFields(result, finding)
|
|
3566
|
+
};
|
|
3567
|
+
console.log(JSON.stringify(event));
|
|
3568
|
+
}
|
|
3569
|
+
}
|
|
3570
|
+
|
|
3459
3571
|
// src/commands/scan.ts
|
|
3460
3572
|
async function scanCommand(directory, options) {
|
|
3461
3573
|
const dir = resolve2(directory || ".");
|
|
@@ -3722,6 +3834,15 @@ async function scanCommand(directory, options) {
|
|
|
3722
3834
|
case "sarif":
|
|
3723
3835
|
renderSarifReport(result);
|
|
3724
3836
|
break;
|
|
3837
|
+
case "splunk-hec":
|
|
3838
|
+
renderSplunkReport(result);
|
|
3839
|
+
break;
|
|
3840
|
+
case "elastic-ecs":
|
|
3841
|
+
renderElasticReport(result);
|
|
3842
|
+
break;
|
|
3843
|
+
case "datadog-logs":
|
|
3844
|
+
renderDatadogReport(result);
|
|
3845
|
+
break;
|
|
3725
3846
|
default:
|
|
3726
3847
|
renderTerminalReport(result, fileContentsForAnalysis);
|
|
3727
3848
|
break;
|
|
@@ -4127,8 +4248,12 @@ async function cursorInstallCommand(opts = {}) {
|
|
|
4127
4248
|
var program = new Command();
|
|
4128
4249
|
program.name("xploitscan").description(
|
|
4129
4250
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
4130
|
-
).version("1.0.
|
|
4131
|
-
program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option(
|
|
4251
|
+
).version("1.0.20");
|
|
4252
|
+
program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option(
|
|
4253
|
+
"-f, --format <format>",
|
|
4254
|
+
"Output format: terminal, json, sarif, splunk-hec, elastic-ecs, datadog-logs",
|
|
4255
|
+
"terminal"
|
|
4256
|
+
).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) => {
|
|
4132
4257
|
await scanCommand(directory, {
|
|
4133
4258
|
directory,
|
|
4134
4259
|
aiAnalysis: opts.ai,
|