pinata-security-cli 0.2.3 → 0.4.1
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 +75 -12
- package/dist/cli/index.js +1403 -29
- 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/src/categories/definitions/security/sql-injection.yml +29 -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
|
-
import { existsSync,
|
|
6
|
-
import { homedir } from 'os';
|
|
5
|
+
import { existsSync, writeFileSync, readFileSync, chmodSync, mkdirSync } from 'fs';
|
|
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);
|
|
@@ -1559,6 +2553,174 @@ var init_tui = __esm({
|
|
|
1559
2553
|
init_App();
|
|
1560
2554
|
}
|
|
1561
2555
|
});
|
|
2556
|
+
|
|
2557
|
+
// src/feedback/types.ts
|
|
2558
|
+
function suggestConfidence(precision) {
|
|
2559
|
+
if (precision >= CONFIDENCE_THRESHOLDS.high) return "high";
|
|
2560
|
+
if (precision >= CONFIDENCE_THRESHOLDS.medium) return "medium";
|
|
2561
|
+
return "low";
|
|
2562
|
+
}
|
|
2563
|
+
var EMPTY_FEEDBACK_STATE, CONFIDENCE_THRESHOLDS;
|
|
2564
|
+
var init_types3 = __esm({
|
|
2565
|
+
"src/feedback/types.ts"() {
|
|
2566
|
+
EMPTY_FEEDBACK_STATE = {
|
|
2567
|
+
version: 1,
|
|
2568
|
+
patterns: {},
|
|
2569
|
+
totalScans: 0,
|
|
2570
|
+
lastScanAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2571
|
+
};
|
|
2572
|
+
CONFIDENCE_THRESHOLDS = {
|
|
2573
|
+
/** Precision >= 0.7 → high confidence */
|
|
2574
|
+
high: 0.7,
|
|
2575
|
+
/** Precision >= 0.4 → medium confidence */
|
|
2576
|
+
medium: 0.4,
|
|
2577
|
+
/** Precision < 0.4 → low confidence */
|
|
2578
|
+
low: 0
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
});
|
|
2582
|
+
async function loadFeedback() {
|
|
2583
|
+
try {
|
|
2584
|
+
const content = await readFile(FEEDBACK_FILE, "utf-8");
|
|
2585
|
+
const state = JSON.parse(content);
|
|
2586
|
+
if (state.version !== 1) {
|
|
2587
|
+
console.warn("Feedback version mismatch, resetting...");
|
|
2588
|
+
return { ...EMPTY_FEEDBACK_STATE };
|
|
2589
|
+
}
|
|
2590
|
+
return state;
|
|
2591
|
+
} catch {
|
|
2592
|
+
return { ...EMPTY_FEEDBACK_STATE };
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
async function saveFeedback(state) {
|
|
2596
|
+
try {
|
|
2597
|
+
await mkdir(FEEDBACK_DIR, { recursive: true });
|
|
2598
|
+
await writeFile(FEEDBACK_FILE, JSON.stringify(state, null, 2));
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
console.warn(`Failed to save feedback: ${error instanceof Error ? error.message : String(error)}`);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function applyUpdates(state, updates) {
|
|
2604
|
+
const newState = { ...state };
|
|
2605
|
+
newState.patterns = { ...state.patterns };
|
|
2606
|
+
for (const update of updates) {
|
|
2607
|
+
const existing = newState.patterns[update.patternId];
|
|
2608
|
+
const pattern = existing ?? {
|
|
2609
|
+
patternId: update.patternId,
|
|
2610
|
+
categoryId: update.categoryId,
|
|
2611
|
+
totalMatches: 0,
|
|
2612
|
+
confirmedCount: 0,
|
|
2613
|
+
unconfirmedCount: 0,
|
|
2614
|
+
aiDismissedCount: 0,
|
|
2615
|
+
aiVerifiedCount: 0,
|
|
2616
|
+
precision: 0,
|
|
2617
|
+
suggestedConfidence: "medium",
|
|
2618
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2619
|
+
};
|
|
2620
|
+
switch (update.outcome) {
|
|
2621
|
+
case "matched":
|
|
2622
|
+
pattern.totalMatches++;
|
|
2623
|
+
break;
|
|
2624
|
+
case "confirmed":
|
|
2625
|
+
pattern.confirmedCount++;
|
|
2626
|
+
break;
|
|
2627
|
+
case "unconfirmed":
|
|
2628
|
+
pattern.unconfirmedCount++;
|
|
2629
|
+
break;
|
|
2630
|
+
case "ai_verified":
|
|
2631
|
+
pattern.aiVerifiedCount++;
|
|
2632
|
+
break;
|
|
2633
|
+
case "ai_dismissed":
|
|
2634
|
+
pattern.aiDismissedCount++;
|
|
2635
|
+
break;
|
|
2636
|
+
}
|
|
2637
|
+
const total = pattern.confirmedCount + pattern.unconfirmedCount;
|
|
2638
|
+
pattern.precision = total > 0 ? pattern.confirmedCount / total : 0.5;
|
|
2639
|
+
pattern.suggestedConfidence = suggestConfidence(pattern.precision);
|
|
2640
|
+
pattern.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2641
|
+
newState.patterns[update.patternId] = pattern;
|
|
2642
|
+
}
|
|
2643
|
+
newState.totalScans++;
|
|
2644
|
+
newState.lastScanAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2645
|
+
return newState;
|
|
2646
|
+
}
|
|
2647
|
+
function getConfidenceAdjustment(state, patternId) {
|
|
2648
|
+
const pattern = state.patterns[patternId];
|
|
2649
|
+
if (!pattern) return null;
|
|
2650
|
+
const totalExecutions = pattern.confirmedCount + pattern.unconfirmedCount;
|
|
2651
|
+
if (totalExecutions < 5) return null;
|
|
2652
|
+
return pattern.suggestedConfidence;
|
|
2653
|
+
}
|
|
2654
|
+
function getLowPrecisionPatterns(state, threshold = 0.3) {
|
|
2655
|
+
return Object.values(state.patterns).filter((p) => {
|
|
2656
|
+
const total = p.confirmedCount + p.unconfirmedCount;
|
|
2657
|
+
return total >= 5 && p.precision < threshold;
|
|
2658
|
+
}).sort((a, b) => a.precision - b.precision);
|
|
2659
|
+
}
|
|
2660
|
+
function getHighPrecisionPatterns(state, threshold = 0.8) {
|
|
2661
|
+
return Object.values(state.patterns).filter((p) => {
|
|
2662
|
+
const total = p.confirmedCount + p.unconfirmedCount;
|
|
2663
|
+
return total >= 5 && p.precision >= threshold;
|
|
2664
|
+
}).sort((a, b) => b.precision - a.precision);
|
|
2665
|
+
}
|
|
2666
|
+
function generateReport(state) {
|
|
2667
|
+
const lines = [
|
|
2668
|
+
"# Pinata Feedback Report",
|
|
2669
|
+
"",
|
|
2670
|
+
`Total scans: ${state.totalScans}`,
|
|
2671
|
+
`Last scan: ${state.lastScanAt}`,
|
|
2672
|
+
`Patterns tracked: ${Object.keys(state.patterns).length}`,
|
|
2673
|
+
""
|
|
2674
|
+
];
|
|
2675
|
+
const lowPrecision = getLowPrecisionPatterns(state);
|
|
2676
|
+
if (lowPrecision.length > 0) {
|
|
2677
|
+
lines.push("## Low Precision Patterns (potential false positive sources)");
|
|
2678
|
+
lines.push("");
|
|
2679
|
+
for (const p of lowPrecision.slice(0, 10)) {
|
|
2680
|
+
lines.push(`- ${p.patternId}: ${(p.precision * 100).toFixed(1)}% precision (${p.confirmedCount}/${p.confirmedCount + p.unconfirmedCount})`);
|
|
2681
|
+
}
|
|
2682
|
+
lines.push("");
|
|
2683
|
+
}
|
|
2684
|
+
const highPrecision = getHighPrecisionPatterns(state);
|
|
2685
|
+
if (highPrecision.length > 0) {
|
|
2686
|
+
lines.push("## High Precision Patterns");
|
|
2687
|
+
lines.push("");
|
|
2688
|
+
for (const p of highPrecision.slice(0, 10)) {
|
|
2689
|
+
lines.push(`- ${p.patternId}: ${(p.precision * 100).toFixed(1)}% precision (${p.confirmedCount}/${p.confirmedCount + p.unconfirmedCount})`);
|
|
2690
|
+
}
|
|
2691
|
+
lines.push("");
|
|
2692
|
+
}
|
|
2693
|
+
return lines.join("\n");
|
|
2694
|
+
}
|
|
2695
|
+
var FEEDBACK_DIR, FEEDBACK_FILE;
|
|
2696
|
+
var init_store = __esm({
|
|
2697
|
+
"src/feedback/store.ts"() {
|
|
2698
|
+
init_types3();
|
|
2699
|
+
FEEDBACK_DIR = join(homedir(), ".pinata");
|
|
2700
|
+
FEEDBACK_FILE = join(FEEDBACK_DIR, "feedback.json");
|
|
2701
|
+
}
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
// src/feedback/index.ts
|
|
2705
|
+
var feedback_exports = {};
|
|
2706
|
+
__export(feedback_exports, {
|
|
2707
|
+
CONFIDENCE_THRESHOLDS: () => CONFIDENCE_THRESHOLDS,
|
|
2708
|
+
EMPTY_FEEDBACK_STATE: () => EMPTY_FEEDBACK_STATE,
|
|
2709
|
+
applyUpdates: () => applyUpdates,
|
|
2710
|
+
generateReport: () => generateReport,
|
|
2711
|
+
getConfidenceAdjustment: () => getConfidenceAdjustment,
|
|
2712
|
+
getHighPrecisionPatterns: () => getHighPrecisionPatterns,
|
|
2713
|
+
getLowPrecisionPatterns: () => getLowPrecisionPatterns,
|
|
2714
|
+
loadFeedback: () => loadFeedback,
|
|
2715
|
+
saveFeedback: () => saveFeedback,
|
|
2716
|
+
suggestConfidence: () => suggestConfidence
|
|
2717
|
+
});
|
|
2718
|
+
var init_feedback = __esm({
|
|
2719
|
+
"src/feedback/index.ts"() {
|
|
2720
|
+
init_types3();
|
|
2721
|
+
init_store();
|
|
2722
|
+
}
|
|
2723
|
+
});
|
|
1562
2724
|
var RiskDomainSchema = z.enum([
|
|
1563
2725
|
"security",
|
|
1564
2726
|
"data",
|
|
@@ -3239,8 +4401,8 @@ var Scanner = class {
|
|
|
3239
4401
|
readPinataIgnore(targetDirectory) {
|
|
3240
4402
|
const ignorePath = resolve(targetDirectory, ".pinataignore");
|
|
3241
4403
|
try {
|
|
3242
|
-
const { readFileSync:
|
|
3243
|
-
const content =
|
|
4404
|
+
const { readFileSync: readFileSync3 } = __require("fs");
|
|
4405
|
+
const content = readFileSync3(ignorePath, "utf-8");
|
|
3244
4406
|
return content.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((line) => line.replace(/\/$/, ""));
|
|
3245
4407
|
} catch {
|
|
3246
4408
|
return [];
|
|
@@ -3576,7 +4738,7 @@ function createScanner(categoryStore) {
|
|
|
3576
4738
|
init_types();
|
|
3577
4739
|
|
|
3578
4740
|
// src/core/index.ts
|
|
3579
|
-
var VERSION = "0.
|
|
4741
|
+
var VERSION = "0.4.0";
|
|
3580
4742
|
|
|
3581
4743
|
// src/lib/index.ts
|
|
3582
4744
|
init_errors();
|
|
@@ -4497,14 +5659,14 @@ var AIService = class {
|
|
|
4497
5659
|
*/
|
|
4498
5660
|
getApiKeyFromConfig(provider) {
|
|
4499
5661
|
try {
|
|
4500
|
-
const { existsSync: existsSync4, readFileSync:
|
|
4501
|
-
const { homedir:
|
|
4502
|
-
const { join:
|
|
4503
|
-
const configPath =
|
|
5662
|
+
const { existsSync: existsSync4, readFileSync: readFileSync3 } = __require("fs");
|
|
5663
|
+
const { homedir: homedir3 } = __require("os");
|
|
5664
|
+
const { join: join5 } = __require("path");
|
|
5665
|
+
const configPath = join5(homedir3(), ".pinata", "config.json");
|
|
4504
5666
|
if (!existsSync4(configPath)) {
|
|
4505
5667
|
return "";
|
|
4506
5668
|
}
|
|
4507
|
-
const content =
|
|
5669
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
4508
5670
|
const config2 = JSON.parse(content);
|
|
4509
5671
|
return (provider === "anthropic" ? config2.anthropicApiKey : config2.openaiApiKey) ?? "";
|
|
4510
5672
|
} catch {
|
|
@@ -5888,7 +7050,7 @@ function getDefinitionsPath() {
|
|
|
5888
7050
|
}
|
|
5889
7051
|
var program = new Command();
|
|
5890
7052
|
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) => {
|
|
7053
|
+
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("--output-file <path>", "Write output to file (useful for SARIF upload)").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
7054
|
const isQuiet = Boolean(options["quiet"]);
|
|
5893
7055
|
const isVerbose = Boolean(options["verbose"]);
|
|
5894
7056
|
if (isQuiet) {
|
|
@@ -6011,12 +7173,12 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
|
|
|
6011
7173
|
const verifySpinner = showSpinner ? ora("Verifying gaps with AI...").start() : null;
|
|
6012
7174
|
try {
|
|
6013
7175
|
const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
|
|
6014
|
-
const { readFile:
|
|
7176
|
+
const { readFile: readFile6 } = await import('fs/promises');
|
|
6015
7177
|
const apiKey = getApiKey2(provider);
|
|
6016
7178
|
const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
|
|
6017
7179
|
const { verified, dismissed, stats } = await verifier.verifyAll(
|
|
6018
7180
|
scanResult.data.gaps,
|
|
6019
|
-
async (path2) =>
|
|
7181
|
+
async (path2) => readFile6(path2, "utf-8")
|
|
6020
7182
|
);
|
|
6021
7183
|
scanResult.data.gaps = verified;
|
|
6022
7184
|
const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
|
|
@@ -6049,12 +7211,61 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
|
|
|
6049
7211
|
}
|
|
6050
7212
|
}
|
|
6051
7213
|
}
|
|
7214
|
+
const shouldExecute = Boolean(options["execute"]);
|
|
7215
|
+
const isDryRun = Boolean(options["dryRun"]);
|
|
7216
|
+
if (shouldExecute && scanResult.data.gaps.length > 0) {
|
|
7217
|
+
const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
|
|
7218
|
+
const { readFile: readFile6 } = await import('fs/promises');
|
|
7219
|
+
const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
|
|
7220
|
+
if (testableGaps.length === 0) {
|
|
7221
|
+
console.log(chalk5.yellow("\nNo dynamically testable gaps found."));
|
|
7222
|
+
console.log(chalk5.gray("Testable types: sql-injection, xss, command-injection, path-traversal"));
|
|
7223
|
+
} else {
|
|
7224
|
+
const runner = createRunner2(void 0, isDryRun);
|
|
7225
|
+
const initResult = await runner.initialize();
|
|
7226
|
+
if (!initResult.ready) {
|
|
7227
|
+
console.log(chalk5.red(`
|
|
7228
|
+
Dynamic execution unavailable: ${initResult.error}`));
|
|
7229
|
+
} else {
|
|
7230
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
7231
|
+
for (const gap of testableGaps) {
|
|
7232
|
+
if (!fileContents.has(gap.filePath)) {
|
|
7233
|
+
try {
|
|
7234
|
+
fileContents.set(gap.filePath, await readFile6(gap.filePath, "utf-8"));
|
|
7235
|
+
} catch {
|
|
7236
|
+
}
|
|
7237
|
+
}
|
|
7238
|
+
}
|
|
7239
|
+
const executionSummary = await runner.executeAll(testableGaps, fileContents);
|
|
7240
|
+
for (const result of executionSummary.results) {
|
|
7241
|
+
const gap = scanResult.data.gaps.find(
|
|
7242
|
+
(g) => g.filePath === result.gap.filePath && g.lineStart === result.gap.lineStart
|
|
7243
|
+
);
|
|
7244
|
+
if (gap && result.status === "confirmed") {
|
|
7245
|
+
gap.confirmed = true;
|
|
7246
|
+
gap.evidence = result.evidence;
|
|
7247
|
+
}
|
|
7248
|
+
}
|
|
7249
|
+
if (executionSummary.confirmed > 0) {
|
|
7250
|
+
console.log(chalk5.red.bold(`
|
|
7251
|
+
\u26A0\uFE0F ${executionSummary.confirmed} CONFIRMED vulnerabilities found!`));
|
|
7252
|
+
}
|
|
7253
|
+
}
|
|
7254
|
+
}
|
|
7255
|
+
}
|
|
6052
7256
|
const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
|
|
6053
7257
|
if (!cacheResult.success) {
|
|
6054
7258
|
logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
|
|
6055
7259
|
}
|
|
6056
7260
|
const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
|
|
6057
|
-
|
|
7261
|
+
const outputFile = options["outputFile"];
|
|
7262
|
+
if (outputFile) {
|
|
7263
|
+
const outputPath = resolve(outputFile);
|
|
7264
|
+
writeFileSync(outputPath, output, "utf-8");
|
|
7265
|
+
logger.info(`Results written to: ${outputPath}`);
|
|
7266
|
+
} else {
|
|
7267
|
+
console.log(output);
|
|
7268
|
+
}
|
|
6058
7269
|
if (isVerbose && scanResult.data.warnings.length > 0) {
|
|
6059
7270
|
console.error("\nWarnings:");
|
|
6060
7271
|
for (const warning of scanResult.data.warnings) {
|
|
@@ -6461,8 +7672,8 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
|
|
|
6461
7672
|
let vulnerableCode = [...codeSnippets];
|
|
6462
7673
|
if (filePath) {
|
|
6463
7674
|
try {
|
|
6464
|
-
const { readFile:
|
|
6465
|
-
const content = await
|
|
7675
|
+
const { readFile: readFile6 } = await import('fs/promises');
|
|
7676
|
+
const content = await readFile6(filePath, "utf-8");
|
|
6466
7677
|
vulnerableCode = [...vulnerableCode, ...content.split("\n---\n").filter(Boolean)];
|
|
6467
7678
|
} catch (error) {
|
|
6468
7679
|
console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
|
|
@@ -6761,16 +7972,16 @@ thresholds:
|
|
|
6761
7972
|
high: 5
|
|
6762
7973
|
medium: 20
|
|
6763
7974
|
`;
|
|
6764
|
-
const { writeFile: writeFileAsync, mkdir:
|
|
7975
|
+
const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
|
|
6765
7976
|
try {
|
|
6766
7977
|
await writeFileAsync(configPath, defaultConfig, "utf8");
|
|
6767
7978
|
console.log(chalk5.green("Created .pinata.yml"));
|
|
6768
|
-
await
|
|
7979
|
+
await mkdir4(cacheDir, { recursive: true });
|
|
6769
7980
|
console.log(chalk5.green("Created .pinata/ directory"));
|
|
6770
7981
|
const gitignorePath = resolve(process.cwd(), ".gitignore");
|
|
6771
7982
|
if (existsSync(gitignorePath)) {
|
|
6772
|
-
const { readFile:
|
|
6773
|
-
const gitignore = await
|
|
7983
|
+
const { readFile: readFile6, appendFile } = await import('fs/promises');
|
|
7984
|
+
const gitignore = await readFile6(gitignorePath, "utf8");
|
|
6774
7985
|
if (!gitignore.includes(".pinata/")) {
|
|
6775
7986
|
await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
|
|
6776
7987
|
console.log(chalk5.green("Added .pinata/ to .gitignore"));
|
|
@@ -6788,6 +7999,169 @@ thresholds:
|
|
|
6788
7999
|
process.exit(1);
|
|
6789
8000
|
}
|
|
6790
8001
|
});
|
|
8002
|
+
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) => {
|
|
8003
|
+
const packagePath = resolve(process.cwd(), String(options["path"] ?? "package.json"));
|
|
8004
|
+
const checkRegistry = Boolean(options["checkRegistry"]);
|
|
8005
|
+
const checkDownloads = Boolean(options["checkDownloads"]);
|
|
8006
|
+
const checkAge = Boolean(options["checkAge"]);
|
|
8007
|
+
const strictMode = Boolean(options["strict"]);
|
|
8008
|
+
const doAllChecks = !checkRegistry && !checkDownloads && !checkAge;
|
|
8009
|
+
console.log(chalk5.bold("\nPinata Dependency Audit\n"));
|
|
8010
|
+
if (!existsSync(packagePath)) {
|
|
8011
|
+
console.error(chalk5.red(`Error: ${packagePath} not found`));
|
|
8012
|
+
process.exit(1);
|
|
8013
|
+
}
|
|
8014
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
8015
|
+
const allDeps = {
|
|
8016
|
+
...packageJson.dependencies,
|
|
8017
|
+
...packageJson.devDependencies
|
|
8018
|
+
};
|
|
8019
|
+
const packages = Object.keys(allDeps);
|
|
8020
|
+
console.log(chalk5.gray(`Found ${packages.length} dependencies
|
|
8021
|
+
`));
|
|
8022
|
+
const issues = [];
|
|
8023
|
+
const KNOWN_MALWARE = /* @__PURE__ */ new Set([
|
|
8024
|
+
"ngx-bootstrap",
|
|
8025
|
+
"ng2-file-upload",
|
|
8026
|
+
"@ctrl/tinycolor",
|
|
8027
|
+
"@acitons/artifact",
|
|
8028
|
+
"huggingface-cli",
|
|
8029
|
+
"react-dom-utils-helper",
|
|
8030
|
+
"l0dash",
|
|
8031
|
+
"lodahs",
|
|
8032
|
+
"1odash",
|
|
8033
|
+
"lodassh",
|
|
8034
|
+
"expres",
|
|
8035
|
+
"expresss",
|
|
8036
|
+
"3xpress",
|
|
8037
|
+
"reqeusts",
|
|
8038
|
+
"requets",
|
|
8039
|
+
"requ3sts"
|
|
8040
|
+
]);
|
|
8041
|
+
for (const pkg of packages) {
|
|
8042
|
+
if (KNOWN_MALWARE.has(pkg)) {
|
|
8043
|
+
issues.push({
|
|
8044
|
+
pkg,
|
|
8045
|
+
severity: "critical",
|
|
8046
|
+
message: "Known malicious/compromised package (Shai-Hulud/typosquat)"
|
|
8047
|
+
});
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
for (const [pkg, version] of Object.entries(allDeps)) {
|
|
8051
|
+
if (version?.startsWith("^")) {
|
|
8052
|
+
issues.push({
|
|
8053
|
+
pkg,
|
|
8054
|
+
severity: "warning",
|
|
8055
|
+
message: `Unpinned version (${version}) - allows minor updates`
|
|
8056
|
+
});
|
|
8057
|
+
} else if (version?.startsWith("~")) {
|
|
8058
|
+
issues.push({
|
|
8059
|
+
pkg,
|
|
8060
|
+
severity: "warning",
|
|
8061
|
+
message: `Unpinned version (${version}) - allows patch updates`
|
|
8062
|
+
});
|
|
8063
|
+
} else if (version === "*" || version === "latest") {
|
|
8064
|
+
issues.push({
|
|
8065
|
+
pkg,
|
|
8066
|
+
severity: "critical",
|
|
8067
|
+
message: `Extremely dangerous version (${version}) - allows any version`
|
|
8068
|
+
});
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
8071
|
+
if (checkRegistry || doAllChecks) {
|
|
8072
|
+
const spinner = ora("Checking npm registry...").start();
|
|
8073
|
+
for (const pkg of packages.slice(0, 50)) {
|
|
8074
|
+
try {
|
|
8075
|
+
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`);
|
|
8076
|
+
if (response.status === 404) {
|
|
8077
|
+
issues.push({
|
|
8078
|
+
pkg,
|
|
8079
|
+
severity: "critical",
|
|
8080
|
+
message: "Package NOT FOUND in npm registry (slopsquatting risk)"
|
|
8081
|
+
});
|
|
8082
|
+
} else if (response.ok) {
|
|
8083
|
+
const data = await response.json();
|
|
8084
|
+
if ((checkAge || doAllChecks) && data.time?.created) {
|
|
8085
|
+
const created = new Date(data.time.created);
|
|
8086
|
+
const ageInDays = (Date.now() - created.getTime()) / (1e3 * 60 * 60 * 24);
|
|
8087
|
+
if (ageInDays < 30) {
|
|
8088
|
+
issues.push({
|
|
8089
|
+
pkg,
|
|
8090
|
+
severity: "warning",
|
|
8091
|
+
message: `Very new package (${Math.floor(ageInDays)} days old)`
|
|
8092
|
+
});
|
|
8093
|
+
}
|
|
8094
|
+
}
|
|
8095
|
+
}
|
|
8096
|
+
} catch {
|
|
8097
|
+
}
|
|
8098
|
+
}
|
|
8099
|
+
spinner.succeed("Registry check complete");
|
|
8100
|
+
}
|
|
8101
|
+
const criticals = issues.filter((i) => i.severity === "critical");
|
|
8102
|
+
const warnings = issues.filter((i) => i.severity === "warning");
|
|
8103
|
+
if (criticals.length > 0) {
|
|
8104
|
+
console.log(chalk5.red.bold(`
|
|
8105
|
+
Critical Issues (${criticals.length}):`));
|
|
8106
|
+
for (const issue of criticals) {
|
|
8107
|
+
console.log(chalk5.red(` \u2717 ${issue.pkg}: ${issue.message}`));
|
|
8108
|
+
}
|
|
8109
|
+
}
|
|
8110
|
+
if (warnings.length > 0) {
|
|
8111
|
+
console.log(chalk5.yellow.bold(`
|
|
8112
|
+
Warnings (${warnings.length}):`));
|
|
8113
|
+
for (const issue of warnings.slice(0, 20)) {
|
|
8114
|
+
console.log(chalk5.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
|
|
8115
|
+
}
|
|
8116
|
+
if (warnings.length > 20) {
|
|
8117
|
+
console.log(chalk5.gray(` ... and ${warnings.length - 20} more`));
|
|
8118
|
+
}
|
|
8119
|
+
}
|
|
8120
|
+
if (issues.length === 0) {
|
|
8121
|
+
console.log(chalk5.green("\u2713 No dependency issues found"));
|
|
8122
|
+
}
|
|
8123
|
+
console.log();
|
|
8124
|
+
if (criticals.length > 0 || strictMode && warnings.length > 0) {
|
|
8125
|
+
process.exit(1);
|
|
8126
|
+
}
|
|
8127
|
+
});
|
|
8128
|
+
program.command("feedback").description("View pattern performance feedback (Layer 6)").option("--reset", "Reset all feedback data").option("-o, --output <format>", "Output format: terminal, json, markdown", "terminal").action(async (options) => {
|
|
8129
|
+
const { loadFeedback: loadFeedback2, saveFeedback: saveFeedback2, generateReport: generateReport2, EMPTY_FEEDBACK_STATE: EMPTY_FEEDBACK_STATE2 } = await Promise.resolve().then(() => (init_feedback(), feedback_exports));
|
|
8130
|
+
const outputFormat = String(options["output"] ?? "terminal");
|
|
8131
|
+
const shouldReset = Boolean(options["reset"]);
|
|
8132
|
+
if (shouldReset) {
|
|
8133
|
+
await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
|
|
8134
|
+
console.log(chalk5.green("Feedback data reset."));
|
|
8135
|
+
return;
|
|
8136
|
+
}
|
|
8137
|
+
const state = await loadFeedback2();
|
|
8138
|
+
if (outputFormat === "json") {
|
|
8139
|
+
console.log(JSON.stringify(state, null, 2));
|
|
8140
|
+
return;
|
|
8141
|
+
}
|
|
8142
|
+
if (outputFormat === "markdown") {
|
|
8143
|
+
console.log(generateReport2(state));
|
|
8144
|
+
return;
|
|
8145
|
+
}
|
|
8146
|
+
console.log(chalk5.bold("\nPinata Feedback Report\n"));
|
|
8147
|
+
console.log(`Total scans: ${state.totalScans}`);
|
|
8148
|
+
console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
|
|
8149
|
+
if (state.totalScans === 0) {
|
|
8150
|
+
console.log(chalk5.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
|
|
8151
|
+
return;
|
|
8152
|
+
}
|
|
8153
|
+
const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
|
|
8154
|
+
if (patterns.length > 0) {
|
|
8155
|
+
console.log(chalk5.bold("\nPattern Performance:"));
|
|
8156
|
+
for (const p of patterns.slice(0, 15)) {
|
|
8157
|
+
const total = p.confirmedCount + p.unconfirmedCount;
|
|
8158
|
+
const precisionPct = (p.precision * 100).toFixed(0);
|
|
8159
|
+
const color = p.precision >= 0.7 ? chalk5.green : p.precision >= 0.4 ? chalk5.yellow : chalk5.red;
|
|
8160
|
+
console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
|
|
8161
|
+
}
|
|
8162
|
+
}
|
|
8163
|
+
console.log();
|
|
8164
|
+
});
|
|
6791
8165
|
var config = program.command("config").description("Manage AI provider configuration");
|
|
6792
8166
|
config.command("set <key> <value>").description("Set a configuration value").addHelpText("after", `
|
|
6793
8167
|
Available keys:
|
|
@@ -6916,9 +8290,9 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
|
|
|
6916
8290
|
}
|
|
6917
8291
|
const configDir = resolve(process.cwd(), ".pinata");
|
|
6918
8292
|
const authPath = resolve(configDir, "auth.json");
|
|
6919
|
-
const { mkdir:
|
|
8293
|
+
const { mkdir: mkdir4, writeFile: writeFileAsync } = await import('fs/promises');
|
|
6920
8294
|
try {
|
|
6921
|
-
await
|
|
8295
|
+
await mkdir4(configDir, { recursive: true });
|
|
6922
8296
|
const maskedKey = `****${apiKey.slice(-8)}`;
|
|
6923
8297
|
const authData = {
|
|
6924
8298
|
configured: true,
|
|
@@ -6942,15 +8316,15 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
6942
8316
|
const configDir = resolve(process.cwd(), ".pinata");
|
|
6943
8317
|
const authPath = resolve(configDir, "auth.json");
|
|
6944
8318
|
const envPath = resolve(configDir, ".env");
|
|
6945
|
-
const { rm } = await import('fs/promises');
|
|
8319
|
+
const { rm: rm2 } = await import('fs/promises');
|
|
6946
8320
|
try {
|
|
6947
8321
|
let removed = false;
|
|
6948
8322
|
if (existsSync(authPath)) {
|
|
6949
|
-
await
|
|
8323
|
+
await rm2(authPath);
|
|
6950
8324
|
removed = true;
|
|
6951
8325
|
}
|
|
6952
8326
|
if (existsSync(envPath)) {
|
|
6953
|
-
await
|
|
8327
|
+
await rm2(envPath);
|
|
6954
8328
|
removed = true;
|
|
6955
8329
|
}
|
|
6956
8330
|
if (removed) {
|
|
@@ -6971,8 +8345,8 @@ auth.command("status").description("Check authentication status").action(async (
|
|
|
6971
8345
|
process.exit(0);
|
|
6972
8346
|
}
|
|
6973
8347
|
try {
|
|
6974
|
-
const { readFile:
|
|
6975
|
-
const authData = JSON.parse(await
|
|
8348
|
+
const { readFile: readFile6 } = await import('fs/promises');
|
|
8349
|
+
const authData = JSON.parse(await readFile6(authPath, "utf8"));
|
|
6976
8350
|
console.log(chalk5.green("Authenticated"));
|
|
6977
8351
|
console.log(chalk5.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
|
|
6978
8352
|
console.log(chalk5.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
|