pinata-security-cli 0.5.2 → 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/dist/cli/index.js +1200 -1377
- package/dist/cli/index.js.map +1 -1
- package/package.json +6 -4
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
|
/**
|
|
@@ -5705,7 +5690,32 @@ function getProjectTypeDescription(type) {
|
|
|
5705
5690
|
}
|
|
5706
5691
|
init_errors();
|
|
5707
5692
|
init_result();
|
|
5708
|
-
|
|
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
|
|
5709
5719
|
var DEFAULT_OPTIONS = {
|
|
5710
5720
|
excludeDirs: [
|
|
5711
5721
|
// Package managers
|
|
@@ -6335,9 +6345,6 @@ function createScanner(categoryStore) {
|
|
|
6335
6345
|
return new Scanner(categoryStore);
|
|
6336
6346
|
}
|
|
6337
6347
|
|
|
6338
|
-
// src/core/scanner/index.ts
|
|
6339
|
-
init_types();
|
|
6340
|
-
|
|
6341
6348
|
// src/core/index.ts
|
|
6342
6349
|
var VERSION = "0.4.0";
|
|
6343
6350
|
|
|
@@ -7071,34 +7078,34 @@ function createRenderer(options) {
|
|
|
7071
7078
|
return new TemplateRenderer(options);
|
|
7072
7079
|
}
|
|
7073
7080
|
var SEVERITY_COLORS = {
|
|
7074
|
-
critical:
|
|
7075
|
-
high:
|
|
7076
|
-
medium:
|
|
7077
|
-
low:
|
|
7081
|
+
critical: chalk7.red.bold,
|
|
7082
|
+
high: chalk7.red,
|
|
7083
|
+
medium: chalk7.yellow,
|
|
7084
|
+
low: chalk7.gray
|
|
7078
7085
|
};
|
|
7079
7086
|
var PRIORITY_COLORS = {
|
|
7080
|
-
P0:
|
|
7081
|
-
P1:
|
|
7082
|
-
P2:
|
|
7087
|
+
P0: chalk7.red.bold,
|
|
7088
|
+
P1: chalk7.yellow,
|
|
7089
|
+
P2: chalk7.gray
|
|
7083
7090
|
};
|
|
7084
7091
|
var DOMAIN_COLORS = {
|
|
7085
|
-
security:
|
|
7086
|
-
data:
|
|
7087
|
-
concurrency:
|
|
7088
|
-
input:
|
|
7089
|
-
resource:
|
|
7090
|
-
reliability:
|
|
7091
|
-
performance:
|
|
7092
|
-
platform:
|
|
7093
|
-
business:
|
|
7094
|
-
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
|
|
7095
7102
|
};
|
|
7096
7103
|
function formatTerminal(categories) {
|
|
7097
7104
|
if (categories.length === 0) {
|
|
7098
|
-
return
|
|
7105
|
+
return chalk7.yellow("No categories found matching the filters.");
|
|
7099
7106
|
}
|
|
7100
7107
|
const lines = [];
|
|
7101
|
-
lines.push(
|
|
7108
|
+
lines.push(chalk7.bold.underline(`Found ${categories.length} categories:
|
|
7102
7109
|
`));
|
|
7103
7110
|
const byDomain = /* @__PURE__ */ new Map();
|
|
7104
7111
|
for (const cat of categories) {
|
|
@@ -7109,26 +7116,26 @@ function formatTerminal(categories) {
|
|
|
7109
7116
|
byDomain.get(domain).push(cat);
|
|
7110
7117
|
}
|
|
7111
7118
|
for (const [domain, domainCategories] of byDomain) {
|
|
7112
|
-
const domainColor = DOMAIN_COLORS[domain] ??
|
|
7119
|
+
const domainColor = DOMAIN_COLORS[domain] ?? chalk7.white;
|
|
7113
7120
|
lines.push(domainColor.bold(`
|
|
7114
7121
|
${domain.toUpperCase()} (${domainCategories.length})`));
|
|
7115
|
-
lines.push(
|
|
7122
|
+
lines.push(chalk7.gray("\u2500".repeat(40)));
|
|
7116
7123
|
for (const cat of domainCategories) {
|
|
7117
7124
|
const priorityColor = PRIORITY_COLORS[cat.priority];
|
|
7118
7125
|
const severityColor = SEVERITY_COLORS[cat.severity];
|
|
7119
7126
|
const priority = priorityColor(`[${cat.priority}]`);
|
|
7120
7127
|
const severity = severityColor(`${cat.severity}`);
|
|
7121
|
-
const level =
|
|
7122
|
-
const name =
|
|
7123
|
-
const id =
|
|
7128
|
+
const level = chalk7.cyan(`${cat.level}`);
|
|
7129
|
+
const name = chalk7.white.bold(cat.name);
|
|
7130
|
+
const id = chalk7.gray(`(${cat.id})`);
|
|
7124
7131
|
lines.push(` ${priority} ${name} ${id}`);
|
|
7125
7132
|
lines.push(` ${severity} | ${level}`);
|
|
7126
7133
|
const desc = cat.description.length > 80 ? cat.description.slice(0, 77) + "..." : cat.description;
|
|
7127
|
-
lines.push(
|
|
7134
|
+
lines.push(chalk7.gray(` ${desc}`));
|
|
7128
7135
|
lines.push("");
|
|
7129
7136
|
}
|
|
7130
7137
|
}
|
|
7131
|
-
lines.push(
|
|
7138
|
+
lines.push(chalk7.gray("\u2500".repeat(40)));
|
|
7132
7139
|
lines.push(formatStats(categories));
|
|
7133
7140
|
return lines.join("\n");
|
|
7134
7141
|
}
|
|
@@ -7150,7 +7157,7 @@ function formatStats(categories) {
|
|
|
7150
7157
|
if (stats.P0 > 0) parts.push(PRIORITY_COLORS.P0(`${stats.P0} P0`));
|
|
7151
7158
|
if (stats.P1 > 0) parts.push(PRIORITY_COLORS.P1(`${stats.P1} P1`));
|
|
7152
7159
|
if (stats.P2 > 0) parts.push(PRIORITY_COLORS.P2(`${stats.P2} P2`));
|
|
7153
|
-
parts.push(
|
|
7160
|
+
parts.push(chalk7.gray("|"));
|
|
7154
7161
|
if (stats.critical > 0) parts.push(SEVERITY_COLORS.critical(`${stats.critical} critical`));
|
|
7155
7162
|
if (stats.high > 0) parts.push(SEVERITY_COLORS.high(`${stats.high} high`));
|
|
7156
7163
|
if (stats.medium > 0) parts.push(SEVERITY_COLORS.medium(`${stats.medium} medium`));
|
|
@@ -7207,13 +7214,9 @@ function isValidOutputFormat(format) {
|
|
|
7207
7214
|
return ["terminal", "json", "markdown"].includes(format);
|
|
7208
7215
|
}
|
|
7209
7216
|
function formatError(error) {
|
|
7210
|
-
return
|
|
7217
|
+
return chalk7.red(`Error: ${error.message}`);
|
|
7211
7218
|
}
|
|
7212
7219
|
|
|
7213
|
-
// src/cli/generate-formatters.ts
|
|
7214
|
-
init_errors();
|
|
7215
|
-
init_result();
|
|
7216
|
-
|
|
7217
7220
|
// src/ai/service.ts
|
|
7218
7221
|
var DEFAULT_CONFIG = {
|
|
7219
7222
|
provider: "anthropic",
|
|
@@ -7260,11 +7263,11 @@ var AIService = class {
|
|
|
7260
7263
|
*/
|
|
7261
7264
|
getApiKeyFromConfig(provider) {
|
|
7262
7265
|
try {
|
|
7263
|
-
const { existsSync:
|
|
7266
|
+
const { existsSync: existsSync6, readFileSync: readFileSync3 } = __require("fs");
|
|
7264
7267
|
const { homedir: homedir3 } = __require("os");
|
|
7265
7268
|
const { join: join5 } = __require("path");
|
|
7266
7269
|
const configPath = join5(homedir3(), ".pinata", "config.json");
|
|
7267
|
-
if (!
|
|
7270
|
+
if (!existsSync6(configPath)) {
|
|
7268
7271
|
return "";
|
|
7269
7272
|
}
|
|
7270
7273
|
const content = readFileSync3(configPath, "utf-8");
|
|
@@ -7515,8 +7518,119 @@ function createAIService(config2) {
|
|
|
7515
7518
|
return new AIService(config2);
|
|
7516
7519
|
}
|
|
7517
7520
|
|
|
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
|
|
7528
|
+
|
|
7529
|
+
Always respond with valid JSON matching this structure:
|
|
7530
|
+
{
|
|
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) {
|
|
7539
|
+
const ai = createAIService(config2);
|
|
7540
|
+
if (!ai.isConfigured()) {
|
|
7541
|
+
return {
|
|
7542
|
+
success: false,
|
|
7543
|
+
error: "AI service not configured",
|
|
7544
|
+
durationMs: 0
|
|
7545
|
+
};
|
|
7546
|
+
}
|
|
7547
|
+
const prompt = buildExplainPrompt(gap);
|
|
7548
|
+
const response = await ai.completeJSON({
|
|
7549
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
7550
|
+
messages: [{ role: "user", content: prompt }],
|
|
7551
|
+
maxTokens: 1024,
|
|
7552
|
+
temperature: 0.3
|
|
7553
|
+
});
|
|
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);
|
|
7569
|
+
}
|
|
7570
|
+
}
|
|
7571
|
+
return results;
|
|
7572
|
+
}
|
|
7573
|
+
function buildExplainPrompt(gap, category) {
|
|
7574
|
+
const parts = [];
|
|
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
|
+
|
|
7518
7632
|
// src/ai/template-filler.ts
|
|
7519
|
-
var
|
|
7633
|
+
var SYSTEM_PROMPT2 = `You are an expert at analyzing code and extracting meaningful variable values for test generation.
|
|
7520
7634
|
Given a code snippet and a list of template variables, suggest appropriate values for each variable.
|
|
7521
7635
|
|
|
7522
7636
|
For each variable, analyze:
|
|
@@ -7551,7 +7665,7 @@ async function suggestVariables(request, config2) {
|
|
|
7551
7665
|
const prompt = buildVariablePrompt(request);
|
|
7552
7666
|
const startTime = Date.now();
|
|
7553
7667
|
const response = await ai.completeJSON({
|
|
7554
|
-
systemPrompt:
|
|
7668
|
+
systemPrompt: SYSTEM_PROMPT2,
|
|
7555
7669
|
messages: [{ role: "user", content: prompt }],
|
|
7556
7670
|
maxTokens: 1024,
|
|
7557
7671
|
temperature: 0.2
|
|
@@ -7742,438 +7856,72 @@ function toPascalCase(str) {
|
|
|
7742
7856
|
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
7743
7857
|
}
|
|
7744
7858
|
|
|
7745
|
-
// src/
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
if (test.result.imports.length > 0) {
|
|
7767
|
-
for (const imp of test.result.imports) {
|
|
7768
|
-
lines.push(chalk5.gray(imp));
|
|
7769
|
-
}
|
|
7770
|
-
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"
|
|
7771
7880
|
}
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
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
|
+
};
|
|
7775
7896
|
}
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
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
|
+
};
|
|
7785
7910
|
}
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
const
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7798
|
-
|
|
7799
|
-
|
|
7800
|
-
template: {
|
|
7801
|
-
id: test.template.id,
|
|
7802
|
-
framework: test.template.framework,
|
|
7803
|
-
language: test.template.language
|
|
7804
|
-
},
|
|
7805
|
-
suggestedPath: test.suggestedPath,
|
|
7806
|
-
content: test.result.content,
|
|
7807
|
-
imports: test.result.imports,
|
|
7808
|
-
fixtures: test.result.fixtures,
|
|
7809
|
-
substituted: test.result.substituted,
|
|
7810
|
-
unresolved: test.result.unresolved
|
|
7811
|
-
}));
|
|
7812
|
-
return JSON.stringify(output, null, 2);
|
|
7813
|
-
}
|
|
7814
|
-
function suggestTestPath(sourceFile, template, basePath) {
|
|
7815
|
-
const dir = dirname(sourceFile);
|
|
7816
|
-
const name = basename(sourceFile);
|
|
7817
|
-
const nameWithoutExt = name.replace(/\.[^.]+$/, "");
|
|
7818
|
-
const extMap = {
|
|
7819
|
-
python: ".py",
|
|
7820
|
-
typescript: ".ts",
|
|
7821
|
-
javascript: ".js",
|
|
7822
|
-
go: "_test.go",
|
|
7823
|
-
java: "Test.java",
|
|
7824
|
-
rust: ".rs"
|
|
7825
|
-
};
|
|
7826
|
-
const ext = extMap[template.language] ?? ".test.ts";
|
|
7827
|
-
let testFileName;
|
|
7828
|
-
switch (template.language) {
|
|
7829
|
-
case "python":
|
|
7830
|
-
testFileName = `test_${nameWithoutExt}${ext}`;
|
|
7831
|
-
break;
|
|
7832
|
-
case "go":
|
|
7833
|
-
testFileName = `${nameWithoutExt}${ext}`;
|
|
7834
|
-
break;
|
|
7835
|
-
case "java":
|
|
7836
|
-
testFileName = `${nameWithoutExt}${ext}`;
|
|
7837
|
-
break;
|
|
7838
|
-
default:
|
|
7839
|
-
testFileName = `${nameWithoutExt}.test${ext}`;
|
|
7840
|
-
}
|
|
7841
|
-
const relativeSrc = relative(basePath, dir);
|
|
7842
|
-
const testDir = relativeSrc.replace(/^src/, "tests");
|
|
7843
|
-
return `${testDir}/${testFileName}`;
|
|
7844
|
-
}
|
|
7845
|
-
function extractVariablesFromGap(gap) {
|
|
7846
|
-
const fileName = basename(gap.filePath);
|
|
7847
|
-
const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
|
|
7848
|
-
const funcMatch = gap.codeSnippet.match(/(?:def|function|async function|const|let|var)\s+(\w+)/);
|
|
7849
|
-
const classMatch = gap.codeSnippet.match(/(?:class)\s+(\w+)/);
|
|
7850
|
-
return {
|
|
7851
|
-
// File info
|
|
7852
|
-
filePath: gap.filePath,
|
|
7853
|
-
fileName,
|
|
7854
|
-
fileNameWithoutExt,
|
|
7855
|
-
lineNumber: gap.lineStart,
|
|
7856
|
-
// Category info
|
|
7857
|
-
categoryId: gap.categoryId,
|
|
7858
|
-
categoryName: gap.categoryName,
|
|
7859
|
-
domain: gap.domain,
|
|
7860
|
-
level: gap.level,
|
|
7861
|
-
severity: gap.severity,
|
|
7862
|
-
confidence: gap.confidence,
|
|
7863
|
-
// Code context
|
|
7864
|
-
codeSnippet: gap.codeSnippet,
|
|
7865
|
-
functionName: funcMatch?.[1] ?? "targetFunction",
|
|
7866
|
-
className: classMatch?.[1] ?? "TargetClass",
|
|
7867
|
-
// Common template variables
|
|
7868
|
-
testName: `test_${gap.categoryId.replace(/-/g, "_")}`,
|
|
7869
|
-
testDescription: `Test for ${gap.categoryName} in ${fileName}:${gap.lineStart}`,
|
|
7870
|
-
// Pattern info
|
|
7871
|
-
patternId: gap.patternId,
|
|
7872
|
-
patternType: gap.patternType
|
|
7873
|
-
};
|
|
7874
|
-
}
|
|
7875
|
-
async function extractVariablesWithAI(gap, templateVariables, aiConfig) {
|
|
7876
|
-
const baseVars = extractVariablesFromGap(gap);
|
|
7877
|
-
const result = await suggestVariables(
|
|
7878
|
-
{
|
|
7879
|
-
codeSnippet: gap.codeSnippet,
|
|
7880
|
-
filePath: gap.filePath,
|
|
7881
|
-
variables: templateVariables,
|
|
7882
|
-
gap,
|
|
7883
|
-
existingValues: baseVars
|
|
7884
|
-
},
|
|
7885
|
-
aiConfig
|
|
7886
|
-
);
|
|
7887
|
-
if (result.success && result.data) {
|
|
7888
|
-
return result.data.values;
|
|
7889
|
-
}
|
|
7890
|
-
return baseVars;
|
|
7891
|
-
}
|
|
7892
|
-
async function writeGeneratedTests(tests, basePath, outputDir) {
|
|
7893
|
-
const summary = {
|
|
7894
|
-
created: [],
|
|
7895
|
-
updated: [],
|
|
7896
|
-
failed: [],
|
|
7897
|
-
totalTests: 0
|
|
7898
|
-
};
|
|
7899
|
-
const testsByFile = /* @__PURE__ */ new Map();
|
|
7900
|
-
for (const test of tests) {
|
|
7901
|
-
let outputPath;
|
|
7902
|
-
if (outputDir) {
|
|
7903
|
-
outputPath = resolve(basePath, outputDir, test.suggestedPath);
|
|
7904
|
-
} else {
|
|
7905
|
-
outputPath = resolve(basePath, test.suggestedPath);
|
|
7906
|
-
}
|
|
7907
|
-
const existing = testsByFile.get(outputPath) ?? [];
|
|
7908
|
-
existing.push(test);
|
|
7909
|
-
testsByFile.set(outputPath, existing);
|
|
7910
|
-
}
|
|
7911
|
-
for (const [outputPath, fileTests] of testsByFile) {
|
|
7912
|
-
try {
|
|
7913
|
-
const dir = dirname(outputPath);
|
|
7914
|
-
await mkdir(dir, { recursive: true });
|
|
7915
|
-
let existingContent = "";
|
|
7916
|
-
let fileExists = false;
|
|
7917
|
-
try {
|
|
7918
|
-
existingContent = await readFile(outputPath, "utf-8");
|
|
7919
|
-
fileExists = true;
|
|
7920
|
-
} catch {
|
|
7921
|
-
}
|
|
7922
|
-
const contentParts = [];
|
|
7923
|
-
const allImports = /* @__PURE__ */ new Set();
|
|
7924
|
-
for (const test of fileTests) {
|
|
7925
|
-
for (const imp of test.result.imports) {
|
|
7926
|
-
allImports.add(imp);
|
|
7927
|
-
}
|
|
7928
|
-
}
|
|
7929
|
-
if (!fileExists && allImports.size > 0) {
|
|
7930
|
-
contentParts.push(Array.from(allImports).join("\n"));
|
|
7931
|
-
contentParts.push("");
|
|
7932
|
-
}
|
|
7933
|
-
for (const test of fileTests) {
|
|
7934
|
-
contentParts.push(`// Test for ${test.gap.categoryName}`);
|
|
7935
|
-
contentParts.push(`// Gap: ${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`);
|
|
7936
|
-
contentParts.push(`// Generated by Pinata`);
|
|
7937
|
-
contentParts.push("");
|
|
7938
|
-
contentParts.push(test.result.content);
|
|
7939
|
-
contentParts.push("");
|
|
7940
|
-
}
|
|
7941
|
-
const newContent = contentParts.join("\n");
|
|
7942
|
-
let finalContent;
|
|
7943
|
-
let appended = false;
|
|
7944
|
-
if (fileExists) {
|
|
7945
|
-
finalContent = existingContent.trimEnd() + "\n\n" + newContent;
|
|
7946
|
-
appended = true;
|
|
7947
|
-
} else {
|
|
7948
|
-
finalContent = newContent;
|
|
7949
|
-
}
|
|
7950
|
-
await writeFile(outputPath, finalContent, "utf-8");
|
|
7951
|
-
for (const test of fileTests) {
|
|
7952
|
-
const result = {
|
|
7953
|
-
path: outputPath,
|
|
7954
|
-
created: !fileExists,
|
|
7955
|
-
appended,
|
|
7956
|
-
categoryId: test.gap.categoryId,
|
|
7957
|
-
gapLocation: `${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`
|
|
7958
|
-
};
|
|
7959
|
-
if (fileExists) {
|
|
7960
|
-
summary.updated.push(result);
|
|
7961
|
-
} else {
|
|
7962
|
-
summary.created.push(result);
|
|
7963
|
-
}
|
|
7964
|
-
summary.totalTests++;
|
|
7965
|
-
}
|
|
7966
|
-
} catch (error) {
|
|
7967
|
-
summary.failed.push({
|
|
7968
|
-
path: outputPath,
|
|
7969
|
-
error: error instanceof Error ? error.message : String(error)
|
|
7970
|
-
});
|
|
7971
|
-
}
|
|
7972
|
-
}
|
|
7973
|
-
return ok(summary);
|
|
7974
|
-
}
|
|
7975
|
-
function formatWriteSummary(summary, basePath) {
|
|
7976
|
-
const lines = [];
|
|
7977
|
-
lines.push("");
|
|
7978
|
-
lines.push(chalk5.bold.cyan("Write Summary:"));
|
|
7979
|
-
lines.push(chalk5.gray("\u2500".repeat(60)));
|
|
7980
|
-
if (summary.created.length > 0) {
|
|
7981
|
-
const uniquePaths = new Set(summary.created.map((r) => r.path));
|
|
7982
|
-
lines.push("");
|
|
7983
|
-
lines.push(chalk5.green.bold(`Created ${uniquePaths.size} file(s):`));
|
|
7984
|
-
for (const path2 of uniquePaths) {
|
|
7985
|
-
const relPath = relative(basePath, path2);
|
|
7986
|
-
const testsInFile = summary.created.filter((r) => r.path === path2).length;
|
|
7987
|
-
lines.push(chalk5.green(` + ${relPath} (${testsInFile} test(s))`));
|
|
7988
|
-
}
|
|
7989
|
-
}
|
|
7990
|
-
if (summary.updated.length > 0) {
|
|
7991
|
-
const uniquePaths = new Set(summary.updated.map((r) => r.path));
|
|
7992
|
-
lines.push("");
|
|
7993
|
-
lines.push(chalk5.yellow.bold(`Updated ${uniquePaths.size} file(s):`));
|
|
7994
|
-
for (const path2 of uniquePaths) {
|
|
7995
|
-
const relPath = relative(basePath, path2);
|
|
7996
|
-
const testsInFile = summary.updated.filter((r) => r.path === path2).length;
|
|
7997
|
-
lines.push(chalk5.yellow(` ~ ${relPath} (${testsInFile} test(s) appended)`));
|
|
7998
|
-
}
|
|
7999
|
-
}
|
|
8000
|
-
if (summary.failed.length > 0) {
|
|
8001
|
-
lines.push("");
|
|
8002
|
-
lines.push(chalk5.red.bold(`Failed to write ${summary.failed.length} file(s):`));
|
|
8003
|
-
for (const fail of summary.failed) {
|
|
8004
|
-
const relPath = relative(basePath, fail.path);
|
|
8005
|
-
lines.push(chalk5.red(` \u2717 ${relPath}: ${fail.error}`));
|
|
8006
|
-
}
|
|
8007
|
-
}
|
|
8008
|
-
lines.push("");
|
|
8009
|
-
lines.push(chalk5.gray("\u2500".repeat(60)));
|
|
8010
|
-
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)`));
|
|
8011
|
-
if (summary.failed.length > 0) {
|
|
8012
|
-
lines.push(chalk5.red(`Failures: ${summary.failed.length}`));
|
|
8013
|
-
}
|
|
8014
|
-
return lines.join("\n");
|
|
8015
|
-
}
|
|
8016
|
-
|
|
8017
|
-
// src/ai/explainer.ts
|
|
8018
|
-
var SYSTEM_PROMPT2 = `You are a security expert explaining code vulnerabilities to developers.
|
|
8019
|
-
Your explanations should be:
|
|
8020
|
-
- Clear and actionable
|
|
8021
|
-
- Focused on the specific code pattern
|
|
8022
|
-
- Include concrete remediation steps
|
|
8023
|
-
- Reference relevant security standards (OWASP, CWE) when applicable
|
|
8024
|
-
|
|
8025
|
-
Always respond with valid JSON matching this structure:
|
|
8026
|
-
{
|
|
8027
|
-
"summary": "1-2 sentence summary",
|
|
8028
|
-
"explanation": "Detailed explanation of the vulnerability",
|
|
8029
|
-
"risk": "What an attacker could do if this is exploited",
|
|
8030
|
-
"remediation": "Step-by-step instructions to fix",
|
|
8031
|
-
"safeExample": "Code example showing the safe pattern",
|
|
8032
|
-
"references": ["optional array of CVE/CWE/OWASP references"]
|
|
8033
|
-
}`;
|
|
8034
|
-
async function explainGap(gap, category, config2) {
|
|
8035
|
-
const ai = createAIService(config2);
|
|
8036
|
-
if (!ai.isConfigured()) {
|
|
8037
|
-
return {
|
|
8038
|
-
success: false,
|
|
8039
|
-
error: "AI service not configured",
|
|
8040
|
-
durationMs: 0
|
|
8041
|
-
};
|
|
8042
|
-
}
|
|
8043
|
-
const prompt = buildExplainPrompt(gap);
|
|
8044
|
-
const response = await ai.completeJSON({
|
|
8045
|
-
systemPrompt: SYSTEM_PROMPT2,
|
|
8046
|
-
messages: [{ role: "user", content: prompt }],
|
|
8047
|
-
maxTokens: 1024,
|
|
8048
|
-
temperature: 0.3
|
|
8049
|
-
});
|
|
8050
|
-
return response;
|
|
8051
|
-
}
|
|
8052
|
-
function buildExplainPrompt(gap, category) {
|
|
8053
|
-
const parts = [];
|
|
8054
|
-
parts.push(`Explain this security finding:
|
|
8055
|
-
`);
|
|
8056
|
-
parts.push(`**Category:** ${gap.categoryName} (${gap.categoryId})`);
|
|
8057
|
-
parts.push(`**Severity:** ${gap.severity}`);
|
|
8058
|
-
parts.push(`**Confidence:** ${gap.confidence}`);
|
|
8059
|
-
parts.push(`**File:** ${gap.filePath}`);
|
|
8060
|
-
parts.push(`**Line:** ${gap.lineStart}`);
|
|
8061
|
-
if (gap.codeSnippet) {
|
|
8062
|
-
parts.push(`
|
|
8063
|
-
**Code:**
|
|
8064
|
-
\`\`\`
|
|
8065
|
-
${gap.codeSnippet}
|
|
8066
|
-
\`\`\``);
|
|
8067
|
-
}
|
|
8068
|
-
parts.push(`
|
|
8069
|
-
**Pattern:** ${gap.patternId}`);
|
|
8070
|
-
parts.push(`**Detection Type:** ${gap.patternType}`);
|
|
8071
|
-
parts.push(`
|
|
8072
|
-
Provide a clear, actionable explanation for a developer.`);
|
|
8073
|
-
return parts.join("\n");
|
|
8074
|
-
}
|
|
8075
|
-
function generateFallbackExplanation(gap) {
|
|
8076
|
-
const summaries = {
|
|
8077
|
-
"sql-injection": "SQL query constructed with user input may allow injection attacks.",
|
|
8078
|
-
"xss": "User input rendered without escaping may allow script injection.",
|
|
8079
|
-
"command-injection": "Shell command constructed with user input may allow command execution.",
|
|
8080
|
-
"path-traversal": "File path constructed with user input may allow directory traversal.",
|
|
8081
|
-
"hardcoded-secrets": "Sensitive credentials found in source code.",
|
|
8082
|
-
"deserialization": "Untrusted data deserialization may allow code execution.",
|
|
8083
|
-
"ssrf": "Server-side request with user-controlled URL may allow internal access.",
|
|
8084
|
-
"xxe": "XML parser may be vulnerable to external entity injection.",
|
|
8085
|
-
"csrf": "State-changing request lacks CSRF protection.",
|
|
8086
|
-
"ldap-injection": "LDAP query constructed with user input may allow injection."
|
|
8087
|
-
};
|
|
8088
|
-
const remediations = {
|
|
8089
|
-
"sql-injection": "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
|
|
8090
|
-
"xss": "Escape all user input before rendering in HTML. Use framework auto-escaping features.",
|
|
8091
|
-
"command-injection": "Avoid shell execution with user input. Use allowlists and subprocess arrays instead of shell strings.",
|
|
8092
|
-
"path-traversal": "Validate and sanitize file paths. Use path.resolve() and verify the result is within allowed directories.",
|
|
8093
|
-
"hardcoded-secrets": "Move secrets to environment variables or a secrets manager. Never commit credentials to source control.",
|
|
8094
|
-
"deserialization": "Avoid deserializing untrusted data. If necessary, use safe formats like JSON instead of pickle/yaml.",
|
|
8095
|
-
"ssrf": "Validate and allowlist URLs. Block private IP ranges and localhost.",
|
|
8096
|
-
"xxe": "Disable external entity processing in XML parser configuration.",
|
|
8097
|
-
"csrf": "Implement CSRF tokens for all state-changing requests.",
|
|
8098
|
-
"ldap-injection": "Escape special LDAP characters in user input. Use parameterized LDAP queries."
|
|
8099
|
-
};
|
|
8100
|
-
const summary = summaries[gap.categoryId] ?? `Potential ${gap.categoryName} vulnerability detected.`;
|
|
8101
|
-
const remediation = remediations[gap.categoryId] ?? `Review the code for security issues and apply appropriate fixes.`;
|
|
8102
|
-
return {
|
|
8103
|
-
summary,
|
|
8104
|
-
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.`,
|
|
8105
|
-
risk: `If exploited, this vulnerability could compromise the security of the application. Severity: ${gap.severity}.`,
|
|
8106
|
-
remediation,
|
|
8107
|
-
references: []
|
|
8108
|
-
};
|
|
8109
|
-
}
|
|
8110
|
-
|
|
8111
|
-
// src/ai/pattern-suggester.ts
|
|
8112
|
-
var SYSTEM_PROMPT3 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
|
|
8113
|
-
Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
|
|
8114
|
-
|
|
8115
|
-
Your patterns should:
|
|
8116
|
-
1. Be specific enough to avoid false positives
|
|
8117
|
-
2. Be general enough to catch variations
|
|
8118
|
-
3. Use standard regex syntax (no lookbehind for compatibility)
|
|
8119
|
-
4. Include examples of what matches and what doesn't
|
|
8120
|
-
|
|
8121
|
-
Always respond with valid JSON matching this structure:
|
|
8122
|
-
{
|
|
8123
|
-
"suggestions": [
|
|
8124
|
-
{
|
|
8125
|
-
"id": "pattern-id-kebab-case",
|
|
8126
|
-
"pattern": "regex pattern here",
|
|
8127
|
-
"description": "What this pattern detects",
|
|
8128
|
-
"confidence": "high|medium|low",
|
|
8129
|
-
"matchExample": "code that should match",
|
|
8130
|
-
"safeExample": "similar code that should NOT match",
|
|
8131
|
-
"reasoning": "Why this pattern works"
|
|
8132
|
-
}
|
|
8133
|
-
]
|
|
8134
|
-
}
|
|
8135
|
-
|
|
8136
|
-
Important:
|
|
8137
|
-
- Escape backslashes properly for JSON (use \\\\s not \\s)
|
|
8138
|
-
- Test your patterns mentally against the examples
|
|
8139
|
-
- Prefer simpler patterns that are less likely to cause ReDoS`;
|
|
8140
|
-
async function suggestPatterns(request, config2) {
|
|
8141
|
-
const ai = createAIService(config2);
|
|
8142
|
-
if (!ai.isConfigured()) {
|
|
8143
|
-
return {
|
|
8144
|
-
success: false,
|
|
8145
|
-
error: "AI service not configured. Set ANTHROPIC_API_KEY or OPENAI_API_KEY.",
|
|
8146
|
-
durationMs: 0
|
|
8147
|
-
};
|
|
8148
|
-
}
|
|
8149
|
-
const prompt = buildPatternPrompt(request);
|
|
8150
|
-
const response = await ai.completeJSON({
|
|
8151
|
-
systemPrompt: SYSTEM_PROMPT3,
|
|
8152
|
-
messages: [{ role: "user", content: prompt }],
|
|
8153
|
-
maxTokens: 2048,
|
|
8154
|
-
temperature: 0.3
|
|
8155
|
-
});
|
|
8156
|
-
if (!response.success || !response.data) {
|
|
8157
|
-
return {
|
|
8158
|
-
success: false,
|
|
8159
|
-
error: response.error ?? "Failed to generate patterns",
|
|
8160
|
-
durationMs: response.durationMs
|
|
8161
|
-
};
|
|
8162
|
-
}
|
|
8163
|
-
const validated = validatePatterns(
|
|
8164
|
-
response.data.suggestions ?? [],
|
|
8165
|
-
request.vulnerableCode,
|
|
8166
|
-
request.safeCode ?? []
|
|
8167
|
-
);
|
|
8168
|
-
const result = {
|
|
8169
|
-
success: true,
|
|
8170
|
-
data: validated,
|
|
8171
|
-
durationMs: response.durationMs
|
|
8172
|
-
};
|
|
8173
|
-
if (response.usage) {
|
|
8174
|
-
result.usage = response.usage;
|
|
8175
|
-
}
|
|
8176
|
-
return result;
|
|
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;
|
|
8177
7925
|
}
|
|
8178
7926
|
function buildPatternPrompt(request) {
|
|
8179
7927
|
const parts = [];
|
|
@@ -8277,73 +8025,89 @@ function hasRedosPotential(pattern) {
|
|
|
8277
8025
|
|
|
8278
8026
|
// src/cli/index.ts
|
|
8279
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;
|
|
8039
|
+
}
|
|
8040
|
+
}
|
|
8041
|
+
return candidates[0];
|
|
8042
|
+
}
|
|
8043
|
+
init_results_cache();
|
|
8280
8044
|
var SEVERITY_COLORS2 = {
|
|
8281
|
-
critical:
|
|
8282
|
-
high:
|
|
8283
|
-
medium:
|
|
8284
|
-
low:
|
|
8045
|
+
critical: chalk7.red.bold,
|
|
8046
|
+
high: chalk7.red,
|
|
8047
|
+
medium: chalk7.yellow,
|
|
8048
|
+
low: chalk7.gray
|
|
8285
8049
|
};
|
|
8286
8050
|
var DOMAIN_COLORS2 = {
|
|
8287
|
-
security:
|
|
8288
|
-
data:
|
|
8289
|
-
concurrency:
|
|
8290
|
-
input:
|
|
8291
|
-
resource:
|
|
8292
|
-
reliability:
|
|
8293
|
-
performance:
|
|
8294
|
-
platform:
|
|
8295
|
-
business:
|
|
8296
|
-
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
|
|
8297
8061
|
};
|
|
8298
8062
|
var GRADE_COLORS = {
|
|
8299
|
-
A:
|
|
8300
|
-
B:
|
|
8301
|
-
C:
|
|
8302
|
-
D:
|
|
8303
|
-
F:
|
|
8063
|
+
A: chalk7.green.bold,
|
|
8064
|
+
B: chalk7.green,
|
|
8065
|
+
C: chalk7.yellow,
|
|
8066
|
+
D: chalk7.red,
|
|
8067
|
+
F: chalk7.red.bold
|
|
8304
8068
|
};
|
|
8305
8069
|
var BANNER = `
|
|
8306
|
-
${
|
|
8307
|
-
${
|
|
8308
|
-
${
|
|
8309
|
-
${
|
|
8310
|
-
${
|
|
8070
|
+
${chalk7.cyan(" ____ _ _ ")}
|
|
8071
|
+
${chalk7.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
|
|
8072
|
+
${chalk7.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
|
|
8073
|
+
${chalk7.cyan("| __/| | | | | (_| | || (_| |")}
|
|
8074
|
+
${chalk7.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
|
|
8311
8075
|
`;
|
|
8312
8076
|
function formatScanTerminal(result, basePath) {
|
|
8313
8077
|
const lines = [];
|
|
8314
8078
|
lines.push(BANNER);
|
|
8315
|
-
lines.push(
|
|
8079
|
+
lines.push(chalk7.gray(`Analyzing: ${result.targetDirectory}`));
|
|
8316
8080
|
const projectTypeLabel = getProjectTypeDescription(result.projectType.type);
|
|
8317
|
-
lines.push(
|
|
8318
|
-
lines.push(
|
|
8081
|
+
lines.push(chalk7.gray(`Project: ${projectTypeLabel} (${result.projectType.confidence} confidence)`));
|
|
8082
|
+
lines.push(chalk7.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
|
|
8319
8083
|
lines.push("");
|
|
8320
8084
|
lines.push(formatScoreBox(result.score));
|
|
8321
8085
|
lines.push("");
|
|
8322
|
-
lines.push(
|
|
8086
|
+
lines.push(chalk7.bold("Domain Coverage:"));
|
|
8323
8087
|
lines.push(formatDomainCoverage(result.coverage));
|
|
8324
8088
|
lines.push("");
|
|
8325
8089
|
if (result.gaps.length > 0) {
|
|
8326
8090
|
lines.push(formatGapsSummary(result.gaps, basePath));
|
|
8327
8091
|
lines.push("");
|
|
8328
8092
|
} else {
|
|
8329
|
-
lines.push(
|
|
8093
|
+
lines.push(chalk7.green.bold("No gaps detected! Your codebase has good test coverage."));
|
|
8330
8094
|
lines.push("");
|
|
8331
8095
|
}
|
|
8332
8096
|
if (result.gaps.length > 0) {
|
|
8333
|
-
lines.push(
|
|
8097
|
+
lines.push(chalk7.gray("Run `pinata generate --gaps` to create tests for these gaps."));
|
|
8334
8098
|
}
|
|
8335
|
-
lines.push(
|
|
8099
|
+
lines.push(chalk7.gray(`
|
|
8336
8100
|
Scan completed in ${result.durationMs}ms`));
|
|
8337
8101
|
return lines.join("\n");
|
|
8338
8102
|
}
|
|
8339
8103
|
function formatScoreBox(score) {
|
|
8340
|
-
const gradeColor = GRADE_COLORS[score.grade] ??
|
|
8104
|
+
const gradeColor = GRADE_COLORS[score.grade] ?? chalk7.white;
|
|
8341
8105
|
const scoreStr = `Pinata Score: ${score.overall}/100 ${gradeColor(`(${score.grade})`)}`;
|
|
8342
8106
|
const boxWidth = 60;
|
|
8343
8107
|
const padding = Math.floor((boxWidth - scoreStr.length) / 2);
|
|
8344
|
-
const top =
|
|
8345
|
-
const middle =
|
|
8346
|
-
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");
|
|
8347
8111
|
return `${top}
|
|
8348
8112
|
${middle}
|
|
8349
8113
|
${bottom}`;
|
|
@@ -8358,14 +8122,14 @@ function formatDomainCoverage(coverage) {
|
|
|
8358
8122
|
}
|
|
8359
8123
|
const percent = domainCoverage.coveragePercent;
|
|
8360
8124
|
const filledWidth = Math.round(percent / 100 * barWidth);
|
|
8361
|
-
const bar =
|
|
8362
|
-
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;
|
|
8363
8127
|
const domainName = domain.padEnd(15);
|
|
8364
8128
|
const stats = `${domainCoverage.categoriesCovered}/${domainCoverage.categoriesScanned} categories`;
|
|
8365
8129
|
lines.push(` ${domainColor(domainName)} ${bar} ${percent.toString().padStart(3)}% (${stats})`);
|
|
8366
8130
|
}
|
|
8367
8131
|
if (lines.length === 0) {
|
|
8368
|
-
lines.push(
|
|
8132
|
+
lines.push(chalk7.gray(" No domain coverage data available."));
|
|
8369
8133
|
}
|
|
8370
8134
|
return lines.join("\n");
|
|
8371
8135
|
}
|
|
@@ -8376,48 +8140,48 @@ function formatGapsSummary(gaps, basePath) {
|
|
|
8376
8140
|
const medium = gaps.filter((g) => g.severity === "medium");
|
|
8377
8141
|
const low = gaps.filter((g) => g.severity === "low");
|
|
8378
8142
|
if (critical.length > 0) {
|
|
8379
|
-
lines.push(
|
|
8143
|
+
lines.push(chalk7.red.bold(`
|
|
8380
8144
|
Critical Gaps (${critical.length}):`));
|
|
8381
8145
|
for (const gap of critical.slice(0, 5)) {
|
|
8382
8146
|
lines.push(formatGapLine(gap, basePath, "critical"));
|
|
8383
8147
|
}
|
|
8384
8148
|
if (critical.length > 5) {
|
|
8385
|
-
lines.push(
|
|
8149
|
+
lines.push(chalk7.gray(` ... and ${critical.length - 5} more critical gaps`));
|
|
8386
8150
|
}
|
|
8387
8151
|
}
|
|
8388
8152
|
if (high.length > 0) {
|
|
8389
|
-
lines.push(
|
|
8153
|
+
lines.push(chalk7.red(`
|
|
8390
8154
|
High Severity Gaps (${high.length}):`));
|
|
8391
8155
|
for (const gap of high.slice(0, 5)) {
|
|
8392
8156
|
lines.push(formatGapLine(gap, basePath, "high"));
|
|
8393
8157
|
}
|
|
8394
8158
|
if (high.length > 5) {
|
|
8395
|
-
lines.push(
|
|
8159
|
+
lines.push(chalk7.gray(` ... and ${high.length - 5} more high severity gaps`));
|
|
8396
8160
|
}
|
|
8397
8161
|
}
|
|
8398
8162
|
if (medium.length > 0) {
|
|
8399
|
-
lines.push(
|
|
8163
|
+
lines.push(chalk7.yellow(`
|
|
8400
8164
|
Medium Severity Gaps (${medium.length}):`));
|
|
8401
8165
|
for (const gap of medium.slice(0, 3)) {
|
|
8402
8166
|
lines.push(formatGapLine(gap, basePath, "medium"));
|
|
8403
8167
|
}
|
|
8404
8168
|
if (medium.length > 3) {
|
|
8405
|
-
lines.push(
|
|
8169
|
+
lines.push(chalk7.gray(` ... and ${medium.length - 3} more medium severity gaps`));
|
|
8406
8170
|
}
|
|
8407
8171
|
}
|
|
8408
8172
|
if (low.length > 0) {
|
|
8409
|
-
lines.push(
|
|
8173
|
+
lines.push(chalk7.gray(`
|
|
8410
8174
|
Low Severity: ${low.length} gaps`));
|
|
8411
8175
|
}
|
|
8412
8176
|
return lines.join("\n");
|
|
8413
8177
|
}
|
|
8414
8178
|
function formatGapLine(gap, basePath, severity) {
|
|
8415
|
-
const severityColor = SEVERITY_COLORS2[severity] ??
|
|
8179
|
+
const severityColor = SEVERITY_COLORS2[severity] ?? chalk7.white;
|
|
8416
8180
|
const icon = severity === "critical" ? "\u26D4" : severity === "high" ? "\u{1F534}" : severity === "medium" ? "\u{1F7E1}" : "\u26AA";
|
|
8417
8181
|
const relPath = relative(basePath, gap.filePath);
|
|
8418
8182
|
const location = `${relPath}:${gap.lineStart}`;
|
|
8419
8183
|
const confidence = gap.confidence.toUpperCase();
|
|
8420
|
-
return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${
|
|
8184
|
+
return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk7.cyan(location.padEnd(30))} ${chalk7.gray(confidence)} confidence`;
|
|
8421
8185
|
}
|
|
8422
8186
|
function formatLanguages(result) {
|
|
8423
8187
|
const languages = [];
|
|
@@ -8640,637 +8404,785 @@ function isValidScanOutputFormat(format) {
|
|
|
8640
8404
|
return ["terminal", "json", "markdown", "sarif", "html", "junit-xml"].includes(format);
|
|
8641
8405
|
}
|
|
8642
8406
|
|
|
8643
|
-
// src/cli/
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
// When bundled in dist (future)
|
|
8653
|
-
resolve(__dirname2, "../categories/definitions")
|
|
8654
|
-
];
|
|
8655
|
-
for (const candidate of candidates) {
|
|
8656
|
-
if (existsSync(candidate)) {
|
|
8657
|
-
return candidate;
|
|
8658
|
-
}
|
|
8659
|
-
}
|
|
8660
|
-
return candidates[0];
|
|
8661
|
-
}
|
|
8662
|
-
var program = new Command();
|
|
8663
|
-
program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
|
|
8664
|
-
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) => {
|
|
8665
|
-
const isQuiet = Boolean(options["quiet"]);
|
|
8666
|
-
const isVerbose = Boolean(options["verbose"]);
|
|
8667
|
-
if (isQuiet) {
|
|
8668
|
-
logger.configure({ level: "error" });
|
|
8669
|
-
} else if (isVerbose) {
|
|
8670
|
-
logger.configure({ level: "debug" });
|
|
8671
|
-
}
|
|
8672
|
-
const targetDirectory = resolve(targetPath ?? process.cwd());
|
|
8673
|
-
if (!existsSync(targetDirectory)) {
|
|
8674
|
-
console.error(formatError(new Error(`Directory not found: ${targetDirectory}`)));
|
|
8675
|
-
process.exit(1);
|
|
8676
|
-
}
|
|
8677
|
-
const outputFormat = String(options["output"] ?? "terminal");
|
|
8678
|
-
if (!isValidScanOutputFormat(outputFormat)) {
|
|
8679
|
-
console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown, sarif`)));
|
|
8680
|
-
process.exit(1);
|
|
8681
|
-
}
|
|
8682
|
-
const validSeverities = ["critical", "high", "medium", "low"];
|
|
8683
|
-
const minSeverity = String(options["severity"] ?? "low");
|
|
8684
|
-
if (!validSeverities.includes(minSeverity)) {
|
|
8685
|
-
console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
|
|
8686
|
-
process.exit(1);
|
|
8687
|
-
}
|
|
8688
|
-
const validConfidences = ["high", "medium", "low"];
|
|
8689
|
-
const minConfidence = String(options["confidence"] ?? "high");
|
|
8690
|
-
if (!validConfidences.includes(minConfidence)) {
|
|
8691
|
-
console.error(formatError(new Error(`Invalid confidence: ${minConfidence}. Use: high, medium, low`)));
|
|
8692
|
-
process.exit(1);
|
|
8693
|
-
}
|
|
8694
|
-
const domainsStr = options["domains"];
|
|
8695
|
-
let domains = [];
|
|
8696
|
-
if (domainsStr) {
|
|
8697
|
-
const domainList = domainsStr.split(",").map((d) => d.trim());
|
|
8698
|
-
for (const domain of domainList) {
|
|
8699
|
-
if (!RISK_DOMAINS.includes(domain)) {
|
|
8700
|
-
console.error(formatError(new Error(`Invalid domain: ${domain}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
|
|
8701
|
-
process.exit(1);
|
|
8702
|
-
}
|
|
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" });
|
|
8703
8416
|
}
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
const excludeDirs = excludeStr ? excludeStr.split(",").map((d) => d.trim()) : void 0;
|
|
8708
|
-
const failOn = options["failOn"];
|
|
8709
|
-
if (failOn && !["critical", "high", "medium"].includes(failOn)) {
|
|
8710
|
-
console.error(formatError(new Error(`Invalid fail-on level: ${failOn}. Use: critical, high, medium`)));
|
|
8711
|
-
process.exit(1);
|
|
8712
|
-
}
|
|
8713
|
-
const showSpinner = outputFormat === "terminal" && !isQuiet;
|
|
8714
|
-
const spinner = showSpinner ? ora("Loading categories...").start() : null;
|
|
8715
|
-
try {
|
|
8716
|
-
const store = createCategoryStore();
|
|
8717
|
-
const definitionsPath = getDefinitionsPath();
|
|
8718
|
-
logger.debug(`Loading categories from: ${definitionsPath}`);
|
|
8719
|
-
const loadResult = await store.loadFromDirectory(definitionsPath);
|
|
8720
|
-
if (!loadResult.success) {
|
|
8721
|
-
spinner?.fail("Failed to load categories");
|
|
8722
|
-
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}`)));
|
|
8723
8420
|
process.exit(1);
|
|
8724
8421
|
}
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
const scanner = createScanner(store);
|
|
8730
|
-
const scanOptions = {
|
|
8731
|
-
minSeverity,
|
|
8732
|
-
minConfidence,
|
|
8733
|
-
detectTestFiles: true
|
|
8734
|
-
};
|
|
8735
|
-
if (domains.length > 0) {
|
|
8736
|
-
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);
|
|
8737
8426
|
}
|
|
8738
|
-
|
|
8739
|
-
|
|
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);
|
|
8740
8432
|
}
|
|
8741
|
-
const
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
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`)));
|
|
8745
8437
|
process.exit(1);
|
|
8746
8438
|
}
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
if (
|
|
8750
|
-
const
|
|
8751
|
-
const
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
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);
|
|
8447
|
+
}
|
|
8448
|
+
}
|
|
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."));
|
|
8773
8508
|
} else {
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
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
|
+
}
|
|
8777
8518
|
}
|
|
8519
|
+
} else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
|
|
8520
|
+
provider = "openai";
|
|
8778
8521
|
}
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
8788
|
-
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
8803
|
-
|
|
8804
|
-
|
|
8805
|
-
|
|
8806
|
-
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
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
|
+
}
|
|
8812
8555
|
}
|
|
8813
|
-
|
|
8814
|
-
|
|
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)}`));
|
|
8815
8560
|
}
|
|
8816
8561
|
}
|
|
8817
|
-
} catch (error) {
|
|
8818
|
-
verifySpinner?.fail("AI verification failed (results unverified)");
|
|
8819
|
-
if (isVerbose) {
|
|
8820
|
-
console.error(chalk5.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8821
|
-
}
|
|
8822
8562
|
}
|
|
8823
8563
|
}
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
|
|
8832
|
-
console.log(chalk5.yellow("\nNo dynamically testable gaps found."));
|
|
8833
|
-
console.log(chalk5.gray("Testable types: sql-injection, xss, command-injection, path-traversal"));
|
|
8834
|
-
} else {
|
|
8835
|
-
const runner = createRunner2(void 0, isDryRun);
|
|
8836
|
-
const initResult = await runner.initialize();
|
|
8837
|
-
if (!initResult.ready) {
|
|
8838
|
-
console.log(chalk5.red(`
|
|
8839
|
-
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."));
|
|
8840
8572
|
} else {
|
|
8841
|
-
const
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
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
|
+
}
|
|
8847
8586
|
}
|
|
8848
8587
|
}
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
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
|
+
}
|
|
8858
8597
|
}
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
console.log(chalk5.red.bold(`
|
|
8598
|
+
if (executionSummary.confirmed > 0) {
|
|
8599
|
+
console.log(chalk7.red.bold(`
|
|
8862
8600
|
\u26A0\uFE0F ${executionSummary.confirmed} CONFIRMED vulnerabilities found!`));
|
|
8601
|
+
}
|
|
8863
8602
|
}
|
|
8864
8603
|
}
|
|
8865
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);
|
|
8866
8636
|
}
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
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("");
|
|
8870
8668
|
}
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
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);
|
|
8877
8801
|
} else {
|
|
8878
|
-
|
|
8802
|
+
outputPath = resolve(basePath, test.suggestedPath);
|
|
8879
8803
|
}
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
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 {
|
|
8884
8818
|
}
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
const
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
}
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
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++;
|
|
8904
8862
|
}
|
|
8863
|
+
} catch (error) {
|
|
8864
|
+
summary.failed.push({
|
|
8865
|
+
path: outputPath,
|
|
8866
|
+
error: error instanceof Error ? error.message : String(error)
|
|
8867
|
+
});
|
|
8905
8868
|
}
|
|
8906
|
-
process.exit(0);
|
|
8907
|
-
} catch (error) {
|
|
8908
|
-
spinner?.fail("Analysis failed");
|
|
8909
|
-
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
8910
|
-
process.exit(1);
|
|
8911
|
-
}
|
|
8912
|
-
});
|
|
8913
|
-
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) => {
|
|
8914
|
-
const isQuiet = Boolean(options["quiet"]);
|
|
8915
|
-
const isVerbose = Boolean(options["verbose"]);
|
|
8916
|
-
const dryRun = !options["write"];
|
|
8917
|
-
const useAI = Boolean(options["ai"]);
|
|
8918
|
-
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
8919
|
-
const outputFormat = String(options["output"] ?? "terminal");
|
|
8920
|
-
if (isQuiet) {
|
|
8921
|
-
logger.configure({ level: "error" });
|
|
8922
|
-
} else if (isVerbose) {
|
|
8923
|
-
logger.configure({ level: "debug" });
|
|
8924
8869
|
}
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
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
|
+
}
|
|
8928
8886
|
}
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
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
|
+
}
|
|
8937
8896
|
}
|
|
8938
|
-
if (
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
)
|
|
8942
|
-
|
|
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
|
+
}
|
|
8943
8904
|
}
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
)));
|
|
8950
|
-
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}`));
|
|
8951
8910
|
}
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
const
|
|
8962
|
-
const
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
const cached = cacheResult.data;
|
|
8970
|
-
let gaps = cached.gaps;
|
|
8971
|
-
if (spinner) {
|
|
8972
|
-
spinner.text = `Loaded ${gaps.length} gaps from cache. Filtering...`;
|
|
8973
|
-
}
|
|
8974
|
-
if (categoryId) {
|
|
8975
|
-
gaps = gaps.filter((g) => g.categoryId === categoryId);
|
|
8976
|
-
}
|
|
8977
|
-
if (domainFilter) {
|
|
8978
|
-
gaps = gaps.filter((g) => g.domain === domainFilter);
|
|
8979
|
-
}
|
|
8980
|
-
gaps = gaps.filter((g) => {
|
|
8981
|
-
const gapLevel = severityOrder[g.severity] ?? 0;
|
|
8982
|
-
const minLevel = severityOrder[minSeverity] ?? 0;
|
|
8983
|
-
return gapLevel >= minLevel;
|
|
8984
|
-
});
|
|
8985
|
-
if (gaps.length === 0) {
|
|
8986
|
-
spinner?.succeed("No gaps match the filters");
|
|
8987
|
-
console.log(chalk5.yellow("\nNo gaps found matching the specified filters."));
|
|
8988
|
-
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" });
|
|
8989
8928
|
}
|
|
8990
|
-
if (
|
|
8991
|
-
|
|
8929
|
+
if (!["terminal", "json"].includes(outputFormat)) {
|
|
8930
|
+
console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json`)));
|
|
8931
|
+
process.exit(1);
|
|
8992
8932
|
}
|
|
8993
|
-
const
|
|
8994
|
-
const
|
|
8995
|
-
const
|
|
8996
|
-
if (!
|
|
8997
|
-
|
|
8998
|
-
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>")));
|
|
8999
8938
|
process.exit(1);
|
|
9000
8939
|
}
|
|
9001
|
-
if (
|
|
9002
|
-
|
|
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);
|
|
9003
8943
|
}
|
|
9004
|
-
const
|
|
9005
|
-
const
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
const existing = gapsByCategory.get(gap.categoryId) ?? [];
|
|
9010
|
-
existing.push(gap);
|
|
9011
|
-
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);
|
|
9012
8949
|
}
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
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);
|
|
9018
8961
|
}
|
|
9019
|
-
const
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
|
|
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}`);
|
|
9039
9006
|
continue;
|
|
9040
9007
|
}
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
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 });
|
|
9053
9034
|
}
|
|
9054
|
-
const suggestedPath = suggestTestPath(gap.filePath, template, cached.targetDirectory);
|
|
9055
|
-
generatedTests.push({
|
|
9056
|
-
gap,
|
|
9057
|
-
category,
|
|
9058
|
-
template,
|
|
9059
|
-
result: renderResult.data,
|
|
9060
|
-
suggestedPath
|
|
9061
|
-
});
|
|
9062
9035
|
}
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
console.log(formatGeneratedTerminal(generatedTests, cached.targetDirectory));
|
|
9069
|
-
}
|
|
9070
|
-
if (isVerbose && errors.length > 0) {
|
|
9071
|
-
console.error(chalk5.yellow("\nWarnings:"));
|
|
9072
|
-
for (const error of errors) {
|
|
9073
|
-
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));
|
|
9074
9041
|
}
|
|
9075
|
-
|
|
9076
|
-
|
|
9077
|
-
|
|
9078
|
-
|
|
9079
|
-
|
|
9080
|
-
cached.targetDirectory,
|
|
9081
|
-
outputDirOption
|
|
9082
|
-
);
|
|
9083
|
-
if (!writeResult.success) {
|
|
9084
|
-
console.error(formatError(writeResult.error));
|
|
9085
|
-
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
|
+
}
|
|
9086
9047
|
}
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
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
|
+
}
|
|
9090
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);
|
|
9091
9065
|
}
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
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);
|
|
9099
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) => {
|
|
9100
9075
|
const isQuiet = Boolean(options["quiet"]);
|
|
9101
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"];
|
|
9102
9080
|
const useAI = Boolean(options["ai"]);
|
|
9103
9081
|
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
9104
9082
|
const outputFormat = String(options["output"] ?? "terminal");
|
|
9105
|
-
const topN = parseInt(String(options["top"] ?? "5"), 10);
|
|
9106
9083
|
if (isQuiet) {
|
|
9107
9084
|
logger.configure({ level: "error" });
|
|
9108
9085
|
} else if (isVerbose) {
|
|
9109
9086
|
logger.configure({ level: "debug" });
|
|
9110
9087
|
}
|
|
9111
9088
|
if (!["terminal", "json", "markdown"].includes(outputFormat)) {
|
|
9112
|
-
console.error(formatError(new Error(`Invalid
|
|
9089
|
+
console.error(formatError(new Error(`Invalid format: ${outputFormat}. Use: terminal, json, markdown`)));
|
|
9113
9090
|
process.exit(1);
|
|
9114
9091
|
}
|
|
9115
|
-
const
|
|
9116
|
-
const
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
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}`)));
|
|
9124
9107
|
process.exit(1);
|
|
9125
9108
|
}
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
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
|
+
}
|
|
9138
9127
|
}
|
|
9139
|
-
|
|
9140
|
-
|
|
9141
|
-
|
|
9142
|
-
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
spinner.
|
|
9149
|
-
}
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
const ai = createAIService({ provider: aiProvider });
|
|
9153
|
-
if (!ai.isConfigured()) {
|
|
9154
|
-
spinner?.warn("AI not configured, using fallback explanations");
|
|
9155
|
-
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(`
|
|
9156
9141
|
Set ${aiProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} for AI explanations.
|
|
9157
9142
|
`));
|
|
9158
|
-
|
|
9159
|
-
explanations.push({
|
|
9160
|
-
gap,
|
|
9161
|
-
explanation: generateFallbackExplanation(gap)
|
|
9162
|
-
});
|
|
9163
|
-
}
|
|
9164
|
-
} else {
|
|
9165
|
-
for (const gap of gaps) {
|
|
9166
|
-
const result = await explainGap(gap, void 0, { provider: aiProvider });
|
|
9167
|
-
if (result.success && result.data) {
|
|
9168
|
-
explanations.push({ gap, explanation: result.data });
|
|
9169
|
-
} else {
|
|
9170
|
-
explanations.push({
|
|
9171
|
-
gap,
|
|
9172
|
-
explanation: generateFallbackExplanation(gap)
|
|
9173
|
-
});
|
|
9174
|
-
}
|
|
9175
|
-
}
|
|
9176
|
-
}
|
|
9177
|
-
} else {
|
|
9178
|
-
for (const gap of gaps) {
|
|
9179
|
-
explanations.push({
|
|
9180
|
-
gap,
|
|
9181
|
-
explanation: generateFallbackExplanation(gap)
|
|
9182
|
-
});
|
|
9183
|
-
}
|
|
9143
|
+
explanations = gaps.map((g) => generateFallbackExplanation(g));
|
|
9184
9144
|
}
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9194
|
-
|
|
9195
|
-
|
|
9196
|
-
|
|
9197
|
-
explanation: e.explanation
|
|
9198
|
-
})), null, 2));
|
|
9199
|
-
} else if (outputFormat === "markdown") {
|
|
9200
|
-
console.log(`# Gap Explanations
|
|
9201
|
-
`);
|
|
9202
|
-
console.log(`Generated ${explanations.length} explanation(s).
|
|
9203
|
-
`);
|
|
9204
|
-
for (const { gap, explanation } of explanations) {
|
|
9205
|
-
console.log(`## ${gap.categoryName}
|
|
9206
|
-
`);
|
|
9207
|
-
console.log(`**File:** \`${gap.filePath}:${gap.lineStart}\`
|
|
9208
|
-
`);
|
|
9209
|
-
console.log(`**Severity:** ${gap.severity} | **Confidence:** ${gap.confidence}
|
|
9210
|
-
`);
|
|
9211
|
-
console.log(`### Summary
|
|
9212
|
-
${explanation.summary}
|
|
9213
|
-
`);
|
|
9214
|
-
console.log(`### Explanation
|
|
9215
|
-
${explanation.explanation}
|
|
9216
|
-
`);
|
|
9217
|
-
console.log(`### Risk
|
|
9218
|
-
${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}
|
|
9219
9157
|
`);
|
|
9220
|
-
|
|
9221
|
-
${explanation.remediation}
|
|
9158
|
+
console.log(`**Severity**: ${gap.severity} | **Category**: ${gap.categoryId}
|
|
9222
9159
|
`);
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
${
|
|
9227
|
-
\`\`\`
|
|
9160
|
+
console.log(exp.explanation);
|
|
9161
|
+
if (exp.remediation) {
|
|
9162
|
+
console.log(`
|
|
9163
|
+
**Remediation**: ${exp.remediation}
|
|
9228
9164
|
`);
|
|
9229
|
-
}
|
|
9230
|
-
console.log("---\n");
|
|
9231
9165
|
}
|
|
9232
|
-
|
|
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}`));
|
|
9233
9176
|
console.log();
|
|
9234
|
-
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
console.log(chalk5.bold.white(gap.categoryName));
|
|
9239
|
-
console.log(chalk5.gray(` ${gap.filePath}:${gap.lineStart}`));
|
|
9240
|
-
const severityColor = gap.severity === "critical" ? chalk5.red : gap.severity === "high" ? chalk5.yellow : chalk5.blue;
|
|
9241
|
-
console.log(` ${severityColor(gap.severity)} | ${gap.confidence} confidence`);
|
|
9242
|
-
console.log();
|
|
9243
|
-
console.log(chalk5.cyan(" Summary:"));
|
|
9244
|
-
console.log(` ${explanation.summary}`);
|
|
9245
|
-
if (isVerbose) {
|
|
9246
|
-
console.log();
|
|
9247
|
-
console.log(chalk5.cyan(" Explanation:"));
|
|
9248
|
-
for (const line of explanation.explanation.split("\n")) {
|
|
9249
|
-
console.log(` ${line}`);
|
|
9250
|
-
}
|
|
9251
|
-
}
|
|
9252
|
-
console.log();
|
|
9253
|
-
console.log(chalk5.red(" Risk:"));
|
|
9254
|
-
console.log(` ${explanation.risk}`);
|
|
9255
|
-
console.log();
|
|
9256
|
-
console.log(chalk5.green(" How to Fix:"));
|
|
9257
|
-
for (const line of explanation.remediation.split("\n")) {
|
|
9258
|
-
console.log(` ${line}`);
|
|
9259
|
-
}
|
|
9260
|
-
if (explanation.safeExample) {
|
|
9261
|
-
console.log();
|
|
9262
|
-
console.log(chalk5.cyan(" Safe Example:"));
|
|
9263
|
-
console.log(chalk5.gray(` ${explanation.safeExample}`));
|
|
9264
|
-
}
|
|
9177
|
+
for (const line of exp.explanation.split("\n")) {
|
|
9178
|
+
console.log(` ${line}`);
|
|
9179
|
+
}
|
|
9180
|
+
if (exp.remediation) {
|
|
9265
9181
|
console.log();
|
|
9266
|
-
console.log(
|
|
9182
|
+
console.log(chalk7.green(` Fix: ${exp.remediation}`));
|
|
9267
9183
|
}
|
|
9184
|
+
console.log();
|
|
9268
9185
|
}
|
|
9269
|
-
process.exit(0);
|
|
9270
|
-
} catch (error) {
|
|
9271
|
-
spinner?.fail("Explanation failed");
|
|
9272
|
-
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9273
|
-
process.exit(1);
|
|
9274
9186
|
}
|
|
9275
9187
|
});
|
|
9276
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) => {
|
|
@@ -9278,96 +9190,73 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
|
|
|
9278
9190
|
const language = String(options["language"]);
|
|
9279
9191
|
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
9280
9192
|
const outputFormat = String(options["output"] ?? "terminal");
|
|
9281
|
-
const codeSnippets = options["code"];
|
|
9282
9193
|
const filePath = options["file"];
|
|
9283
|
-
|
|
9194
|
+
const codeSnippets = options["code"] ?? [];
|
|
9195
|
+
const samples = [...codeSnippets];
|
|
9284
9196
|
if (filePath) {
|
|
9197
|
+
const { readFile: readFile7 } = await import('fs/promises');
|
|
9285
9198
|
try {
|
|
9286
|
-
const
|
|
9287
|
-
|
|
9288
|
-
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()));
|
|
9289
9201
|
} catch (error) {
|
|
9290
9202
|
console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
|
|
9291
9203
|
process.exit(1);
|
|
9292
9204
|
}
|
|
9293
9205
|
}
|
|
9294
|
-
if (
|
|
9206
|
+
if (samples.length === 0) {
|
|
9295
9207
|
console.error(formatError(new Error("Provide code samples via --code or --file")));
|
|
9296
9208
|
process.exit(1);
|
|
9297
9209
|
}
|
|
9298
|
-
const spinner =
|
|
9210
|
+
const spinner = ora3("Generating pattern suggestions with AI...").start();
|
|
9299
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
|
+
}
|
|
9300
9220
|
const result = await suggestPatterns(
|
|
9301
|
-
{
|
|
9302
|
-
|
|
9303
|
-
language,
|
|
9304
|
-
vulnerableCode,
|
|
9305
|
-
maxSuggestions: 5
|
|
9306
|
-
},
|
|
9307
|
-
{ provider: aiProvider }
|
|
9221
|
+
{ category: categoryId, vulnerableCode: samples, language },
|
|
9222
|
+
aiConfig
|
|
9308
9223
|
);
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
console.error(
|
|
9224
|
+
if (!result.success || !result.data) {
|
|
9225
|
+
spinner.fail("Pattern suggestion failed");
|
|
9226
|
+
console.error(chalk7.red(result.error ?? "Unknown error"));
|
|
9312
9227
|
process.exit(1);
|
|
9313
9228
|
}
|
|
9314
|
-
const
|
|
9229
|
+
const suggestions = result.data.suggestions;
|
|
9230
|
+
spinner.succeed(`Generated ${suggestions.length} pattern suggestions`);
|
|
9315
9231
|
if (outputFormat === "json") {
|
|
9316
|
-
console.log(JSON.stringify(
|
|
9232
|
+
console.log(JSON.stringify(suggestions, null, 2));
|
|
9317
9233
|
} else if (outputFormat === "yaml") {
|
|
9318
|
-
|
|
9319
|
-
|
|
9320
|
-
|
|
9321
|
-
|
|
9322
|
-
|
|
9323
|
-
console.log(`
|
|
9324
|
-
console.log(`
|
|
9325
|
-
console.log(`
|
|
9326
|
-
console.log(` pattern: "${escapedPattern}"`);
|
|
9327
|
-
console.log(` confidence: ${suggestion.confidence}`);
|
|
9328
|
-
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}"`);
|
|
9329
9242
|
console.log();
|
|
9330
9243
|
}
|
|
9331
9244
|
} else {
|
|
9332
9245
|
console.log();
|
|
9333
|
-
console.log(
|
|
9334
|
-
console.log(
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9338
|
-
|
|
9339
|
-
|
|
9340
|
-
|
|
9341
|
-
|
|
9342
|
-
console.log();
|
|
9343
|
-
console.log(chalk5.cyan(" Pattern:"));
|
|
9344
|
-
console.log(` ${suggestion.pattern}`);
|
|
9345
|
-
console.log();
|
|
9346
|
-
console.log(chalk5.cyan(" Confidence:") + ` ${suggestion.confidence}`);
|
|
9347
|
-
console.log();
|
|
9348
|
-
console.log(chalk5.green(" Would match:"));
|
|
9349
|
-
console.log(chalk5.gray(` ${suggestion.matchExample}`));
|
|
9350
|
-
console.log();
|
|
9351
|
-
console.log(chalk5.red(" Should NOT match:"));
|
|
9352
|
-
console.log(chalk5.gray(` ${suggestion.safeExample}`));
|
|
9353
|
-
console.log();
|
|
9354
|
-
console.log(chalk5.cyan(" Reasoning:"));
|
|
9355
|
-
console.log(` ${suggestion.reasoning}`);
|
|
9356
|
-
console.log();
|
|
9357
|
-
console.log(chalk5.gray("\u2500".repeat(60)));
|
|
9358
|
-
}
|
|
9359
|
-
}
|
|
9360
|
-
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))}`);
|
|
9361
9255
|
console.log();
|
|
9362
|
-
console.log(chalk5.yellow.bold(`Rejected ${rejected.length} pattern(s):`));
|
|
9363
|
-
for (const r of rejected) {
|
|
9364
|
-
console.log(chalk5.gray(` - ${r.pattern.slice(0, 40)}... : ${r.reason}`));
|
|
9365
|
-
}
|
|
9366
9256
|
}
|
|
9367
9257
|
}
|
|
9368
|
-
process.exit(0);
|
|
9369
9258
|
} catch (error) {
|
|
9370
|
-
spinner.fail("Pattern
|
|
9259
|
+
spinner.fail("Pattern suggestion failed");
|
|
9371
9260
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9372
9261
|
process.exit(1);
|
|
9373
9262
|
}
|
|
@@ -9422,9 +9311,7 @@ program.command("search <query>").description("Search category taxonomy by name,
|
|
|
9422
9311
|
}
|
|
9423
9312
|
const langFilter = options["language"];
|
|
9424
9313
|
if (langFilter) {
|
|
9425
|
-
results = results.filter(
|
|
9426
|
-
(cat) => cat.applicableLanguages.includes(langFilter)
|
|
9427
|
-
);
|
|
9314
|
+
results = results.filter((cat) => cat.applicableLanguages.includes(langFilter));
|
|
9428
9315
|
}
|
|
9429
9316
|
if (outputFormat === "json") {
|
|
9430
9317
|
console.log(JSON.stringify(results, null, 2));
|
|
@@ -9446,19 +9333,18 @@ ${cat.description}
|
|
|
9446
9333
|
}
|
|
9447
9334
|
} else {
|
|
9448
9335
|
console.log();
|
|
9449
|
-
console.log(
|
|
9450
|
-
console.log(
|
|
9336
|
+
console.log(chalk7.bold(`Search Results for "${query}"`));
|
|
9337
|
+
console.log(chalk7.gray(`Found ${results.length} matching categories.`));
|
|
9451
9338
|
console.log();
|
|
9452
9339
|
if (results.length === 0) {
|
|
9453
|
-
console.log(
|
|
9454
|
-
console.log(chalk5.gray("Try a different query or broaden your filters."));
|
|
9340
|
+
console.log(chalk7.yellow("No categories match your search."));
|
|
9455
9341
|
} else {
|
|
9456
9342
|
for (const cat of results) {
|
|
9457
|
-
const domainColor = cat.domain === "security" ?
|
|
9458
|
-
console.log(` ${
|
|
9343
|
+
const domainColor = cat.domain === "security" ? chalk7.red : chalk7.blue;
|
|
9344
|
+
console.log(` ${chalk7.cyan(cat.id)} - ${chalk7.bold(cat.name)}`);
|
|
9459
9345
|
console.log(` ${domainColor(cat.domain)} | ${cat.level} | ${cat.priority}`);
|
|
9460
9346
|
if (options["verbose"]) {
|
|
9461
|
-
console.log(` ${
|
|
9347
|
+
console.log(` ${chalk7.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
|
|
9462
9348
|
}
|
|
9463
9349
|
console.log();
|
|
9464
9350
|
}
|
|
@@ -9492,21 +9378,17 @@ program.command("list").description("List all categories").option("-d, --domain
|
|
|
9492
9378
|
process.exit(1);
|
|
9493
9379
|
}
|
|
9494
9380
|
const priorityFilter = options["priority"];
|
|
9495
|
-
|
|
9496
|
-
if (priorityFilter !== void 0 && !validPriorities.includes(priorityFilter)) {
|
|
9381
|
+
if (priorityFilter !== void 0 && !["P0", "P1", "P2"].includes(priorityFilter)) {
|
|
9497
9382
|
console.error(formatError(new Error(`Invalid priority: ${priorityFilter}. Use: P0, P1, P2`)));
|
|
9498
9383
|
process.exit(1);
|
|
9499
9384
|
}
|
|
9500
|
-
logger.debug("Loading categories...");
|
|
9501
9385
|
const store = createCategoryStore();
|
|
9502
9386
|
const definitionsPath = getDefinitionsPath();
|
|
9503
|
-
logger.debug(`Loading from: ${definitionsPath}`);
|
|
9504
9387
|
const loadResult = await store.loadFromDirectory(definitionsPath);
|
|
9505
9388
|
if (!loadResult.success) {
|
|
9506
9389
|
console.error(formatError(loadResult.error));
|
|
9507
9390
|
process.exit(1);
|
|
9508
9391
|
}
|
|
9509
|
-
logger.debug(`Loaded ${loadResult.data} categories`);
|
|
9510
9392
|
const filter = {};
|
|
9511
9393
|
if (domainFilter) {
|
|
9512
9394
|
filter.domain = domainFilter;
|
|
@@ -9530,54 +9412,42 @@ program.command("init").description("Initialize Pinata configuration in project"
|
|
|
9530
9412
|
const configPath = resolve(process.cwd(), ".pinata.yml");
|
|
9531
9413
|
const cacheDir = resolve(process.cwd(), ".pinata");
|
|
9532
9414
|
if (existsSync(configPath) && !options["force"]) {
|
|
9533
|
-
console.log(
|
|
9534
|
-
console.log(
|
|
9415
|
+
console.log(chalk7.yellow("Configuration file already exists at .pinata.yml"));
|
|
9416
|
+
console.log(chalk7.gray("Use --force to overwrite."));
|
|
9535
9417
|
process.exit(0);
|
|
9536
9418
|
}
|
|
9537
9419
|
const defaultConfig = `# Pinata Configuration
|
|
9538
9420
|
# https://github.com/pinata/pinata
|
|
9539
9421
|
|
|
9540
|
-
# Paths to analyze
|
|
9541
9422
|
include:
|
|
9542
9423
|
- "src/**/*.ts"
|
|
9543
9424
|
- "src/**/*.tsx"
|
|
9544
9425
|
- "src/**/*.py"
|
|
9545
9426
|
- "src/**/*.js"
|
|
9546
9427
|
|
|
9547
|
-
# Paths to exclude from analysis
|
|
9548
9428
|
exclude:
|
|
9549
9429
|
- "node_modules/**"
|
|
9550
9430
|
- "dist/**"
|
|
9551
9431
|
- "build/**"
|
|
9552
9432
|
- "**/*.test.ts"
|
|
9553
9433
|
- "**/*.spec.ts"
|
|
9554
|
-
- "**/test/**"
|
|
9555
|
-
- "**/tests/**"
|
|
9556
|
-
- "**/__tests__/**"
|
|
9557
9434
|
|
|
9558
|
-
# Risk domains to analyze
|
|
9559
|
-
# Options: security, data, concurrency, input, resource, reliability, performance, platform, business, compliance
|
|
9560
9435
|
domains:
|
|
9561
9436
|
- security
|
|
9562
9437
|
- data
|
|
9563
9438
|
- concurrency
|
|
9564
9439
|
- input
|
|
9565
9440
|
|
|
9566
|
-
# Minimum severity to report
|
|
9567
|
-
# Options: critical, high, medium, low
|
|
9568
9441
|
minSeverity: medium
|
|
9569
9442
|
|
|
9570
|
-
# Output configuration
|
|
9571
9443
|
output:
|
|
9572
|
-
format: terminal
|
|
9444
|
+
format: terminal
|
|
9573
9445
|
color: true
|
|
9574
9446
|
|
|
9575
|
-
# Test generation settings
|
|
9576
9447
|
generate:
|
|
9577
9448
|
outputDir: tests/generated
|
|
9578
|
-
framework: auto
|
|
9449
|
+
framework: auto
|
|
9579
9450
|
|
|
9580
|
-
# Fail CI if gaps exceed thresholds
|
|
9581
9451
|
thresholds:
|
|
9582
9452
|
critical: 0
|
|
9583
9453
|
high: 5
|
|
@@ -9586,25 +9456,23 @@ thresholds:
|
|
|
9586
9456
|
const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
|
|
9587
9457
|
try {
|
|
9588
9458
|
await writeFileAsync(configPath, defaultConfig, "utf8");
|
|
9589
|
-
console.log(
|
|
9459
|
+
console.log(chalk7.green("Created .pinata.yml"));
|
|
9590
9460
|
await mkdir4(cacheDir, { recursive: true });
|
|
9591
|
-
console.log(
|
|
9461
|
+
console.log(chalk7.green("Created .pinata/ directory"));
|
|
9592
9462
|
const gitignorePath = resolve(process.cwd(), ".gitignore");
|
|
9593
9463
|
if (existsSync(gitignorePath)) {
|
|
9594
9464
|
const { readFile: readFile7, appendFile } = await import('fs/promises');
|
|
9595
9465
|
const gitignore = await readFile7(gitignorePath, "utf8");
|
|
9596
9466
|
if (!gitignore.includes(".pinata/")) {
|
|
9597
9467
|
await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
|
|
9598
|
-
console.log(
|
|
9468
|
+
console.log(chalk7.green("Added .pinata/ to .gitignore"));
|
|
9599
9469
|
}
|
|
9600
9470
|
}
|
|
9601
9471
|
console.log();
|
|
9602
|
-
console.log(
|
|
9603
|
-
console.log();
|
|
9604
|
-
console.log("
|
|
9605
|
-
console.log(
|
|
9606
|
-
console.log(chalk5.gray(" 2. Run: pinata analyze"));
|
|
9607
|
-
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"));
|
|
9608
9476
|
} catch (error) {
|
|
9609
9477
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9610
9478
|
process.exit(1);
|
|
@@ -9617,18 +9485,15 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9617
9485
|
const checkAge = Boolean(options["checkAge"]);
|
|
9618
9486
|
const strictMode = Boolean(options["strict"]);
|
|
9619
9487
|
const doAllChecks = !checkRegistry && !checkDownloads && !checkAge;
|
|
9620
|
-
console.log(
|
|
9488
|
+
console.log(chalk7.bold("\nPinata Dependency Audit\n"));
|
|
9621
9489
|
if (!existsSync(packagePath)) {
|
|
9622
|
-
console.error(
|
|
9490
|
+
console.error(chalk7.red(`Error: ${packagePath} not found`));
|
|
9623
9491
|
process.exit(1);
|
|
9624
9492
|
}
|
|
9625
9493
|
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
9626
|
-
const allDeps = {
|
|
9627
|
-
...packageJson.dependencies,
|
|
9628
|
-
...packageJson.devDependencies
|
|
9629
|
-
};
|
|
9494
|
+
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
9630
9495
|
const packages = Object.keys(allDeps);
|
|
9631
|
-
console.log(
|
|
9496
|
+
console.log(chalk7.gray(`Found ${packages.length} dependencies
|
|
9632
9497
|
`));
|
|
9633
9498
|
const issues = [];
|
|
9634
9499
|
const KNOWN_MALWARE = /* @__PURE__ */ new Set([
|
|
@@ -9651,56 +9516,31 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9651
9516
|
]);
|
|
9652
9517
|
for (const pkg of packages) {
|
|
9653
9518
|
if (KNOWN_MALWARE.has(pkg)) {
|
|
9654
|
-
issues.push({
|
|
9655
|
-
pkg,
|
|
9656
|
-
severity: "critical",
|
|
9657
|
-
message: "Known malicious/compromised package (Shai-Hulud/typosquat)"
|
|
9658
|
-
});
|
|
9519
|
+
issues.push({ pkg, severity: "critical", message: "Known malicious/compromised package (Shai-Hulud/typosquat)" });
|
|
9659
9520
|
}
|
|
9660
9521
|
}
|
|
9661
9522
|
for (const [pkg, version] of Object.entries(allDeps)) {
|
|
9662
9523
|
if (version?.startsWith("^")) {
|
|
9663
|
-
issues.push({
|
|
9664
|
-
pkg,
|
|
9665
|
-
severity: "warning",
|
|
9666
|
-
message: `Unpinned version (${version}) - allows minor updates`
|
|
9667
|
-
});
|
|
9524
|
+
issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows minor updates` });
|
|
9668
9525
|
} else if (version?.startsWith("~")) {
|
|
9669
|
-
issues.push({
|
|
9670
|
-
pkg,
|
|
9671
|
-
severity: "warning",
|
|
9672
|
-
message: `Unpinned version (${version}) - allows patch updates`
|
|
9673
|
-
});
|
|
9526
|
+
issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows patch updates` });
|
|
9674
9527
|
} else if (version === "*" || version === "latest") {
|
|
9675
|
-
issues.push({
|
|
9676
|
-
pkg,
|
|
9677
|
-
severity: "critical",
|
|
9678
|
-
message: `Extremely dangerous version (${version}) - allows any version`
|
|
9679
|
-
});
|
|
9528
|
+
issues.push({ pkg, severity: "critical", message: `Extremely dangerous version (${version}) - allows any version` });
|
|
9680
9529
|
}
|
|
9681
9530
|
}
|
|
9682
9531
|
if (checkRegistry || doAllChecks) {
|
|
9683
|
-
const spinner =
|
|
9532
|
+
const spinner = ora3("Checking npm registry...").start();
|
|
9684
9533
|
for (const pkg of packages.slice(0, 50)) {
|
|
9685
9534
|
try {
|
|
9686
9535
|
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`);
|
|
9687
9536
|
if (response.status === 404) {
|
|
9688
|
-
issues.push({
|
|
9689
|
-
pkg,
|
|
9690
|
-
severity: "critical",
|
|
9691
|
-
message: "Package NOT FOUND in npm registry (slopsquatting risk)"
|
|
9692
|
-
});
|
|
9537
|
+
issues.push({ pkg, severity: "critical", message: "Package NOT FOUND in npm registry (slopsquatting risk)" });
|
|
9693
9538
|
} else if (response.ok) {
|
|
9694
9539
|
const data = await response.json();
|
|
9695
9540
|
if ((checkAge || doAllChecks) && data.time?.created) {
|
|
9696
|
-
const
|
|
9697
|
-
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);
|
|
9698
9542
|
if (ageInDays < 30) {
|
|
9699
|
-
issues.push({
|
|
9700
|
-
pkg,
|
|
9701
|
-
severity: "warning",
|
|
9702
|
-
message: `Very new package (${Math.floor(ageInDays)} days old)`
|
|
9703
|
-
});
|
|
9543
|
+
issues.push({ pkg, severity: "warning", message: `Very new package (${Math.floor(ageInDays)} days old)` });
|
|
9704
9544
|
}
|
|
9705
9545
|
}
|
|
9706
9546
|
}
|
|
@@ -9712,24 +9552,24 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9712
9552
|
const criticals = issues.filter((i) => i.severity === "critical");
|
|
9713
9553
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
9714
9554
|
if (criticals.length > 0) {
|
|
9715
|
-
console.log(
|
|
9555
|
+
console.log(chalk7.red.bold(`
|
|
9716
9556
|
Critical Issues (${criticals.length}):`));
|
|
9717
9557
|
for (const issue of criticals) {
|
|
9718
|
-
console.log(
|
|
9558
|
+
console.log(chalk7.red(` \u2717 ${issue.pkg}: ${issue.message}`));
|
|
9719
9559
|
}
|
|
9720
9560
|
}
|
|
9721
9561
|
if (warnings.length > 0) {
|
|
9722
|
-
console.log(
|
|
9562
|
+
console.log(chalk7.yellow.bold(`
|
|
9723
9563
|
Warnings (${warnings.length}):`));
|
|
9724
9564
|
for (const issue of warnings.slice(0, 20)) {
|
|
9725
|
-
console.log(
|
|
9565
|
+
console.log(chalk7.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
|
|
9726
9566
|
}
|
|
9727
9567
|
if (warnings.length > 20) {
|
|
9728
|
-
console.log(
|
|
9568
|
+
console.log(chalk7.gray(` ... and ${warnings.length - 20} more`));
|
|
9729
9569
|
}
|
|
9730
9570
|
}
|
|
9731
9571
|
if (issues.length === 0) {
|
|
9732
|
-
console.log(
|
|
9572
|
+
console.log(chalk7.green("\u2713 No dependency issues found"));
|
|
9733
9573
|
}
|
|
9734
9574
|
console.log();
|
|
9735
9575
|
if (criticals.length > 0 || strictMode && warnings.length > 0) {
|
|
@@ -9742,7 +9582,7 @@ program.command("feedback").description("View pattern performance feedback (Laye
|
|
|
9742
9582
|
const shouldReset = Boolean(options["reset"]);
|
|
9743
9583
|
if (shouldReset) {
|
|
9744
9584
|
await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
|
|
9745
|
-
console.log(
|
|
9585
|
+
console.log(chalk7.green("Feedback data reset."));
|
|
9746
9586
|
return;
|
|
9747
9587
|
}
|
|
9748
9588
|
const state = await loadFeedback2();
|
|
@@ -9754,20 +9594,20 @@ program.command("feedback").description("View pattern performance feedback (Laye
|
|
|
9754
9594
|
console.log(generateReport2(state));
|
|
9755
9595
|
return;
|
|
9756
9596
|
}
|
|
9757
|
-
console.log(
|
|
9597
|
+
console.log(chalk7.bold("\nPinata Feedback Report\n"));
|
|
9758
9598
|
console.log(`Total scans: ${state.totalScans}`);
|
|
9759
9599
|
console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
|
|
9760
9600
|
if (state.totalScans === 0) {
|
|
9761
|
-
console.log(
|
|
9601
|
+
console.log(chalk7.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
|
|
9762
9602
|
return;
|
|
9763
9603
|
}
|
|
9764
9604
|
const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
|
|
9765
9605
|
if (patterns.length > 0) {
|
|
9766
|
-
console.log(
|
|
9606
|
+
console.log(chalk7.bold("\nPattern Performance:"));
|
|
9767
9607
|
for (const p of patterns.slice(0, 15)) {
|
|
9768
9608
|
const total = p.confirmedCount + p.unconfirmedCount;
|
|
9769
9609
|
const precisionPct = (p.precision * 100).toFixed(0);
|
|
9770
|
-
const color = p.precision >= 0.7 ?
|
|
9610
|
+
const color = p.precision >= 0.7 ? chalk7.green : p.precision >= 0.4 ? chalk7.yellow : chalk7.red;
|
|
9771
9611
|
console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
|
|
9772
9612
|
}
|
|
9773
9613
|
}
|
|
@@ -9789,76 +9629,74 @@ Examples:
|
|
|
9789
9629
|
case "anthropic-api-key": {
|
|
9790
9630
|
const validation = validateApiKey2("anthropic", value);
|
|
9791
9631
|
if (!validation.valid) {
|
|
9792
|
-
console.log(
|
|
9632
|
+
console.log(chalk7.red(`Invalid API key: ${validation.error}`));
|
|
9793
9633
|
process.exit(1);
|
|
9794
9634
|
}
|
|
9795
9635
|
setConfigValue2("anthropicApiKey", value);
|
|
9796
|
-
console.log(
|
|
9636
|
+
console.log(chalk7.green(`Anthropic API key set: ${maskApiKey2(value)}`));
|
|
9797
9637
|
break;
|
|
9798
9638
|
}
|
|
9799
9639
|
case "openai-api-key": {
|
|
9800
9640
|
const validation = validateApiKey2("openai", value);
|
|
9801
9641
|
if (!validation.valid) {
|
|
9802
|
-
console.log(
|
|
9642
|
+
console.log(chalk7.red(`Invalid API key: ${validation.error}`));
|
|
9803
9643
|
process.exit(1);
|
|
9804
9644
|
}
|
|
9805
9645
|
setConfigValue2("openaiApiKey", value);
|
|
9806
|
-
console.log(
|
|
9646
|
+
console.log(chalk7.green(`OpenAI API key set: ${maskApiKey2(value)}`));
|
|
9807
9647
|
break;
|
|
9808
9648
|
}
|
|
9809
9649
|
case "default-provider": {
|
|
9810
9650
|
if (value !== "anthropic" && value !== "openai") {
|
|
9811
|
-
console.log(
|
|
9651
|
+
console.log(chalk7.red("Provider must be 'anthropic' or 'openai'"));
|
|
9812
9652
|
process.exit(1);
|
|
9813
9653
|
}
|
|
9814
9654
|
setConfigValue2("defaultProvider", value);
|
|
9815
|
-
console.log(
|
|
9655
|
+
console.log(chalk7.green(`Default provider set to: ${value}`));
|
|
9816
9656
|
break;
|
|
9817
9657
|
}
|
|
9818
9658
|
default:
|
|
9819
|
-
console.log(
|
|
9820
|
-
console.log(
|
|
9659
|
+
console.log(chalk7.red(`Unknown config key: ${key}`));
|
|
9660
|
+
console.log(chalk7.gray("Run 'pinata config set --help' for available keys"));
|
|
9821
9661
|
process.exit(1);
|
|
9822
9662
|
}
|
|
9823
|
-
console.log(
|
|
9663
|
+
console.log(chalk7.gray(`Config stored at: ${getConfigPath2()}`));
|
|
9824
9664
|
});
|
|
9825
9665
|
config.command("get <key>").description("Get a configuration value").action(async (key) => {
|
|
9826
9666
|
const { loadConfig: loadConfig2, maskApiKey: maskApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9827
9667
|
const cfg = loadConfig2();
|
|
9828
9668
|
switch (key) {
|
|
9829
9669
|
case "anthropic-api-key":
|
|
9830
|
-
console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) :
|
|
9670
|
+
console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk7.gray("(not set)"));
|
|
9831
9671
|
break;
|
|
9832
9672
|
case "openai-api-key":
|
|
9833
|
-
console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) :
|
|
9673
|
+
console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk7.gray("(not set)"));
|
|
9834
9674
|
break;
|
|
9835
9675
|
case "default-provider":
|
|
9836
|
-
console.log(cfg.defaultProvider ??
|
|
9676
|
+
console.log(cfg.defaultProvider ?? chalk7.gray("anthropic (default)"));
|
|
9837
9677
|
break;
|
|
9838
9678
|
default:
|
|
9839
|
-
console.log(
|
|
9679
|
+
console.log(chalk7.red(`Unknown config key: ${key}`));
|
|
9840
9680
|
process.exit(1);
|
|
9841
9681
|
}
|
|
9842
9682
|
});
|
|
9843
9683
|
config.command("list").description("List all configuration values").action(async () => {
|
|
9844
9684
|
const { loadConfig: loadConfig2, maskApiKey: maskApiKey2, getConfigPath: getConfigPath2, hasApiKey: hasApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9845
9685
|
const cfg = loadConfig2();
|
|
9846
|
-
console.log(
|
|
9847
|
-
console.log(
|
|
9686
|
+
console.log(chalk7.bold("Pinata Configuration"));
|
|
9687
|
+
console.log(chalk7.gray(`Config file: ${getConfigPath2()}`));
|
|
9848
9688
|
console.log();
|
|
9849
9689
|
console.log("AI Providers:");
|
|
9850
|
-
const anthropicStatus = hasApiKey2("anthropic") ?
|
|
9851
|
-
const openaiStatus = hasApiKey2("openai") ?
|
|
9852
|
-
console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ?
|
|
9853
|
-
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)})`) : ""}`);
|
|
9854
9694
|
console.log(` Default provider: ${cfg.defaultProvider ?? "anthropic"}`);
|
|
9855
9695
|
console.log();
|
|
9856
9696
|
if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
|
|
9857
|
-
console.log(
|
|
9858
|
-
console.log(
|
|
9859
|
-
console.log(
|
|
9860
|
-
console.log(chalk5.gray(" # or"));
|
|
9861
|
-
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"));
|
|
9862
9700
|
}
|
|
9863
9701
|
});
|
|
9864
9702
|
config.command("unset <key>").description("Remove a configuration value").action(async (key) => {
|
|
@@ -9866,18 +9704,18 @@ config.command("unset <key>").description("Remove a configuration value").action
|
|
|
9866
9704
|
switch (key) {
|
|
9867
9705
|
case "anthropic-api-key":
|
|
9868
9706
|
deleteConfigValue2("anthropicApiKey");
|
|
9869
|
-
console.log(
|
|
9707
|
+
console.log(chalk7.green("Anthropic API key removed"));
|
|
9870
9708
|
break;
|
|
9871
9709
|
case "openai-api-key":
|
|
9872
9710
|
deleteConfigValue2("openaiApiKey");
|
|
9873
|
-
console.log(
|
|
9711
|
+
console.log(chalk7.green("OpenAI API key removed"));
|
|
9874
9712
|
break;
|
|
9875
9713
|
case "default-provider":
|
|
9876
9714
|
deleteConfigValue2("defaultProvider");
|
|
9877
|
-
console.log(
|
|
9715
|
+
console.log(chalk7.green("Default provider reset to: anthropic"));
|
|
9878
9716
|
break;
|
|
9879
9717
|
default:
|
|
9880
|
-
console.log(
|
|
9718
|
+
console.log(chalk7.red(`Unknown config key: ${key}`));
|
|
9881
9719
|
process.exit(1);
|
|
9882
9720
|
}
|
|
9883
9721
|
});
|
|
@@ -9885,18 +9723,13 @@ var auth = program.command("auth").description("Manage API key authentication");
|
|
|
9885
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) => {
|
|
9886
9724
|
const apiKey = options["key"] ?? process.env["PINATA_API_KEY"];
|
|
9887
9725
|
if (!apiKey) {
|
|
9888
|
-
console.log(
|
|
9889
|
-
console.log();
|
|
9890
|
-
console.log("
|
|
9891
|
-
console.log(chalk5.gray(" pinata auth login --key <your-api-key>"));
|
|
9892
|
-
console.log(chalk5.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
|
|
9893
|
-
console.log();
|
|
9894
|
-
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"));
|
|
9895
9729
|
process.exit(1);
|
|
9896
9730
|
}
|
|
9897
9731
|
if (apiKey.length < 20 || !apiKey.startsWith("pk_")) {
|
|
9898
|
-
console.log(
|
|
9899
|
-
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_'."));
|
|
9900
9733
|
process.exit(1);
|
|
9901
9734
|
}
|
|
9902
9735
|
const configDir = resolve(process.cwd(), ".pinata");
|
|
@@ -9905,19 +9738,13 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
|
|
|
9905
9738
|
try {
|
|
9906
9739
|
await mkdir4(configDir, { recursive: true });
|
|
9907
9740
|
const maskedKey = `****${apiKey.slice(-8)}`;
|
|
9908
|
-
|
|
9909
|
-
configured: true,
|
|
9910
|
-
keyId: maskedKey,
|
|
9911
|
-
configuredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
9912
|
-
};
|
|
9913
|
-
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");
|
|
9914
9742
|
const envPath = resolve(configDir, ".env");
|
|
9915
9743
|
await writeFileAsync(envPath, `PINATA_API_KEY=${apiKey}
|
|
9916
9744
|
`, { mode: 384 });
|
|
9917
|
-
console.log(
|
|
9918
|
-
console.log(
|
|
9919
|
-
console.log();
|
|
9920
|
-
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"));
|
|
9921
9748
|
} catch (error) {
|
|
9922
9749
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9923
9750
|
process.exit(1);
|
|
@@ -9938,11 +9765,7 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
9938
9765
|
await rm2(envPath);
|
|
9939
9766
|
removed = true;
|
|
9940
9767
|
}
|
|
9941
|
-
|
|
9942
|
-
console.log(chalk5.green("API key removed successfully."));
|
|
9943
|
-
} else {
|
|
9944
|
-
console.log(chalk5.yellow("No stored API key found."));
|
|
9945
|
-
}
|
|
9768
|
+
console.log(removed ? chalk7.green("API key removed successfully.") : chalk7.yellow("No stored API key found."));
|
|
9946
9769
|
} catch (error) {
|
|
9947
9770
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9948
9771
|
process.exit(1);
|
|
@@ -9951,19 +9774,19 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
9951
9774
|
auth.command("status").description("Check authentication status").action(async () => {
|
|
9952
9775
|
const authPath = resolve(process.cwd(), ".pinata", "auth.json");
|
|
9953
9776
|
if (!existsSync(authPath)) {
|
|
9954
|
-
console.log(
|
|
9955
|
-
console.log(
|
|
9777
|
+
console.log(chalk7.yellow("Not authenticated."));
|
|
9778
|
+
console.log(chalk7.gray("Run: pinata auth login --key <your-api-key>"));
|
|
9956
9779
|
process.exit(0);
|
|
9957
9780
|
}
|
|
9958
9781
|
try {
|
|
9959
9782
|
const { readFile: readFile7 } = await import('fs/promises');
|
|
9960
9783
|
const authData = JSON.parse(await readFile7(authPath, "utf8"));
|
|
9961
|
-
console.log(
|
|
9962
|
-
console.log(
|
|
9963
|
-
console.log(
|
|
9964
|
-
} catch
|
|
9965
|
-
console.log(
|
|
9966
|
-
console.log(
|
|
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."));
|
|
9967
9790
|
}
|
|
9968
9791
|
});
|
|
9969
9792
|
program.parse();
|