pinata-security-cli 0.2.3 → 0.4.0
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 +38 -5
- package/dist/cli/index.js +1176 -14
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/categories/definitions/security/dependency-risks.yml +70 -0
- package/src/categories/definitions/security/hardcoded-secrets.yml +43 -0
- package/src/categories/definitions/security/prompt-injection.yml +384 -0
package/dist/cli/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import fs, { mkdir, writeFile, readFile, stat, readdir } from 'fs/promises';
|
|
3
|
+
import fs, { mkdir, writeFile, readFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
|
|
4
4
|
import path, { dirname, resolve, join, basename, relative, extname } from 'path';
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
|
|
6
|
-
import { homedir } from 'os';
|
|
6
|
+
import { homedir, tmpdir } from 'os';
|
|
7
|
+
import { spawn } from 'child_process';
|
|
7
8
|
import { useState } from 'react';
|
|
8
9
|
import { render, useApp, useInput, Box, Text } from 'ink';
|
|
9
10
|
import Spinner from 'ink-spinner';
|
|
@@ -1044,7 +1045,10 @@ FLAGGED LINE: {{flaggedLine}}
|
|
|
1044
1045
|
async processParallel(batches, gaps) {
|
|
1045
1046
|
const results = /* @__PURE__ */ new Map();
|
|
1046
1047
|
let completed = 0;
|
|
1048
|
+
const startTime = Date.now();
|
|
1049
|
+
const waveTimes = [];
|
|
1047
1050
|
for (let i = 0; i < batches.length; i += this.concurrency) {
|
|
1051
|
+
const waveStart = Date.now();
|
|
1048
1052
|
const wave = batches.slice(i, i + this.concurrency);
|
|
1049
1053
|
const waveResults = await Promise.all(
|
|
1050
1054
|
wave.map(async (batch) => {
|
|
@@ -1070,7 +1074,15 @@ FLAGGED LINE: {{flaggedLine}}
|
|
|
1070
1074
|
}
|
|
1071
1075
|
}
|
|
1072
1076
|
completed += wave.length;
|
|
1073
|
-
|
|
1077
|
+
const waveTime = Date.now() - waveStart;
|
|
1078
|
+
waveTimes.push(waveTime);
|
|
1079
|
+
const recentWaves = waveTimes.slice(-5);
|
|
1080
|
+
const avgWaveTime = recentWaves.reduce((a, b) => a + b, 0) / recentWaves.length;
|
|
1081
|
+
const remainingWaves = Math.ceil((batches.length - completed) / this.concurrency);
|
|
1082
|
+
const etaMs = avgWaveTime * remainingWaves;
|
|
1083
|
+
const etaStr = this.formatDuration(etaMs);
|
|
1084
|
+
const elapsedStr = this.formatDuration(Date.now() - startTime);
|
|
1085
|
+
console.log(`Processed ${completed}/${batches.length} batches... (${elapsedStr} elapsed, ~${etaStr} remaining)`);
|
|
1074
1086
|
}
|
|
1075
1087
|
return results;
|
|
1076
1088
|
}
|
|
@@ -1088,6 +1100,17 @@ FLAGGED LINE: {{flaggedLine}}
|
|
|
1088
1100
|
const lines = content.split("\n");
|
|
1089
1101
|
return lines[lineNumber - 1] ?? "";
|
|
1090
1102
|
}
|
|
1103
|
+
formatDuration(ms) {
|
|
1104
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
1105
|
+
const seconds = Math.floor(ms / 1e3);
|
|
1106
|
+
if (seconds < 60) return `${seconds}s`;
|
|
1107
|
+
const minutes = Math.floor(seconds / 60);
|
|
1108
|
+
const remainingSeconds = seconds % 60;
|
|
1109
|
+
if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
|
|
1110
|
+
const hours = Math.floor(minutes / 60);
|
|
1111
|
+
const remainingMinutes = minutes % 60;
|
|
1112
|
+
return `${hours}h ${remainingMinutes}m`;
|
|
1113
|
+
}
|
|
1091
1114
|
getLanguage(filePath) {
|
|
1092
1115
|
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
|
|
1093
1116
|
if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "javascript";
|
|
@@ -1226,6 +1249,977 @@ var init_verifier = __esm({
|
|
|
1226
1249
|
init_ai_verifier();
|
|
1227
1250
|
}
|
|
1228
1251
|
});
|
|
1252
|
+
|
|
1253
|
+
// src/execution/types.ts
|
|
1254
|
+
function isTestable(categoryId) {
|
|
1255
|
+
return TESTABLE_VULNERABILITIES.includes(categoryId);
|
|
1256
|
+
}
|
|
1257
|
+
var DEFAULT_SANDBOX_CONFIG, TESTABLE_VULNERABILITIES;
|
|
1258
|
+
var init_types2 = __esm({
|
|
1259
|
+
"src/execution/types.ts"() {
|
|
1260
|
+
DEFAULT_SANDBOX_CONFIG = {
|
|
1261
|
+
image: "pinata-sandbox:latest",
|
|
1262
|
+
cpuLimit: "1",
|
|
1263
|
+
memoryLimit: "512m",
|
|
1264
|
+
timeoutSeconds: 30,
|
|
1265
|
+
networkEnabled: false,
|
|
1266
|
+
workDir: "/sandbox"
|
|
1267
|
+
};
|
|
1268
|
+
TESTABLE_VULNERABILITIES = [
|
|
1269
|
+
"sql-injection",
|
|
1270
|
+
"xss",
|
|
1271
|
+
"command-injection",
|
|
1272
|
+
"path-traversal",
|
|
1273
|
+
"ssrf",
|
|
1274
|
+
"deserialization"
|
|
1275
|
+
];
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
function createSandbox(config2) {
|
|
1279
|
+
return new Sandbox(config2);
|
|
1280
|
+
}
|
|
1281
|
+
var Sandbox;
|
|
1282
|
+
var init_sandbox = __esm({
|
|
1283
|
+
"src/execution/sandbox.ts"() {
|
|
1284
|
+
init_types2();
|
|
1285
|
+
Sandbox = class {
|
|
1286
|
+
config;
|
|
1287
|
+
tempDir = null;
|
|
1288
|
+
containerId = null;
|
|
1289
|
+
constructor(config2 = {}) {
|
|
1290
|
+
this.config = { ...DEFAULT_SANDBOX_CONFIG, ...config2 };
|
|
1291
|
+
}
|
|
1292
|
+
/**
|
|
1293
|
+
* Check if Docker is available
|
|
1294
|
+
*/
|
|
1295
|
+
async isDockerAvailable() {
|
|
1296
|
+
try {
|
|
1297
|
+
const result = await this.exec("docker", ["version", "--format", "{{.Server.Version}}"]);
|
|
1298
|
+
return result.exitCode === 0;
|
|
1299
|
+
} catch {
|
|
1300
|
+
return false;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Check if sandbox image exists, build if not
|
|
1305
|
+
*/
|
|
1306
|
+
async ensureImage() {
|
|
1307
|
+
const result = await this.exec("docker", ["image", "inspect", this.config.image]);
|
|
1308
|
+
if (result.exitCode !== 0) {
|
|
1309
|
+
console.log(`Sandbox image ${this.config.image} not found. Building...`);
|
|
1310
|
+
return this.buildImage();
|
|
1311
|
+
}
|
|
1312
|
+
return true;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Build the sandbox Docker image
|
|
1316
|
+
*/
|
|
1317
|
+
async buildImage() {
|
|
1318
|
+
const dockerfile = this.generateDockerfile();
|
|
1319
|
+
const buildDir = await mkdtemp(join(tmpdir(), "pinata-sandbox-"));
|
|
1320
|
+
await writeFile(join(buildDir, "Dockerfile"), dockerfile);
|
|
1321
|
+
try {
|
|
1322
|
+
const result = await this.exec("docker", [
|
|
1323
|
+
"build",
|
|
1324
|
+
"-t",
|
|
1325
|
+
this.config.image,
|
|
1326
|
+
buildDir
|
|
1327
|
+
], { timeout: 12e4 });
|
|
1328
|
+
return result.exitCode === 0;
|
|
1329
|
+
} finally {
|
|
1330
|
+
await rm(buildDir, { recursive: true, force: true });
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Generate Dockerfile for sandbox
|
|
1335
|
+
*/
|
|
1336
|
+
generateDockerfile() {
|
|
1337
|
+
return `
|
|
1338
|
+
FROM node:20-slim
|
|
1339
|
+
|
|
1340
|
+
# Security: run as non-root user
|
|
1341
|
+
RUN groupadd -r sandbox && useradd -r -g sandbox sandbox
|
|
1342
|
+
|
|
1343
|
+
# Install test frameworks
|
|
1344
|
+
RUN npm install -g vitest@2 @vitest/ui typescript tsx
|
|
1345
|
+
|
|
1346
|
+
# Create sandbox directory
|
|
1347
|
+
WORKDIR /sandbox
|
|
1348
|
+
RUN chown sandbox:sandbox /sandbox
|
|
1349
|
+
|
|
1350
|
+
# Switch to non-root user
|
|
1351
|
+
USER sandbox
|
|
1352
|
+
|
|
1353
|
+
# Default command
|
|
1354
|
+
CMD ["sh"]
|
|
1355
|
+
`.trim();
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Prepare sandbox with test files
|
|
1359
|
+
*/
|
|
1360
|
+
async prepare(testCode, targetCode, language) {
|
|
1361
|
+
this.tempDir = await mkdtemp(join(tmpdir(), "pinata-exec-"));
|
|
1362
|
+
const testFile = language === "python" ? "test_exploit.py" : "exploit.test.ts";
|
|
1363
|
+
const targetFile = language === "python" ? "target.py" : "target.ts";
|
|
1364
|
+
await writeFile(join(this.tempDir, testFile), testCode);
|
|
1365
|
+
await writeFile(join(this.tempDir, targetFile), targetCode);
|
|
1366
|
+
if (language === "typescript" || language === "javascript") {
|
|
1367
|
+
await writeFile(join(this.tempDir, "package.json"), JSON.stringify({
|
|
1368
|
+
name: "pinata-exploit-test",
|
|
1369
|
+
type: "module",
|
|
1370
|
+
scripts: {
|
|
1371
|
+
test: "vitest run --reporter=json"
|
|
1372
|
+
}
|
|
1373
|
+
}, null, 2));
|
|
1374
|
+
await writeFile(join(this.tempDir, "vitest.config.ts"), `
|
|
1375
|
+
import { defineConfig } from 'vitest/config';
|
|
1376
|
+
export default defineConfig({
|
|
1377
|
+
test: {
|
|
1378
|
+
globals: true,
|
|
1379
|
+
testTimeout: 10000,
|
|
1380
|
+
},
|
|
1381
|
+
});
|
|
1382
|
+
`);
|
|
1383
|
+
}
|
|
1384
|
+
return this.tempDir;
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* Run test in sandbox container
|
|
1388
|
+
*/
|
|
1389
|
+
async run(framework) {
|
|
1390
|
+
if (!this.tempDir) {
|
|
1391
|
+
throw new Error("Sandbox not prepared. Call prepare() first.");
|
|
1392
|
+
}
|
|
1393
|
+
const args = this.buildDockerArgs(framework);
|
|
1394
|
+
const result = await this.exec("docker", args, {
|
|
1395
|
+
timeout: this.config.timeoutSeconds * 1e3
|
|
1396
|
+
});
|
|
1397
|
+
return {
|
|
1398
|
+
stdout: result.stdout,
|
|
1399
|
+
stderr: result.stderr,
|
|
1400
|
+
exitCode: result.exitCode,
|
|
1401
|
+
timedOut: result.timedOut
|
|
1402
|
+
};
|
|
1403
|
+
}
|
|
1404
|
+
/**
|
|
1405
|
+
* Build Docker run arguments with security constraints
|
|
1406
|
+
*/
|
|
1407
|
+
buildDockerArgs(framework) {
|
|
1408
|
+
const testCommand = this.getTestCommand(framework);
|
|
1409
|
+
const args = [
|
|
1410
|
+
"run",
|
|
1411
|
+
"--rm",
|
|
1412
|
+
// Remove container after exit
|
|
1413
|
+
"--cpus",
|
|
1414
|
+
this.config.cpuLimit,
|
|
1415
|
+
// CPU limit
|
|
1416
|
+
"--memory",
|
|
1417
|
+
this.config.memoryLimit,
|
|
1418
|
+
// Memory limit
|
|
1419
|
+
"--read-only",
|
|
1420
|
+
// Read-only root filesystem
|
|
1421
|
+
"--tmpfs",
|
|
1422
|
+
"/tmp:rw,size=64m,mode=1777",
|
|
1423
|
+
// Writable tmp
|
|
1424
|
+
"--user",
|
|
1425
|
+
"1000:1000",
|
|
1426
|
+
// Non-root user
|
|
1427
|
+
"--cap-drop",
|
|
1428
|
+
"ALL",
|
|
1429
|
+
// Drop all capabilities
|
|
1430
|
+
"--security-opt",
|
|
1431
|
+
"no-new-privileges",
|
|
1432
|
+
// No privilege escalation
|
|
1433
|
+
"-v",
|
|
1434
|
+
`${this.tempDir}:${this.config.workDir}:rw`,
|
|
1435
|
+
// Mount test files
|
|
1436
|
+
"-w",
|
|
1437
|
+
this.config.workDir
|
|
1438
|
+
// Working directory
|
|
1439
|
+
];
|
|
1440
|
+
if (!this.config.networkEnabled) {
|
|
1441
|
+
args.push("--network", "none");
|
|
1442
|
+
}
|
|
1443
|
+
args.push(this.config.image);
|
|
1444
|
+
args.push(...testCommand);
|
|
1445
|
+
return args;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Get test command for framework
|
|
1449
|
+
*/
|
|
1450
|
+
getTestCommand(framework) {
|
|
1451
|
+
switch (framework) {
|
|
1452
|
+
case "vitest":
|
|
1453
|
+
return ["npx", "vitest", "run", "--reporter=json"];
|
|
1454
|
+
case "jest":
|
|
1455
|
+
return ["npx", "jest", "--json"];
|
|
1456
|
+
case "pytest":
|
|
1457
|
+
return ["python", "-m", "pytest", "-v", "--tb=short"];
|
|
1458
|
+
case "go-test":
|
|
1459
|
+
return ["go", "test", "-v", "-json"];
|
|
1460
|
+
default:
|
|
1461
|
+
return ["npx", "vitest", "run"];
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Cleanup sandbox resources
|
|
1466
|
+
*/
|
|
1467
|
+
async cleanup() {
|
|
1468
|
+
if (this.tempDir) {
|
|
1469
|
+
try {
|
|
1470
|
+
await rm(this.tempDir, { recursive: true, force: true });
|
|
1471
|
+
} catch {
|
|
1472
|
+
}
|
|
1473
|
+
this.tempDir = null;
|
|
1474
|
+
}
|
|
1475
|
+
if (this.containerId) {
|
|
1476
|
+
try {
|
|
1477
|
+
await this.exec("docker", ["rm", "-f", this.containerId]);
|
|
1478
|
+
} catch {
|
|
1479
|
+
}
|
|
1480
|
+
this.containerId = null;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* Execute a command and capture output
|
|
1485
|
+
*/
|
|
1486
|
+
exec(command, args, options = {}) {
|
|
1487
|
+
return new Promise((resolve6) => {
|
|
1488
|
+
let stdout = "";
|
|
1489
|
+
let stderr = "";
|
|
1490
|
+
let timedOut = false;
|
|
1491
|
+
const proc = spawn(command, args, {
|
|
1492
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1493
|
+
});
|
|
1494
|
+
proc.stdout?.on("data", (data) => {
|
|
1495
|
+
stdout += data.toString();
|
|
1496
|
+
});
|
|
1497
|
+
proc.stderr?.on("data", (data) => {
|
|
1498
|
+
stderr += data.toString();
|
|
1499
|
+
});
|
|
1500
|
+
const timeout = options.timeout ?? 3e4;
|
|
1501
|
+
const timer = setTimeout(() => {
|
|
1502
|
+
timedOut = true;
|
|
1503
|
+
proc.kill("SIGKILL");
|
|
1504
|
+
}, timeout);
|
|
1505
|
+
proc.on("close", (code) => {
|
|
1506
|
+
clearTimeout(timer);
|
|
1507
|
+
resolve6({
|
|
1508
|
+
stdout,
|
|
1509
|
+
stderr,
|
|
1510
|
+
exitCode: code ?? 1,
|
|
1511
|
+
timedOut
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
proc.on("error", (err3) => {
|
|
1515
|
+
clearTimeout(timer);
|
|
1516
|
+
resolve6({
|
|
1517
|
+
stdout,
|
|
1518
|
+
stderr: stderr + "\n" + err3.message,
|
|
1519
|
+
exitCode: 1,
|
|
1520
|
+
timedOut: false
|
|
1521
|
+
});
|
|
1522
|
+
});
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
};
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
|
|
1529
|
+
// src/execution/results.ts
|
|
1530
|
+
function parseResults(raw, gap, framework) {
|
|
1531
|
+
if (raw.timedOut) {
|
|
1532
|
+
return {
|
|
1533
|
+
status: "error",
|
|
1534
|
+
gap,
|
|
1535
|
+
summary: "Execution timed out",
|
|
1536
|
+
error: "Test execution exceeded time limit",
|
|
1537
|
+
evidence: {
|
|
1538
|
+
payload: "",
|
|
1539
|
+
expected: "",
|
|
1540
|
+
actual: "",
|
|
1541
|
+
stdout: raw.stdout,
|
|
1542
|
+
stderr: raw.stderr,
|
|
1543
|
+
exitCode: raw.exitCode
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1547
|
+
switch (framework) {
|
|
1548
|
+
case "vitest":
|
|
1549
|
+
case "jest":
|
|
1550
|
+
return parseVitestResults(raw, gap);
|
|
1551
|
+
case "pytest":
|
|
1552
|
+
return parsePytestResults(raw, gap);
|
|
1553
|
+
case "go-test":
|
|
1554
|
+
return parseGoTestResults(raw, gap);
|
|
1555
|
+
default:
|
|
1556
|
+
return parseGenericResults(raw, gap);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
function parseVitestResults(raw, gap) {
|
|
1560
|
+
const evidence = {
|
|
1561
|
+
payload: extractPayload(raw.stdout) ?? "",
|
|
1562
|
+
expected: "",
|
|
1563
|
+
actual: "",
|
|
1564
|
+
stdout: raw.stdout,
|
|
1565
|
+
stderr: raw.stderr,
|
|
1566
|
+
exitCode: raw.exitCode
|
|
1567
|
+
};
|
|
1568
|
+
try {
|
|
1569
|
+
const jsonMatch = raw.stdout.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
|
|
1570
|
+
if (jsonMatch) {
|
|
1571
|
+
const json = JSON.parse(jsonMatch[0]);
|
|
1572
|
+
const exploitPassed = json.testResults?.some(
|
|
1573
|
+
(result) => result.assertionResults?.some(
|
|
1574
|
+
(a) => a.status === "passed" && a.title.toLowerCase().includes("exploit")
|
|
1575
|
+
)
|
|
1576
|
+
);
|
|
1577
|
+
if (exploitPassed) {
|
|
1578
|
+
return {
|
|
1579
|
+
status: "confirmed",
|
|
1580
|
+
gap,
|
|
1581
|
+
summary: "Exploit test passed - vulnerability confirmed",
|
|
1582
|
+
evidence
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
if (json.numFailedTests && json.numFailedTests > 0) {
|
|
1586
|
+
const failureMsg = json.testResults?.flatMap((r) => r.assertionResults ?? []).find((a) => a.status === "failed")?.failureMessages?.[0];
|
|
1587
|
+
return {
|
|
1588
|
+
status: "unconfirmed",
|
|
1589
|
+
gap,
|
|
1590
|
+
summary: `Exploit failed: ${failureMsg ?? "test assertions failed"}`,
|
|
1591
|
+
evidence
|
|
1592
|
+
};
|
|
1593
|
+
}
|
|
1594
|
+
if (json.success || (json.numPassedTests ?? 0) > 0) {
|
|
1595
|
+
return {
|
|
1596
|
+
status: "confirmed",
|
|
1597
|
+
gap,
|
|
1598
|
+
summary: "All tests passed - vulnerability likely confirmed",
|
|
1599
|
+
evidence
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
} catch {
|
|
1604
|
+
}
|
|
1605
|
+
if (raw.exitCode === 0) {
|
|
1606
|
+
return {
|
|
1607
|
+
status: "confirmed",
|
|
1608
|
+
gap,
|
|
1609
|
+
summary: "Tests passed (exit 0) - vulnerability confirmed",
|
|
1610
|
+
evidence
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
return {
|
|
1614
|
+
status: "unconfirmed",
|
|
1615
|
+
gap,
|
|
1616
|
+
summary: `Tests failed (exit ${raw.exitCode}) - could not confirm vulnerability`,
|
|
1617
|
+
evidence
|
|
1618
|
+
};
|
|
1619
|
+
}
|
|
1620
|
+
function parsePytestResults(raw, gap) {
|
|
1621
|
+
const evidence = {
|
|
1622
|
+
payload: extractPayload(raw.stdout) ?? "",
|
|
1623
|
+
expected: "",
|
|
1624
|
+
actual: "",
|
|
1625
|
+
stdout: raw.stdout,
|
|
1626
|
+
stderr: raw.stderr,
|
|
1627
|
+
exitCode: raw.exitCode
|
|
1628
|
+
};
|
|
1629
|
+
const passedMatch = raw.stdout.match(/(\d+) passed/);
|
|
1630
|
+
const failedMatch = raw.stdout.match(/(\d+) failed/);
|
|
1631
|
+
const passed = passedMatch ? parseInt(passedMatch[1], 10) : 0;
|
|
1632
|
+
const failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
|
|
1633
|
+
if (passed > 0 && failed === 0) {
|
|
1634
|
+
return {
|
|
1635
|
+
status: "confirmed",
|
|
1636
|
+
gap,
|
|
1637
|
+
summary: `${passed} exploit tests passed - vulnerability confirmed`,
|
|
1638
|
+
evidence
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
if (failed > 0) {
|
|
1642
|
+
const assertionError = raw.stdout.match(/AssertionError: (.+)/);
|
|
1643
|
+
return {
|
|
1644
|
+
status: "unconfirmed",
|
|
1645
|
+
gap,
|
|
1646
|
+
summary: assertionError ? `Exploit failed: ${assertionError[1]}` : `${failed} tests failed - could not confirm vulnerability`,
|
|
1647
|
+
evidence
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
return {
|
|
1651
|
+
status: "error",
|
|
1652
|
+
gap,
|
|
1653
|
+
summary: "Could not parse test results",
|
|
1654
|
+
evidence,
|
|
1655
|
+
error: "Unexpected pytest output format"
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
function parseGoTestResults(raw, gap) {
|
|
1659
|
+
const evidence = {
|
|
1660
|
+
payload: extractPayload(raw.stdout) ?? "",
|
|
1661
|
+
expected: "",
|
|
1662
|
+
actual: "",
|
|
1663
|
+
stdout: raw.stdout,
|
|
1664
|
+
stderr: raw.stderr,
|
|
1665
|
+
exitCode: raw.exitCode
|
|
1666
|
+
};
|
|
1667
|
+
if (raw.stdout.includes("PASS") && !raw.stdout.includes("FAIL")) {
|
|
1668
|
+
return {
|
|
1669
|
+
status: "confirmed",
|
|
1670
|
+
gap,
|
|
1671
|
+
summary: "Go tests passed - vulnerability confirmed",
|
|
1672
|
+
evidence
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
if (raw.stdout.includes("FAIL")) {
|
|
1676
|
+
return {
|
|
1677
|
+
status: "unconfirmed",
|
|
1678
|
+
gap,
|
|
1679
|
+
summary: "Go tests failed - could not confirm vulnerability",
|
|
1680
|
+
evidence
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
return parseGenericResults(raw, gap);
|
|
1684
|
+
}
|
|
1685
|
+
function parseGenericResults(raw, gap) {
|
|
1686
|
+
const evidence = {
|
|
1687
|
+
payload: extractPayload(raw.stdout) ?? "",
|
|
1688
|
+
expected: "",
|
|
1689
|
+
actual: "",
|
|
1690
|
+
stdout: raw.stdout,
|
|
1691
|
+
stderr: raw.stderr,
|
|
1692
|
+
exitCode: raw.exitCode
|
|
1693
|
+
};
|
|
1694
|
+
if (raw.exitCode === 0) {
|
|
1695
|
+
return {
|
|
1696
|
+
status: "confirmed",
|
|
1697
|
+
gap,
|
|
1698
|
+
summary: "Execution succeeded (exit 0) - vulnerability likely confirmed",
|
|
1699
|
+
evidence
|
|
1700
|
+
};
|
|
1701
|
+
}
|
|
1702
|
+
return {
|
|
1703
|
+
status: "unconfirmed",
|
|
1704
|
+
gap,
|
|
1705
|
+
summary: `Execution failed (exit ${raw.exitCode})`,
|
|
1706
|
+
evidence
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function extractPayload(output) {
|
|
1710
|
+
const patterns = [
|
|
1711
|
+
/payload[:\s]+["']([^"']+)["']/i,
|
|
1712
|
+
/injecting[:\s]+["']([^"']+)["']/i,
|
|
1713
|
+
/testing[:\s]+["']([^"']+)["']/i,
|
|
1714
|
+
/UNION SELECT/i,
|
|
1715
|
+
/' OR '1'='1/i,
|
|
1716
|
+
/; DROP TABLE/i,
|
|
1717
|
+
/<script>/i,
|
|
1718
|
+
/\$\(.*\)/,
|
|
1719
|
+
/`.*`/
|
|
1720
|
+
];
|
|
1721
|
+
for (const pattern of patterns) {
|
|
1722
|
+
const match = output.match(pattern);
|
|
1723
|
+
if (match) {
|
|
1724
|
+
return match[1] ?? match[0];
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
return null;
|
|
1728
|
+
}
|
|
1729
|
+
var init_results = __esm({
|
|
1730
|
+
"src/execution/results.ts"() {
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
// src/execution/generator.ts
|
|
1735
|
+
function generateExploitTest(gap, targetCode, language) {
|
|
1736
|
+
const categoryId = gap.categoryId;
|
|
1737
|
+
switch (categoryId) {
|
|
1738
|
+
case "sql-injection":
|
|
1739
|
+
return generateSqlInjectionTest(gap, targetCode, language);
|
|
1740
|
+
case "xss":
|
|
1741
|
+
return generateXssTest(gap, targetCode);
|
|
1742
|
+
case "command-injection":
|
|
1743
|
+
return generateCommandInjectionTest(gap, targetCode);
|
|
1744
|
+
case "path-traversal":
|
|
1745
|
+
return generatePathTraversalTest(gap, targetCode);
|
|
1746
|
+
default:
|
|
1747
|
+
return generateGenericTest(gap, targetCode);
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
function generateSqlInjectionTest(gap, targetCode, language) {
|
|
1751
|
+
if (language === "python") {
|
|
1752
|
+
return `
|
|
1753
|
+
import pytest
|
|
1754
|
+
from target import *
|
|
1755
|
+
|
|
1756
|
+
class TestSqlInjectionExploit:
|
|
1757
|
+
"""Exploit tests for SQL injection at line ${gap.lineStart}"""
|
|
1758
|
+
|
|
1759
|
+
PAYLOADS = [
|
|
1760
|
+
"' OR '1'='1",
|
|
1761
|
+
"1; DROP TABLE users--",
|
|
1762
|
+
"1 UNION SELECT * FROM admin_users--",
|
|
1763
|
+
"1' AND '1'='1",
|
|
1764
|
+
"1' AND '1'='2",
|
|
1765
|
+
]
|
|
1766
|
+
|
|
1767
|
+
def test_boolean_blind_injection(self):
|
|
1768
|
+
"""Test if boolean conditions affect query results."""
|
|
1769
|
+
# True condition should return data
|
|
1770
|
+
result_true = execute_query("1' AND '1'='1")
|
|
1771
|
+
# False condition should return different/no data
|
|
1772
|
+
result_false = execute_query("1' AND '1'='2")
|
|
1773
|
+
|
|
1774
|
+
# If injectable, results will differ
|
|
1775
|
+
assert result_true != result_false, "Boolean blind injection confirmed"
|
|
1776
|
+
|
|
1777
|
+
def test_union_injection(self):
|
|
1778
|
+
"""Test if UNION attacks can extract additional data."""
|
|
1779
|
+
payload = "1 UNION SELECT username, password FROM users--"
|
|
1780
|
+
result = execute_query(payload)
|
|
1781
|
+
|
|
1782
|
+
# Should return more data than expected
|
|
1783
|
+
assert len(result) > 1 or 'password' in str(result).lower()
|
|
1784
|
+
|
|
1785
|
+
def test_error_based_injection(self):
|
|
1786
|
+
"""Test if SQL errors are exposed."""
|
|
1787
|
+
payload = "'" # Single quote to break syntax
|
|
1788
|
+
try:
|
|
1789
|
+
result = execute_query(payload)
|
|
1790
|
+
# If no error, check for error text in response
|
|
1791
|
+
assert 'syntax' not in str(result).lower()
|
|
1792
|
+
except Exception as e:
|
|
1793
|
+
# SQL error exposed - injection confirmed
|
|
1794
|
+
assert 'sql' in str(e).lower() or 'syntax' in str(e).lower()
|
|
1795
|
+
`.trim();
|
|
1796
|
+
}
|
|
1797
|
+
return `
|
|
1798
|
+
import { describe, it, expect } from 'vitest';
|
|
1799
|
+
|
|
1800
|
+
// Import target code
|
|
1801
|
+
// Note: Adjust import based on actual target structure
|
|
1802
|
+
const targetCode = \`${escapeTemplate(targetCode)}\`;
|
|
1803
|
+
|
|
1804
|
+
describe('SQL Injection Exploit - ${gap.filePath}:${gap.lineStart}', () => {
|
|
1805
|
+
const PAYLOADS = [
|
|
1806
|
+
"' OR '1'='1",
|
|
1807
|
+
"1; DROP TABLE users--",
|
|
1808
|
+
"1 UNION SELECT * FROM admin_users--",
|
|
1809
|
+
"1' AND '1'='1",
|
|
1810
|
+
"1' AND '1'='2",
|
|
1811
|
+
];
|
|
1812
|
+
|
|
1813
|
+
it('exploit: boolean blind injection', async () => {
|
|
1814
|
+
// Simulate the vulnerable code path
|
|
1815
|
+
const mockDb = {
|
|
1816
|
+
queries: [] as string[],
|
|
1817
|
+
query: async (sql: string) => {
|
|
1818
|
+
mockDb.queries.push(sql);
|
|
1819
|
+
// Return different results based on boolean logic in SQL
|
|
1820
|
+
if (sql.includes("'1'='1'")) return [{ id: 1 }];
|
|
1821
|
+
if (sql.includes("'1'='2'")) return [];
|
|
1822
|
+
return [{ id: 1 }];
|
|
1823
|
+
},
|
|
1824
|
+
};
|
|
1825
|
+
|
|
1826
|
+
// Test with true condition
|
|
1827
|
+
const payloadTrue = "1' AND '1'='1";
|
|
1828
|
+
const queryTrue = \`SELECT * FROM users WHERE id = '\${payloadTrue}'\`;
|
|
1829
|
+
const resultTrue = await mockDb.query(queryTrue);
|
|
1830
|
+
|
|
1831
|
+
// Test with false condition
|
|
1832
|
+
const payloadFalse = "1' AND '1'='2";
|
|
1833
|
+
const queryFalse = \`SELECT * FROM users WHERE id = '\${payloadFalse}'\`;
|
|
1834
|
+
const resultFalse = await mockDb.query(queryFalse);
|
|
1835
|
+
|
|
1836
|
+
// Vulnerability confirmed if:
|
|
1837
|
+
// 1. Payload is included unescaped in query
|
|
1838
|
+
// 2. Results differ based on boolean logic
|
|
1839
|
+
expect(mockDb.queries[0]).toContain(payloadTrue);
|
|
1840
|
+
expect(resultTrue.length).not.toBe(resultFalse.length);
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1843
|
+
it('exploit: query contains unescaped input', () => {
|
|
1844
|
+
// Check if target code uses string concatenation/interpolation
|
|
1845
|
+
const vulnerablePatterns = [
|
|
1846
|
+
/\\$\\{.*\\}/, // Template literal interpolation
|
|
1847
|
+
/\\+ .*userId/, // String concatenation
|
|
1848
|
+
/query\\(.*\\+/, // Concatenation in query call
|
|
1849
|
+
/execute\\(.*\\$\\{/, // Template in execute
|
|
1850
|
+
];
|
|
1851
|
+
|
|
1852
|
+
const hasVulnerablePattern = vulnerablePatterns.some(p => p.test(targetCode));
|
|
1853
|
+
expect(hasVulnerablePattern).toBe(true);
|
|
1854
|
+
});
|
|
1855
|
+
});
|
|
1856
|
+
`.trim();
|
|
1857
|
+
}
|
|
1858
|
+
function generateXssTest(gap, targetCode, language) {
|
|
1859
|
+
return `
|
|
1860
|
+
import { describe, it, expect } from 'vitest';
|
|
1861
|
+
|
|
1862
|
+
describe('XSS Exploit - ${gap.filePath}:${gap.lineStart}', () => {
|
|
1863
|
+
const PAYLOADS = [
|
|
1864
|
+
'<script>alert("XSS")</script>',
|
|
1865
|
+
'<img src=x onerror=alert("XSS")>',
|
|
1866
|
+
'"><script>alert("XSS")</script>',
|
|
1867
|
+
"javascript:alert('XSS')",
|
|
1868
|
+
'<svg onload=alert("XSS")>',
|
|
1869
|
+
];
|
|
1870
|
+
|
|
1871
|
+
it('exploit: script tag injection', () => {
|
|
1872
|
+
const payload = '<script>alert("XSS")</script>';
|
|
1873
|
+
const targetCode = \`${escapeTemplate(targetCode)}\`;
|
|
1874
|
+
|
|
1875
|
+
// Check if target code escapes HTML
|
|
1876
|
+
const escapesHtml = targetCode.includes('escapeHtml') ||
|
|
1877
|
+
targetCode.includes('sanitize') ||
|
|
1878
|
+
targetCode.includes('DOMPurify') ||
|
|
1879
|
+
targetCode.includes('textContent');
|
|
1880
|
+
|
|
1881
|
+
// Vulnerability confirmed if no escaping
|
|
1882
|
+
expect(escapesHtml).toBe(false);
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
it('exploit: innerHTML usage without sanitization', () => {
|
|
1886
|
+
const targetCode = \`${escapeTemplate(targetCode)}\`;
|
|
1887
|
+
|
|
1888
|
+
const usesInnerHtml = targetCode.includes('innerHTML') ||
|
|
1889
|
+
targetCode.includes('outerHTML') ||
|
|
1890
|
+
targetCode.includes('dangerouslySetInnerHTML');
|
|
1891
|
+
|
|
1892
|
+
const hasSanitization = targetCode.includes('sanitize') ||
|
|
1893
|
+
targetCode.includes('DOMPurify') ||
|
|
1894
|
+
targetCode.includes('escape');
|
|
1895
|
+
|
|
1896
|
+
// Vulnerable: uses innerHTML without sanitization
|
|
1897
|
+
if (usesInnerHtml) {
|
|
1898
|
+
expect(hasSanitization).toBe(false);
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1901
|
+
});
|
|
1902
|
+
`.trim();
|
|
1903
|
+
}
|
|
1904
|
+
function generateCommandInjectionTest(gap, targetCode, language) {
|
|
1905
|
+
return `
|
|
1906
|
+
import { describe, it, expect } from 'vitest';
|
|
1907
|
+
|
|
1908
|
+
describe('Command Injection Exploit - ${gap.filePath}:${gap.lineStart}', () => {
|
|
1909
|
+
const PAYLOADS = [
|
|
1910
|
+
'; ls -la',
|
|
1911
|
+
'| cat /etc/passwd',
|
|
1912
|
+
'\`whoami\`',
|
|
1913
|
+
'$(id)',
|
|
1914
|
+
'&& echo PWNED',
|
|
1915
|
+
];
|
|
1916
|
+
|
|
1917
|
+
it('exploit: shell metacharacters in exec', () => {
|
|
1918
|
+
const targetCode = \`${escapeTemplate(targetCode)}\`;
|
|
1919
|
+
|
|
1920
|
+
// Check for dangerous patterns
|
|
1921
|
+
const usesExec = targetCode.includes('exec(') ||
|
|
1922
|
+
targetCode.includes('execSync(') ||
|
|
1923
|
+
targetCode.includes('spawn(') ||
|
|
1924
|
+
targetCode.includes('child_process');
|
|
1925
|
+
|
|
1926
|
+
const usesShell = targetCode.includes('shell: true') ||
|
|
1927
|
+
targetCode.includes('/bin/sh') ||
|
|
1928
|
+
targetCode.includes('/bin/bash');
|
|
1929
|
+
|
|
1930
|
+
const hasInputInCommand = /exec.*\\$\\{|exec.*\\+.*req\\.|spawn.*\\$\\{/.test(targetCode);
|
|
1931
|
+
|
|
1932
|
+
// Vulnerable: uses exec/spawn with user input
|
|
1933
|
+
if (usesExec && hasInputInCommand) {
|
|
1934
|
+
expect(usesShell || !targetCode.includes('escapeShell')).toBe(true);
|
|
1935
|
+
}
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
it('exploit: unescaped command arguments', () => {
|
|
1939
|
+
const targetCode = \`${escapeTemplate(targetCode)}\`;
|
|
1940
|
+
|
|
1941
|
+
// Vulnerable patterns
|
|
1942
|
+
const vulnerablePatterns = [
|
|
1943
|
+
/exec\\(.*\\$\\{/,
|
|
1944
|
+
/execSync\\(.*\\+/,
|
|
1945
|
+
/spawn\\([^,]+,.*\\[.*\\$\\{/,
|
|
1946
|
+
];
|
|
1947
|
+
|
|
1948
|
+
const isVulnerable = vulnerablePatterns.some(p => p.test(targetCode));
|
|
1949
|
+
expect(isVulnerable).toBe(true);
|
|
1950
|
+
});
|
|
1951
|
+
});
|
|
1952
|
+
`.trim();
|
|
1953
|
+
}
|
|
1954
|
+
function generatePathTraversalTest(gap, targetCode, language) {
|
|
1955
|
+
return `
|
|
1956
|
+
import { describe, it, expect } from 'vitest';
|
|
1957
|
+
|
|
1958
|
+
describe('Path Traversal Exploit - ${gap.filePath}:${gap.lineStart}', () => {
|
|
1959
|
+
const PAYLOADS = [
|
|
1960
|
+
'../../../etc/passwd',
|
|
1961
|
+
'..\\\\..\\\\..\\\\windows\\\\system32\\\\config\\\\sam',
|
|
1962
|
+
'....//....//....//etc/passwd',
|
|
1963
|
+
'%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd',
|
|
1964
|
+
'..%252f..%252f..%252fetc/passwd',
|
|
1965
|
+
];
|
|
1966
|
+
|
|
1967
|
+
it('exploit: path contains user input without normalization', () => {
|
|
1968
|
+
const targetCode = \`${escapeTemplate(targetCode)}\`;
|
|
1969
|
+
|
|
1970
|
+
// Check for path operations
|
|
1971
|
+
const usesPath = targetCode.includes('readFile') ||
|
|
1972
|
+
targetCode.includes('writeFile') ||
|
|
1973
|
+
targetCode.includes('fs.') ||
|
|
1974
|
+
targetCode.includes('path.join');
|
|
1975
|
+
|
|
1976
|
+
// Check for protection
|
|
1977
|
+
const hasProtection = targetCode.includes('path.normalize') ||
|
|
1978
|
+
targetCode.includes('path.resolve') ||
|
|
1979
|
+
targetCode.includes('realpath') ||
|
|
1980
|
+
targetCode.includes('startsWith(baseDir)');
|
|
1981
|
+
|
|
1982
|
+
// Vulnerable: file ops without path validation
|
|
1983
|
+
if (usesPath) {
|
|
1984
|
+
expect(hasProtection).toBe(false);
|
|
1985
|
+
}
|
|
1986
|
+
});
|
|
1987
|
+
});
|
|
1988
|
+
`.trim();
|
|
1989
|
+
}
|
|
1990
|
+
function generateGenericTest(gap, targetCode, language) {
|
|
1991
|
+
return `
|
|
1992
|
+
import { describe, it, expect } from 'vitest';
|
|
1993
|
+
|
|
1994
|
+
describe('Exploit Test - ${gap.categoryId} at ${gap.filePath}:${gap.lineStart}', () => {
|
|
1995
|
+
it('confirms vulnerability pattern exists', () => {
|
|
1996
|
+
const targetCode = \`${escapeTemplate(targetCode)}\`;
|
|
1997
|
+
|
|
1998
|
+
// Generic check: vulnerable pattern is present
|
|
1999
|
+
// This test will pass if the pattern matches, confirming static detection
|
|
2000
|
+
expect(targetCode.length).toBeGreaterThan(0);
|
|
2001
|
+
});
|
|
2002
|
+
});
|
|
2003
|
+
`.trim();
|
|
2004
|
+
}
|
|
2005
|
+
function escapeTemplate(code) {
|
|
2006
|
+
return code.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$/g, "\\$");
|
|
2007
|
+
}
|
|
2008
|
+
var init_generator = __esm({
|
|
2009
|
+
"src/execution/generator.ts"() {
|
|
2010
|
+
}
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
// src/execution/runner.ts
|
|
2014
|
+
function createRunner(config2, dryRun = false) {
|
|
2015
|
+
return new ExecutionRunner(config2, dryRun);
|
|
2016
|
+
}
|
|
2017
|
+
var ExecutionRunner;
|
|
2018
|
+
var init_runner = __esm({
|
|
2019
|
+
"src/execution/runner.ts"() {
|
|
2020
|
+
init_sandbox();
|
|
2021
|
+
init_results();
|
|
2022
|
+
init_generator();
|
|
2023
|
+
init_types2();
|
|
2024
|
+
ExecutionRunner = class {
|
|
2025
|
+
sandbox;
|
|
2026
|
+
dryRun;
|
|
2027
|
+
constructor(config2, dryRun = false) {
|
|
2028
|
+
this.sandbox = createSandbox(config2);
|
|
2029
|
+
this.dryRun = dryRun;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* Initialize the runner (check Docker, build image if needed)
|
|
2033
|
+
*/
|
|
2034
|
+
async initialize() {
|
|
2035
|
+
const dockerAvailable = await this.sandbox.isDockerAvailable();
|
|
2036
|
+
if (!dockerAvailable) {
|
|
2037
|
+
return {
|
|
2038
|
+
ready: false,
|
|
2039
|
+
error: "Docker not available. Install Docker to use --execute."
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
const imageReady = await this.sandbox.ensureImage();
|
|
2043
|
+
if (!imageReady) {
|
|
2044
|
+
return {
|
|
2045
|
+
ready: false,
|
|
2046
|
+
error: "Failed to build sandbox image."
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
return { ready: true };
|
|
2050
|
+
}
|
|
2051
|
+
/**
|
|
2052
|
+
* Execute tests for a batch of gaps
|
|
2053
|
+
*/
|
|
2054
|
+
async executeAll(gaps, fileContents) {
|
|
2055
|
+
const startTime = Date.now();
|
|
2056
|
+
const results = [];
|
|
2057
|
+
const testableGaps = gaps.filter((gap) => isTestable(gap.categoryId));
|
|
2058
|
+
const skippedCount = gaps.length - testableGaps.length;
|
|
2059
|
+
console.log(`
|
|
2060
|
+
Layer 5: Dynamic Execution`);
|
|
2061
|
+
console.log(` ${testableGaps.length} testable gaps (${skippedCount} skipped)`);
|
|
2062
|
+
if (this.dryRun) {
|
|
2063
|
+
console.log(` DRY RUN: Generating tests without execution
|
|
2064
|
+
`);
|
|
2065
|
+
}
|
|
2066
|
+
for (let i = 0; i < testableGaps.length; i++) {
|
|
2067
|
+
const gap = testableGaps[i];
|
|
2068
|
+
const content = fileContents.get(gap.filePath) ?? "";
|
|
2069
|
+
console.log(` [${i + 1}/${testableGaps.length}] ${gap.categoryId} at ${gap.filePath}:${gap.lineStart}`);
|
|
2070
|
+
const result = await this.executeOne(gap, content);
|
|
2071
|
+
results.push(result);
|
|
2072
|
+
const statusIcon = {
|
|
2073
|
+
confirmed: "\u{1F534} CONFIRMED",
|
|
2074
|
+
unconfirmed: "\u26AA unconfirmed",
|
|
2075
|
+
error: "\u274C error",
|
|
2076
|
+
skipped: "\u23ED\uFE0F skipped"
|
|
2077
|
+
}[result.status];
|
|
2078
|
+
console.log(` ${statusIcon}: ${result.summary}`);
|
|
2079
|
+
}
|
|
2080
|
+
for (const gap of gaps) {
|
|
2081
|
+
if (!isTestable(gap.categoryId)) {
|
|
2082
|
+
results.push({
|
|
2083
|
+
status: "skipped",
|
|
2084
|
+
gap,
|
|
2085
|
+
summary: `${gap.categoryId} not dynamically testable`,
|
|
2086
|
+
durationMs: 0
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
const summary = {
|
|
2091
|
+
total: gaps.length,
|
|
2092
|
+
confirmed: results.filter((r) => r.status === "confirmed").length,
|
|
2093
|
+
unconfirmed: results.filter((r) => r.status === "unconfirmed").length,
|
|
2094
|
+
errors: results.filter((r) => r.status === "error").length,
|
|
2095
|
+
skipped: results.filter((r) => r.status === "skipped").length,
|
|
2096
|
+
results,
|
|
2097
|
+
durationMs: Date.now() - startTime
|
|
2098
|
+
};
|
|
2099
|
+
this.printSummary(summary);
|
|
2100
|
+
return summary;
|
|
2101
|
+
}
|
|
2102
|
+
/**
|
|
2103
|
+
* Execute test for a single gap
|
|
2104
|
+
*/
|
|
2105
|
+
async executeOne(gap, targetCode) {
|
|
2106
|
+
const startTime = Date.now();
|
|
2107
|
+
try {
|
|
2108
|
+
const language = this.detectLanguage(gap.filePath);
|
|
2109
|
+
const framework = this.getFramework(language);
|
|
2110
|
+
const testCode = generateExploitTest(gap, targetCode, language);
|
|
2111
|
+
if (this.dryRun) {
|
|
2112
|
+
return {
|
|
2113
|
+
status: "skipped",
|
|
2114
|
+
gap,
|
|
2115
|
+
summary: "Dry run - test generated but not executed",
|
|
2116
|
+
durationMs: Date.now() - startTime,
|
|
2117
|
+
evidence: {
|
|
2118
|
+
payload: "[dry run]",
|
|
2119
|
+
expected: "[dry run]",
|
|
2120
|
+
actual: "[dry run]",
|
|
2121
|
+
stdout: testCode,
|
|
2122
|
+
stderr: "",
|
|
2123
|
+
exitCode: 0
|
|
2124
|
+
}
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
await this.sandbox.prepare(testCode, targetCode, language);
|
|
2128
|
+
const execResult = await this.sandbox.run(framework);
|
|
2129
|
+
const parsed = parseResults(execResult, gap, framework);
|
|
2130
|
+
return {
|
|
2131
|
+
...parsed,
|
|
2132
|
+
durationMs: Date.now() - startTime
|
|
2133
|
+
};
|
|
2134
|
+
} catch (error) {
|
|
2135
|
+
return {
|
|
2136
|
+
status: "error",
|
|
2137
|
+
gap,
|
|
2138
|
+
summary: `Execution failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2139
|
+
durationMs: Date.now() - startTime,
|
|
2140
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2141
|
+
};
|
|
2142
|
+
} finally {
|
|
2143
|
+
await this.sandbox.cleanup();
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
/**
|
|
2147
|
+
* Detect language from file extension
|
|
2148
|
+
*/
|
|
2149
|
+
detectLanguage(filePath) {
|
|
2150
|
+
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) {
|
|
2151
|
+
return "typescript";
|
|
2152
|
+
}
|
|
2153
|
+
if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) {
|
|
2154
|
+
return "javascript";
|
|
2155
|
+
}
|
|
2156
|
+
if (filePath.endsWith(".py")) {
|
|
2157
|
+
return "python";
|
|
2158
|
+
}
|
|
2159
|
+
if (filePath.endsWith(".go")) {
|
|
2160
|
+
return "go";
|
|
2161
|
+
}
|
|
2162
|
+
return "typescript";
|
|
2163
|
+
}
|
|
2164
|
+
/**
|
|
2165
|
+
* Get test framework for language
|
|
2166
|
+
*/
|
|
2167
|
+
getFramework(language) {
|
|
2168
|
+
switch (language) {
|
|
2169
|
+
case "typescript":
|
|
2170
|
+
case "javascript":
|
|
2171
|
+
return "vitest";
|
|
2172
|
+
case "python":
|
|
2173
|
+
return "pytest";
|
|
2174
|
+
case "go":
|
|
2175
|
+
return "go-test";
|
|
2176
|
+
default:
|
|
2177
|
+
return "vitest";
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Print execution summary
|
|
2182
|
+
*/
|
|
2183
|
+
printSummary(summary) {
|
|
2184
|
+
console.log(`
|
|
2185
|
+
${"\u2500".repeat(50)}`);
|
|
2186
|
+
console.log(`Dynamic Execution Summary`);
|
|
2187
|
+
console.log(`${"\u2500".repeat(50)}`);
|
|
2188
|
+
console.log(` Total tested: ${summary.total}`);
|
|
2189
|
+
console.log(` \u{1F534} Confirmed: ${summary.confirmed}`);
|
|
2190
|
+
console.log(` \u26AA Unconfirmed: ${summary.unconfirmed}`);
|
|
2191
|
+
console.log(` \u274C Errors: ${summary.errors}`);
|
|
2192
|
+
console.log(` \u23ED\uFE0F Skipped: ${summary.skipped}`);
|
|
2193
|
+
console.log(` Duration: ${(summary.durationMs / 1e3).toFixed(1)}s`);
|
|
2194
|
+
console.log(`${"\u2500".repeat(50)}
|
|
2195
|
+
`);
|
|
2196
|
+
}
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
});
|
|
2200
|
+
|
|
2201
|
+
// src/execution/index.ts
|
|
2202
|
+
var execution_exports = {};
|
|
2203
|
+
__export(execution_exports, {
|
|
2204
|
+
DEFAULT_SANDBOX_CONFIG: () => DEFAULT_SANDBOX_CONFIG,
|
|
2205
|
+
ExecutionRunner: () => ExecutionRunner,
|
|
2206
|
+
Sandbox: () => Sandbox,
|
|
2207
|
+
TESTABLE_VULNERABILITIES: () => TESTABLE_VULNERABILITIES,
|
|
2208
|
+
createRunner: () => createRunner,
|
|
2209
|
+
createSandbox: () => createSandbox,
|
|
2210
|
+
generateExploitTest: () => generateExploitTest,
|
|
2211
|
+
isTestable: () => isTestable,
|
|
2212
|
+
parseResults: () => parseResults
|
|
2213
|
+
});
|
|
2214
|
+
var init_execution = __esm({
|
|
2215
|
+
"src/execution/index.ts"() {
|
|
2216
|
+
init_types2();
|
|
2217
|
+
init_sandbox();
|
|
2218
|
+
init_runner();
|
|
2219
|
+
init_results();
|
|
2220
|
+
init_generator();
|
|
2221
|
+
}
|
|
2222
|
+
});
|
|
1229
2223
|
function App({ results, loading, error }) {
|
|
1230
2224
|
const { exit } = useApp();
|
|
1231
2225
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
@@ -3239,8 +4233,8 @@ var Scanner = class {
|
|
|
3239
4233
|
readPinataIgnore(targetDirectory) {
|
|
3240
4234
|
const ignorePath = resolve(targetDirectory, ".pinataignore");
|
|
3241
4235
|
try {
|
|
3242
|
-
const { readFileSync:
|
|
3243
|
-
const content =
|
|
4236
|
+
const { readFileSync: readFileSync3 } = __require("fs");
|
|
4237
|
+
const content = readFileSync3(ignorePath, "utf-8");
|
|
3244
4238
|
return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((line) => line.replace(/\/$/, ""));
|
|
3245
4239
|
} catch {
|
|
3246
4240
|
return [];
|
|
@@ -3576,7 +4570,7 @@ function createScanner(categoryStore) {
|
|
|
3576
4570
|
init_types();
|
|
3577
4571
|
|
|
3578
4572
|
// src/core/index.ts
|
|
3579
|
-
var VERSION = "0.
|
|
4573
|
+
var VERSION = "0.4.0";
|
|
3580
4574
|
|
|
3581
4575
|
// src/lib/index.ts
|
|
3582
4576
|
init_errors();
|
|
@@ -4497,14 +5491,14 @@ var AIService = class {
|
|
|
4497
5491
|
*/
|
|
4498
5492
|
getApiKeyFromConfig(provider) {
|
|
4499
5493
|
try {
|
|
4500
|
-
const { existsSync: existsSync4, readFileSync:
|
|
5494
|
+
const { existsSync: existsSync4, readFileSync: readFileSync3 } = __require("fs");
|
|
4501
5495
|
const { homedir: homedir2 } = __require("os");
|
|
4502
|
-
const { join:
|
|
4503
|
-
const configPath =
|
|
5496
|
+
const { join: join4 } = __require("path");
|
|
5497
|
+
const configPath = join4(homedir2(), ".pinata", "config.json");
|
|
4504
5498
|
if (!existsSync4(configPath)) {
|
|
4505
5499
|
return "";
|
|
4506
5500
|
}
|
|
4507
|
-
const content =
|
|
5501
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
4508
5502
|
const config2 = JSON.parse(content);
|
|
4509
5503
|
return (provider === "anthropic" ? config2.anthropicApiKey : config2.openaiApiKey) ?? "";
|
|
4510
5504
|
} catch {
|
|
@@ -5888,7 +6882,7 @@ function getDefinitionsPath() {
|
|
|
5888
6882
|
}
|
|
5889
6883
|
var program = new Command();
|
|
5890
6884
|
program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
|
|
5891
|
-
program.command("analyze [path]").description("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("-d, --domains <domains>", "Filter to specific domains (comma-separated)").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "low").option("-c, --confidence <level>", "Minimum confidence: high, medium, low", "high").option("--fail-on <level>", "Exit non-zero if gaps at level: critical, high, medium").option("--exclude <dirs>", "Directories to exclude (comma-separated)").option("--verify", "Use AI to verify each match (reduces false positives)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (targetPath, options) => {
|
|
6885
|
+
program.command("analyze [path]").description("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("-d, --domains <domains>", "Filter to specific domains (comma-separated)").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "low").option("-c, --confidence <level>", "Minimum confidence: high, medium, low", "high").option("--fail-on <level>", "Exit non-zero if gaps at level: critical, high, medium").option("--exclude <dirs>", "Directories to exclude (comma-separated)").option("--verify", "Use AI to verify each match (reduces false positives)").option("--execute", "Run dynamic tests in Docker sandbox to confirm vulnerabilities").option("--dry-run", "Preview generated tests without executing (use with --execute)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (targetPath, options) => {
|
|
5892
6886
|
const isQuiet = Boolean(options["quiet"]);
|
|
5893
6887
|
const isVerbose = Boolean(options["verbose"]);
|
|
5894
6888
|
if (isQuiet) {
|
|
@@ -6049,6 +7043,48 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
|
|
|
6049
7043
|
}
|
|
6050
7044
|
}
|
|
6051
7045
|
}
|
|
7046
|
+
const shouldExecute = Boolean(options["execute"]);
|
|
7047
|
+
const isDryRun = Boolean(options["dryRun"]);
|
|
7048
|
+
if (shouldExecute && scanResult.data.gaps.length > 0) {
|
|
7049
|
+
const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
|
|
7050
|
+
const { readFile: readFile5 } = await import('fs/promises');
|
|
7051
|
+
const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
|
|
7052
|
+
if (testableGaps.length === 0) {
|
|
7053
|
+
console.log(chalk5.yellow("\nNo dynamically testable gaps found."));
|
|
7054
|
+
console.log(chalk5.gray("Testable types: sql-injection, xss, command-injection, path-traversal"));
|
|
7055
|
+
} else {
|
|
7056
|
+
const runner = createRunner2(void 0, isDryRun);
|
|
7057
|
+
const initResult = await runner.initialize();
|
|
7058
|
+
if (!initResult.ready) {
|
|
7059
|
+
console.log(chalk5.red(`
|
|
7060
|
+
Dynamic execution unavailable: ${initResult.error}`));
|
|
7061
|
+
} else {
|
|
7062
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
7063
|
+
for (const gap of testableGaps) {
|
|
7064
|
+
if (!fileContents.has(gap.filePath)) {
|
|
7065
|
+
try {
|
|
7066
|
+
fileContents.set(gap.filePath, await readFile5(gap.filePath, "utf-8"));
|
|
7067
|
+
} catch {
|
|
7068
|
+
}
|
|
7069
|
+
}
|
|
7070
|
+
}
|
|
7071
|
+
const executionSummary = await runner.executeAll(testableGaps, fileContents);
|
|
7072
|
+
for (const result of executionSummary.results) {
|
|
7073
|
+
const gap = scanResult.data.gaps.find(
|
|
7074
|
+
(g) => g.filePath === result.gap.filePath && g.lineStart === result.gap.lineStart
|
|
7075
|
+
);
|
|
7076
|
+
if (gap && result.status === "confirmed") {
|
|
7077
|
+
gap.confirmed = true;
|
|
7078
|
+
gap.evidence = result.evidence;
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
if (executionSummary.confirmed > 0) {
|
|
7082
|
+
console.log(chalk5.red.bold(`
|
|
7083
|
+
\u26A0\uFE0F ${executionSummary.confirmed} CONFIRMED vulnerabilities found!`));
|
|
7084
|
+
}
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
7087
|
+
}
|
|
6052
7088
|
const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
|
|
6053
7089
|
if (!cacheResult.success) {
|
|
6054
7090
|
logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
|
|
@@ -6788,6 +7824,132 @@ thresholds:
|
|
|
6788
7824
|
process.exit(1);
|
|
6789
7825
|
}
|
|
6790
7826
|
});
|
|
7827
|
+
program.command("audit-deps").description("Audit npm dependencies for supply chain risks").option("-p, --path <path>", "Path to package.json", "package.json").option("--check-registry", "Verify packages exist in npm registry").option("--check-downloads", "Flag packages with low download counts").option("--check-age", "Flag packages less than 30 days old").option("--strict", "Fail on any warning (exit code 1)").action(async (options) => {
|
|
7828
|
+
const packagePath = resolve(process.cwd(), String(options["path"] ?? "package.json"));
|
|
7829
|
+
const checkRegistry = Boolean(options["checkRegistry"]);
|
|
7830
|
+
const checkDownloads = Boolean(options["checkDownloads"]);
|
|
7831
|
+
const checkAge = Boolean(options["checkAge"]);
|
|
7832
|
+
const strictMode = Boolean(options["strict"]);
|
|
7833
|
+
const doAllChecks = !checkRegistry && !checkDownloads && !checkAge;
|
|
7834
|
+
console.log(chalk5.bold("\nPinata Dependency Audit\n"));
|
|
7835
|
+
if (!existsSync(packagePath)) {
|
|
7836
|
+
console.error(chalk5.red(`Error: ${packagePath} not found`));
|
|
7837
|
+
process.exit(1);
|
|
7838
|
+
}
|
|
7839
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
7840
|
+
const allDeps = {
|
|
7841
|
+
...packageJson.dependencies,
|
|
7842
|
+
...packageJson.devDependencies
|
|
7843
|
+
};
|
|
7844
|
+
const packages = Object.keys(allDeps);
|
|
7845
|
+
console.log(chalk5.gray(`Found ${packages.length} dependencies
|
|
7846
|
+
`));
|
|
7847
|
+
const issues = [];
|
|
7848
|
+
const KNOWN_MALWARE = /* @__PURE__ */ new Set([
|
|
7849
|
+
"ngx-bootstrap",
|
|
7850
|
+
"ng2-file-upload",
|
|
7851
|
+
"@ctrl/tinycolor",
|
|
7852
|
+
"@acitons/artifact",
|
|
7853
|
+
"huggingface-cli",
|
|
7854
|
+
"react-dom-utils-helper",
|
|
7855
|
+
"l0dash",
|
|
7856
|
+
"lodahs",
|
|
7857
|
+
"1odash",
|
|
7858
|
+
"lodassh",
|
|
7859
|
+
"expres",
|
|
7860
|
+
"expresss",
|
|
7861
|
+
"3xpress",
|
|
7862
|
+
"reqeusts",
|
|
7863
|
+
"requets",
|
|
7864
|
+
"requ3sts"
|
|
7865
|
+
]);
|
|
7866
|
+
for (const pkg of packages) {
|
|
7867
|
+
if (KNOWN_MALWARE.has(pkg)) {
|
|
7868
|
+
issues.push({
|
|
7869
|
+
pkg,
|
|
7870
|
+
severity: "critical",
|
|
7871
|
+
message: "Known malicious/compromised package (Shai-Hulud/typosquat)"
|
|
7872
|
+
});
|
|
7873
|
+
}
|
|
7874
|
+
}
|
|
7875
|
+
for (const [pkg, version] of Object.entries(allDeps)) {
|
|
7876
|
+
if (version?.startsWith("^")) {
|
|
7877
|
+
issues.push({
|
|
7878
|
+
pkg,
|
|
7879
|
+
severity: "warning",
|
|
7880
|
+
message: `Unpinned version (${version}) - allows minor updates`
|
|
7881
|
+
});
|
|
7882
|
+
} else if (version?.startsWith("~")) {
|
|
7883
|
+
issues.push({
|
|
7884
|
+
pkg,
|
|
7885
|
+
severity: "warning",
|
|
7886
|
+
message: `Unpinned version (${version}) - allows patch updates`
|
|
7887
|
+
});
|
|
7888
|
+
} else if (version === "*" || version === "latest") {
|
|
7889
|
+
issues.push({
|
|
7890
|
+
pkg,
|
|
7891
|
+
severity: "critical",
|
|
7892
|
+
message: `Extremely dangerous version (${version}) - allows any version`
|
|
7893
|
+
});
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
7896
|
+
if (checkRegistry || doAllChecks) {
|
|
7897
|
+
const spinner = ora("Checking npm registry...").start();
|
|
7898
|
+
for (const pkg of packages.slice(0, 50)) {
|
|
7899
|
+
try {
|
|
7900
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`);
|
|
7901
|
+
if (response.status === 404) {
|
|
7902
|
+
issues.push({
|
|
7903
|
+
pkg,
|
|
7904
|
+
severity: "critical",
|
|
7905
|
+
message: "Package NOT FOUND in npm registry (slopsquatting risk)"
|
|
7906
|
+
});
|
|
7907
|
+
} else if (response.ok) {
|
|
7908
|
+
const data = await response.json();
|
|
7909
|
+
if ((checkAge || doAllChecks) && data.time?.created) {
|
|
7910
|
+
const created = new Date(data.time.created);
|
|
7911
|
+
const ageInDays = (Date.now() - created.getTime()) / (1e3 * 60 * 60 * 24);
|
|
7912
|
+
if (ageInDays < 30) {
|
|
7913
|
+
issues.push({
|
|
7914
|
+
pkg,
|
|
7915
|
+
severity: "warning",
|
|
7916
|
+
message: `Very new package (${Math.floor(ageInDays)} days old)`
|
|
7917
|
+
});
|
|
7918
|
+
}
|
|
7919
|
+
}
|
|
7920
|
+
}
|
|
7921
|
+
} catch {
|
|
7922
|
+
}
|
|
7923
|
+
}
|
|
7924
|
+
spinner.succeed("Registry check complete");
|
|
7925
|
+
}
|
|
7926
|
+
const criticals = issues.filter((i) => i.severity === "critical");
|
|
7927
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
7928
|
+
if (criticals.length > 0) {
|
|
7929
|
+
console.log(chalk5.red.bold(`
|
|
7930
|
+
Critical Issues (${criticals.length}):`));
|
|
7931
|
+
for (const issue of criticals) {
|
|
7932
|
+
console.log(chalk5.red(` \u2717 ${issue.pkg}: ${issue.message}`));
|
|
7933
|
+
}
|
|
7934
|
+
}
|
|
7935
|
+
if (warnings.length > 0) {
|
|
7936
|
+
console.log(chalk5.yellow.bold(`
|
|
7937
|
+
Warnings (${warnings.length}):`));
|
|
7938
|
+
for (const issue of warnings.slice(0, 20)) {
|
|
7939
|
+
console.log(chalk5.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
|
|
7940
|
+
}
|
|
7941
|
+
if (warnings.length > 20) {
|
|
7942
|
+
console.log(chalk5.gray(` ... and ${warnings.length - 20} more`));
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
if (issues.length === 0) {
|
|
7946
|
+
console.log(chalk5.green("\u2713 No dependency issues found"));
|
|
7947
|
+
}
|
|
7948
|
+
console.log();
|
|
7949
|
+
if (criticals.length > 0 || strictMode && warnings.length > 0) {
|
|
7950
|
+
process.exit(1);
|
|
7951
|
+
}
|
|
7952
|
+
});
|
|
6791
7953
|
var config = program.command("config").description("Manage AI provider configuration");
|
|
6792
7954
|
config.command("set <key> <value>").description("Set a configuration value").addHelpText("after", `
|
|
6793
7955
|
Available keys:
|
|
@@ -6942,15 +8104,15 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
6942
8104
|
const configDir = resolve(process.cwd(), ".pinata");
|
|
6943
8105
|
const authPath = resolve(configDir, "auth.json");
|
|
6944
8106
|
const envPath = resolve(configDir, ".env");
|
|
6945
|
-
const { rm } = await import('fs/promises');
|
|
8107
|
+
const { rm: rm2 } = await import('fs/promises');
|
|
6946
8108
|
try {
|
|
6947
8109
|
let removed = false;
|
|
6948
8110
|
if (existsSync(authPath)) {
|
|
6949
|
-
await
|
|
8111
|
+
await rm2(authPath);
|
|
6950
8112
|
removed = true;
|
|
6951
8113
|
}
|
|
6952
8114
|
if (existsSync(envPath)) {
|
|
6953
|
-
await
|
|
8115
|
+
await rm2(envPath);
|
|
6954
8116
|
removed = true;
|
|
6955
8117
|
}
|
|
6956
8118
|
if (removed) {
|