pinata-security-cli 0.5.1 → 0.5.3
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 +39 -11
- package/dist/cli/index.js +1730 -1527
- package/dist/cli/index.js.map +1 -1
- package/package.json +8 -6
package/dist/cli/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import fs, { mkdir, writeFile, readFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
|
|
2
|
+
import fs, { readFile, mkdir, writeFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
|
|
4
3
|
import path, { dirname, resolve, join, basename, relative, extname } from 'path';
|
|
5
|
-
import { existsSync,
|
|
4
|
+
import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
|
|
6
5
|
import { homedir, tmpdir } from 'os';
|
|
6
|
+
import { z } from 'zod';
|
|
7
7
|
import { spawn } from 'child_process';
|
|
8
8
|
import { useState } from 'react';
|
|
9
9
|
import { render, useApp, useInput, Box, Text } from 'ink';
|
|
10
10
|
import Spinner from 'ink-spinner';
|
|
11
11
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
12
|
-
import
|
|
13
|
-
import chalk5 from 'chalk';
|
|
12
|
+
import chalk7 from 'chalk';
|
|
14
13
|
import { Command } from 'commander';
|
|
15
|
-
import
|
|
14
|
+
import ora3 from 'ora';
|
|
16
15
|
import YAML from 'yaml';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
17
|
import { Query, Parser, Language } from 'web-tree-sitter';
|
|
18
18
|
import { minimatch } from 'minimatch';
|
|
19
19
|
|
|
@@ -125,35 +125,6 @@ var init_result = __esm({
|
|
|
125
125
|
"src/lib/result.ts"() {
|
|
126
126
|
}
|
|
127
127
|
});
|
|
128
|
-
var SEVERITY_WEIGHTS, CONFIDENCE_WEIGHTS, PRIORITY_WEIGHTS, DEFAULT_TEST_PATTERNS;
|
|
129
|
-
var init_types = __esm({
|
|
130
|
-
"src/core/scanner/types.ts"() {
|
|
131
|
-
SEVERITY_WEIGHTS = {
|
|
132
|
-
critical: 4,
|
|
133
|
-
high: 3,
|
|
134
|
-
medium: 2,
|
|
135
|
-
low: 1
|
|
136
|
-
};
|
|
137
|
-
CONFIDENCE_WEIGHTS = {
|
|
138
|
-
high: 1,
|
|
139
|
-
medium: 0.7,
|
|
140
|
-
low: 0.4
|
|
141
|
-
};
|
|
142
|
-
PRIORITY_WEIGHTS = {
|
|
143
|
-
P0: 3,
|
|
144
|
-
P1: 2,
|
|
145
|
-
P2: 1
|
|
146
|
-
};
|
|
147
|
-
DEFAULT_TEST_PATTERNS = {
|
|
148
|
-
python: ["test_*.py", "*_test.py", "tests/**/*.py", "test/**/*.py"],
|
|
149
|
-
typescript: ["*.test.ts", "*.spec.ts", "__tests__/**/*.ts", "tests/**/*.ts"],
|
|
150
|
-
javascript: ["*.test.js", "*.spec.js", "__tests__/**/*.js", "tests/**/*.js"],
|
|
151
|
-
go: ["*_test.go"],
|
|
152
|
-
java: ["*Test.java", "*Tests.java", "src/test/**/*.java"],
|
|
153
|
-
rust: ["tests/**/*.rs"]
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
128
|
function getCachePath(projectRoot) {
|
|
158
129
|
return resolve(projectRoot, CACHE_DIR, CACHE_FILE);
|
|
159
130
|
}
|
|
@@ -850,7 +821,6 @@ var init_config = __esm({
|
|
|
850
821
|
var SKIP_PATTERNS, BATCH_PROMPT, SINGLE_ITEM_TEMPLATE, AIVerifier;
|
|
851
822
|
var init_ai_verifier = __esm({
|
|
852
823
|
"src/core/verifier/ai-verifier.ts"() {
|
|
853
|
-
init_types();
|
|
854
824
|
SKIP_PATTERNS = {
|
|
855
825
|
paths: [
|
|
856
826
|
/\.test\.(ts|js|tsx|jsx)$/,
|
|
@@ -1255,7 +1225,7 @@ function isTestable(categoryId) {
|
|
|
1255
1225
|
return TESTABLE_VULNERABILITIES.includes(categoryId);
|
|
1256
1226
|
}
|
|
1257
1227
|
var DEFAULT_SANDBOX_CONFIG, TESTABLE_VULNERABILITIES;
|
|
1258
|
-
var
|
|
1228
|
+
var init_types = __esm({
|
|
1259
1229
|
"src/execution/types.ts"() {
|
|
1260
1230
|
DEFAULT_SANDBOX_CONFIG = {
|
|
1261
1231
|
image: "pinata-sandbox:latest",
|
|
@@ -1285,7 +1255,7 @@ function createSandbox(config2) {
|
|
|
1285
1255
|
var Sandbox;
|
|
1286
1256
|
var init_sandbox = __esm({
|
|
1287
1257
|
"src/execution/sandbox.ts"() {
|
|
1288
|
-
|
|
1258
|
+
init_types();
|
|
1289
1259
|
Sandbox = class {
|
|
1290
1260
|
config;
|
|
1291
1261
|
tempDir = null;
|
|
@@ -1488,7 +1458,7 @@ export default defineConfig({
|
|
|
1488
1458
|
* Execute a command and capture output
|
|
1489
1459
|
*/
|
|
1490
1460
|
exec(command, args, options = {}) {
|
|
1491
|
-
return new Promise((
|
|
1461
|
+
return new Promise((resolve9) => {
|
|
1492
1462
|
let stdout = "";
|
|
1493
1463
|
let stderr = "";
|
|
1494
1464
|
let timedOut = false;
|
|
@@ -1508,7 +1478,7 @@ export default defineConfig({
|
|
|
1508
1478
|
}, timeout);
|
|
1509
1479
|
proc.on("close", (code) => {
|
|
1510
1480
|
clearTimeout(timer);
|
|
1511
|
-
|
|
1481
|
+
resolve9({
|
|
1512
1482
|
stdout,
|
|
1513
1483
|
stderr,
|
|
1514
1484
|
exitCode: code ?? 1,
|
|
@@ -1517,7 +1487,7 @@ export default defineConfig({
|
|
|
1517
1487
|
});
|
|
1518
1488
|
proc.on("error", (err3) => {
|
|
1519
1489
|
clearTimeout(timer);
|
|
1520
|
-
|
|
1490
|
+
resolve9({
|
|
1521
1491
|
stdout,
|
|
1522
1492
|
stderr: stderr + "\n" + err3.message,
|
|
1523
1493
|
exitCode: 1,
|
|
@@ -2776,7 +2746,7 @@ var init_runner = __esm({
|
|
|
2776
2746
|
init_sandbox();
|
|
2777
2747
|
init_results();
|
|
2778
2748
|
init_generator();
|
|
2779
|
-
|
|
2749
|
+
init_types();
|
|
2780
2750
|
ExecutionRunner = class {
|
|
2781
2751
|
sandbox;
|
|
2782
2752
|
dryRun;
|
|
@@ -2983,53 +2953,35 @@ ${code}
|
|
|
2983
2953
|
Generate 3-5 targeted payloads that would exploit THIS SPECIFIC code.
|
|
2984
2954
|
Consider the exact variable names, function calls, and data flow shown above.`;
|
|
2985
2955
|
}
|
|
2956
|
+
function matchFirst(code, patterns) {
|
|
2957
|
+
for (const [keywords, name] of patterns) {
|
|
2958
|
+
if (keywords.some((k) => code.includes(k))) {
|
|
2959
|
+
return name;
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
return void 0;
|
|
2963
|
+
}
|
|
2986
2964
|
function extractTechStack(code, filePath) {
|
|
2965
|
+
const ext = "." + (filePath.split(".").pop() ?? "");
|
|
2987
2966
|
const hints = {
|
|
2988
|
-
language:
|
|
2967
|
+
language: LANGUAGE_EXTENSIONS[ext] ?? "unknown",
|
|
2968
|
+
hasEscaping: ESCAPING_KEYWORDS.some((k) => code.includes(k)),
|
|
2969
|
+
hasWaf: WAF_KEYWORDS.some((k) => code.includes(k))
|
|
2989
2970
|
};
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
hints.database = "postgres";
|
|
3003
|
-
} else if (code.includes("mysql") || code.includes("?") && code.includes("query")) {
|
|
3004
|
-
hints.database = "mysql";
|
|
3005
|
-
} else if (code.includes("mongodb") || code.includes("mongoose") || code.includes("$where")) {
|
|
3006
|
-
hints.database = "mongodb";
|
|
3007
|
-
} else if (code.includes("sqlite") || code.includes("sqlite3")) {
|
|
3008
|
-
hints.database = "sqlite";
|
|
3009
|
-
}
|
|
3010
|
-
if (code.includes("prisma")) {
|
|
3011
|
-
hints.orm = "prisma";
|
|
3012
|
-
} else if (code.includes("sequelize")) {
|
|
3013
|
-
hints.orm = "sequelize";
|
|
3014
|
-
} else if (code.includes("typeorm") || code.includes("TypeORM")) {
|
|
3015
|
-
hints.orm = "typeorm";
|
|
3016
|
-
} else if (code.includes("sqlalchemy") || code.includes("SQLAlchemy")) {
|
|
3017
|
-
hints.orm = "sqlalchemy";
|
|
3018
|
-
}
|
|
3019
|
-
hints.hasEscaping = code.includes("escape") || code.includes("sanitize") || code.includes("DOMPurify") || code.includes("htmlspecialchars");
|
|
3020
|
-
hints.hasWaf = code.includes("waf") || code.includes("WAF") || code.includes("cloudflare") || code.includes("akamai");
|
|
2971
|
+
const framework = matchFirst(code, FRAMEWORK_PATTERNS);
|
|
2972
|
+
if (framework) {
|
|
2973
|
+
hints.framework = framework;
|
|
2974
|
+
}
|
|
2975
|
+
const database = matchFirst(code, DATABASE_PATTERNS);
|
|
2976
|
+
if (database) {
|
|
2977
|
+
hints.database = database;
|
|
2978
|
+
}
|
|
2979
|
+
const orm = matchFirst(code, ORM_PATTERNS);
|
|
2980
|
+
if (orm) {
|
|
2981
|
+
hints.orm = orm;
|
|
2982
|
+
}
|
|
3021
2983
|
return hints;
|
|
3022
2984
|
}
|
|
3023
|
-
function detectLanguage2(filePath) {
|
|
3024
|
-
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
|
|
3025
|
-
if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "javascript";
|
|
3026
|
-
if (filePath.endsWith(".py")) return "python";
|
|
3027
|
-
if (filePath.endsWith(".go")) return "go";
|
|
3028
|
-
if (filePath.endsWith(".java")) return "java";
|
|
3029
|
-
if (filePath.endsWith(".rb")) return "ruby";
|
|
3030
|
-
if (filePath.endsWith(".php")) return "php";
|
|
3031
|
-
return "unknown";
|
|
3032
|
-
}
|
|
3033
2985
|
function parseAiPayloadResponse(response) {
|
|
3034
2986
|
try {
|
|
3035
2987
|
const jsonMatch = response.match(/\{[\s\S]*"payloads"[\s\S]*\}/);
|
|
@@ -3080,7 +3032,7 @@ function getFallbackPayloads(context, maxPayloads = 10) {
|
|
|
3080
3032
|
}
|
|
3081
3033
|
return [...new Set(payloads)].slice(0, maxPayloads);
|
|
3082
3034
|
}
|
|
3083
|
-
var AI_PAYLOAD_SYSTEM_PROMPT;
|
|
3035
|
+
var AI_PAYLOAD_SYSTEM_PROMPT, FRAMEWORK_PATTERNS, DATABASE_PATTERNS, ORM_PATTERNS, ESCAPING_KEYWORDS, WAF_KEYWORDS, LANGUAGE_EXTENSIONS;
|
|
3084
3036
|
var init_ai_payloads = __esm({
|
|
3085
3037
|
"src/execution/ai-payloads.ts"() {
|
|
3086
3038
|
init_payloads();
|
|
@@ -3119,6 +3071,39 @@ Return payloads in JSON format:
|
|
|
3119
3071
|
}
|
|
3120
3072
|
]
|
|
3121
3073
|
}`;
|
|
3074
|
+
FRAMEWORK_PATTERNS = [
|
|
3075
|
+
[["express", "app.get", "app.post"], "express"],
|
|
3076
|
+
[["fastify"], "fastify"],
|
|
3077
|
+
[["django", "from django"], "django"],
|
|
3078
|
+
[["flask", "from flask"], "flask"],
|
|
3079
|
+
[["gin."], "gin"],
|
|
3080
|
+
[["fiber."], "fiber"]
|
|
3081
|
+
];
|
|
3082
|
+
DATABASE_PATTERNS = [
|
|
3083
|
+
[["postgres", "pg."], "postgres"],
|
|
3084
|
+
[["mysql"], "mysql"],
|
|
3085
|
+
[["mongodb", "mongoose", "$where"], "mongodb"],
|
|
3086
|
+
[["sqlite", "sqlite3"], "sqlite"]
|
|
3087
|
+
];
|
|
3088
|
+
ORM_PATTERNS = [
|
|
3089
|
+
[["prisma"], "prisma"],
|
|
3090
|
+
[["sequelize"], "sequelize"],
|
|
3091
|
+
[["typeorm", "TypeORM"], "typeorm"],
|
|
3092
|
+
[["sqlalchemy", "SQLAlchemy"], "sqlalchemy"]
|
|
3093
|
+
];
|
|
3094
|
+
ESCAPING_KEYWORDS = ["escape", "sanitize", "DOMPurify", "htmlspecialchars"];
|
|
3095
|
+
WAF_KEYWORDS = ["waf", "WAF", "cloudflare", "akamai"];
|
|
3096
|
+
LANGUAGE_EXTENSIONS = {
|
|
3097
|
+
".ts": "typescript",
|
|
3098
|
+
".tsx": "typescript",
|
|
3099
|
+
".js": "javascript",
|
|
3100
|
+
".jsx": "javascript",
|
|
3101
|
+
".py": "python",
|
|
3102
|
+
".go": "go",
|
|
3103
|
+
".java": "java",
|
|
3104
|
+
".rb": "ruby",
|
|
3105
|
+
".php": "php"
|
|
3106
|
+
};
|
|
3122
3107
|
}
|
|
3123
3108
|
});
|
|
3124
3109
|
|
|
@@ -3441,7 +3426,7 @@ __export(execution_exports, {
|
|
|
3441
3426
|
});
|
|
3442
3427
|
var init_execution = __esm({
|
|
3443
3428
|
"src/execution/index.ts"() {
|
|
3444
|
-
|
|
3429
|
+
init_types();
|
|
3445
3430
|
init_sandbox();
|
|
3446
3431
|
init_runner();
|
|
3447
3432
|
init_results();
|
|
@@ -3792,7 +3777,7 @@ function suggestConfidence(precision) {
|
|
|
3792
3777
|
return "low";
|
|
3793
3778
|
}
|
|
3794
3779
|
var EMPTY_FEEDBACK_STATE, CONFIDENCE_THRESHOLDS;
|
|
3795
|
-
var
|
|
3780
|
+
var init_types2 = __esm({
|
|
3796
3781
|
"src/feedback/types.ts"() {
|
|
3797
3782
|
EMPTY_FEEDBACK_STATE = {
|
|
3798
3783
|
version: 1,
|
|
@@ -3926,7 +3911,7 @@ function generateReport(state) {
|
|
|
3926
3911
|
var FEEDBACK_DIR, FEEDBACK_FILE;
|
|
3927
3912
|
var init_store = __esm({
|
|
3928
3913
|
"src/feedback/store.ts"() {
|
|
3929
|
-
|
|
3914
|
+
init_types2();
|
|
3930
3915
|
FEEDBACK_DIR = join(homedir(), ".pinata");
|
|
3931
3916
|
FEEDBACK_FILE = join(FEEDBACK_DIR, "feedback.json");
|
|
3932
3917
|
}
|
|
@@ -3948,7 +3933,7 @@ __export(feedback_exports, {
|
|
|
3948
3933
|
});
|
|
3949
3934
|
var init_feedback = __esm({
|
|
3950
3935
|
"src/feedback/index.ts"() {
|
|
3951
|
-
|
|
3936
|
+
init_types2();
|
|
3952
3937
|
init_store();
|
|
3953
3938
|
}
|
|
3954
3939
|
});
|
|
@@ -4598,7 +4583,7 @@ var Logger = class _Logger {
|
|
|
4598
4583
|
*/
|
|
4599
4584
|
debug(message, ...args) {
|
|
4600
4585
|
if (this.shouldLog("debug")) {
|
|
4601
|
-
console.debug(
|
|
4586
|
+
console.debug(chalk7.gray(this.format(message)), ...args);
|
|
4602
4587
|
}
|
|
4603
4588
|
}
|
|
4604
4589
|
/**
|
|
@@ -4614,7 +4599,7 @@ var Logger = class _Logger {
|
|
|
4614
4599
|
*/
|
|
4615
4600
|
warn(message, ...args) {
|
|
4616
4601
|
if (this.shouldLog("warn")) {
|
|
4617
|
-
console.warn(
|
|
4602
|
+
console.warn(chalk7.yellow(this.format(message)), ...args);
|
|
4618
4603
|
}
|
|
4619
4604
|
}
|
|
4620
4605
|
/**
|
|
@@ -4622,7 +4607,7 @@ var Logger = class _Logger {
|
|
|
4622
4607
|
*/
|
|
4623
4608
|
error(message, ...args) {
|
|
4624
4609
|
if (this.shouldLog("error")) {
|
|
4625
|
-
console.error(
|
|
4610
|
+
console.error(chalk7.red(this.format(message)), ...args);
|
|
4626
4611
|
}
|
|
4627
4612
|
}
|
|
4628
4613
|
/**
|
|
@@ -4630,7 +4615,7 @@ var Logger = class _Logger {
|
|
|
4630
4615
|
*/
|
|
4631
4616
|
success(message, ...args) {
|
|
4632
4617
|
if (this.shouldLog("info")) {
|
|
4633
|
-
console.info(
|
|
4618
|
+
console.info(chalk7.green(this.format(message)), ...args);
|
|
4634
4619
|
}
|
|
4635
4620
|
}
|
|
4636
4621
|
/**
|
|
@@ -5353,9 +5338,384 @@ function detectLanguage(filePath) {
|
|
|
5353
5338
|
const ext = extname(filePath).toLowerCase();
|
|
5354
5339
|
return EXTENSION_TO_LANGUAGE[ext] ?? null;
|
|
5355
5340
|
}
|
|
5341
|
+
var DETECTION_PATTERNS = [
|
|
5342
|
+
// CLI Detection
|
|
5343
|
+
{
|
|
5344
|
+
type: "cli",
|
|
5345
|
+
packageJson: {
|
|
5346
|
+
hasField: ["bin"]
|
|
5347
|
+
},
|
|
5348
|
+
weight: 10
|
|
5349
|
+
},
|
|
5350
|
+
{
|
|
5351
|
+
type: "cli",
|
|
5352
|
+
files: ["src/cli.ts", "src/cli/index.ts", "cli/index.ts"],
|
|
5353
|
+
weight: 5
|
|
5354
|
+
},
|
|
5355
|
+
{
|
|
5356
|
+
type: "cli",
|
|
5357
|
+
packageJson: {
|
|
5358
|
+
dependencies: ["commander", "yargs", "meow", "oclif", "inquirer", "prompts"]
|
|
5359
|
+
},
|
|
5360
|
+
weight: 3
|
|
5361
|
+
},
|
|
5362
|
+
// Web Server Detection
|
|
5363
|
+
{
|
|
5364
|
+
type: "web-server",
|
|
5365
|
+
packageJson: {
|
|
5366
|
+
dependencies: ["express", "fastify", "koa", "hapi", "@hapi/hapi", "restify"]
|
|
5367
|
+
},
|
|
5368
|
+
weight: 10
|
|
5369
|
+
},
|
|
5370
|
+
{
|
|
5371
|
+
type: "web-server",
|
|
5372
|
+
files: ["server.ts", "server.js", "app.ts", "app.js", "src/server.ts"],
|
|
5373
|
+
weight: 3
|
|
5374
|
+
},
|
|
5375
|
+
// API Detection
|
|
5376
|
+
{
|
|
5377
|
+
type: "api",
|
|
5378
|
+
files: ["routes/", "handlers/", "controllers/", "api/"],
|
|
5379
|
+
weight: 5
|
|
5380
|
+
},
|
|
5381
|
+
{
|
|
5382
|
+
type: "api",
|
|
5383
|
+
files: ["openapi.yaml", "openapi.json", "swagger.yaml", "swagger.json"],
|
|
5384
|
+
weight: 8
|
|
5385
|
+
},
|
|
5386
|
+
{
|
|
5387
|
+
type: "api",
|
|
5388
|
+
packageJson: {
|
|
5389
|
+
dependencies: ["@nestjs/core", "trpc", "@trpc/server"]
|
|
5390
|
+
},
|
|
5391
|
+
weight: 10
|
|
5392
|
+
},
|
|
5393
|
+
// Library Detection
|
|
5394
|
+
{
|
|
5395
|
+
type: "library",
|
|
5396
|
+
packageJson: {
|
|
5397
|
+
hasField: ["exports", "main", "module", "types"]
|
|
5398
|
+
},
|
|
5399
|
+
weight: 3
|
|
5400
|
+
},
|
|
5401
|
+
{
|
|
5402
|
+
type: "library",
|
|
5403
|
+
files: ["tsup.config.ts", "rollup.config.js", "vite.config.ts"],
|
|
5404
|
+
weight: 2
|
|
5405
|
+
},
|
|
5406
|
+
// Frontend SPA Detection
|
|
5407
|
+
{
|
|
5408
|
+
type: "frontend-spa",
|
|
5409
|
+
packageJson: {
|
|
5410
|
+
dependencies: ["react", "vue", "angular", "svelte", "@angular/core"]
|
|
5411
|
+
},
|
|
5412
|
+
weight: 5
|
|
5413
|
+
},
|
|
5414
|
+
{
|
|
5415
|
+
type: "frontend-spa",
|
|
5416
|
+
files: ["src/App.tsx", "src/App.vue", "src/app/app.component.ts"],
|
|
5417
|
+
weight: 8
|
|
5418
|
+
},
|
|
5419
|
+
// SSR Framework Detection
|
|
5420
|
+
{
|
|
5421
|
+
type: "ssr-framework",
|
|
5422
|
+
packageJson: {
|
|
5423
|
+
dependencies: ["next", "nuxt", "@nuxt/core", "remix", "@remix-run/node", "astro", "sveltekit"]
|
|
5424
|
+
},
|
|
5425
|
+
weight: 10
|
|
5426
|
+
},
|
|
5427
|
+
{
|
|
5428
|
+
type: "ssr-framework",
|
|
5429
|
+
files: ["next.config.js", "next.config.ts", "nuxt.config.ts", "remix.config.js", "astro.config.mjs"],
|
|
5430
|
+
weight: 10
|
|
5431
|
+
},
|
|
5432
|
+
// Serverless Detection
|
|
5433
|
+
{
|
|
5434
|
+
type: "serverless",
|
|
5435
|
+
files: ["serverless.yml", "serverless.yaml", "serverless.ts", "sam.yaml", "template.yaml"],
|
|
5436
|
+
weight: 10
|
|
5437
|
+
},
|
|
5438
|
+
{
|
|
5439
|
+
type: "serverless",
|
|
5440
|
+
packageJson: {
|
|
5441
|
+
dependencies: ["@aws-sdk/client-lambda", "aws-lambda", "@google-cloud/functions-framework"]
|
|
5442
|
+
},
|
|
5443
|
+
weight: 5
|
|
5444
|
+
},
|
|
5445
|
+
{
|
|
5446
|
+
type: "serverless",
|
|
5447
|
+
files: ["functions/", "lambda/", "netlify/functions/", "api/"],
|
|
5448
|
+
weight: 3
|
|
5449
|
+
},
|
|
5450
|
+
// Desktop Detection
|
|
5451
|
+
{
|
|
5452
|
+
type: "desktop",
|
|
5453
|
+
packageJson: {
|
|
5454
|
+
dependencies: ["electron", "@electron/remote", "tauri", "@tauri-apps/api"]
|
|
5455
|
+
},
|
|
5456
|
+
weight: 10
|
|
5457
|
+
},
|
|
5458
|
+
{
|
|
5459
|
+
type: "desktop",
|
|
5460
|
+
files: ["electron/main.ts", "src-tauri/", "electron.js", "main.electron.ts"],
|
|
5461
|
+
weight: 8
|
|
5462
|
+
},
|
|
5463
|
+
// Mobile Detection
|
|
5464
|
+
{
|
|
5465
|
+
type: "mobile",
|
|
5466
|
+
packageJson: {
|
|
5467
|
+
dependencies: ["react-native", "expo", "@react-native-community/cli"]
|
|
5468
|
+
},
|
|
5469
|
+
weight: 10
|
|
5470
|
+
},
|
|
5471
|
+
{
|
|
5472
|
+
type: "mobile",
|
|
5473
|
+
files: ["app.json", "metro.config.js", "ios/", "android/"],
|
|
5474
|
+
weight: 5
|
|
5475
|
+
},
|
|
5476
|
+
// Monorepo Detection
|
|
5477
|
+
{
|
|
5478
|
+
type: "monorepo",
|
|
5479
|
+
packageJson: {
|
|
5480
|
+
hasField: ["workspaces"]
|
|
5481
|
+
},
|
|
5482
|
+
weight: 10
|
|
5483
|
+
},
|
|
5484
|
+
{
|
|
5485
|
+
type: "monorepo",
|
|
5486
|
+
files: ["lerna.json", "pnpm-workspace.yaml", "turbo.json", "nx.json"],
|
|
5487
|
+
weight: 10
|
|
5488
|
+
},
|
|
5489
|
+
{
|
|
5490
|
+
type: "monorepo",
|
|
5491
|
+
files: ["packages/", "apps/"],
|
|
5492
|
+
weight: 5
|
|
5493
|
+
},
|
|
5494
|
+
// Script Detection (low weight, fallback)
|
|
5495
|
+
{
|
|
5496
|
+
type: "script",
|
|
5497
|
+
files: ["script.ts", "script.js", "run.ts", "run.js"],
|
|
5498
|
+
weight: 2
|
|
5499
|
+
}
|
|
5500
|
+
];
|
|
5501
|
+
var SCORING_ADJUSTMENTS = [
|
|
5502
|
+
// Blocking I/O is fine in CLI and scripts
|
|
5503
|
+
{
|
|
5504
|
+
categoryId: "blocking-io",
|
|
5505
|
+
skip: ["cli", "script", "desktop"],
|
|
5506
|
+
lowerWeight: ["serverless"]
|
|
5507
|
+
},
|
|
5508
|
+
// SQL injection not relevant for pure frontends
|
|
5509
|
+
{
|
|
5510
|
+
categoryId: "sql-injection",
|
|
5511
|
+
skip: ["frontend-spa", "mobile"],
|
|
5512
|
+
higherWeight: ["web-server", "api"]
|
|
5513
|
+
},
|
|
5514
|
+
// XSS is critical for frontends, less so for pure APIs
|
|
5515
|
+
{
|
|
5516
|
+
categoryId: "xss",
|
|
5517
|
+
higherWeight: ["frontend-spa", "ssr-framework"],
|
|
5518
|
+
lowerWeight: ["api", "cli"]
|
|
5519
|
+
},
|
|
5520
|
+
// SSRF is critical for servers
|
|
5521
|
+
{
|
|
5522
|
+
categoryId: "ssrf",
|
|
5523
|
+
skip: ["frontend-spa", "cli", "script"],
|
|
5524
|
+
higherWeight: ["web-server", "api", "serverless"]
|
|
5525
|
+
},
|
|
5526
|
+
// Connection pool exhaustion not relevant for serverless
|
|
5527
|
+
{
|
|
5528
|
+
categoryId: "connection-pool-exhaustion",
|
|
5529
|
+
skip: ["serverless", "frontend-spa", "cli"],
|
|
5530
|
+
higherWeight: ["web-server", "api"]
|
|
5531
|
+
},
|
|
5532
|
+
// Memory leaks are critical for long-running servers
|
|
5533
|
+
{
|
|
5534
|
+
categoryId: "memory-leak",
|
|
5535
|
+
skip: ["serverless", "script"],
|
|
5536
|
+
higherWeight: ["web-server", "desktop"]
|
|
5537
|
+
},
|
|
5538
|
+
// Rate limiting not needed for CLI
|
|
5539
|
+
{
|
|
5540
|
+
categoryId: "rate-limiting",
|
|
5541
|
+
skip: ["cli", "script", "library"],
|
|
5542
|
+
higherWeight: ["web-server", "api"]
|
|
5543
|
+
},
|
|
5544
|
+
// CSRF not relevant for CLI or pure APIs
|
|
5545
|
+
{
|
|
5546
|
+
categoryId: "csrf",
|
|
5547
|
+
skip: ["cli", "script", "library", "api"],
|
|
5548
|
+
higherWeight: ["web-server", "ssr-framework"]
|
|
5549
|
+
},
|
|
5550
|
+
// Deserialization critical for APIs, less so for frontends
|
|
5551
|
+
{
|
|
5552
|
+
categoryId: "deserialization",
|
|
5553
|
+
skip: ["frontend-spa"],
|
|
5554
|
+
higherWeight: ["api", "web-server"]
|
|
5555
|
+
},
|
|
5556
|
+
// Command injection critical for servers, OK in CLI
|
|
5557
|
+
{
|
|
5558
|
+
categoryId: "command-injection",
|
|
5559
|
+
lowerWeight: ["cli", "script"],
|
|
5560
|
+
higherWeight: ["web-server", "api", "serverless"]
|
|
5561
|
+
}
|
|
5562
|
+
];
|
|
5563
|
+
async function detectProjectType(projectPath) {
|
|
5564
|
+
const scores = /* @__PURE__ */ new Map();
|
|
5565
|
+
const evidence = [];
|
|
5566
|
+
const frameworks = [];
|
|
5567
|
+
const packageJsonPath = resolve(projectPath, "package.json");
|
|
5568
|
+
let packageJson = null;
|
|
5569
|
+
if (existsSync(packageJsonPath)) {
|
|
5570
|
+
try {
|
|
5571
|
+
const content = await readFile(packageJsonPath, "utf-8");
|
|
5572
|
+
packageJson = JSON.parse(content);
|
|
5573
|
+
} catch {
|
|
5574
|
+
}
|
|
5575
|
+
}
|
|
5576
|
+
for (const pattern of DETECTION_PATTERNS) {
|
|
5577
|
+
let matched = false;
|
|
5578
|
+
if (pattern.packageJson && packageJson) {
|
|
5579
|
+
if (pattern.packageJson.hasField) {
|
|
5580
|
+
for (const field of pattern.packageJson.hasField) {
|
|
5581
|
+
if (field in packageJson) {
|
|
5582
|
+
matched = true;
|
|
5583
|
+
evidence.push(`package.json has "${field}" field`);
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
}
|
|
5587
|
+
if (pattern.packageJson.dependencies) {
|
|
5588
|
+
const deps = packageJson["dependencies"];
|
|
5589
|
+
if (deps) {
|
|
5590
|
+
for (const dep of pattern.packageJson.dependencies) {
|
|
5591
|
+
if (dep in deps) {
|
|
5592
|
+
matched = true;
|
|
5593
|
+
evidence.push(`Uses ${dep}`);
|
|
5594
|
+
frameworks.push(dep);
|
|
5595
|
+
}
|
|
5596
|
+
}
|
|
5597
|
+
}
|
|
5598
|
+
}
|
|
5599
|
+
if (pattern.packageJson.devDependencies) {
|
|
5600
|
+
const devDeps = packageJson["devDependencies"];
|
|
5601
|
+
if (devDeps) {
|
|
5602
|
+
for (const dep of pattern.packageJson.devDependencies) {
|
|
5603
|
+
if (dep in devDeps) {
|
|
5604
|
+
matched = true;
|
|
5605
|
+
evidence.push(`Uses ${dep} (dev)`);
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
}
|
|
5609
|
+
}
|
|
5610
|
+
}
|
|
5611
|
+
if (pattern.files) {
|
|
5612
|
+
for (const file of pattern.files) {
|
|
5613
|
+
const filePath = resolve(projectPath, file);
|
|
5614
|
+
if (existsSync(filePath)) {
|
|
5615
|
+
matched = true;
|
|
5616
|
+
evidence.push(`Has ${file}`);
|
|
5617
|
+
}
|
|
5618
|
+
}
|
|
5619
|
+
}
|
|
5620
|
+
if (matched) {
|
|
5621
|
+
const current = scores.get(pattern.type) ?? 0;
|
|
5622
|
+
scores.set(pattern.type, current + pattern.weight);
|
|
5623
|
+
}
|
|
5624
|
+
}
|
|
5625
|
+
let bestType = "unknown";
|
|
5626
|
+
let bestScore = 0;
|
|
5627
|
+
for (const [type, score] of scores.entries()) {
|
|
5628
|
+
if (score > bestScore) {
|
|
5629
|
+
bestScore = score;
|
|
5630
|
+
bestType = type;
|
|
5631
|
+
}
|
|
5632
|
+
}
|
|
5633
|
+
let confidence = "low";
|
|
5634
|
+
if (bestScore >= 10) {
|
|
5635
|
+
confidence = "high";
|
|
5636
|
+
} else if (bestScore >= 5) {
|
|
5637
|
+
confidence = "medium";
|
|
5638
|
+
}
|
|
5639
|
+
const secondaryTypes = [];
|
|
5640
|
+
if (bestType === "monorepo") {
|
|
5641
|
+
for (const [type, score] of scores.entries()) {
|
|
5642
|
+
if (type !== "monorepo" && score >= 3) {
|
|
5643
|
+
secondaryTypes.push(type);
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
const languages = [];
|
|
5648
|
+
if (existsSync(resolve(projectPath, "tsconfig.json"))) languages.push("typescript");
|
|
5649
|
+
if (existsSync(resolve(projectPath, "package.json"))) languages.push("javascript");
|
|
5650
|
+
if (existsSync(resolve(projectPath, "requirements.txt"))) languages.push("python");
|
|
5651
|
+
if (existsSync(resolve(projectPath, "go.mod"))) languages.push("go");
|
|
5652
|
+
if (existsSync(resolve(projectPath, "Cargo.toml"))) languages.push("rust");
|
|
5653
|
+
if (existsSync(resolve(projectPath, "pom.xml"))) languages.push("java");
|
|
5654
|
+
const result = {
|
|
5655
|
+
type: bestType,
|
|
5656
|
+
confidence,
|
|
5657
|
+
evidence: [...new Set(evidence)],
|
|
5658
|
+
frameworks: [...new Set(frameworks)],
|
|
5659
|
+
languages
|
|
5660
|
+
};
|
|
5661
|
+
if (secondaryTypes.length > 0) {
|
|
5662
|
+
result.secondaryTypes = secondaryTypes;
|
|
5663
|
+
}
|
|
5664
|
+
return result;
|
|
5665
|
+
}
|
|
5666
|
+
function getCategoryWeight(categoryId, projectType) {
|
|
5667
|
+
const adjustment = SCORING_ADJUSTMENTS.find((a) => a.categoryId === categoryId);
|
|
5668
|
+
if (!adjustment) return 1;
|
|
5669
|
+
if (adjustment.skip?.includes(projectType)) return 0;
|
|
5670
|
+
if (adjustment.higherWeight?.includes(projectType)) return 1.5;
|
|
5671
|
+
if (adjustment.lowerWeight?.includes(projectType)) return 0.5;
|
|
5672
|
+
return 1;
|
|
5673
|
+
}
|
|
5674
|
+
function getProjectTypeDescription(type) {
|
|
5675
|
+
const descriptions = {
|
|
5676
|
+
"cli": "Command-line tool",
|
|
5677
|
+
"web-server": "Web server (Express, Fastify, etc.)",
|
|
5678
|
+
"api": "REST/GraphQL API",
|
|
5679
|
+
"library": "Library/package for consumption",
|
|
5680
|
+
"frontend-spa": "Frontend single-page application",
|
|
5681
|
+
"ssr-framework": "Server-side rendering framework",
|
|
5682
|
+
"serverless": "Serverless function",
|
|
5683
|
+
"desktop": "Desktop application",
|
|
5684
|
+
"mobile": "Mobile application",
|
|
5685
|
+
"script": "Script/automation",
|
|
5686
|
+
"monorepo": "Monorepo workspace",
|
|
5687
|
+
"unknown": "Unknown project type"
|
|
5688
|
+
};
|
|
5689
|
+
return descriptions[type];
|
|
5690
|
+
}
|
|
5356
5691
|
init_errors();
|
|
5357
5692
|
init_result();
|
|
5358
|
-
|
|
5693
|
+
var SEVERITY_WEIGHTS = {
|
|
5694
|
+
critical: 4,
|
|
5695
|
+
high: 3,
|
|
5696
|
+
medium: 2,
|
|
5697
|
+
low: 1
|
|
5698
|
+
};
|
|
5699
|
+
var CONFIDENCE_WEIGHTS = {
|
|
5700
|
+
high: 1,
|
|
5701
|
+
medium: 0.7,
|
|
5702
|
+
low: 0.4
|
|
5703
|
+
};
|
|
5704
|
+
var PRIORITY_WEIGHTS = {
|
|
5705
|
+
P0: 3,
|
|
5706
|
+
P1: 2,
|
|
5707
|
+
P2: 1
|
|
5708
|
+
};
|
|
5709
|
+
var DEFAULT_TEST_PATTERNS = {
|
|
5710
|
+
python: ["test_*.py", "*_test.py", "tests/**/*.py", "test/**/*.py"],
|
|
5711
|
+
typescript: ["*.test.ts", "*.spec.ts", "__tests__/**/*.ts", "tests/**/*.ts"],
|
|
5712
|
+
javascript: ["*.test.js", "*.spec.js", "__tests__/**/*.js", "tests/**/*.js"],
|
|
5713
|
+
go: ["*_test.go"],
|
|
5714
|
+
java: ["*Test.java", "*Tests.java", "src/test/**/*.java"],
|
|
5715
|
+
rust: ["tests/**/*.rs"]
|
|
5716
|
+
};
|
|
5717
|
+
|
|
5718
|
+
// src/core/scanner/scanner.ts
|
|
5359
5719
|
var DEFAULT_OPTIONS = {
|
|
5360
5720
|
excludeDirs: [
|
|
5361
5721
|
// Package managers
|
|
@@ -5438,6 +5798,8 @@ var Scanner = class {
|
|
|
5438
5798
|
} catch {
|
|
5439
5799
|
return err(new AnalysisError(`Directory not found: ${targetDirectory}`));
|
|
5440
5800
|
}
|
|
5801
|
+
const projectType = await detectProjectType(targetDirectory);
|
|
5802
|
+
this.log.info(`Detected project type: ${projectType.type} (${projectType.confidence} confidence)`);
|
|
5441
5803
|
const categoriesResult = this.getCategoriesToScan(opts);
|
|
5442
5804
|
if (!categoriesResult.success) {
|
|
5443
5805
|
return categoriesResult;
|
|
@@ -5493,19 +5855,27 @@ var Scanner = class {
|
|
|
5493
5855
|
}
|
|
5494
5856
|
fileStats.testFiles = testFiles.size;
|
|
5495
5857
|
fileStats.sourceFiles = fileStats.totalFiles - testFiles.size;
|
|
5496
|
-
const
|
|
5858
|
+
const allGaps = this.detectionsToGaps(allDetections, categories, testFiles, opts);
|
|
5859
|
+
const gaps = allGaps.filter((gap) => {
|
|
5860
|
+
const weight = getCategoryWeight(gap.categoryId, projectType.type);
|
|
5861
|
+
return weight > 0;
|
|
5862
|
+
});
|
|
5863
|
+
if (allGaps.length !== gaps.length) {
|
|
5864
|
+
this.log.info(`Filtered ${allGaps.length - gaps.length} gaps as irrelevant for ${projectType.type} project type`);
|
|
5865
|
+
}
|
|
5497
5866
|
const filesWithGaps = new Set(gaps.map((g) => g.filePath));
|
|
5498
5867
|
fileStats.filesWithGaps = filesWithGaps.size;
|
|
5499
5868
|
const gapsByCategory = this.groupGapsByCategory(gaps);
|
|
5500
5869
|
const gapsByFile = this.groupGapsByFile(gaps);
|
|
5501
5870
|
const coverage = this.calculateCoverage(categories, gapsByCategory);
|
|
5502
|
-
const score = this.calculateScore(gaps, coverage, categories);
|
|
5871
|
+
const score = this.calculateScore(gaps, coverage, categories, projectType.type);
|
|
5503
5872
|
const summary = this.buildSummary(gaps, score, coverage, fileStats, categories);
|
|
5504
5873
|
const completedAt = /* @__PURE__ */ new Date();
|
|
5505
5874
|
const durationMs = completedAt.getTime() - startedAt.getTime();
|
|
5506
5875
|
this.log.info(`Scan complete: ${gaps.length} gaps found in ${durationMs}ms`);
|
|
5507
5876
|
return ok({
|
|
5508
5877
|
targetDirectory,
|
|
5878
|
+
projectType,
|
|
5509
5879
|
startedAt,
|
|
5510
5880
|
completedAt,
|
|
5511
5881
|
durationMs,
|
|
@@ -5531,8 +5901,13 @@ var Scanner = class {
|
|
|
5531
5901
|
}
|
|
5532
5902
|
/**
|
|
5533
5903
|
* Calculate Pinata Score from gaps and coverage
|
|
5904
|
+
*
|
|
5905
|
+
* @param gaps - Detected gaps
|
|
5906
|
+
* @param coverage - Coverage metrics
|
|
5907
|
+
* @param categories - Categories that were scanned
|
|
5908
|
+
* @param projectType - Detected project type for context-aware weighting
|
|
5534
5909
|
*/
|
|
5535
|
-
calculateScore(gaps, coverage, categories) {
|
|
5910
|
+
calculateScore(gaps, coverage, categories, projectType = "unknown") {
|
|
5536
5911
|
let baseScore = 100;
|
|
5537
5912
|
const penalties = [];
|
|
5538
5913
|
const bonuses = [];
|
|
@@ -5544,14 +5919,19 @@ var Scanner = class {
|
|
|
5544
5919
|
const severityWeight = SEVERITY_WEIGHTS[gap.severity];
|
|
5545
5920
|
const confidenceWeight = CONFIDENCE_WEIGHTS[gap.confidence];
|
|
5546
5921
|
const priorityWeight = PRIORITY_WEIGHTS[gap.priority];
|
|
5922
|
+
const projectTypeWeight = getCategoryWeight(gap.categoryId, projectType);
|
|
5547
5923
|
const basePenalty = 2;
|
|
5548
|
-
const penalty = basePenalty * severityWeight * confidenceWeight * Math.sqrt(priorityWeight);
|
|
5924
|
+
const penalty = basePenalty * severityWeight * confidenceWeight * Math.sqrt(priorityWeight) * projectTypeWeight;
|
|
5925
|
+
if (projectTypeWeight === 0) {
|
|
5926
|
+
continue;
|
|
5927
|
+
}
|
|
5549
5928
|
baseScore -= penalty;
|
|
5550
5929
|
const currentDomainScore = domainScores.get(gap.domain) ?? 100;
|
|
5551
5930
|
domainScores.set(gap.domain, Math.max(0, currentDomainScore - penalty * 2));
|
|
5552
5931
|
if (penalty >= 5) {
|
|
5932
|
+
const weightNote = projectTypeWeight !== 1 ? ` [${projectType} weight: ${projectTypeWeight}x]` : "";
|
|
5553
5933
|
penalties.push({
|
|
5554
|
-
reason: `${gap.severity} ${gap.domain} gap: ${gap.categoryName}`,
|
|
5934
|
+
reason: `${gap.severity} ${gap.domain} gap: ${gap.categoryName}${weightNote}`,
|
|
5555
5935
|
points: Math.round(penalty),
|
|
5556
5936
|
categoryId: gap.categoryId
|
|
5557
5937
|
});
|
|
@@ -5965,9 +6345,6 @@ function createScanner(categoryStore) {
|
|
|
5965
6345
|
return new Scanner(categoryStore);
|
|
5966
6346
|
}
|
|
5967
6347
|
|
|
5968
|
-
// src/core/scanner/index.ts
|
|
5969
|
-
init_types();
|
|
5970
|
-
|
|
5971
6348
|
// src/core/index.ts
|
|
5972
6349
|
var VERSION = "0.4.0";
|
|
5973
6350
|
|
|
@@ -6701,34 +7078,34 @@ function createRenderer(options) {
|
|
|
6701
7078
|
return new TemplateRenderer(options);
|
|
6702
7079
|
}
|
|
6703
7080
|
var SEVERITY_COLORS = {
|
|
6704
|
-
critical:
|
|
6705
|
-
high:
|
|
6706
|
-
medium:
|
|
6707
|
-
low:
|
|
7081
|
+
critical: chalk7.red.bold,
|
|
7082
|
+
high: chalk7.red,
|
|
7083
|
+
medium: chalk7.yellow,
|
|
7084
|
+
low: chalk7.gray
|
|
6708
7085
|
};
|
|
6709
7086
|
var PRIORITY_COLORS = {
|
|
6710
|
-
P0:
|
|
6711
|
-
P1:
|
|
6712
|
-
P2:
|
|
7087
|
+
P0: chalk7.red.bold,
|
|
7088
|
+
P1: chalk7.yellow,
|
|
7089
|
+
P2: chalk7.gray
|
|
6713
7090
|
};
|
|
6714
7091
|
var DOMAIN_COLORS = {
|
|
6715
|
-
security:
|
|
6716
|
-
data:
|
|
6717
|
-
concurrency:
|
|
6718
|
-
input:
|
|
6719
|
-
resource:
|
|
6720
|
-
reliability:
|
|
6721
|
-
performance:
|
|
6722
|
-
platform:
|
|
6723
|
-
business:
|
|
6724
|
-
compliance:
|
|
7092
|
+
security: chalk7.red,
|
|
7093
|
+
data: chalk7.blue,
|
|
7094
|
+
concurrency: chalk7.magenta,
|
|
7095
|
+
input: chalk7.cyan,
|
|
7096
|
+
resource: chalk7.yellow,
|
|
7097
|
+
reliability: chalk7.green,
|
|
7098
|
+
performance: chalk7.yellowBright,
|
|
7099
|
+
platform: chalk7.gray,
|
|
7100
|
+
business: chalk7.white,
|
|
7101
|
+
compliance: chalk7.blueBright
|
|
6725
7102
|
};
|
|
6726
7103
|
function formatTerminal(categories) {
|
|
6727
7104
|
if (categories.length === 0) {
|
|
6728
|
-
return
|
|
7105
|
+
return chalk7.yellow("No categories found matching the filters.");
|
|
6729
7106
|
}
|
|
6730
7107
|
const lines = [];
|
|
6731
|
-
lines.push(
|
|
7108
|
+
lines.push(chalk7.bold.underline(`Found ${categories.length} categories:
|
|
6732
7109
|
`));
|
|
6733
7110
|
const byDomain = /* @__PURE__ */ new Map();
|
|
6734
7111
|
for (const cat of categories) {
|
|
@@ -6739,26 +7116,26 @@ function formatTerminal(categories) {
|
|
|
6739
7116
|
byDomain.get(domain).push(cat);
|
|
6740
7117
|
}
|
|
6741
7118
|
for (const [domain, domainCategories] of byDomain) {
|
|
6742
|
-
const domainColor = DOMAIN_COLORS[domain] ??
|
|
7119
|
+
const domainColor = DOMAIN_COLORS[domain] ?? chalk7.white;
|
|
6743
7120
|
lines.push(domainColor.bold(`
|
|
6744
7121
|
${domain.toUpperCase()} (${domainCategories.length})`));
|
|
6745
|
-
lines.push(
|
|
7122
|
+
lines.push(chalk7.gray("\u2500".repeat(40)));
|
|
6746
7123
|
for (const cat of domainCategories) {
|
|
6747
7124
|
const priorityColor = PRIORITY_COLORS[cat.priority];
|
|
6748
7125
|
const severityColor = SEVERITY_COLORS[cat.severity];
|
|
6749
7126
|
const priority = priorityColor(`[${cat.priority}]`);
|
|
6750
7127
|
const severity = severityColor(`${cat.severity}`);
|
|
6751
|
-
const level =
|
|
6752
|
-
const name =
|
|
6753
|
-
const id =
|
|
7128
|
+
const level = chalk7.cyan(`${cat.level}`);
|
|
7129
|
+
const name = chalk7.white.bold(cat.name);
|
|
7130
|
+
const id = chalk7.gray(`(${cat.id})`);
|
|
6754
7131
|
lines.push(` ${priority} ${name} ${id}`);
|
|
6755
7132
|
lines.push(` ${severity} | ${level}`);
|
|
6756
7133
|
const desc = cat.description.length > 80 ? cat.description.slice(0, 77) + "..." : cat.description;
|
|
6757
|
-
lines.push(
|
|
7134
|
+
lines.push(chalk7.gray(` ${desc}`));
|
|
6758
7135
|
lines.push("");
|
|
6759
7136
|
}
|
|
6760
7137
|
}
|
|
6761
|
-
lines.push(
|
|
7138
|
+
lines.push(chalk7.gray("\u2500".repeat(40)));
|
|
6762
7139
|
lines.push(formatStats(categories));
|
|
6763
7140
|
return lines.join("\n");
|
|
6764
7141
|
}
|
|
@@ -6780,7 +7157,7 @@ function formatStats(categories) {
|
|
|
6780
7157
|
if (stats.P0 > 0) parts.push(PRIORITY_COLORS.P0(`${stats.P0} P0`));
|
|
6781
7158
|
if (stats.P1 > 0) parts.push(PRIORITY_COLORS.P1(`${stats.P1} P1`));
|
|
6782
7159
|
if (stats.P2 > 0) parts.push(PRIORITY_COLORS.P2(`${stats.P2} P2`));
|
|
6783
|
-
parts.push(
|
|
7160
|
+
parts.push(chalk7.gray("|"));
|
|
6784
7161
|
if (stats.critical > 0) parts.push(SEVERITY_COLORS.critical(`${stats.critical} critical`));
|
|
6785
7162
|
if (stats.high > 0) parts.push(SEVERITY_COLORS.high(`${stats.high} high`));
|
|
6786
7163
|
if (stats.medium > 0) parts.push(SEVERITY_COLORS.medium(`${stats.medium} medium`));
|
|
@@ -6837,13 +7214,9 @@ function isValidOutputFormat(format) {
|
|
|
6837
7214
|
return ["terminal", "json", "markdown"].includes(format);
|
|
6838
7215
|
}
|
|
6839
7216
|
function formatError(error) {
|
|
6840
|
-
return
|
|
7217
|
+
return chalk7.red(`Error: ${error.message}`);
|
|
6841
7218
|
}
|
|
6842
7219
|
|
|
6843
|
-
// src/cli/generate-formatters.ts
|
|
6844
|
-
init_errors();
|
|
6845
|
-
init_result();
|
|
6846
|
-
|
|
6847
7220
|
// src/ai/service.ts
|
|
6848
7221
|
var DEFAULT_CONFIG = {
|
|
6849
7222
|
provider: "anthropic",
|
|
@@ -6890,11 +7263,11 @@ var AIService = class {
|
|
|
6890
7263
|
*/
|
|
6891
7264
|
getApiKeyFromConfig(provider) {
|
|
6892
7265
|
try {
|
|
6893
|
-
const { existsSync:
|
|
7266
|
+
const { existsSync: existsSync6, readFileSync: readFileSync3 } = __require("fs");
|
|
6894
7267
|
const { homedir: homedir3 } = __require("os");
|
|
6895
7268
|
const { join: join5 } = __require("path");
|
|
6896
7269
|
const configPath = join5(homedir3(), ".pinata", "config.json");
|
|
6897
|
-
if (!
|
|
7270
|
+
if (!existsSync6(configPath)) {
|
|
6898
7271
|
return "";
|
|
6899
7272
|
}
|
|
6900
7273
|
const content = readFileSync3(configPath, "utf-8");
|
|
@@ -7145,91 +7518,202 @@ function createAIService(config2) {
|
|
|
7145
7518
|
return new AIService(config2);
|
|
7146
7519
|
}
|
|
7147
7520
|
|
|
7148
|
-
// src/ai/
|
|
7149
|
-
var SYSTEM_PROMPT = `You are
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
3. The variable type to ensure correct formatting
|
|
7521
|
+
// src/ai/explainer.ts
|
|
7522
|
+
var SYSTEM_PROMPT = `You are a security expert explaining code vulnerabilities to developers.
|
|
7523
|
+
Your explanations should be:
|
|
7524
|
+
- Clear and actionable
|
|
7525
|
+
- Focused on the specific code pattern
|
|
7526
|
+
- Include concrete remediation steps
|
|
7527
|
+
- Reference relevant security standards (OWASP, CWE) when applicable
|
|
7156
7528
|
|
|
7157
7529
|
Always respond with valid JSON matching this structure:
|
|
7158
7530
|
{
|
|
7159
|
-
"
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
}
|
|
7168
|
-
|
|
7169
|
-
For arrays, use: "value": ["item1", "item2"]
|
|
7170
|
-
For booleans, use: "value": true or "value": false
|
|
7171
|
-
For numbers, use: "value": 42`;
|
|
7172
|
-
async function suggestVariables(request, config2) {
|
|
7531
|
+
"summary": "1-2 sentence summary",
|
|
7532
|
+
"explanation": "Detailed explanation of the vulnerability",
|
|
7533
|
+
"risk": "What an attacker could do if this is exploited",
|
|
7534
|
+
"remediation": "Step-by-step instructions to fix",
|
|
7535
|
+
"safeExample": "Code example showing the safe pattern",
|
|
7536
|
+
"references": ["optional array of CVE/CWE/OWASP references"]
|
|
7537
|
+
}`;
|
|
7538
|
+
async function explainGap(gap, category, config2) {
|
|
7173
7539
|
const ai = createAIService(config2);
|
|
7174
7540
|
if (!ai.isConfigured()) {
|
|
7175
7541
|
return {
|
|
7176
|
-
success:
|
|
7177
|
-
|
|
7542
|
+
success: false,
|
|
7543
|
+
error: "AI service not configured",
|
|
7178
7544
|
durationMs: 0
|
|
7179
7545
|
};
|
|
7180
7546
|
}
|
|
7181
|
-
const prompt =
|
|
7182
|
-
const startTime = Date.now();
|
|
7547
|
+
const prompt = buildExplainPrompt(gap);
|
|
7183
7548
|
const response = await ai.completeJSON({
|
|
7184
7549
|
systemPrompt: SYSTEM_PROMPT,
|
|
7185
7550
|
messages: [{ role: "user", content: prompt }],
|
|
7186
7551
|
maxTokens: 1024,
|
|
7187
|
-
temperature: 0.
|
|
7552
|
+
temperature: 0.3
|
|
7188
7553
|
});
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7192
|
-
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
|
|
7204
|
-
}
|
|
7205
|
-
}
|
|
7206
|
-
for (const variable of request.variables) {
|
|
7207
|
-
if (!suggestions.has(variable.name) && !(variable.name in values)) {
|
|
7208
|
-
if (variable.defaultValue !== void 0) {
|
|
7209
|
-
values[variable.name] = variable.defaultValue;
|
|
7210
|
-
} else {
|
|
7211
|
-
unfilled.push(variable.name);
|
|
7212
|
-
}
|
|
7554
|
+
return response;
|
|
7555
|
+
}
|
|
7556
|
+
async function explainGaps(gaps, categories, config2) {
|
|
7557
|
+
const results = /* @__PURE__ */ new Map();
|
|
7558
|
+
const BATCH_SIZE = 5;
|
|
7559
|
+
for (let i = 0; i < gaps.length; i += BATCH_SIZE) {
|
|
7560
|
+
const batch = gaps.slice(i, i + BATCH_SIZE);
|
|
7561
|
+
const promises = batch.map(async (gap) => {
|
|
7562
|
+
const category = categories?.get(gap.categoryId);
|
|
7563
|
+
const result = await explainGap(gap, category, config2);
|
|
7564
|
+
return { key: `${gap.filePath}:${gap.lineStart}:${gap.categoryId}`, result };
|
|
7565
|
+
});
|
|
7566
|
+
const batchResults = await Promise.all(promises);
|
|
7567
|
+
for (const { key, result } of batchResults) {
|
|
7568
|
+
results.set(key, result);
|
|
7213
7569
|
}
|
|
7214
7570
|
}
|
|
7215
|
-
|
|
7216
|
-
success: true,
|
|
7217
|
-
data: { suggestions, unfilled, values },
|
|
7218
|
-
durationMs: response.durationMs
|
|
7219
|
-
};
|
|
7220
|
-
if (response.usage) {
|
|
7221
|
-
result.usage = response.usage;
|
|
7222
|
-
}
|
|
7223
|
-
return result;
|
|
7571
|
+
return results;
|
|
7224
7572
|
}
|
|
7225
|
-
function
|
|
7573
|
+
function buildExplainPrompt(gap, category) {
|
|
7226
7574
|
const parts = [];
|
|
7227
|
-
parts.push(
|
|
7228
|
-
|
|
7229
|
-
parts.push(
|
|
7230
|
-
parts.push(
|
|
7231
|
-
parts.push(
|
|
7232
|
-
parts.push(`**File:** ${
|
|
7575
|
+
parts.push(`Explain this security finding:
|
|
7576
|
+
`);
|
|
7577
|
+
parts.push(`**Category:** ${gap.categoryName} (${gap.categoryId})`);
|
|
7578
|
+
parts.push(`**Severity:** ${gap.severity}`);
|
|
7579
|
+
parts.push(`**Confidence:** ${gap.confidence}`);
|
|
7580
|
+
parts.push(`**File:** ${gap.filePath}`);
|
|
7581
|
+
parts.push(`**Line:** ${gap.lineStart}`);
|
|
7582
|
+
if (gap.codeSnippet) {
|
|
7583
|
+
parts.push(`
|
|
7584
|
+
**Code:**
|
|
7585
|
+
\`\`\`
|
|
7586
|
+
${gap.codeSnippet}
|
|
7587
|
+
\`\`\``);
|
|
7588
|
+
}
|
|
7589
|
+
parts.push(`
|
|
7590
|
+
**Pattern:** ${gap.patternId}`);
|
|
7591
|
+
parts.push(`**Detection Type:** ${gap.patternType}`);
|
|
7592
|
+
parts.push(`
|
|
7593
|
+
Provide a clear, actionable explanation for a developer.`);
|
|
7594
|
+
return parts.join("\n");
|
|
7595
|
+
}
|
|
7596
|
+
function generateFallbackExplanation(gap) {
|
|
7597
|
+
const summaries = {
|
|
7598
|
+
"sql-injection": "SQL query constructed with user input may allow injection attacks.",
|
|
7599
|
+
"xss": "User input rendered without escaping may allow script injection.",
|
|
7600
|
+
"command-injection": "Shell command constructed with user input may allow command execution.",
|
|
7601
|
+
"path-traversal": "File path constructed with user input may allow directory traversal.",
|
|
7602
|
+
"hardcoded-secrets": "Sensitive credentials found in source code.",
|
|
7603
|
+
"deserialization": "Untrusted data deserialization may allow code execution.",
|
|
7604
|
+
"ssrf": "Server-side request with user-controlled URL may allow internal access.",
|
|
7605
|
+
"xxe": "XML parser may be vulnerable to external entity injection.",
|
|
7606
|
+
"csrf": "State-changing request lacks CSRF protection.",
|
|
7607
|
+
"ldap-injection": "LDAP query constructed with user input may allow injection."
|
|
7608
|
+
};
|
|
7609
|
+
const remediations = {
|
|
7610
|
+
"sql-injection": "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
|
|
7611
|
+
"xss": "Escape all user input before rendering in HTML. Use framework auto-escaping features.",
|
|
7612
|
+
"command-injection": "Avoid shell execution with user input. Use allowlists and subprocess arrays instead of shell strings.",
|
|
7613
|
+
"path-traversal": "Validate and sanitize file paths. Use path.resolve() and verify the result is within allowed directories.",
|
|
7614
|
+
"hardcoded-secrets": "Move secrets to environment variables or a secrets manager. Never commit credentials to source control.",
|
|
7615
|
+
"deserialization": "Avoid deserializing untrusted data. If necessary, use safe formats like JSON instead of pickle/yaml.",
|
|
7616
|
+
"ssrf": "Validate and allowlist URLs. Block private IP ranges and localhost.",
|
|
7617
|
+
"xxe": "Disable external entity processing in XML parser configuration.",
|
|
7618
|
+
"csrf": "Implement CSRF tokens for all state-changing requests.",
|
|
7619
|
+
"ldap-injection": "Escape special LDAP characters in user input. Use parameterized LDAP queries."
|
|
7620
|
+
};
|
|
7621
|
+
const summary = summaries[gap.categoryId] ?? `Potential ${gap.categoryName} vulnerability detected.`;
|
|
7622
|
+
const remediation = remediations[gap.categoryId] ?? `Review the code for security issues and apply appropriate fixes.`;
|
|
7623
|
+
return {
|
|
7624
|
+
summary,
|
|
7625
|
+
explanation: `The pattern "${gap.patternId}" detected a potential ${gap.categoryName} vulnerability at line ${gap.lineStart}. This type of issue has ${gap.severity} severity and was detected with ${gap.confidence} confidence.`,
|
|
7626
|
+
risk: `If exploited, this vulnerability could compromise the security of the application. Severity: ${gap.severity}.`,
|
|
7627
|
+
remediation,
|
|
7628
|
+
references: []
|
|
7629
|
+
};
|
|
7630
|
+
}
|
|
7631
|
+
|
|
7632
|
+
// src/ai/template-filler.ts
|
|
7633
|
+
var SYSTEM_PROMPT2 = `You are an expert at analyzing code and extracting meaningful variable values for test generation.
|
|
7634
|
+
Given a code snippet and a list of template variables, suggest appropriate values for each variable.
|
|
7635
|
+
|
|
7636
|
+
For each variable, analyze:
|
|
7637
|
+
1. The code snippet to extract relevant information (class names, function names, etc.)
|
|
7638
|
+
2. The variable description to understand what's needed
|
|
7639
|
+
3. The variable type to ensure correct formatting
|
|
7640
|
+
|
|
7641
|
+
Always respond with valid JSON matching this structure:
|
|
7642
|
+
{
|
|
7643
|
+
"suggestions": [
|
|
7644
|
+
{
|
|
7645
|
+
"name": "variableName",
|
|
7646
|
+
"value": "suggested value",
|
|
7647
|
+
"reasoning": "why this value was chosen",
|
|
7648
|
+
"confidence": 0.0-1.0
|
|
7649
|
+
}
|
|
7650
|
+
]
|
|
7651
|
+
}
|
|
7652
|
+
|
|
7653
|
+
For arrays, use: "value": ["item1", "item2"]
|
|
7654
|
+
For booleans, use: "value": true or "value": false
|
|
7655
|
+
For numbers, use: "value": 42`;
|
|
7656
|
+
async function suggestVariables(request, config2) {
|
|
7657
|
+
const ai = createAIService(config2);
|
|
7658
|
+
if (!ai.isConfigured()) {
|
|
7659
|
+
return {
|
|
7660
|
+
success: true,
|
|
7661
|
+
data: extractVariablesFromCode(request),
|
|
7662
|
+
durationMs: 0
|
|
7663
|
+
};
|
|
7664
|
+
}
|
|
7665
|
+
const prompt = buildVariablePrompt(request);
|
|
7666
|
+
const startTime = Date.now();
|
|
7667
|
+
const response = await ai.completeJSON({
|
|
7668
|
+
systemPrompt: SYSTEM_PROMPT2,
|
|
7669
|
+
messages: [{ role: "user", content: prompt }],
|
|
7670
|
+
maxTokens: 1024,
|
|
7671
|
+
temperature: 0.2
|
|
7672
|
+
});
|
|
7673
|
+
if (!response.success || !response.data) {
|
|
7674
|
+
return {
|
|
7675
|
+
success: true,
|
|
7676
|
+
data: extractVariablesFromCode(request),
|
|
7677
|
+
durationMs: Date.now() - startTime
|
|
7678
|
+
};
|
|
7679
|
+
}
|
|
7680
|
+
const suggestions = /* @__PURE__ */ new Map();
|
|
7681
|
+
const unfilled = [];
|
|
7682
|
+
const values = { ...request.existingValues };
|
|
7683
|
+
const suggestionsList = response.data.suggestions ?? [];
|
|
7684
|
+
for (const suggestion of suggestionsList) {
|
|
7685
|
+
suggestions.set(suggestion.name, suggestion);
|
|
7686
|
+
if (!(suggestion.name in values)) {
|
|
7687
|
+
values[suggestion.name] = suggestion.value;
|
|
7688
|
+
}
|
|
7689
|
+
}
|
|
7690
|
+
for (const variable of request.variables) {
|
|
7691
|
+
if (!suggestions.has(variable.name) && !(variable.name in values)) {
|
|
7692
|
+
if (variable.defaultValue !== void 0) {
|
|
7693
|
+
values[variable.name] = variable.defaultValue;
|
|
7694
|
+
} else {
|
|
7695
|
+
unfilled.push(variable.name);
|
|
7696
|
+
}
|
|
7697
|
+
}
|
|
7698
|
+
}
|
|
7699
|
+
const result = {
|
|
7700
|
+
success: true,
|
|
7701
|
+
data: { suggestions, unfilled, values },
|
|
7702
|
+
durationMs: response.durationMs
|
|
7703
|
+
};
|
|
7704
|
+
if (response.usage) {
|
|
7705
|
+
result.usage = response.usage;
|
|
7706
|
+
}
|
|
7707
|
+
return result;
|
|
7708
|
+
}
|
|
7709
|
+
function buildVariablePrompt(request) {
|
|
7710
|
+
const parts = [];
|
|
7711
|
+
parts.push("Analyze this code and suggest values for the template variables:\n");
|
|
7712
|
+
parts.push("**Code:**");
|
|
7713
|
+
parts.push("```");
|
|
7714
|
+
parts.push(request.codeSnippet);
|
|
7715
|
+
parts.push("```\n");
|
|
7716
|
+
parts.push(`**File:** ${request.filePath}
|
|
7233
7717
|
`);
|
|
7234
7718
|
if (request.gap) {
|
|
7235
7719
|
parts.push(`**Category:** ${request.gap.categoryName}`);
|
|
@@ -7372,606 +7856,258 @@ function toPascalCase(str) {
|
|
|
7372
7856
|
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
7373
7857
|
}
|
|
7374
7858
|
|
|
7375
|
-
// src/
|
|
7376
|
-
|
|
7377
|
-
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
7383
|
-
|
|
7384
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
|
|
7388
|
-
|
|
7389
|
-
|
|
7390
|
-
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
|
|
7396
|
-
if (test.result.imports.length > 0) {
|
|
7397
|
-
for (const imp of test.result.imports) {
|
|
7398
|
-
lines.push(chalk5.gray(imp));
|
|
7399
|
-
}
|
|
7400
|
-
lines.push("");
|
|
7859
|
+
// src/ai/pattern-suggester.ts
|
|
7860
|
+
var SYSTEM_PROMPT3 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
|
|
7861
|
+
Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
|
|
7862
|
+
|
|
7863
|
+
Your patterns should:
|
|
7864
|
+
1. Be specific enough to avoid false positives
|
|
7865
|
+
2. Be general enough to catch variations
|
|
7866
|
+
3. Use standard regex syntax (no lookbehind for compatibility)
|
|
7867
|
+
4. Include examples of what matches and what doesn't
|
|
7868
|
+
|
|
7869
|
+
Always respond with valid JSON matching this structure:
|
|
7870
|
+
{
|
|
7871
|
+
"suggestions": [
|
|
7872
|
+
{
|
|
7873
|
+
"id": "pattern-id-kebab-case",
|
|
7874
|
+
"pattern": "regex pattern here",
|
|
7875
|
+
"description": "What this pattern detects",
|
|
7876
|
+
"confidence": "high|medium|low",
|
|
7877
|
+
"matchExample": "code that should match",
|
|
7878
|
+
"safeExample": "similar code that should NOT match",
|
|
7879
|
+
"reasoning": "Why this pattern works"
|
|
7401
7880
|
}
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7881
|
+
]
|
|
7882
|
+
}
|
|
7883
|
+
|
|
7884
|
+
Important:
|
|
7885
|
+
- Escape backslashes properly for JSON (use \\\\s not \\s)
|
|
7886
|
+
- Test your patterns mentally against the examples
|
|
7887
|
+
- Prefer simpler patterns that are less likely to cause ReDoS`;
|
|
7888
|
+
async function suggestPatterns(request, config2) {
|
|
7889
|
+
const ai = createAIService(config2);
|
|
7890
|
+
if (!ai.isConfigured()) {
|
|
7891
|
+
return {
|
|
7892
|
+
success: false,
|
|
7893
|
+
error: "AI service not configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.",
|
|
7894
|
+
durationMs: 0
|
|
7895
|
+
};
|
|
7405
7896
|
}
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7897
|
+
const prompt = buildPatternPrompt(request);
|
|
7898
|
+
const response = await ai.completeJSON({
|
|
7899
|
+
systemPrompt: SYSTEM_PROMPT3,
|
|
7900
|
+
messages: [{ role: "user", content: prompt }],
|
|
7901
|
+
maxTokens: 2048,
|
|
7902
|
+
temperature: 0.3
|
|
7903
|
+
});
|
|
7904
|
+
if (!response.success || !response.data) {
|
|
7905
|
+
return {
|
|
7906
|
+
success: false,
|
|
7907
|
+
error: response.error ?? "Failed to generate patterns",
|
|
7908
|
+
durationMs: response.durationMs
|
|
7909
|
+
};
|
|
7415
7910
|
}
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
const
|
|
7422
|
-
|
|
7423
|
-
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
template: {
|
|
7431
|
-
id: test.template.id,
|
|
7432
|
-
framework: test.template.framework,
|
|
7433
|
-
language: test.template.language
|
|
7434
|
-
},
|
|
7435
|
-
suggestedPath: test.suggestedPath,
|
|
7436
|
-
content: test.result.content,
|
|
7437
|
-
imports: test.result.imports,
|
|
7438
|
-
fixtures: test.result.fixtures,
|
|
7439
|
-
substituted: test.result.substituted,
|
|
7440
|
-
unresolved: test.result.unresolved
|
|
7441
|
-
}));
|
|
7442
|
-
return JSON.stringify(output, null, 2);
|
|
7911
|
+
const validated = validatePatterns(
|
|
7912
|
+
response.data.suggestions ?? [],
|
|
7913
|
+
request.vulnerableCode,
|
|
7914
|
+
request.safeCode ?? []
|
|
7915
|
+
);
|
|
7916
|
+
const result = {
|
|
7917
|
+
success: true,
|
|
7918
|
+
data: validated,
|
|
7919
|
+
durationMs: response.durationMs
|
|
7920
|
+
};
|
|
7921
|
+
if (response.usage) {
|
|
7922
|
+
result.usage = response.usage;
|
|
7923
|
+
}
|
|
7924
|
+
return result;
|
|
7443
7925
|
}
|
|
7444
|
-
function
|
|
7445
|
-
const
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7450
|
-
|
|
7451
|
-
|
|
7452
|
-
|
|
7453
|
-
|
|
7454
|
-
|
|
7455
|
-
};
|
|
7456
|
-
const ext = extMap[template.language] ?? ".test.ts";
|
|
7457
|
-
let testFileName;
|
|
7458
|
-
switch (template.language) {
|
|
7459
|
-
case "python":
|
|
7460
|
-
testFileName = `test_${nameWithoutExt}${ext}`;
|
|
7461
|
-
break;
|
|
7462
|
-
case "go":
|
|
7463
|
-
testFileName = `${nameWithoutExt}${ext}`;
|
|
7464
|
-
break;
|
|
7465
|
-
case "java":
|
|
7466
|
-
testFileName = `${nameWithoutExt}${ext}`;
|
|
7467
|
-
break;
|
|
7468
|
-
default:
|
|
7469
|
-
testFileName = `${nameWithoutExt}.test${ext}`;
|
|
7926
|
+
function buildPatternPrompt(request) {
|
|
7927
|
+
const parts = [];
|
|
7928
|
+
parts.push(`Generate regex patterns to detect ${request.category} vulnerabilities in ${request.language} code.
|
|
7929
|
+
`);
|
|
7930
|
+
parts.push("**Vulnerable code samples (patterns SHOULD match these):**");
|
|
7931
|
+
for (let i = 0; i < request.vulnerableCode.length; i++) {
|
|
7932
|
+
parts.push(`
|
|
7933
|
+
Example ${i + 1}:`);
|
|
7934
|
+
parts.push("```");
|
|
7935
|
+
parts.push(request.vulnerableCode[i] ?? "");
|
|
7936
|
+
parts.push("```");
|
|
7470
7937
|
}
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
return {
|
|
7481
|
-
// File info
|
|
7482
|
-
filePath: gap.filePath,
|
|
7483
|
-
fileName,
|
|
7484
|
-
fileNameWithoutExt,
|
|
7485
|
-
lineNumber: gap.lineStart,
|
|
7486
|
-
// Category info
|
|
7487
|
-
categoryId: gap.categoryId,
|
|
7488
|
-
categoryName: gap.categoryName,
|
|
7489
|
-
domain: gap.domain,
|
|
7490
|
-
level: gap.level,
|
|
7491
|
-
severity: gap.severity,
|
|
7492
|
-
confidence: gap.confidence,
|
|
7493
|
-
// Code context
|
|
7494
|
-
codeSnippet: gap.codeSnippet,
|
|
7495
|
-
functionName: funcMatch?.[1] ?? "targetFunction",
|
|
7496
|
-
className: classMatch?.[1] ?? "TargetClass",
|
|
7497
|
-
// Common template variables
|
|
7498
|
-
testName: `test_${gap.categoryId.replace(/-/g, "_")}`,
|
|
7499
|
-
testDescription: `Test for ${gap.categoryName} in ${fileName}:${gap.lineStart}`,
|
|
7500
|
-
// Pattern info
|
|
7501
|
-
patternId: gap.patternId,
|
|
7502
|
-
patternType: gap.patternType
|
|
7503
|
-
};
|
|
7504
|
-
}
|
|
7505
|
-
async function extractVariablesWithAI(gap, templateVariables, aiConfig) {
|
|
7506
|
-
const baseVars = extractVariablesFromGap(gap);
|
|
7507
|
-
const result = await suggestVariables(
|
|
7508
|
-
{
|
|
7509
|
-
codeSnippet: gap.codeSnippet,
|
|
7510
|
-
filePath: gap.filePath,
|
|
7511
|
-
variables: templateVariables,
|
|
7512
|
-
gap,
|
|
7513
|
-
existingValues: baseVars
|
|
7514
|
-
},
|
|
7515
|
-
aiConfig
|
|
7516
|
-
);
|
|
7517
|
-
if (result.success && result.data) {
|
|
7518
|
-
return result.data.values;
|
|
7938
|
+
if (request.safeCode && request.safeCode.length > 0) {
|
|
7939
|
+
parts.push("\n**Safe code samples (patterns should NOT match these):**");
|
|
7940
|
+
for (let i = 0; i < request.safeCode.length; i++) {
|
|
7941
|
+
parts.push(`
|
|
7942
|
+
Safe ${i + 1}:`);
|
|
7943
|
+
parts.push("```");
|
|
7944
|
+
parts.push(request.safeCode[i] ?? "");
|
|
7945
|
+
parts.push("```");
|
|
7946
|
+
}
|
|
7519
7947
|
}
|
|
7520
|
-
|
|
7521
|
-
|
|
7522
|
-
|
|
7523
|
-
|
|
7524
|
-
created: [],
|
|
7525
|
-
updated: [],
|
|
7526
|
-
failed: [],
|
|
7527
|
-
totalTests: 0
|
|
7528
|
-
};
|
|
7529
|
-
const testsByFile = /* @__PURE__ */ new Map();
|
|
7530
|
-
for (const test of tests) {
|
|
7531
|
-
let outputPath;
|
|
7532
|
-
if (outputDir) {
|
|
7533
|
-
outputPath = resolve(basePath, outputDir, test.suggestedPath);
|
|
7534
|
-
} else {
|
|
7535
|
-
outputPath = resolve(basePath, test.suggestedPath);
|
|
7948
|
+
if (request.existingPatterns && request.existingPatterns.length > 0) {
|
|
7949
|
+
parts.push("\n**Existing patterns (avoid duplicating):**");
|
|
7950
|
+
for (const pattern of request.existingPatterns) {
|
|
7951
|
+
parts.push(`- ${pattern}`);
|
|
7536
7952
|
}
|
|
7537
|
-
const existing = testsByFile.get(outputPath) ?? [];
|
|
7538
|
-
existing.push(test);
|
|
7539
|
-
testsByFile.set(outputPath, existing);
|
|
7540
7953
|
}
|
|
7541
|
-
|
|
7954
|
+
parts.push(`
|
|
7955
|
+
Generate up to ${request.maxSuggestions ?? 3} distinct patterns.`);
|
|
7956
|
+
parts.push("Focus on patterns that will have high precision (low false positives).");
|
|
7957
|
+
return parts.join("\n");
|
|
7958
|
+
}
|
|
7959
|
+
function validatePatterns(suggestions, vulnerableCode, safeCode) {
|
|
7960
|
+
const validated = [];
|
|
7961
|
+
const rejected = [];
|
|
7962
|
+
for (const suggestion of suggestions) {
|
|
7542
7963
|
try {
|
|
7543
|
-
const
|
|
7544
|
-
|
|
7545
|
-
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
fileExists = true;
|
|
7550
|
-
} catch {
|
|
7551
|
-
}
|
|
7552
|
-
const contentParts = [];
|
|
7553
|
-
const allImports = /* @__PURE__ */ new Set();
|
|
7554
|
-
for (const test of fileTests) {
|
|
7555
|
-
for (const imp of test.result.imports) {
|
|
7556
|
-
allImports.add(imp);
|
|
7964
|
+
const regex = new RegExp(suggestion.pattern, "gm");
|
|
7965
|
+
let matchCount = 0;
|
|
7966
|
+
for (const code of vulnerableCode) {
|
|
7967
|
+
regex.lastIndex = 0;
|
|
7968
|
+
if (regex.test(code)) {
|
|
7969
|
+
matchCount++;
|
|
7557
7970
|
}
|
|
7558
7971
|
}
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
contentParts.push(`// Gap: ${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`);
|
|
7566
|
-
contentParts.push(`// Generated by Pinata`);
|
|
7567
|
-
contentParts.push("");
|
|
7568
|
-
contentParts.push(test.result.content);
|
|
7569
|
-
contentParts.push("");
|
|
7972
|
+
let falsePositives = 0;
|
|
7973
|
+
for (const code of safeCode) {
|
|
7974
|
+
regex.lastIndex = 0;
|
|
7975
|
+
if (regex.test(code)) {
|
|
7976
|
+
falsePositives++;
|
|
7977
|
+
}
|
|
7570
7978
|
}
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
} else {
|
|
7578
|
-
finalContent = newContent;
|
|
7979
|
+
if (hasRedosPotential(suggestion.pattern)) {
|
|
7980
|
+
rejected.push({
|
|
7981
|
+
pattern: suggestion.pattern,
|
|
7982
|
+
reason: "Pattern may be vulnerable to ReDoS"
|
|
7983
|
+
});
|
|
7984
|
+
continue;
|
|
7579
7985
|
}
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7583
|
-
path: outputPath,
|
|
7584
|
-
created: !fileExists,
|
|
7585
|
-
appended,
|
|
7586
|
-
categoryId: test.gap.categoryId,
|
|
7587
|
-
gapLocation: `${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`
|
|
7588
|
-
};
|
|
7589
|
-
if (fileExists) {
|
|
7590
|
-
summary.updated.push(result);
|
|
7591
|
-
} else {
|
|
7592
|
-
summary.created.push(result);
|
|
7986
|
+
if (matchCount > 0) {
|
|
7987
|
+
if (falsePositives > 0 && suggestion.confidence === "high") {
|
|
7988
|
+
suggestion.confidence = "medium";
|
|
7593
7989
|
}
|
|
7594
|
-
|
|
7990
|
+
if (matchCount < vulnerableCode.length / 2 && suggestion.confidence === "high") {
|
|
7991
|
+
suggestion.confidence = "medium";
|
|
7992
|
+
}
|
|
7993
|
+
validated.push(suggestion);
|
|
7994
|
+
} else {
|
|
7995
|
+
rejected.push({
|
|
7996
|
+
pattern: suggestion.pattern,
|
|
7997
|
+
reason: `Pattern did not match any vulnerable samples (0/${vulnerableCode.length})`
|
|
7998
|
+
});
|
|
7595
7999
|
}
|
|
7596
8000
|
} catch (error) {
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
8001
|
+
rejected.push({
|
|
8002
|
+
pattern: suggestion.pattern,
|
|
8003
|
+
reason: `Invalid regex: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
7600
8004
|
});
|
|
7601
8005
|
}
|
|
7602
8006
|
}
|
|
7603
|
-
return
|
|
8007
|
+
return { suggestions: validated, rejected };
|
|
7604
8008
|
}
|
|
7605
|
-
function
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
lines.push(chalk5.bold.cyan("Write Summary:"));
|
|
7609
|
-
lines.push(chalk5.gray("\u2500".repeat(60)));
|
|
7610
|
-
if (summary.created.length > 0) {
|
|
7611
|
-
const uniquePaths = new Set(summary.created.map((r) => r.path));
|
|
7612
|
-
lines.push("");
|
|
7613
|
-
lines.push(chalk5.green.bold(`Created ${uniquePaths.size} file(s):`));
|
|
7614
|
-
for (const path2 of uniquePaths) {
|
|
7615
|
-
const relPath = relative(basePath, path2);
|
|
7616
|
-
const testsInFile = summary.created.filter((r) => r.path === path2).length;
|
|
7617
|
-
lines.push(chalk5.green(` + ${relPath} (${testsInFile} test(s))`));
|
|
7618
|
-
}
|
|
7619
|
-
}
|
|
7620
|
-
if (summary.updated.length > 0) {
|
|
7621
|
-
const uniquePaths = new Set(summary.updated.map((r) => r.path));
|
|
7622
|
-
lines.push("");
|
|
7623
|
-
lines.push(chalk5.yellow.bold(`Updated ${uniquePaths.size} file(s):`));
|
|
7624
|
-
for (const path2 of uniquePaths) {
|
|
7625
|
-
const relPath = relative(basePath, path2);
|
|
7626
|
-
const testsInFile = summary.updated.filter((r) => r.path === path2).length;
|
|
7627
|
-
lines.push(chalk5.yellow(` ~ ${relPath} (${testsInFile} test(s) appended)`));
|
|
7628
|
-
}
|
|
8009
|
+
function hasRedosPotential(pattern) {
|
|
8010
|
+
if (/\([^)]*[+*][^)]*\)[+*]/.test(pattern)) {
|
|
8011
|
+
return true;
|
|
7629
8012
|
}
|
|
7630
|
-
if (
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
8013
|
+
if (/\([^)]*\|[^)]*\)[+*]/.test(pattern)) {
|
|
8014
|
+
const alternationMatch = pattern.match(/\(([^)]+)\)/g);
|
|
8015
|
+
if (alternationMatch) {
|
|
8016
|
+
for (const alt of alternationMatch) {
|
|
8017
|
+
if (/\w+\|\w*\w/.test(alt) && /[+*]/.test(alt)) {
|
|
8018
|
+
return true;
|
|
8019
|
+
}
|
|
8020
|
+
}
|
|
7636
8021
|
}
|
|
7637
8022
|
}
|
|
7638
|
-
|
|
7639
|
-
lines.push(chalk5.gray("\u2500".repeat(60)));
|
|
7640
|
-
lines.push(chalk5.bold(`Total: ${summary.totalTests} test(s) written to ${(/* @__PURE__ */ new Set([...summary.created.map((r) => r.path), ...summary.updated.map((r) => r.path)])).size} file(s)`));
|
|
7641
|
-
if (summary.failed.length > 0) {
|
|
7642
|
-
lines.push(chalk5.red(`Failures: ${summary.failed.length}`));
|
|
7643
|
-
}
|
|
7644
|
-
return lines.join("\n");
|
|
8023
|
+
return false;
|
|
7645
8024
|
}
|
|
7646
8025
|
|
|
7647
|
-
// src/
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
"remediation": "Step-by-step instructions to fix",
|
|
7661
|
-
"safeExample": "Code example showing the safe pattern",
|
|
7662
|
-
"references": ["optional array of CVE/CWE/OWASP references"]
|
|
7663
|
-
}`;
|
|
7664
|
-
async function explainGap(gap, category, config2) {
|
|
7665
|
-
const ai = createAIService(config2);
|
|
7666
|
-
if (!ai.isConfigured()) {
|
|
7667
|
-
return {
|
|
7668
|
-
success: false,
|
|
7669
|
-
error: "AI service not configured",
|
|
7670
|
-
durationMs: 0
|
|
7671
|
-
};
|
|
7672
|
-
}
|
|
7673
|
-
const prompt = buildExplainPrompt(gap);
|
|
7674
|
-
const response = await ai.completeJSON({
|
|
7675
|
-
systemPrompt: SYSTEM_PROMPT2,
|
|
7676
|
-
messages: [{ role: "user", content: prompt }],
|
|
7677
|
-
maxTokens: 1024,
|
|
7678
|
-
temperature: 0.3
|
|
7679
|
-
});
|
|
7680
|
-
return response;
|
|
7681
|
-
}
|
|
7682
|
-
function buildExplainPrompt(gap, category) {
|
|
7683
|
-
const parts = [];
|
|
7684
|
-
parts.push(`Explain this security finding:
|
|
7685
|
-
`);
|
|
7686
|
-
parts.push(`**Category:** ${gap.categoryName} (${gap.categoryId})`);
|
|
7687
|
-
parts.push(`**Severity:** ${gap.severity}`);
|
|
7688
|
-
parts.push(`**Confidence:** ${gap.confidence}`);
|
|
7689
|
-
parts.push(`**File:** ${gap.filePath}`);
|
|
7690
|
-
parts.push(`**Line:** ${gap.lineStart}`);
|
|
7691
|
-
if (gap.codeSnippet) {
|
|
7692
|
-
parts.push(`
|
|
7693
|
-
**Code:**
|
|
7694
|
-
\`\`\`
|
|
7695
|
-
${gap.codeSnippet}
|
|
7696
|
-
\`\`\``);
|
|
7697
|
-
}
|
|
7698
|
-
parts.push(`
|
|
7699
|
-
**Pattern:** ${gap.patternId}`);
|
|
7700
|
-
parts.push(`**Detection Type:** ${gap.patternType}`);
|
|
7701
|
-
parts.push(`
|
|
7702
|
-
Provide a clear, actionable explanation for a developer.`);
|
|
7703
|
-
return parts.join("\n");
|
|
7704
|
-
}
|
|
7705
|
-
function generateFallbackExplanation(gap) {
|
|
7706
|
-
const summaries = {
|
|
7707
|
-
"sql-injection": "SQL query constructed with user input may allow injection attacks.",
|
|
7708
|
-
"xss": "User input rendered without escaping may allow script injection.",
|
|
7709
|
-
"command-injection": "Shell command constructed with user input may allow command execution.",
|
|
7710
|
-
"path-traversal": "File path constructed with user input may allow directory traversal.",
|
|
7711
|
-
"hardcoded-secrets": "Sensitive credentials found in source code.",
|
|
7712
|
-
"deserialization": "Untrusted data deserialization may allow code execution.",
|
|
7713
|
-
"ssrf": "Server-side request with user-controlled URL may allow internal access.",
|
|
7714
|
-
"xxe": "XML parser may be vulnerable to external entity injection.",
|
|
7715
|
-
"csrf": "State-changing request lacks CSRF protection.",
|
|
7716
|
-
"ldap-injection": "LDAP query constructed with user input may allow injection."
|
|
7717
|
-
};
|
|
7718
|
-
const remediations = {
|
|
7719
|
-
"sql-injection": "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
|
|
7720
|
-
"xss": "Escape all user input before rendering in HTML. Use framework auto-escaping features.",
|
|
7721
|
-
"command-injection": "Avoid shell execution with user input. Use allowlists and subprocess arrays instead of shell strings.",
|
|
7722
|
-
"path-traversal": "Validate and sanitize file paths. Use path.resolve() and verify the result is within allowed directories.",
|
|
7723
|
-
"hardcoded-secrets": "Move secrets to environment variables or a secrets manager. Never commit credentials to source control.",
|
|
7724
|
-
"deserialization": "Avoid deserializing untrusted data. If necessary, use safe formats like JSON instead of pickle/yaml.",
|
|
7725
|
-
"ssrf": "Validate and allowlist URLs. Block private IP ranges and localhost.",
|
|
7726
|
-
"xxe": "Disable external entity processing in XML parser configuration.",
|
|
7727
|
-
"csrf": "Implement CSRF tokens for all state-changing requests.",
|
|
7728
|
-
"ldap-injection": "Escape special LDAP characters in user input. Use parameterized LDAP queries."
|
|
7729
|
-
};
|
|
7730
|
-
const summary = summaries[gap.categoryId] ?? `Potential ${gap.categoryName} vulnerability detected.`;
|
|
7731
|
-
const remediation = remediations[gap.categoryId] ?? `Review the code for security issues and apply appropriate fixes.`;
|
|
7732
|
-
return {
|
|
7733
|
-
summary,
|
|
7734
|
-
explanation: `The pattern "${gap.patternId}" detected a potential ${gap.categoryName} vulnerability at line ${gap.lineStart}. This type of issue has ${gap.severity} severity and was detected with ${gap.confidence} confidence.`,
|
|
7735
|
-
risk: `If exploited, this vulnerability could compromise the security of the application. Severity: ${gap.severity}.`,
|
|
7736
|
-
remediation,
|
|
7737
|
-
references: []
|
|
7738
|
-
};
|
|
7739
|
-
}
|
|
7740
|
-
|
|
7741
|
-
// src/ai/pattern-suggester.ts
|
|
7742
|
-
var SYSTEM_PROMPT3 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
|
|
7743
|
-
Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
|
|
7744
|
-
|
|
7745
|
-
Your patterns should:
|
|
7746
|
-
1. Be specific enough to avoid false positives
|
|
7747
|
-
2. Be general enough to catch variations
|
|
7748
|
-
3. Use standard regex syntax (no lookbehind for compatibility)
|
|
7749
|
-
4. Include examples of what matches and what doesn't
|
|
7750
|
-
|
|
7751
|
-
Always respond with valid JSON matching this structure:
|
|
7752
|
-
{
|
|
7753
|
-
"suggestions": [
|
|
7754
|
-
{
|
|
7755
|
-
"id": "pattern-id-kebab-case",
|
|
7756
|
-
"pattern": "regex pattern here",
|
|
7757
|
-
"description": "What this pattern detects",
|
|
7758
|
-
"confidence": "high|medium|low",
|
|
7759
|
-
"matchExample": "code that should match",
|
|
7760
|
-
"safeExample": "similar code that should NOT match",
|
|
7761
|
-
"reasoning": "Why this pattern works"
|
|
7762
|
-
}
|
|
7763
|
-
]
|
|
7764
|
-
}
|
|
7765
|
-
|
|
7766
|
-
Important:
|
|
7767
|
-
- Escape backslashes properly for JSON (use \\\\s not \\s)
|
|
7768
|
-
- Test your patterns mentally against the examples
|
|
7769
|
-
- Prefer simpler patterns that are less likely to cause ReDoS`;
|
|
7770
|
-
async function suggestPatterns(request, config2) {
|
|
7771
|
-
const ai = createAIService(config2);
|
|
7772
|
-
if (!ai.isConfigured()) {
|
|
7773
|
-
return {
|
|
7774
|
-
success: false,
|
|
7775
|
-
error: "AI service not configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.",
|
|
7776
|
-
durationMs: 0
|
|
7777
|
-
};
|
|
7778
|
-
}
|
|
7779
|
-
const prompt = buildPatternPrompt(request);
|
|
7780
|
-
const response = await ai.completeJSON({
|
|
7781
|
-
systemPrompt: SYSTEM_PROMPT3,
|
|
7782
|
-
messages: [{ role: "user", content: prompt }],
|
|
7783
|
-
maxTokens: 2048,
|
|
7784
|
-
temperature: 0.3
|
|
7785
|
-
});
|
|
7786
|
-
if (!response.success || !response.data) {
|
|
7787
|
-
return {
|
|
7788
|
-
success: false,
|
|
7789
|
-
error: response.error ?? "Failed to generate patterns",
|
|
7790
|
-
durationMs: response.durationMs
|
|
7791
|
-
};
|
|
7792
|
-
}
|
|
7793
|
-
const validated = validatePatterns(
|
|
7794
|
-
response.data.suggestions ?? [],
|
|
7795
|
-
request.vulnerableCode,
|
|
7796
|
-
request.safeCode ?? []
|
|
7797
|
-
);
|
|
7798
|
-
const result = {
|
|
7799
|
-
success: true,
|
|
7800
|
-
data: validated,
|
|
7801
|
-
durationMs: response.durationMs
|
|
7802
|
-
};
|
|
7803
|
-
if (response.usage) {
|
|
7804
|
-
result.usage = response.usage;
|
|
7805
|
-
}
|
|
7806
|
-
return result;
|
|
7807
|
-
}
|
|
7808
|
-
function buildPatternPrompt(request) {
|
|
7809
|
-
const parts = [];
|
|
7810
|
-
parts.push(`Generate regex patterns to detect ${request.category} vulnerabilities in ${request.language} code.
|
|
7811
|
-
`);
|
|
7812
|
-
parts.push("**Vulnerable code samples (patterns SHOULD match these):**");
|
|
7813
|
-
for (let i = 0; i < request.vulnerableCode.length; i++) {
|
|
7814
|
-
parts.push(`
|
|
7815
|
-
Example ${i + 1}:`);
|
|
7816
|
-
parts.push("```");
|
|
7817
|
-
parts.push(request.vulnerableCode[i] ?? "");
|
|
7818
|
-
parts.push("```");
|
|
7819
|
-
}
|
|
7820
|
-
if (request.safeCode && request.safeCode.length > 0) {
|
|
7821
|
-
parts.push("\n**Safe code samples (patterns should NOT match these):**");
|
|
7822
|
-
for (let i = 0; i < request.safeCode.length; i++) {
|
|
7823
|
-
parts.push(`
|
|
7824
|
-
Safe ${i + 1}:`);
|
|
7825
|
-
parts.push("```");
|
|
7826
|
-
parts.push(request.safeCode[i] ?? "");
|
|
7827
|
-
parts.push("```");
|
|
7828
|
-
}
|
|
7829
|
-
}
|
|
7830
|
-
if (request.existingPatterns && request.existingPatterns.length > 0) {
|
|
7831
|
-
parts.push("\n**Existing patterns (avoid duplicating):**");
|
|
7832
|
-
for (const pattern of request.existingPatterns) {
|
|
7833
|
-
parts.push(`- ${pattern}`);
|
|
7834
|
-
}
|
|
7835
|
-
}
|
|
7836
|
-
parts.push(`
|
|
7837
|
-
Generate up to ${request.maxSuggestions ?? 3} distinct patterns.`);
|
|
7838
|
-
parts.push("Focus on patterns that will have high precision (low false positives).");
|
|
7839
|
-
return parts.join("\n");
|
|
7840
|
-
}
|
|
7841
|
-
function validatePatterns(suggestions, vulnerableCode, safeCode) {
|
|
7842
|
-
const validated = [];
|
|
7843
|
-
const rejected = [];
|
|
7844
|
-
for (const suggestion of suggestions) {
|
|
7845
|
-
try {
|
|
7846
|
-
const regex = new RegExp(suggestion.pattern, "gm");
|
|
7847
|
-
let matchCount = 0;
|
|
7848
|
-
for (const code of vulnerableCode) {
|
|
7849
|
-
regex.lastIndex = 0;
|
|
7850
|
-
if (regex.test(code)) {
|
|
7851
|
-
matchCount++;
|
|
7852
|
-
}
|
|
7853
|
-
}
|
|
7854
|
-
let falsePositives = 0;
|
|
7855
|
-
for (const code of safeCode) {
|
|
7856
|
-
regex.lastIndex = 0;
|
|
7857
|
-
if (regex.test(code)) {
|
|
7858
|
-
falsePositives++;
|
|
7859
|
-
}
|
|
7860
|
-
}
|
|
7861
|
-
if (hasRedosPotential(suggestion.pattern)) {
|
|
7862
|
-
rejected.push({
|
|
7863
|
-
pattern: suggestion.pattern,
|
|
7864
|
-
reason: "Pattern may be vulnerable to ReDoS"
|
|
7865
|
-
});
|
|
7866
|
-
continue;
|
|
7867
|
-
}
|
|
7868
|
-
if (matchCount > 0) {
|
|
7869
|
-
if (falsePositives > 0 && suggestion.confidence === "high") {
|
|
7870
|
-
suggestion.confidence = "medium";
|
|
7871
|
-
}
|
|
7872
|
-
if (matchCount < vulnerableCode.length / 2 && suggestion.confidence === "high") {
|
|
7873
|
-
suggestion.confidence = "medium";
|
|
7874
|
-
}
|
|
7875
|
-
validated.push(suggestion);
|
|
7876
|
-
} else {
|
|
7877
|
-
rejected.push({
|
|
7878
|
-
pattern: suggestion.pattern,
|
|
7879
|
-
reason: `Pattern did not match any vulnerable samples (0/${vulnerableCode.length})`
|
|
7880
|
-
});
|
|
7881
|
-
}
|
|
7882
|
-
} catch (error) {
|
|
7883
|
-
rejected.push({
|
|
7884
|
-
pattern: suggestion.pattern,
|
|
7885
|
-
reason: `Invalid regex: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
7886
|
-
});
|
|
7887
|
-
}
|
|
7888
|
-
}
|
|
7889
|
-
return { suggestions: validated, rejected };
|
|
7890
|
-
}
|
|
7891
|
-
function hasRedosPotential(pattern) {
|
|
7892
|
-
if (/\([^)]*[+*][^)]*\)[+*]/.test(pattern)) {
|
|
7893
|
-
return true;
|
|
7894
|
-
}
|
|
7895
|
-
if (/\([^)]*\|[^)]*\)[+*]/.test(pattern)) {
|
|
7896
|
-
const alternationMatch = pattern.match(/\(([^)]+)\)/g);
|
|
7897
|
-
if (alternationMatch) {
|
|
7898
|
-
for (const alt of alternationMatch) {
|
|
7899
|
-
if (/\w+\|\w*\w/.test(alt) && /[+*]/.test(alt)) {
|
|
7900
|
-
return true;
|
|
7901
|
-
}
|
|
7902
|
-
}
|
|
8026
|
+
// src/cli/index.ts
|
|
8027
|
+
init_results_cache();
|
|
8028
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
8029
|
+
var __dirname2 = dirname(__filename2);
|
|
8030
|
+
function getDefinitionsPath() {
|
|
8031
|
+
const candidates = [
|
|
8032
|
+
resolve(__dirname2, "../../src/categories/definitions"),
|
|
8033
|
+
resolve(process.cwd(), "src/categories/definitions"),
|
|
8034
|
+
resolve(__dirname2, "../categories/definitions")
|
|
8035
|
+
];
|
|
8036
|
+
for (const candidate of candidates) {
|
|
8037
|
+
if (existsSync(candidate)) {
|
|
8038
|
+
return candidate;
|
|
7903
8039
|
}
|
|
7904
8040
|
}
|
|
7905
|
-
return
|
|
8041
|
+
return candidates[0];
|
|
7906
8042
|
}
|
|
7907
|
-
|
|
7908
|
-
// src/cli/index.ts
|
|
7909
8043
|
init_results_cache();
|
|
7910
8044
|
var SEVERITY_COLORS2 = {
|
|
7911
|
-
critical:
|
|
7912
|
-
high:
|
|
7913
|
-
medium:
|
|
7914
|
-
low:
|
|
8045
|
+
critical: chalk7.red.bold,
|
|
8046
|
+
high: chalk7.red,
|
|
8047
|
+
medium: chalk7.yellow,
|
|
8048
|
+
low: chalk7.gray
|
|
7915
8049
|
};
|
|
7916
8050
|
var DOMAIN_COLORS2 = {
|
|
7917
|
-
security:
|
|
7918
|
-
data:
|
|
7919
|
-
concurrency:
|
|
7920
|
-
input:
|
|
7921
|
-
resource:
|
|
7922
|
-
reliability:
|
|
7923
|
-
performance:
|
|
7924
|
-
platform:
|
|
7925
|
-
business:
|
|
7926
|
-
compliance:
|
|
8051
|
+
security: chalk7.red,
|
|
8052
|
+
data: chalk7.blue,
|
|
8053
|
+
concurrency: chalk7.magenta,
|
|
8054
|
+
input: chalk7.cyan,
|
|
8055
|
+
resource: chalk7.yellow,
|
|
8056
|
+
reliability: chalk7.green,
|
|
8057
|
+
performance: chalk7.yellowBright,
|
|
8058
|
+
platform: chalk7.gray,
|
|
8059
|
+
business: chalk7.white,
|
|
8060
|
+
compliance: chalk7.blueBright
|
|
7927
8061
|
};
|
|
7928
8062
|
var GRADE_COLORS = {
|
|
7929
|
-
A:
|
|
7930
|
-
B:
|
|
7931
|
-
C:
|
|
7932
|
-
D:
|
|
7933
|
-
F:
|
|
8063
|
+
A: chalk7.green.bold,
|
|
8064
|
+
B: chalk7.green,
|
|
8065
|
+
C: chalk7.yellow,
|
|
8066
|
+
D: chalk7.red,
|
|
8067
|
+
F: chalk7.red.bold
|
|
7934
8068
|
};
|
|
7935
8069
|
var BANNER = `
|
|
7936
|
-
${
|
|
7937
|
-
${
|
|
7938
|
-
${
|
|
7939
|
-
${
|
|
7940
|
-
${
|
|
8070
|
+
${chalk7.cyan(" ____ _ _ ")}
|
|
8071
|
+
${chalk7.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
|
|
8072
|
+
${chalk7.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
|
|
8073
|
+
${chalk7.cyan("| __/| | | | | (_| | || (_| |")}
|
|
8074
|
+
${chalk7.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
|
|
7941
8075
|
`;
|
|
7942
8076
|
function formatScanTerminal(result, basePath) {
|
|
7943
8077
|
const lines = [];
|
|
7944
8078
|
lines.push(BANNER);
|
|
7945
|
-
lines.push(
|
|
7946
|
-
|
|
8079
|
+
lines.push(chalk7.gray(`Analyzing: ${result.targetDirectory}`));
|
|
8080
|
+
const projectTypeLabel = getProjectTypeDescription(result.projectType.type);
|
|
8081
|
+
lines.push(chalk7.gray(`Project: ${projectTypeLabel} (${result.projectType.confidence} confidence)`));
|
|
8082
|
+
lines.push(chalk7.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
|
|
7947
8083
|
lines.push("");
|
|
7948
8084
|
lines.push(formatScoreBox(result.score));
|
|
7949
8085
|
lines.push("");
|
|
7950
|
-
lines.push(
|
|
8086
|
+
lines.push(chalk7.bold("Domain Coverage:"));
|
|
7951
8087
|
lines.push(formatDomainCoverage(result.coverage));
|
|
7952
8088
|
lines.push("");
|
|
7953
8089
|
if (result.gaps.length > 0) {
|
|
7954
8090
|
lines.push(formatGapsSummary(result.gaps, basePath));
|
|
7955
8091
|
lines.push("");
|
|
7956
8092
|
} else {
|
|
7957
|
-
lines.push(
|
|
8093
|
+
lines.push(chalk7.green.bold("No gaps detected! Your codebase has good test coverage."));
|
|
7958
8094
|
lines.push("");
|
|
7959
8095
|
}
|
|
7960
8096
|
if (result.gaps.length > 0) {
|
|
7961
|
-
lines.push(
|
|
8097
|
+
lines.push(chalk7.gray("Run `pinata generate --gaps` to create tests for these gaps."));
|
|
7962
8098
|
}
|
|
7963
|
-
lines.push(
|
|
8099
|
+
lines.push(chalk7.gray(`
|
|
7964
8100
|
Scan completed in ${result.durationMs}ms`));
|
|
7965
8101
|
return lines.join("\n");
|
|
7966
8102
|
}
|
|
7967
8103
|
function formatScoreBox(score) {
|
|
7968
|
-
const gradeColor = GRADE_COLORS[score.grade] ??
|
|
8104
|
+
const gradeColor = GRADE_COLORS[score.grade] ?? chalk7.white;
|
|
7969
8105
|
const scoreStr = `Pinata Score: ${score.overall}/100 ${gradeColor(`(${score.grade})`)}`;
|
|
7970
8106
|
const boxWidth = 60;
|
|
7971
8107
|
const padding = Math.floor((boxWidth - scoreStr.length) / 2);
|
|
7972
|
-
const top =
|
|
7973
|
-
const middle =
|
|
7974
|
-
const bottom =
|
|
8108
|
+
const top = chalk7.cyan("\u2554" + "\u2550".repeat(boxWidth) + "\u2557");
|
|
8109
|
+
const middle = chalk7.cyan("\u2551") + " ".repeat(padding) + scoreStr + " ".repeat(boxWidth - padding - scoreStr.length) + chalk7.cyan("\u2551");
|
|
8110
|
+
const bottom = chalk7.cyan("\u255A" + "\u2550".repeat(boxWidth) + "\u255D");
|
|
7975
8111
|
return `${top}
|
|
7976
8112
|
${middle}
|
|
7977
8113
|
${bottom}`;
|
|
@@ -7986,14 +8122,14 @@ function formatDomainCoverage(coverage) {
|
|
|
7986
8122
|
}
|
|
7987
8123
|
const percent = domainCoverage.coveragePercent;
|
|
7988
8124
|
const filledWidth = Math.round(percent / 100 * barWidth);
|
|
7989
|
-
const bar =
|
|
7990
|
-
const domainColor = DOMAIN_COLORS2[domain] ??
|
|
8125
|
+
const bar = chalk7.green("\u2588".repeat(filledWidth)) + chalk7.gray("\u2591".repeat(barWidth - filledWidth));
|
|
8126
|
+
const domainColor = DOMAIN_COLORS2[domain] ?? chalk7.white;
|
|
7991
8127
|
const domainName = domain.padEnd(15);
|
|
7992
8128
|
const stats = `${domainCoverage.categoriesCovered}/${domainCoverage.categoriesScanned} categories`;
|
|
7993
8129
|
lines.push(` ${domainColor(domainName)} ${bar} ${percent.toString().padStart(3)}% (${stats})`);
|
|
7994
8130
|
}
|
|
7995
8131
|
if (lines.length === 0) {
|
|
7996
|
-
lines.push(
|
|
8132
|
+
lines.push(chalk7.gray(" No domain coverage data available."));
|
|
7997
8133
|
}
|
|
7998
8134
|
return lines.join("\n");
|
|
7999
8135
|
}
|
|
@@ -8004,48 +8140,48 @@ function formatGapsSummary(gaps, basePath) {
|
|
|
8004
8140
|
const medium = gaps.filter((g) => g.severity === "medium");
|
|
8005
8141
|
const low = gaps.filter((g) => g.severity === "low");
|
|
8006
8142
|
if (critical.length > 0) {
|
|
8007
|
-
lines.push(
|
|
8143
|
+
lines.push(chalk7.red.bold(`
|
|
8008
8144
|
Critical Gaps (${critical.length}):`));
|
|
8009
8145
|
for (const gap of critical.slice(0, 5)) {
|
|
8010
8146
|
lines.push(formatGapLine(gap, basePath, "critical"));
|
|
8011
8147
|
}
|
|
8012
8148
|
if (critical.length > 5) {
|
|
8013
|
-
lines.push(
|
|
8149
|
+
lines.push(chalk7.gray(` ... and ${critical.length - 5} more critical gaps`));
|
|
8014
8150
|
}
|
|
8015
8151
|
}
|
|
8016
8152
|
if (high.length > 0) {
|
|
8017
|
-
lines.push(
|
|
8153
|
+
lines.push(chalk7.red(`
|
|
8018
8154
|
High Severity Gaps (${high.length}):`));
|
|
8019
8155
|
for (const gap of high.slice(0, 5)) {
|
|
8020
8156
|
lines.push(formatGapLine(gap, basePath, "high"));
|
|
8021
8157
|
}
|
|
8022
8158
|
if (high.length > 5) {
|
|
8023
|
-
lines.push(
|
|
8159
|
+
lines.push(chalk7.gray(` ... and ${high.length - 5} more high severity gaps`));
|
|
8024
8160
|
}
|
|
8025
8161
|
}
|
|
8026
8162
|
if (medium.length > 0) {
|
|
8027
|
-
lines.push(
|
|
8163
|
+
lines.push(chalk7.yellow(`
|
|
8028
8164
|
Medium Severity Gaps (${medium.length}):`));
|
|
8029
8165
|
for (const gap of medium.slice(0, 3)) {
|
|
8030
8166
|
lines.push(formatGapLine(gap, basePath, "medium"));
|
|
8031
8167
|
}
|
|
8032
8168
|
if (medium.length > 3) {
|
|
8033
|
-
lines.push(
|
|
8169
|
+
lines.push(chalk7.gray(` ... and ${medium.length - 3} more medium severity gaps`));
|
|
8034
8170
|
}
|
|
8035
8171
|
}
|
|
8036
8172
|
if (low.length > 0) {
|
|
8037
|
-
lines.push(
|
|
8173
|
+
lines.push(chalk7.gray(`
|
|
8038
8174
|
Low Severity: ${low.length} gaps`));
|
|
8039
8175
|
}
|
|
8040
8176
|
return lines.join("\n");
|
|
8041
8177
|
}
|
|
8042
8178
|
function formatGapLine(gap, basePath, severity) {
|
|
8043
|
-
const severityColor = SEVERITY_COLORS2[severity] ??
|
|
8179
|
+
const severityColor = SEVERITY_COLORS2[severity] ?? chalk7.white;
|
|
8044
8180
|
const icon = severity === "critical" ? "\u26D4" : severity === "high" ? "\u{1F534}" : severity === "medium" ? "\u{1F7E1}" : "\u26AA";
|
|
8045
8181
|
const relPath = relative(basePath, gap.filePath);
|
|
8046
8182
|
const location = `${relPath}:${gap.lineStart}`;
|
|
8047
8183
|
const confidence = gap.confidence.toUpperCase();
|
|
8048
|
-
return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${
|
|
8184
|
+
return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk7.cyan(location.padEnd(30))} ${chalk7.gray(confidence)} confidence`;
|
|
8049
8185
|
}
|
|
8050
8186
|
function formatLanguages(result) {
|
|
8051
8187
|
const languages = [];
|
|
@@ -8059,6 +8195,14 @@ function formatLanguages(result) {
|
|
|
8059
8195
|
function formatScanJson(result) {
|
|
8060
8196
|
const serializable = {
|
|
8061
8197
|
targetDirectory: result.targetDirectory,
|
|
8198
|
+
projectType: {
|
|
8199
|
+
type: result.projectType.type,
|
|
8200
|
+
confidence: result.projectType.confidence,
|
|
8201
|
+
evidence: result.projectType.evidence,
|
|
8202
|
+
frameworks: result.projectType.frameworks,
|
|
8203
|
+
languages: result.projectType.languages,
|
|
8204
|
+
...result.projectType.secondaryTypes && { secondaryTypes: result.projectType.secondaryTypes }
|
|
8205
|
+
},
|
|
8062
8206
|
startedAt: result.startedAt.toISOString(),
|
|
8063
8207
|
completedAt: result.completedAt.toISOString(),
|
|
8064
8208
|
durationMs: result.durationMs,
|
|
@@ -8260,637 +8404,785 @@ function isValidScanOutputFormat(format) {
|
|
|
8260
8404
|
return ["terminal", "json", "markdown", "sarif", "html", "junit-xml"].includes(format);
|
|
8261
8405
|
}
|
|
8262
8406
|
|
|
8263
|
-
// src/cli/
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
// When bundled in dist (future)
|
|
8273
|
-
resolve(__dirname2, "../categories/definitions")
|
|
8274
|
-
];
|
|
8275
|
-
for (const candidate of candidates) {
|
|
8276
|
-
if (existsSync(candidate)) {
|
|
8277
|
-
return candidate;
|
|
8278
|
-
}
|
|
8279
|
-
}
|
|
8280
|
-
return candidates[0];
|
|
8281
|
-
}
|
|
8282
|
-
var program = new Command();
|
|
8283
|
-
program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
|
|
8284
|
-
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) => {
|
|
8285
|
-
const isQuiet = Boolean(options["quiet"]);
|
|
8286
|
-
const isVerbose = Boolean(options["verbose"]);
|
|
8287
|
-
if (isQuiet) {
|
|
8288
|
-
logger.configure({ level: "error" });
|
|
8289
|
-
} else if (isVerbose) {
|
|
8290
|
-
logger.configure({ level: "debug" });
|
|
8291
|
-
}
|
|
8292
|
-
const targetDirectory = resolve(targetPath ?? process.cwd());
|
|
8293
|
-
if (!existsSync(targetDirectory)) {
|
|
8294
|
-
console.error(formatError(new Error(`Directory not found: ${targetDirectory}`)));
|
|
8295
|
-
process.exit(1);
|
|
8296
|
-
}
|
|
8297
|
-
const outputFormat = String(options["output"] ?? "terminal");
|
|
8298
|
-
if (!isValidScanOutputFormat(outputFormat)) {
|
|
8299
|
-
console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown, sarif`)));
|
|
8300
|
-
process.exit(1);
|
|
8301
|
-
}
|
|
8302
|
-
const validSeverities = ["critical", "high", "medium", "low"];
|
|
8303
|
-
const minSeverity = String(options["severity"] ?? "low");
|
|
8304
|
-
if (!validSeverities.includes(minSeverity)) {
|
|
8305
|
-
console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
|
|
8306
|
-
process.exit(1);
|
|
8307
|
-
}
|
|
8308
|
-
const validConfidences = ["high", "medium", "low"];
|
|
8309
|
-
const minConfidence = String(options["confidence"] ?? "high");
|
|
8310
|
-
if (!validConfidences.includes(minConfidence)) {
|
|
8311
|
-
console.error(formatError(new Error(`Invalid confidence: ${minConfidence}. Use: high, medium, low`)));
|
|
8312
|
-
process.exit(1);
|
|
8313
|
-
}
|
|
8314
|
-
const domainsStr = options["domains"];
|
|
8315
|
-
let domains = [];
|
|
8316
|
-
if (domainsStr) {
|
|
8317
|
-
const domainList = domainsStr.split(",").map((d) => d.trim());
|
|
8318
|
-
for (const domain of domainList) {
|
|
8319
|
-
if (!RISK_DOMAINS.includes(domain)) {
|
|
8320
|
-
console.error(formatError(new Error(`Invalid domain: ${domain}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
|
|
8321
|
-
process.exit(1);
|
|
8322
|
-
}
|
|
8407
|
+
// src/cli/commands/analyze.ts
|
|
8408
|
+
function registerAnalyzeCommand(program2) {
|
|
8409
|
+
program2.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) => {
|
|
8410
|
+
const isQuiet = Boolean(options["quiet"]);
|
|
8411
|
+
const isVerbose = Boolean(options["verbose"]);
|
|
8412
|
+
if (isQuiet) {
|
|
8413
|
+
logger.configure({ level: "error" });
|
|
8414
|
+
} else if (isVerbose) {
|
|
8415
|
+
logger.configure({ level: "debug" });
|
|
8323
8416
|
}
|
|
8324
|
-
|
|
8325
|
-
|
|
8326
|
-
|
|
8327
|
-
const excludeDirs = excludeStr ? excludeStr.split(",").map((d) => d.trim()) : void 0;
|
|
8328
|
-
const failOn = options["failOn"];
|
|
8329
|
-
if (failOn && !["critical", "high", "medium"].includes(failOn)) {
|
|
8330
|
-
console.error(formatError(new Error(`Invalid fail-on level: ${failOn}. Use: critical, high, medium`)));
|
|
8331
|
-
process.exit(1);
|
|
8332
|
-
}
|
|
8333
|
-
const showSpinner = outputFormat === "terminal" && !isQuiet;
|
|
8334
|
-
const spinner = showSpinner ? ora("Loading categories...").start() : null;
|
|
8335
|
-
try {
|
|
8336
|
-
const store = createCategoryStore();
|
|
8337
|
-
const definitionsPath = getDefinitionsPath();
|
|
8338
|
-
logger.debug(`Loading categories from: ${definitionsPath}`);
|
|
8339
|
-
const loadResult = await store.loadFromDirectory(definitionsPath);
|
|
8340
|
-
if (!loadResult.success) {
|
|
8341
|
-
spinner?.fail("Failed to load categories");
|
|
8342
|
-
console.error(formatError(loadResult.error));
|
|
8417
|
+
const targetDirectory = resolve(targetPath ?? process.cwd());
|
|
8418
|
+
if (!existsSync(targetDirectory)) {
|
|
8419
|
+
console.error(formatError(new Error(`Directory not found: ${targetDirectory}`)));
|
|
8343
8420
|
process.exit(1);
|
|
8344
8421
|
}
|
|
8345
|
-
|
|
8346
|
-
|
|
8347
|
-
|
|
8348
|
-
|
|
8349
|
-
const scanner = createScanner(store);
|
|
8350
|
-
const scanOptions = {
|
|
8351
|
-
minSeverity,
|
|
8352
|
-
minConfidence,
|
|
8353
|
-
detectTestFiles: true
|
|
8354
|
-
};
|
|
8355
|
-
if (domains.length > 0) {
|
|
8356
|
-
scanOptions.domains = domains;
|
|
8422
|
+
const outputFormat = String(options["output"] ?? "terminal");
|
|
8423
|
+
if (!isValidScanOutputFormat(outputFormat)) {
|
|
8424
|
+
console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown, sarif`)));
|
|
8425
|
+
process.exit(1);
|
|
8357
8426
|
}
|
|
8358
|
-
|
|
8359
|
-
|
|
8427
|
+
const validSeverities = ["critical", "high", "medium", "low"];
|
|
8428
|
+
const minSeverity = String(options["severity"] ?? "low");
|
|
8429
|
+
if (!validSeverities.includes(minSeverity)) {
|
|
8430
|
+
console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
|
|
8431
|
+
process.exit(1);
|
|
8360
8432
|
}
|
|
8361
|
-
const
|
|
8362
|
-
|
|
8363
|
-
|
|
8364
|
-
console.error(formatError(
|
|
8433
|
+
const validConfidences = ["high", "medium", "low"];
|
|
8434
|
+
const minConfidence = String(options["confidence"] ?? "high");
|
|
8435
|
+
if (!validConfidences.includes(minConfidence)) {
|
|
8436
|
+
console.error(formatError(new Error(`Invalid confidence: ${minConfidence}. Use: high, medium, low`)));
|
|
8365
8437
|
process.exit(1);
|
|
8366
8438
|
}
|
|
8367
|
-
|
|
8368
|
-
|
|
8369
|
-
if (
|
|
8370
|
-
const
|
|
8371
|
-
const
|
|
8372
|
-
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
console.log(chalk5.yellow("\nAI verification requires an API key."));
|
|
8376
|
-
console.log(chalk5.gray("Get one at: https://console.anthropic.com/settings/keys"));
|
|
8377
|
-
console.log(chalk5.gray("Or: https://platform.openai.com/api-keys\n"));
|
|
8378
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8379
|
-
const askQuestion = (question) => {
|
|
8380
|
-
return new Promise((resolve6) => {
|
|
8381
|
-
rl.question(question, (answer) => resolve6(answer.trim()));
|
|
8382
|
-
});
|
|
8383
|
-
};
|
|
8384
|
-
const apiKey = await askQuestion(chalk5.cyan("Enter your Anthropic or OpenAI API key: "));
|
|
8385
|
-
rl.close();
|
|
8386
|
-
if (!apiKey) {
|
|
8387
|
-
console.log(chalk5.red("No API key provided. Skipping AI verification."));
|
|
8388
|
-
} else {
|
|
8389
|
-
if (apiKey.startsWith("sk-ant-")) {
|
|
8390
|
-
setConfigValue2("anthropicApiKey", apiKey);
|
|
8391
|
-
provider = "anthropic";
|
|
8392
|
-
console.log(chalk5.green("Anthropic API key saved to ~/.pinata/config.json\n"));
|
|
8393
|
-
} else {
|
|
8394
|
-
setConfigValue2("openaiApiKey", apiKey);
|
|
8395
|
-
provider = "openai";
|
|
8396
|
-
console.log(chalk5.green("OpenAI API key saved to ~/.pinata/config.json\n"));
|
|
8397
|
-
}
|
|
8439
|
+
const domainsStr = options["domains"];
|
|
8440
|
+
let domains = [];
|
|
8441
|
+
if (domainsStr) {
|
|
8442
|
+
const domainList = domainsStr.split(",").map((d) => d.trim());
|
|
8443
|
+
for (const domain of domainList) {
|
|
8444
|
+
if (!RISK_DOMAINS.includes(domain)) {
|
|
8445
|
+
console.error(formatError(new Error(`Invalid domain: ${domain}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
|
|
8446
|
+
process.exit(1);
|
|
8398
8447
|
}
|
|
8399
|
-
} else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
|
|
8400
|
-
provider = "openai";
|
|
8401
8448
|
}
|
|
8402
|
-
|
|
8403
|
-
|
|
8404
|
-
|
|
8405
|
-
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
8449
|
+
domains = domainList;
|
|
8450
|
+
}
|
|
8451
|
+
const excludeStr = options["exclude"];
|
|
8452
|
+
const excludeDirs = excludeStr ? excludeStr.split(",").map((d) => d.trim()) : void 0;
|
|
8453
|
+
const failOn = options["failOn"];
|
|
8454
|
+
if (failOn && !["critical", "high", "medium"].includes(failOn)) {
|
|
8455
|
+
console.error(formatError(new Error(`Invalid fail-on level: ${failOn}. Use: critical, high, medium`)));
|
|
8456
|
+
process.exit(1);
|
|
8457
|
+
}
|
|
8458
|
+
const showSpinner = outputFormat === "terminal" && !isQuiet;
|
|
8459
|
+
const spinner = showSpinner ? ora3("Loading categories...").start() : null;
|
|
8460
|
+
try {
|
|
8461
|
+
const store = createCategoryStore();
|
|
8462
|
+
const definitionsPath = getDefinitionsPath();
|
|
8463
|
+
logger.debug(`Loading categories from: ${definitionsPath}`);
|
|
8464
|
+
const loadResult = await store.loadFromDirectory(definitionsPath);
|
|
8465
|
+
if (!loadResult.success) {
|
|
8466
|
+
spinner?.fail("Failed to load categories");
|
|
8467
|
+
console.error(formatError(loadResult.error));
|
|
8468
|
+
process.exit(1);
|
|
8469
|
+
}
|
|
8470
|
+
if (spinner) {
|
|
8471
|
+
spinner.text = `Loaded ${loadResult.data} categories. Scanning...`;
|
|
8472
|
+
}
|
|
8473
|
+
const scanner = createScanner(store);
|
|
8474
|
+
const scanOptions = {
|
|
8475
|
+
minSeverity,
|
|
8476
|
+
minConfidence,
|
|
8477
|
+
detectTestFiles: true
|
|
8478
|
+
};
|
|
8479
|
+
if (domains.length > 0) {
|
|
8480
|
+
scanOptions.domains = domains;
|
|
8481
|
+
}
|
|
8482
|
+
if (excludeDirs) {
|
|
8483
|
+
scanOptions.excludeDirs = excludeDirs;
|
|
8484
|
+
}
|
|
8485
|
+
const scanResult = await scanner.scanDirectory(targetDirectory, scanOptions);
|
|
8486
|
+
if (!scanResult.success) {
|
|
8487
|
+
spinner?.fail("Scan failed");
|
|
8488
|
+
console.error(formatError(scanResult.error));
|
|
8489
|
+
process.exit(1);
|
|
8490
|
+
}
|
|
8491
|
+
spinner?.stop();
|
|
8492
|
+
const shouldVerify = Boolean(options["verify"]);
|
|
8493
|
+
if (shouldVerify && scanResult.data.gaps.length > 0) {
|
|
8494
|
+
const { hasApiKey: hasApiKey2, setConfigValue: setConfigValue2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
8495
|
+
const { createInterface } = await import('readline');
|
|
8496
|
+
let provider = "anthropic";
|
|
8497
|
+
if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
|
|
8498
|
+
spinner?.stop();
|
|
8499
|
+
console.log(chalk7.yellow("\nAI verification requires an API key."));
|
|
8500
|
+
console.log(chalk7.gray("Get one at: https://console.anthropic.com/settings/keys"));
|
|
8501
|
+
console.log(chalk7.gray("Or: https://platform.openai.com/api-keys\n"));
|
|
8502
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8503
|
+
const askQuestion = (question) => new Promise((resolve9) => rl.question(question, (answer) => resolve9(answer.trim())));
|
|
8504
|
+
const apiKey = await askQuestion(chalk7.cyan("Enter your Anthropic or OpenAI API key: "));
|
|
8505
|
+
rl.close();
|
|
8506
|
+
if (!apiKey) {
|
|
8507
|
+
console.log(chalk7.red("No API key provided. Skipping AI verification."));
|
|
8508
|
+
} else {
|
|
8509
|
+
if (apiKey.startsWith("sk-ant-")) {
|
|
8510
|
+
setConfigValue2("anthropicApiKey", apiKey);
|
|
8511
|
+
provider = "anthropic";
|
|
8512
|
+
console.log(chalk7.green("Anthropic API key saved to ~/.pinata/config.json\n"));
|
|
8513
|
+
} else {
|
|
8514
|
+
setConfigValue2("openaiApiKey", apiKey);
|
|
8515
|
+
provider = "openai";
|
|
8516
|
+
console.log(chalk7.green("OpenAI API key saved to ~/.pinata/config.json\n"));
|
|
8517
|
+
}
|
|
8419
8518
|
}
|
|
8420
|
-
|
|
8421
|
-
|
|
8422
|
-
|
|
8423
|
-
|
|
8424
|
-
verifySpinner
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8519
|
+
} else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
|
|
8520
|
+
provider = "openai";
|
|
8521
|
+
}
|
|
8522
|
+
if (hasApiKey2(provider)) {
|
|
8523
|
+
const verifySpinner = showSpinner ? ora3("Verifying gaps with AI...").start() : null;
|
|
8524
|
+
try {
|
|
8525
|
+
const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
|
|
8526
|
+
const { readFile: readFile7 } = await import('fs/promises');
|
|
8527
|
+
const apiKey = getApiKey2(provider);
|
|
8528
|
+
const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
|
|
8529
|
+
const { verified, dismissed, stats } = await verifier.verifyAll(
|
|
8530
|
+
scanResult.data.gaps,
|
|
8531
|
+
async (path2) => readFile7(path2, "utf-8")
|
|
8532
|
+
);
|
|
8533
|
+
scanResult.data.gaps = verified;
|
|
8534
|
+
const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
|
|
8535
|
+
let deduction = 0;
|
|
8536
|
+
for (const gap of verified) {
|
|
8537
|
+
deduction += severityWeights[gap.severity] ?? 1;
|
|
8538
|
+
}
|
|
8539
|
+
const newOverall = Math.max(0, 100 - deduction);
|
|
8540
|
+
const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
|
|
8541
|
+
scanResult.data.score.overall = newOverall;
|
|
8542
|
+
scanResult.data.score.grade = newGrade;
|
|
8543
|
+
verifySpinner?.succeed(
|
|
8544
|
+
`AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
|
|
8545
|
+
);
|
|
8546
|
+
if (isVerbose && dismissed.length > 0) {
|
|
8547
|
+
console.log(chalk7.gray("\nDismissed as false positives:"));
|
|
8548
|
+
for (const { gap, reason } of dismissed.slice(0, 5)) {
|
|
8549
|
+
console.log(chalk7.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
|
|
8550
|
+
console.log(chalk7.gray(` Reason: ${reason.slice(0, 100)}...`));
|
|
8551
|
+
}
|
|
8552
|
+
if (dismissed.length > 5) {
|
|
8553
|
+
console.log(chalk7.gray(` ... and ${dismissed.length - 5} more`));
|
|
8554
|
+
}
|
|
8432
8555
|
}
|
|
8433
|
-
|
|
8434
|
-
|
|
8556
|
+
} catch (error) {
|
|
8557
|
+
verifySpinner?.fail("AI verification failed (results unverified)");
|
|
8558
|
+
if (isVerbose) {
|
|
8559
|
+
console.error(chalk7.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8435
8560
|
}
|
|
8436
8561
|
}
|
|
8437
|
-
} catch (error) {
|
|
8438
|
-
verifySpinner?.fail("AI verification failed (results unverified)");
|
|
8439
|
-
if (isVerbose) {
|
|
8440
|
-
console.error(chalk5.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8441
|
-
}
|
|
8442
8562
|
}
|
|
8443
8563
|
}
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
|
|
8447
|
-
|
|
8448
|
-
|
|
8449
|
-
|
|
8450
|
-
|
|
8451
|
-
|
|
8452
|
-
console.log(chalk5.yellow("\nNo dynamically testable gaps found."));
|
|
8453
|
-
console.log(chalk5.gray("Testable types: sql-injection, xss, command-injection, path-traversal"));
|
|
8454
|
-
} else {
|
|
8455
|
-
const runner = createRunner2(void 0, isDryRun);
|
|
8456
|
-
const initResult = await runner.initialize();
|
|
8457
|
-
if (!initResult.ready) {
|
|
8458
|
-
console.log(chalk5.red(`
|
|
8459
|
-
Dynamic execution unavailable: ${initResult.error}`));
|
|
8564
|
+
const shouldExecute = Boolean(options["execute"]);
|
|
8565
|
+
const isDryRun = Boolean(options["dryRun"]);
|
|
8566
|
+
if (shouldExecute && scanResult.data.gaps.length > 0) {
|
|
8567
|
+
const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
|
|
8568
|
+
const { readFile: readFile7 } = await import('fs/promises');
|
|
8569
|
+
const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
|
|
8570
|
+
if (testableGaps.length === 0) {
|
|
8571
|
+
console.log(chalk7.yellow("\nNo dynamically testable gaps found."));
|
|
8460
8572
|
} else {
|
|
8461
|
-
const
|
|
8462
|
-
|
|
8463
|
-
|
|
8464
|
-
|
|
8465
|
-
|
|
8466
|
-
|
|
8573
|
+
const runner = createRunner2(void 0, isDryRun);
|
|
8574
|
+
const initResult = await runner.initialize();
|
|
8575
|
+
if (!initResult.ready) {
|
|
8576
|
+
console.log(chalk7.red(`
|
|
8577
|
+
Dynamic execution unavailable: ${initResult.error}`));
|
|
8578
|
+
} else {
|
|
8579
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
8580
|
+
for (const gap of testableGaps) {
|
|
8581
|
+
if (!fileContents.has(gap.filePath)) {
|
|
8582
|
+
try {
|
|
8583
|
+
fileContents.set(gap.filePath, await readFile7(gap.filePath, "utf-8"));
|
|
8584
|
+
} catch {
|
|
8585
|
+
}
|
|
8467
8586
|
}
|
|
8468
8587
|
}
|
|
8469
|
-
|
|
8470
|
-
|
|
8471
|
-
|
|
8472
|
-
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8588
|
+
const executionSummary = await runner.executeAll(testableGaps, fileContents);
|
|
8589
|
+
for (const result of executionSummary.results) {
|
|
8590
|
+
const gap = scanResult.data.gaps.find(
|
|
8591
|
+
(g) => g.filePath === result.gap.filePath && g.lineStart === result.gap.lineStart
|
|
8592
|
+
);
|
|
8593
|
+
if (gap && result.status === "confirmed") {
|
|
8594
|
+
gap.confirmed = true;
|
|
8595
|
+
gap.evidence = result.evidence;
|
|
8596
|
+
}
|
|
8478
8597
|
}
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
console.log(chalk5.red.bold(`
|
|
8598
|
+
if (executionSummary.confirmed > 0) {
|
|
8599
|
+
console.log(chalk7.red.bold(`
|
|
8482
8600
|
\u26A0\uFE0F ${executionSummary.confirmed} CONFIRMED vulnerabilities found!`));
|
|
8601
|
+
}
|
|
8483
8602
|
}
|
|
8484
8603
|
}
|
|
8485
8604
|
}
|
|
8605
|
+
const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
|
|
8606
|
+
if (!cacheResult.success) {
|
|
8607
|
+
logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
|
|
8608
|
+
}
|
|
8609
|
+
const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
|
|
8610
|
+
const outputFile = options["outputFile"];
|
|
8611
|
+
if (outputFile) {
|
|
8612
|
+
writeFileSync(resolve(outputFile), output, "utf-8");
|
|
8613
|
+
logger.info(`Results written to: ${resolve(outputFile)}`);
|
|
8614
|
+
} else {
|
|
8615
|
+
console.log(output);
|
|
8616
|
+
}
|
|
8617
|
+
if (isVerbose && scanResult.data.warnings.length > 0) {
|
|
8618
|
+
console.error("\nWarnings:");
|
|
8619
|
+
for (const warning of scanResult.data.warnings) {
|
|
8620
|
+
console.error(` - ${warning}`);
|
|
8621
|
+
}
|
|
8622
|
+
}
|
|
8623
|
+
if (failOn) {
|
|
8624
|
+
const severityOrder = { critical: 3, high: 2, medium: 1 };
|
|
8625
|
+
const failLevel = severityOrder[failOn] ?? 0;
|
|
8626
|
+
const hasFailingGaps = scanResult.data.gaps.some((gap) => (severityOrder[gap.severity] ?? 0) >= failLevel);
|
|
8627
|
+
if (hasFailingGaps) {
|
|
8628
|
+
process.exit(1);
|
|
8629
|
+
}
|
|
8630
|
+
}
|
|
8631
|
+
process.exit(0);
|
|
8632
|
+
} catch (error) {
|
|
8633
|
+
spinner?.fail("Analysis failed");
|
|
8634
|
+
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
8635
|
+
process.exit(1);
|
|
8486
8636
|
}
|
|
8487
|
-
|
|
8488
|
-
|
|
8489
|
-
|
|
8637
|
+
});
|
|
8638
|
+
}
|
|
8639
|
+
|
|
8640
|
+
// src/cli/generate-formatters.ts
|
|
8641
|
+
init_errors();
|
|
8642
|
+
init_result();
|
|
8643
|
+
function formatGeneratedTerminal(tests, basePath) {
|
|
8644
|
+
const lines = [];
|
|
8645
|
+
if (tests.length === 0) {
|
|
8646
|
+
lines.push(chalk7.yellow("No tests generated."));
|
|
8647
|
+
return lines.join("\n");
|
|
8648
|
+
}
|
|
8649
|
+
lines.push(chalk7.bold.cyan(`
|
|
8650
|
+
Generated ${tests.length} test(s):
|
|
8651
|
+
`));
|
|
8652
|
+
lines.push(chalk7.gray("\u2500".repeat(60)));
|
|
8653
|
+
for (const test of tests) {
|
|
8654
|
+
const relGapPath = relative(basePath, test.gap.filePath);
|
|
8655
|
+
lines.push("");
|
|
8656
|
+
lines.push(chalk7.bold.white(`Test for: ${test.gap.categoryName}`));
|
|
8657
|
+
lines.push(chalk7.gray(` Gap location: ${relGapPath}:${test.gap.lineStart}`));
|
|
8658
|
+
lines.push(chalk7.gray(` Template: ${test.template.id}`));
|
|
8659
|
+
lines.push(chalk7.gray(` Output: ${test.suggestedPath}`));
|
|
8660
|
+
lines.push("");
|
|
8661
|
+
lines.push(chalk7.cyan(`// --- ${test.suggestedPath} ---`));
|
|
8662
|
+
lines.push("");
|
|
8663
|
+
if (test.result.imports.length > 0) {
|
|
8664
|
+
for (const imp of test.result.imports) {
|
|
8665
|
+
lines.push(chalk7.gray(imp));
|
|
8666
|
+
}
|
|
8667
|
+
lines.push("");
|
|
8490
8668
|
}
|
|
8491
|
-
|
|
8492
|
-
|
|
8493
|
-
|
|
8494
|
-
|
|
8495
|
-
|
|
8496
|
-
|
|
8669
|
+
lines.push(test.result.content);
|
|
8670
|
+
lines.push("");
|
|
8671
|
+
lines.push(chalk7.gray("\u2500".repeat(60)));
|
|
8672
|
+
}
|
|
8673
|
+
lines.push("");
|
|
8674
|
+
lines.push(chalk7.bold("Summary:"));
|
|
8675
|
+
lines.push(` Tests generated: ${chalk7.green(tests.length.toString())}`);
|
|
8676
|
+
const categories = new Set(tests.map((t) => t.category.id));
|
|
8677
|
+
lines.push(` Categories covered: ${chalk7.cyan(categories.size.toString())}`);
|
|
8678
|
+
if (tests.some((t) => t.result.unresolved.length > 0)) {
|
|
8679
|
+
const unresolvedCount = tests.reduce((acc, t) => acc + t.result.unresolved.length, 0);
|
|
8680
|
+
lines.push(chalk7.yellow(` Unresolved placeholders: ${unresolvedCount}`));
|
|
8681
|
+
lines.push(chalk7.gray(" (Some placeholders need manual completion)"));
|
|
8682
|
+
}
|
|
8683
|
+
lines.push("");
|
|
8684
|
+
lines.push(chalk7.gray("This is a dry run. Use --write to save files."));
|
|
8685
|
+
return lines.join("\n");
|
|
8686
|
+
}
|
|
8687
|
+
function formatGeneratedJson(tests) {
|
|
8688
|
+
const output = tests.map((test) => ({
|
|
8689
|
+
gap: {
|
|
8690
|
+
categoryId: test.gap.categoryId,
|
|
8691
|
+
categoryName: test.gap.categoryName,
|
|
8692
|
+
filePath: test.gap.filePath,
|
|
8693
|
+
lineStart: test.gap.lineStart,
|
|
8694
|
+
severity: test.gap.severity,
|
|
8695
|
+
confidence: test.gap.confidence
|
|
8696
|
+
},
|
|
8697
|
+
template: {
|
|
8698
|
+
id: test.template.id,
|
|
8699
|
+
framework: test.template.framework,
|
|
8700
|
+
language: test.template.language
|
|
8701
|
+
},
|
|
8702
|
+
suggestedPath: test.suggestedPath,
|
|
8703
|
+
content: test.result.content,
|
|
8704
|
+
imports: test.result.imports,
|
|
8705
|
+
fixtures: test.result.fixtures,
|
|
8706
|
+
substituted: test.result.substituted,
|
|
8707
|
+
unresolved: test.result.unresolved
|
|
8708
|
+
}));
|
|
8709
|
+
return JSON.stringify(output, null, 2);
|
|
8710
|
+
}
|
|
8711
|
+
function suggestTestPath(sourceFile, template, basePath) {
|
|
8712
|
+
const dir = dirname(sourceFile);
|
|
8713
|
+
const name = basename(sourceFile);
|
|
8714
|
+
const nameWithoutExt = name.replace(/\.[^.]+$/, "");
|
|
8715
|
+
const extMap = {
|
|
8716
|
+
python: ".py",
|
|
8717
|
+
typescript: ".ts",
|
|
8718
|
+
javascript: ".js",
|
|
8719
|
+
go: "_test.go",
|
|
8720
|
+
java: "Test.java",
|
|
8721
|
+
rust: ".rs"
|
|
8722
|
+
};
|
|
8723
|
+
const ext = extMap[template.language] ?? ".test.ts";
|
|
8724
|
+
let testFileName;
|
|
8725
|
+
switch (template.language) {
|
|
8726
|
+
case "python":
|
|
8727
|
+
testFileName = `test_${nameWithoutExt}${ext}`;
|
|
8728
|
+
break;
|
|
8729
|
+
case "go":
|
|
8730
|
+
testFileName = `${nameWithoutExt}${ext}`;
|
|
8731
|
+
break;
|
|
8732
|
+
case "java":
|
|
8733
|
+
testFileName = `${nameWithoutExt}${ext}`;
|
|
8734
|
+
break;
|
|
8735
|
+
default:
|
|
8736
|
+
testFileName = `${nameWithoutExt}.test${ext}`;
|
|
8737
|
+
}
|
|
8738
|
+
const relativeSrc = relative(basePath, dir);
|
|
8739
|
+
const testDir = relativeSrc.replace(/^src/, "tests");
|
|
8740
|
+
return `${testDir}/${testFileName}`;
|
|
8741
|
+
}
|
|
8742
|
+
function extractVariablesFromGap(gap) {
|
|
8743
|
+
const fileName = basename(gap.filePath);
|
|
8744
|
+
const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
|
|
8745
|
+
const funcMatch = gap.codeSnippet.match(/(?:def|function|async function|const|let|var)\s+(\w+)/);
|
|
8746
|
+
const classMatch = gap.codeSnippet.match(/(?:class)\s+(\w+)/);
|
|
8747
|
+
return {
|
|
8748
|
+
// File info
|
|
8749
|
+
filePath: gap.filePath,
|
|
8750
|
+
fileName,
|
|
8751
|
+
fileNameWithoutExt,
|
|
8752
|
+
lineNumber: gap.lineStart,
|
|
8753
|
+
// Category info
|
|
8754
|
+
categoryId: gap.categoryId,
|
|
8755
|
+
categoryName: gap.categoryName,
|
|
8756
|
+
domain: gap.domain,
|
|
8757
|
+
level: gap.level,
|
|
8758
|
+
severity: gap.severity,
|
|
8759
|
+
confidence: gap.confidence,
|
|
8760
|
+
// Code context
|
|
8761
|
+
codeSnippet: gap.codeSnippet,
|
|
8762
|
+
functionName: funcMatch?.[1] ?? "targetFunction",
|
|
8763
|
+
className: classMatch?.[1] ?? "TargetClass",
|
|
8764
|
+
// Common template variables
|
|
8765
|
+
testName: `test_${gap.categoryId.replace(/-/g, "_")}`,
|
|
8766
|
+
testDescription: `Test for ${gap.categoryName} in ${fileName}:${gap.lineStart}`,
|
|
8767
|
+
// Pattern info
|
|
8768
|
+
patternId: gap.patternId,
|
|
8769
|
+
patternType: gap.patternType
|
|
8770
|
+
};
|
|
8771
|
+
}
|
|
8772
|
+
async function extractVariablesWithAI(gap, templateVariables, aiConfig) {
|
|
8773
|
+
const baseVars = extractVariablesFromGap(gap);
|
|
8774
|
+
const result = await suggestVariables(
|
|
8775
|
+
{
|
|
8776
|
+
codeSnippet: gap.codeSnippet,
|
|
8777
|
+
filePath: gap.filePath,
|
|
8778
|
+
variables: templateVariables,
|
|
8779
|
+
gap,
|
|
8780
|
+
existingValues: baseVars
|
|
8781
|
+
},
|
|
8782
|
+
aiConfig
|
|
8783
|
+
);
|
|
8784
|
+
if (result.success && result.data) {
|
|
8785
|
+
return result.data.values;
|
|
8786
|
+
}
|
|
8787
|
+
return baseVars;
|
|
8788
|
+
}
|
|
8789
|
+
async function writeGeneratedTests(tests, basePath, outputDir) {
|
|
8790
|
+
const summary = {
|
|
8791
|
+
created: [],
|
|
8792
|
+
updated: [],
|
|
8793
|
+
failed: [],
|
|
8794
|
+
totalTests: 0
|
|
8795
|
+
};
|
|
8796
|
+
const testsByFile = /* @__PURE__ */ new Map();
|
|
8797
|
+
for (const test of tests) {
|
|
8798
|
+
let outputPath;
|
|
8799
|
+
if (outputDir) {
|
|
8800
|
+
outputPath = resolve(basePath, outputDir, test.suggestedPath);
|
|
8497
8801
|
} else {
|
|
8498
|
-
|
|
8802
|
+
outputPath = resolve(basePath, test.suggestedPath);
|
|
8499
8803
|
}
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8804
|
+
const existing = testsByFile.get(outputPath) ?? [];
|
|
8805
|
+
existing.push(test);
|
|
8806
|
+
testsByFile.set(outputPath, existing);
|
|
8807
|
+
}
|
|
8808
|
+
for (const [outputPath, fileTests] of testsByFile) {
|
|
8809
|
+
try {
|
|
8810
|
+
const dir = dirname(outputPath);
|
|
8811
|
+
await mkdir(dir, { recursive: true });
|
|
8812
|
+
let existingContent = "";
|
|
8813
|
+
let fileExists = false;
|
|
8814
|
+
try {
|
|
8815
|
+
existingContent = await readFile(outputPath, "utf-8");
|
|
8816
|
+
fileExists = true;
|
|
8817
|
+
} catch {
|
|
8818
|
+
}
|
|
8819
|
+
const contentParts = [];
|
|
8820
|
+
const allImports = /* @__PURE__ */ new Set();
|
|
8821
|
+
for (const test of fileTests) {
|
|
8822
|
+
for (const imp of test.result.imports) {
|
|
8823
|
+
allImports.add(imp);
|
|
8824
|
+
}
|
|
8825
|
+
}
|
|
8826
|
+
if (!fileExists && allImports.size > 0) {
|
|
8827
|
+
contentParts.push(Array.from(allImports).join("\n"));
|
|
8828
|
+
contentParts.push("");
|
|
8829
|
+
}
|
|
8830
|
+
for (const test of fileTests) {
|
|
8831
|
+
contentParts.push(`// Test for ${test.gap.categoryName}`);
|
|
8832
|
+
contentParts.push(`// Gap: ${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`);
|
|
8833
|
+
contentParts.push(`// Generated by Pinata`);
|
|
8834
|
+
contentParts.push("");
|
|
8835
|
+
contentParts.push(test.result.content);
|
|
8836
|
+
contentParts.push("");
|
|
8837
|
+
}
|
|
8838
|
+
const newContent = contentParts.join("\n");
|
|
8839
|
+
let finalContent;
|
|
8840
|
+
let appended = false;
|
|
8841
|
+
if (fileExists) {
|
|
8842
|
+
finalContent = existingContent.trimEnd() + "\n\n" + newContent;
|
|
8843
|
+
appended = true;
|
|
8844
|
+
} else {
|
|
8845
|
+
finalContent = newContent;
|
|
8846
|
+
}
|
|
8847
|
+
await writeFile(outputPath, finalContent, "utf-8");
|
|
8848
|
+
for (const test of fileTests) {
|
|
8849
|
+
const result = {
|
|
8850
|
+
path: outputPath,
|
|
8851
|
+
created: !fileExists,
|
|
8852
|
+
appended,
|
|
8853
|
+
categoryId: test.gap.categoryId,
|
|
8854
|
+
gapLocation: `${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`
|
|
8855
|
+
};
|
|
8856
|
+
if (fileExists) {
|
|
8857
|
+
summary.updated.push(result);
|
|
8858
|
+
} else {
|
|
8859
|
+
summary.created.push(result);
|
|
8860
|
+
}
|
|
8861
|
+
summary.totalTests++;
|
|
8504
8862
|
}
|
|
8505
|
-
}
|
|
8506
|
-
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
high: 2,
|
|
8510
|
-
medium: 1
|
|
8511
|
-
};
|
|
8512
|
-
const failLevel = severityOrder[failOn] ?? 0;
|
|
8513
|
-
const hasFailingGaps = scanResult.data.gaps.some((gap) => {
|
|
8514
|
-
const gapLevel = severityOrder[gap.severity] ?? 0;
|
|
8515
|
-
return gapLevel >= failLevel;
|
|
8863
|
+
} catch (error) {
|
|
8864
|
+
summary.failed.push({
|
|
8865
|
+
path: outputPath,
|
|
8866
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8516
8867
|
});
|
|
8517
|
-
if (hasFailingGaps) {
|
|
8518
|
-
const count = scanResult.data.gaps.filter((gap) => {
|
|
8519
|
-
const gapLevel = severityOrder[gap.severity] ?? 0;
|
|
8520
|
-
return gapLevel >= failLevel;
|
|
8521
|
-
}).length;
|
|
8522
|
-
logger.debug(`Exiting with code 1 due to ${count} gaps at ${failOn} level or above`);
|
|
8523
|
-
process.exit(1);
|
|
8524
|
-
}
|
|
8525
8868
|
}
|
|
8526
|
-
process.exit(0);
|
|
8527
|
-
} catch (error) {
|
|
8528
|
-
spinner?.fail("Analysis failed");
|
|
8529
|
-
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
8530
|
-
process.exit(1);
|
|
8531
|
-
}
|
|
8532
|
-
});
|
|
8533
|
-
program.command("generate").description("Generate tests for identified gaps").option("--gaps", "Generate tests for all identified gaps").option("-c, --category <id>", "Generate tests for specific category").option("-d, --domain <domain>", "Generate tests for all categories in domain").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "medium").option("--output-dir <dir>", "Directory for generated test files").option("--write", "Write files to disk (default is dry-run)").option("--ai", "Use AI for smarter template variable filling").option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, json", "terminal").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (options) => {
|
|
8534
|
-
const isQuiet = Boolean(options["quiet"]);
|
|
8535
|
-
const isVerbose = Boolean(options["verbose"]);
|
|
8536
|
-
const dryRun = !options["write"];
|
|
8537
|
-
const useAI = Boolean(options["ai"]);
|
|
8538
|
-
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
8539
|
-
const outputFormat = String(options["output"] ?? "terminal");
|
|
8540
|
-
if (isQuiet) {
|
|
8541
|
-
logger.configure({ level: "error" });
|
|
8542
|
-
} else if (isVerbose) {
|
|
8543
|
-
logger.configure({ level: "debug" });
|
|
8544
8869
|
}
|
|
8545
|
-
|
|
8546
|
-
|
|
8547
|
-
|
|
8870
|
+
return ok(summary);
|
|
8871
|
+
}
|
|
8872
|
+
function formatWriteSummary(summary, basePath) {
|
|
8873
|
+
const lines = [];
|
|
8874
|
+
lines.push("");
|
|
8875
|
+
lines.push(chalk7.bold.cyan("Write Summary:"));
|
|
8876
|
+
lines.push(chalk7.gray("\u2500".repeat(60)));
|
|
8877
|
+
if (summary.created.length > 0) {
|
|
8878
|
+
const uniquePaths = new Set(summary.created.map((r) => r.path));
|
|
8879
|
+
lines.push("");
|
|
8880
|
+
lines.push(chalk7.green.bold(`Created ${uniquePaths.size} file(s):`));
|
|
8881
|
+
for (const path2 of uniquePaths) {
|
|
8882
|
+
const relPath = relative(basePath, path2);
|
|
8883
|
+
const testsInFile = summary.created.filter((r) => r.path === path2).length;
|
|
8884
|
+
lines.push(chalk7.green(` + ${relPath} (${testsInFile} test(s))`));
|
|
8885
|
+
}
|
|
8548
8886
|
}
|
|
8549
|
-
|
|
8550
|
-
|
|
8551
|
-
|
|
8552
|
-
|
|
8553
|
-
|
|
8554
|
-
|
|
8555
|
-
|
|
8556
|
-
|
|
8887
|
+
if (summary.updated.length > 0) {
|
|
8888
|
+
const uniquePaths = new Set(summary.updated.map((r) => r.path));
|
|
8889
|
+
lines.push("");
|
|
8890
|
+
lines.push(chalk7.yellow.bold(`Updated ${uniquePaths.size} file(s):`));
|
|
8891
|
+
for (const path2 of uniquePaths) {
|
|
8892
|
+
const relPath = relative(basePath, path2);
|
|
8893
|
+
const testsInFile = summary.updated.filter((r) => r.path === path2).length;
|
|
8894
|
+
lines.push(chalk7.yellow(` ~ ${relPath} (${testsInFile} test(s) appended)`));
|
|
8895
|
+
}
|
|
8557
8896
|
}
|
|
8558
|
-
if (
|
|
8559
|
-
|
|
8560
|
-
|
|
8561
|
-
)
|
|
8562
|
-
|
|
8897
|
+
if (summary.failed.length > 0) {
|
|
8898
|
+
lines.push("");
|
|
8899
|
+
lines.push(chalk7.red.bold(`Failed to write ${summary.failed.length} file(s):`));
|
|
8900
|
+
for (const fail of summary.failed) {
|
|
8901
|
+
const relPath = relative(basePath, fail.path);
|
|
8902
|
+
lines.push(chalk7.red(` \u2717 ${relPath}: ${fail.error}`));
|
|
8903
|
+
}
|
|
8563
8904
|
}
|
|
8564
|
-
|
|
8565
|
-
|
|
8566
|
-
|
|
8567
|
-
|
|
8568
|
-
|
|
8569
|
-
)));
|
|
8570
|
-
process.exit(1);
|
|
8905
|
+
lines.push("");
|
|
8906
|
+
lines.push(chalk7.gray("\u2500".repeat(60)));
|
|
8907
|
+
lines.push(chalk7.bold(`Total: ${summary.totalTests} test(s) written to ${(/* @__PURE__ */ new Set([...summary.created.map((r) => r.path), ...summary.updated.map((r) => r.path)])).size} file(s)`));
|
|
8908
|
+
if (summary.failed.length > 0) {
|
|
8909
|
+
lines.push(chalk7.red(`Failures: ${summary.failed.length}`));
|
|
8571
8910
|
}
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
const
|
|
8582
|
-
const
|
|
8583
|
-
|
|
8584
|
-
|
|
8585
|
-
|
|
8586
|
-
|
|
8587
|
-
|
|
8588
|
-
|
|
8589
|
-
const cached = cacheResult.data;
|
|
8590
|
-
let gaps = cached.gaps;
|
|
8591
|
-
if (spinner) {
|
|
8592
|
-
spinner.text = `Loaded ${gaps.length} gaps from cache. Filtering...`;
|
|
8593
|
-
}
|
|
8594
|
-
if (categoryId) {
|
|
8595
|
-
gaps = gaps.filter((g) => g.categoryId === categoryId);
|
|
8596
|
-
}
|
|
8597
|
-
if (domainFilter) {
|
|
8598
|
-
gaps = gaps.filter((g) => g.domain === domainFilter);
|
|
8599
|
-
}
|
|
8600
|
-
gaps = gaps.filter((g) => {
|
|
8601
|
-
const gapLevel = severityOrder[g.severity] ?? 0;
|
|
8602
|
-
const minLevel = severityOrder[minSeverity] ?? 0;
|
|
8603
|
-
return gapLevel >= minLevel;
|
|
8604
|
-
});
|
|
8605
|
-
if (gaps.length === 0) {
|
|
8606
|
-
spinner?.succeed("No gaps match the filters");
|
|
8607
|
-
console.log(chalk5.yellow("\nNo gaps found matching the specified filters."));
|
|
8608
|
-
process.exit(0);
|
|
8911
|
+
return lines.join("\n");
|
|
8912
|
+
}
|
|
8913
|
+
|
|
8914
|
+
// src/cli/commands/generate.ts
|
|
8915
|
+
init_results_cache();
|
|
8916
|
+
function registerGenerateCommand(program2) {
|
|
8917
|
+
program2.command("generate").description("Generate tests for identified gaps").option("--gaps", "Generate tests for all identified gaps").option("-c, --category <id>", "Generate tests for specific category").option("-d, --domain <domain>", "Generate tests for all categories in domain").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "medium").option("--output-dir <dir>", "Directory for generated test files").option("--write", "Write files to disk (default is dry-run)").option("--ai", "Use AI for smarter template variable filling").option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, json", "terminal").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (options) => {
|
|
8918
|
+
const isQuiet = Boolean(options["quiet"]);
|
|
8919
|
+
const isVerbose = Boolean(options["verbose"]);
|
|
8920
|
+
const dryRun = !options["write"];
|
|
8921
|
+
const useAI = Boolean(options["ai"]);
|
|
8922
|
+
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
8923
|
+
const outputFormat = String(options["output"] ?? "terminal");
|
|
8924
|
+
if (isQuiet) {
|
|
8925
|
+
logger.configure({ level: "error" });
|
|
8926
|
+
} else if (isVerbose) {
|
|
8927
|
+
logger.configure({ level: "debug" });
|
|
8609
8928
|
}
|
|
8610
|
-
if (
|
|
8611
|
-
|
|
8929
|
+
if (!["terminal", "json"].includes(outputFormat)) {
|
|
8930
|
+
console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json`)));
|
|
8931
|
+
process.exit(1);
|
|
8612
8932
|
}
|
|
8613
|
-
const
|
|
8614
|
-
const
|
|
8615
|
-
const
|
|
8616
|
-
if (!
|
|
8617
|
-
|
|
8618
|
-
console.error(formatError(loadResult.error));
|
|
8933
|
+
const hasGaps = Boolean(options["gaps"]);
|
|
8934
|
+
const categoryId = options["category"];
|
|
8935
|
+
const domainFilter = options["domain"];
|
|
8936
|
+
if (!hasGaps && !categoryId && !domainFilter) {
|
|
8937
|
+
console.error(formatError(new Error("Specify what to generate: --gaps (all gaps), --category <id>, or --domain <domain>")));
|
|
8619
8938
|
process.exit(1);
|
|
8620
8939
|
}
|
|
8621
|
-
if (
|
|
8622
|
-
|
|
8940
|
+
if (domainFilter && !RISK_DOMAINS.includes(domainFilter)) {
|
|
8941
|
+
console.error(formatError(new Error(`Invalid domain: ${domainFilter}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
|
|
8942
|
+
process.exit(1);
|
|
8623
8943
|
}
|
|
8624
|
-
const
|
|
8625
|
-
const
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
const existing = gapsByCategory.get(gap.categoryId) ?? [];
|
|
8630
|
-
existing.push(gap);
|
|
8631
|
-
gapsByCategory.set(gap.categoryId, existing);
|
|
8944
|
+
const validSeverities = ["critical", "high", "medium", "low"];
|
|
8945
|
+
const minSeverity = String(options["severity"] ?? "medium");
|
|
8946
|
+
if (!validSeverities.includes(minSeverity)) {
|
|
8947
|
+
console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
|
|
8948
|
+
process.exit(1);
|
|
8632
8949
|
}
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8950
|
+
const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
8951
|
+
const showSpinner = outputFormat === "terminal" && !isQuiet;
|
|
8952
|
+
const spinner = showSpinner ? ora3("Loading cached scan results...").start() : null;
|
|
8953
|
+
try {
|
|
8954
|
+
const projectRoot = process.cwd();
|
|
8955
|
+
const cacheResult = await loadScanResults(projectRoot);
|
|
8956
|
+
if (!cacheResult.success) {
|
|
8957
|
+
spinner?.fail("No cached results");
|
|
8958
|
+
console.error(formatError(cacheResult.error));
|
|
8959
|
+
console.error(chalk7.yellow("\nRun `pinata analyze` first to scan for gaps."));
|
|
8960
|
+
process.exit(1);
|
|
8638
8961
|
}
|
|
8639
|
-
const
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8962
|
+
const cached = cacheResult.data;
|
|
8963
|
+
let gaps = cached.gaps;
|
|
8964
|
+
if (spinner) {
|
|
8965
|
+
spinner.text = `Loaded ${gaps.length} gaps from cache. Filtering...`;
|
|
8966
|
+
}
|
|
8967
|
+
if (categoryId) {
|
|
8968
|
+
gaps = gaps.filter((g) => g.categoryId === categoryId);
|
|
8969
|
+
}
|
|
8970
|
+
if (domainFilter) {
|
|
8971
|
+
gaps = gaps.filter((g) => g.domain === domainFilter);
|
|
8972
|
+
}
|
|
8973
|
+
gaps = gaps.filter((g) => (severityOrder[g.severity] ?? 0) >= (severityOrder[minSeverity] ?? 0));
|
|
8974
|
+
if (gaps.length === 0) {
|
|
8975
|
+
spinner?.succeed("No gaps match the filters");
|
|
8976
|
+
console.log(chalk7.yellow("\nNo gaps found matching the specified filters."));
|
|
8977
|
+
process.exit(0);
|
|
8978
|
+
}
|
|
8979
|
+
if (spinner) {
|
|
8980
|
+
spinner.text = `Found ${gaps.length} gaps. Loading categories...`;
|
|
8981
|
+
}
|
|
8982
|
+
const store = createCategoryStore();
|
|
8983
|
+
const definitionsPath = getDefinitionsPath();
|
|
8984
|
+
const loadResult = await store.loadFromDirectory(definitionsPath);
|
|
8985
|
+
if (!loadResult.success) {
|
|
8986
|
+
spinner?.fail("Failed to load categories");
|
|
8987
|
+
console.error(formatError(loadResult.error));
|
|
8988
|
+
process.exit(1);
|
|
8989
|
+
}
|
|
8990
|
+
if (spinner) {
|
|
8991
|
+
spinner.text = `Generating tests for ${gaps.length} gaps...`;
|
|
8992
|
+
}
|
|
8993
|
+
const renderer = createRenderer({ strict: false, allowUnresolved: true });
|
|
8994
|
+
const generatedTests = [];
|
|
8995
|
+
const errors = [];
|
|
8996
|
+
const gapsByCategory = /* @__PURE__ */ new Map();
|
|
8997
|
+
for (const gap of gaps) {
|
|
8998
|
+
const existing = gapsByCategory.get(gap.categoryId) ?? [];
|
|
8999
|
+
existing.push(gap);
|
|
9000
|
+
gapsByCategory.set(gap.categoryId, existing);
|
|
9001
|
+
}
|
|
9002
|
+
for (const [catId, categoryGaps] of gapsByCategory) {
|
|
9003
|
+
const categoryResult = store.get(catId);
|
|
9004
|
+
if (!categoryResult.success) {
|
|
9005
|
+
errors.push(`Category not found: ${catId}`);
|
|
8659
9006
|
continue;
|
|
8660
9007
|
}
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8667
|
-
|
|
8668
|
-
|
|
8669
|
-
|
|
8670
|
-
|
|
8671
|
-
|
|
8672
|
-
|
|
9008
|
+
const category = categoryResult.data;
|
|
9009
|
+
for (const gap of categoryGaps) {
|
|
9010
|
+
const gapExt = gap.filePath.split(".").pop() ?? "";
|
|
9011
|
+
const langMap = { py: "python", ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", go: "go", java: "java", rs: "rust" };
|
|
9012
|
+
const gapLang = langMap[gapExt];
|
|
9013
|
+
let template = category.testTemplates.find((t) => t.language === gapLang);
|
|
9014
|
+
if (!template) {
|
|
9015
|
+
template = category.testTemplates[0];
|
|
9016
|
+
}
|
|
9017
|
+
if (!template) {
|
|
9018
|
+
errors.push(`No templates available for ${catId}`);
|
|
9019
|
+
continue;
|
|
9020
|
+
}
|
|
9021
|
+
let variables;
|
|
9022
|
+
if (useAI) {
|
|
9023
|
+
variables = await extractVariablesWithAI(gap, template.variables, { provider: aiProvider });
|
|
9024
|
+
} else {
|
|
9025
|
+
variables = extractVariablesFromGap(gap);
|
|
9026
|
+
}
|
|
9027
|
+
const renderResult = renderer.renderTemplate(template, variables);
|
|
9028
|
+
if (!renderResult.success) {
|
|
9029
|
+
errors.push(`Failed to render ${catId}: ${renderResult.error.message}`);
|
|
9030
|
+
continue;
|
|
9031
|
+
}
|
|
9032
|
+
const suggestedPath = suggestTestPath(gap.filePath, template, cached.targetDirectory);
|
|
9033
|
+
generatedTests.push({ gap, category, template, result: renderResult.data, suggestedPath });
|
|
8673
9034
|
}
|
|
8674
|
-
const suggestedPath = suggestTestPath(gap.filePath, template, cached.targetDirectory);
|
|
8675
|
-
generatedTests.push({
|
|
8676
|
-
gap,
|
|
8677
|
-
category,
|
|
8678
|
-
template,
|
|
8679
|
-
result: renderResult.data,
|
|
8680
|
-
suggestedPath
|
|
8681
|
-
});
|
|
8682
9035
|
}
|
|
8683
|
-
|
|
8684
|
-
|
|
8685
|
-
|
|
8686
|
-
|
|
8687
|
-
|
|
8688
|
-
console.log(formatGeneratedTerminal(generatedTests, cached.targetDirectory));
|
|
8689
|
-
}
|
|
8690
|
-
if (isVerbose && errors.length > 0) {
|
|
8691
|
-
console.error(chalk5.yellow("\nWarnings:"));
|
|
8692
|
-
for (const error of errors) {
|
|
8693
|
-
console.error(chalk5.gray(` - ${error}`));
|
|
9036
|
+
spinner?.stop();
|
|
9037
|
+
if (outputFormat === "json") {
|
|
9038
|
+
console.log(formatGeneratedJson(generatedTests));
|
|
9039
|
+
} else {
|
|
9040
|
+
console.log(formatGeneratedTerminal(generatedTests, cached.targetDirectory));
|
|
8694
9041
|
}
|
|
8695
|
-
|
|
8696
|
-
|
|
8697
|
-
|
|
8698
|
-
|
|
8699
|
-
|
|
8700
|
-
cached.targetDirectory,
|
|
8701
|
-
outputDirOption
|
|
8702
|
-
);
|
|
8703
|
-
if (!writeResult.success) {
|
|
8704
|
-
console.error(formatError(writeResult.error));
|
|
8705
|
-
process.exit(1);
|
|
9042
|
+
if (isVerbose && errors.length > 0) {
|
|
9043
|
+
console.error(chalk7.yellow("\nWarnings:"));
|
|
9044
|
+
for (const error of errors) {
|
|
9045
|
+
console.error(chalk7.gray(` - ${error}`));
|
|
9046
|
+
}
|
|
8706
9047
|
}
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
9048
|
+
if (!dryRun) {
|
|
9049
|
+
const outputDirOption = options["outputDir"];
|
|
9050
|
+
const writeResult = await writeGeneratedTests(generatedTests, cached.targetDirectory, outputDirOption);
|
|
9051
|
+
if (!writeResult.success) {
|
|
9052
|
+
console.error(formatError(writeResult.error));
|
|
9053
|
+
process.exit(1);
|
|
9054
|
+
}
|
|
9055
|
+
console.log(formatWriteSummary(writeResult.data, cached.targetDirectory));
|
|
9056
|
+
if (writeResult.data.failed.length > 0) {
|
|
9057
|
+
process.exit(1);
|
|
9058
|
+
}
|
|
8710
9059
|
}
|
|
9060
|
+
process.exit(0);
|
|
9061
|
+
} catch (error) {
|
|
9062
|
+
spinner?.fail("Generation failed");
|
|
9063
|
+
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9064
|
+
process.exit(1);
|
|
8711
9065
|
}
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8716
|
-
|
|
8717
|
-
|
|
8718
|
-
|
|
9066
|
+
});
|
|
9067
|
+
}
|
|
9068
|
+
|
|
9069
|
+
// src/cli/index.ts
|
|
9070
|
+
var program = new Command();
|
|
9071
|
+
program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
|
|
9072
|
+
registerAnalyzeCommand(program);
|
|
9073
|
+
registerGenerateCommand(program);
|
|
8719
9074
|
program.command("explain").description("Get natural language explanations for detected gaps").option("-n, --top <count>", "Explain top N gaps by priority", "5").option("-c, --category <id>", "Explain gaps for specific category").option("-d, --domain <domain>", "Explain gaps for specific domain").option("--ai", "Use AI for detailed explanations (requires API key)").option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, json, markdown", "terminal").option("-v, --verbose", "Show more details").option("-q, --quiet", "Quiet mode (errors only)").action(async (options) => {
|
|
8720
9075
|
const isQuiet = Boolean(options["quiet"]);
|
|
8721
9076
|
const isVerbose = Boolean(options["verbose"]);
|
|
9077
|
+
const topN = parseInt(String(options["top"] ?? "5"), 10);
|
|
9078
|
+
const categoryFilter = options["category"];
|
|
9079
|
+
const domainFilter = options["domain"];
|
|
8722
9080
|
const useAI = Boolean(options["ai"]);
|
|
8723
9081
|
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
8724
9082
|
const outputFormat = String(options["output"] ?? "terminal");
|
|
8725
|
-
const topN = parseInt(String(options["top"] ?? "5"), 10);
|
|
8726
9083
|
if (isQuiet) {
|
|
8727
9084
|
logger.configure({ level: "error" });
|
|
8728
9085
|
} else if (isVerbose) {
|
|
8729
9086
|
logger.configure({ level: "debug" });
|
|
8730
9087
|
}
|
|
8731
9088
|
if (!["terminal", "json", "markdown"].includes(outputFormat)) {
|
|
8732
|
-
console.error(formatError(new Error(`Invalid
|
|
9089
|
+
console.error(formatError(new Error(`Invalid format: ${outputFormat}. Use: terminal, json, markdown`)));
|
|
8733
9090
|
process.exit(1);
|
|
8734
9091
|
}
|
|
8735
|
-
const
|
|
8736
|
-
const
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
9092
|
+
const projectRoot = process.cwd();
|
|
9093
|
+
const cacheResult = await loadScanResults(projectRoot);
|
|
9094
|
+
if (!cacheResult.success) {
|
|
9095
|
+
console.error(formatError(cacheResult.error));
|
|
9096
|
+
console.error(chalk7.yellow("\nRun `pinata analyze` first to scan for gaps."));
|
|
9097
|
+
process.exit(1);
|
|
9098
|
+
}
|
|
9099
|
+
const cached = cacheResult.data;
|
|
9100
|
+
let gaps = cached.gaps;
|
|
9101
|
+
if (categoryFilter) {
|
|
9102
|
+
gaps = gaps.filter((g) => g.categoryId === categoryFilter);
|
|
9103
|
+
}
|
|
9104
|
+
if (domainFilter) {
|
|
9105
|
+
if (!RISK_DOMAINS.includes(domainFilter)) {
|
|
9106
|
+
console.error(formatError(new Error(`Invalid domain: ${domainFilter}`)));
|
|
8744
9107
|
process.exit(1);
|
|
8745
9108
|
}
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
9109
|
+
gaps = gaps.filter((g) => g.domain === domainFilter);
|
|
9110
|
+
}
|
|
9111
|
+
gaps = gaps.slice(0, topN);
|
|
9112
|
+
if (gaps.length === 0) {
|
|
9113
|
+
console.log(chalk7.yellow("No gaps to explain."));
|
|
9114
|
+
process.exit(0);
|
|
9115
|
+
}
|
|
9116
|
+
let explanations;
|
|
9117
|
+
if (useAI) {
|
|
9118
|
+
const spinner = ora3("Generating AI explanations...").start();
|
|
9119
|
+
try {
|
|
9120
|
+
const aiConfig = { provider: aiProvider };
|
|
9121
|
+
const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9122
|
+
if (hasApiKey2(aiProvider)) {
|
|
9123
|
+
const key = getApiKey2(aiProvider);
|
|
9124
|
+
if (key) {
|
|
9125
|
+
aiConfig.apiKey = key;
|
|
9126
|
+
}
|
|
8758
9127
|
}
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
spinner.
|
|
8769
|
-
}
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
const ai = createAIService({ provider: aiProvider });
|
|
8773
|
-
if (!ai.isConfigured()) {
|
|
8774
|
-
spinner?.warn("AI not configured, using fallback explanations");
|
|
8775
|
-
console.error(chalk5.yellow(`
|
|
9128
|
+
const resultMap = await explainGaps(gaps, void 0, aiConfig);
|
|
9129
|
+
explanations = gaps.map((g) => {
|
|
9130
|
+
const key = `${g.categoryId}:${g.filePath}:${g.lineStart}`;
|
|
9131
|
+
const result = resultMap.get(key);
|
|
9132
|
+
if (result?.success && result.data) {
|
|
9133
|
+
return result.data;
|
|
9134
|
+
}
|
|
9135
|
+
return generateFallbackExplanation(g);
|
|
9136
|
+
});
|
|
9137
|
+
spinner.succeed(`Generated ${explanations.length} explanations`);
|
|
9138
|
+
} catch (error) {
|
|
9139
|
+
spinner.fail("AI explanation failed");
|
|
9140
|
+
console.error(chalk7.yellow(`
|
|
8776
9141
|
Set ${aiProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} for AI explanations.
|
|
8777
9142
|
`));
|
|
8778
|
-
|
|
8779
|
-
explanations.push({
|
|
8780
|
-
gap,
|
|
8781
|
-
explanation: generateFallbackExplanation(gap)
|
|
8782
|
-
});
|
|
8783
|
-
}
|
|
8784
|
-
} else {
|
|
8785
|
-
for (const gap of gaps) {
|
|
8786
|
-
const result = await explainGap(gap, void 0, { provider: aiProvider });
|
|
8787
|
-
if (result.success && result.data) {
|
|
8788
|
-
explanations.push({ gap, explanation: result.data });
|
|
8789
|
-
} else {
|
|
8790
|
-
explanations.push({
|
|
8791
|
-
gap,
|
|
8792
|
-
explanation: generateFallbackExplanation(gap)
|
|
8793
|
-
});
|
|
8794
|
-
}
|
|
8795
|
-
}
|
|
8796
|
-
}
|
|
8797
|
-
} else {
|
|
8798
|
-
for (const gap of gaps) {
|
|
8799
|
-
explanations.push({
|
|
8800
|
-
gap,
|
|
8801
|
-
explanation: generateFallbackExplanation(gap)
|
|
8802
|
-
});
|
|
8803
|
-
}
|
|
9143
|
+
explanations = gaps.map((g) => generateFallbackExplanation(g));
|
|
8804
9144
|
}
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
8814
|
-
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
explanation: e.explanation
|
|
8818
|
-
})), null, 2));
|
|
8819
|
-
} else if (outputFormat === "markdown") {
|
|
8820
|
-
console.log(`# Gap Explanations
|
|
8821
|
-
`);
|
|
8822
|
-
console.log(`Generated ${explanations.length} explanation(s).
|
|
8823
|
-
`);
|
|
8824
|
-
for (const { gap, explanation } of explanations) {
|
|
8825
|
-
console.log(`## ${gap.categoryName}
|
|
8826
|
-
`);
|
|
8827
|
-
console.log(`**File:** \`${gap.filePath}:${gap.lineStart}\`
|
|
8828
|
-
`);
|
|
8829
|
-
console.log(`**Severity:** ${gap.severity} | **Confidence:** ${gap.confidence}
|
|
8830
|
-
`);
|
|
8831
|
-
console.log(`### Summary
|
|
8832
|
-
${explanation.summary}
|
|
8833
|
-
`);
|
|
8834
|
-
console.log(`### Explanation
|
|
8835
|
-
${explanation.explanation}
|
|
8836
|
-
`);
|
|
8837
|
-
console.log(`### Risk
|
|
8838
|
-
${explanation.risk}
|
|
9145
|
+
} else {
|
|
9146
|
+
explanations = gaps.map((g) => generateFallbackExplanation(g));
|
|
9147
|
+
}
|
|
9148
|
+
if (outputFormat === "json") {
|
|
9149
|
+
const output = gaps.map((g, i) => ({ gap: { categoryId: g.categoryId, severity: g.severity, filePath: g.filePath, lineStart: g.lineStart }, ...explanations[i] }));
|
|
9150
|
+
console.log(JSON.stringify(output, null, 2));
|
|
9151
|
+
} else if (outputFormat === "markdown") {
|
|
9152
|
+
console.log("# Gap Explanations\n");
|
|
9153
|
+
for (let i = 0; i < explanations.length; i++) {
|
|
9154
|
+
const exp = explanations[i];
|
|
9155
|
+
const gap = gaps[i];
|
|
9156
|
+
console.log(`## ${exp.summary}
|
|
8839
9157
|
`);
|
|
8840
|
-
|
|
8841
|
-
${explanation.remediation}
|
|
9158
|
+
console.log(`**Severity**: ${gap.severity} | **Category**: ${gap.categoryId}
|
|
8842
9159
|
`);
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
${
|
|
8847
|
-
\`\`\`
|
|
9160
|
+
console.log(exp.explanation);
|
|
9161
|
+
if (exp.remediation) {
|
|
9162
|
+
console.log(`
|
|
9163
|
+
**Remediation**: ${exp.remediation}
|
|
8848
9164
|
`);
|
|
8849
|
-
}
|
|
8850
|
-
console.log("---\n");
|
|
8851
9165
|
}
|
|
8852
|
-
|
|
9166
|
+
console.log("---\n");
|
|
9167
|
+
}
|
|
9168
|
+
} else {
|
|
9169
|
+
console.log();
|
|
9170
|
+
for (let i = 0; i < explanations.length; i++) {
|
|
9171
|
+
const exp = explanations[i];
|
|
9172
|
+
const gap = gaps[i];
|
|
9173
|
+
const severityColor = gap.severity === "critical" ? chalk7.red : gap.severity === "high" ? chalk7.yellow : chalk7.blue;
|
|
9174
|
+
console.log(`${severityColor.bold(`[${gap.severity.toUpperCase()}]`)} ${chalk7.bold(exp.summary)}`);
|
|
9175
|
+
console.log(chalk7.gray(` Category: ${gap.categoryId} | ${gap.filePath}:${gap.lineStart}`));
|
|
8853
9176
|
console.log();
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
8858
|
-
console.log(chalk5.bold.white(gap.categoryName));
|
|
8859
|
-
console.log(chalk5.gray(` ${gap.filePath}:${gap.lineStart}`));
|
|
8860
|
-
const severityColor = gap.severity === "critical" ? chalk5.red : gap.severity === "high" ? chalk5.yellow : chalk5.blue;
|
|
8861
|
-
console.log(` ${severityColor(gap.severity)} | ${gap.confidence} confidence`);
|
|
8862
|
-
console.log();
|
|
8863
|
-
console.log(chalk5.cyan(" Summary:"));
|
|
8864
|
-
console.log(` ${explanation.summary}`);
|
|
8865
|
-
if (isVerbose) {
|
|
8866
|
-
console.log();
|
|
8867
|
-
console.log(chalk5.cyan(" Explanation:"));
|
|
8868
|
-
for (const line of explanation.explanation.split("\n")) {
|
|
8869
|
-
console.log(` ${line}`);
|
|
8870
|
-
}
|
|
8871
|
-
}
|
|
8872
|
-
console.log();
|
|
8873
|
-
console.log(chalk5.red(" Risk:"));
|
|
8874
|
-
console.log(` ${explanation.risk}`);
|
|
8875
|
-
console.log();
|
|
8876
|
-
console.log(chalk5.green(" How to Fix:"));
|
|
8877
|
-
for (const line of explanation.remediation.split("\n")) {
|
|
8878
|
-
console.log(` ${line}`);
|
|
8879
|
-
}
|
|
8880
|
-
if (explanation.safeExample) {
|
|
8881
|
-
console.log();
|
|
8882
|
-
console.log(chalk5.cyan(" Safe Example:"));
|
|
8883
|
-
console.log(chalk5.gray(` ${explanation.safeExample}`));
|
|
8884
|
-
}
|
|
9177
|
+
for (const line of exp.explanation.split("\n")) {
|
|
9178
|
+
console.log(` ${line}`);
|
|
9179
|
+
}
|
|
9180
|
+
if (exp.remediation) {
|
|
8885
9181
|
console.log();
|
|
8886
|
-
console.log(
|
|
9182
|
+
console.log(chalk7.green(` Fix: ${exp.remediation}`));
|
|
8887
9183
|
}
|
|
9184
|
+
console.log();
|
|
8888
9185
|
}
|
|
8889
|
-
process.exit(0);
|
|
8890
|
-
} catch (error) {
|
|
8891
|
-
spinner?.fail("Explanation failed");
|
|
8892
|
-
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
8893
|
-
process.exit(1);
|
|
8894
9186
|
}
|
|
8895
9187
|
});
|
|
8896
9188
|
program.command("suggest-patterns").description("Use AI to suggest new detection patterns based on code samples").requiredOption("-c, --category <id>", "Category to suggest patterns for").requiredOption("-l, --language <lang>", "Language of the code samples").option("-f, --file <path>", "File containing vulnerable code samples (one per line)").option("--code <snippet>", "Vulnerable code snippet (can be specified multiple times)", (v, a) => [...a, v], []).option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, yaml, json", "terminal").action(async (options) => {
|
|
@@ -8898,96 +9190,73 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
|
|
|
8898
9190
|
const language = String(options["language"]);
|
|
8899
9191
|
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
8900
9192
|
const outputFormat = String(options["output"] ?? "terminal");
|
|
8901
|
-
const codeSnippets = options["code"];
|
|
8902
9193
|
const filePath = options["file"];
|
|
8903
|
-
|
|
9194
|
+
const codeSnippets = options["code"] ?? [];
|
|
9195
|
+
const samples = [...codeSnippets];
|
|
8904
9196
|
if (filePath) {
|
|
9197
|
+
const { readFile: readFile7 } = await import('fs/promises');
|
|
8905
9198
|
try {
|
|
8906
|
-
const
|
|
8907
|
-
|
|
8908
|
-
vulnerableCode = [...vulnerableCode, ...content.split("\n---\n").filter(Boolean)];
|
|
9199
|
+
const content = await readFile7(resolve(filePath), "utf-8");
|
|
9200
|
+
samples.push(...content.split("\n---\n").filter((s) => s.trim()));
|
|
8909
9201
|
} catch (error) {
|
|
8910
9202
|
console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
|
|
8911
9203
|
process.exit(1);
|
|
8912
9204
|
}
|
|
8913
9205
|
}
|
|
8914
|
-
if (
|
|
9206
|
+
if (samples.length === 0) {
|
|
8915
9207
|
console.error(formatError(new Error("Provide code samples via --code or --file")));
|
|
8916
9208
|
process.exit(1);
|
|
8917
9209
|
}
|
|
8918
|
-
const spinner =
|
|
9210
|
+
const spinner = ora3("Generating pattern suggestions with AI...").start();
|
|
8919
9211
|
try {
|
|
9212
|
+
const aiConfig = { provider: aiProvider };
|
|
9213
|
+
const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9214
|
+
if (hasApiKey2(aiProvider)) {
|
|
9215
|
+
const key = getApiKey2(aiProvider);
|
|
9216
|
+
if (key) {
|
|
9217
|
+
aiConfig.apiKey = key;
|
|
9218
|
+
}
|
|
9219
|
+
}
|
|
8920
9220
|
const result = await suggestPatterns(
|
|
8921
|
-
{
|
|
8922
|
-
|
|
8923
|
-
language,
|
|
8924
|
-
vulnerableCode,
|
|
8925
|
-
maxSuggestions: 5
|
|
8926
|
-
},
|
|
8927
|
-
{ provider: aiProvider }
|
|
9221
|
+
{ category: categoryId, vulnerableCode: samples, language },
|
|
9222
|
+
aiConfig
|
|
8928
9223
|
);
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
console.error(
|
|
9224
|
+
if (!result.success || !result.data) {
|
|
9225
|
+
spinner.fail("Pattern suggestion failed");
|
|
9226
|
+
console.error(chalk7.red(result.error ?? "Unknown error"));
|
|
8932
9227
|
process.exit(1);
|
|
8933
9228
|
}
|
|
8934
|
-
const
|
|
9229
|
+
const suggestions = result.data.suggestions;
|
|
9230
|
+
spinner.succeed(`Generated ${suggestions.length} pattern suggestions`);
|
|
8935
9231
|
if (outputFormat === "json") {
|
|
8936
|
-
console.log(JSON.stringify(
|
|
9232
|
+
console.log(JSON.stringify(suggestions, null, 2));
|
|
8937
9233
|
} else if (outputFormat === "yaml") {
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8943
|
-
console.log(`
|
|
8944
|
-
console.log(`
|
|
8945
|
-
console.log(`
|
|
8946
|
-
console.log(` pattern: "${escapedPattern}"`);
|
|
8947
|
-
console.log(` confidence: ${suggestion.confidence}`);
|
|
8948
|
-
console.log(` description: ${suggestion.description}`);
|
|
9234
|
+
for (const s of suggestions) {
|
|
9235
|
+
console.log("---");
|
|
9236
|
+
console.log(`id: ${s.id}`);
|
|
9237
|
+
console.log(`pattern: "${s.pattern}"`);
|
|
9238
|
+
console.log(`confidence: ${s.confidence}`);
|
|
9239
|
+
console.log(`description: "${s.description}"`);
|
|
9240
|
+
console.log(`matchExample: "${s.matchExample}"`);
|
|
9241
|
+
console.log(`safeExample: "${s.safeExample}"`);
|
|
8949
9242
|
console.log();
|
|
8950
9243
|
}
|
|
8951
9244
|
} else {
|
|
8952
9245
|
console.log();
|
|
8953
|
-
console.log(
|
|
8954
|
-
console.log(
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
console.log();
|
|
8963
|
-
console.log(chalk5.cyan(" Pattern:"));
|
|
8964
|
-
console.log(` ${suggestion.pattern}`);
|
|
8965
|
-
console.log();
|
|
8966
|
-
console.log(chalk5.cyan(" Confidence:") + ` ${suggestion.confidence}`);
|
|
8967
|
-
console.log();
|
|
8968
|
-
console.log(chalk5.green(" Would match:"));
|
|
8969
|
-
console.log(chalk5.gray(` ${suggestion.matchExample}`));
|
|
8970
|
-
console.log();
|
|
8971
|
-
console.log(chalk5.red(" Should NOT match:"));
|
|
8972
|
-
console.log(chalk5.gray(` ${suggestion.safeExample}`));
|
|
8973
|
-
console.log();
|
|
8974
|
-
console.log(chalk5.cyan(" Reasoning:"));
|
|
8975
|
-
console.log(` ${suggestion.reasoning}`);
|
|
8976
|
-
console.log();
|
|
8977
|
-
console.log(chalk5.gray("\u2500".repeat(60)));
|
|
8978
|
-
}
|
|
8979
|
-
}
|
|
8980
|
-
if (rejected.length > 0) {
|
|
9246
|
+
console.log(chalk7.bold(`Suggested Patterns for ${categoryId}`));
|
|
9247
|
+
console.log();
|
|
9248
|
+
for (const s of suggestions) {
|
|
9249
|
+
const confColor = s.confidence === "high" ? chalk7.green : s.confidence === "medium" ? chalk7.yellow : chalk7.red;
|
|
9250
|
+
console.log(` ${chalk7.cyan(s.id)} [${confColor(s.confidence)}]`);
|
|
9251
|
+
console.log(` Pattern: ${chalk7.gray(s.pattern)}`);
|
|
9252
|
+
console.log(` ${s.description}`);
|
|
9253
|
+
console.log(` Match: ${chalk7.gray(s.matchExample.slice(0, 80))}`);
|
|
9254
|
+
console.log(` Safe: ${chalk7.gray(s.safeExample.slice(0, 80))}`);
|
|
8981
9255
|
console.log();
|
|
8982
|
-
console.log(chalk5.yellow.bold(`Rejected ${rejected.length} pattern(s):`));
|
|
8983
|
-
for (const r of rejected) {
|
|
8984
|
-
console.log(chalk5.gray(` - ${r.pattern.slice(0, 40)}... : ${r.reason}`));
|
|
8985
|
-
}
|
|
8986
9256
|
}
|
|
8987
9257
|
}
|
|
8988
|
-
process.exit(0);
|
|
8989
9258
|
} catch (error) {
|
|
8990
|
-
spinner.fail("Pattern
|
|
9259
|
+
spinner.fail("Pattern suggestion failed");
|
|
8991
9260
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
8992
9261
|
process.exit(1);
|
|
8993
9262
|
}
|
|
@@ -9042,9 +9311,7 @@ program.command("search <query>").description("Search category taxonomy by name,
|
|
|
9042
9311
|
}
|
|
9043
9312
|
const langFilter = options["language"];
|
|
9044
9313
|
if (langFilter) {
|
|
9045
|
-
results = results.filter(
|
|
9046
|
-
(cat) => cat.applicableLanguages.includes(langFilter)
|
|
9047
|
-
);
|
|
9314
|
+
results = results.filter((cat) => cat.applicableLanguages.includes(langFilter));
|
|
9048
9315
|
}
|
|
9049
9316
|
if (outputFormat === "json") {
|
|
9050
9317
|
console.log(JSON.stringify(results, null, 2));
|
|
@@ -9066,19 +9333,18 @@ ${cat.description}
|
|
|
9066
9333
|
}
|
|
9067
9334
|
} else {
|
|
9068
9335
|
console.log();
|
|
9069
|
-
console.log(
|
|
9070
|
-
console.log(
|
|
9336
|
+
console.log(chalk7.bold(`Search Results for "${query}"`));
|
|
9337
|
+
console.log(chalk7.gray(`Found ${results.length} matching categories.`));
|
|
9071
9338
|
console.log();
|
|
9072
9339
|
if (results.length === 0) {
|
|
9073
|
-
console.log(
|
|
9074
|
-
console.log(chalk5.gray("Try a different query or broaden your filters."));
|
|
9340
|
+
console.log(chalk7.yellow("No categories match your search."));
|
|
9075
9341
|
} else {
|
|
9076
9342
|
for (const cat of results) {
|
|
9077
|
-
const domainColor = cat.domain === "security" ?
|
|
9078
|
-
console.log(` ${
|
|
9343
|
+
const domainColor = cat.domain === "security" ? chalk7.red : chalk7.blue;
|
|
9344
|
+
console.log(` ${chalk7.cyan(cat.id)} - ${chalk7.bold(cat.name)}`);
|
|
9079
9345
|
console.log(` ${domainColor(cat.domain)} | ${cat.level} | ${cat.priority}`);
|
|
9080
9346
|
if (options["verbose"]) {
|
|
9081
|
-
console.log(` ${
|
|
9347
|
+
console.log(` ${chalk7.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
|
|
9082
9348
|
}
|
|
9083
9349
|
console.log();
|
|
9084
9350
|
}
|
|
@@ -9112,21 +9378,17 @@ program.command("list").description("List all categories").option("-d, --domain
|
|
|
9112
9378
|
process.exit(1);
|
|
9113
9379
|
}
|
|
9114
9380
|
const priorityFilter = options["priority"];
|
|
9115
|
-
|
|
9116
|
-
if (priorityFilter !== void 0 && !validPriorities.includes(priorityFilter)) {
|
|
9381
|
+
if (priorityFilter !== void 0 && !["P0", "P1", "P2"].includes(priorityFilter)) {
|
|
9117
9382
|
console.error(formatError(new Error(`Invalid priority: ${priorityFilter}. Use: P0, P1, P2`)));
|
|
9118
9383
|
process.exit(1);
|
|
9119
9384
|
}
|
|
9120
|
-
logger.debug("Loading categories...");
|
|
9121
9385
|
const store = createCategoryStore();
|
|
9122
9386
|
const definitionsPath = getDefinitionsPath();
|
|
9123
|
-
logger.debug(`Loading from: ${definitionsPath}`);
|
|
9124
9387
|
const loadResult = await store.loadFromDirectory(definitionsPath);
|
|
9125
9388
|
if (!loadResult.success) {
|
|
9126
9389
|
console.error(formatError(loadResult.error));
|
|
9127
9390
|
process.exit(1);
|
|
9128
9391
|
}
|
|
9129
|
-
logger.debug(`Loaded ${loadResult.data} categories`);
|
|
9130
9392
|
const filter = {};
|
|
9131
9393
|
if (domainFilter) {
|
|
9132
9394
|
filter.domain = domainFilter;
|
|
@@ -9150,54 +9412,42 @@ program.command("init").description("Initialize Pinata configuration in project"
|
|
|
9150
9412
|
const configPath = resolve(process.cwd(), ".pinata.yml");
|
|
9151
9413
|
const cacheDir = resolve(process.cwd(), ".pinata");
|
|
9152
9414
|
if (existsSync(configPath) && !options["force"]) {
|
|
9153
|
-
console.log(
|
|
9154
|
-
console.log(
|
|
9415
|
+
console.log(chalk7.yellow("Configuration file already exists at .pinata.yml"));
|
|
9416
|
+
console.log(chalk7.gray("Use --force to overwrite."));
|
|
9155
9417
|
process.exit(0);
|
|
9156
9418
|
}
|
|
9157
9419
|
const defaultConfig = `# Pinata Configuration
|
|
9158
9420
|
# https://github.com/pinata/pinata
|
|
9159
9421
|
|
|
9160
|
-
# Paths to analyze
|
|
9161
9422
|
include:
|
|
9162
9423
|
- "src/**/*.ts"
|
|
9163
9424
|
- "src/**/*.tsx"
|
|
9164
9425
|
- "src/**/*.py"
|
|
9165
9426
|
- "src/**/*.js"
|
|
9166
9427
|
|
|
9167
|
-
# Paths to exclude from analysis
|
|
9168
9428
|
exclude:
|
|
9169
9429
|
- "node_modules/**"
|
|
9170
9430
|
- "dist/**"
|
|
9171
9431
|
- "build/**"
|
|
9172
9432
|
- "**/*.test.ts"
|
|
9173
9433
|
- "**/*.spec.ts"
|
|
9174
|
-
- "**/test/**"
|
|
9175
|
-
- "**/tests/**"
|
|
9176
|
-
- "**/__tests__/**"
|
|
9177
9434
|
|
|
9178
|
-
# Risk domains to analyze
|
|
9179
|
-
# Options: security, data, concurrency, input, resource, reliability, performance, platform, business, compliance
|
|
9180
9435
|
domains:
|
|
9181
9436
|
- security
|
|
9182
9437
|
- data
|
|
9183
9438
|
- concurrency
|
|
9184
9439
|
- input
|
|
9185
9440
|
|
|
9186
|
-
# Minimum severity to report
|
|
9187
|
-
# Options: critical, high, medium, low
|
|
9188
9441
|
minSeverity: medium
|
|
9189
9442
|
|
|
9190
|
-
# Output configuration
|
|
9191
9443
|
output:
|
|
9192
|
-
format: terminal
|
|
9444
|
+
format: terminal
|
|
9193
9445
|
color: true
|
|
9194
9446
|
|
|
9195
|
-
# Test generation settings
|
|
9196
9447
|
generate:
|
|
9197
9448
|
outputDir: tests/generated
|
|
9198
|
-
framework: auto
|
|
9449
|
+
framework: auto
|
|
9199
9450
|
|
|
9200
|
-
# Fail CI if gaps exceed thresholds
|
|
9201
9451
|
thresholds:
|
|
9202
9452
|
critical: 0
|
|
9203
9453
|
high: 5
|
|
@@ -9206,25 +9456,23 @@ thresholds:
|
|
|
9206
9456
|
const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
|
|
9207
9457
|
try {
|
|
9208
9458
|
await writeFileAsync(configPath, defaultConfig, "utf8");
|
|
9209
|
-
console.log(
|
|
9459
|
+
console.log(chalk7.green("Created .pinata.yml"));
|
|
9210
9460
|
await mkdir4(cacheDir, { recursive: true });
|
|
9211
|
-
console.log(
|
|
9461
|
+
console.log(chalk7.green("Created .pinata/ directory"));
|
|
9212
9462
|
const gitignorePath = resolve(process.cwd(), ".gitignore");
|
|
9213
9463
|
if (existsSync(gitignorePath)) {
|
|
9214
|
-
const { readFile:
|
|
9215
|
-
const gitignore = await
|
|
9464
|
+
const { readFile: readFile7, appendFile } = await import('fs/promises');
|
|
9465
|
+
const gitignore = await readFile7(gitignorePath, "utf8");
|
|
9216
9466
|
if (!gitignore.includes(".pinata/")) {
|
|
9217
9467
|
await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
|
|
9218
|
-
console.log(
|
|
9468
|
+
console.log(chalk7.green("Added .pinata/ to .gitignore"));
|
|
9219
9469
|
}
|
|
9220
9470
|
}
|
|
9221
9471
|
console.log();
|
|
9222
|
-
console.log(
|
|
9223
|
-
console.log();
|
|
9224
|
-
console.log("
|
|
9225
|
-
console.log(
|
|
9226
|
-
console.log(chalk5.gray(" 2. Run: pinata analyze"));
|
|
9227
|
-
console.log(chalk5.gray(" 3. Generate tests: pinata generate"));
|
|
9472
|
+
console.log(chalk7.bold("Pinata initialized successfully!"));
|
|
9473
|
+
console.log(chalk7.gray(" 1. Review and customize .pinata.yml"));
|
|
9474
|
+
console.log(chalk7.gray(" 2. Run: pinata analyze"));
|
|
9475
|
+
console.log(chalk7.gray(" 3. Generate tests: pinata generate"));
|
|
9228
9476
|
} catch (error) {
|
|
9229
9477
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9230
9478
|
process.exit(1);
|
|
@@ -9237,18 +9485,15 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9237
9485
|
const checkAge = Boolean(options["checkAge"]);
|
|
9238
9486
|
const strictMode = Boolean(options["strict"]);
|
|
9239
9487
|
const doAllChecks = !checkRegistry && !checkDownloads && !checkAge;
|
|
9240
|
-
console.log(
|
|
9488
|
+
console.log(chalk7.bold("\nPinata Dependency Audit\n"));
|
|
9241
9489
|
if (!existsSync(packagePath)) {
|
|
9242
|
-
console.error(
|
|
9490
|
+
console.error(chalk7.red(`Error: ${packagePath} not found`));
|
|
9243
9491
|
process.exit(1);
|
|
9244
9492
|
}
|
|
9245
9493
|
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
9246
|
-
const allDeps = {
|
|
9247
|
-
...packageJson.dependencies,
|
|
9248
|
-
...packageJson.devDependencies
|
|
9249
|
-
};
|
|
9494
|
+
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
9250
9495
|
const packages = Object.keys(allDeps);
|
|
9251
|
-
console.log(
|
|
9496
|
+
console.log(chalk7.gray(`Found ${packages.length} dependencies
|
|
9252
9497
|
`));
|
|
9253
9498
|
const issues = [];
|
|
9254
9499
|
const KNOWN_MALWARE = /* @__PURE__ */ new Set([
|
|
@@ -9271,56 +9516,31 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9271
9516
|
]);
|
|
9272
9517
|
for (const pkg of packages) {
|
|
9273
9518
|
if (KNOWN_MALWARE.has(pkg)) {
|
|
9274
|
-
issues.push({
|
|
9275
|
-
pkg,
|
|
9276
|
-
severity: "critical",
|
|
9277
|
-
message: "Known malicious/compromised package (Shai-Hulud/typosquat)"
|
|
9278
|
-
});
|
|
9519
|
+
issues.push({ pkg, severity: "critical", message: "Known malicious/compromised package (Shai-Hulud/typosquat)" });
|
|
9279
9520
|
}
|
|
9280
9521
|
}
|
|
9281
9522
|
for (const [pkg, version] of Object.entries(allDeps)) {
|
|
9282
9523
|
if (version?.startsWith("^")) {
|
|
9283
|
-
issues.push({
|
|
9284
|
-
pkg,
|
|
9285
|
-
severity: "warning",
|
|
9286
|
-
message: `Unpinned version (${version}) - allows minor updates`
|
|
9287
|
-
});
|
|
9524
|
+
issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows minor updates` });
|
|
9288
9525
|
} else if (version?.startsWith("~")) {
|
|
9289
|
-
issues.push({
|
|
9290
|
-
pkg,
|
|
9291
|
-
severity: "warning",
|
|
9292
|
-
message: `Unpinned version (${version}) - allows patch updates`
|
|
9293
|
-
});
|
|
9526
|
+
issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows patch updates` });
|
|
9294
9527
|
} else if (version === "*" || version === "latest") {
|
|
9295
|
-
issues.push({
|
|
9296
|
-
pkg,
|
|
9297
|
-
severity: "critical",
|
|
9298
|
-
message: `Extremely dangerous version (${version}) - allows any version`
|
|
9299
|
-
});
|
|
9528
|
+
issues.push({ pkg, severity: "critical", message: `Extremely dangerous version (${version}) - allows any version` });
|
|
9300
9529
|
}
|
|
9301
9530
|
}
|
|
9302
9531
|
if (checkRegistry || doAllChecks) {
|
|
9303
|
-
const spinner =
|
|
9532
|
+
const spinner = ora3("Checking npm registry...").start();
|
|
9304
9533
|
for (const pkg of packages.slice(0, 50)) {
|
|
9305
9534
|
try {
|
|
9306
9535
|
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`);
|
|
9307
9536
|
if (response.status === 404) {
|
|
9308
|
-
issues.push({
|
|
9309
|
-
pkg,
|
|
9310
|
-
severity: "critical",
|
|
9311
|
-
message: "Package NOT FOUND in npm registry (slopsquatting risk)"
|
|
9312
|
-
});
|
|
9537
|
+
issues.push({ pkg, severity: "critical", message: "Package NOT FOUND in npm registry (slopsquatting risk)" });
|
|
9313
9538
|
} else if (response.ok) {
|
|
9314
9539
|
const data = await response.json();
|
|
9315
9540
|
if ((checkAge || doAllChecks) && data.time?.created) {
|
|
9316
|
-
const
|
|
9317
|
-
const ageInDays = (Date.now() - created.getTime()) / (1e3 * 60 * 60 * 24);
|
|
9541
|
+
const ageInDays = (Date.now() - new Date(data.time.created).getTime()) / (1e3 * 60 * 60 * 24);
|
|
9318
9542
|
if (ageInDays < 30) {
|
|
9319
|
-
issues.push({
|
|
9320
|
-
pkg,
|
|
9321
|
-
severity: "warning",
|
|
9322
|
-
message: `Very new package (${Math.floor(ageInDays)} days old)`
|
|
9323
|
-
});
|
|
9543
|
+
issues.push({ pkg, severity: "warning", message: `Very new package (${Math.floor(ageInDays)} days old)` });
|
|
9324
9544
|
}
|
|
9325
9545
|
}
|
|
9326
9546
|
}
|
|
@@ -9332,24 +9552,24 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9332
9552
|
const criticals = issues.filter((i) => i.severity === "critical");
|
|
9333
9553
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
9334
9554
|
if (criticals.length > 0) {
|
|
9335
|
-
console.log(
|
|
9555
|
+
console.log(chalk7.red.bold(`
|
|
9336
9556
|
Critical Issues (${criticals.length}):`));
|
|
9337
9557
|
for (const issue of criticals) {
|
|
9338
|
-
console.log(
|
|
9558
|
+
console.log(chalk7.red(` \u2717 ${issue.pkg}: ${issue.message}`));
|
|
9339
9559
|
}
|
|
9340
9560
|
}
|
|
9341
9561
|
if (warnings.length > 0) {
|
|
9342
|
-
console.log(
|
|
9562
|
+
console.log(chalk7.yellow.bold(`
|
|
9343
9563
|
Warnings (${warnings.length}):`));
|
|
9344
9564
|
for (const issue of warnings.slice(0, 20)) {
|
|
9345
|
-
console.log(
|
|
9565
|
+
console.log(chalk7.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
|
|
9346
9566
|
}
|
|
9347
9567
|
if (warnings.length > 20) {
|
|
9348
|
-
console.log(
|
|
9568
|
+
console.log(chalk7.gray(` ... and ${warnings.length - 20} more`));
|
|
9349
9569
|
}
|
|
9350
9570
|
}
|
|
9351
9571
|
if (issues.length === 0) {
|
|
9352
|
-
console.log(
|
|
9572
|
+
console.log(chalk7.green("\u2713 No dependency issues found"));
|
|
9353
9573
|
}
|
|
9354
9574
|
console.log();
|
|
9355
9575
|
if (criticals.length > 0 || strictMode && warnings.length > 0) {
|
|
@@ -9362,7 +9582,7 @@ program.command("feedback").description("View pattern performance feedback (Laye
|
|
|
9362
9582
|
const shouldReset = Boolean(options["reset"]);
|
|
9363
9583
|
if (shouldReset) {
|
|
9364
9584
|
await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
|
|
9365
|
-
console.log(
|
|
9585
|
+
console.log(chalk7.green("Feedback data reset."));
|
|
9366
9586
|
return;
|
|
9367
9587
|
}
|
|
9368
9588
|
const state = await loadFeedback2();
|
|
@@ -9374,20 +9594,20 @@ program.command("feedback").description("View pattern performance feedback (Laye
|
|
|
9374
9594
|
console.log(generateReport2(state));
|
|
9375
9595
|
return;
|
|
9376
9596
|
}
|
|
9377
|
-
console.log(
|
|
9597
|
+
console.log(chalk7.bold("\nPinata Feedback Report\n"));
|
|
9378
9598
|
console.log(`Total scans: ${state.totalScans}`);
|
|
9379
9599
|
console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
|
|
9380
9600
|
if (state.totalScans === 0) {
|
|
9381
|
-
console.log(
|
|
9601
|
+
console.log(chalk7.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
|
|
9382
9602
|
return;
|
|
9383
9603
|
}
|
|
9384
9604
|
const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
|
|
9385
9605
|
if (patterns.length > 0) {
|
|
9386
|
-
console.log(
|
|
9606
|
+
console.log(chalk7.bold("\nPattern Performance:"));
|
|
9387
9607
|
for (const p of patterns.slice(0, 15)) {
|
|
9388
9608
|
const total = p.confirmedCount + p.unconfirmedCount;
|
|
9389
9609
|
const precisionPct = (p.precision * 100).toFixed(0);
|
|
9390
|
-
const color = p.precision >= 0.7 ?
|
|
9610
|
+
const color = p.precision >= 0.7 ? chalk7.green : p.precision >= 0.4 ? chalk7.yellow : chalk7.red;
|
|
9391
9611
|
console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
|
|
9392
9612
|
}
|
|
9393
9613
|
}
|
|
@@ -9409,76 +9629,74 @@ Examples:
|
|
|
9409
9629
|
case "anthropic-api-key": {
|
|
9410
9630
|
const validation = validateApiKey2("anthropic", value);
|
|
9411
9631
|
if (!validation.valid) {
|
|
9412
|
-
console.log(
|
|
9632
|
+
console.log(chalk7.red(`Invalid API key: ${validation.error}`));
|
|
9413
9633
|
process.exit(1);
|
|
9414
9634
|
}
|
|
9415
9635
|
setConfigValue2("anthropicApiKey", value);
|
|
9416
|
-
console.log(
|
|
9636
|
+
console.log(chalk7.green(`Anthropic API key set: ${maskApiKey2(value)}`));
|
|
9417
9637
|
break;
|
|
9418
9638
|
}
|
|
9419
9639
|
case "openai-api-key": {
|
|
9420
9640
|
const validation = validateApiKey2("openai", value);
|
|
9421
9641
|
if (!validation.valid) {
|
|
9422
|
-
console.log(
|
|
9642
|
+
console.log(chalk7.red(`Invalid API key: ${validation.error}`));
|
|
9423
9643
|
process.exit(1);
|
|
9424
9644
|
}
|
|
9425
9645
|
setConfigValue2("openaiApiKey", value);
|
|
9426
|
-
console.log(
|
|
9646
|
+
console.log(chalk7.green(`OpenAI API key set: ${maskApiKey2(value)}`));
|
|
9427
9647
|
break;
|
|
9428
9648
|
}
|
|
9429
9649
|
case "default-provider": {
|
|
9430
9650
|
if (value !== "anthropic" && value !== "openai") {
|
|
9431
|
-
console.log(
|
|
9651
|
+
console.log(chalk7.red("Provider must be 'anthropic' or 'openai'"));
|
|
9432
9652
|
process.exit(1);
|
|
9433
9653
|
}
|
|
9434
9654
|
setConfigValue2("defaultProvider", value);
|
|
9435
|
-
console.log(
|
|
9655
|
+
console.log(chalk7.green(`Default provider set to: ${value}`));
|
|
9436
9656
|
break;
|
|
9437
9657
|
}
|
|
9438
9658
|
default:
|
|
9439
|
-
console.log(
|
|
9440
|
-
console.log(
|
|
9659
|
+
console.log(chalk7.red(`Unknown config key: ${key}`));
|
|
9660
|
+
console.log(chalk7.gray("Run 'pinata config set --help' for available keys"));
|
|
9441
9661
|
process.exit(1);
|
|
9442
9662
|
}
|
|
9443
|
-
console.log(
|
|
9663
|
+
console.log(chalk7.gray(`Config stored at: ${getConfigPath2()}`));
|
|
9444
9664
|
});
|
|
9445
9665
|
config.command("get <key>").description("Get a configuration value").action(async (key) => {
|
|
9446
9666
|
const { loadConfig: loadConfig2, maskApiKey: maskApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9447
9667
|
const cfg = loadConfig2();
|
|
9448
9668
|
switch (key) {
|
|
9449
9669
|
case "anthropic-api-key":
|
|
9450
|
-
console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) :
|
|
9670
|
+
console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk7.gray("(not set)"));
|
|
9451
9671
|
break;
|
|
9452
9672
|
case "openai-api-key":
|
|
9453
|
-
console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) :
|
|
9673
|
+
console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk7.gray("(not set)"));
|
|
9454
9674
|
break;
|
|
9455
9675
|
case "default-provider":
|
|
9456
|
-
console.log(cfg.defaultProvider ??
|
|
9676
|
+
console.log(cfg.defaultProvider ?? chalk7.gray("anthropic (default)"));
|
|
9457
9677
|
break;
|
|
9458
9678
|
default:
|
|
9459
|
-
console.log(
|
|
9679
|
+
console.log(chalk7.red(`Unknown config key: ${key}`));
|
|
9460
9680
|
process.exit(1);
|
|
9461
9681
|
}
|
|
9462
9682
|
});
|
|
9463
9683
|
config.command("list").description("List all configuration values").action(async () => {
|
|
9464
9684
|
const { loadConfig: loadConfig2, maskApiKey: maskApiKey2, getConfigPath: getConfigPath2, hasApiKey: hasApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9465
9685
|
const cfg = loadConfig2();
|
|
9466
|
-
console.log(
|
|
9467
|
-
console.log(
|
|
9686
|
+
console.log(chalk7.bold("Pinata Configuration"));
|
|
9687
|
+
console.log(chalk7.gray(`Config file: ${getConfigPath2()}`));
|
|
9468
9688
|
console.log();
|
|
9469
9689
|
console.log("AI Providers:");
|
|
9470
|
-
const anthropicStatus = hasApiKey2("anthropic") ?
|
|
9471
|
-
const openaiStatus = hasApiKey2("openai") ?
|
|
9472
|
-
console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ?
|
|
9473
|
-
console.log(` OpenAI API key: ${openaiStatus} ${cfg.openaiApiKey ?
|
|
9690
|
+
const anthropicStatus = hasApiKey2("anthropic") ? chalk7.green("configured") : chalk7.gray("not set");
|
|
9691
|
+
const openaiStatus = hasApiKey2("openai") ? chalk7.green("configured") : chalk7.gray("not set");
|
|
9692
|
+
console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ? chalk7.gray(`(${maskApiKey2(cfg.anthropicApiKey)})`) : ""}`);
|
|
9693
|
+
console.log(` OpenAI API key: ${openaiStatus} ${cfg.openaiApiKey ? chalk7.gray(`(${maskApiKey2(cfg.openaiApiKey)})`) : ""}`);
|
|
9474
9694
|
console.log(` Default provider: ${cfg.defaultProvider ?? "anthropic"}`);
|
|
9475
9695
|
console.log();
|
|
9476
9696
|
if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
|
|
9477
|
-
console.log(
|
|
9478
|
-
console.log(
|
|
9479
|
-
console.log(
|
|
9480
|
-
console.log(chalk5.gray(" # or"));
|
|
9481
|
-
console.log(chalk5.gray(" export ANTHROPIC_API_KEY=sk-ant-xxx"));
|
|
9697
|
+
console.log(chalk7.yellow("No AI provider configured."));
|
|
9698
|
+
console.log(chalk7.gray(" pinata config set anthropic-api-key sk-ant-xxx"));
|
|
9699
|
+
console.log(chalk7.gray(" export ANTHROPIC_API_KEY=sk-ant-xxx"));
|
|
9482
9700
|
}
|
|
9483
9701
|
});
|
|
9484
9702
|
config.command("unset <key>").description("Remove a configuration value").action(async (key) => {
|
|
@@ -9486,18 +9704,18 @@ config.command("unset <key>").description("Remove a configuration value").action
|
|
|
9486
9704
|
switch (key) {
|
|
9487
9705
|
case "anthropic-api-key":
|
|
9488
9706
|
deleteConfigValue2("anthropicApiKey");
|
|
9489
|
-
console.log(
|
|
9707
|
+
console.log(chalk7.green("Anthropic API key removed"));
|
|
9490
9708
|
break;
|
|
9491
9709
|
case "openai-api-key":
|
|
9492
9710
|
deleteConfigValue2("openaiApiKey");
|
|
9493
|
-
console.log(
|
|
9711
|
+
console.log(chalk7.green("OpenAI API key removed"));
|
|
9494
9712
|
break;
|
|
9495
9713
|
case "default-provider":
|
|
9496
9714
|
deleteConfigValue2("defaultProvider");
|
|
9497
|
-
console.log(
|
|
9715
|
+
console.log(chalk7.green("Default provider reset to: anthropic"));
|
|
9498
9716
|
break;
|
|
9499
9717
|
default:
|
|
9500
|
-
console.log(
|
|
9718
|
+
console.log(chalk7.red(`Unknown config key: ${key}`));
|
|
9501
9719
|
process.exit(1);
|
|
9502
9720
|
}
|
|
9503
9721
|
});
|
|
@@ -9505,18 +9723,13 @@ var auth = program.command("auth").description("Manage API key authentication");
|
|
|
9505
9723
|
auth.command("login").description("Set API key for Pinata Cloud").option("-k, --key <key>", "API key (or set PINATA_API_KEY env var)").action(async (options) => {
|
|
9506
9724
|
const apiKey = options["key"] ?? process.env["PINATA_API_KEY"];
|
|
9507
9725
|
if (!apiKey) {
|
|
9508
|
-
console.log(
|
|
9509
|
-
console.log();
|
|
9510
|
-
console.log("
|
|
9511
|
-
console.log(chalk5.gray(" pinata auth login --key <your-api-key>"));
|
|
9512
|
-
console.log(chalk5.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
|
|
9513
|
-
console.log();
|
|
9514
|
-
console.log("Get your API key at: https://app.pinata.dev/settings/api");
|
|
9726
|
+
console.log(chalk7.yellow("No API key provided."));
|
|
9727
|
+
console.log(chalk7.gray(" pinata auth login --key <your-api-key>"));
|
|
9728
|
+
console.log(chalk7.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
|
|
9515
9729
|
process.exit(1);
|
|
9516
9730
|
}
|
|
9517
9731
|
if (apiKey.length < 20 || !apiKey.startsWith("pk_")) {
|
|
9518
|
-
console.log(
|
|
9519
|
-
console.log(chalk5.gray("Keys should start with 'pk_' and be at least 20 characters."));
|
|
9732
|
+
console.log(chalk7.red("Invalid API key format. Keys should start with 'pk_'."));
|
|
9520
9733
|
process.exit(1);
|
|
9521
9734
|
}
|
|
9522
9735
|
const configDir = resolve(process.cwd(), ".pinata");
|
|
@@ -9525,19 +9738,13 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
|
|
|
9525
9738
|
try {
|
|
9526
9739
|
await mkdir4(configDir, { recursive: true });
|
|
9527
9740
|
const maskedKey = `****${apiKey.slice(-8)}`;
|
|
9528
|
-
|
|
9529
|
-
configured: true,
|
|
9530
|
-
keyId: maskedKey,
|
|
9531
|
-
configuredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9532
|
-
};
|
|
9533
|
-
await writeFileAsync(authPath, JSON.stringify(authData, null, 2), "utf8");
|
|
9741
|
+
await writeFileAsync(authPath, JSON.stringify({ configured: true, keyId: maskedKey, configuredAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), "utf8");
|
|
9534
9742
|
const envPath = resolve(configDir, ".env");
|
|
9535
9743
|
await writeFileAsync(envPath, `PINATA_API_KEY=${apiKey}
|
|
9536
9744
|
`, { mode: 384 });
|
|
9537
|
-
console.log(
|
|
9538
|
-
console.log(
|
|
9539
|
-
console.log();
|
|
9540
|
-
console.log(chalk5.yellow("Important: Add .pinata/.env to your .gitignore"));
|
|
9745
|
+
console.log(chalk7.green("API key configured successfully!"));
|
|
9746
|
+
console.log(chalk7.gray(`Key ID: ${maskedKey}`));
|
|
9747
|
+
console.log(chalk7.yellow("Important: Add .pinata/.env to your .gitignore"));
|
|
9541
9748
|
} catch (error) {
|
|
9542
9749
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9543
9750
|
process.exit(1);
|
|
@@ -9558,11 +9765,7 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
9558
9765
|
await rm2(envPath);
|
|
9559
9766
|
removed = true;
|
|
9560
9767
|
}
|
|
9561
|
-
|
|
9562
|
-
console.log(chalk5.green("API key removed successfully."));
|
|
9563
|
-
} else {
|
|
9564
|
-
console.log(chalk5.yellow("No stored API key found."));
|
|
9565
|
-
}
|
|
9768
|
+
console.log(removed ? chalk7.green("API key removed successfully.") : chalk7.yellow("No stored API key found."));
|
|
9566
9769
|
} catch (error) {
|
|
9567
9770
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9568
9771
|
process.exit(1);
|
|
@@ -9571,19 +9774,19 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
9571
9774
|
auth.command("status").description("Check authentication status").action(async () => {
|
|
9572
9775
|
const authPath = resolve(process.cwd(), ".pinata", "auth.json");
|
|
9573
9776
|
if (!existsSync(authPath)) {
|
|
9574
|
-
console.log(
|
|
9575
|
-
console.log(
|
|
9777
|
+
console.log(chalk7.yellow("Not authenticated."));
|
|
9778
|
+
console.log(chalk7.gray("Run: pinata auth login --key <your-api-key>"));
|
|
9576
9779
|
process.exit(0);
|
|
9577
9780
|
}
|
|
9578
9781
|
try {
|
|
9579
|
-
const { readFile:
|
|
9580
|
-
const authData = JSON.parse(await
|
|
9581
|
-
console.log(
|
|
9582
|
-
console.log(
|
|
9583
|
-
console.log(
|
|
9584
|
-
} catch
|
|
9585
|
-
console.log(
|
|
9586
|
-
console.log(
|
|
9782
|
+
const { readFile: readFile7 } = await import('fs/promises');
|
|
9783
|
+
const authData = JSON.parse(await readFile7(authPath, "utf8"));
|
|
9784
|
+
console.log(chalk7.green("Authenticated"));
|
|
9785
|
+
console.log(chalk7.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
|
|
9786
|
+
console.log(chalk7.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
|
|
9787
|
+
} catch {
|
|
9788
|
+
console.log(chalk7.yellow("Authentication status unknown."));
|
|
9789
|
+
console.log(chalk7.gray("Run: pinata auth login to reconfigure."));
|
|
9587
9790
|
}
|
|
9588
9791
|
});
|
|
9589
9792
|
program.parse();
|