pinata-security-cli 0.5.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -2
- package/dist/cli/index.js +1534 -2355
- package/dist/cli/index.js.map +1 -1
- package/package.json +9 -4
- package/src/categories/definitions/concurrency/idempotency-missing.yml +5 -5
- package/src/categories/definitions/concurrency/race-condition.yml +2 -2
- package/src/categories/definitions/data/data-race.yml +9 -16
- package/src/categories/definitions/data/encoding-mismatch.yml +4 -4
- package/src/categories/definitions/data/null-handling.yml +8 -23
- package/src/categories/definitions/input/boundary-testing.yml +8 -43
- package/src/categories/definitions/input/null-undefined.yml +1 -43
- package/src/categories/definitions/network/connection-failure.yml +1 -1
- package/src/categories/definitions/resource/memory-leak.yml +8 -15
package/dist/cli/index.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { z } from 'zod';
|
|
3
2
|
import fs, { mkdir, writeFile, readFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
|
|
4
|
-
import path, { dirname, resolve, join, basename,
|
|
5
|
-
import { existsSync,
|
|
3
|
+
import path, { dirname, resolve, relative, join, basename, extname } from 'path';
|
|
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 chalk6 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,18 +1478,18 @@ 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,
|
|
1515
1485
|
timedOut
|
|
1516
1486
|
});
|
|
1517
1487
|
});
|
|
1518
|
-
proc.on("error", (
|
|
1488
|
+
proc.on("error", (err2) => {
|
|
1519
1489
|
clearTimeout(timer);
|
|
1520
|
-
|
|
1490
|
+
resolve9({
|
|
1521
1491
|
stdout,
|
|
1522
|
-
stderr: stderr + "\n" +
|
|
1492
|
+
stderr: stderr + "\n" + err2.message,
|
|
1523
1493
|
exitCode: 1,
|
|
1524
1494
|
timedOut: false
|
|
1525
1495
|
});
|
|
@@ -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(chalk6.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(chalk6.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(chalk6.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(chalk6.green(this.format(message)), ...args);
|
|
4634
4619
|
}
|
|
4635
4620
|
}
|
|
4636
4621
|
/**
|
|
@@ -5573,6 +5558,64 @@ var SCORING_ADJUSTMENTS = [
|
|
|
5573
5558
|
categoryId: "command-injection",
|
|
5574
5559
|
lowerWeight: ["cli", "script"],
|
|
5575
5560
|
higherWeight: ["web-server", "api", "serverless"]
|
|
5561
|
+
},
|
|
5562
|
+
// Connection failure handling less relevant for CLI
|
|
5563
|
+
{
|
|
5564
|
+
categoryId: "connection-failure",
|
|
5565
|
+
lowerWeight: ["cli", "script", "library"],
|
|
5566
|
+
higherWeight: ["web-server", "api"]
|
|
5567
|
+
},
|
|
5568
|
+
// Memory bloat less relevant for short-lived processes
|
|
5569
|
+
{
|
|
5570
|
+
categoryId: "memory-bloat",
|
|
5571
|
+
skip: ["cli", "script", "serverless"],
|
|
5572
|
+
higherWeight: ["web-server", "desktop"]
|
|
5573
|
+
},
|
|
5574
|
+
// Data race less relevant for single-threaded CLI
|
|
5575
|
+
{
|
|
5576
|
+
categoryId: "data-race",
|
|
5577
|
+
lowerWeight: ["cli", "script"],
|
|
5578
|
+
higherWeight: ["web-server", "api"]
|
|
5579
|
+
},
|
|
5580
|
+
// Network partition not relevant for CLI
|
|
5581
|
+
{
|
|
5582
|
+
categoryId: "network-partition",
|
|
5583
|
+
skip: ["cli", "script", "library", "frontend-spa"],
|
|
5584
|
+
higherWeight: ["web-server", "api"]
|
|
5585
|
+
},
|
|
5586
|
+
// Packet loss not relevant for CLI
|
|
5587
|
+
{
|
|
5588
|
+
categoryId: "packet-loss",
|
|
5589
|
+
skip: ["cli", "script", "library", "frontend-spa"],
|
|
5590
|
+
higherWeight: ["web-server", "api"]
|
|
5591
|
+
},
|
|
5592
|
+
// Thundering herd not relevant for CLI
|
|
5593
|
+
{
|
|
5594
|
+
categoryId: "thundering-herd",
|
|
5595
|
+
skip: ["cli", "script", "library", "frontend-spa"],
|
|
5596
|
+
higherWeight: ["web-server", "api"]
|
|
5597
|
+
},
|
|
5598
|
+
// Network timeout less relevant for CLI
|
|
5599
|
+
{
|
|
5600
|
+
categoryId: "network-timeout",
|
|
5601
|
+
lowerWeight: ["cli", "script"],
|
|
5602
|
+
higherWeight: ["web-server", "api"]
|
|
5603
|
+
},
|
|
5604
|
+
// High latency not relevant for CLI
|
|
5605
|
+
{
|
|
5606
|
+
categoryId: "high-latency",
|
|
5607
|
+
skip: ["cli", "script", "library"],
|
|
5608
|
+
higherWeight: ["web-server", "api"]
|
|
5609
|
+
},
|
|
5610
|
+
// Encoding mismatch less relevant for CLI
|
|
5611
|
+
{
|
|
5612
|
+
categoryId: "encoding-mismatch",
|
|
5613
|
+
lowerWeight: ["cli", "script"]
|
|
5614
|
+
},
|
|
5615
|
+
// Precision loss less relevant for CLI
|
|
5616
|
+
{
|
|
5617
|
+
categoryId: "precision-loss",
|
|
5618
|
+
lowerWeight: ["cli", "script"]
|
|
5576
5619
|
}
|
|
5577
5620
|
];
|
|
5578
5621
|
async function detectProjectType(projectPath) {
|
|
@@ -5705,7 +5748,32 @@ function getProjectTypeDescription(type) {
|
|
|
5705
5748
|
}
|
|
5706
5749
|
init_errors();
|
|
5707
5750
|
init_result();
|
|
5708
|
-
|
|
5751
|
+
var SEVERITY_WEIGHTS = {
|
|
5752
|
+
critical: 4,
|
|
5753
|
+
high: 3,
|
|
5754
|
+
medium: 2,
|
|
5755
|
+
low: 1
|
|
5756
|
+
};
|
|
5757
|
+
var CONFIDENCE_WEIGHTS = {
|
|
5758
|
+
high: 1,
|
|
5759
|
+
medium: 0.3,
|
|
5760
|
+
low: 0.1
|
|
5761
|
+
};
|
|
5762
|
+
var PRIORITY_WEIGHTS = {
|
|
5763
|
+
P0: 3,
|
|
5764
|
+
P1: 2,
|
|
5765
|
+
P2: 1
|
|
5766
|
+
};
|
|
5767
|
+
var DEFAULT_TEST_PATTERNS = {
|
|
5768
|
+
python: ["test_*.py", "*_test.py", "tests/**/*.py", "test/**/*.py"],
|
|
5769
|
+
typescript: ["*.test.ts", "*.spec.ts", "__tests__/**/*.ts", "tests/**/*.ts"],
|
|
5770
|
+
javascript: ["*.test.js", "*.spec.js", "__tests__/**/*.js", "tests/**/*.js"],
|
|
5771
|
+
go: ["*_test.go"],
|
|
5772
|
+
java: ["*Test.java", "*Tests.java", "src/test/**/*.java"],
|
|
5773
|
+
rust: ["tests/**/*.rs"]
|
|
5774
|
+
};
|
|
5775
|
+
|
|
5776
|
+
// src/core/scanner/scanner.ts
|
|
5709
5777
|
var DEFAULT_OPTIONS = {
|
|
5710
5778
|
excludeDirs: [
|
|
5711
5779
|
// Package managers
|
|
@@ -5770,7 +5838,7 @@ var Scanner = class {
|
|
|
5770
5838
|
this.patternMatcher = new PatternMatcher();
|
|
5771
5839
|
}
|
|
5772
5840
|
/**
|
|
5773
|
-
* Scan a directory for
|
|
5841
|
+
* Scan a directory for security vulnerabilities
|
|
5774
5842
|
*
|
|
5775
5843
|
* @param targetDirectory Directory to scan
|
|
5776
5844
|
* @param options Scan options
|
|
@@ -5905,25 +5973,36 @@ var Scanner = class {
|
|
|
5905
5973
|
for (const domain of RISK_DOMAINS) {
|
|
5906
5974
|
domainScores.set(domain, 100);
|
|
5907
5975
|
}
|
|
5976
|
+
const gapsByCategory = /* @__PURE__ */ new Map();
|
|
5908
5977
|
for (const gap of gaps) {
|
|
5909
|
-
const severityWeight = SEVERITY_WEIGHTS[gap.severity];
|
|
5910
|
-
const confidenceWeight = CONFIDENCE_WEIGHTS[gap.confidence];
|
|
5911
|
-
const priorityWeight = PRIORITY_WEIGHTS[gap.priority];
|
|
5912
5978
|
const projectTypeWeight = getCategoryWeight(gap.categoryId, projectType);
|
|
5913
|
-
|
|
5914
|
-
const
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
|
|
5979
|
+
if (projectTypeWeight === 0) continue;
|
|
5980
|
+
const existing = gapsByCategory.get(gap.categoryId) ?? [];
|
|
5981
|
+
existing.push(gap);
|
|
5982
|
+
gapsByCategory.set(gap.categoryId, existing);
|
|
5983
|
+
}
|
|
5984
|
+
for (const [categoryId, categoryGaps] of gapsByCategory) {
|
|
5985
|
+
const representative = categoryGaps[0];
|
|
5986
|
+
const severityWeight = SEVERITY_WEIGHTS[representative.severity];
|
|
5987
|
+
const projectTypeWeight = getCategoryWeight(categoryId, projectType);
|
|
5988
|
+
const maxConfidence = categoryGaps.reduce((max, g) => {
|
|
5989
|
+
const w = CONFIDENCE_WEIGHTS[g.confidence];
|
|
5990
|
+
return w > max ? w : max;
|
|
5991
|
+
}, 0);
|
|
5992
|
+
const count = categoryGaps.length;
|
|
5993
|
+
const countFactor = Math.log2(count + 1);
|
|
5994
|
+
const MAX_CATEGORY_PENALTY = 15;
|
|
5995
|
+
const rawPenalty = severityWeight * maxConfidence * countFactor * projectTypeWeight;
|
|
5996
|
+
const penalty = Math.min(rawPenalty, MAX_CATEGORY_PENALTY);
|
|
5918
5997
|
baseScore -= penalty;
|
|
5919
|
-
const currentDomainScore = domainScores.get(
|
|
5920
|
-
domainScores.set(
|
|
5921
|
-
if (penalty >=
|
|
5922
|
-
const weightNote = projectTypeWeight !== 1 ? ` [${projectType}
|
|
5998
|
+
const currentDomainScore = domainScores.get(representative.domain) ?? 100;
|
|
5999
|
+
domainScores.set(representative.domain, Math.max(0, currentDomainScore - penalty * 1.5));
|
|
6000
|
+
if (penalty >= 3) {
|
|
6001
|
+
const weightNote = projectTypeWeight !== 1 ? ` [${projectType}: ${projectTypeWeight}x]` : "";
|
|
5923
6002
|
penalties.push({
|
|
5924
|
-
reason: `${
|
|
6003
|
+
reason: `${representative.severity} ${representative.domain}: ${representative.categoryName} (${count} findings)${weightNote}`,
|
|
5925
6004
|
points: Math.round(penalty),
|
|
5926
|
-
categoryId
|
|
6005
|
+
categoryId
|
|
5927
6006
|
});
|
|
5928
6007
|
}
|
|
5929
6008
|
}
|
|
@@ -6335,9 +6414,6 @@ function createScanner(categoryStore) {
|
|
|
6335
6414
|
return new Scanner(categoryStore);
|
|
6336
6415
|
}
|
|
6337
6416
|
|
|
6338
|
-
// src/core/scanner/index.ts
|
|
6339
|
-
init_types();
|
|
6340
|
-
|
|
6341
6417
|
// src/core/index.ts
|
|
6342
6418
|
var VERSION = "0.4.0";
|
|
6343
6419
|
|
|
@@ -6346,925 +6422,197 @@ init_errors();
|
|
|
6346
6422
|
init_result();
|
|
6347
6423
|
init_errors();
|
|
6348
6424
|
init_result();
|
|
6349
|
-
var
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6425
|
+
var SEVERITY_COLORS = {
|
|
6426
|
+
critical: chalk6.red.bold,
|
|
6427
|
+
high: chalk6.red,
|
|
6428
|
+
medium: chalk6.yellow,
|
|
6429
|
+
low: chalk6.gray
|
|
6354
6430
|
};
|
|
6355
|
-
var
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
}
|
|
6431
|
+
var PRIORITY_COLORS = {
|
|
6432
|
+
P0: chalk6.red.bold,
|
|
6433
|
+
P1: chalk6.yellow,
|
|
6434
|
+
P2: chalk6.gray
|
|
6360
6435
|
};
|
|
6361
|
-
var
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6436
|
+
var DOMAIN_COLORS = {
|
|
6437
|
+
security: chalk6.red,
|
|
6438
|
+
data: chalk6.blue,
|
|
6439
|
+
concurrency: chalk6.magenta,
|
|
6440
|
+
input: chalk6.cyan,
|
|
6441
|
+
resource: chalk6.yellow,
|
|
6442
|
+
reliability: chalk6.green,
|
|
6443
|
+
performance: chalk6.yellowBright,
|
|
6444
|
+
platform: chalk6.gray,
|
|
6445
|
+
business: chalk6.white,
|
|
6446
|
+
compliance: chalk6.blueBright
|
|
6447
|
+
};
|
|
6448
|
+
function formatTerminal(categories) {
|
|
6449
|
+
if (categories.length === 0) {
|
|
6450
|
+
return chalk6.yellow("No categories found matching the filters.");
|
|
6372
6451
|
}
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
if (this.hasUnclosedBlock(template, "if")) {
|
|
6382
|
-
errors.push(
|
|
6383
|
-
new TemplateSyntaxError("Unclosed {{#if}} block", {
|
|
6384
|
-
hint: "Every {{#if variable}} must have a matching {{/if}}"
|
|
6385
|
-
})
|
|
6386
|
-
);
|
|
6387
|
-
}
|
|
6388
|
-
if (this.hasUnclosedBlock(template, "each")) {
|
|
6389
|
-
errors.push(
|
|
6390
|
-
new TemplateSyntaxError("Unclosed {{#each}} block", {
|
|
6391
|
-
hint: "Every {{#each variable}} must have a matching {{/each}}"
|
|
6392
|
-
})
|
|
6393
|
-
);
|
|
6394
|
-
}
|
|
6395
|
-
if (this.hasUnclosedBlock(template, "unless")) {
|
|
6396
|
-
errors.push(
|
|
6397
|
-
new TemplateSyntaxError("Unclosed {{#unless}} block", {
|
|
6398
|
-
hint: "Every {{#unless variable}} must have a matching {{/unless}}"
|
|
6399
|
-
})
|
|
6400
|
-
);
|
|
6452
|
+
const lines = [];
|
|
6453
|
+
lines.push(chalk6.bold.underline(`Found ${categories.length} categories:
|
|
6454
|
+
`));
|
|
6455
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
6456
|
+
for (const cat of categories) {
|
|
6457
|
+
const domain = cat.domain;
|
|
6458
|
+
if (!byDomain.has(domain)) {
|
|
6459
|
+
byDomain.set(domain, []);
|
|
6401
6460
|
}
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6461
|
+
byDomain.get(domain).push(cat);
|
|
6462
|
+
}
|
|
6463
|
+
for (const [domain, domainCategories] of byDomain) {
|
|
6464
|
+
const domainColor = DOMAIN_COLORS[domain] ?? chalk6.white;
|
|
6465
|
+
lines.push(domainColor.bold(`
|
|
6466
|
+
${domain.toUpperCase()} (${domainCategories.length})`));
|
|
6467
|
+
lines.push(chalk6.gray("\u2500".repeat(40)));
|
|
6468
|
+
for (const cat of domainCategories) {
|
|
6469
|
+
const priorityColor = PRIORITY_COLORS[cat.priority];
|
|
6470
|
+
const severityColor = SEVERITY_COLORS[cat.severity];
|
|
6471
|
+
const priority = priorityColor(`[${cat.priority}]`);
|
|
6472
|
+
const severity = severityColor(`${cat.severity}`);
|
|
6473
|
+
const level = chalk6.cyan(`${cat.level}`);
|
|
6474
|
+
const name = chalk6.white.bold(cat.name);
|
|
6475
|
+
const id = chalk6.gray(`(${cat.id})`);
|
|
6476
|
+
lines.push(` ${priority} ${name} ${id}`);
|
|
6477
|
+
lines.push(` ${severity} | ${level}`);
|
|
6478
|
+
const desc = cat.description.length > 80 ? cat.description.slice(0, 77) + "..." : cat.description;
|
|
6479
|
+
lines.push(chalk6.gray(` ${desc}`));
|
|
6480
|
+
lines.push("");
|
|
6408
6481
|
}
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6482
|
+
}
|
|
6483
|
+
lines.push(chalk6.gray("\u2500".repeat(40)));
|
|
6484
|
+
lines.push(formatStats(categories));
|
|
6485
|
+
return lines.join("\n");
|
|
6486
|
+
}
|
|
6487
|
+
function formatStats(categories) {
|
|
6488
|
+
const stats = {
|
|
6489
|
+
P0: 0,
|
|
6490
|
+
P1: 0,
|
|
6491
|
+
P2: 0,
|
|
6492
|
+
critical: 0,
|
|
6493
|
+
high: 0,
|
|
6494
|
+
medium: 0,
|
|
6495
|
+
low: 0
|
|
6496
|
+
};
|
|
6497
|
+
for (const cat of categories) {
|
|
6498
|
+
stats[cat.priority]++;
|
|
6499
|
+
stats[cat.severity]++;
|
|
6500
|
+
}
|
|
6501
|
+
const parts = [];
|
|
6502
|
+
if (stats.P0 > 0) parts.push(PRIORITY_COLORS.P0(`${stats.P0} P0`));
|
|
6503
|
+
if (stats.P1 > 0) parts.push(PRIORITY_COLORS.P1(`${stats.P1} P1`));
|
|
6504
|
+
if (stats.P2 > 0) parts.push(PRIORITY_COLORS.P2(`${stats.P2} P2`));
|
|
6505
|
+
parts.push(chalk6.gray("|"));
|
|
6506
|
+
if (stats.critical > 0) parts.push(SEVERITY_COLORS.critical(`${stats.critical} critical`));
|
|
6507
|
+
if (stats.high > 0) parts.push(SEVERITY_COLORS.high(`${stats.high} high`));
|
|
6508
|
+
if (stats.medium > 0) parts.push(SEVERITY_COLORS.medium(`${stats.medium} medium`));
|
|
6509
|
+
if (stats.low > 0) parts.push(SEVERITY_COLORS.low(`${stats.low} low`));
|
|
6510
|
+
return parts.join(" ");
|
|
6511
|
+
}
|
|
6512
|
+
function formatJson(categories) {
|
|
6513
|
+
return JSON.stringify(categories, null, 2);
|
|
6514
|
+
}
|
|
6515
|
+
function formatMarkdown(categories) {
|
|
6516
|
+
if (categories.length === 0) {
|
|
6517
|
+
return "_No categories found matching the filters._";
|
|
6518
|
+
}
|
|
6519
|
+
const lines = [];
|
|
6520
|
+
lines.push(`# Categories (${categories.length})
|
|
6521
|
+
`);
|
|
6522
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
6523
|
+
for (const cat of categories) {
|
|
6524
|
+
const domain = cat.domain;
|
|
6525
|
+
if (!byDomain.has(domain)) {
|
|
6526
|
+
byDomain.set(domain, []);
|
|
6415
6527
|
}
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6528
|
+
byDomain.get(domain).push(cat);
|
|
6529
|
+
}
|
|
6530
|
+
for (const [domain, domainCategories] of byDomain) {
|
|
6531
|
+
lines.push(`
|
|
6532
|
+
## ${domain.charAt(0).toUpperCase() + domain.slice(1)} (${domainCategories.length})
|
|
6533
|
+
`);
|
|
6534
|
+
for (const cat of domainCategories) {
|
|
6535
|
+
lines.push(`### ${cat.name}`);
|
|
6536
|
+
lines.push(`- **ID**: \`${cat.id}\``);
|
|
6537
|
+
lines.push(`- **Priority**: ${cat.priority}`);
|
|
6538
|
+
lines.push(`- **Severity**: ${cat.severity}`);
|
|
6539
|
+
lines.push(`- **Level**: ${cat.level}`);
|
|
6540
|
+
lines.push(`
|
|
6541
|
+
${cat.description}
|
|
6542
|
+
`);
|
|
6422
6543
|
}
|
|
6423
|
-
const mismatchErrors = this.checkBlockNesting(template);
|
|
6424
|
-
errors.push(...mismatchErrors);
|
|
6425
|
-
return {
|
|
6426
|
-
valid: errors.length === 0,
|
|
6427
|
-
errors
|
|
6428
|
-
};
|
|
6429
6544
|
}
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6545
|
+
return lines.join("\n");
|
|
6546
|
+
}
|
|
6547
|
+
function formatCategories(categories, format) {
|
|
6548
|
+
switch (format) {
|
|
6549
|
+
case "json":
|
|
6550
|
+
return formatJson(categories);
|
|
6551
|
+
case "markdown":
|
|
6552
|
+
return formatMarkdown(categories);
|
|
6553
|
+
case "terminal":
|
|
6554
|
+
default:
|
|
6555
|
+
return formatTerminal(categories);
|
|
6439
6556
|
}
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6557
|
+
}
|
|
6558
|
+
function isValidOutputFormat(format) {
|
|
6559
|
+
return ["terminal", "json", "markdown"].includes(format);
|
|
6560
|
+
}
|
|
6561
|
+
function formatError(error) {
|
|
6562
|
+
return chalk6.red(`Error: ${error.message}`);
|
|
6563
|
+
}
|
|
6564
|
+
|
|
6565
|
+
// src/ai/service.ts
|
|
6566
|
+
var DEFAULT_CONFIG = {
|
|
6567
|
+
provider: "anthropic",
|
|
6568
|
+
apiKey: "",
|
|
6569
|
+
model: "claude-sonnet-4-20250514",
|
|
6570
|
+
maxTokens: 1024,
|
|
6571
|
+
temperature: 0.3,
|
|
6572
|
+
timeoutMs: 3e4
|
|
6573
|
+
};
|
|
6574
|
+
var PROVIDER_MODELS = {
|
|
6575
|
+
anthropic: "claude-sonnet-4-20250514",
|
|
6576
|
+
openai: "gpt-4o",
|
|
6577
|
+
mock: "mock-model"
|
|
6578
|
+
};
|
|
6579
|
+
var PROVIDER_ENDPOINTS = {
|
|
6580
|
+
anthropic: "https://api.anthropic.com/v1/messages",
|
|
6581
|
+
openai: "https://api.openai.com/v1/chat/completions"};
|
|
6582
|
+
var AIService = class {
|
|
6583
|
+
config;
|
|
6584
|
+
constructor(config2 = {}) {
|
|
6585
|
+
this.config = {
|
|
6586
|
+
...DEFAULT_CONFIG,
|
|
6587
|
+
...config2,
|
|
6588
|
+
apiKey: config2.apiKey ?? this.getApiKeyFromEnv(config2.provider ?? "anthropic"),
|
|
6589
|
+
model: config2.model ?? PROVIDER_MODELS[config2.provider ?? "anthropic"]
|
|
6590
|
+
};
|
|
6449
6591
|
}
|
|
6450
6592
|
/**
|
|
6451
|
-
*
|
|
6593
|
+
* Get API key from environment variable
|
|
6594
|
+
* For config file support, use the sync version below
|
|
6452
6595
|
*/
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
const
|
|
6456
|
-
const
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
const tag = match[1] ?? "";
|
|
6460
|
-
if (tag.startsWith("#")) {
|
|
6461
|
-
const type = tag.slice(1).split(/\s/)[0] ?? "";
|
|
6462
|
-
stack.push({ type, index: match.index });
|
|
6463
|
-
} else if (tag.startsWith("/")) {
|
|
6464
|
-
const type = tag.slice(1);
|
|
6465
|
-
const last = stack.pop();
|
|
6466
|
-
if (!last) {
|
|
6467
|
-
continue;
|
|
6468
|
-
}
|
|
6469
|
-
if (last.type !== type) {
|
|
6470
|
-
errors.push(
|
|
6471
|
-
new TemplateSyntaxError(`Mismatched block: opened {{#${last.type}}} but closed with {{/${type}}}`, {
|
|
6472
|
-
openedAt: last.index,
|
|
6473
|
-
closedAt: match.index
|
|
6474
|
-
})
|
|
6475
|
-
);
|
|
6476
|
-
}
|
|
6477
|
-
}
|
|
6596
|
+
getApiKeyFromEnv(provider) {
|
|
6597
|
+
if (provider === "mock") return "mock-key";
|
|
6598
|
+
const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
|
|
6599
|
+
const envValue = process.env[envVar];
|
|
6600
|
+
if (envValue !== void 0 && envValue.length > 0) {
|
|
6601
|
+
return envValue;
|
|
6478
6602
|
}
|
|
6479
|
-
return
|
|
6603
|
+
return this.getApiKeyFromConfig(provider);
|
|
6480
6604
|
}
|
|
6481
6605
|
/**
|
|
6482
|
-
*
|
|
6483
|
-
*
|
|
6484
|
-
* @param template Template string to parse
|
|
6485
|
-
* @returns Array of parsed placeholders
|
|
6486
|
-
*/
|
|
6487
|
-
parsePlaceholders(template) {
|
|
6488
|
-
const placeholders = [];
|
|
6489
|
-
const regex = this.getPlaceholderRegex();
|
|
6490
|
-
let match;
|
|
6491
|
-
regex.lastIndex = 0;
|
|
6492
|
-
while ((match = regex.exec(template)) !== null) {
|
|
6493
|
-
const name = match[1] ?? "";
|
|
6494
|
-
const pathSegments = name.split(".");
|
|
6495
|
-
placeholders.push({
|
|
6496
|
-
match: match[0],
|
|
6497
|
-
name,
|
|
6498
|
-
startIndex: match.index,
|
|
6499
|
-
endIndex: match.index + match[0].length,
|
|
6500
|
-
isNestedPath: pathSegments.length > 1,
|
|
6501
|
-
pathSegments
|
|
6502
|
-
});
|
|
6503
|
-
}
|
|
6504
|
-
return placeholders;
|
|
6505
|
-
}
|
|
6506
|
-
/**
|
|
6507
|
-
* Parse conditional blocks from template
|
|
6508
|
-
*
|
|
6509
|
-
* @param template Template string to parse
|
|
6510
|
-
* @returns Array of parsed conditionals
|
|
6511
|
-
*/
|
|
6512
|
-
parseConditionals(template) {
|
|
6513
|
-
const conditionals = [];
|
|
6514
|
-
const regex = new RegExp(CONDITIONAL_REGEX.source, "g");
|
|
6515
|
-
let match;
|
|
6516
|
-
while ((match = regex.exec(template)) !== null) {
|
|
6517
|
-
const falseBranch = match[3];
|
|
6518
|
-
conditionals.push({
|
|
6519
|
-
match: match[0],
|
|
6520
|
-
variable: match[1] ?? "",
|
|
6521
|
-
trueBranch: match[2] ?? "",
|
|
6522
|
-
...falseBranch !== void 0 && { falseBranch },
|
|
6523
|
-
startIndex: match.index,
|
|
6524
|
-
endIndex: match.index + match[0].length
|
|
6525
|
-
});
|
|
6526
|
-
}
|
|
6527
|
-
return conditionals;
|
|
6528
|
-
}
|
|
6529
|
-
/**
|
|
6530
|
-
* Parse loop blocks from template
|
|
6531
|
-
*
|
|
6532
|
-
* @param template Template string to parse
|
|
6533
|
-
* @returns Array of parsed loops
|
|
6534
|
-
*/
|
|
6535
|
-
parseLoops(template) {
|
|
6536
|
-
const loops = [];
|
|
6537
|
-
const regex = new RegExp(EACH_REGEX.source, "g");
|
|
6538
|
-
let match;
|
|
6539
|
-
while ((match = regex.exec(template)) !== null) {
|
|
6540
|
-
loops.push({
|
|
6541
|
-
match: match[0],
|
|
6542
|
-
variable: match[1] ?? "",
|
|
6543
|
-
body: match[2] ?? "",
|
|
6544
|
-
startIndex: match.index,
|
|
6545
|
-
endIndex: match.index + match[0].length
|
|
6546
|
-
});
|
|
6547
|
-
}
|
|
6548
|
-
return loops;
|
|
6549
|
-
}
|
|
6550
|
-
/**
|
|
6551
|
-
* Get unique variable names from template (including nested paths)
|
|
6552
|
-
* Excludes loop-internal variables like {{this}}, {{@index}}, and item properties
|
|
6553
|
-
*
|
|
6554
|
-
* @param template Template string to analyze
|
|
6555
|
-
* @param excludeLoopInternal Whether to exclude variables inside loop blocks
|
|
6556
|
-
* @returns Unique variable names found in template
|
|
6557
|
-
*/
|
|
6558
|
-
getVariableNames(template, excludeLoopInternal = true) {
|
|
6559
|
-
let processedTemplate = template;
|
|
6560
|
-
if (excludeLoopInternal) {
|
|
6561
|
-
processedTemplate = processedTemplate.replace(EACH_REGEX, (match, variable) => {
|
|
6562
|
-
return `{{${variable}}}`;
|
|
6563
|
-
});
|
|
6564
|
-
}
|
|
6565
|
-
const placeholders = this.parsePlaceholders(processedTemplate);
|
|
6566
|
-
const names = /* @__PURE__ */ new Set();
|
|
6567
|
-
const loopSpecialVars = /* @__PURE__ */ new Set(["this", "@index", "@first", "@last"]);
|
|
6568
|
-
for (const p of placeholders) {
|
|
6569
|
-
if (loopSpecialVars.has(p.name)) {
|
|
6570
|
-
continue;
|
|
6571
|
-
}
|
|
6572
|
-
names.add(p.name);
|
|
6573
|
-
if (p.isNestedPath && p.pathSegments[0]) {
|
|
6574
|
-
names.add(p.pathSegments[0]);
|
|
6575
|
-
}
|
|
6576
|
-
}
|
|
6577
|
-
const conditionals = this.parseConditionals(processedTemplate);
|
|
6578
|
-
for (const c of conditionals) {
|
|
6579
|
-
names.add(c.variable);
|
|
6580
|
-
const root = c.variable.split(".")[0];
|
|
6581
|
-
if (root && root !== c.variable) {
|
|
6582
|
-
names.add(root);
|
|
6583
|
-
}
|
|
6584
|
-
}
|
|
6585
|
-
const loops = this.parseLoops(template);
|
|
6586
|
-
for (const l of loops) {
|
|
6587
|
-
names.add(l.variable);
|
|
6588
|
-
const root = l.variable.split(".")[0];
|
|
6589
|
-
if (root && root !== l.variable) {
|
|
6590
|
-
names.add(root);
|
|
6591
|
-
}
|
|
6592
|
-
}
|
|
6593
|
-
return [...names];
|
|
6594
|
-
}
|
|
6595
|
-
/**
|
|
6596
|
-
* Get value from object using dot notation path
|
|
6597
|
-
*
|
|
6598
|
-
* @param obj Object to traverse
|
|
6599
|
-
* @param path Dot-separated path (e.g., "user.address.city")
|
|
6600
|
-
* @returns Value at path or undefined
|
|
6601
|
-
*/
|
|
6602
|
-
getNestedValue(obj, path2) {
|
|
6603
|
-
const segments = path2.split(".");
|
|
6604
|
-
let current = obj;
|
|
6605
|
-
for (const segment of segments) {
|
|
6606
|
-
if (current === null || current === void 0) {
|
|
6607
|
-
return void 0;
|
|
6608
|
-
}
|
|
6609
|
-
if (typeof current !== "object") {
|
|
6610
|
-
return void 0;
|
|
6611
|
-
}
|
|
6612
|
-
current = current[segment];
|
|
6613
|
-
}
|
|
6614
|
-
return current;
|
|
6615
|
-
}
|
|
6616
|
-
/**
|
|
6617
|
-
* Evaluate if a value is truthy for conditional blocks
|
|
6618
|
-
*
|
|
6619
|
-
* @param value Value to evaluate
|
|
6620
|
-
* @returns True if value is truthy
|
|
6621
|
-
*/
|
|
6622
|
-
isTruthy(value) {
|
|
6623
|
-
if (value === null || value === void 0) {
|
|
6624
|
-
return false;
|
|
6625
|
-
}
|
|
6626
|
-
if (typeof value === "boolean") {
|
|
6627
|
-
return value;
|
|
6628
|
-
}
|
|
6629
|
-
if (typeof value === "number") {
|
|
6630
|
-
return value !== 0;
|
|
6631
|
-
}
|
|
6632
|
-
if (typeof value === "string") {
|
|
6633
|
-
return value.length > 0;
|
|
6634
|
-
}
|
|
6635
|
-
if (Array.isArray(value)) {
|
|
6636
|
-
return value.length > 0;
|
|
6637
|
-
}
|
|
6638
|
-
if (typeof value === "object") {
|
|
6639
|
-
return Object.keys(value).length > 0;
|
|
6640
|
-
}
|
|
6641
|
-
return Boolean(value);
|
|
6642
|
-
}
|
|
6643
|
-
/**
|
|
6644
|
-
* Process conditional blocks in template
|
|
6645
|
-
*
|
|
6646
|
-
* @param template Template string
|
|
6647
|
-
* @param values Variable values
|
|
6648
|
-
* @returns Processed template with conditionals resolved
|
|
6649
|
-
*/
|
|
6650
|
-
processConditionals(template, values) {
|
|
6651
|
-
let result = template;
|
|
6652
|
-
result = result.replace(CONDITIONAL_REGEX, (match, variable, trueBranch, falseBranch) => {
|
|
6653
|
-
const value = this.getNestedValue(values, variable);
|
|
6654
|
-
const condition = this.isTruthy(value);
|
|
6655
|
-
return condition ? trueBranch : falseBranch ?? "";
|
|
6656
|
-
});
|
|
6657
|
-
result = result.replace(UNLESS_REGEX, (match, variable, content) => {
|
|
6658
|
-
const value = this.getNestedValue(values, variable);
|
|
6659
|
-
const condition = this.isTruthy(value);
|
|
6660
|
-
return condition ? "" : content;
|
|
6661
|
-
});
|
|
6662
|
-
return result;
|
|
6663
|
-
}
|
|
6664
|
-
/**
|
|
6665
|
-
* Process loop blocks in template
|
|
6666
|
-
*
|
|
6667
|
-
* @param template Template string
|
|
6668
|
-
* @param values Variable values
|
|
6669
|
-
* @returns Processed template with loops expanded
|
|
6670
|
-
*/
|
|
6671
|
-
processLoops(template, values) {
|
|
6672
|
-
let result = template;
|
|
6673
|
-
result = result.replace(EACH_REGEX, (match, variable, body) => {
|
|
6674
|
-
const arrayValue = this.getNestedValue(values, variable);
|
|
6675
|
-
if (!Array.isArray(arrayValue)) {
|
|
6676
|
-
return "";
|
|
6677
|
-
}
|
|
6678
|
-
const expanded = [];
|
|
6679
|
-
for (let i = 0; i < arrayValue.length; i++) {
|
|
6680
|
-
const item = arrayValue[i];
|
|
6681
|
-
let iterationBody = body;
|
|
6682
|
-
iterationBody = iterationBody.replace(/\{\{this\}\}/g, this.stringify(item));
|
|
6683
|
-
iterationBody = iterationBody.replace(/\{\{@index\}\}/g, String(i));
|
|
6684
|
-
iterationBody = iterationBody.replace(/\{\{@first\}\}/g, String(i === 0));
|
|
6685
|
-
iterationBody = iterationBody.replace(/\{\{@last\}\}/g, String(i === arrayValue.length - 1));
|
|
6686
|
-
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
6687
|
-
const itemObj = item;
|
|
6688
|
-
iterationBody = this.processConditionals(iterationBody, itemObj);
|
|
6689
|
-
for (const [key, value] of Object.entries(itemObj)) {
|
|
6690
|
-
const propRegex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
6691
|
-
iterationBody = iterationBody.replace(propRegex, this.stringify(value));
|
|
6692
|
-
}
|
|
6693
|
-
}
|
|
6694
|
-
expanded.push(iterationBody);
|
|
6695
|
-
}
|
|
6696
|
-
return expanded.join("");
|
|
6697
|
-
});
|
|
6698
|
-
return result;
|
|
6699
|
-
}
|
|
6700
|
-
/**
|
|
6701
|
-
* Validate provided variables against template requirements
|
|
6702
|
-
*
|
|
6703
|
-
* @param template Test template with variable definitions
|
|
6704
|
-
* @param values Provided variable values
|
|
6705
|
-
* @returns Validation result
|
|
6706
|
-
*/
|
|
6707
|
-
validateVariables(template, values) {
|
|
6708
|
-
const results = [];
|
|
6709
|
-
const missingRequired = [];
|
|
6710
|
-
const unknownVariables = [];
|
|
6711
|
-
const typeErrors = [];
|
|
6712
|
-
const usedInTemplate = new Set(this.getVariableNames(template.template));
|
|
6713
|
-
const definedVariables = /* @__PURE__ */ new Map();
|
|
6714
|
-
for (const v of template.variables) {
|
|
6715
|
-
definedVariables.set(v.name, v);
|
|
6716
|
-
}
|
|
6717
|
-
for (const variable of template.variables) {
|
|
6718
|
-
const result = {
|
|
6719
|
-
name: variable.name,
|
|
6720
|
-
valid: true,
|
|
6721
|
-
errors: []
|
|
6722
|
-
};
|
|
6723
|
-
const value = values[variable.name];
|
|
6724
|
-
const hasValue = variable.name in values;
|
|
6725
|
-
if (variable.required && !hasValue && variable.defaultValue === void 0) {
|
|
6726
|
-
result.valid = false;
|
|
6727
|
-
result.errors.push(`Required variable '${variable.name}' is missing`);
|
|
6728
|
-
missingRequired.push(variable.name);
|
|
6729
|
-
}
|
|
6730
|
-
if (hasValue && value !== void 0 && value !== null) {
|
|
6731
|
-
const typeError = this.checkType(variable.name, value, variable.type);
|
|
6732
|
-
if (typeError) {
|
|
6733
|
-
result.valid = false;
|
|
6734
|
-
result.errors.push(typeError);
|
|
6735
|
-
typeErrors.push(typeError);
|
|
6736
|
-
}
|
|
6737
|
-
}
|
|
6738
|
-
results.push(result);
|
|
6739
|
-
}
|
|
6740
|
-
const providedNames = Object.keys(values);
|
|
6741
|
-
for (const name of providedNames) {
|
|
6742
|
-
if (!definedVariables.has(name)) {
|
|
6743
|
-
const isUsed = [...usedInTemplate].some((used) => used === name || used.startsWith(name + "."));
|
|
6744
|
-
if (isUsed) {
|
|
6745
|
-
results.push({
|
|
6746
|
-
name,
|
|
6747
|
-
valid: true,
|
|
6748
|
-
errors: [`Variable '${name}' is used in template but not formally defined`]
|
|
6749
|
-
});
|
|
6750
|
-
} else {
|
|
6751
|
-
unknownVariables.push(name);
|
|
6752
|
-
}
|
|
6753
|
-
}
|
|
6754
|
-
}
|
|
6755
|
-
for (const placeholder of usedInTemplate) {
|
|
6756
|
-
const defined = definedVariables.get(placeholder);
|
|
6757
|
-
const rootVar = placeholder.split(".")[0] ?? placeholder;
|
|
6758
|
-
const hasValue = placeholder in values || rootVar in values;
|
|
6759
|
-
if (!hasValue && !defined?.defaultValue) {
|
|
6760
|
-
if (!defined) {
|
|
6761
|
-
if (!placeholder.includes(".")) {
|
|
6762
|
-
missingRequired.push(placeholder);
|
|
6763
|
-
} else if (!(rootVar in values)) {
|
|
6764
|
-
missingRequired.push(rootVar);
|
|
6765
|
-
}
|
|
6766
|
-
}
|
|
6767
|
-
}
|
|
6768
|
-
}
|
|
6769
|
-
const valid = missingRequired.length === 0 && typeErrors.length === 0 && (unknownVariables.length === 0 || !this.options.strict);
|
|
6770
|
-
return {
|
|
6771
|
-
valid,
|
|
6772
|
-
results,
|
|
6773
|
-
missingRequired: [...new Set(missingRequired)],
|
|
6774
|
-
unknownVariables,
|
|
6775
|
-
typeErrors
|
|
6776
|
-
};
|
|
6777
|
-
}
|
|
6778
|
-
/**
|
|
6779
|
-
* Check if a value matches the expected type
|
|
6780
|
-
*/
|
|
6781
|
-
checkType(name, value, expectedType) {
|
|
6782
|
-
const actualType = this.getValueType(value);
|
|
6783
|
-
if (actualType !== expectedType) {
|
|
6784
|
-
return `Variable '${name}' expected type '${expectedType}' but got '${actualType}'`;
|
|
6785
|
-
}
|
|
6786
|
-
return null;
|
|
6787
|
-
}
|
|
6788
|
-
/**
|
|
6789
|
-
* Determine the VariableType of a value
|
|
6790
|
-
*/
|
|
6791
|
-
getValueType(value) {
|
|
6792
|
-
if (value === null || value === void 0) {
|
|
6793
|
-
return "string";
|
|
6794
|
-
}
|
|
6795
|
-
if (Array.isArray(value)) {
|
|
6796
|
-
return "array";
|
|
6797
|
-
}
|
|
6798
|
-
if (typeof value === "object") {
|
|
6799
|
-
return "object";
|
|
6800
|
-
}
|
|
6801
|
-
if (typeof value === "boolean") {
|
|
6802
|
-
return "boolean";
|
|
6803
|
-
}
|
|
6804
|
-
if (typeof value === "number") {
|
|
6805
|
-
return "number";
|
|
6806
|
-
}
|
|
6807
|
-
return "string";
|
|
6808
|
-
}
|
|
6809
|
-
/**
|
|
6810
|
-
* Substitute variables in a template string
|
|
6811
|
-
*
|
|
6812
|
-
* @param template Template string with placeholders
|
|
6813
|
-
* @param values Variable values to substitute
|
|
6814
|
-
* @param variableDefs Optional variable definitions for defaults
|
|
6815
|
-
* @returns Substitution result
|
|
6816
|
-
*/
|
|
6817
|
-
substituteVariables(template, values, variableDefs) {
|
|
6818
|
-
const substituted = [];
|
|
6819
|
-
const unresolved = [];
|
|
6820
|
-
const resolvedValues = /* @__PURE__ */ new Map();
|
|
6821
|
-
if (variableDefs) {
|
|
6822
|
-
for (const def of variableDefs) {
|
|
6823
|
-
if (def.defaultValue !== void 0) {
|
|
6824
|
-
resolvedValues.set(def.name, def.defaultValue);
|
|
6825
|
-
}
|
|
6826
|
-
}
|
|
6827
|
-
}
|
|
6828
|
-
for (const [key, value] of Object.entries(values)) {
|
|
6829
|
-
if (value === void 0 || value === null) {
|
|
6830
|
-
resolvedValues.set(key, null);
|
|
6831
|
-
} else {
|
|
6832
|
-
resolvedValues.set(key, value);
|
|
6833
|
-
}
|
|
6834
|
-
}
|
|
6835
|
-
const combinedValues = {};
|
|
6836
|
-
for (const [key, value] of resolvedValues) {
|
|
6837
|
-
combinedValues[key] = value;
|
|
6838
|
-
}
|
|
6839
|
-
const regex = this.getPlaceholderRegex();
|
|
6840
|
-
const content = template.replace(regex, (match, name) => {
|
|
6841
|
-
let value;
|
|
6842
|
-
let hasValue = false;
|
|
6843
|
-
if (name.includes(".")) {
|
|
6844
|
-
value = this.getNestedValue(combinedValues, name);
|
|
6845
|
-
hasValue = value !== void 0;
|
|
6846
|
-
} else {
|
|
6847
|
-
hasValue = resolvedValues.has(name);
|
|
6848
|
-
value = resolvedValues.get(name);
|
|
6849
|
-
}
|
|
6850
|
-
if (hasValue) {
|
|
6851
|
-
substituted.push(name);
|
|
6852
|
-
return this.stringify(value);
|
|
6853
|
-
}
|
|
6854
|
-
if (this.options.allowUnresolved) {
|
|
6855
|
-
unresolved.push(name);
|
|
6856
|
-
return match;
|
|
6857
|
-
}
|
|
6858
|
-
unresolved.push(name);
|
|
6859
|
-
return match;
|
|
6860
|
-
});
|
|
6861
|
-
return {
|
|
6862
|
-
content,
|
|
6863
|
-
substituted: [...new Set(substituted)],
|
|
6864
|
-
unresolved: [...new Set(unresolved)]
|
|
6865
|
-
};
|
|
6866
|
-
}
|
|
6867
|
-
/**
|
|
6868
|
-
* Convert a value to string for template substitution
|
|
6869
|
-
*/
|
|
6870
|
-
stringify(value) {
|
|
6871
|
-
if (value === null || value === void 0) {
|
|
6872
|
-
return "";
|
|
6873
|
-
}
|
|
6874
|
-
if (typeof value === "string") {
|
|
6875
|
-
return value;
|
|
6876
|
-
}
|
|
6877
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
6878
|
-
return String(value);
|
|
6879
|
-
}
|
|
6880
|
-
if (Array.isArray(value)) {
|
|
6881
|
-
return JSON.stringify(value);
|
|
6882
|
-
}
|
|
6883
|
-
if (typeof value === "object") {
|
|
6884
|
-
return JSON.stringify(value);
|
|
6885
|
-
}
|
|
6886
|
-
return String(value);
|
|
6887
|
-
}
|
|
6888
|
-
/**
|
|
6889
|
-
* Process all template features: conditionals, loops, then variable substitution
|
|
6890
|
-
*
|
|
6891
|
-
* @param template Template string
|
|
6892
|
-
* @param values Variable values
|
|
6893
|
-
* @param variableDefs Optional variable definitions
|
|
6894
|
-
* @returns Processed content with all features applied
|
|
6895
|
-
*/
|
|
6896
|
-
processTemplate(template, values, variableDefs) {
|
|
6897
|
-
const syntaxResult = this.validateSyntax(template);
|
|
6898
|
-
if (!syntaxResult.valid && this.options.strict) {
|
|
6899
|
-
return err(syntaxResult.errors[0] ?? new TemplateSyntaxError("Unknown syntax error"));
|
|
6900
|
-
}
|
|
6901
|
-
const combinedValues = {};
|
|
6902
|
-
if (variableDefs) {
|
|
6903
|
-
for (const def of variableDefs) {
|
|
6904
|
-
if (def.defaultValue !== void 0) {
|
|
6905
|
-
combinedValues[def.name] = def.defaultValue;
|
|
6906
|
-
}
|
|
6907
|
-
}
|
|
6908
|
-
}
|
|
6909
|
-
for (const [key, value] of Object.entries(values)) {
|
|
6910
|
-
combinedValues[key] = value;
|
|
6911
|
-
}
|
|
6912
|
-
let processed = template;
|
|
6913
|
-
processed = this.processLoops(processed, combinedValues);
|
|
6914
|
-
processed = this.processConditionals(processed, combinedValues);
|
|
6915
|
-
const result = this.substituteVariables(processed, combinedValues, variableDefs);
|
|
6916
|
-
return ok(result);
|
|
6917
|
-
}
|
|
6918
|
-
/**
|
|
6919
|
-
* Render a complete test template with variable substitution
|
|
6920
|
-
*
|
|
6921
|
-
* @param template Test template to render
|
|
6922
|
-
* @param values Variable values to substitute
|
|
6923
|
-
* @param options Optional render options
|
|
6924
|
-
* @returns Render result or error
|
|
6925
|
-
*/
|
|
6926
|
-
renderTemplate(template, values, options) {
|
|
6927
|
-
const mergedOptions = { ...this.options, ...options };
|
|
6928
|
-
const syntaxResult = this.validateSyntax(template.template);
|
|
6929
|
-
if (!syntaxResult.valid && mergedOptions.strict) {
|
|
6930
|
-
return err(syntaxResult.errors[0] ?? new TemplateSyntaxError("Unknown syntax error"));
|
|
6931
|
-
}
|
|
6932
|
-
const validation = this.validateVariables(template, values);
|
|
6933
|
-
if (!validation.valid && mergedOptions.strict) {
|
|
6934
|
-
const errors = [];
|
|
6935
|
-
if (validation.missingRequired.length > 0) {
|
|
6936
|
-
errors.push(`Missing required variables: ${validation.missingRequired.join(", ")}`);
|
|
6937
|
-
}
|
|
6938
|
-
if (validation.typeErrors.length > 0) {
|
|
6939
|
-
errors.push(...validation.typeErrors);
|
|
6940
|
-
}
|
|
6941
|
-
if (validation.unknownVariables.length > 0) {
|
|
6942
|
-
errors.push(`Unknown variables: ${validation.unknownVariables.join(", ")}`);
|
|
6943
|
-
}
|
|
6944
|
-
return err(
|
|
6945
|
-
new TemplateRenderError("Template validation failed", {
|
|
6946
|
-
errors,
|
|
6947
|
-
validation
|
|
6948
|
-
})
|
|
6949
|
-
);
|
|
6950
|
-
}
|
|
6951
|
-
const processResult = this.processTemplate(template.template, values, template.variables);
|
|
6952
|
-
if (!processResult.success) {
|
|
6953
|
-
return processResult;
|
|
6954
|
-
}
|
|
6955
|
-
const { content, substituted, unresolved } = processResult.data;
|
|
6956
|
-
if (unresolved.length > 0 && !mergedOptions.allowUnresolved && mergedOptions.strict) {
|
|
6957
|
-
return err(
|
|
6958
|
-
new TemplateRenderError("Unresolved template variables", {
|
|
6959
|
-
unresolved
|
|
6960
|
-
})
|
|
6961
|
-
);
|
|
6962
|
-
}
|
|
6963
|
-
return ok({
|
|
6964
|
-
content,
|
|
6965
|
-
substituted,
|
|
6966
|
-
unresolved,
|
|
6967
|
-
imports: template.imports ?? [],
|
|
6968
|
-
fixtures: template.fixtures ?? []
|
|
6969
|
-
});
|
|
6970
|
-
}
|
|
6971
|
-
/**
|
|
6972
|
-
* Render multiple templates at once
|
|
6973
|
-
*
|
|
6974
|
-
* @param templates Array of templates to render
|
|
6975
|
-
* @param values Variable values (applied to all templates)
|
|
6976
|
-
* @returns Array of render results
|
|
6977
|
-
*/
|
|
6978
|
-
renderTemplates(templates, values) {
|
|
6979
|
-
const results = [];
|
|
6980
|
-
for (const template of templates) {
|
|
6981
|
-
const result = this.renderTemplate(template, values);
|
|
6982
|
-
if (!result.success) {
|
|
6983
|
-
return err(
|
|
6984
|
-
new TemplateRenderError(`Failed to render template '${template.id}'`, {
|
|
6985
|
-
templateId: template.id,
|
|
6986
|
-
originalError: result.error.message
|
|
6987
|
-
})
|
|
6988
|
-
);
|
|
6989
|
-
}
|
|
6990
|
-
results.push(result.data);
|
|
6991
|
-
}
|
|
6992
|
-
return ok(results);
|
|
6993
|
-
}
|
|
6994
|
-
/**
|
|
6995
|
-
* Check if a template string contains nested placeholders
|
|
6996
|
-
* (e.g., {{outer{{inner}}}})
|
|
6997
|
-
*
|
|
6998
|
-
* @param template Template string to check
|
|
6999
|
-
* @returns True if nested placeholders detected
|
|
7000
|
-
*/
|
|
7001
|
-
hasNestedPlaceholders(template) {
|
|
7002
|
-
const nestedPattern = /\{\{[^{}]*\{\{/;
|
|
7003
|
-
return nestedPattern.test(template);
|
|
7004
|
-
}
|
|
7005
|
-
/**
|
|
7006
|
-
* Extract all imports from rendered templates
|
|
7007
|
-
*
|
|
7008
|
-
* @param results Array of render results
|
|
7009
|
-
* @returns Deduplicated array of imports
|
|
7010
|
-
*/
|
|
7011
|
-
collectImports(results) {
|
|
7012
|
-
const imports = /* @__PURE__ */ new Set();
|
|
7013
|
-
for (const result of results) {
|
|
7014
|
-
for (const imp of result.imports) {
|
|
7015
|
-
imports.add(imp);
|
|
7016
|
-
}
|
|
7017
|
-
}
|
|
7018
|
-
return [...imports];
|
|
7019
|
-
}
|
|
7020
|
-
/**
|
|
7021
|
-
* Extract all fixtures from rendered templates
|
|
7022
|
-
*
|
|
7023
|
-
* @param results Array of render results
|
|
7024
|
-
* @returns Deduplicated array of fixtures
|
|
7025
|
-
*/
|
|
7026
|
-
collectFixtures(results) {
|
|
7027
|
-
const fixtures = /* @__PURE__ */ new Set();
|
|
7028
|
-
for (const result of results) {
|
|
7029
|
-
for (const fix of result.fixtures) {
|
|
7030
|
-
fixtures.add(fix);
|
|
7031
|
-
}
|
|
7032
|
-
}
|
|
7033
|
-
return [...fixtures];
|
|
7034
|
-
}
|
|
7035
|
-
/**
|
|
7036
|
-
* Get the appropriate regex for the configured placeholder format
|
|
7037
|
-
*/
|
|
7038
|
-
getPlaceholderRegex() {
|
|
7039
|
-
switch (this.options.placeholderFormat) {
|
|
7040
|
-
case "dollar":
|
|
7041
|
-
return /\$\{([a-zA-Z][a-zA-Z0-9_.]*)\}/g;
|
|
7042
|
-
case "percent":
|
|
7043
|
-
return /%\(([a-zA-Z][a-zA-Z0-9_.]*)\)s/g;
|
|
7044
|
-
case "mustache":
|
|
7045
|
-
default:
|
|
7046
|
-
return /\{\{([a-zA-Z][a-zA-Z0-9_.]*)\}\}/g;
|
|
7047
|
-
}
|
|
7048
|
-
}
|
|
7049
|
-
/**
|
|
7050
|
-
* Create a template from a string with variable definitions
|
|
7051
|
-
*
|
|
7052
|
-
* @param templateString Template content
|
|
7053
|
-
* @param variables Variable definitions
|
|
7054
|
-
* @param metadata Additional template metadata
|
|
7055
|
-
* @returns TestTemplate object
|
|
7056
|
-
*/
|
|
7057
|
-
static createTemplate(templateString, variables, metadata = {}) {
|
|
7058
|
-
return {
|
|
7059
|
-
id: metadata.id ?? "custom-template",
|
|
7060
|
-
language: metadata.language ?? "typescript",
|
|
7061
|
-
framework: metadata.framework ?? "jest",
|
|
7062
|
-
template: templateString,
|
|
7063
|
-
variables,
|
|
7064
|
-
imports: metadata.imports,
|
|
7065
|
-
fixtures: metadata.fixtures,
|
|
7066
|
-
description: metadata.description
|
|
7067
|
-
};
|
|
7068
|
-
}
|
|
7069
|
-
};
|
|
7070
|
-
function createRenderer(options) {
|
|
7071
|
-
return new TemplateRenderer(options);
|
|
7072
|
-
}
|
|
7073
|
-
var SEVERITY_COLORS = {
|
|
7074
|
-
critical: chalk5.red.bold,
|
|
7075
|
-
high: chalk5.red,
|
|
7076
|
-
medium: chalk5.yellow,
|
|
7077
|
-
low: chalk5.gray
|
|
7078
|
-
};
|
|
7079
|
-
var PRIORITY_COLORS = {
|
|
7080
|
-
P0: chalk5.red.bold,
|
|
7081
|
-
P1: chalk5.yellow,
|
|
7082
|
-
P2: chalk5.gray
|
|
7083
|
-
};
|
|
7084
|
-
var DOMAIN_COLORS = {
|
|
7085
|
-
security: chalk5.red,
|
|
7086
|
-
data: chalk5.blue,
|
|
7087
|
-
concurrency: chalk5.magenta,
|
|
7088
|
-
input: chalk5.cyan,
|
|
7089
|
-
resource: chalk5.yellow,
|
|
7090
|
-
reliability: chalk5.green,
|
|
7091
|
-
performance: chalk5.yellowBright,
|
|
7092
|
-
platform: chalk5.gray,
|
|
7093
|
-
business: chalk5.white,
|
|
7094
|
-
compliance: chalk5.blueBright
|
|
7095
|
-
};
|
|
7096
|
-
function formatTerminal(categories) {
|
|
7097
|
-
if (categories.length === 0) {
|
|
7098
|
-
return chalk5.yellow("No categories found matching the filters.");
|
|
7099
|
-
}
|
|
7100
|
-
const lines = [];
|
|
7101
|
-
lines.push(chalk5.bold.underline(`Found ${categories.length} categories:
|
|
7102
|
-
`));
|
|
7103
|
-
const byDomain = /* @__PURE__ */ new Map();
|
|
7104
|
-
for (const cat of categories) {
|
|
7105
|
-
const domain = cat.domain;
|
|
7106
|
-
if (!byDomain.has(domain)) {
|
|
7107
|
-
byDomain.set(domain, []);
|
|
7108
|
-
}
|
|
7109
|
-
byDomain.get(domain).push(cat);
|
|
7110
|
-
}
|
|
7111
|
-
for (const [domain, domainCategories] of byDomain) {
|
|
7112
|
-
const domainColor = DOMAIN_COLORS[domain] ?? chalk5.white;
|
|
7113
|
-
lines.push(domainColor.bold(`
|
|
7114
|
-
${domain.toUpperCase()} (${domainCategories.length})`));
|
|
7115
|
-
lines.push(chalk5.gray("\u2500".repeat(40)));
|
|
7116
|
-
for (const cat of domainCategories) {
|
|
7117
|
-
const priorityColor = PRIORITY_COLORS[cat.priority];
|
|
7118
|
-
const severityColor = SEVERITY_COLORS[cat.severity];
|
|
7119
|
-
const priority = priorityColor(`[${cat.priority}]`);
|
|
7120
|
-
const severity = severityColor(`${cat.severity}`);
|
|
7121
|
-
const level = chalk5.cyan(`${cat.level}`);
|
|
7122
|
-
const name = chalk5.white.bold(cat.name);
|
|
7123
|
-
const id = chalk5.gray(`(${cat.id})`);
|
|
7124
|
-
lines.push(` ${priority} ${name} ${id}`);
|
|
7125
|
-
lines.push(` ${severity} | ${level}`);
|
|
7126
|
-
const desc = cat.description.length > 80 ? cat.description.slice(0, 77) + "..." : cat.description;
|
|
7127
|
-
lines.push(chalk5.gray(` ${desc}`));
|
|
7128
|
-
lines.push("");
|
|
7129
|
-
}
|
|
7130
|
-
}
|
|
7131
|
-
lines.push(chalk5.gray("\u2500".repeat(40)));
|
|
7132
|
-
lines.push(formatStats(categories));
|
|
7133
|
-
return lines.join("\n");
|
|
7134
|
-
}
|
|
7135
|
-
function formatStats(categories) {
|
|
7136
|
-
const stats = {
|
|
7137
|
-
P0: 0,
|
|
7138
|
-
P1: 0,
|
|
7139
|
-
P2: 0,
|
|
7140
|
-
critical: 0,
|
|
7141
|
-
high: 0,
|
|
7142
|
-
medium: 0,
|
|
7143
|
-
low: 0
|
|
7144
|
-
};
|
|
7145
|
-
for (const cat of categories) {
|
|
7146
|
-
stats[cat.priority]++;
|
|
7147
|
-
stats[cat.severity]++;
|
|
7148
|
-
}
|
|
7149
|
-
const parts = [];
|
|
7150
|
-
if (stats.P0 > 0) parts.push(PRIORITY_COLORS.P0(`${stats.P0} P0`));
|
|
7151
|
-
if (stats.P1 > 0) parts.push(PRIORITY_COLORS.P1(`${stats.P1} P1`));
|
|
7152
|
-
if (stats.P2 > 0) parts.push(PRIORITY_COLORS.P2(`${stats.P2} P2`));
|
|
7153
|
-
parts.push(chalk5.gray("|"));
|
|
7154
|
-
if (stats.critical > 0) parts.push(SEVERITY_COLORS.critical(`${stats.critical} critical`));
|
|
7155
|
-
if (stats.high > 0) parts.push(SEVERITY_COLORS.high(`${stats.high} high`));
|
|
7156
|
-
if (stats.medium > 0) parts.push(SEVERITY_COLORS.medium(`${stats.medium} medium`));
|
|
7157
|
-
if (stats.low > 0) parts.push(SEVERITY_COLORS.low(`${stats.low} low`));
|
|
7158
|
-
return parts.join(" ");
|
|
7159
|
-
}
|
|
7160
|
-
function formatJson(categories) {
|
|
7161
|
-
return JSON.stringify(categories, null, 2);
|
|
7162
|
-
}
|
|
7163
|
-
function formatMarkdown(categories) {
|
|
7164
|
-
if (categories.length === 0) {
|
|
7165
|
-
return "_No categories found matching the filters._";
|
|
7166
|
-
}
|
|
7167
|
-
const lines = [];
|
|
7168
|
-
lines.push(`# Categories (${categories.length})
|
|
7169
|
-
`);
|
|
7170
|
-
const byDomain = /* @__PURE__ */ new Map();
|
|
7171
|
-
for (const cat of categories) {
|
|
7172
|
-
const domain = cat.domain;
|
|
7173
|
-
if (!byDomain.has(domain)) {
|
|
7174
|
-
byDomain.set(domain, []);
|
|
7175
|
-
}
|
|
7176
|
-
byDomain.get(domain).push(cat);
|
|
7177
|
-
}
|
|
7178
|
-
for (const [domain, domainCategories] of byDomain) {
|
|
7179
|
-
lines.push(`
|
|
7180
|
-
## ${domain.charAt(0).toUpperCase() + domain.slice(1)} (${domainCategories.length})
|
|
7181
|
-
`);
|
|
7182
|
-
for (const cat of domainCategories) {
|
|
7183
|
-
lines.push(`### ${cat.name}`);
|
|
7184
|
-
lines.push(`- **ID**: \`${cat.id}\``);
|
|
7185
|
-
lines.push(`- **Priority**: ${cat.priority}`);
|
|
7186
|
-
lines.push(`- **Severity**: ${cat.severity}`);
|
|
7187
|
-
lines.push(`- **Level**: ${cat.level}`);
|
|
7188
|
-
lines.push(`
|
|
7189
|
-
${cat.description}
|
|
7190
|
-
`);
|
|
7191
|
-
}
|
|
7192
|
-
}
|
|
7193
|
-
return lines.join("\n");
|
|
7194
|
-
}
|
|
7195
|
-
function formatCategories(categories, format) {
|
|
7196
|
-
switch (format) {
|
|
7197
|
-
case "json":
|
|
7198
|
-
return formatJson(categories);
|
|
7199
|
-
case "markdown":
|
|
7200
|
-
return formatMarkdown(categories);
|
|
7201
|
-
case "terminal":
|
|
7202
|
-
default:
|
|
7203
|
-
return formatTerminal(categories);
|
|
7204
|
-
}
|
|
7205
|
-
}
|
|
7206
|
-
function isValidOutputFormat(format) {
|
|
7207
|
-
return ["terminal", "json", "markdown"].includes(format);
|
|
7208
|
-
}
|
|
7209
|
-
function formatError(error) {
|
|
7210
|
-
return chalk5.red(`Error: ${error.message}`);
|
|
7211
|
-
}
|
|
7212
|
-
|
|
7213
|
-
// src/cli/generate-formatters.ts
|
|
7214
|
-
init_errors();
|
|
7215
|
-
init_result();
|
|
7216
|
-
|
|
7217
|
-
// src/ai/service.ts
|
|
7218
|
-
var DEFAULT_CONFIG = {
|
|
7219
|
-
provider: "anthropic",
|
|
7220
|
-
apiKey: "",
|
|
7221
|
-
model: "claude-sonnet-4-20250514",
|
|
7222
|
-
maxTokens: 1024,
|
|
7223
|
-
temperature: 0.3,
|
|
7224
|
-
timeoutMs: 3e4
|
|
7225
|
-
};
|
|
7226
|
-
var PROVIDER_MODELS = {
|
|
7227
|
-
anthropic: "claude-sonnet-4-20250514",
|
|
7228
|
-
openai: "gpt-4o",
|
|
7229
|
-
mock: "mock-model"
|
|
7230
|
-
};
|
|
7231
|
-
var PROVIDER_ENDPOINTS = {
|
|
7232
|
-
anthropic: "https://api.anthropic.com/v1/messages",
|
|
7233
|
-
openai: "https://api.openai.com/v1/chat/completions"};
|
|
7234
|
-
var AIService = class {
|
|
7235
|
-
config;
|
|
7236
|
-
constructor(config2 = {}) {
|
|
7237
|
-
this.config = {
|
|
7238
|
-
...DEFAULT_CONFIG,
|
|
7239
|
-
...config2,
|
|
7240
|
-
apiKey: config2.apiKey ?? this.getApiKeyFromEnv(config2.provider ?? "anthropic"),
|
|
7241
|
-
model: config2.model ?? PROVIDER_MODELS[config2.provider ?? "anthropic"]
|
|
7242
|
-
};
|
|
7243
|
-
}
|
|
7244
|
-
/**
|
|
7245
|
-
* Get API key from environment variable
|
|
7246
|
-
* For config file support, use the sync version below
|
|
7247
|
-
*/
|
|
7248
|
-
getApiKeyFromEnv(provider) {
|
|
7249
|
-
if (provider === "mock") return "mock-key";
|
|
7250
|
-
const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
|
|
7251
|
-
const envValue = process.env[envVar];
|
|
7252
|
-
if (envValue !== void 0 && envValue.length > 0) {
|
|
7253
|
-
return envValue;
|
|
7254
|
-
}
|
|
7255
|
-
return this.getApiKeyFromConfig(provider);
|
|
7256
|
-
}
|
|
7257
|
-
/**
|
|
7258
|
-
* Read API key from config file synchronously
|
|
7259
|
-
* Uses require() for sync file access in constructor context
|
|
6606
|
+
* Read API key from config file synchronously
|
|
6607
|
+
* Uses require() for sync file access in constructor context
|
|
7260
6608
|
*/
|
|
7261
6609
|
getApiKeyFromConfig(provider) {
|
|
7262
6610
|
try {
|
|
7263
|
-
const { existsSync:
|
|
6611
|
+
const { existsSync: existsSync7, readFileSync: readFileSync3 } = __require("fs");
|
|
7264
6612
|
const { homedir: homedir3 } = __require("os");
|
|
7265
6613
|
const { join: join5 } = __require("path");
|
|
7266
6614
|
const configPath = join5(homedir3(), ".pinata", "config.json");
|
|
7267
|
-
if (!
|
|
6615
|
+
if (!existsSync7(configPath)) {
|
|
7268
6616
|
return "";
|
|
7269
6617
|
}
|
|
7270
6618
|
const content = readFileSync3(configPath, "utf-8");
|
|
@@ -7481,541 +6829,42 @@ var AIService = class {
|
|
|
7481
6829
|
confidence: 0.8
|
|
7482
6830
|
},
|
|
7483
6831
|
{
|
|
7484
|
-
name: "functionName",
|
|
7485
|
-
value: "get_user",
|
|
7486
|
-
reasoning: "Extracted from the code snippet",
|
|
7487
|
-
confidence: 0.9
|
|
7488
|
-
}
|
|
7489
|
-
]
|
|
7490
|
-
});
|
|
7491
|
-
} else if (content.includes("pattern") || content.includes("regex")) {
|
|
7492
|
-
response = JSON.stringify({
|
|
7493
|
-
suggestions: [
|
|
7494
|
-
{
|
|
7495
|
-
id: "custom-sql-pattern",
|
|
7496
|
-
pattern: "execute\\s*\\(.*\\+",
|
|
7497
|
-
description: "Detects SQL execution with string concatenation",
|
|
7498
|
-
confidence: "medium",
|
|
7499
|
-
matchExample: "cursor.execute(query + user_input)",
|
|
7500
|
-
safeExample: "cursor.execute(query, (user_input,))",
|
|
7501
|
-
reasoning: "String concatenation in SQL queries is a common injection vector"
|
|
7502
|
-
}
|
|
7503
|
-
]
|
|
7504
|
-
});
|
|
7505
|
-
}
|
|
7506
|
-
return {
|
|
7507
|
-
success: true,
|
|
7508
|
-
data: response,
|
|
7509
|
-
usage: { inputTokens: 100, outputTokens: 50 },
|
|
7510
|
-
durationMs: Date.now() - startTime
|
|
7511
|
-
};
|
|
7512
|
-
}
|
|
7513
|
-
};
|
|
7514
|
-
function createAIService(config2) {
|
|
7515
|
-
return new AIService(config2);
|
|
7516
|
-
}
|
|
7517
|
-
|
|
7518
|
-
// src/ai/template-filler.ts
|
|
7519
|
-
var SYSTEM_PROMPT = `You are an expert at analyzing code and extracting meaningful variable values for test generation.
|
|
7520
|
-
Given a code snippet and a list of template variables, suggest appropriate values for each variable.
|
|
7521
|
-
|
|
7522
|
-
For each variable, analyze:
|
|
7523
|
-
1. The code snippet to extract relevant information (class names, function names, etc.)
|
|
7524
|
-
2. The variable description to understand what's needed
|
|
7525
|
-
3. The variable type to ensure correct formatting
|
|
7526
|
-
|
|
7527
|
-
Always respond with valid JSON matching this structure:
|
|
7528
|
-
{
|
|
7529
|
-
"suggestions": [
|
|
7530
|
-
{
|
|
7531
|
-
"name": "variableName",
|
|
7532
|
-
"value": "suggested value",
|
|
7533
|
-
"reasoning": "why this value was chosen",
|
|
7534
|
-
"confidence": 0.0-1.0
|
|
7535
|
-
}
|
|
7536
|
-
]
|
|
7537
|
-
}
|
|
7538
|
-
|
|
7539
|
-
For arrays, use: "value": ["item1", "item2"]
|
|
7540
|
-
For booleans, use: "value": true or "value": false
|
|
7541
|
-
For numbers, use: "value": 42`;
|
|
7542
|
-
async function suggestVariables(request, config2) {
|
|
7543
|
-
const ai = createAIService(config2);
|
|
7544
|
-
if (!ai.isConfigured()) {
|
|
7545
|
-
return {
|
|
7546
|
-
success: true,
|
|
7547
|
-
data: extractVariablesFromCode(request),
|
|
7548
|
-
durationMs: 0
|
|
7549
|
-
};
|
|
7550
|
-
}
|
|
7551
|
-
const prompt = buildVariablePrompt(request);
|
|
7552
|
-
const startTime = Date.now();
|
|
7553
|
-
const response = await ai.completeJSON({
|
|
7554
|
-
systemPrompt: SYSTEM_PROMPT,
|
|
7555
|
-
messages: [{ role: "user", content: prompt }],
|
|
7556
|
-
maxTokens: 1024,
|
|
7557
|
-
temperature: 0.2
|
|
7558
|
-
});
|
|
7559
|
-
if (!response.success || !response.data) {
|
|
7560
|
-
return {
|
|
7561
|
-
success: true,
|
|
7562
|
-
data: extractVariablesFromCode(request),
|
|
7563
|
-
durationMs: Date.now() - startTime
|
|
7564
|
-
};
|
|
7565
|
-
}
|
|
7566
|
-
const suggestions = /* @__PURE__ */ new Map();
|
|
7567
|
-
const unfilled = [];
|
|
7568
|
-
const values = { ...request.existingValues };
|
|
7569
|
-
const suggestionsList = response.data.suggestions ?? [];
|
|
7570
|
-
for (const suggestion of suggestionsList) {
|
|
7571
|
-
suggestions.set(suggestion.name, suggestion);
|
|
7572
|
-
if (!(suggestion.name in values)) {
|
|
7573
|
-
values[suggestion.name] = suggestion.value;
|
|
7574
|
-
}
|
|
7575
|
-
}
|
|
7576
|
-
for (const variable of request.variables) {
|
|
7577
|
-
if (!suggestions.has(variable.name) && !(variable.name in values)) {
|
|
7578
|
-
if (variable.defaultValue !== void 0) {
|
|
7579
|
-
values[variable.name] = variable.defaultValue;
|
|
7580
|
-
} else {
|
|
7581
|
-
unfilled.push(variable.name);
|
|
7582
|
-
}
|
|
7583
|
-
}
|
|
7584
|
-
}
|
|
7585
|
-
const result = {
|
|
7586
|
-
success: true,
|
|
7587
|
-
data: { suggestions, unfilled, values },
|
|
7588
|
-
durationMs: response.durationMs
|
|
7589
|
-
};
|
|
7590
|
-
if (response.usage) {
|
|
7591
|
-
result.usage = response.usage;
|
|
7592
|
-
}
|
|
7593
|
-
return result;
|
|
7594
|
-
}
|
|
7595
|
-
function buildVariablePrompt(request) {
|
|
7596
|
-
const parts = [];
|
|
7597
|
-
parts.push("Analyze this code and suggest values for the template variables:\n");
|
|
7598
|
-
parts.push("**Code:**");
|
|
7599
|
-
parts.push("```");
|
|
7600
|
-
parts.push(request.codeSnippet);
|
|
7601
|
-
parts.push("```\n");
|
|
7602
|
-
parts.push(`**File:** ${request.filePath}
|
|
7603
|
-
`);
|
|
7604
|
-
if (request.gap) {
|
|
7605
|
-
parts.push(`**Category:** ${request.gap.categoryName}`);
|
|
7606
|
-
parts.push(`**Line:** ${request.gap.lineStart}
|
|
7607
|
-
`);
|
|
7608
|
-
}
|
|
7609
|
-
parts.push("**Variables to fill:**");
|
|
7610
|
-
for (const variable of request.variables) {
|
|
7611
|
-
const required = variable.required ? " (required)" : " (optional)";
|
|
7612
|
-
const defaultVal = variable.defaultValue !== void 0 ? ` [default: ${JSON.stringify(variable.defaultValue)}]` : "";
|
|
7613
|
-
parts.push(`- ${variable.name} (${variable.type})${required}${defaultVal}: ${variable.description}`);
|
|
7614
|
-
}
|
|
7615
|
-
if (request.existingValues && Object.keys(request.existingValues).length > 0) {
|
|
7616
|
-
parts.push("\n**Already provided:**");
|
|
7617
|
-
for (const [name, value] of Object.entries(request.existingValues)) {
|
|
7618
|
-
parts.push(`- ${name}: ${JSON.stringify(value)}`);
|
|
7619
|
-
}
|
|
7620
|
-
}
|
|
7621
|
-
parts.push("\nExtract appropriate values from the code context.");
|
|
7622
|
-
return parts.join("\n");
|
|
7623
|
-
}
|
|
7624
|
-
function extractVariablesFromCode(request) {
|
|
7625
|
-
const suggestions = /* @__PURE__ */ new Map();
|
|
7626
|
-
const unfilled = [];
|
|
7627
|
-
const values = { ...request.existingValues };
|
|
7628
|
-
const code = request.codeSnippet;
|
|
7629
|
-
const filePath = request.filePath;
|
|
7630
|
-
for (const variable of request.variables) {
|
|
7631
|
-
if (variable.name in values) continue;
|
|
7632
|
-
let value = void 0;
|
|
7633
|
-
let reasoning = "";
|
|
7634
|
-
let confidence = 0;
|
|
7635
|
-
switch (variable.name.toLowerCase()) {
|
|
7636
|
-
case "classname":
|
|
7637
|
-
case "class_name": {
|
|
7638
|
-
const classMatch = code.match(/class\s+(\w+)/);
|
|
7639
|
-
if (classMatch) {
|
|
7640
|
-
value = classMatch[1];
|
|
7641
|
-
reasoning = "Extracted from class definition in code";
|
|
7642
|
-
confidence = 0.9;
|
|
7643
|
-
} else {
|
|
7644
|
-
const fileName = filePath.split("/").pop()?.replace(/\.\w+$/, "") ?? "";
|
|
7645
|
-
value = toPascalCase(fileName);
|
|
7646
|
-
reasoning = "Inferred from file name";
|
|
7647
|
-
confidence = 0.6;
|
|
7648
|
-
}
|
|
7649
|
-
break;
|
|
7650
|
-
}
|
|
7651
|
-
case "functionname":
|
|
7652
|
-
case "function_name":
|
|
7653
|
-
case "methodname": {
|
|
7654
|
-
const funcMatch = code.match(/(?:def|function|async function)\s+(\w+)/);
|
|
7655
|
-
if (funcMatch) {
|
|
7656
|
-
value = funcMatch[1];
|
|
7657
|
-
reasoning = "Extracted from function definition";
|
|
7658
|
-
confidence = 0.9;
|
|
7659
|
-
}
|
|
7660
|
-
break;
|
|
7661
|
-
}
|
|
7662
|
-
case "modulepath":
|
|
7663
|
-
case "module_path": {
|
|
7664
|
-
value = filePath.replace(/\.[jt]sx?$/, "").replace(/\.py$/, "").replace(/\//g, ".").replace(/^\.+/, "");
|
|
7665
|
-
reasoning = "Derived from file path";
|
|
7666
|
-
confidence = 0.7;
|
|
7667
|
-
break;
|
|
7668
|
-
}
|
|
7669
|
-
case "tablename":
|
|
7670
|
-
case "table_name": {
|
|
7671
|
-
const tableMatch = code.match(/(?:FROM|INTO|UPDATE)\s+(\w+)/i);
|
|
7672
|
-
if (tableMatch) {
|
|
7673
|
-
value = tableMatch[1];
|
|
7674
|
-
reasoning = "Extracted from SQL statement";
|
|
7675
|
-
confidence = 0.8;
|
|
7676
|
-
} else {
|
|
7677
|
-
value = "users";
|
|
7678
|
-
reasoning = "Default table name";
|
|
7679
|
-
confidence = 0.3;
|
|
7680
|
-
}
|
|
7681
|
-
break;
|
|
7682
|
-
}
|
|
7683
|
-
case "exceptionclass":
|
|
7684
|
-
case "exception_class": {
|
|
7685
|
-
value = "ValueError";
|
|
7686
|
-
reasoning = "Common exception for input validation";
|
|
7687
|
-
confidence = 0.5;
|
|
7688
|
-
break;
|
|
7689
|
-
}
|
|
7690
|
-
case "dbclient":
|
|
7691
|
-
case "db_client": {
|
|
7692
|
-
const clientMatch = code.match(/(db|conn|connection|client|cursor)\s*[=.]/i);
|
|
7693
|
-
if (clientMatch) {
|
|
7694
|
-
value = clientMatch[1];
|
|
7695
|
-
reasoning = "Extracted from code";
|
|
7696
|
-
confidence = 0.7;
|
|
7697
|
-
} else {
|
|
7698
|
-
value = "db";
|
|
7699
|
-
reasoning = "Default database client name";
|
|
7700
|
-
confidence = 0.4;
|
|
7701
|
-
}
|
|
7702
|
-
break;
|
|
7703
|
-
}
|
|
7704
|
-
case "functioncall":
|
|
7705
|
-
case "function_call": {
|
|
7706
|
-
const funcName = values["functionName"] ?? values["function_name"];
|
|
7707
|
-
if (typeof funcName === "string" && funcName.length > 0) {
|
|
7708
|
-
value = `${funcName}(user_input)`;
|
|
7709
|
-
reasoning = "Constructed from function name";
|
|
7710
|
-
confidence = 0.6;
|
|
7711
|
-
}
|
|
7712
|
-
break;
|
|
7713
|
-
}
|
|
7714
|
-
case "fixtures": {
|
|
7715
|
-
value = "db_session";
|
|
7716
|
-
reasoning = "Common pytest fixture";
|
|
7717
|
-
confidence = 0.5;
|
|
7718
|
-
break;
|
|
7719
|
-
}
|
|
7720
|
-
default:
|
|
7721
|
-
if (variable.defaultValue !== void 0) {
|
|
7722
|
-
value = variable.defaultValue;
|
|
7723
|
-
reasoning = "Using default value";
|
|
7724
|
-
confidence = 1;
|
|
7725
|
-
}
|
|
7726
|
-
}
|
|
7727
|
-
if (value !== void 0) {
|
|
7728
|
-
suggestions.set(variable.name, {
|
|
7729
|
-
name: variable.name,
|
|
7730
|
-
value,
|
|
7731
|
-
reasoning,
|
|
7732
|
-
confidence
|
|
7733
|
-
});
|
|
7734
|
-
values[variable.name] = value;
|
|
7735
|
-
} else if (variable.required) {
|
|
7736
|
-
unfilled.push(variable.name);
|
|
7737
|
-
}
|
|
7738
|
-
}
|
|
7739
|
-
return { suggestions, unfilled, values };
|
|
7740
|
-
}
|
|
7741
|
-
function toPascalCase(str) {
|
|
7742
|
-
return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
7743
|
-
}
|
|
7744
|
-
|
|
7745
|
-
// src/cli/generate-formatters.ts
|
|
7746
|
-
function formatGeneratedTerminal(tests, basePath) {
|
|
7747
|
-
const lines = [];
|
|
7748
|
-
if (tests.length === 0) {
|
|
7749
|
-
lines.push(chalk5.yellow("No tests generated."));
|
|
7750
|
-
return lines.join("\n");
|
|
7751
|
-
}
|
|
7752
|
-
lines.push(chalk5.bold.cyan(`
|
|
7753
|
-
Generated ${tests.length} test(s):
|
|
7754
|
-
`));
|
|
7755
|
-
lines.push(chalk5.gray("\u2500".repeat(60)));
|
|
7756
|
-
for (const test of tests) {
|
|
7757
|
-
const relGapPath = relative(basePath, test.gap.filePath);
|
|
7758
|
-
lines.push("");
|
|
7759
|
-
lines.push(chalk5.bold.white(`Test for: ${test.gap.categoryName}`));
|
|
7760
|
-
lines.push(chalk5.gray(` Gap location: ${relGapPath}:${test.gap.lineStart}`));
|
|
7761
|
-
lines.push(chalk5.gray(` Template: ${test.template.id}`));
|
|
7762
|
-
lines.push(chalk5.gray(` Output: ${test.suggestedPath}`));
|
|
7763
|
-
lines.push("");
|
|
7764
|
-
lines.push(chalk5.cyan(`// --- ${test.suggestedPath} ---`));
|
|
7765
|
-
lines.push("");
|
|
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("");
|
|
7771
|
-
}
|
|
7772
|
-
lines.push(test.result.content);
|
|
7773
|
-
lines.push("");
|
|
7774
|
-
lines.push(chalk5.gray("\u2500".repeat(60)));
|
|
7775
|
-
}
|
|
7776
|
-
lines.push("");
|
|
7777
|
-
lines.push(chalk5.bold("Summary:"));
|
|
7778
|
-
lines.push(` Tests generated: ${chalk5.green(tests.length.toString())}`);
|
|
7779
|
-
const categories = new Set(tests.map((t) => t.category.id));
|
|
7780
|
-
lines.push(` Categories covered: ${chalk5.cyan(categories.size.toString())}`);
|
|
7781
|
-
if (tests.some((t) => t.result.unresolved.length > 0)) {
|
|
7782
|
-
const unresolvedCount = tests.reduce((acc, t) => acc + t.result.unresolved.length, 0);
|
|
7783
|
-
lines.push(chalk5.yellow(` Unresolved placeholders: ${unresolvedCount}`));
|
|
7784
|
-
lines.push(chalk5.gray(" (Some placeholders need manual completion)"));
|
|
7785
|
-
}
|
|
7786
|
-
lines.push("");
|
|
7787
|
-
lines.push(chalk5.gray("This is a dry run. Use --write to save files."));
|
|
7788
|
-
return lines.join("\n");
|
|
7789
|
-
}
|
|
7790
|
-
function formatGeneratedJson(tests) {
|
|
7791
|
-
const output = tests.map((test) => ({
|
|
7792
|
-
gap: {
|
|
7793
|
-
categoryId: test.gap.categoryId,
|
|
7794
|
-
categoryName: test.gap.categoryName,
|
|
7795
|
-
filePath: test.gap.filePath,
|
|
7796
|
-
lineStart: test.gap.lineStart,
|
|
7797
|
-
severity: test.gap.severity,
|
|
7798
|
-
confidence: test.gap.confidence
|
|
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)
|
|
6832
|
+
name: "functionName",
|
|
6833
|
+
value: "get_user",
|
|
6834
|
+
reasoning: "Extracted from the code snippet",
|
|
6835
|
+
confidence: 0.9
|
|
6836
|
+
}
|
|
6837
|
+
]
|
|
6838
|
+
});
|
|
6839
|
+
} else if (content.includes("pattern") || content.includes("regex")) {
|
|
6840
|
+
response = JSON.stringify({
|
|
6841
|
+
suggestions: [
|
|
6842
|
+
{
|
|
6843
|
+
id: "custom-sql-pattern",
|
|
6844
|
+
pattern: "execute\\s*\\(.*\\+",
|
|
6845
|
+
description: "Detects SQL execution with string concatenation",
|
|
6846
|
+
confidence: "medium",
|
|
6847
|
+
matchExample: "cursor.execute(query + user_input)",
|
|
6848
|
+
safeExample: "cursor.execute(query, (user_input,))",
|
|
6849
|
+
reasoning: "String concatenation in SQL queries is a common injection vector"
|
|
6850
|
+
}
|
|
6851
|
+
]
|
|
7970
6852
|
});
|
|
7971
6853
|
}
|
|
6854
|
+
return {
|
|
6855
|
+
success: true,
|
|
6856
|
+
data: response,
|
|
6857
|
+
usage: { inputTokens: 100, outputTokens: 50 },
|
|
6858
|
+
durationMs: Date.now() - startTime
|
|
6859
|
+
};
|
|
7972
6860
|
}
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
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");
|
|
6861
|
+
};
|
|
6862
|
+
function createAIService(config2) {
|
|
6863
|
+
return new AIService(config2);
|
|
8015
6864
|
}
|
|
8016
6865
|
|
|
8017
6866
|
// src/ai/explainer.ts
|
|
8018
|
-
var
|
|
6867
|
+
var SYSTEM_PROMPT = `You are a security expert explaining code vulnerabilities to developers.
|
|
8019
6868
|
Your explanations should be:
|
|
8020
6869
|
- Clear and actionable
|
|
8021
6870
|
- Focused on the specific code pattern
|
|
@@ -8042,13 +6891,30 @@ async function explainGap(gap, category, config2) {
|
|
|
8042
6891
|
}
|
|
8043
6892
|
const prompt = buildExplainPrompt(gap);
|
|
8044
6893
|
const response = await ai.completeJSON({
|
|
8045
|
-
systemPrompt:
|
|
6894
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
8046
6895
|
messages: [{ role: "user", content: prompt }],
|
|
8047
6896
|
maxTokens: 1024,
|
|
8048
6897
|
temperature: 0.3
|
|
8049
6898
|
});
|
|
8050
6899
|
return response;
|
|
8051
6900
|
}
|
|
6901
|
+
async function explainGaps(gaps, categories, config2) {
|
|
6902
|
+
const results = /* @__PURE__ */ new Map();
|
|
6903
|
+
const BATCH_SIZE = 5;
|
|
6904
|
+
for (let i = 0; i < gaps.length; i += BATCH_SIZE) {
|
|
6905
|
+
const batch = gaps.slice(i, i + BATCH_SIZE);
|
|
6906
|
+
const promises = batch.map(async (gap) => {
|
|
6907
|
+
const category = categories?.get(gap.categoryId);
|
|
6908
|
+
const result = await explainGap(gap, category, config2);
|
|
6909
|
+
return { key: `${gap.filePath}:${gap.lineStart}:${gap.categoryId}`, result };
|
|
6910
|
+
});
|
|
6911
|
+
const batchResults = await Promise.all(promises);
|
|
6912
|
+
for (const { key, result } of batchResults) {
|
|
6913
|
+
results.set(key, result);
|
|
6914
|
+
}
|
|
6915
|
+
}
|
|
6916
|
+
return results;
|
|
6917
|
+
}
|
|
8052
6918
|
function buildExplainPrompt(gap, category) {
|
|
8053
6919
|
const parts = [];
|
|
8054
6920
|
parts.push(`Explain this security finding:
|
|
@@ -8109,7 +6975,7 @@ function generateFallbackExplanation(gap) {
|
|
|
8109
6975
|
}
|
|
8110
6976
|
|
|
8111
6977
|
// src/ai/pattern-suggester.ts
|
|
8112
|
-
var
|
|
6978
|
+
var SYSTEM_PROMPT2 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
|
|
8113
6979
|
Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
|
|
8114
6980
|
|
|
8115
6981
|
Your patterns should:
|
|
@@ -8148,7 +7014,7 @@ async function suggestPatterns(request, config2) {
|
|
|
8148
7014
|
}
|
|
8149
7015
|
const prompt = buildPatternPrompt(request);
|
|
8150
7016
|
const response = await ai.completeJSON({
|
|
8151
|
-
systemPrompt:
|
|
7017
|
+
systemPrompt: SYSTEM_PROMPT2,
|
|
8152
7018
|
messages: [{ role: "user", content: prompt }],
|
|
8153
7019
|
maxTokens: 2048,
|
|
8154
7020
|
temperature: 0.3
|
|
@@ -8277,73 +7143,89 @@ function hasRedosPotential(pattern) {
|
|
|
8277
7143
|
|
|
8278
7144
|
// src/cli/index.ts
|
|
8279
7145
|
init_results_cache();
|
|
7146
|
+
var __filename2 = fileURLToPath(import.meta.url);
|
|
7147
|
+
var __dirname2 = dirname(__filename2);
|
|
7148
|
+
function getDefinitionsPath() {
|
|
7149
|
+
const candidates = [
|
|
7150
|
+
resolve(__dirname2, "../../src/categories/definitions"),
|
|
7151
|
+
resolve(process.cwd(), "src/categories/definitions"),
|
|
7152
|
+
resolve(__dirname2, "../categories/definitions")
|
|
7153
|
+
];
|
|
7154
|
+
for (const candidate of candidates) {
|
|
7155
|
+
if (existsSync(candidate)) {
|
|
7156
|
+
return candidate;
|
|
7157
|
+
}
|
|
7158
|
+
}
|
|
7159
|
+
return candidates[0];
|
|
7160
|
+
}
|
|
7161
|
+
init_results_cache();
|
|
8280
7162
|
var SEVERITY_COLORS2 = {
|
|
8281
|
-
critical:
|
|
8282
|
-
high:
|
|
8283
|
-
medium:
|
|
8284
|
-
low:
|
|
7163
|
+
critical: chalk6.red.bold,
|
|
7164
|
+
high: chalk6.red,
|
|
7165
|
+
medium: chalk6.yellow,
|
|
7166
|
+
low: chalk6.gray
|
|
8285
7167
|
};
|
|
8286
7168
|
var DOMAIN_COLORS2 = {
|
|
8287
|
-
security:
|
|
8288
|
-
data:
|
|
8289
|
-
concurrency:
|
|
8290
|
-
input:
|
|
8291
|
-
resource:
|
|
8292
|
-
reliability:
|
|
8293
|
-
performance:
|
|
8294
|
-
platform:
|
|
8295
|
-
business:
|
|
8296
|
-
compliance:
|
|
7169
|
+
security: chalk6.red,
|
|
7170
|
+
data: chalk6.blue,
|
|
7171
|
+
concurrency: chalk6.magenta,
|
|
7172
|
+
input: chalk6.cyan,
|
|
7173
|
+
resource: chalk6.yellow,
|
|
7174
|
+
reliability: chalk6.green,
|
|
7175
|
+
performance: chalk6.yellowBright,
|
|
7176
|
+
platform: chalk6.gray,
|
|
7177
|
+
business: chalk6.white,
|
|
7178
|
+
compliance: chalk6.blueBright
|
|
8297
7179
|
};
|
|
8298
7180
|
var GRADE_COLORS = {
|
|
8299
|
-
A:
|
|
8300
|
-
B:
|
|
8301
|
-
C:
|
|
8302
|
-
D:
|
|
8303
|
-
F:
|
|
7181
|
+
A: chalk6.green.bold,
|
|
7182
|
+
B: chalk6.green,
|
|
7183
|
+
C: chalk6.yellow,
|
|
7184
|
+
D: chalk6.red,
|
|
7185
|
+
F: chalk6.red.bold
|
|
8304
7186
|
};
|
|
8305
7187
|
var BANNER = `
|
|
8306
|
-
${
|
|
8307
|
-
${
|
|
8308
|
-
${
|
|
8309
|
-
${
|
|
8310
|
-
${
|
|
7188
|
+
${chalk6.cyan(" ____ _ _ ")}
|
|
7189
|
+
${chalk6.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
|
|
7190
|
+
${chalk6.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
|
|
7191
|
+
${chalk6.cyan("| __/| | | | | (_| | || (_| |")}
|
|
7192
|
+
${chalk6.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
|
|
8311
7193
|
`;
|
|
8312
7194
|
function formatScanTerminal(result, basePath) {
|
|
8313
7195
|
const lines = [];
|
|
8314
7196
|
lines.push(BANNER);
|
|
8315
|
-
lines.push(
|
|
7197
|
+
lines.push(chalk6.gray(`Analyzing: ${result.targetDirectory}`));
|
|
8316
7198
|
const projectTypeLabel = getProjectTypeDescription(result.projectType.type);
|
|
8317
|
-
lines.push(
|
|
8318
|
-
lines.push(
|
|
7199
|
+
lines.push(chalk6.gray(`Project: ${projectTypeLabel} (${result.projectType.confidence} confidence)`));
|
|
7200
|
+
lines.push(chalk6.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
|
|
8319
7201
|
lines.push("");
|
|
8320
7202
|
lines.push(formatScoreBox(result.score));
|
|
8321
7203
|
lines.push("");
|
|
8322
|
-
lines.push(
|
|
7204
|
+
lines.push(chalk6.bold("Domain Coverage:"));
|
|
8323
7205
|
lines.push(formatDomainCoverage(result.coverage));
|
|
8324
7206
|
lines.push("");
|
|
8325
7207
|
if (result.gaps.length > 0) {
|
|
8326
7208
|
lines.push(formatGapsSummary(result.gaps, basePath));
|
|
8327
7209
|
lines.push("");
|
|
8328
7210
|
} else {
|
|
8329
|
-
lines.push(
|
|
7211
|
+
lines.push(chalk6.green.bold("No vulnerabilities detected."));
|
|
8330
7212
|
lines.push("");
|
|
8331
7213
|
}
|
|
8332
7214
|
if (result.gaps.length > 0) {
|
|
8333
|
-
lines.push(
|
|
7215
|
+
lines.push(chalk6.gray("Run `pinata generate --gaps` to create tests for these gaps."));
|
|
8334
7216
|
}
|
|
8335
|
-
lines.push(
|
|
7217
|
+
lines.push(chalk6.gray(`
|
|
8336
7218
|
Scan completed in ${result.durationMs}ms`));
|
|
8337
7219
|
return lines.join("\n");
|
|
8338
7220
|
}
|
|
8339
7221
|
function formatScoreBox(score) {
|
|
8340
|
-
const gradeColor = GRADE_COLORS[score.grade] ??
|
|
7222
|
+
const gradeColor = GRADE_COLORS[score.grade] ?? chalk6.white;
|
|
8341
7223
|
const scoreStr = `Pinata Score: ${score.overall}/100 ${gradeColor(`(${score.grade})`)}`;
|
|
8342
7224
|
const boxWidth = 60;
|
|
8343
7225
|
const padding = Math.floor((boxWidth - scoreStr.length) / 2);
|
|
8344
|
-
const top =
|
|
8345
|
-
const middle =
|
|
8346
|
-
const bottom =
|
|
7226
|
+
const top = chalk6.cyan("\u2554" + "\u2550".repeat(boxWidth) + "\u2557");
|
|
7227
|
+
const middle = chalk6.cyan("\u2551") + " ".repeat(padding) + scoreStr + " ".repeat(boxWidth - padding - scoreStr.length) + chalk6.cyan("\u2551");
|
|
7228
|
+
const bottom = chalk6.cyan("\u255A" + "\u2550".repeat(boxWidth) + "\u255D");
|
|
8347
7229
|
return `${top}
|
|
8348
7230
|
${middle}
|
|
8349
7231
|
${bottom}`;
|
|
@@ -8358,14 +7240,14 @@ function formatDomainCoverage(coverage) {
|
|
|
8358
7240
|
}
|
|
8359
7241
|
const percent = domainCoverage.coveragePercent;
|
|
8360
7242
|
const filledWidth = Math.round(percent / 100 * barWidth);
|
|
8361
|
-
const bar =
|
|
8362
|
-
const domainColor = DOMAIN_COLORS2[domain] ??
|
|
7243
|
+
const bar = chalk6.green("\u2588".repeat(filledWidth)) + chalk6.gray("\u2591".repeat(barWidth - filledWidth));
|
|
7244
|
+
const domainColor = DOMAIN_COLORS2[domain] ?? chalk6.white;
|
|
8363
7245
|
const domainName = domain.padEnd(15);
|
|
8364
7246
|
const stats = `${domainCoverage.categoriesCovered}/${domainCoverage.categoriesScanned} categories`;
|
|
8365
7247
|
lines.push(` ${domainColor(domainName)} ${bar} ${percent.toString().padStart(3)}% (${stats})`);
|
|
8366
7248
|
}
|
|
8367
7249
|
if (lines.length === 0) {
|
|
8368
|
-
lines.push(
|
|
7250
|
+
lines.push(chalk6.gray(" No domain coverage data available."));
|
|
8369
7251
|
}
|
|
8370
7252
|
return lines.join("\n");
|
|
8371
7253
|
}
|
|
@@ -8376,48 +7258,48 @@ function formatGapsSummary(gaps, basePath) {
|
|
|
8376
7258
|
const medium = gaps.filter((g) => g.severity === "medium");
|
|
8377
7259
|
const low = gaps.filter((g) => g.severity === "low");
|
|
8378
7260
|
if (critical.length > 0) {
|
|
8379
|
-
lines.push(
|
|
7261
|
+
lines.push(chalk6.red.bold(`
|
|
8380
7262
|
Critical Gaps (${critical.length}):`));
|
|
8381
7263
|
for (const gap of critical.slice(0, 5)) {
|
|
8382
7264
|
lines.push(formatGapLine(gap, basePath, "critical"));
|
|
8383
7265
|
}
|
|
8384
7266
|
if (critical.length > 5) {
|
|
8385
|
-
lines.push(
|
|
7267
|
+
lines.push(chalk6.gray(` ... and ${critical.length - 5} more critical gaps`));
|
|
8386
7268
|
}
|
|
8387
7269
|
}
|
|
8388
7270
|
if (high.length > 0) {
|
|
8389
|
-
lines.push(
|
|
7271
|
+
lines.push(chalk6.red(`
|
|
8390
7272
|
High Severity Gaps (${high.length}):`));
|
|
8391
7273
|
for (const gap of high.slice(0, 5)) {
|
|
8392
7274
|
lines.push(formatGapLine(gap, basePath, "high"));
|
|
8393
7275
|
}
|
|
8394
7276
|
if (high.length > 5) {
|
|
8395
|
-
lines.push(
|
|
7277
|
+
lines.push(chalk6.gray(` ... and ${high.length - 5} more high severity gaps`));
|
|
8396
7278
|
}
|
|
8397
7279
|
}
|
|
8398
7280
|
if (medium.length > 0) {
|
|
8399
|
-
lines.push(
|
|
7281
|
+
lines.push(chalk6.yellow(`
|
|
8400
7282
|
Medium Severity Gaps (${medium.length}):`));
|
|
8401
7283
|
for (const gap of medium.slice(0, 3)) {
|
|
8402
7284
|
lines.push(formatGapLine(gap, basePath, "medium"));
|
|
8403
7285
|
}
|
|
8404
7286
|
if (medium.length > 3) {
|
|
8405
|
-
lines.push(
|
|
7287
|
+
lines.push(chalk6.gray(` ... and ${medium.length - 3} more medium severity gaps`));
|
|
8406
7288
|
}
|
|
8407
7289
|
}
|
|
8408
7290
|
if (low.length > 0) {
|
|
8409
|
-
lines.push(
|
|
7291
|
+
lines.push(chalk6.gray(`
|
|
8410
7292
|
Low Severity: ${low.length} gaps`));
|
|
8411
7293
|
}
|
|
8412
7294
|
return lines.join("\n");
|
|
8413
7295
|
}
|
|
8414
7296
|
function formatGapLine(gap, basePath, severity) {
|
|
8415
|
-
const severityColor = SEVERITY_COLORS2[severity] ??
|
|
7297
|
+
const severityColor = SEVERITY_COLORS2[severity] ?? chalk6.white;
|
|
8416
7298
|
const icon = severity === "critical" ? "\u26D4" : severity === "high" ? "\u{1F534}" : severity === "medium" ? "\u{1F7E1}" : "\u26AA";
|
|
8417
7299
|
const relPath = relative(basePath, gap.filePath);
|
|
8418
7300
|
const location = `${relPath}:${gap.lineStart}`;
|
|
8419
7301
|
const confidence = gap.confidence.toUpperCase();
|
|
8420
|
-
return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${
|
|
7302
|
+
return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk6.cyan(location.padEnd(30))} ${chalk6.gray(confidence)} confidence`;
|
|
8421
7303
|
}
|
|
8422
7304
|
function formatLanguages(result) {
|
|
8423
7305
|
const languages = [];
|
|
@@ -8520,7 +7402,7 @@ _...and ${gaps.length - 10} more ${severity} gaps_
|
|
|
8520
7402
|
}
|
|
8521
7403
|
} else {
|
|
8522
7404
|
lines.push("## No Gaps Detected\n");
|
|
8523
|
-
lines.push("
|
|
7405
|
+
lines.push("No vulnerabilities detected.\n");
|
|
8524
7406
|
}
|
|
8525
7407
|
lines.push("## Summary\n");
|
|
8526
7408
|
lines.push(`- Total Gaps: ${result.summary.totalGaps}`);
|
|
@@ -8577,7 +7459,7 @@ function buildSarifResults(result, basePath) {
|
|
|
8577
7459
|
ruleId: gap.categoryId,
|
|
8578
7460
|
level: sarifLevel(gap.severity),
|
|
8579
7461
|
message: {
|
|
8580
|
-
text: `
|
|
7462
|
+
text: `Potential vulnerability: ${gap.categoryName}`
|
|
8581
7463
|
},
|
|
8582
7464
|
locations: [
|
|
8583
7465
|
{
|
|
@@ -8640,637 +7522,1023 @@ function isValidScanOutputFormat(format) {
|
|
|
8640
7522
|
return ["terminal", "json", "markdown", "sarif", "html", "junit-xml"].includes(format);
|
|
8641
7523
|
}
|
|
8642
7524
|
|
|
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;
|
|
7525
|
+
// src/cli/commands/analyze.ts
|
|
7526
|
+
function registerAnalyzeCommand(program2) {
|
|
7527
|
+
program2.command("analyze [path]").description("Scan codebase for security vulnerabilities").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) => {
|
|
7528
|
+
const isQuiet = Boolean(options["quiet"]);
|
|
7529
|
+
const isVerbose = Boolean(options["verbose"]);
|
|
7530
|
+
if (isQuiet) {
|
|
7531
|
+
logger.configure({ level: "error" });
|
|
7532
|
+
} else if (isVerbose) {
|
|
7533
|
+
logger.configure({ level: "debug" });
|
|
8658
7534
|
}
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
}
|
|
8662
|
-
|
|
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
|
-
}
|
|
7535
|
+
const targetDirectory = resolve(targetPath ?? process.cwd());
|
|
7536
|
+
if (!existsSync(targetDirectory)) {
|
|
7537
|
+
console.error(formatError(new Error(`Directory not found: ${targetDirectory}`)));
|
|
7538
|
+
process.exit(1);
|
|
8703
7539
|
}
|
|
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));
|
|
7540
|
+
const outputFormat = String(options["output"] ?? "terminal");
|
|
7541
|
+
if (!isValidScanOutputFormat(outputFormat)) {
|
|
7542
|
+
console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json, markdown, sarif`)));
|
|
8723
7543
|
process.exit(1);
|
|
8724
7544
|
}
|
|
8725
|
-
|
|
8726
|
-
|
|
7545
|
+
const validSeverities = ["critical", "high", "medium", "low"];
|
|
7546
|
+
const minSeverity = String(options["severity"] ?? "low");
|
|
7547
|
+
if (!validSeverities.includes(minSeverity)) {
|
|
7548
|
+
console.error(formatError(new Error(`Invalid severity: ${minSeverity}. Use: critical, high, medium, low`)));
|
|
7549
|
+
process.exit(1);
|
|
8727
7550
|
}
|
|
8728
|
-
|
|
8729
|
-
const
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
detectTestFiles: true
|
|
8734
|
-
};
|
|
8735
|
-
if (domains.length > 0) {
|
|
8736
|
-
scanOptions.domains = domains;
|
|
7551
|
+
const validConfidences = ["high", "medium", "low"];
|
|
7552
|
+
const minConfidence = String(options["confidence"] ?? "high");
|
|
7553
|
+
if (!validConfidences.includes(minConfidence)) {
|
|
7554
|
+
console.error(formatError(new Error(`Invalid confidence: ${minConfidence}. Use: high, medium, low`)));
|
|
7555
|
+
process.exit(1);
|
|
8737
7556
|
}
|
|
8738
|
-
|
|
8739
|
-
|
|
7557
|
+
const domainsStr = options["domains"];
|
|
7558
|
+
let domains = [];
|
|
7559
|
+
if (domainsStr) {
|
|
7560
|
+
const domainList = domainsStr.split(",").map((d) => d.trim());
|
|
7561
|
+
for (const domain of domainList) {
|
|
7562
|
+
if (!RISK_DOMAINS.includes(domain)) {
|
|
7563
|
+
console.error(formatError(new Error(`Invalid domain: ${domain}. Valid domains: ${RISK_DOMAINS.join(", ")}`)));
|
|
7564
|
+
process.exit(1);
|
|
7565
|
+
}
|
|
7566
|
+
}
|
|
7567
|
+
domains = domainList;
|
|
8740
7568
|
}
|
|
8741
|
-
const
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
7569
|
+
const excludeStr = options["exclude"];
|
|
7570
|
+
const excludeDirs = excludeStr ? excludeStr.split(",").map((d) => d.trim()) : void 0;
|
|
7571
|
+
const failOn = options["failOn"];
|
|
7572
|
+
if (failOn && !["critical", "high", "medium"].includes(failOn)) {
|
|
7573
|
+
console.error(formatError(new Error(`Invalid fail-on level: ${failOn}. Use: critical, high, medium`)));
|
|
8745
7574
|
process.exit(1);
|
|
8746
7575
|
}
|
|
8747
|
-
|
|
8748
|
-
const
|
|
8749
|
-
|
|
8750
|
-
const
|
|
8751
|
-
const
|
|
8752
|
-
|
|
8753
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
8756
|
-
console.
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
7576
|
+
const showSpinner = outputFormat === "terminal" && !isQuiet;
|
|
7577
|
+
const spinner = showSpinner ? ora3("Loading categories...").start() : null;
|
|
7578
|
+
try {
|
|
7579
|
+
const store = createCategoryStore();
|
|
7580
|
+
const definitionsPath = getDefinitionsPath();
|
|
7581
|
+
logger.debug(`Loading categories from: ${definitionsPath}`);
|
|
7582
|
+
const loadResult = await store.loadFromDirectory(definitionsPath);
|
|
7583
|
+
if (!loadResult.success) {
|
|
7584
|
+
spinner?.fail("Failed to load categories");
|
|
7585
|
+
console.error(formatError(loadResult.error));
|
|
7586
|
+
process.exit(1);
|
|
7587
|
+
}
|
|
7588
|
+
if (spinner) {
|
|
7589
|
+
spinner.text = `Loaded ${loadResult.data} categories. Scanning...`;
|
|
7590
|
+
}
|
|
7591
|
+
const scanner = createScanner(store);
|
|
7592
|
+
const scanOptions = {
|
|
7593
|
+
minSeverity,
|
|
7594
|
+
minConfidence,
|
|
7595
|
+
detectTestFiles: true
|
|
7596
|
+
};
|
|
7597
|
+
if (domains.length > 0) {
|
|
7598
|
+
scanOptions.domains = domains;
|
|
7599
|
+
}
|
|
7600
|
+
if (excludeDirs) {
|
|
7601
|
+
scanOptions.excludeDirs = excludeDirs;
|
|
7602
|
+
}
|
|
7603
|
+
const scanResult = await scanner.scanDirectory(targetDirectory, scanOptions);
|
|
7604
|
+
if (!scanResult.success) {
|
|
7605
|
+
spinner?.fail("Scan failed");
|
|
7606
|
+
console.error(formatError(scanResult.error));
|
|
7607
|
+
process.exit(1);
|
|
7608
|
+
}
|
|
7609
|
+
spinner?.stop();
|
|
7610
|
+
const shouldVerify = Boolean(options["verify"]);
|
|
7611
|
+
if (shouldVerify && scanResult.data.gaps.length > 0) {
|
|
7612
|
+
const { hasApiKey: hasApiKey2, setConfigValue: setConfigValue2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
7613
|
+
const { createInterface } = await import('readline');
|
|
7614
|
+
let provider = "anthropic";
|
|
7615
|
+
if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
|
|
7616
|
+
spinner?.stop();
|
|
7617
|
+
console.log(chalk6.yellow("\nAI verification requires an API key."));
|
|
7618
|
+
console.log(chalk6.gray("Get one at: https://console.anthropic.com/settings/keys"));
|
|
7619
|
+
console.log(chalk6.gray("Or: https://platform.openai.com/api-keys\n"));
|
|
7620
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
7621
|
+
const askQuestion = (question) => new Promise((resolve9) => rl.question(question, (answer) => resolve9(answer.trim())));
|
|
7622
|
+
const apiKey = await askQuestion(chalk6.cyan("Enter your Anthropic or OpenAI API key: "));
|
|
7623
|
+
rl.close();
|
|
7624
|
+
if (!apiKey) {
|
|
7625
|
+
console.log(chalk6.red("No API key provided. Skipping AI verification."));
|
|
8773
7626
|
} else {
|
|
8774
|
-
|
|
8775
|
-
|
|
8776
|
-
|
|
7627
|
+
if (apiKey.startsWith("sk-ant-")) {
|
|
7628
|
+
setConfigValue2("anthropicApiKey", apiKey);
|
|
7629
|
+
provider = "anthropic";
|
|
7630
|
+
console.log(chalk6.green("Anthropic API key saved to ~/.pinata/config.json\n"));
|
|
7631
|
+
} else {
|
|
7632
|
+
setConfigValue2("openaiApiKey", apiKey);
|
|
7633
|
+
provider = "openai";
|
|
7634
|
+
console.log(chalk6.green("OpenAI API key saved to ~/.pinata/config.json\n"));
|
|
7635
|
+
}
|
|
8777
7636
|
}
|
|
7637
|
+
} else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
|
|
7638
|
+
provider = "openai";
|
|
8778
7639
|
}
|
|
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
|
-
|
|
7640
|
+
if (hasApiKey2(provider)) {
|
|
7641
|
+
const verifySpinner = showSpinner ? ora3("Verifying gaps with AI...").start() : null;
|
|
7642
|
+
try {
|
|
7643
|
+
const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
|
|
7644
|
+
const { readFile: readFile7 } = await import('fs/promises');
|
|
7645
|
+
const apiKey = getApiKey2(provider);
|
|
7646
|
+
const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
|
|
7647
|
+
const { verified, dismissed, stats } = await verifier.verifyAll(
|
|
7648
|
+
scanResult.data.gaps,
|
|
7649
|
+
async (path2) => readFile7(path2, "utf-8")
|
|
7650
|
+
);
|
|
7651
|
+
scanResult.data.gaps = verified;
|
|
7652
|
+
const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
|
|
7653
|
+
let deduction = 0;
|
|
7654
|
+
for (const gap of verified) {
|
|
7655
|
+
deduction += severityWeights[gap.severity] ?? 1;
|
|
7656
|
+
}
|
|
7657
|
+
const newOverall = Math.max(0, 100 - deduction);
|
|
7658
|
+
const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
|
|
7659
|
+
scanResult.data.score.overall = newOverall;
|
|
7660
|
+
scanResult.data.score.grade = newGrade;
|
|
7661
|
+
verifySpinner?.succeed(
|
|
7662
|
+
`AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
|
|
7663
|
+
);
|
|
7664
|
+
if (isVerbose && dismissed.length > 0) {
|
|
7665
|
+
console.log(chalk6.gray("\nDismissed as false positives:"));
|
|
7666
|
+
for (const { gap, reason } of dismissed.slice(0, 5)) {
|
|
7667
|
+
console.log(chalk6.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
|
|
7668
|
+
console.log(chalk6.gray(` Reason: ${reason.slice(0, 100)}...`));
|
|
7669
|
+
}
|
|
7670
|
+
if (dismissed.length > 5) {
|
|
7671
|
+
console.log(chalk6.gray(` ... and ${dismissed.length - 5} more`));
|
|
7672
|
+
}
|
|
8812
7673
|
}
|
|
8813
|
-
|
|
8814
|
-
|
|
7674
|
+
} catch (error) {
|
|
7675
|
+
verifySpinner?.fail("AI verification failed (results unverified)");
|
|
7676
|
+
if (isVerbose) {
|
|
7677
|
+
console.error(chalk6.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8815
7678
|
}
|
|
8816
7679
|
}
|
|
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
7680
|
}
|
|
8823
7681
|
}
|
|
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}`));
|
|
7682
|
+
const shouldExecute = Boolean(options["execute"]);
|
|
7683
|
+
const isDryRun = Boolean(options["dryRun"]);
|
|
7684
|
+
if (shouldExecute && scanResult.data.gaps.length > 0) {
|
|
7685
|
+
const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
|
|
7686
|
+
const { readFile: readFile7 } = await import('fs/promises');
|
|
7687
|
+
const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
|
|
7688
|
+
if (testableGaps.length === 0) {
|
|
7689
|
+
console.log(chalk6.yellow("\nNo dynamically testable gaps found."));
|
|
8840
7690
|
} else {
|
|
8841
|
-
const
|
|
8842
|
-
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
7691
|
+
const runner = createRunner2(void 0, isDryRun);
|
|
7692
|
+
const initResult = await runner.initialize();
|
|
7693
|
+
if (!initResult.ready) {
|
|
7694
|
+
console.log(chalk6.red(`
|
|
7695
|
+
Dynamic execution unavailable: ${initResult.error}`));
|
|
7696
|
+
} else {
|
|
7697
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
7698
|
+
for (const gap of testableGaps) {
|
|
7699
|
+
if (!fileContents.has(gap.filePath)) {
|
|
7700
|
+
try {
|
|
7701
|
+
fileContents.set(gap.filePath, await readFile7(gap.filePath, "utf-8"));
|
|
7702
|
+
} catch {
|
|
7703
|
+
}
|
|
8847
7704
|
}
|
|
8848
7705
|
}
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8853
|
-
|
|
8854
|
-
|
|
8855
|
-
|
|
8856
|
-
|
|
8857
|
-
|
|
7706
|
+
const executionSummary = await runner.executeAll(testableGaps, fileContents);
|
|
7707
|
+
for (const result of executionSummary.results) {
|
|
7708
|
+
const gap = scanResult.data.gaps.find(
|
|
7709
|
+
(g) => g.filePath === result.gap.filePath && g.lineStart === result.gap.lineStart
|
|
7710
|
+
);
|
|
7711
|
+
if (gap && result.status === "confirmed") {
|
|
7712
|
+
gap.confirmed = true;
|
|
7713
|
+
gap.evidence = result.evidence;
|
|
7714
|
+
}
|
|
8858
7715
|
}
|
|
8859
|
-
|
|
8860
|
-
|
|
8861
|
-
console.log(chalk5.red.bold(`
|
|
7716
|
+
if (executionSummary.confirmed > 0) {
|
|
7717
|
+
console.log(chalk6.red.bold(`
|
|
8862
7718
|
\u26A0\uFE0F ${executionSummary.confirmed} CONFIRMED vulnerabilities found!`));
|
|
7719
|
+
}
|
|
8863
7720
|
}
|
|
8864
7721
|
}
|
|
8865
7722
|
}
|
|
7723
|
+
const cacheResult = await saveScanResults(process.cwd(), scanResult.data);
|
|
7724
|
+
if (!cacheResult.success) {
|
|
7725
|
+
logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
|
|
7726
|
+
}
|
|
7727
|
+
const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
|
|
7728
|
+
const outputFile = options["outputFile"];
|
|
7729
|
+
if (outputFile) {
|
|
7730
|
+
writeFileSync(resolve(outputFile), output, "utf-8");
|
|
7731
|
+
logger.info(`Results written to: ${resolve(outputFile)}`);
|
|
7732
|
+
} else {
|
|
7733
|
+
console.log(output);
|
|
7734
|
+
}
|
|
7735
|
+
if (isVerbose && scanResult.data.warnings.length > 0) {
|
|
7736
|
+
console.error("\nWarnings:");
|
|
7737
|
+
for (const warning of scanResult.data.warnings) {
|
|
7738
|
+
console.error(` - ${warning}`);
|
|
7739
|
+
}
|
|
7740
|
+
}
|
|
7741
|
+
if (failOn) {
|
|
7742
|
+
const severityOrder = { critical: 3, high: 2, medium: 1 };
|
|
7743
|
+
const failLevel = severityOrder[failOn] ?? 0;
|
|
7744
|
+
const hasFailingGaps = scanResult.data.gaps.some((gap) => (severityOrder[gap.severity] ?? 0) >= failLevel);
|
|
7745
|
+
if (hasFailingGaps) {
|
|
7746
|
+
process.exit(1);
|
|
7747
|
+
}
|
|
7748
|
+
}
|
|
7749
|
+
process.exit(0);
|
|
7750
|
+
} catch (error) {
|
|
7751
|
+
spinner?.fail("Analysis failed");
|
|
7752
|
+
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
7753
|
+
process.exit(1);
|
|
7754
|
+
}
|
|
7755
|
+
});
|
|
7756
|
+
}
|
|
7757
|
+
init_results_cache();
|
|
7758
|
+
var FRAMEWORK_INDICATORS = {
|
|
7759
|
+
vitest: {
|
|
7760
|
+
deps: [],
|
|
7761
|
+
devDeps: ["vitest"],
|
|
7762
|
+
files: ["vitest.config.ts", "vitest.config.js", "vitest.config.mts"]
|
|
7763
|
+
},
|
|
7764
|
+
jest: {
|
|
7765
|
+
deps: [],
|
|
7766
|
+
devDeps: ["jest", "@jest/core", "ts-jest"],
|
|
7767
|
+
files: ["jest.config.ts", "jest.config.js", "jest.config.mjs"]
|
|
7768
|
+
},
|
|
7769
|
+
pytest: {
|
|
7770
|
+
deps: ["pytest"],
|
|
7771
|
+
devDeps: ["pytest"],
|
|
7772
|
+
files: ["pytest.ini", "pyproject.toml", "setup.cfg", "conftest.py"]
|
|
7773
|
+
},
|
|
7774
|
+
"go-test": {
|
|
7775
|
+
deps: [],
|
|
7776
|
+
devDeps: [],
|
|
7777
|
+
files: ["go.mod"]
|
|
7778
|
+
},
|
|
7779
|
+
mocha: {
|
|
7780
|
+
deps: [],
|
|
7781
|
+
devDeps: ["mocha"],
|
|
7782
|
+
files: [".mocharc.yml", ".mocharc.js"]
|
|
7783
|
+
}
|
|
7784
|
+
};
|
|
7785
|
+
var FRAMEWORK_DEFAULTS = {
|
|
7786
|
+
vitest: {
|
|
7787
|
+
name: "vitest",
|
|
7788
|
+
importStyle: 'import { describe, it, expect, beforeEach } from "vitest";',
|
|
7789
|
+
runner: "npx vitest run"
|
|
7790
|
+
},
|
|
7791
|
+
jest: {
|
|
7792
|
+
name: "jest",
|
|
7793
|
+
importStyle: "// jest globals auto-imported",
|
|
7794
|
+
runner: "npx jest"
|
|
7795
|
+
},
|
|
7796
|
+
pytest: {
|
|
7797
|
+
name: "pytest",
|
|
7798
|
+
importStyle: "import pytest",
|
|
7799
|
+
runner: "pytest"
|
|
7800
|
+
},
|
|
7801
|
+
"go-test": {
|
|
7802
|
+
name: "go-test",
|
|
7803
|
+
importStyle: 'import "testing"',
|
|
7804
|
+
runner: "go test ./..."
|
|
7805
|
+
},
|
|
7806
|
+
mocha: {
|
|
7807
|
+
name: "mocha",
|
|
7808
|
+
importStyle: 'import { describe, it } from "mocha"; import { expect } from "chai";',
|
|
7809
|
+
runner: "npx mocha"
|
|
7810
|
+
}
|
|
7811
|
+
};
|
|
7812
|
+
var EXT_TO_LANG = {
|
|
7813
|
+
".ts": "typescript",
|
|
7814
|
+
".tsx": "typescript",
|
|
7815
|
+
".js": "javascript",
|
|
7816
|
+
".jsx": "javascript",
|
|
7817
|
+
".py": "python",
|
|
7818
|
+
".go": "go",
|
|
7819
|
+
".java": "java",
|
|
7820
|
+
".rs": "rust"
|
|
7821
|
+
};
|
|
7822
|
+
var WEB_FRAMEWORK_PATTERNS = [
|
|
7823
|
+
[/from\s+["']express["']|require\s*\(\s*["']express["']\)/, "express"],
|
|
7824
|
+
[/from\s+["']fastify["']/, "fastify"],
|
|
7825
|
+
[/from\s+["']koa["']/, "koa"],
|
|
7826
|
+
[/from\s+["']@nestjs\//, "nestjs"],
|
|
7827
|
+
[/from\s+["']next["']|from\s+["']next\//, "nextjs"],
|
|
7828
|
+
[/from\s+flask\s+import|import\s+flask/, "flask"],
|
|
7829
|
+
[/from\s+django|import\s+django/, "django"],
|
|
7830
|
+
[/from\s+fastapi|import\s+fastapi/, "fastapi"],
|
|
7831
|
+
[/"github\.com\/gin-gonic\/gin"/, "gin"],
|
|
7832
|
+
[/"github\.com\/gofiber\/fiber"/, "fiber"]
|
|
7833
|
+
];
|
|
7834
|
+
var DB_PATTERNS = [
|
|
7835
|
+
[/prisma|@prisma\/client/, "postgres"],
|
|
7836
|
+
[/pg\b|postgres|postgresql/, "postgres"],
|
|
7837
|
+
[/mysql2?["'\s]|from\s+["']mysql/, "mysql"],
|
|
7838
|
+
[/mongoose|mongodb|MongoClient/, "mongodb"],
|
|
7839
|
+
[/sqlite3|better-sqlite/, "sqlite"],
|
|
7840
|
+
[/sqlalchemy|psycopg2/, "postgres"],
|
|
7841
|
+
[/pymysql|mysqlclient/, "mysql"]
|
|
7842
|
+
];
|
|
7843
|
+
function extractFunction(source, targetLine, language) {
|
|
7844
|
+
const lines = source.split("\n");
|
|
7845
|
+
const idx = targetLine - 1;
|
|
7846
|
+
if (idx < 0 || idx >= lines.length) {
|
|
7847
|
+
const start = Math.max(0, idx - 10);
|
|
7848
|
+
const end = Math.min(lines.length, idx + 10);
|
|
7849
|
+
return { body: lines.slice(start, end).join("\n"), name: void 0 };
|
|
7850
|
+
}
|
|
7851
|
+
if (language === "python") {
|
|
7852
|
+
return extractPythonFunction(lines, idx);
|
|
7853
|
+
}
|
|
7854
|
+
return extractBraceFunction(lines, idx);
|
|
7855
|
+
}
|
|
7856
|
+
function findBalancedEnd(lines, startLine) {
|
|
7857
|
+
let depth = 0;
|
|
7858
|
+
let started = false;
|
|
7859
|
+
let endIdx = startLine;
|
|
7860
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
7861
|
+
const line = lines[i];
|
|
7862
|
+
for (const ch of line) {
|
|
7863
|
+
if (ch === "{") {
|
|
7864
|
+
depth++;
|
|
7865
|
+
started = true;
|
|
7866
|
+
}
|
|
7867
|
+
if (ch === "}") depth--;
|
|
7868
|
+
if (started && depth === 0) {
|
|
7869
|
+
endIdx = i;
|
|
7870
|
+
return { endIdx, started };
|
|
7871
|
+
}
|
|
7872
|
+
}
|
|
7873
|
+
}
|
|
7874
|
+
return { endIdx, started };
|
|
7875
|
+
}
|
|
7876
|
+
var SIGNATURE_PATTERN = /^(export\s+)?(async\s+)?function\s|^(export\s+)?(const|let|var)\s+\w+\s*=|^\w+\s*\(|^(public|private|protected)\s/;
|
|
7877
|
+
var NAME_PATTERN = /function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=|(\w+)\s*\(/;
|
|
7878
|
+
function extractBraceFunction(lines, targetIdx) {
|
|
7879
|
+
let startIdx = targetIdx;
|
|
7880
|
+
let braceDepth = 0;
|
|
7881
|
+
let foundOpenBrace = false;
|
|
7882
|
+
for (let i = targetIdx; i >= 0; i--) {
|
|
7883
|
+
const line = lines[i];
|
|
7884
|
+
for (let j = line.length - 1; j >= 0; j--) {
|
|
7885
|
+
if (line[j] === "}") braceDepth++;
|
|
7886
|
+
if (line[j] === "{") {
|
|
7887
|
+
braceDepth--;
|
|
7888
|
+
if (braceDepth < 0) {
|
|
7889
|
+
startIdx = i;
|
|
7890
|
+
foundOpenBrace = true;
|
|
7891
|
+
break;
|
|
7892
|
+
}
|
|
7893
|
+
}
|
|
8866
7894
|
}
|
|
8867
|
-
|
|
8868
|
-
|
|
8869
|
-
|
|
7895
|
+
if (foundOpenBrace) break;
|
|
7896
|
+
}
|
|
7897
|
+
let sigStart = startIdx;
|
|
7898
|
+
for (let i = startIdx; i >= Math.max(0, startIdx - 5); i--) {
|
|
7899
|
+
const line = lines[i].trim();
|
|
7900
|
+
if (line.match(SIGNATURE_PATTERN)) {
|
|
7901
|
+
sigStart = i;
|
|
7902
|
+
break;
|
|
8870
7903
|
}
|
|
8871
|
-
|
|
8872
|
-
|
|
8873
|
-
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
7904
|
+
}
|
|
7905
|
+
const { endIdx } = findBalancedEnd(lines, sigStart);
|
|
7906
|
+
const body = lines.slice(sigStart, endIdx + 1).join("\n");
|
|
7907
|
+
const sigLine = lines[sigStart]?.trim() ?? "";
|
|
7908
|
+
const nameMatch = sigLine.match(NAME_PATTERN);
|
|
7909
|
+
const name = nameMatch?.[1] ?? nameMatch?.[2] ?? nameMatch?.[3];
|
|
7910
|
+
return { body, name };
|
|
7911
|
+
}
|
|
7912
|
+
var PYTHON_DEF_PATTERN = /^(\s*)def\s+\w+|^(\s*)class\s+\w+|^(\s*)async\s+def\s+\w+/;
|
|
7913
|
+
var PYTHON_INDENT_PATTERN = /^(\s*)/;
|
|
7914
|
+
var PYTHON_NAME_PATTERN = /def\s+(\w+)|class\s+(\w+)/;
|
|
7915
|
+
function extractPythonFunction(lines, targetIdx) {
|
|
7916
|
+
let startIdx = targetIdx;
|
|
7917
|
+
for (let i = targetIdx; i >= 0; i--) {
|
|
7918
|
+
if (lines[i].match(PYTHON_DEF_PATTERN)) {
|
|
7919
|
+
startIdx = i;
|
|
7920
|
+
break;
|
|
7921
|
+
}
|
|
7922
|
+
}
|
|
7923
|
+
const indent = (lines[startIdx].match(PYTHON_INDENT_PATTERN) ?? ["", ""])[1].length;
|
|
7924
|
+
let endIdx = targetIdx;
|
|
7925
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
7926
|
+
const line = lines[i];
|
|
7927
|
+
const trimmedLine = line.trim();
|
|
7928
|
+
if (trimmedLine === "") continue;
|
|
7929
|
+
const lineIndent = (line.match(PYTHON_INDENT_PATTERN) ?? ["", ""])[1].length;
|
|
7930
|
+
if (lineIndent <= indent && trimmedLine !== "") {
|
|
7931
|
+
endIdx = i - 1;
|
|
7932
|
+
break;
|
|
8879
7933
|
}
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
7934
|
+
endIdx = i;
|
|
7935
|
+
}
|
|
7936
|
+
const body = lines.slice(startIdx, endIdx + 1).join("\n");
|
|
7937
|
+
const nameMatch = lines[startIdx].match(PYTHON_NAME_PATTERN);
|
|
7938
|
+
const name = nameMatch?.[1] ?? nameMatch?.[2];
|
|
7939
|
+
return { body, name };
|
|
7940
|
+
}
|
|
7941
|
+
var REQUIRE_PATTERN = /^const\s+\w+\s*=\s*require\(/;
|
|
7942
|
+
function extractImports(source, language) {
|
|
7943
|
+
const lines = source.split("\n");
|
|
7944
|
+
const imports = [];
|
|
7945
|
+
for (const line of lines) {
|
|
7946
|
+
const trimmed = line.trim();
|
|
7947
|
+
if (language === "python") {
|
|
7948
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("from ")) {
|
|
7949
|
+
imports.push(trimmed);
|
|
7950
|
+
}
|
|
7951
|
+
} else if (language === "typescript" || language === "javascript") {
|
|
7952
|
+
if (trimmed.startsWith("import ") || trimmed.match(REQUIRE_PATTERN)) {
|
|
7953
|
+
imports.push(trimmed);
|
|
7954
|
+
}
|
|
7955
|
+
} else if (language === "go") {
|
|
7956
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith('"')) {
|
|
7957
|
+
imports.push(trimmed);
|
|
7958
|
+
}
|
|
7959
|
+
} else if (language === "java") {
|
|
7960
|
+
if (trimmed.startsWith("import ")) {
|
|
7961
|
+
imports.push(trimmed);
|
|
8884
7962
|
}
|
|
8885
7963
|
}
|
|
8886
|
-
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
const
|
|
8895
|
-
|
|
8896
|
-
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
8903
|
-
|
|
7964
|
+
}
|
|
7965
|
+
return imports;
|
|
7966
|
+
}
|
|
7967
|
+
async function detectTestFramework(projectRoot, language) {
|
|
7968
|
+
if (language === "typescript" || language === "javascript") {
|
|
7969
|
+
const pkgPath = resolve(projectRoot, "package.json");
|
|
7970
|
+
if (existsSync(pkgPath)) {
|
|
7971
|
+
try {
|
|
7972
|
+
const pkg = JSON.parse(await readFile(pkgPath, "utf-8"));
|
|
7973
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
7974
|
+
for (const [framework, indicators] of Object.entries(FRAMEWORK_INDICATORS)) {
|
|
7975
|
+
for (const dep of [...indicators.deps, ...indicators.devDeps]) {
|
|
7976
|
+
if (dep in allDeps) {
|
|
7977
|
+
return FRAMEWORK_DEFAULTS[framework];
|
|
7978
|
+
}
|
|
7979
|
+
}
|
|
7980
|
+
}
|
|
7981
|
+
} catch {
|
|
8904
7982
|
}
|
|
8905
7983
|
}
|
|
8906
|
-
|
|
8907
|
-
|
|
8908
|
-
|
|
8909
|
-
|
|
8910
|
-
|
|
7984
|
+
for (const [framework, indicators] of Object.entries(FRAMEWORK_INDICATORS)) {
|
|
7985
|
+
for (const file of indicators.files) {
|
|
7986
|
+
if (existsSync(resolve(projectRoot, file))) {
|
|
7987
|
+
return FRAMEWORK_DEFAULTS[framework];
|
|
7988
|
+
}
|
|
7989
|
+
}
|
|
7990
|
+
}
|
|
7991
|
+
return language === "typescript" ? FRAMEWORK_DEFAULTS["vitest"] : FRAMEWORK_DEFAULTS["jest"];
|
|
8911
7992
|
}
|
|
8912
|
-
|
|
8913
|
-
|
|
8914
|
-
|
|
8915
|
-
|
|
8916
|
-
|
|
8917
|
-
const
|
|
8918
|
-
const
|
|
8919
|
-
const
|
|
8920
|
-
|
|
8921
|
-
|
|
8922
|
-
|
|
8923
|
-
|
|
7993
|
+
if (language === "python") return FRAMEWORK_DEFAULTS["pytest"];
|
|
7994
|
+
if (language === "go") return FRAMEWORK_DEFAULTS["go-test"];
|
|
7995
|
+
return FRAMEWORK_DEFAULTS["vitest"];
|
|
7996
|
+
}
|
|
7997
|
+
async function findExistingTest(filePath, projectRoot) {
|
|
7998
|
+
const base = basename(filePath, extname(filePath));
|
|
7999
|
+
const dir = dirname(filePath);
|
|
8000
|
+
const rel = relative(projectRoot, dir);
|
|
8001
|
+
const candidates = [
|
|
8002
|
+
resolve(dir, `${base}.test${extname(filePath)}`),
|
|
8003
|
+
resolve(dir, `${base}.spec${extname(filePath)}`),
|
|
8004
|
+
resolve(dir, `__tests__/${base}${extname(filePath)}`),
|
|
8005
|
+
resolve(projectRoot, "tests", rel, `${base}.test${extname(filePath)}`),
|
|
8006
|
+
resolve(projectRoot, "test", rel, `${base}.test${extname(filePath)}`)
|
|
8007
|
+
];
|
|
8008
|
+
for (const candidate of candidates) {
|
|
8009
|
+
if (existsSync(candidate)) {
|
|
8010
|
+
try {
|
|
8011
|
+
const content = await readFile(candidate, "utf-8");
|
|
8012
|
+
return content.split("\n").slice(0, 50).join("\n");
|
|
8013
|
+
} catch {
|
|
8014
|
+
}
|
|
8015
|
+
}
|
|
8924
8016
|
}
|
|
8925
|
-
|
|
8926
|
-
|
|
8927
|
-
|
|
8017
|
+
return void 0;
|
|
8018
|
+
}
|
|
8019
|
+
function suggestTestPath(gap, projectRoot, language) {
|
|
8020
|
+
relative(projectRoot, gap.filePath);
|
|
8021
|
+
const base = basename(gap.filePath, extname(gap.filePath));
|
|
8022
|
+
const ext = language === "python" ? ".py" : extname(gap.filePath);
|
|
8023
|
+
const safeCategory = gap.categoryId.replace(/[^a-z0-9-]/g, "-");
|
|
8024
|
+
return resolve(projectRoot, "tests", "security", `${safeCategory}-${base}.test${ext}`);
|
|
8025
|
+
}
|
|
8026
|
+
function detectFromCode(source, patterns) {
|
|
8027
|
+
for (const [pattern, name] of patterns) {
|
|
8028
|
+
if (pattern.test(source)) return name;
|
|
8928
8029
|
}
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8932
|
-
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
|
|
8030
|
+
return void 0;
|
|
8031
|
+
}
|
|
8032
|
+
async function extractTestContext(gap, projectRoot) {
|
|
8033
|
+
const ext = extname(gap.filePath);
|
|
8034
|
+
const language = EXT_TO_LANG[ext] ?? "unknown";
|
|
8035
|
+
const fileSource = await readFile(gap.filePath, "utf-8");
|
|
8036
|
+
const { body: functionBody, name: functionName } = extractFunction(fileSource, gap.lineStart, language);
|
|
8037
|
+
const imports = extractImports(fileSource, language);
|
|
8038
|
+
const testFramework = await detectTestFramework(projectRoot, language);
|
|
8039
|
+
const webFramework = detectFromCode(fileSource, WEB_FRAMEWORK_PATTERNS);
|
|
8040
|
+
const dbType = detectFromCode(fileSource, DB_PATTERNS);
|
|
8041
|
+
const existingTestSample = await findExistingTest(gap.filePath, projectRoot);
|
|
8042
|
+
const suggestedTestPath = suggestTestPath(gap, projectRoot, language);
|
|
8043
|
+
return {
|
|
8044
|
+
gap,
|
|
8045
|
+
fileSource,
|
|
8046
|
+
functionBody,
|
|
8047
|
+
functionName,
|
|
8048
|
+
imports,
|
|
8049
|
+
language,
|
|
8050
|
+
testFramework,
|
|
8051
|
+
suggestedTestPath,
|
|
8052
|
+
webFramework,
|
|
8053
|
+
dbType,
|
|
8054
|
+
existingTestSample,
|
|
8055
|
+
projectRoot
|
|
8056
|
+
};
|
|
8057
|
+
}
|
|
8058
|
+
async function extractTestContexts(gaps, projectRoot) {
|
|
8059
|
+
const contexts = [];
|
|
8060
|
+
for (const gap of gaps) {
|
|
8061
|
+
try {
|
|
8062
|
+
const ctx = await extractTestContext(gap, projectRoot);
|
|
8063
|
+
contexts.push(ctx);
|
|
8064
|
+
} catch {
|
|
8065
|
+
}
|
|
8937
8066
|
}
|
|
8938
|
-
|
|
8939
|
-
|
|
8940
|
-
|
|
8941
|
-
|
|
8942
|
-
|
|
8067
|
+
return contexts;
|
|
8068
|
+
}
|
|
8069
|
+
|
|
8070
|
+
// src/testgen/generator.ts
|
|
8071
|
+
function buildGenerationPrompt(ctx) {
|
|
8072
|
+
const parts = [];
|
|
8073
|
+
parts.push(`Generate a complete, runnable ${ctx.testFramework.name} test file for this security vulnerability.`);
|
|
8074
|
+
parts.push("");
|
|
8075
|
+
parts.push("## Vulnerability");
|
|
8076
|
+
parts.push(`Type: ${ctx.gap.categoryId}`);
|
|
8077
|
+
parts.push(`Severity: ${ctx.gap.severity}`);
|
|
8078
|
+
parts.push(`File: ${ctx.gap.filePath}:${ctx.gap.lineStart}`);
|
|
8079
|
+
parts.push(`Pattern: ${ctx.gap.patternId}`);
|
|
8080
|
+
parts.push("");
|
|
8081
|
+
parts.push("## Vulnerable Code");
|
|
8082
|
+
parts.push("```");
|
|
8083
|
+
parts.push(ctx.functionBody);
|
|
8084
|
+
parts.push("```");
|
|
8085
|
+
parts.push("");
|
|
8086
|
+
if (ctx.functionName) {
|
|
8087
|
+
parts.push(`Function name: ${ctx.functionName}`);
|
|
8943
8088
|
}
|
|
8944
|
-
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8948
|
-
|
|
8949
|
-
|
|
8950
|
-
|
|
8089
|
+
parts.push("## File Imports");
|
|
8090
|
+
parts.push("```");
|
|
8091
|
+
parts.push(ctx.imports.join("\n"));
|
|
8092
|
+
parts.push("```");
|
|
8093
|
+
parts.push("");
|
|
8094
|
+
parts.push("## Context");
|
|
8095
|
+
parts.push(`Language: ${ctx.language}`);
|
|
8096
|
+
parts.push(`Test framework: ${ctx.testFramework.name}`);
|
|
8097
|
+
if (ctx.webFramework) parts.push(`Web framework: ${ctx.webFramework}`);
|
|
8098
|
+
if (ctx.dbType) parts.push(`Database: ${ctx.dbType}`);
|
|
8099
|
+
parts.push("");
|
|
8100
|
+
if (ctx.existingTestSample) {
|
|
8101
|
+
parts.push("## Existing Test Style (match this style)");
|
|
8102
|
+
parts.push("```");
|
|
8103
|
+
parts.push(ctx.existingTestSample.slice(0, 1500));
|
|
8104
|
+
parts.push("```");
|
|
8105
|
+
parts.push("");
|
|
8106
|
+
}
|
|
8107
|
+
parts.push("## Requirements");
|
|
8108
|
+
parts.push("1. Output ONLY the complete test file. No explanations, no markdown fences.");
|
|
8109
|
+
parts.push("2. Use real imports that resolve in this project.");
|
|
8110
|
+
parts.push("3. The test MUST FAIL when run against the current vulnerable code.");
|
|
8111
|
+
parts.push("4. Include at least 5 attack payloads specific to this vulnerability type.");
|
|
8112
|
+
parts.push("5. Include at least one boundary/edge case (empty string, null, very long input, unicode).");
|
|
8113
|
+
parts.push("6. If testing an HTTP endpoint, use supertest or direct function calls.");
|
|
8114
|
+
parts.push("7. Test the specific vulnerable code path, not a generic function.");
|
|
8115
|
+
parts.push("8. Each test should have a clear assertion that proves the vulnerability exists or is mitigated.");
|
|
8116
|
+
return parts.join("\n");
|
|
8117
|
+
}
|
|
8118
|
+
function buildPropertyPrompt(ctx) {
|
|
8119
|
+
const parts = [];
|
|
8120
|
+
parts.push(`Generate a property-based test using fast-check (TypeScript) or hypothesis (Python) for this security vulnerability.`);
|
|
8121
|
+
parts.push("");
|
|
8122
|
+
parts.push("## Vulnerability");
|
|
8123
|
+
parts.push(`Type: ${ctx.gap.categoryId}`);
|
|
8124
|
+
parts.push(`File: ${ctx.gap.filePath}:${ctx.gap.lineStart}`);
|
|
8125
|
+
parts.push("");
|
|
8126
|
+
parts.push("## Vulnerable Code");
|
|
8127
|
+
parts.push("```");
|
|
8128
|
+
parts.push(ctx.functionBody);
|
|
8129
|
+
parts.push("```");
|
|
8130
|
+
parts.push("");
|
|
8131
|
+
parts.push("## Requirements");
|
|
8132
|
+
parts.push("1. Output ONLY the complete test file. No explanations.");
|
|
8133
|
+
parts.push("2. Express a security INVARIANT as a property.");
|
|
8134
|
+
parts.push("3. The property should hold for ALL inputs, not just specific payloads.");
|
|
8135
|
+
parts.push("4. Use fast-check for TypeScript/JavaScript or hypothesis for Python.");
|
|
8136
|
+
parts.push("5. Example invariant: 'for all strings s, the output of sanitize(s) never contains <script>'");
|
|
8137
|
+
parts.push(`6. Test framework: ${ctx.testFramework.name}`);
|
|
8138
|
+
const invariantHints = {
|
|
8139
|
+
"sql-injection": "user input should never appear unescaped in the SQL query string",
|
|
8140
|
+
"xss": "user input should never appear as raw HTML in the output",
|
|
8141
|
+
"command-injection": "user input should never be passed to a shell command unescaped",
|
|
8142
|
+
"path-traversal": "resolved file path should always stay within the allowed directory",
|
|
8143
|
+
"ssrf": "user-supplied URL should never resolve to a private/internal IP",
|
|
8144
|
+
"xxe": "XML parsing should never resolve external entities",
|
|
8145
|
+
"deserialization": "deserialized objects should only be of expected types",
|
|
8146
|
+
"hardcoded-secrets": "no string matching secret patterns should exist in source"
|
|
8147
|
+
};
|
|
8148
|
+
const hint = invariantHints[ctx.gap.categoryId];
|
|
8149
|
+
if (hint) {
|
|
8150
|
+
parts.push(`7. Invariant hint: "${hint}"`);
|
|
8951
8151
|
}
|
|
8952
|
-
|
|
8953
|
-
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8152
|
+
return parts.join("\n");
|
|
8153
|
+
}
|
|
8154
|
+
async function generateTest(ctx, callAI) {
|
|
8155
|
+
const systemPrompt = [
|
|
8156
|
+
"You are a senior security engineer writing adversarial tests.",
|
|
8157
|
+
"You write tests that BREAK code, not tests that pass.",
|
|
8158
|
+
"Your tests must be complete, runnable files with real imports.",
|
|
8159
|
+
"Output ONLY code. No markdown fences. No explanations.",
|
|
8160
|
+
"The test must FAIL against vulnerable code and PASS after a fix."
|
|
8161
|
+
].join(" ");
|
|
8162
|
+
const prompt = buildGenerationPrompt(ctx);
|
|
8163
|
+
const content = await callAI(prompt, systemPrompt);
|
|
8164
|
+
const cleaned = stripMarkdownFences(content);
|
|
8165
|
+
return {
|
|
8166
|
+
filePath: ctx.suggestedTestPath,
|
|
8167
|
+
content: cleaned,
|
|
8168
|
+
categoryId: ctx.gap.categoryId,
|
|
8169
|
+
description: `Security test for ${ctx.gap.categoryId} in ${ctx.functionName ?? "unknown function"} at ${ctx.gap.filePath}:${ctx.gap.lineStart}`,
|
|
8170
|
+
isPropertyBased: false
|
|
8957
8171
|
};
|
|
8958
|
-
|
|
8959
|
-
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
|
|
8964
|
-
|
|
8965
|
-
|
|
8966
|
-
|
|
8967
|
-
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8172
|
+
}
|
|
8173
|
+
async function generatePropertyTest(ctx, callAI) {
|
|
8174
|
+
const systemPrompt = [
|
|
8175
|
+
"You are a formal verification expert writing property-based tests.",
|
|
8176
|
+
"Express security invariants that must hold for ALL inputs.",
|
|
8177
|
+
"Use fast-check for TypeScript/JavaScript or hypothesis for Python.",
|
|
8178
|
+
"Output ONLY code. No markdown fences. No explanations."
|
|
8179
|
+
].join(" ");
|
|
8180
|
+
const prompt = buildPropertyPrompt(ctx);
|
|
8181
|
+
const content = await callAI(prompt, systemPrompt);
|
|
8182
|
+
const cleaned = stripMarkdownFences(content);
|
|
8183
|
+
const ext = ctx.language === "python" ? ".py" : ".ts";
|
|
8184
|
+
const propPath = ctx.suggestedTestPath.replace(/\.test\.(ts|js|py)$/, `.prop${ext}`);
|
|
8185
|
+
return {
|
|
8186
|
+
filePath: propPath,
|
|
8187
|
+
content: cleaned,
|
|
8188
|
+
categoryId: ctx.gap.categoryId,
|
|
8189
|
+
description: `Property-based security invariant for ${ctx.gap.categoryId}`,
|
|
8190
|
+
isPropertyBased: true
|
|
8191
|
+
};
|
|
8192
|
+
}
|
|
8193
|
+
function stripMarkdownFences(content) {
|
|
8194
|
+
let result = content.trim();
|
|
8195
|
+
if (result.startsWith("```")) {
|
|
8196
|
+
const firstNewline = result.indexOf("\n");
|
|
8197
|
+
if (firstNewline !== -1) {
|
|
8198
|
+
result = result.slice(firstNewline + 1);
|
|
8979
8199
|
}
|
|
8980
|
-
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8200
|
+
}
|
|
8201
|
+
if (result.endsWith("```")) {
|
|
8202
|
+
result = result.slice(0, -3).trimEnd();
|
|
8203
|
+
}
|
|
8204
|
+
return result;
|
|
8205
|
+
}
|
|
8206
|
+
|
|
8207
|
+
// src/cli/commands/generate.ts
|
|
8208
|
+
function registerGenerateCommand(program2) {
|
|
8209
|
+
program2.command("generate").description("Generate adversarial security tests for detected vulnerabilities").option("--gaps", "Generate tests for all detected 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("--write", "Write test files to disk").option("--property", "Also generate property-based tests (fast-check/hypothesis)").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) => {
|
|
8210
|
+
const isQuiet = Boolean(options["quiet"]);
|
|
8211
|
+
const isVerbose = Boolean(options["verbose"]);
|
|
8212
|
+
const shouldWrite = Boolean(options["write"]);
|
|
8213
|
+
const withProperty = Boolean(options["property"]);
|
|
8214
|
+
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
8215
|
+
const outputFormat = String(options["output"] ?? "terminal");
|
|
8216
|
+
if (isQuiet) {
|
|
8217
|
+
logger.configure({ level: "error" });
|
|
8218
|
+
} else if (isVerbose) {
|
|
8219
|
+
logger.configure({ level: "debug" });
|
|
8989
8220
|
}
|
|
8990
|
-
if (
|
|
8991
|
-
|
|
8221
|
+
if (!["terminal", "json"].includes(outputFormat)) {
|
|
8222
|
+
console.error(formatError(new Error(`Invalid output format: ${outputFormat}. Use: terminal, json`)));
|
|
8223
|
+
process.exit(1);
|
|
8992
8224
|
}
|
|
8993
|
-
const
|
|
8994
|
-
const
|
|
8995
|
-
const
|
|
8996
|
-
if (!
|
|
8997
|
-
|
|
8998
|
-
console.error(formatError(loadResult.error));
|
|
8225
|
+
const hasGaps = Boolean(options["gaps"]);
|
|
8226
|
+
const categoryId = options["category"];
|
|
8227
|
+
const domainFilter = options["domain"];
|
|
8228
|
+
if (!hasGaps && !categoryId && !domainFilter) {
|
|
8229
|
+
console.error(formatError(new Error("Specify what to generate: --gaps (all gaps), --category <id>, or --domain <domain>")));
|
|
8999
8230
|
process.exit(1);
|
|
9000
8231
|
}
|
|
9001
|
-
if (
|
|
9002
|
-
|
|
8232
|
+
if (domainFilter && !RISK_DOMAINS.includes(domainFilter)) {
|
|
8233
|
+
console.error(formatError(new Error(`Invalid domain: ${domainFilter}. Valid: ${RISK_DOMAINS.join(", ")}`)));
|
|
8234
|
+
process.exit(1);
|
|
9003
8235
|
}
|
|
9004
|
-
const
|
|
9005
|
-
const
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
const existing = gapsByCategory.get(gap.categoryId) ?? [];
|
|
9010
|
-
existing.push(gap);
|
|
9011
|
-
gapsByCategory.set(gap.categoryId, existing);
|
|
8236
|
+
const validSeverities = ["critical", "high", "medium", "low"];
|
|
8237
|
+
const minSeverity = String(options["severity"] ?? "medium");
|
|
8238
|
+
if (!validSeverities.includes(minSeverity)) {
|
|
8239
|
+
console.error(formatError(new Error(`Invalid severity: ${minSeverity}`)));
|
|
8240
|
+
process.exit(1);
|
|
9012
8241
|
}
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
8242
|
+
const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
8243
|
+
const showSpinner = outputFormat === "terminal" && !isQuiet;
|
|
8244
|
+
const spinner = showSpinner ? ora3("Loading scan results...").start() : null;
|
|
8245
|
+
try {
|
|
8246
|
+
const projectRoot = process.cwd();
|
|
8247
|
+
const cacheResult = await loadScanResults(projectRoot);
|
|
8248
|
+
if (!cacheResult.success) {
|
|
8249
|
+
spinner?.fail("No cached results");
|
|
8250
|
+
console.error(formatError(cacheResult.error));
|
|
8251
|
+
console.error(chalk6.yellow("\nRun `pinata analyze` first."));
|
|
8252
|
+
process.exit(1);
|
|
9018
8253
|
}
|
|
9019
|
-
|
|
9020
|
-
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
8254
|
+
let gaps = cacheResult.data.gaps;
|
|
8255
|
+
if (categoryId) {
|
|
8256
|
+
gaps = gaps.filter((g) => g.categoryId === categoryId);
|
|
8257
|
+
}
|
|
8258
|
+
if (domainFilter) {
|
|
8259
|
+
gaps = gaps.filter((g) => g.domain === domainFilter);
|
|
8260
|
+
}
|
|
8261
|
+
gaps = gaps.filter((g) => (severityOrder[g.severity] ?? 0) >= (severityOrder[minSeverity] ?? 0));
|
|
8262
|
+
const seen = /* @__PURE__ */ new Set();
|
|
8263
|
+
gaps = gaps.filter((g) => {
|
|
8264
|
+
const key = `${g.categoryId}:${g.filePath}`;
|
|
8265
|
+
if (seen.has(key)) return false;
|
|
8266
|
+
seen.add(key);
|
|
8267
|
+
return true;
|
|
8268
|
+
});
|
|
8269
|
+
if (gaps.length === 0) {
|
|
8270
|
+
spinner?.succeed("No gaps match filters");
|
|
8271
|
+
console.log(chalk6.yellow("\nNo gaps to generate tests for."));
|
|
8272
|
+
process.exit(0);
|
|
8273
|
+
}
|
|
8274
|
+
if (spinner) {
|
|
8275
|
+
spinner.text = `Extracting context for ${gaps.length} findings...`;
|
|
8276
|
+
}
|
|
8277
|
+
const contexts = await extractTestContexts(gaps, projectRoot);
|
|
8278
|
+
if (contexts.length === 0) {
|
|
8279
|
+
spinner?.fail("Failed to extract context from any finding");
|
|
8280
|
+
process.exit(1);
|
|
8281
|
+
}
|
|
8282
|
+
if (spinner) {
|
|
8283
|
+
spinner.text = `Generating tests for ${contexts.length} findings with AI...`;
|
|
8284
|
+
}
|
|
8285
|
+
const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
8286
|
+
if (!hasApiKey2(aiProvider)) {
|
|
8287
|
+
spinner?.fail("No API key configured");
|
|
8288
|
+
console.error(chalk6.yellow(`
|
|
8289
|
+
AI test generation requires an API key.`));
|
|
8290
|
+
console.error(chalk6.gray(` pinata config set ${aiProvider === "anthropic" ? "anthropic-api-key" : "openai-api-key"} YOUR_KEY`));
|
|
8291
|
+
process.exit(1);
|
|
8292
|
+
}
|
|
8293
|
+
const apiKey = getApiKey2(aiProvider) ?? "";
|
|
8294
|
+
const callAI = buildAICaller(aiProvider, apiKey);
|
|
8295
|
+
const generated = [];
|
|
8296
|
+
const errors = [];
|
|
8297
|
+
for (let i = 0; i < contexts.length; i++) {
|
|
8298
|
+
const ctx = contexts[i];
|
|
8299
|
+
if (spinner) {
|
|
8300
|
+
spinner.text = `Generating test ${i + 1}/${contexts.length}: ${ctx.gap.categoryId} in ${relative(projectRoot, ctx.gap.filePath)}`;
|
|
9036
8301
|
}
|
|
9037
|
-
|
|
9038
|
-
|
|
9039
|
-
|
|
8302
|
+
try {
|
|
8303
|
+
const test = await generateTest(ctx, callAI);
|
|
8304
|
+
generated.push(test);
|
|
8305
|
+
if (withProperty) {
|
|
8306
|
+
try {
|
|
8307
|
+
const propTest = await generatePropertyTest(ctx, callAI);
|
|
8308
|
+
generated.push(propTest);
|
|
8309
|
+
} catch (err2) {
|
|
8310
|
+
errors.push(`Property test failed for ${ctx.gap.categoryId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
8311
|
+
}
|
|
8312
|
+
}
|
|
8313
|
+
} catch (err2) {
|
|
8314
|
+
errors.push(`Failed ${ctx.gap.categoryId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
9040
8315
|
}
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
variables = extractVariablesFromGap(gap);
|
|
8316
|
+
}
|
|
8317
|
+
spinner?.stop();
|
|
8318
|
+
if (generated.length === 0) {
|
|
8319
|
+
console.log(chalk6.red("Failed to generate any tests."));
|
|
8320
|
+
for (const error of errors) {
|
|
8321
|
+
console.error(chalk6.gray(` ${error}`));
|
|
9048
8322
|
}
|
|
9049
|
-
|
|
9050
|
-
|
|
9051
|
-
|
|
9052
|
-
|
|
8323
|
+
process.exit(1);
|
|
8324
|
+
}
|
|
8325
|
+
if (outputFormat === "json") {
|
|
8326
|
+
console.log(JSON.stringify({
|
|
8327
|
+
generated: generated.map((t) => ({
|
|
8328
|
+
filePath: relative(projectRoot, t.filePath),
|
|
8329
|
+
categoryId: t.categoryId,
|
|
8330
|
+
description: t.description,
|
|
8331
|
+
isPropertyBased: t.isPropertyBased,
|
|
8332
|
+
lines: t.content.split("\n").length
|
|
8333
|
+
})),
|
|
8334
|
+
errors
|
|
8335
|
+
}, null, 2));
|
|
8336
|
+
} else {
|
|
8337
|
+
console.log();
|
|
8338
|
+
console.log(chalk6.bold(`Generated ${generated.length} test file${generated.length === 1 ? "" : "s"}`));
|
|
8339
|
+
console.log();
|
|
8340
|
+
for (const test of generated) {
|
|
8341
|
+
const relPath = relative(projectRoot, test.filePath);
|
|
8342
|
+
const badge = test.isPropertyBased ? chalk6.magenta(" [property]") : "";
|
|
8343
|
+
console.log(` ${chalk6.green("+")} ${relPath}${badge}`);
|
|
8344
|
+
console.log(chalk6.gray(` ${test.description}`));
|
|
9053
8345
|
}
|
|
9054
|
-
|
|
9055
|
-
generatedTests.push({
|
|
9056
|
-
gap,
|
|
9057
|
-
category,
|
|
9058
|
-
template,
|
|
9059
|
-
result: renderResult.data,
|
|
9060
|
-
suggestedPath
|
|
9061
|
-
});
|
|
8346
|
+
console.log();
|
|
9062
8347
|
}
|
|
9063
|
-
|
|
9064
|
-
|
|
9065
|
-
|
|
9066
|
-
|
|
9067
|
-
|
|
9068
|
-
|
|
9069
|
-
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
8348
|
+
if (shouldWrite) {
|
|
8349
|
+
let written = 0;
|
|
8350
|
+
for (const test of generated) {
|
|
8351
|
+
try {
|
|
8352
|
+
await mkdir(dirname(test.filePath), { recursive: true });
|
|
8353
|
+
await writeFile(test.filePath, test.content, "utf-8");
|
|
8354
|
+
written++;
|
|
8355
|
+
} catch (err2) {
|
|
8356
|
+
errors.push(`Write failed: ${relative(projectRoot, test.filePath)}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
8357
|
+
}
|
|
8358
|
+
}
|
|
8359
|
+
console.log(chalk6.green(`Wrote ${written} test file${written === 1 ? "" : "s"}`));
|
|
8360
|
+
} else {
|
|
8361
|
+
console.log(chalk6.gray("Dry run. Use --write to save test files to disk."));
|
|
9074
8362
|
}
|
|
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);
|
|
8363
|
+
if (errors.length > 0 && isVerbose) {
|
|
8364
|
+
console.error(chalk6.yellow("\nWarnings:"));
|
|
8365
|
+
for (const error of errors) {
|
|
8366
|
+
console.error(chalk6.gray(` ${error}`));
|
|
8367
|
+
}
|
|
9086
8368
|
}
|
|
9087
|
-
|
|
9088
|
-
|
|
9089
|
-
|
|
8369
|
+
process.exit(0);
|
|
8370
|
+
} catch (error) {
|
|
8371
|
+
spinner?.fail("Generation failed");
|
|
8372
|
+
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
8373
|
+
process.exit(1);
|
|
8374
|
+
}
|
|
8375
|
+
});
|
|
8376
|
+
}
|
|
8377
|
+
function buildAICaller(provider, apiKey) {
|
|
8378
|
+
return async (prompt, systemPrompt) => {
|
|
8379
|
+
if (provider === "anthropic") {
|
|
8380
|
+
const response2 = await fetch("https://api.anthropic.com/v1/messages", {
|
|
8381
|
+
method: "POST",
|
|
8382
|
+
headers: {
|
|
8383
|
+
"Content-Type": "application/json",
|
|
8384
|
+
"x-api-key": apiKey,
|
|
8385
|
+
"anthropic-version": "2023-06-01"
|
|
8386
|
+
},
|
|
8387
|
+
body: JSON.stringify({
|
|
8388
|
+
model: "claude-sonnet-4-20250514",
|
|
8389
|
+
max_tokens: 4096,
|
|
8390
|
+
system: systemPrompt,
|
|
8391
|
+
messages: [{ role: "user", content: prompt }]
|
|
8392
|
+
})
|
|
8393
|
+
});
|
|
8394
|
+
if (!response2.ok) {
|
|
8395
|
+
const body = await response2.text();
|
|
8396
|
+
throw new Error(`Anthropic API error: ${response2.status} - ${body}`);
|
|
9090
8397
|
}
|
|
8398
|
+
const data2 = await response2.json();
|
|
8399
|
+
return data2.content[0]?.text ?? "";
|
|
9091
8400
|
}
|
|
9092
|
-
|
|
9093
|
-
|
|
9094
|
-
|
|
9095
|
-
|
|
9096
|
-
|
|
9097
|
-
|
|
9098
|
-
|
|
8401
|
+
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
8402
|
+
method: "POST",
|
|
8403
|
+
headers: {
|
|
8404
|
+
"Content-Type": "application/json",
|
|
8405
|
+
Authorization: `Bearer ${apiKey}`
|
|
8406
|
+
},
|
|
8407
|
+
body: JSON.stringify({
|
|
8408
|
+
model: "gpt-4o",
|
|
8409
|
+
max_tokens: 4096,
|
|
8410
|
+
messages: [
|
|
8411
|
+
{ role: "system", content: systemPrompt },
|
|
8412
|
+
{ role: "user", content: prompt }
|
|
8413
|
+
]
|
|
8414
|
+
})
|
|
8415
|
+
});
|
|
8416
|
+
if (!response.ok) {
|
|
8417
|
+
const body = await response.text();
|
|
8418
|
+
throw new Error(`OpenAI API error: ${response.status} - ${body}`);
|
|
8419
|
+
}
|
|
8420
|
+
const data = await response.json();
|
|
8421
|
+
return data.choices[0]?.message?.content ?? "";
|
|
8422
|
+
};
|
|
8423
|
+
}
|
|
8424
|
+
|
|
8425
|
+
// src/cli/index.ts
|
|
8426
|
+
var program = new Command();
|
|
8427
|
+
program.name("pinata").description("AI-powered security vulnerability detection").version(VERSION);
|
|
8428
|
+
registerAnalyzeCommand(program);
|
|
8429
|
+
registerGenerateCommand(program);
|
|
9099
8430
|
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
8431
|
const isQuiet = Boolean(options["quiet"]);
|
|
9101
8432
|
const isVerbose = Boolean(options["verbose"]);
|
|
8433
|
+
const topN = parseInt(String(options["top"] ?? "5"), 10);
|
|
8434
|
+
const categoryFilter = options["category"];
|
|
8435
|
+
const domainFilter = options["domain"];
|
|
9102
8436
|
const useAI = Boolean(options["ai"]);
|
|
9103
8437
|
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
9104
8438
|
const outputFormat = String(options["output"] ?? "terminal");
|
|
9105
|
-
const topN = parseInt(String(options["top"] ?? "5"), 10);
|
|
9106
8439
|
if (isQuiet) {
|
|
9107
8440
|
logger.configure({ level: "error" });
|
|
9108
8441
|
} else if (isVerbose) {
|
|
9109
8442
|
logger.configure({ level: "debug" });
|
|
9110
8443
|
}
|
|
9111
8444
|
if (!["terminal", "json", "markdown"].includes(outputFormat)) {
|
|
9112
|
-
console.error(formatError(new Error(`Invalid
|
|
8445
|
+
console.error(formatError(new Error(`Invalid format: ${outputFormat}. Use: terminal, json, markdown`)));
|
|
9113
8446
|
process.exit(1);
|
|
9114
8447
|
}
|
|
9115
|
-
const
|
|
9116
|
-
const
|
|
9117
|
-
|
|
9118
|
-
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
8448
|
+
const projectRoot = process.cwd();
|
|
8449
|
+
const cacheResult = await loadScanResults(projectRoot);
|
|
8450
|
+
if (!cacheResult.success) {
|
|
8451
|
+
console.error(formatError(cacheResult.error));
|
|
8452
|
+
console.error(chalk6.yellow("\nRun `pinata analyze` first to scan for gaps."));
|
|
8453
|
+
process.exit(1);
|
|
8454
|
+
}
|
|
8455
|
+
const cached = cacheResult.data;
|
|
8456
|
+
let gaps = cached.gaps;
|
|
8457
|
+
if (categoryFilter) {
|
|
8458
|
+
gaps = gaps.filter((g) => g.categoryId === categoryFilter);
|
|
8459
|
+
}
|
|
8460
|
+
if (domainFilter) {
|
|
8461
|
+
if (!RISK_DOMAINS.includes(domainFilter)) {
|
|
8462
|
+
console.error(formatError(new Error(`Invalid domain: ${domainFilter}`)));
|
|
9124
8463
|
process.exit(1);
|
|
9125
8464
|
}
|
|
9126
|
-
|
|
9127
|
-
|
|
9128
|
-
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9136
|
-
|
|
9137
|
-
|
|
8465
|
+
gaps = gaps.filter((g) => g.domain === domainFilter);
|
|
8466
|
+
}
|
|
8467
|
+
gaps = gaps.slice(0, topN);
|
|
8468
|
+
if (gaps.length === 0) {
|
|
8469
|
+
console.log(chalk6.yellow("No gaps to explain."));
|
|
8470
|
+
process.exit(0);
|
|
8471
|
+
}
|
|
8472
|
+
let explanations;
|
|
8473
|
+
if (useAI) {
|
|
8474
|
+
const spinner = ora3("Generating AI explanations...").start();
|
|
8475
|
+
try {
|
|
8476
|
+
const aiConfig = { provider: aiProvider };
|
|
8477
|
+
const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
8478
|
+
if (hasApiKey2(aiProvider)) {
|
|
8479
|
+
const key = getApiKey2(aiProvider);
|
|
8480
|
+
if (key) {
|
|
8481
|
+
aiConfig.apiKey = key;
|
|
8482
|
+
}
|
|
9138
8483
|
}
|
|
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(`
|
|
8484
|
+
const resultMap = await explainGaps(gaps, void 0, aiConfig);
|
|
8485
|
+
explanations = gaps.map((g) => {
|
|
8486
|
+
const key = `${g.categoryId}:${g.filePath}:${g.lineStart}`;
|
|
8487
|
+
const result = resultMap.get(key);
|
|
8488
|
+
if (result?.success && result.data) {
|
|
8489
|
+
return result.data;
|
|
8490
|
+
}
|
|
8491
|
+
return generateFallbackExplanation(g);
|
|
8492
|
+
});
|
|
8493
|
+
spinner.succeed(`Generated ${explanations.length} explanations`);
|
|
8494
|
+
} catch (error) {
|
|
8495
|
+
spinner.fail("AI explanation failed");
|
|
8496
|
+
console.error(chalk6.yellow(`
|
|
9156
8497
|
Set ${aiProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} for AI explanations.
|
|
9157
8498
|
`));
|
|
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
|
-
}
|
|
8499
|
+
explanations = gaps.map((g) => generateFallbackExplanation(g));
|
|
9184
8500
|
}
|
|
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}
|
|
8501
|
+
} else {
|
|
8502
|
+
explanations = gaps.map((g) => generateFallbackExplanation(g));
|
|
8503
|
+
}
|
|
8504
|
+
if (outputFormat === "json") {
|
|
8505
|
+
const output = gaps.map((g, i) => ({ gap: { categoryId: g.categoryId, severity: g.severity, filePath: g.filePath, lineStart: g.lineStart }, ...explanations[i] }));
|
|
8506
|
+
console.log(JSON.stringify(output, null, 2));
|
|
8507
|
+
} else if (outputFormat === "markdown") {
|
|
8508
|
+
console.log("# Gap Explanations\n");
|
|
8509
|
+
for (let i = 0; i < explanations.length; i++) {
|
|
8510
|
+
const exp = explanations[i];
|
|
8511
|
+
const gap = gaps[i];
|
|
8512
|
+
console.log(`## ${exp.summary}
|
|
9219
8513
|
`);
|
|
9220
|
-
|
|
9221
|
-
${explanation.remediation}
|
|
8514
|
+
console.log(`**Severity**: ${gap.severity} | **Category**: ${gap.categoryId}
|
|
9222
8515
|
`);
|
|
9223
|
-
|
|
9224
|
-
|
|
9225
|
-
|
|
9226
|
-
${
|
|
9227
|
-
\`\`\`
|
|
8516
|
+
console.log(exp.explanation);
|
|
8517
|
+
if (exp.remediation) {
|
|
8518
|
+
console.log(`
|
|
8519
|
+
**Remediation**: ${exp.remediation}
|
|
9228
8520
|
`);
|
|
9229
|
-
}
|
|
9230
|
-
console.log("---\n");
|
|
9231
8521
|
}
|
|
9232
|
-
|
|
8522
|
+
console.log("---\n");
|
|
8523
|
+
}
|
|
8524
|
+
} else {
|
|
8525
|
+
console.log();
|
|
8526
|
+
for (let i = 0; i < explanations.length; i++) {
|
|
8527
|
+
const exp = explanations[i];
|
|
8528
|
+
const gap = gaps[i];
|
|
8529
|
+
const severityColor = gap.severity === "critical" ? chalk6.red : gap.severity === "high" ? chalk6.yellow : chalk6.blue;
|
|
8530
|
+
console.log(`${severityColor.bold(`[${gap.severity.toUpperCase()}]`)} ${chalk6.bold(exp.summary)}`);
|
|
8531
|
+
console.log(chalk6.gray(` Category: ${gap.categoryId} | ${gap.filePath}:${gap.lineStart}`));
|
|
9233
8532
|
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
|
-
}
|
|
8533
|
+
for (const line of exp.explanation.split("\n")) {
|
|
8534
|
+
console.log(` ${line}`);
|
|
8535
|
+
}
|
|
8536
|
+
if (exp.remediation) {
|
|
9265
8537
|
console.log();
|
|
9266
|
-
console.log(
|
|
8538
|
+
console.log(chalk6.green(` Fix: ${exp.remediation}`));
|
|
9267
8539
|
}
|
|
8540
|
+
console.log();
|
|
9268
8541
|
}
|
|
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
8542
|
}
|
|
9275
8543
|
});
|
|
9276
8544
|
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 +8546,73 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
|
|
|
9278
8546
|
const language = String(options["language"]);
|
|
9279
8547
|
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
9280
8548
|
const outputFormat = String(options["output"] ?? "terminal");
|
|
9281
|
-
const codeSnippets = options["code"];
|
|
9282
8549
|
const filePath = options["file"];
|
|
9283
|
-
|
|
8550
|
+
const codeSnippets = options["code"] ?? [];
|
|
8551
|
+
const samples = [...codeSnippets];
|
|
9284
8552
|
if (filePath) {
|
|
8553
|
+
const { readFile: readFile7 } = await import('fs/promises');
|
|
9285
8554
|
try {
|
|
9286
|
-
const
|
|
9287
|
-
|
|
9288
|
-
vulnerableCode = [...vulnerableCode, ...content.split("\n---\n").filter(Boolean)];
|
|
8555
|
+
const content = await readFile7(resolve(filePath), "utf-8");
|
|
8556
|
+
samples.push(...content.split("\n---\n").filter((s) => s.trim()));
|
|
9289
8557
|
} catch (error) {
|
|
9290
8558
|
console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
|
|
9291
8559
|
process.exit(1);
|
|
9292
8560
|
}
|
|
9293
8561
|
}
|
|
9294
|
-
if (
|
|
8562
|
+
if (samples.length === 0) {
|
|
9295
8563
|
console.error(formatError(new Error("Provide code samples via --code or --file")));
|
|
9296
8564
|
process.exit(1);
|
|
9297
8565
|
}
|
|
9298
|
-
const spinner =
|
|
8566
|
+
const spinner = ora3("Generating pattern suggestions with AI...").start();
|
|
9299
8567
|
try {
|
|
8568
|
+
const aiConfig = { provider: aiProvider };
|
|
8569
|
+
const { hasApiKey: hasApiKey2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
8570
|
+
if (hasApiKey2(aiProvider)) {
|
|
8571
|
+
const key = getApiKey2(aiProvider);
|
|
8572
|
+
if (key) {
|
|
8573
|
+
aiConfig.apiKey = key;
|
|
8574
|
+
}
|
|
8575
|
+
}
|
|
9300
8576
|
const result = await suggestPatterns(
|
|
9301
|
-
{
|
|
9302
|
-
|
|
9303
|
-
language,
|
|
9304
|
-
vulnerableCode,
|
|
9305
|
-
maxSuggestions: 5
|
|
9306
|
-
},
|
|
9307
|
-
{ provider: aiProvider }
|
|
8577
|
+
{ category: categoryId, vulnerableCode: samples, language },
|
|
8578
|
+
aiConfig
|
|
9308
8579
|
);
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
console.error(
|
|
8580
|
+
if (!result.success || !result.data) {
|
|
8581
|
+
spinner.fail("Pattern suggestion failed");
|
|
8582
|
+
console.error(chalk6.red(result.error ?? "Unknown error"));
|
|
9312
8583
|
process.exit(1);
|
|
9313
8584
|
}
|
|
9314
|
-
const
|
|
8585
|
+
const suggestions = result.data.suggestions;
|
|
8586
|
+
spinner.succeed(`Generated ${suggestions.length} pattern suggestions`);
|
|
9315
8587
|
if (outputFormat === "json") {
|
|
9316
|
-
console.log(JSON.stringify(
|
|
8588
|
+
console.log(JSON.stringify(suggestions, null, 2));
|
|
9317
8589
|
} 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}`);
|
|
8590
|
+
for (const s of suggestions) {
|
|
8591
|
+
console.log("---");
|
|
8592
|
+
console.log(`id: ${s.id}`);
|
|
8593
|
+
console.log(`pattern: "${s.pattern}"`);
|
|
8594
|
+
console.log(`confidence: ${s.confidence}`);
|
|
8595
|
+
console.log(`description: "${s.description}"`);
|
|
8596
|
+
console.log(`matchExample: "${s.matchExample}"`);
|
|
8597
|
+
console.log(`safeExample: "${s.safeExample}"`);
|
|
9329
8598
|
console.log();
|
|
9330
8599
|
}
|
|
9331
8600
|
} else {
|
|
9332
8601
|
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) {
|
|
8602
|
+
console.log(chalk6.bold(`Suggested Patterns for ${categoryId}`));
|
|
8603
|
+
console.log();
|
|
8604
|
+
for (const s of suggestions) {
|
|
8605
|
+
const confColor = s.confidence === "high" ? chalk6.green : s.confidence === "medium" ? chalk6.yellow : chalk6.red;
|
|
8606
|
+
console.log(` ${chalk6.cyan(s.id)} [${confColor(s.confidence)}]`);
|
|
8607
|
+
console.log(` Pattern: ${chalk6.gray(s.pattern)}`);
|
|
8608
|
+
console.log(` ${s.description}`);
|
|
8609
|
+
console.log(` Match: ${chalk6.gray(s.matchExample.slice(0, 80))}`);
|
|
8610
|
+
console.log(` Safe: ${chalk6.gray(s.safeExample.slice(0, 80))}`);
|
|
9361
8611
|
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
8612
|
}
|
|
9367
8613
|
}
|
|
9368
|
-
process.exit(0);
|
|
9369
8614
|
} catch (error) {
|
|
9370
|
-
spinner.fail("Pattern
|
|
8615
|
+
spinner.fail("Pattern suggestion failed");
|
|
9371
8616
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9372
8617
|
process.exit(1);
|
|
9373
8618
|
}
|
|
@@ -9422,9 +8667,7 @@ program.command("search <query>").description("Search category taxonomy by name,
|
|
|
9422
8667
|
}
|
|
9423
8668
|
const langFilter = options["language"];
|
|
9424
8669
|
if (langFilter) {
|
|
9425
|
-
results = results.filter(
|
|
9426
|
-
(cat) => cat.applicableLanguages.includes(langFilter)
|
|
9427
|
-
);
|
|
8670
|
+
results = results.filter((cat) => cat.applicableLanguages.includes(langFilter));
|
|
9428
8671
|
}
|
|
9429
8672
|
if (outputFormat === "json") {
|
|
9430
8673
|
console.log(JSON.stringify(results, null, 2));
|
|
@@ -9446,19 +8689,18 @@ ${cat.description}
|
|
|
9446
8689
|
}
|
|
9447
8690
|
} else {
|
|
9448
8691
|
console.log();
|
|
9449
|
-
console.log(
|
|
9450
|
-
console.log(
|
|
8692
|
+
console.log(chalk6.bold(`Search Results for "${query}"`));
|
|
8693
|
+
console.log(chalk6.gray(`Found ${results.length} matching categories.`));
|
|
9451
8694
|
console.log();
|
|
9452
8695
|
if (results.length === 0) {
|
|
9453
|
-
console.log(
|
|
9454
|
-
console.log(chalk5.gray("Try a different query or broaden your filters."));
|
|
8696
|
+
console.log(chalk6.yellow("No categories match your search."));
|
|
9455
8697
|
} else {
|
|
9456
8698
|
for (const cat of results) {
|
|
9457
|
-
const domainColor = cat.domain === "security" ?
|
|
9458
|
-
console.log(` ${
|
|
8699
|
+
const domainColor = cat.domain === "security" ? chalk6.red : chalk6.blue;
|
|
8700
|
+
console.log(` ${chalk6.cyan(cat.id)} - ${chalk6.bold(cat.name)}`);
|
|
9459
8701
|
console.log(` ${domainColor(cat.domain)} | ${cat.level} | ${cat.priority}`);
|
|
9460
8702
|
if (options["verbose"]) {
|
|
9461
|
-
console.log(` ${
|
|
8703
|
+
console.log(` ${chalk6.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
|
|
9462
8704
|
}
|
|
9463
8705
|
console.log();
|
|
9464
8706
|
}
|
|
@@ -9492,21 +8734,17 @@ program.command("list").description("List all categories").option("-d, --domain
|
|
|
9492
8734
|
process.exit(1);
|
|
9493
8735
|
}
|
|
9494
8736
|
const priorityFilter = options["priority"];
|
|
9495
|
-
|
|
9496
|
-
if (priorityFilter !== void 0 && !validPriorities.includes(priorityFilter)) {
|
|
8737
|
+
if (priorityFilter !== void 0 && !["P0", "P1", "P2"].includes(priorityFilter)) {
|
|
9497
8738
|
console.error(formatError(new Error(`Invalid priority: ${priorityFilter}. Use: P0, P1, P2`)));
|
|
9498
8739
|
process.exit(1);
|
|
9499
8740
|
}
|
|
9500
|
-
logger.debug("Loading categories...");
|
|
9501
8741
|
const store = createCategoryStore();
|
|
9502
8742
|
const definitionsPath = getDefinitionsPath();
|
|
9503
|
-
logger.debug(`Loading from: ${definitionsPath}`);
|
|
9504
8743
|
const loadResult = await store.loadFromDirectory(definitionsPath);
|
|
9505
8744
|
if (!loadResult.success) {
|
|
9506
8745
|
console.error(formatError(loadResult.error));
|
|
9507
8746
|
process.exit(1);
|
|
9508
8747
|
}
|
|
9509
|
-
logger.debug(`Loaded ${loadResult.data} categories`);
|
|
9510
8748
|
const filter = {};
|
|
9511
8749
|
if (domainFilter) {
|
|
9512
8750
|
filter.domain = domainFilter;
|
|
@@ -9530,54 +8768,42 @@ program.command("init").description("Initialize Pinata configuration in project"
|
|
|
9530
8768
|
const configPath = resolve(process.cwd(), ".pinata.yml");
|
|
9531
8769
|
const cacheDir = resolve(process.cwd(), ".pinata");
|
|
9532
8770
|
if (existsSync(configPath) && !options["force"]) {
|
|
9533
|
-
console.log(
|
|
9534
|
-
console.log(
|
|
8771
|
+
console.log(chalk6.yellow("Configuration file already exists at .pinata.yml"));
|
|
8772
|
+
console.log(chalk6.gray("Use --force to overwrite."));
|
|
9535
8773
|
process.exit(0);
|
|
9536
8774
|
}
|
|
9537
8775
|
const defaultConfig = `# Pinata Configuration
|
|
9538
8776
|
# https://github.com/pinata/pinata
|
|
9539
8777
|
|
|
9540
|
-
# Paths to analyze
|
|
9541
8778
|
include:
|
|
9542
8779
|
- "src/**/*.ts"
|
|
9543
8780
|
- "src/**/*.tsx"
|
|
9544
8781
|
- "src/**/*.py"
|
|
9545
8782
|
- "src/**/*.js"
|
|
9546
8783
|
|
|
9547
|
-
# Paths to exclude from analysis
|
|
9548
8784
|
exclude:
|
|
9549
8785
|
- "node_modules/**"
|
|
9550
8786
|
- "dist/**"
|
|
9551
8787
|
- "build/**"
|
|
9552
8788
|
- "**/*.test.ts"
|
|
9553
8789
|
- "**/*.spec.ts"
|
|
9554
|
-
- "**/test/**"
|
|
9555
|
-
- "**/tests/**"
|
|
9556
|
-
- "**/__tests__/**"
|
|
9557
8790
|
|
|
9558
|
-
# Risk domains to analyze
|
|
9559
|
-
# Options: security, data, concurrency, input, resource, reliability, performance, platform, business, compliance
|
|
9560
8791
|
domains:
|
|
9561
8792
|
- security
|
|
9562
8793
|
- data
|
|
9563
8794
|
- concurrency
|
|
9564
8795
|
- input
|
|
9565
8796
|
|
|
9566
|
-
# Minimum severity to report
|
|
9567
|
-
# Options: critical, high, medium, low
|
|
9568
8797
|
minSeverity: medium
|
|
9569
8798
|
|
|
9570
|
-
# Output configuration
|
|
9571
8799
|
output:
|
|
9572
|
-
format: terminal
|
|
8800
|
+
format: terminal
|
|
9573
8801
|
color: true
|
|
9574
8802
|
|
|
9575
|
-
# Test generation settings
|
|
9576
8803
|
generate:
|
|
9577
8804
|
outputDir: tests/generated
|
|
9578
|
-
framework: auto
|
|
8805
|
+
framework: auto
|
|
9579
8806
|
|
|
9580
|
-
# Fail CI if gaps exceed thresholds
|
|
9581
8807
|
thresholds:
|
|
9582
8808
|
critical: 0
|
|
9583
8809
|
high: 5
|
|
@@ -9586,25 +8812,23 @@ thresholds:
|
|
|
9586
8812
|
const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
|
|
9587
8813
|
try {
|
|
9588
8814
|
await writeFileAsync(configPath, defaultConfig, "utf8");
|
|
9589
|
-
console.log(
|
|
8815
|
+
console.log(chalk6.green("Created .pinata.yml"));
|
|
9590
8816
|
await mkdir4(cacheDir, { recursive: true });
|
|
9591
|
-
console.log(
|
|
8817
|
+
console.log(chalk6.green("Created .pinata/ directory"));
|
|
9592
8818
|
const gitignorePath = resolve(process.cwd(), ".gitignore");
|
|
9593
8819
|
if (existsSync(gitignorePath)) {
|
|
9594
8820
|
const { readFile: readFile7, appendFile } = await import('fs/promises');
|
|
9595
8821
|
const gitignore = await readFile7(gitignorePath, "utf8");
|
|
9596
8822
|
if (!gitignore.includes(".pinata/")) {
|
|
9597
8823
|
await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
|
|
9598
|
-
console.log(
|
|
8824
|
+
console.log(chalk6.green("Added .pinata/ to .gitignore"));
|
|
9599
8825
|
}
|
|
9600
8826
|
}
|
|
9601
8827
|
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"));
|
|
8828
|
+
console.log(chalk6.bold("Pinata initialized successfully!"));
|
|
8829
|
+
console.log(chalk6.gray(" 1. Review and customize .pinata.yml"));
|
|
8830
|
+
console.log(chalk6.gray(" 2. Run: pinata analyze"));
|
|
8831
|
+
console.log(chalk6.gray(" 3. Generate tests: pinata generate"));
|
|
9608
8832
|
} catch (error) {
|
|
9609
8833
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9610
8834
|
process.exit(1);
|
|
@@ -9617,18 +8841,15 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9617
8841
|
const checkAge = Boolean(options["checkAge"]);
|
|
9618
8842
|
const strictMode = Boolean(options["strict"]);
|
|
9619
8843
|
const doAllChecks = !checkRegistry && !checkDownloads && !checkAge;
|
|
9620
|
-
console.log(
|
|
8844
|
+
console.log(chalk6.bold("\nPinata Dependency Audit\n"));
|
|
9621
8845
|
if (!existsSync(packagePath)) {
|
|
9622
|
-
console.error(
|
|
8846
|
+
console.error(chalk6.red(`Error: ${packagePath} not found`));
|
|
9623
8847
|
process.exit(1);
|
|
9624
8848
|
}
|
|
9625
8849
|
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
9626
|
-
const allDeps = {
|
|
9627
|
-
...packageJson.dependencies,
|
|
9628
|
-
...packageJson.devDependencies
|
|
9629
|
-
};
|
|
8850
|
+
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
9630
8851
|
const packages = Object.keys(allDeps);
|
|
9631
|
-
console.log(
|
|
8852
|
+
console.log(chalk6.gray(`Found ${packages.length} dependencies
|
|
9632
8853
|
`));
|
|
9633
8854
|
const issues = [];
|
|
9634
8855
|
const KNOWN_MALWARE = /* @__PURE__ */ new Set([
|
|
@@ -9651,56 +8872,31 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9651
8872
|
]);
|
|
9652
8873
|
for (const pkg of packages) {
|
|
9653
8874
|
if (KNOWN_MALWARE.has(pkg)) {
|
|
9654
|
-
issues.push({
|
|
9655
|
-
pkg,
|
|
9656
|
-
severity: "critical",
|
|
9657
|
-
message: "Known malicious/compromised package (Shai-Hulud/typosquat)"
|
|
9658
|
-
});
|
|
8875
|
+
issues.push({ pkg, severity: "critical", message: "Known malicious/compromised package (Shai-Hulud/typosquat)" });
|
|
9659
8876
|
}
|
|
9660
8877
|
}
|
|
9661
8878
|
for (const [pkg, version] of Object.entries(allDeps)) {
|
|
9662
8879
|
if (version?.startsWith("^")) {
|
|
9663
|
-
issues.push({
|
|
9664
|
-
pkg,
|
|
9665
|
-
severity: "warning",
|
|
9666
|
-
message: `Unpinned version (${version}) - allows minor updates`
|
|
9667
|
-
});
|
|
8880
|
+
issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows minor updates` });
|
|
9668
8881
|
} else if (version?.startsWith("~")) {
|
|
9669
|
-
issues.push({
|
|
9670
|
-
pkg,
|
|
9671
|
-
severity: "warning",
|
|
9672
|
-
message: `Unpinned version (${version}) - allows patch updates`
|
|
9673
|
-
});
|
|
8882
|
+
issues.push({ pkg, severity: "warning", message: `Unpinned version (${version}) - allows patch updates` });
|
|
9674
8883
|
} else if (version === "*" || version === "latest") {
|
|
9675
|
-
issues.push({
|
|
9676
|
-
pkg,
|
|
9677
|
-
severity: "critical",
|
|
9678
|
-
message: `Extremely dangerous version (${version}) - allows any version`
|
|
9679
|
-
});
|
|
8884
|
+
issues.push({ pkg, severity: "critical", message: `Extremely dangerous version (${version}) - allows any version` });
|
|
9680
8885
|
}
|
|
9681
8886
|
}
|
|
9682
8887
|
if (checkRegistry || doAllChecks) {
|
|
9683
|
-
const spinner =
|
|
8888
|
+
const spinner = ora3("Checking npm registry...").start();
|
|
9684
8889
|
for (const pkg of packages.slice(0, 50)) {
|
|
9685
8890
|
try {
|
|
9686
8891
|
const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(pkg)}`);
|
|
9687
8892
|
if (response.status === 404) {
|
|
9688
|
-
issues.push({
|
|
9689
|
-
pkg,
|
|
9690
|
-
severity: "critical",
|
|
9691
|
-
message: "Package NOT FOUND in npm registry (slopsquatting risk)"
|
|
9692
|
-
});
|
|
8893
|
+
issues.push({ pkg, severity: "critical", message: "Package NOT FOUND in npm registry (slopsquatting risk)" });
|
|
9693
8894
|
} else if (response.ok) {
|
|
9694
8895
|
const data = await response.json();
|
|
9695
8896
|
if ((checkAge || doAllChecks) && data.time?.created) {
|
|
9696
|
-
const
|
|
9697
|
-
const ageInDays = (Date.now() - created.getTime()) / (1e3 * 60 * 60 * 24);
|
|
8897
|
+
const ageInDays = (Date.now() - new Date(data.time.created).getTime()) / (1e3 * 60 * 60 * 24);
|
|
9698
8898
|
if (ageInDays < 30) {
|
|
9699
|
-
issues.push({
|
|
9700
|
-
pkg,
|
|
9701
|
-
severity: "warning",
|
|
9702
|
-
message: `Very new package (${Math.floor(ageInDays)} days old)`
|
|
9703
|
-
});
|
|
8899
|
+
issues.push({ pkg, severity: "warning", message: `Very new package (${Math.floor(ageInDays)} days old)` });
|
|
9704
8900
|
}
|
|
9705
8901
|
}
|
|
9706
8902
|
}
|
|
@@ -9712,24 +8908,24 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9712
8908
|
const criticals = issues.filter((i) => i.severity === "critical");
|
|
9713
8909
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
9714
8910
|
if (criticals.length > 0) {
|
|
9715
|
-
console.log(
|
|
8911
|
+
console.log(chalk6.red.bold(`
|
|
9716
8912
|
Critical Issues (${criticals.length}):`));
|
|
9717
8913
|
for (const issue of criticals) {
|
|
9718
|
-
console.log(
|
|
8914
|
+
console.log(chalk6.red(` \u2717 ${issue.pkg}: ${issue.message}`));
|
|
9719
8915
|
}
|
|
9720
8916
|
}
|
|
9721
8917
|
if (warnings.length > 0) {
|
|
9722
|
-
console.log(
|
|
8918
|
+
console.log(chalk6.yellow.bold(`
|
|
9723
8919
|
Warnings (${warnings.length}):`));
|
|
9724
8920
|
for (const issue of warnings.slice(0, 20)) {
|
|
9725
|
-
console.log(
|
|
8921
|
+
console.log(chalk6.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
|
|
9726
8922
|
}
|
|
9727
8923
|
if (warnings.length > 20) {
|
|
9728
|
-
console.log(
|
|
8924
|
+
console.log(chalk6.gray(` ... and ${warnings.length - 20} more`));
|
|
9729
8925
|
}
|
|
9730
8926
|
}
|
|
9731
8927
|
if (issues.length === 0) {
|
|
9732
|
-
console.log(
|
|
8928
|
+
console.log(chalk6.green("\u2713 No dependency issues found"));
|
|
9733
8929
|
}
|
|
9734
8930
|
console.log();
|
|
9735
8931
|
if (criticals.length > 0 || strictMode && warnings.length > 0) {
|
|
@@ -9742,7 +8938,7 @@ program.command("feedback").description("View pattern performance feedback (Laye
|
|
|
9742
8938
|
const shouldReset = Boolean(options["reset"]);
|
|
9743
8939
|
if (shouldReset) {
|
|
9744
8940
|
await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
|
|
9745
|
-
console.log(
|
|
8941
|
+
console.log(chalk6.green("Feedback data reset."));
|
|
9746
8942
|
return;
|
|
9747
8943
|
}
|
|
9748
8944
|
const state = await loadFeedback2();
|
|
@@ -9754,20 +8950,20 @@ program.command("feedback").description("View pattern performance feedback (Laye
|
|
|
9754
8950
|
console.log(generateReport2(state));
|
|
9755
8951
|
return;
|
|
9756
8952
|
}
|
|
9757
|
-
console.log(
|
|
8953
|
+
console.log(chalk6.bold("\nPinata Feedback Report\n"));
|
|
9758
8954
|
console.log(`Total scans: ${state.totalScans}`);
|
|
9759
8955
|
console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
|
|
9760
8956
|
if (state.totalScans === 0) {
|
|
9761
|
-
console.log(
|
|
8957
|
+
console.log(chalk6.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
|
|
9762
8958
|
return;
|
|
9763
8959
|
}
|
|
9764
8960
|
const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
|
|
9765
8961
|
if (patterns.length > 0) {
|
|
9766
|
-
console.log(
|
|
8962
|
+
console.log(chalk6.bold("\nPattern Performance:"));
|
|
9767
8963
|
for (const p of patterns.slice(0, 15)) {
|
|
9768
8964
|
const total = p.confirmedCount + p.unconfirmedCount;
|
|
9769
8965
|
const precisionPct = (p.precision * 100).toFixed(0);
|
|
9770
|
-
const color = p.precision >= 0.7 ?
|
|
8966
|
+
const color = p.precision >= 0.7 ? chalk6.green : p.precision >= 0.4 ? chalk6.yellow : chalk6.red;
|
|
9771
8967
|
console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
|
|
9772
8968
|
}
|
|
9773
8969
|
}
|
|
@@ -9789,76 +8985,74 @@ Examples:
|
|
|
9789
8985
|
case "anthropic-api-key": {
|
|
9790
8986
|
const validation = validateApiKey2("anthropic", value);
|
|
9791
8987
|
if (!validation.valid) {
|
|
9792
|
-
console.log(
|
|
8988
|
+
console.log(chalk6.red(`Invalid API key: ${validation.error}`));
|
|
9793
8989
|
process.exit(1);
|
|
9794
8990
|
}
|
|
9795
8991
|
setConfigValue2("anthropicApiKey", value);
|
|
9796
|
-
console.log(
|
|
8992
|
+
console.log(chalk6.green(`Anthropic API key set: ${maskApiKey2(value)}`));
|
|
9797
8993
|
break;
|
|
9798
8994
|
}
|
|
9799
8995
|
case "openai-api-key": {
|
|
9800
8996
|
const validation = validateApiKey2("openai", value);
|
|
9801
8997
|
if (!validation.valid) {
|
|
9802
|
-
console.log(
|
|
8998
|
+
console.log(chalk6.red(`Invalid API key: ${validation.error}`));
|
|
9803
8999
|
process.exit(1);
|
|
9804
9000
|
}
|
|
9805
9001
|
setConfigValue2("openaiApiKey", value);
|
|
9806
|
-
console.log(
|
|
9002
|
+
console.log(chalk6.green(`OpenAI API key set: ${maskApiKey2(value)}`));
|
|
9807
9003
|
break;
|
|
9808
9004
|
}
|
|
9809
9005
|
case "default-provider": {
|
|
9810
9006
|
if (value !== "anthropic" && value !== "openai") {
|
|
9811
|
-
console.log(
|
|
9007
|
+
console.log(chalk6.red("Provider must be 'anthropic' or 'openai'"));
|
|
9812
9008
|
process.exit(1);
|
|
9813
9009
|
}
|
|
9814
9010
|
setConfigValue2("defaultProvider", value);
|
|
9815
|
-
console.log(
|
|
9011
|
+
console.log(chalk6.green(`Default provider set to: ${value}`));
|
|
9816
9012
|
break;
|
|
9817
9013
|
}
|
|
9818
9014
|
default:
|
|
9819
|
-
console.log(
|
|
9820
|
-
console.log(
|
|
9015
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
9016
|
+
console.log(chalk6.gray("Run 'pinata config set --help' for available keys"));
|
|
9821
9017
|
process.exit(1);
|
|
9822
9018
|
}
|
|
9823
|
-
console.log(
|
|
9019
|
+
console.log(chalk6.gray(`Config stored at: ${getConfigPath2()}`));
|
|
9824
9020
|
});
|
|
9825
9021
|
config.command("get <key>").description("Get a configuration value").action(async (key) => {
|
|
9826
9022
|
const { loadConfig: loadConfig2, maskApiKey: maskApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9827
9023
|
const cfg = loadConfig2();
|
|
9828
9024
|
switch (key) {
|
|
9829
9025
|
case "anthropic-api-key":
|
|
9830
|
-
console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) :
|
|
9026
|
+
console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk6.gray("(not set)"));
|
|
9831
9027
|
break;
|
|
9832
9028
|
case "openai-api-key":
|
|
9833
|
-
console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) :
|
|
9029
|
+
console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk6.gray("(not set)"));
|
|
9834
9030
|
break;
|
|
9835
9031
|
case "default-provider":
|
|
9836
|
-
console.log(cfg.defaultProvider ??
|
|
9032
|
+
console.log(cfg.defaultProvider ?? chalk6.gray("anthropic (default)"));
|
|
9837
9033
|
break;
|
|
9838
9034
|
default:
|
|
9839
|
-
console.log(
|
|
9035
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
9840
9036
|
process.exit(1);
|
|
9841
9037
|
}
|
|
9842
9038
|
});
|
|
9843
9039
|
config.command("list").description("List all configuration values").action(async () => {
|
|
9844
9040
|
const { loadConfig: loadConfig2, maskApiKey: maskApiKey2, getConfigPath: getConfigPath2, hasApiKey: hasApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9845
9041
|
const cfg = loadConfig2();
|
|
9846
|
-
console.log(
|
|
9847
|
-
console.log(
|
|
9042
|
+
console.log(chalk6.bold("Pinata Configuration"));
|
|
9043
|
+
console.log(chalk6.gray(`Config file: ${getConfigPath2()}`));
|
|
9848
9044
|
console.log();
|
|
9849
9045
|
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 ?
|
|
9046
|
+
const anthropicStatus = hasApiKey2("anthropic") ? chalk6.green("configured") : chalk6.gray("not set");
|
|
9047
|
+
const openaiStatus = hasApiKey2("openai") ? chalk6.green("configured") : chalk6.gray("not set");
|
|
9048
|
+
console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ? chalk6.gray(`(${maskApiKey2(cfg.anthropicApiKey)})`) : ""}`);
|
|
9049
|
+
console.log(` OpenAI API key: ${openaiStatus} ${cfg.openaiApiKey ? chalk6.gray(`(${maskApiKey2(cfg.openaiApiKey)})`) : ""}`);
|
|
9854
9050
|
console.log(` Default provider: ${cfg.defaultProvider ?? "anthropic"}`);
|
|
9855
9051
|
console.log();
|
|
9856
9052
|
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"));
|
|
9053
|
+
console.log(chalk6.yellow("No AI provider configured."));
|
|
9054
|
+
console.log(chalk6.gray(" pinata config set anthropic-api-key sk-ant-xxx"));
|
|
9055
|
+
console.log(chalk6.gray(" export ANTHROPIC_API_KEY=sk-ant-xxx"));
|
|
9862
9056
|
}
|
|
9863
9057
|
});
|
|
9864
9058
|
config.command("unset <key>").description("Remove a configuration value").action(async (key) => {
|
|
@@ -9866,18 +9060,18 @@ config.command("unset <key>").description("Remove a configuration value").action
|
|
|
9866
9060
|
switch (key) {
|
|
9867
9061
|
case "anthropic-api-key":
|
|
9868
9062
|
deleteConfigValue2("anthropicApiKey");
|
|
9869
|
-
console.log(
|
|
9063
|
+
console.log(chalk6.green("Anthropic API key removed"));
|
|
9870
9064
|
break;
|
|
9871
9065
|
case "openai-api-key":
|
|
9872
9066
|
deleteConfigValue2("openaiApiKey");
|
|
9873
|
-
console.log(
|
|
9067
|
+
console.log(chalk6.green("OpenAI API key removed"));
|
|
9874
9068
|
break;
|
|
9875
9069
|
case "default-provider":
|
|
9876
9070
|
deleteConfigValue2("defaultProvider");
|
|
9877
|
-
console.log(
|
|
9071
|
+
console.log(chalk6.green("Default provider reset to: anthropic"));
|
|
9878
9072
|
break;
|
|
9879
9073
|
default:
|
|
9880
|
-
console.log(
|
|
9074
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
9881
9075
|
process.exit(1);
|
|
9882
9076
|
}
|
|
9883
9077
|
});
|
|
@@ -9885,18 +9079,13 @@ var auth = program.command("auth").description("Manage API key authentication");
|
|
|
9885
9079
|
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
9080
|
const apiKey = options["key"] ?? process.env["PINATA_API_KEY"];
|
|
9887
9081
|
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");
|
|
9082
|
+
console.log(chalk6.yellow("No API key provided."));
|
|
9083
|
+
console.log(chalk6.gray(" pinata auth login --key <your-api-key>"));
|
|
9084
|
+
console.log(chalk6.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
|
|
9895
9085
|
process.exit(1);
|
|
9896
9086
|
}
|
|
9897
9087
|
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."));
|
|
9088
|
+
console.log(chalk6.red("Invalid API key format. Keys should start with 'pk_'."));
|
|
9900
9089
|
process.exit(1);
|
|
9901
9090
|
}
|
|
9902
9091
|
const configDir = resolve(process.cwd(), ".pinata");
|
|
@@ -9905,19 +9094,13 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
|
|
|
9905
9094
|
try {
|
|
9906
9095
|
await mkdir4(configDir, { recursive: true });
|
|
9907
9096
|
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");
|
|
9097
|
+
await writeFileAsync(authPath, JSON.stringify({ configured: true, keyId: maskedKey, configuredAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2), "utf8");
|
|
9914
9098
|
const envPath = resolve(configDir, ".env");
|
|
9915
9099
|
await writeFileAsync(envPath, `PINATA_API_KEY=${apiKey}
|
|
9916
9100
|
`, { mode: 384 });
|
|
9917
|
-
console.log(
|
|
9918
|
-
console.log(
|
|
9919
|
-
console.log();
|
|
9920
|
-
console.log(chalk5.yellow("Important: Add .pinata/.env to your .gitignore"));
|
|
9101
|
+
console.log(chalk6.green("API key configured successfully!"));
|
|
9102
|
+
console.log(chalk6.gray(`Key ID: ${maskedKey}`));
|
|
9103
|
+
console.log(chalk6.yellow("Important: Add .pinata/.env to your .gitignore"));
|
|
9921
9104
|
} catch (error) {
|
|
9922
9105
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9923
9106
|
process.exit(1);
|
|
@@ -9938,11 +9121,7 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
9938
9121
|
await rm2(envPath);
|
|
9939
9122
|
removed = true;
|
|
9940
9123
|
}
|
|
9941
|
-
|
|
9942
|
-
console.log(chalk5.green("API key removed successfully."));
|
|
9943
|
-
} else {
|
|
9944
|
-
console.log(chalk5.yellow("No stored API key found."));
|
|
9945
|
-
}
|
|
9124
|
+
console.log(removed ? chalk6.green("API key removed successfully.") : chalk6.yellow("No stored API key found."));
|
|
9946
9125
|
} catch (error) {
|
|
9947
9126
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9948
9127
|
process.exit(1);
|
|
@@ -9951,19 +9130,19 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
9951
9130
|
auth.command("status").description("Check authentication status").action(async () => {
|
|
9952
9131
|
const authPath = resolve(process.cwd(), ".pinata", "auth.json");
|
|
9953
9132
|
if (!existsSync(authPath)) {
|
|
9954
|
-
console.log(
|
|
9955
|
-
console.log(
|
|
9133
|
+
console.log(chalk6.yellow("Not authenticated."));
|
|
9134
|
+
console.log(chalk6.gray("Run: pinata auth login --key <your-api-key>"));
|
|
9956
9135
|
process.exit(0);
|
|
9957
9136
|
}
|
|
9958
9137
|
try {
|
|
9959
9138
|
const { readFile: readFile7 } = await import('fs/promises');
|
|
9960
9139
|
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(
|
|
9140
|
+
console.log(chalk6.green("Authenticated"));
|
|
9141
|
+
console.log(chalk6.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
|
|
9142
|
+
console.log(chalk6.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
|
|
9143
|
+
} catch {
|
|
9144
|
+
console.log(chalk6.yellow("Authentication status unknown."));
|
|
9145
|
+
console.log(chalk6.gray("Run: pinata auth login to reconfigure."));
|
|
9967
9146
|
}
|
|
9968
9147
|
});
|
|
9969
9148
|
program.parse();
|