pinata-security-cli 0.5.3 → 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 +902 -1546
- package/dist/cli/index.js.map +1 -1
- package/package.json +4 -1
- 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,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import fs, {
|
|
3
|
-
import path, { dirname, resolve, join, basename,
|
|
2
|
+
import fs, { mkdir, writeFile, readFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
|
|
3
|
+
import path, { dirname, resolve, relative, join, basename, extname } from 'path';
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
|
|
5
5
|
import { homedir, tmpdir } from 'os';
|
|
6
6
|
import { z } from 'zod';
|
|
@@ -9,7 +9,7 @@ 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
|
|
12
|
+
import chalk6 from 'chalk';
|
|
13
13
|
import { Command } from 'commander';
|
|
14
14
|
import ora3 from 'ora';
|
|
15
15
|
import YAML from 'yaml';
|
|
@@ -1485,11 +1485,11 @@ export default defineConfig({
|
|
|
1485
1485
|
timedOut
|
|
1486
1486
|
});
|
|
1487
1487
|
});
|
|
1488
|
-
proc.on("error", (
|
|
1488
|
+
proc.on("error", (err2) => {
|
|
1489
1489
|
clearTimeout(timer);
|
|
1490
1490
|
resolve9({
|
|
1491
1491
|
stdout,
|
|
1492
|
-
stderr: stderr + "\n" +
|
|
1492
|
+
stderr: stderr + "\n" + err2.message,
|
|
1493
1493
|
exitCode: 1,
|
|
1494
1494
|
timedOut: false
|
|
1495
1495
|
});
|
|
@@ -4583,7 +4583,7 @@ var Logger = class _Logger {
|
|
|
4583
4583
|
*/
|
|
4584
4584
|
debug(message, ...args) {
|
|
4585
4585
|
if (this.shouldLog("debug")) {
|
|
4586
|
-
console.debug(
|
|
4586
|
+
console.debug(chalk6.gray(this.format(message)), ...args);
|
|
4587
4587
|
}
|
|
4588
4588
|
}
|
|
4589
4589
|
/**
|
|
@@ -4599,7 +4599,7 @@ var Logger = class _Logger {
|
|
|
4599
4599
|
*/
|
|
4600
4600
|
warn(message, ...args) {
|
|
4601
4601
|
if (this.shouldLog("warn")) {
|
|
4602
|
-
console.warn(
|
|
4602
|
+
console.warn(chalk6.yellow(this.format(message)), ...args);
|
|
4603
4603
|
}
|
|
4604
4604
|
}
|
|
4605
4605
|
/**
|
|
@@ -4607,7 +4607,7 @@ var Logger = class _Logger {
|
|
|
4607
4607
|
*/
|
|
4608
4608
|
error(message, ...args) {
|
|
4609
4609
|
if (this.shouldLog("error")) {
|
|
4610
|
-
console.error(
|
|
4610
|
+
console.error(chalk6.red(this.format(message)), ...args);
|
|
4611
4611
|
}
|
|
4612
4612
|
}
|
|
4613
4613
|
/**
|
|
@@ -4615,7 +4615,7 @@ var Logger = class _Logger {
|
|
|
4615
4615
|
*/
|
|
4616
4616
|
success(message, ...args) {
|
|
4617
4617
|
if (this.shouldLog("info")) {
|
|
4618
|
-
console.info(
|
|
4618
|
+
console.info(chalk6.green(this.format(message)), ...args);
|
|
4619
4619
|
}
|
|
4620
4620
|
}
|
|
4621
4621
|
/**
|
|
@@ -5558,6 +5558,64 @@ var SCORING_ADJUSTMENTS = [
|
|
|
5558
5558
|
categoryId: "command-injection",
|
|
5559
5559
|
lowerWeight: ["cli", "script"],
|
|
5560
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"]
|
|
5561
5619
|
}
|
|
5562
5620
|
];
|
|
5563
5621
|
async function detectProjectType(projectPath) {
|
|
@@ -5698,8 +5756,8 @@ var SEVERITY_WEIGHTS = {
|
|
|
5698
5756
|
};
|
|
5699
5757
|
var CONFIDENCE_WEIGHTS = {
|
|
5700
5758
|
high: 1,
|
|
5701
|
-
medium: 0.
|
|
5702
|
-
low: 0.
|
|
5759
|
+
medium: 0.3,
|
|
5760
|
+
low: 0.1
|
|
5703
5761
|
};
|
|
5704
5762
|
var PRIORITY_WEIGHTS = {
|
|
5705
5763
|
P0: 3,
|
|
@@ -5780,7 +5838,7 @@ var Scanner = class {
|
|
|
5780
5838
|
this.patternMatcher = new PatternMatcher();
|
|
5781
5839
|
}
|
|
5782
5840
|
/**
|
|
5783
|
-
* Scan a directory for
|
|
5841
|
+
* Scan a directory for security vulnerabilities
|
|
5784
5842
|
*
|
|
5785
5843
|
* @param targetDirectory Directory to scan
|
|
5786
5844
|
* @param options Scan options
|
|
@@ -5915,25 +5973,36 @@ var Scanner = class {
|
|
|
5915
5973
|
for (const domain of RISK_DOMAINS) {
|
|
5916
5974
|
domainScores.set(domain, 100);
|
|
5917
5975
|
}
|
|
5976
|
+
const gapsByCategory = /* @__PURE__ */ new Map();
|
|
5918
5977
|
for (const gap of gaps) {
|
|
5919
|
-
const severityWeight = SEVERITY_WEIGHTS[gap.severity];
|
|
5920
|
-
const confidenceWeight = CONFIDENCE_WEIGHTS[gap.confidence];
|
|
5921
|
-
const priorityWeight = PRIORITY_WEIGHTS[gap.priority];
|
|
5922
5978
|
const projectTypeWeight = getCategoryWeight(gap.categoryId, projectType);
|
|
5923
|
-
|
|
5924
|
-
const
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
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);
|
|
5928
5997
|
baseScore -= penalty;
|
|
5929
|
-
const currentDomainScore = domainScores.get(
|
|
5930
|
-
domainScores.set(
|
|
5931
|
-
if (penalty >=
|
|
5932
|
-
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]` : "";
|
|
5933
6002
|
penalties.push({
|
|
5934
|
-
reason: `${
|
|
6003
|
+
reason: `${representative.severity} ${representative.domain}: ${representative.categoryName} (${count} findings)${weightNote}`,
|
|
5935
6004
|
points: Math.round(penalty),
|
|
5936
|
-
categoryId
|
|
6005
|
+
categoryId
|
|
5937
6006
|
});
|
|
5938
6007
|
}
|
|
5939
6008
|
}
|
|
@@ -6353,759 +6422,35 @@ init_errors();
|
|
|
6353
6422
|
init_result();
|
|
6354
6423
|
init_errors();
|
|
6355
6424
|
init_result();
|
|
6356
|
-
var TemplateRenderError = class extends PinataError {
|
|
6357
|
-
constructor(message, context) {
|
|
6358
|
-
super(message, "TEMPLATE_RENDER_ERROR", context);
|
|
6359
|
-
this.name = "TemplateRenderError";
|
|
6360
|
-
}
|
|
6361
|
-
};
|
|
6362
|
-
var TemplateSyntaxError = class extends PinataError {
|
|
6363
|
-
constructor(message, context) {
|
|
6364
|
-
super(message, "TEMPLATE_SYNTAX_ERROR", context);
|
|
6365
|
-
this.name = "TemplateSyntaxError";
|
|
6366
|
-
}
|
|
6367
|
-
};
|
|
6368
|
-
var CONDITIONAL_REGEX = /\{\{#if\s+([a-zA-Z][a-zA-Z0-9_.]*)\s*\}\}([\s\S]*?)(?:\{\{#else\}\}([\s\S]*?))?\{\{\/if\}\}/g;
|
|
6369
|
-
var UNLESS_REGEX = /\{\{#unless\s+([a-zA-Z][a-zA-Z0-9_.]*)\s*\}\}([\s\S]*?)\{\{\/unless\}\}/g;
|
|
6370
|
-
var EACH_REGEX = /\{\{#each\s+([a-zA-Z][a-zA-Z0-9_.]*)\s*\}\}([\s\S]*?)\{\{\/each\}\}/g;
|
|
6371
|
-
var TemplateRenderer = class {
|
|
6372
|
-
options;
|
|
6373
|
-
constructor(options = {}) {
|
|
6374
|
-
this.options = {
|
|
6375
|
-
strict: options.strict ?? true,
|
|
6376
|
-
allowUnresolved: options.allowUnresolved ?? false,
|
|
6377
|
-
placeholderFormat: options.placeholderFormat ?? "mustache"
|
|
6378
|
-
};
|
|
6379
|
-
}
|
|
6380
|
-
/**
|
|
6381
|
-
* Validate template syntax for common errors
|
|
6382
|
-
*
|
|
6383
|
-
* @param template Template string to validate
|
|
6384
|
-
* @returns Syntax validation result
|
|
6385
|
-
*/
|
|
6386
|
-
validateSyntax(template) {
|
|
6387
|
-
const errors = [];
|
|
6388
|
-
if (this.hasUnclosedBlock(template, "if")) {
|
|
6389
|
-
errors.push(
|
|
6390
|
-
new TemplateSyntaxError("Unclosed {{#if}} block", {
|
|
6391
|
-
hint: "Every {{#if variable}} must have a matching {{/if}}"
|
|
6392
|
-
})
|
|
6393
|
-
);
|
|
6394
|
-
}
|
|
6395
|
-
if (this.hasUnclosedBlock(template, "each")) {
|
|
6396
|
-
errors.push(
|
|
6397
|
-
new TemplateSyntaxError("Unclosed {{#each}} block", {
|
|
6398
|
-
hint: "Every {{#each variable}} must have a matching {{/each}}"
|
|
6399
|
-
})
|
|
6400
|
-
);
|
|
6401
|
-
}
|
|
6402
|
-
if (this.hasUnclosedBlock(template, "unless")) {
|
|
6403
|
-
errors.push(
|
|
6404
|
-
new TemplateSyntaxError("Unclosed {{#unless}} block", {
|
|
6405
|
-
hint: "Every {{#unless variable}} must have a matching {{/unless}}"
|
|
6406
|
-
})
|
|
6407
|
-
);
|
|
6408
|
-
}
|
|
6409
|
-
if (this.hasOrphanedClosingTag(template, "if")) {
|
|
6410
|
-
errors.push(
|
|
6411
|
-
new TemplateSyntaxError("Orphaned {{/if}} without matching {{#if}}", {
|
|
6412
|
-
hint: "Remove the extra {{/if}} or add the opening {{#if variable}}"
|
|
6413
|
-
})
|
|
6414
|
-
);
|
|
6415
|
-
}
|
|
6416
|
-
if (this.hasOrphanedClosingTag(template, "each")) {
|
|
6417
|
-
errors.push(
|
|
6418
|
-
new TemplateSyntaxError("Orphaned {{/each}} without matching {{#each}}", {
|
|
6419
|
-
hint: "Remove the extra {{/each}} or add the opening {{#each variable}}"
|
|
6420
|
-
})
|
|
6421
|
-
);
|
|
6422
|
-
}
|
|
6423
|
-
if (this.hasOrphanedClosingTag(template, "unless")) {
|
|
6424
|
-
errors.push(
|
|
6425
|
-
new TemplateSyntaxError("Orphaned {{/unless}} without matching {{#unless}}", {
|
|
6426
|
-
hint: "Remove the extra {{/unless}} or add the opening {{#unless variable}}"
|
|
6427
|
-
})
|
|
6428
|
-
);
|
|
6429
|
-
}
|
|
6430
|
-
const mismatchErrors = this.checkBlockNesting(template);
|
|
6431
|
-
errors.push(...mismatchErrors);
|
|
6432
|
-
return {
|
|
6433
|
-
valid: errors.length === 0,
|
|
6434
|
-
errors
|
|
6435
|
-
};
|
|
6436
|
-
}
|
|
6437
|
-
/**
|
|
6438
|
-
* Check if template has an unclosed block of specified type
|
|
6439
|
-
*/
|
|
6440
|
-
hasUnclosedBlock(template, blockType) {
|
|
6441
|
-
const openRegex = new RegExp(`\\{\\{#${blockType}\\s+[^}]+\\}\\}`, "g");
|
|
6442
|
-
const closeRegex = new RegExp(`\\{\\{/${blockType}\\}\\}`, "g");
|
|
6443
|
-
const opens = (template.match(openRegex) || []).length;
|
|
6444
|
-
const closes = (template.match(closeRegex) || []).length;
|
|
6445
|
-
return opens > closes;
|
|
6446
|
-
}
|
|
6447
|
-
/**
|
|
6448
|
-
* Check if template has orphaned closing tags
|
|
6449
|
-
*/
|
|
6450
|
-
hasOrphanedClosingTag(template, blockType) {
|
|
6451
|
-
const openRegex = new RegExp(`\\{\\{#${blockType}\\s+[^}]+\\}\\}`, "g");
|
|
6452
|
-
const closeRegex = new RegExp(`\\{\\{/${blockType}\\}\\}`, "g");
|
|
6453
|
-
const opens = (template.match(openRegex) || []).length;
|
|
6454
|
-
const closes = (template.match(closeRegex) || []).length;
|
|
6455
|
-
return closes > opens;
|
|
6456
|
-
}
|
|
6457
|
-
/**
|
|
6458
|
-
* Check for improperly nested blocks
|
|
6459
|
-
*/
|
|
6460
|
-
checkBlockNesting(template) {
|
|
6461
|
-
const errors = [];
|
|
6462
|
-
const stack = [];
|
|
6463
|
-
const blockPattern = /\{\{(#(?:if|each|unless)|\/(?:if|each|unless))\s*[^}]*\}\}/g;
|
|
6464
|
-
let match;
|
|
6465
|
-
while ((match = blockPattern.exec(template)) !== null) {
|
|
6466
|
-
const tag = match[1] ?? "";
|
|
6467
|
-
if (tag.startsWith("#")) {
|
|
6468
|
-
const type = tag.slice(1).split(/\s/)[0] ?? "";
|
|
6469
|
-
stack.push({ type, index: match.index });
|
|
6470
|
-
} else if (tag.startsWith("/")) {
|
|
6471
|
-
const type = tag.slice(1);
|
|
6472
|
-
const last = stack.pop();
|
|
6473
|
-
if (!last) {
|
|
6474
|
-
continue;
|
|
6475
|
-
}
|
|
6476
|
-
if (last.type !== type) {
|
|
6477
|
-
errors.push(
|
|
6478
|
-
new TemplateSyntaxError(`Mismatched block: opened {{#${last.type}}} but closed with {{/${type}}}`, {
|
|
6479
|
-
openedAt: last.index,
|
|
6480
|
-
closedAt: match.index
|
|
6481
|
-
})
|
|
6482
|
-
);
|
|
6483
|
-
}
|
|
6484
|
-
}
|
|
6485
|
-
}
|
|
6486
|
-
return errors;
|
|
6487
|
-
}
|
|
6488
|
-
/**
|
|
6489
|
-
* Parse all placeholders from a template string
|
|
6490
|
-
*
|
|
6491
|
-
* @param template Template string to parse
|
|
6492
|
-
* @returns Array of parsed placeholders
|
|
6493
|
-
*/
|
|
6494
|
-
parsePlaceholders(template) {
|
|
6495
|
-
const placeholders = [];
|
|
6496
|
-
const regex = this.getPlaceholderRegex();
|
|
6497
|
-
let match;
|
|
6498
|
-
regex.lastIndex = 0;
|
|
6499
|
-
while ((match = regex.exec(template)) !== null) {
|
|
6500
|
-
const name = match[1] ?? "";
|
|
6501
|
-
const pathSegments = name.split(".");
|
|
6502
|
-
placeholders.push({
|
|
6503
|
-
match: match[0],
|
|
6504
|
-
name,
|
|
6505
|
-
startIndex: match.index,
|
|
6506
|
-
endIndex: match.index + match[0].length,
|
|
6507
|
-
isNestedPath: pathSegments.length > 1,
|
|
6508
|
-
pathSegments
|
|
6509
|
-
});
|
|
6510
|
-
}
|
|
6511
|
-
return placeholders;
|
|
6512
|
-
}
|
|
6513
|
-
/**
|
|
6514
|
-
* Parse conditional blocks from template
|
|
6515
|
-
*
|
|
6516
|
-
* @param template Template string to parse
|
|
6517
|
-
* @returns Array of parsed conditionals
|
|
6518
|
-
*/
|
|
6519
|
-
parseConditionals(template) {
|
|
6520
|
-
const conditionals = [];
|
|
6521
|
-
const regex = new RegExp(CONDITIONAL_REGEX.source, "g");
|
|
6522
|
-
let match;
|
|
6523
|
-
while ((match = regex.exec(template)) !== null) {
|
|
6524
|
-
const falseBranch = match[3];
|
|
6525
|
-
conditionals.push({
|
|
6526
|
-
match: match[0],
|
|
6527
|
-
variable: match[1] ?? "",
|
|
6528
|
-
trueBranch: match[2] ?? "",
|
|
6529
|
-
...falseBranch !== void 0 && { falseBranch },
|
|
6530
|
-
startIndex: match.index,
|
|
6531
|
-
endIndex: match.index + match[0].length
|
|
6532
|
-
});
|
|
6533
|
-
}
|
|
6534
|
-
return conditionals;
|
|
6535
|
-
}
|
|
6536
|
-
/**
|
|
6537
|
-
* Parse loop blocks from template
|
|
6538
|
-
*
|
|
6539
|
-
* @param template Template string to parse
|
|
6540
|
-
* @returns Array of parsed loops
|
|
6541
|
-
*/
|
|
6542
|
-
parseLoops(template) {
|
|
6543
|
-
const loops = [];
|
|
6544
|
-
const regex = new RegExp(EACH_REGEX.source, "g");
|
|
6545
|
-
let match;
|
|
6546
|
-
while ((match = regex.exec(template)) !== null) {
|
|
6547
|
-
loops.push({
|
|
6548
|
-
match: match[0],
|
|
6549
|
-
variable: match[1] ?? "",
|
|
6550
|
-
body: match[2] ?? "",
|
|
6551
|
-
startIndex: match.index,
|
|
6552
|
-
endIndex: match.index + match[0].length
|
|
6553
|
-
});
|
|
6554
|
-
}
|
|
6555
|
-
return loops;
|
|
6556
|
-
}
|
|
6557
|
-
/**
|
|
6558
|
-
* Get unique variable names from template (including nested paths)
|
|
6559
|
-
* Excludes loop-internal variables like {{this}}, {{@index}}, and item properties
|
|
6560
|
-
*
|
|
6561
|
-
* @param template Template string to analyze
|
|
6562
|
-
* @param excludeLoopInternal Whether to exclude variables inside loop blocks
|
|
6563
|
-
* @returns Unique variable names found in template
|
|
6564
|
-
*/
|
|
6565
|
-
getVariableNames(template, excludeLoopInternal = true) {
|
|
6566
|
-
let processedTemplate = template;
|
|
6567
|
-
if (excludeLoopInternal) {
|
|
6568
|
-
processedTemplate = processedTemplate.replace(EACH_REGEX, (match, variable) => {
|
|
6569
|
-
return `{{${variable}}}`;
|
|
6570
|
-
});
|
|
6571
|
-
}
|
|
6572
|
-
const placeholders = this.parsePlaceholders(processedTemplate);
|
|
6573
|
-
const names = /* @__PURE__ */ new Set();
|
|
6574
|
-
const loopSpecialVars = /* @__PURE__ */ new Set(["this", "@index", "@first", "@last"]);
|
|
6575
|
-
for (const p of placeholders) {
|
|
6576
|
-
if (loopSpecialVars.has(p.name)) {
|
|
6577
|
-
continue;
|
|
6578
|
-
}
|
|
6579
|
-
names.add(p.name);
|
|
6580
|
-
if (p.isNestedPath && p.pathSegments[0]) {
|
|
6581
|
-
names.add(p.pathSegments[0]);
|
|
6582
|
-
}
|
|
6583
|
-
}
|
|
6584
|
-
const conditionals = this.parseConditionals(processedTemplate);
|
|
6585
|
-
for (const c of conditionals) {
|
|
6586
|
-
names.add(c.variable);
|
|
6587
|
-
const root = c.variable.split(".")[0];
|
|
6588
|
-
if (root && root !== c.variable) {
|
|
6589
|
-
names.add(root);
|
|
6590
|
-
}
|
|
6591
|
-
}
|
|
6592
|
-
const loops = this.parseLoops(template);
|
|
6593
|
-
for (const l of loops) {
|
|
6594
|
-
names.add(l.variable);
|
|
6595
|
-
const root = l.variable.split(".")[0];
|
|
6596
|
-
if (root && root !== l.variable) {
|
|
6597
|
-
names.add(root);
|
|
6598
|
-
}
|
|
6599
|
-
}
|
|
6600
|
-
return [...names];
|
|
6601
|
-
}
|
|
6602
|
-
/**
|
|
6603
|
-
* Get value from object using dot notation path
|
|
6604
|
-
*
|
|
6605
|
-
* @param obj Object to traverse
|
|
6606
|
-
* @param path Dot-separated path (e.g., "user.address.city")
|
|
6607
|
-
* @returns Value at path or undefined
|
|
6608
|
-
*/
|
|
6609
|
-
getNestedValue(obj, path2) {
|
|
6610
|
-
const segments = path2.split(".");
|
|
6611
|
-
let current = obj;
|
|
6612
|
-
for (const segment of segments) {
|
|
6613
|
-
if (current === null || current === void 0) {
|
|
6614
|
-
return void 0;
|
|
6615
|
-
}
|
|
6616
|
-
if (typeof current !== "object") {
|
|
6617
|
-
return void 0;
|
|
6618
|
-
}
|
|
6619
|
-
current = current[segment];
|
|
6620
|
-
}
|
|
6621
|
-
return current;
|
|
6622
|
-
}
|
|
6623
|
-
/**
|
|
6624
|
-
* Evaluate if a value is truthy for conditional blocks
|
|
6625
|
-
*
|
|
6626
|
-
* @param value Value to evaluate
|
|
6627
|
-
* @returns True if value is truthy
|
|
6628
|
-
*/
|
|
6629
|
-
isTruthy(value) {
|
|
6630
|
-
if (value === null || value === void 0) {
|
|
6631
|
-
return false;
|
|
6632
|
-
}
|
|
6633
|
-
if (typeof value === "boolean") {
|
|
6634
|
-
return value;
|
|
6635
|
-
}
|
|
6636
|
-
if (typeof value === "number") {
|
|
6637
|
-
return value !== 0;
|
|
6638
|
-
}
|
|
6639
|
-
if (typeof value === "string") {
|
|
6640
|
-
return value.length > 0;
|
|
6641
|
-
}
|
|
6642
|
-
if (Array.isArray(value)) {
|
|
6643
|
-
return value.length > 0;
|
|
6644
|
-
}
|
|
6645
|
-
if (typeof value === "object") {
|
|
6646
|
-
return Object.keys(value).length > 0;
|
|
6647
|
-
}
|
|
6648
|
-
return Boolean(value);
|
|
6649
|
-
}
|
|
6650
|
-
/**
|
|
6651
|
-
* Process conditional blocks in template
|
|
6652
|
-
*
|
|
6653
|
-
* @param template Template string
|
|
6654
|
-
* @param values Variable values
|
|
6655
|
-
* @returns Processed template with conditionals resolved
|
|
6656
|
-
*/
|
|
6657
|
-
processConditionals(template, values) {
|
|
6658
|
-
let result = template;
|
|
6659
|
-
result = result.replace(CONDITIONAL_REGEX, (match, variable, trueBranch, falseBranch) => {
|
|
6660
|
-
const value = this.getNestedValue(values, variable);
|
|
6661
|
-
const condition = this.isTruthy(value);
|
|
6662
|
-
return condition ? trueBranch : falseBranch ?? "";
|
|
6663
|
-
});
|
|
6664
|
-
result = result.replace(UNLESS_REGEX, (match, variable, content) => {
|
|
6665
|
-
const value = this.getNestedValue(values, variable);
|
|
6666
|
-
const condition = this.isTruthy(value);
|
|
6667
|
-
return condition ? "" : content;
|
|
6668
|
-
});
|
|
6669
|
-
return result;
|
|
6670
|
-
}
|
|
6671
|
-
/**
|
|
6672
|
-
* Process loop blocks in template
|
|
6673
|
-
*
|
|
6674
|
-
* @param template Template string
|
|
6675
|
-
* @param values Variable values
|
|
6676
|
-
* @returns Processed template with loops expanded
|
|
6677
|
-
*/
|
|
6678
|
-
processLoops(template, values) {
|
|
6679
|
-
let result = template;
|
|
6680
|
-
result = result.replace(EACH_REGEX, (match, variable, body) => {
|
|
6681
|
-
const arrayValue = this.getNestedValue(values, variable);
|
|
6682
|
-
if (!Array.isArray(arrayValue)) {
|
|
6683
|
-
return "";
|
|
6684
|
-
}
|
|
6685
|
-
const expanded = [];
|
|
6686
|
-
for (let i = 0; i < arrayValue.length; i++) {
|
|
6687
|
-
const item = arrayValue[i];
|
|
6688
|
-
let iterationBody = body;
|
|
6689
|
-
iterationBody = iterationBody.replace(/\{\{this\}\}/g, this.stringify(item));
|
|
6690
|
-
iterationBody = iterationBody.replace(/\{\{@index\}\}/g, String(i));
|
|
6691
|
-
iterationBody = iterationBody.replace(/\{\{@first\}\}/g, String(i === 0));
|
|
6692
|
-
iterationBody = iterationBody.replace(/\{\{@last\}\}/g, String(i === arrayValue.length - 1));
|
|
6693
|
-
if (item !== null && typeof item === "object" && !Array.isArray(item)) {
|
|
6694
|
-
const itemObj = item;
|
|
6695
|
-
iterationBody = this.processConditionals(iterationBody, itemObj);
|
|
6696
|
-
for (const [key, value] of Object.entries(itemObj)) {
|
|
6697
|
-
const propRegex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
6698
|
-
iterationBody = iterationBody.replace(propRegex, this.stringify(value));
|
|
6699
|
-
}
|
|
6700
|
-
}
|
|
6701
|
-
expanded.push(iterationBody);
|
|
6702
|
-
}
|
|
6703
|
-
return expanded.join("");
|
|
6704
|
-
});
|
|
6705
|
-
return result;
|
|
6706
|
-
}
|
|
6707
|
-
/**
|
|
6708
|
-
* Validate provided variables against template requirements
|
|
6709
|
-
*
|
|
6710
|
-
* @param template Test template with variable definitions
|
|
6711
|
-
* @param values Provided variable values
|
|
6712
|
-
* @returns Validation result
|
|
6713
|
-
*/
|
|
6714
|
-
validateVariables(template, values) {
|
|
6715
|
-
const results = [];
|
|
6716
|
-
const missingRequired = [];
|
|
6717
|
-
const unknownVariables = [];
|
|
6718
|
-
const typeErrors = [];
|
|
6719
|
-
const usedInTemplate = new Set(this.getVariableNames(template.template));
|
|
6720
|
-
const definedVariables = /* @__PURE__ */ new Map();
|
|
6721
|
-
for (const v of template.variables) {
|
|
6722
|
-
definedVariables.set(v.name, v);
|
|
6723
|
-
}
|
|
6724
|
-
for (const variable of template.variables) {
|
|
6725
|
-
const result = {
|
|
6726
|
-
name: variable.name,
|
|
6727
|
-
valid: true,
|
|
6728
|
-
errors: []
|
|
6729
|
-
};
|
|
6730
|
-
const value = values[variable.name];
|
|
6731
|
-
const hasValue = variable.name in values;
|
|
6732
|
-
if (variable.required && !hasValue && variable.defaultValue === void 0) {
|
|
6733
|
-
result.valid = false;
|
|
6734
|
-
result.errors.push(`Required variable '${variable.name}' is missing`);
|
|
6735
|
-
missingRequired.push(variable.name);
|
|
6736
|
-
}
|
|
6737
|
-
if (hasValue && value !== void 0 && value !== null) {
|
|
6738
|
-
const typeError = this.checkType(variable.name, value, variable.type);
|
|
6739
|
-
if (typeError) {
|
|
6740
|
-
result.valid = false;
|
|
6741
|
-
result.errors.push(typeError);
|
|
6742
|
-
typeErrors.push(typeError);
|
|
6743
|
-
}
|
|
6744
|
-
}
|
|
6745
|
-
results.push(result);
|
|
6746
|
-
}
|
|
6747
|
-
const providedNames = Object.keys(values);
|
|
6748
|
-
for (const name of providedNames) {
|
|
6749
|
-
if (!definedVariables.has(name)) {
|
|
6750
|
-
const isUsed = [...usedInTemplate].some((used) => used === name || used.startsWith(name + "."));
|
|
6751
|
-
if (isUsed) {
|
|
6752
|
-
results.push({
|
|
6753
|
-
name,
|
|
6754
|
-
valid: true,
|
|
6755
|
-
errors: [`Variable '${name}' is used in template but not formally defined`]
|
|
6756
|
-
});
|
|
6757
|
-
} else {
|
|
6758
|
-
unknownVariables.push(name);
|
|
6759
|
-
}
|
|
6760
|
-
}
|
|
6761
|
-
}
|
|
6762
|
-
for (const placeholder of usedInTemplate) {
|
|
6763
|
-
const defined = definedVariables.get(placeholder);
|
|
6764
|
-
const rootVar = placeholder.split(".")[0] ?? placeholder;
|
|
6765
|
-
const hasValue = placeholder in values || rootVar in values;
|
|
6766
|
-
if (!hasValue && !defined?.defaultValue) {
|
|
6767
|
-
if (!defined) {
|
|
6768
|
-
if (!placeholder.includes(".")) {
|
|
6769
|
-
missingRequired.push(placeholder);
|
|
6770
|
-
} else if (!(rootVar in values)) {
|
|
6771
|
-
missingRequired.push(rootVar);
|
|
6772
|
-
}
|
|
6773
|
-
}
|
|
6774
|
-
}
|
|
6775
|
-
}
|
|
6776
|
-
const valid = missingRequired.length === 0 && typeErrors.length === 0 && (unknownVariables.length === 0 || !this.options.strict);
|
|
6777
|
-
return {
|
|
6778
|
-
valid,
|
|
6779
|
-
results,
|
|
6780
|
-
missingRequired: [...new Set(missingRequired)],
|
|
6781
|
-
unknownVariables,
|
|
6782
|
-
typeErrors
|
|
6783
|
-
};
|
|
6784
|
-
}
|
|
6785
|
-
/**
|
|
6786
|
-
* Check if a value matches the expected type
|
|
6787
|
-
*/
|
|
6788
|
-
checkType(name, value, expectedType) {
|
|
6789
|
-
const actualType = this.getValueType(value);
|
|
6790
|
-
if (actualType !== expectedType) {
|
|
6791
|
-
return `Variable '${name}' expected type '${expectedType}' but got '${actualType}'`;
|
|
6792
|
-
}
|
|
6793
|
-
return null;
|
|
6794
|
-
}
|
|
6795
|
-
/**
|
|
6796
|
-
* Determine the VariableType of a value
|
|
6797
|
-
*/
|
|
6798
|
-
getValueType(value) {
|
|
6799
|
-
if (value === null || value === void 0) {
|
|
6800
|
-
return "string";
|
|
6801
|
-
}
|
|
6802
|
-
if (Array.isArray(value)) {
|
|
6803
|
-
return "array";
|
|
6804
|
-
}
|
|
6805
|
-
if (typeof value === "object") {
|
|
6806
|
-
return "object";
|
|
6807
|
-
}
|
|
6808
|
-
if (typeof value === "boolean") {
|
|
6809
|
-
return "boolean";
|
|
6810
|
-
}
|
|
6811
|
-
if (typeof value === "number") {
|
|
6812
|
-
return "number";
|
|
6813
|
-
}
|
|
6814
|
-
return "string";
|
|
6815
|
-
}
|
|
6816
|
-
/**
|
|
6817
|
-
* Substitute variables in a template string
|
|
6818
|
-
*
|
|
6819
|
-
* @param template Template string with placeholders
|
|
6820
|
-
* @param values Variable values to substitute
|
|
6821
|
-
* @param variableDefs Optional variable definitions for defaults
|
|
6822
|
-
* @returns Substitution result
|
|
6823
|
-
*/
|
|
6824
|
-
substituteVariables(template, values, variableDefs) {
|
|
6825
|
-
const substituted = [];
|
|
6826
|
-
const unresolved = [];
|
|
6827
|
-
const resolvedValues = /* @__PURE__ */ new Map();
|
|
6828
|
-
if (variableDefs) {
|
|
6829
|
-
for (const def of variableDefs) {
|
|
6830
|
-
if (def.defaultValue !== void 0) {
|
|
6831
|
-
resolvedValues.set(def.name, def.defaultValue);
|
|
6832
|
-
}
|
|
6833
|
-
}
|
|
6834
|
-
}
|
|
6835
|
-
for (const [key, value] of Object.entries(values)) {
|
|
6836
|
-
if (value === void 0 || value === null) {
|
|
6837
|
-
resolvedValues.set(key, null);
|
|
6838
|
-
} else {
|
|
6839
|
-
resolvedValues.set(key, value);
|
|
6840
|
-
}
|
|
6841
|
-
}
|
|
6842
|
-
const combinedValues = {};
|
|
6843
|
-
for (const [key, value] of resolvedValues) {
|
|
6844
|
-
combinedValues[key] = value;
|
|
6845
|
-
}
|
|
6846
|
-
const regex = this.getPlaceholderRegex();
|
|
6847
|
-
const content = template.replace(regex, (match, name) => {
|
|
6848
|
-
let value;
|
|
6849
|
-
let hasValue = false;
|
|
6850
|
-
if (name.includes(".")) {
|
|
6851
|
-
value = this.getNestedValue(combinedValues, name);
|
|
6852
|
-
hasValue = value !== void 0;
|
|
6853
|
-
} else {
|
|
6854
|
-
hasValue = resolvedValues.has(name);
|
|
6855
|
-
value = resolvedValues.get(name);
|
|
6856
|
-
}
|
|
6857
|
-
if (hasValue) {
|
|
6858
|
-
substituted.push(name);
|
|
6859
|
-
return this.stringify(value);
|
|
6860
|
-
}
|
|
6861
|
-
if (this.options.allowUnresolved) {
|
|
6862
|
-
unresolved.push(name);
|
|
6863
|
-
return match;
|
|
6864
|
-
}
|
|
6865
|
-
unresolved.push(name);
|
|
6866
|
-
return match;
|
|
6867
|
-
});
|
|
6868
|
-
return {
|
|
6869
|
-
content,
|
|
6870
|
-
substituted: [...new Set(substituted)],
|
|
6871
|
-
unresolved: [...new Set(unresolved)]
|
|
6872
|
-
};
|
|
6873
|
-
}
|
|
6874
|
-
/**
|
|
6875
|
-
* Convert a value to string for template substitution
|
|
6876
|
-
*/
|
|
6877
|
-
stringify(value) {
|
|
6878
|
-
if (value === null || value === void 0) {
|
|
6879
|
-
return "";
|
|
6880
|
-
}
|
|
6881
|
-
if (typeof value === "string") {
|
|
6882
|
-
return value;
|
|
6883
|
-
}
|
|
6884
|
-
if (typeof value === "number" || typeof value === "boolean") {
|
|
6885
|
-
return String(value);
|
|
6886
|
-
}
|
|
6887
|
-
if (Array.isArray(value)) {
|
|
6888
|
-
return JSON.stringify(value);
|
|
6889
|
-
}
|
|
6890
|
-
if (typeof value === "object") {
|
|
6891
|
-
return JSON.stringify(value);
|
|
6892
|
-
}
|
|
6893
|
-
return String(value);
|
|
6894
|
-
}
|
|
6895
|
-
/**
|
|
6896
|
-
* Process all template features: conditionals, loops, then variable substitution
|
|
6897
|
-
*
|
|
6898
|
-
* @param template Template string
|
|
6899
|
-
* @param values Variable values
|
|
6900
|
-
* @param variableDefs Optional variable definitions
|
|
6901
|
-
* @returns Processed content with all features applied
|
|
6902
|
-
*/
|
|
6903
|
-
processTemplate(template, values, variableDefs) {
|
|
6904
|
-
const syntaxResult = this.validateSyntax(template);
|
|
6905
|
-
if (!syntaxResult.valid && this.options.strict) {
|
|
6906
|
-
return err(syntaxResult.errors[0] ?? new TemplateSyntaxError("Unknown syntax error"));
|
|
6907
|
-
}
|
|
6908
|
-
const combinedValues = {};
|
|
6909
|
-
if (variableDefs) {
|
|
6910
|
-
for (const def of variableDefs) {
|
|
6911
|
-
if (def.defaultValue !== void 0) {
|
|
6912
|
-
combinedValues[def.name] = def.defaultValue;
|
|
6913
|
-
}
|
|
6914
|
-
}
|
|
6915
|
-
}
|
|
6916
|
-
for (const [key, value] of Object.entries(values)) {
|
|
6917
|
-
combinedValues[key] = value;
|
|
6918
|
-
}
|
|
6919
|
-
let processed = template;
|
|
6920
|
-
processed = this.processLoops(processed, combinedValues);
|
|
6921
|
-
processed = this.processConditionals(processed, combinedValues);
|
|
6922
|
-
const result = this.substituteVariables(processed, combinedValues, variableDefs);
|
|
6923
|
-
return ok(result);
|
|
6924
|
-
}
|
|
6925
|
-
/**
|
|
6926
|
-
* Render a complete test template with variable substitution
|
|
6927
|
-
*
|
|
6928
|
-
* @param template Test template to render
|
|
6929
|
-
* @param values Variable values to substitute
|
|
6930
|
-
* @param options Optional render options
|
|
6931
|
-
* @returns Render result or error
|
|
6932
|
-
*/
|
|
6933
|
-
renderTemplate(template, values, options) {
|
|
6934
|
-
const mergedOptions = { ...this.options, ...options };
|
|
6935
|
-
const syntaxResult = this.validateSyntax(template.template);
|
|
6936
|
-
if (!syntaxResult.valid && mergedOptions.strict) {
|
|
6937
|
-
return err(syntaxResult.errors[0] ?? new TemplateSyntaxError("Unknown syntax error"));
|
|
6938
|
-
}
|
|
6939
|
-
const validation = this.validateVariables(template, values);
|
|
6940
|
-
if (!validation.valid && mergedOptions.strict) {
|
|
6941
|
-
const errors = [];
|
|
6942
|
-
if (validation.missingRequired.length > 0) {
|
|
6943
|
-
errors.push(`Missing required variables: ${validation.missingRequired.join(", ")}`);
|
|
6944
|
-
}
|
|
6945
|
-
if (validation.typeErrors.length > 0) {
|
|
6946
|
-
errors.push(...validation.typeErrors);
|
|
6947
|
-
}
|
|
6948
|
-
if (validation.unknownVariables.length > 0) {
|
|
6949
|
-
errors.push(`Unknown variables: ${validation.unknownVariables.join(", ")}`);
|
|
6950
|
-
}
|
|
6951
|
-
return err(
|
|
6952
|
-
new TemplateRenderError("Template validation failed", {
|
|
6953
|
-
errors,
|
|
6954
|
-
validation
|
|
6955
|
-
})
|
|
6956
|
-
);
|
|
6957
|
-
}
|
|
6958
|
-
const processResult = this.processTemplate(template.template, values, template.variables);
|
|
6959
|
-
if (!processResult.success) {
|
|
6960
|
-
return processResult;
|
|
6961
|
-
}
|
|
6962
|
-
const { content, substituted, unresolved } = processResult.data;
|
|
6963
|
-
if (unresolved.length > 0 && !mergedOptions.allowUnresolved && mergedOptions.strict) {
|
|
6964
|
-
return err(
|
|
6965
|
-
new TemplateRenderError("Unresolved template variables", {
|
|
6966
|
-
unresolved
|
|
6967
|
-
})
|
|
6968
|
-
);
|
|
6969
|
-
}
|
|
6970
|
-
return ok({
|
|
6971
|
-
content,
|
|
6972
|
-
substituted,
|
|
6973
|
-
unresolved,
|
|
6974
|
-
imports: template.imports ?? [],
|
|
6975
|
-
fixtures: template.fixtures ?? []
|
|
6976
|
-
});
|
|
6977
|
-
}
|
|
6978
|
-
/**
|
|
6979
|
-
* Render multiple templates at once
|
|
6980
|
-
*
|
|
6981
|
-
* @param templates Array of templates to render
|
|
6982
|
-
* @param values Variable values (applied to all templates)
|
|
6983
|
-
* @returns Array of render results
|
|
6984
|
-
*/
|
|
6985
|
-
renderTemplates(templates, values) {
|
|
6986
|
-
const results = [];
|
|
6987
|
-
for (const template of templates) {
|
|
6988
|
-
const result = this.renderTemplate(template, values);
|
|
6989
|
-
if (!result.success) {
|
|
6990
|
-
return err(
|
|
6991
|
-
new TemplateRenderError(`Failed to render template '${template.id}'`, {
|
|
6992
|
-
templateId: template.id,
|
|
6993
|
-
originalError: result.error.message
|
|
6994
|
-
})
|
|
6995
|
-
);
|
|
6996
|
-
}
|
|
6997
|
-
results.push(result.data);
|
|
6998
|
-
}
|
|
6999
|
-
return ok(results);
|
|
7000
|
-
}
|
|
7001
|
-
/**
|
|
7002
|
-
* Check if a template string contains nested placeholders
|
|
7003
|
-
* (e.g., {{outer{{inner}}}})
|
|
7004
|
-
*
|
|
7005
|
-
* @param template Template string to check
|
|
7006
|
-
* @returns True if nested placeholders detected
|
|
7007
|
-
*/
|
|
7008
|
-
hasNestedPlaceholders(template) {
|
|
7009
|
-
const nestedPattern = /\{\{[^{}]*\{\{/;
|
|
7010
|
-
return nestedPattern.test(template);
|
|
7011
|
-
}
|
|
7012
|
-
/**
|
|
7013
|
-
* Extract all imports from rendered templates
|
|
7014
|
-
*
|
|
7015
|
-
* @param results Array of render results
|
|
7016
|
-
* @returns Deduplicated array of imports
|
|
7017
|
-
*/
|
|
7018
|
-
collectImports(results) {
|
|
7019
|
-
const imports = /* @__PURE__ */ new Set();
|
|
7020
|
-
for (const result of results) {
|
|
7021
|
-
for (const imp of result.imports) {
|
|
7022
|
-
imports.add(imp);
|
|
7023
|
-
}
|
|
7024
|
-
}
|
|
7025
|
-
return [...imports];
|
|
7026
|
-
}
|
|
7027
|
-
/**
|
|
7028
|
-
* Extract all fixtures from rendered templates
|
|
7029
|
-
*
|
|
7030
|
-
* @param results Array of render results
|
|
7031
|
-
* @returns Deduplicated array of fixtures
|
|
7032
|
-
*/
|
|
7033
|
-
collectFixtures(results) {
|
|
7034
|
-
const fixtures = /* @__PURE__ */ new Set();
|
|
7035
|
-
for (const result of results) {
|
|
7036
|
-
for (const fix of result.fixtures) {
|
|
7037
|
-
fixtures.add(fix);
|
|
7038
|
-
}
|
|
7039
|
-
}
|
|
7040
|
-
return [...fixtures];
|
|
7041
|
-
}
|
|
7042
|
-
/**
|
|
7043
|
-
* Get the appropriate regex for the configured placeholder format
|
|
7044
|
-
*/
|
|
7045
|
-
getPlaceholderRegex() {
|
|
7046
|
-
switch (this.options.placeholderFormat) {
|
|
7047
|
-
case "dollar":
|
|
7048
|
-
return /\$\{([a-zA-Z][a-zA-Z0-9_.]*)\}/g;
|
|
7049
|
-
case "percent":
|
|
7050
|
-
return /%\(([a-zA-Z][a-zA-Z0-9_.]*)\)s/g;
|
|
7051
|
-
case "mustache":
|
|
7052
|
-
default:
|
|
7053
|
-
return /\{\{([a-zA-Z][a-zA-Z0-9_.]*)\}\}/g;
|
|
7054
|
-
}
|
|
7055
|
-
}
|
|
7056
|
-
/**
|
|
7057
|
-
* Create a template from a string with variable definitions
|
|
7058
|
-
*
|
|
7059
|
-
* @param templateString Template content
|
|
7060
|
-
* @param variables Variable definitions
|
|
7061
|
-
* @param metadata Additional template metadata
|
|
7062
|
-
* @returns TestTemplate object
|
|
7063
|
-
*/
|
|
7064
|
-
static createTemplate(templateString, variables, metadata = {}) {
|
|
7065
|
-
return {
|
|
7066
|
-
id: metadata.id ?? "custom-template",
|
|
7067
|
-
language: metadata.language ?? "typescript",
|
|
7068
|
-
framework: metadata.framework ?? "jest",
|
|
7069
|
-
template: templateString,
|
|
7070
|
-
variables,
|
|
7071
|
-
imports: metadata.imports,
|
|
7072
|
-
fixtures: metadata.fixtures,
|
|
7073
|
-
description: metadata.description
|
|
7074
|
-
};
|
|
7075
|
-
}
|
|
7076
|
-
};
|
|
7077
|
-
function createRenderer(options) {
|
|
7078
|
-
return new TemplateRenderer(options);
|
|
7079
|
-
}
|
|
7080
6425
|
var SEVERITY_COLORS = {
|
|
7081
|
-
critical:
|
|
7082
|
-
high:
|
|
7083
|
-
medium:
|
|
7084
|
-
low:
|
|
6426
|
+
critical: chalk6.red.bold,
|
|
6427
|
+
high: chalk6.red,
|
|
6428
|
+
medium: chalk6.yellow,
|
|
6429
|
+
low: chalk6.gray
|
|
7085
6430
|
};
|
|
7086
6431
|
var PRIORITY_COLORS = {
|
|
7087
|
-
P0:
|
|
7088
|
-
P1:
|
|
7089
|
-
P2:
|
|
6432
|
+
P0: chalk6.red.bold,
|
|
6433
|
+
P1: chalk6.yellow,
|
|
6434
|
+
P2: chalk6.gray
|
|
7090
6435
|
};
|
|
7091
6436
|
var DOMAIN_COLORS = {
|
|
7092
|
-
security:
|
|
7093
|
-
data:
|
|
7094
|
-
concurrency:
|
|
7095
|
-
input:
|
|
7096
|
-
resource:
|
|
7097
|
-
reliability:
|
|
7098
|
-
performance:
|
|
7099
|
-
platform:
|
|
7100
|
-
business:
|
|
7101
|
-
compliance:
|
|
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
|
|
7102
6447
|
};
|
|
7103
6448
|
function formatTerminal(categories) {
|
|
7104
6449
|
if (categories.length === 0) {
|
|
7105
|
-
return
|
|
6450
|
+
return chalk6.yellow("No categories found matching the filters.");
|
|
7106
6451
|
}
|
|
7107
6452
|
const lines = [];
|
|
7108
|
-
lines.push(
|
|
6453
|
+
lines.push(chalk6.bold.underline(`Found ${categories.length} categories:
|
|
7109
6454
|
`));
|
|
7110
6455
|
const byDomain = /* @__PURE__ */ new Map();
|
|
7111
6456
|
for (const cat of categories) {
|
|
@@ -7116,26 +6461,26 @@ function formatTerminal(categories) {
|
|
|
7116
6461
|
byDomain.get(domain).push(cat);
|
|
7117
6462
|
}
|
|
7118
6463
|
for (const [domain, domainCategories] of byDomain) {
|
|
7119
|
-
const domainColor = DOMAIN_COLORS[domain] ??
|
|
6464
|
+
const domainColor = DOMAIN_COLORS[domain] ?? chalk6.white;
|
|
7120
6465
|
lines.push(domainColor.bold(`
|
|
7121
6466
|
${domain.toUpperCase()} (${domainCategories.length})`));
|
|
7122
|
-
lines.push(
|
|
6467
|
+
lines.push(chalk6.gray("\u2500".repeat(40)));
|
|
7123
6468
|
for (const cat of domainCategories) {
|
|
7124
6469
|
const priorityColor = PRIORITY_COLORS[cat.priority];
|
|
7125
6470
|
const severityColor = SEVERITY_COLORS[cat.severity];
|
|
7126
6471
|
const priority = priorityColor(`[${cat.priority}]`);
|
|
7127
6472
|
const severity = severityColor(`${cat.severity}`);
|
|
7128
|
-
const level =
|
|
7129
|
-
const name =
|
|
7130
|
-
const id =
|
|
6473
|
+
const level = chalk6.cyan(`${cat.level}`);
|
|
6474
|
+
const name = chalk6.white.bold(cat.name);
|
|
6475
|
+
const id = chalk6.gray(`(${cat.id})`);
|
|
7131
6476
|
lines.push(` ${priority} ${name} ${id}`);
|
|
7132
6477
|
lines.push(` ${severity} | ${level}`);
|
|
7133
6478
|
const desc = cat.description.length > 80 ? cat.description.slice(0, 77) + "..." : cat.description;
|
|
7134
|
-
lines.push(
|
|
6479
|
+
lines.push(chalk6.gray(` ${desc}`));
|
|
7135
6480
|
lines.push("");
|
|
7136
6481
|
}
|
|
7137
6482
|
}
|
|
7138
|
-
lines.push(
|
|
6483
|
+
lines.push(chalk6.gray("\u2500".repeat(40)));
|
|
7139
6484
|
lines.push(formatStats(categories));
|
|
7140
6485
|
return lines.join("\n");
|
|
7141
6486
|
}
|
|
@@ -7157,7 +6502,7 @@ function formatStats(categories) {
|
|
|
7157
6502
|
if (stats.P0 > 0) parts.push(PRIORITY_COLORS.P0(`${stats.P0} P0`));
|
|
7158
6503
|
if (stats.P1 > 0) parts.push(PRIORITY_COLORS.P1(`${stats.P1} P1`));
|
|
7159
6504
|
if (stats.P2 > 0) parts.push(PRIORITY_COLORS.P2(`${stats.P2} P2`));
|
|
7160
|
-
parts.push(
|
|
6505
|
+
parts.push(chalk6.gray("|"));
|
|
7161
6506
|
if (stats.critical > 0) parts.push(SEVERITY_COLORS.critical(`${stats.critical} critical`));
|
|
7162
6507
|
if (stats.high > 0) parts.push(SEVERITY_COLORS.high(`${stats.high} high`));
|
|
7163
6508
|
if (stats.medium > 0) parts.push(SEVERITY_COLORS.medium(`${stats.medium} medium`));
|
|
@@ -7214,7 +6559,7 @@ function isValidOutputFormat(format) {
|
|
|
7214
6559
|
return ["terminal", "json", "markdown"].includes(format);
|
|
7215
6560
|
}
|
|
7216
6561
|
function formatError(error) {
|
|
7217
|
-
return
|
|
6562
|
+
return chalk6.red(`Error: ${error.message}`);
|
|
7218
6563
|
}
|
|
7219
6564
|
|
|
7220
6565
|
// src/ai/service.ts
|
|
@@ -7263,11 +6608,11 @@ var AIService = class {
|
|
|
7263
6608
|
*/
|
|
7264
6609
|
getApiKeyFromConfig(provider) {
|
|
7265
6610
|
try {
|
|
7266
|
-
const { existsSync:
|
|
6611
|
+
const { existsSync: existsSync7, readFileSync: readFileSync3 } = __require("fs");
|
|
7267
6612
|
const { homedir: homedir3 } = __require("os");
|
|
7268
6613
|
const { join: join5 } = __require("path");
|
|
7269
6614
|
const configPath = join5(homedir3(), ".pinata", "config.json");
|
|
7270
|
-
if (!
|
|
6615
|
+
if (!existsSync7(configPath)) {
|
|
7271
6616
|
return "";
|
|
7272
6617
|
}
|
|
7273
6618
|
const content = readFileSync3(configPath, "utf-8");
|
|
@@ -7561,303 +6906,76 @@ async function explainGaps(gaps, categories, config2) {
|
|
|
7561
6906
|
const promises = batch.map(async (gap) => {
|
|
7562
6907
|
const category = categories?.get(gap.categoryId);
|
|
7563
6908
|
const result = await explainGap(gap, category, config2);
|
|
7564
|
-
return { key: `${gap.filePath}:${gap.lineStart}:${gap.categoryId}`, result };
|
|
7565
|
-
});
|
|
7566
|
-
const batchResults = await Promise.all(promises);
|
|
7567
|
-
for (const { key, result } of batchResults) {
|
|
7568
|
-
results.set(key, result);
|
|
7569
|
-
}
|
|
7570
|
-
}
|
|
7571
|
-
return results;
|
|
7572
|
-
}
|
|
7573
|
-
function buildExplainPrompt(gap, category) {
|
|
7574
|
-
const parts = [];
|
|
7575
|
-
parts.push(`Explain this security finding:
|
|
7576
|
-
`);
|
|
7577
|
-
parts.push(`**Category:** ${gap.categoryName} (${gap.categoryId})`);
|
|
7578
|
-
parts.push(`**Severity:** ${gap.severity}`);
|
|
7579
|
-
parts.push(`**Confidence:** ${gap.confidence}`);
|
|
7580
|
-
parts.push(`**File:** ${gap.filePath}`);
|
|
7581
|
-
parts.push(`**Line:** ${gap.lineStart}`);
|
|
7582
|
-
if (gap.codeSnippet) {
|
|
7583
|
-
parts.push(`
|
|
7584
|
-
**Code:**
|
|
7585
|
-
\`\`\`
|
|
7586
|
-
${gap.codeSnippet}
|
|
7587
|
-
\`\`\``);
|
|
7588
|
-
}
|
|
7589
|
-
parts.push(`
|
|
7590
|
-
**Pattern:** ${gap.patternId}`);
|
|
7591
|
-
parts.push(`**Detection Type:** ${gap.patternType}`);
|
|
7592
|
-
parts.push(`
|
|
7593
|
-
Provide a clear, actionable explanation for a developer.`);
|
|
7594
|
-
return parts.join("\n");
|
|
7595
|
-
}
|
|
7596
|
-
function generateFallbackExplanation(gap) {
|
|
7597
|
-
const summaries = {
|
|
7598
|
-
"sql-injection": "SQL query constructed with user input may allow injection attacks.",
|
|
7599
|
-
"xss": "User input rendered without escaping may allow script injection.",
|
|
7600
|
-
"command-injection": "Shell command constructed with user input may allow command execution.",
|
|
7601
|
-
"path-traversal": "File path constructed with user input may allow directory traversal.",
|
|
7602
|
-
"hardcoded-secrets": "Sensitive credentials found in source code.",
|
|
7603
|
-
"deserialization": "Untrusted data deserialization may allow code execution.",
|
|
7604
|
-
"ssrf": "Server-side request with user-controlled URL may allow internal access.",
|
|
7605
|
-
"xxe": "XML parser may be vulnerable to external entity injection.",
|
|
7606
|
-
"csrf": "State-changing request lacks CSRF protection.",
|
|
7607
|
-
"ldap-injection": "LDAP query constructed with user input may allow injection."
|
|
7608
|
-
};
|
|
7609
|
-
const remediations = {
|
|
7610
|
-
"sql-injection": "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
|
|
7611
|
-
"xss": "Escape all user input before rendering in HTML. Use framework auto-escaping features.",
|
|
7612
|
-
"command-injection": "Avoid shell execution with user input. Use allowlists and subprocess arrays instead of shell strings.",
|
|
7613
|
-
"path-traversal": "Validate and sanitize file paths. Use path.resolve() and verify the result is within allowed directories.",
|
|
7614
|
-
"hardcoded-secrets": "Move secrets to environment variables or a secrets manager. Never commit credentials to source control.",
|
|
7615
|
-
"deserialization": "Avoid deserializing untrusted data. If necessary, use safe formats like JSON instead of pickle/yaml.",
|
|
7616
|
-
"ssrf": "Validate and allowlist URLs. Block private IP ranges and localhost.",
|
|
7617
|
-
"xxe": "Disable external entity processing in XML parser configuration.",
|
|
7618
|
-
"csrf": "Implement CSRF tokens for all state-changing requests.",
|
|
7619
|
-
"ldap-injection": "Escape special LDAP characters in user input. Use parameterized LDAP queries."
|
|
7620
|
-
};
|
|
7621
|
-
const summary = summaries[gap.categoryId] ?? `Potential ${gap.categoryName} vulnerability detected.`;
|
|
7622
|
-
const remediation = remediations[gap.categoryId] ?? `Review the code for security issues and apply appropriate fixes.`;
|
|
7623
|
-
return {
|
|
7624
|
-
summary,
|
|
7625
|
-
explanation: `The pattern "${gap.patternId}" detected a potential ${gap.categoryName} vulnerability at line ${gap.lineStart}. This type of issue has ${gap.severity} severity and was detected with ${gap.confidence} confidence.`,
|
|
7626
|
-
risk: `If exploited, this vulnerability could compromise the security of the application. Severity: ${gap.severity}.`,
|
|
7627
|
-
remediation,
|
|
7628
|
-
references: []
|
|
7629
|
-
};
|
|
7630
|
-
}
|
|
7631
|
-
|
|
7632
|
-
// src/ai/template-filler.ts
|
|
7633
|
-
var SYSTEM_PROMPT2 = `You are an expert at analyzing code and extracting meaningful variable values for test generation.
|
|
7634
|
-
Given a code snippet and a list of template variables, suggest appropriate values for each variable.
|
|
7635
|
-
|
|
7636
|
-
For each variable, analyze:
|
|
7637
|
-
1. The code snippet to extract relevant information (class names, function names, etc.)
|
|
7638
|
-
2. The variable description to understand what's needed
|
|
7639
|
-
3. The variable type to ensure correct formatting
|
|
7640
|
-
|
|
7641
|
-
Always respond with valid JSON matching this structure:
|
|
7642
|
-
{
|
|
7643
|
-
"suggestions": [
|
|
7644
|
-
{
|
|
7645
|
-
"name": "variableName",
|
|
7646
|
-
"value": "suggested value",
|
|
7647
|
-
"reasoning": "why this value was chosen",
|
|
7648
|
-
"confidence": 0.0-1.0
|
|
7649
|
-
}
|
|
7650
|
-
]
|
|
7651
|
-
}
|
|
7652
|
-
|
|
7653
|
-
For arrays, use: "value": ["item1", "item2"]
|
|
7654
|
-
For booleans, use: "value": true or "value": false
|
|
7655
|
-
For numbers, use: "value": 42`;
|
|
7656
|
-
async function suggestVariables(request, config2) {
|
|
7657
|
-
const ai = createAIService(config2);
|
|
7658
|
-
if (!ai.isConfigured()) {
|
|
7659
|
-
return {
|
|
7660
|
-
success: true,
|
|
7661
|
-
data: extractVariablesFromCode(request),
|
|
7662
|
-
durationMs: 0
|
|
7663
|
-
};
|
|
7664
|
-
}
|
|
7665
|
-
const prompt = buildVariablePrompt(request);
|
|
7666
|
-
const startTime = Date.now();
|
|
7667
|
-
const response = await ai.completeJSON({
|
|
7668
|
-
systemPrompt: SYSTEM_PROMPT2,
|
|
7669
|
-
messages: [{ role: "user", content: prompt }],
|
|
7670
|
-
maxTokens: 1024,
|
|
7671
|
-
temperature: 0.2
|
|
7672
|
-
});
|
|
7673
|
-
if (!response.success || !response.data) {
|
|
7674
|
-
return {
|
|
7675
|
-
success: true,
|
|
7676
|
-
data: extractVariablesFromCode(request),
|
|
7677
|
-
durationMs: Date.now() - startTime
|
|
7678
|
-
};
|
|
7679
|
-
}
|
|
7680
|
-
const suggestions = /* @__PURE__ */ new Map();
|
|
7681
|
-
const unfilled = [];
|
|
7682
|
-
const values = { ...request.existingValues };
|
|
7683
|
-
const suggestionsList = response.data.suggestions ?? [];
|
|
7684
|
-
for (const suggestion of suggestionsList) {
|
|
7685
|
-
suggestions.set(suggestion.name, suggestion);
|
|
7686
|
-
if (!(suggestion.name in values)) {
|
|
7687
|
-
values[suggestion.name] = suggestion.value;
|
|
7688
|
-
}
|
|
7689
|
-
}
|
|
7690
|
-
for (const variable of request.variables) {
|
|
7691
|
-
if (!suggestions.has(variable.name) && !(variable.name in values)) {
|
|
7692
|
-
if (variable.defaultValue !== void 0) {
|
|
7693
|
-
values[variable.name] = variable.defaultValue;
|
|
7694
|
-
} else {
|
|
7695
|
-
unfilled.push(variable.name);
|
|
7696
|
-
}
|
|
7697
|
-
}
|
|
7698
|
-
}
|
|
7699
|
-
const result = {
|
|
7700
|
-
success: true,
|
|
7701
|
-
data: { suggestions, unfilled, values },
|
|
7702
|
-
durationMs: response.durationMs
|
|
7703
|
-
};
|
|
7704
|
-
if (response.usage) {
|
|
7705
|
-
result.usage = response.usage;
|
|
7706
|
-
}
|
|
7707
|
-
return result;
|
|
7708
|
-
}
|
|
7709
|
-
function buildVariablePrompt(request) {
|
|
7710
|
-
const parts = [];
|
|
7711
|
-
parts.push("Analyze this code and suggest values for the template variables:\n");
|
|
7712
|
-
parts.push("**Code:**");
|
|
7713
|
-
parts.push("```");
|
|
7714
|
-
parts.push(request.codeSnippet);
|
|
7715
|
-
parts.push("```\n");
|
|
7716
|
-
parts.push(`**File:** ${request.filePath}
|
|
7717
|
-
`);
|
|
7718
|
-
if (request.gap) {
|
|
7719
|
-
parts.push(`**Category:** ${request.gap.categoryName}`);
|
|
7720
|
-
parts.push(`**Line:** ${request.gap.lineStart}
|
|
7721
|
-
`);
|
|
7722
|
-
}
|
|
7723
|
-
parts.push("**Variables to fill:**");
|
|
7724
|
-
for (const variable of request.variables) {
|
|
7725
|
-
const required = variable.required ? " (required)" : " (optional)";
|
|
7726
|
-
const defaultVal = variable.defaultValue !== void 0 ? ` [default: ${JSON.stringify(variable.defaultValue)}]` : "";
|
|
7727
|
-
parts.push(`- ${variable.name} (${variable.type})${required}${defaultVal}: ${variable.description}`);
|
|
7728
|
-
}
|
|
7729
|
-
if (request.existingValues && Object.keys(request.existingValues).length > 0) {
|
|
7730
|
-
parts.push("\n**Already provided:**");
|
|
7731
|
-
for (const [name, value] of Object.entries(request.existingValues)) {
|
|
7732
|
-
parts.push(`- ${name}: ${JSON.stringify(value)}`);
|
|
7733
|
-
}
|
|
7734
|
-
}
|
|
7735
|
-
parts.push("\nExtract appropriate values from the code context.");
|
|
7736
|
-
return parts.join("\n");
|
|
7737
|
-
}
|
|
7738
|
-
function extractVariablesFromCode(request) {
|
|
7739
|
-
const suggestions = /* @__PURE__ */ new Map();
|
|
7740
|
-
const unfilled = [];
|
|
7741
|
-
const values = { ...request.existingValues };
|
|
7742
|
-
const code = request.codeSnippet;
|
|
7743
|
-
const filePath = request.filePath;
|
|
7744
|
-
for (const variable of request.variables) {
|
|
7745
|
-
if (variable.name in values) continue;
|
|
7746
|
-
let value = void 0;
|
|
7747
|
-
let reasoning = "";
|
|
7748
|
-
let confidence = 0;
|
|
7749
|
-
switch (variable.name.toLowerCase()) {
|
|
7750
|
-
case "classname":
|
|
7751
|
-
case "class_name": {
|
|
7752
|
-
const classMatch = code.match(/class\s+(\w+)/);
|
|
7753
|
-
if (classMatch) {
|
|
7754
|
-
value = classMatch[1];
|
|
7755
|
-
reasoning = "Extracted from class definition in code";
|
|
7756
|
-
confidence = 0.9;
|
|
7757
|
-
} else {
|
|
7758
|
-
const fileName = filePath.split("/").pop()?.replace(/\.\w+$/, "") ?? "";
|
|
7759
|
-
value = toPascalCase(fileName);
|
|
7760
|
-
reasoning = "Inferred from file name";
|
|
7761
|
-
confidence = 0.6;
|
|
7762
|
-
}
|
|
7763
|
-
break;
|
|
7764
|
-
}
|
|
7765
|
-
case "functionname":
|
|
7766
|
-
case "function_name":
|
|
7767
|
-
case "methodname": {
|
|
7768
|
-
const funcMatch = code.match(/(?:def|function|async function)\s+(\w+)/);
|
|
7769
|
-
if (funcMatch) {
|
|
7770
|
-
value = funcMatch[1];
|
|
7771
|
-
reasoning = "Extracted from function definition";
|
|
7772
|
-
confidence = 0.9;
|
|
7773
|
-
}
|
|
7774
|
-
break;
|
|
7775
|
-
}
|
|
7776
|
-
case "modulepath":
|
|
7777
|
-
case "module_path": {
|
|
7778
|
-
value = filePath.replace(/\.[jt]sx?$/, "").replace(/\.py$/, "").replace(/\//g, ".").replace(/^\.+/, "");
|
|
7779
|
-
reasoning = "Derived from file path";
|
|
7780
|
-
confidence = 0.7;
|
|
7781
|
-
break;
|
|
7782
|
-
}
|
|
7783
|
-
case "tablename":
|
|
7784
|
-
case "table_name": {
|
|
7785
|
-
const tableMatch = code.match(/(?:FROM|INTO|UPDATE)\s+(\w+)/i);
|
|
7786
|
-
if (tableMatch) {
|
|
7787
|
-
value = tableMatch[1];
|
|
7788
|
-
reasoning = "Extracted from SQL statement";
|
|
7789
|
-
confidence = 0.8;
|
|
7790
|
-
} else {
|
|
7791
|
-
value = "users";
|
|
7792
|
-
reasoning = "Default table name";
|
|
7793
|
-
confidence = 0.3;
|
|
7794
|
-
}
|
|
7795
|
-
break;
|
|
7796
|
-
}
|
|
7797
|
-
case "exceptionclass":
|
|
7798
|
-
case "exception_class": {
|
|
7799
|
-
value = "ValueError";
|
|
7800
|
-
reasoning = "Common exception for input validation";
|
|
7801
|
-
confidence = 0.5;
|
|
7802
|
-
break;
|
|
7803
|
-
}
|
|
7804
|
-
case "dbclient":
|
|
7805
|
-
case "db_client": {
|
|
7806
|
-
const clientMatch = code.match(/(db|conn|connection|client|cursor)\s*[=.]/i);
|
|
7807
|
-
if (clientMatch) {
|
|
7808
|
-
value = clientMatch[1];
|
|
7809
|
-
reasoning = "Extracted from code";
|
|
7810
|
-
confidence = 0.7;
|
|
7811
|
-
} else {
|
|
7812
|
-
value = "db";
|
|
7813
|
-
reasoning = "Default database client name";
|
|
7814
|
-
confidence = 0.4;
|
|
7815
|
-
}
|
|
7816
|
-
break;
|
|
7817
|
-
}
|
|
7818
|
-
case "functioncall":
|
|
7819
|
-
case "function_call": {
|
|
7820
|
-
const funcName = values["functionName"] ?? values["function_name"];
|
|
7821
|
-
if (typeof funcName === "string" && funcName.length > 0) {
|
|
7822
|
-
value = `${funcName}(user_input)`;
|
|
7823
|
-
reasoning = "Constructed from function name";
|
|
7824
|
-
confidence = 0.6;
|
|
7825
|
-
}
|
|
7826
|
-
break;
|
|
7827
|
-
}
|
|
7828
|
-
case "fixtures": {
|
|
7829
|
-
value = "db_session";
|
|
7830
|
-
reasoning = "Common pytest fixture";
|
|
7831
|
-
confidence = 0.5;
|
|
7832
|
-
break;
|
|
7833
|
-
}
|
|
7834
|
-
default:
|
|
7835
|
-
if (variable.defaultValue !== void 0) {
|
|
7836
|
-
value = variable.defaultValue;
|
|
7837
|
-
reasoning = "Using default value";
|
|
7838
|
-
confidence = 1;
|
|
7839
|
-
}
|
|
7840
|
-
}
|
|
7841
|
-
if (value !== void 0) {
|
|
7842
|
-
suggestions.set(variable.name, {
|
|
7843
|
-
name: variable.name,
|
|
7844
|
-
value,
|
|
7845
|
-
reasoning,
|
|
7846
|
-
confidence
|
|
7847
|
-
});
|
|
7848
|
-
values[variable.name] = value;
|
|
7849
|
-
} else if (variable.required) {
|
|
7850
|
-
unfilled.push(variable.name);
|
|
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);
|
|
7851
6914
|
}
|
|
7852
6915
|
}
|
|
7853
|
-
return
|
|
6916
|
+
return results;
|
|
6917
|
+
}
|
|
6918
|
+
function buildExplainPrompt(gap, category) {
|
|
6919
|
+
const parts = [];
|
|
6920
|
+
parts.push(`Explain this security finding:
|
|
6921
|
+
`);
|
|
6922
|
+
parts.push(`**Category:** ${gap.categoryName} (${gap.categoryId})`);
|
|
6923
|
+
parts.push(`**Severity:** ${gap.severity}`);
|
|
6924
|
+
parts.push(`**Confidence:** ${gap.confidence}`);
|
|
6925
|
+
parts.push(`**File:** ${gap.filePath}`);
|
|
6926
|
+
parts.push(`**Line:** ${gap.lineStart}`);
|
|
6927
|
+
if (gap.codeSnippet) {
|
|
6928
|
+
parts.push(`
|
|
6929
|
+
**Code:**
|
|
6930
|
+
\`\`\`
|
|
6931
|
+
${gap.codeSnippet}
|
|
6932
|
+
\`\`\``);
|
|
6933
|
+
}
|
|
6934
|
+
parts.push(`
|
|
6935
|
+
**Pattern:** ${gap.patternId}`);
|
|
6936
|
+
parts.push(`**Detection Type:** ${gap.patternType}`);
|
|
6937
|
+
parts.push(`
|
|
6938
|
+
Provide a clear, actionable explanation for a developer.`);
|
|
6939
|
+
return parts.join("\n");
|
|
7854
6940
|
}
|
|
7855
|
-
function
|
|
7856
|
-
|
|
6941
|
+
function generateFallbackExplanation(gap) {
|
|
6942
|
+
const summaries = {
|
|
6943
|
+
"sql-injection": "SQL query constructed with user input may allow injection attacks.",
|
|
6944
|
+
"xss": "User input rendered without escaping may allow script injection.",
|
|
6945
|
+
"command-injection": "Shell command constructed with user input may allow command execution.",
|
|
6946
|
+
"path-traversal": "File path constructed with user input may allow directory traversal.",
|
|
6947
|
+
"hardcoded-secrets": "Sensitive credentials found in source code.",
|
|
6948
|
+
"deserialization": "Untrusted data deserialization may allow code execution.",
|
|
6949
|
+
"ssrf": "Server-side request with user-controlled URL may allow internal access.",
|
|
6950
|
+
"xxe": "XML parser may be vulnerable to external entity injection.",
|
|
6951
|
+
"csrf": "State-changing request lacks CSRF protection.",
|
|
6952
|
+
"ldap-injection": "LDAP query constructed with user input may allow injection."
|
|
6953
|
+
};
|
|
6954
|
+
const remediations = {
|
|
6955
|
+
"sql-injection": "Use parameterized queries or prepared statements. Never concatenate user input into SQL strings.",
|
|
6956
|
+
"xss": "Escape all user input before rendering in HTML. Use framework auto-escaping features.",
|
|
6957
|
+
"command-injection": "Avoid shell execution with user input. Use allowlists and subprocess arrays instead of shell strings.",
|
|
6958
|
+
"path-traversal": "Validate and sanitize file paths. Use path.resolve() and verify the result is within allowed directories.",
|
|
6959
|
+
"hardcoded-secrets": "Move secrets to environment variables or a secrets manager. Never commit credentials to source control.",
|
|
6960
|
+
"deserialization": "Avoid deserializing untrusted data. If necessary, use safe formats like JSON instead of pickle/yaml.",
|
|
6961
|
+
"ssrf": "Validate and allowlist URLs. Block private IP ranges and localhost.",
|
|
6962
|
+
"xxe": "Disable external entity processing in XML parser configuration.",
|
|
6963
|
+
"csrf": "Implement CSRF tokens for all state-changing requests.",
|
|
6964
|
+
"ldap-injection": "Escape special LDAP characters in user input. Use parameterized LDAP queries."
|
|
6965
|
+
};
|
|
6966
|
+
const summary = summaries[gap.categoryId] ?? `Potential ${gap.categoryName} vulnerability detected.`;
|
|
6967
|
+
const remediation = remediations[gap.categoryId] ?? `Review the code for security issues and apply appropriate fixes.`;
|
|
6968
|
+
return {
|
|
6969
|
+
summary,
|
|
6970
|
+
explanation: `The pattern "${gap.patternId}" detected a potential ${gap.categoryName} vulnerability at line ${gap.lineStart}. This type of issue has ${gap.severity} severity and was detected with ${gap.confidence} confidence.`,
|
|
6971
|
+
risk: `If exploited, this vulnerability could compromise the security of the application. Severity: ${gap.severity}.`,
|
|
6972
|
+
remediation,
|
|
6973
|
+
references: []
|
|
6974
|
+
};
|
|
7857
6975
|
}
|
|
7858
6976
|
|
|
7859
6977
|
// src/ai/pattern-suggester.ts
|
|
7860
|
-
var
|
|
6978
|
+
var SYSTEM_PROMPT2 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
|
|
7861
6979
|
Given vulnerable code samples, generate regex patterns that will detect similar vulnerabilities.
|
|
7862
6980
|
|
|
7863
6981
|
Your patterns should:
|
|
@@ -7896,7 +7014,7 @@ async function suggestPatterns(request, config2) {
|
|
|
7896
7014
|
}
|
|
7897
7015
|
const prompt = buildPatternPrompt(request);
|
|
7898
7016
|
const response = await ai.completeJSON({
|
|
7899
|
-
systemPrompt:
|
|
7017
|
+
systemPrompt: SYSTEM_PROMPT2,
|
|
7900
7018
|
messages: [{ role: "user", content: prompt }],
|
|
7901
7019
|
maxTokens: 2048,
|
|
7902
7020
|
temperature: 0.3
|
|
@@ -8042,72 +7160,72 @@ function getDefinitionsPath() {
|
|
|
8042
7160
|
}
|
|
8043
7161
|
init_results_cache();
|
|
8044
7162
|
var SEVERITY_COLORS2 = {
|
|
8045
|
-
critical:
|
|
8046
|
-
high:
|
|
8047
|
-
medium:
|
|
8048
|
-
low:
|
|
7163
|
+
critical: chalk6.red.bold,
|
|
7164
|
+
high: chalk6.red,
|
|
7165
|
+
medium: chalk6.yellow,
|
|
7166
|
+
low: chalk6.gray
|
|
8049
7167
|
};
|
|
8050
7168
|
var DOMAIN_COLORS2 = {
|
|
8051
|
-
security:
|
|
8052
|
-
data:
|
|
8053
|
-
concurrency:
|
|
8054
|
-
input:
|
|
8055
|
-
resource:
|
|
8056
|
-
reliability:
|
|
8057
|
-
performance:
|
|
8058
|
-
platform:
|
|
8059
|
-
business:
|
|
8060
|
-
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
|
|
8061
7179
|
};
|
|
8062
7180
|
var GRADE_COLORS = {
|
|
8063
|
-
A:
|
|
8064
|
-
B:
|
|
8065
|
-
C:
|
|
8066
|
-
D:
|
|
8067
|
-
F:
|
|
7181
|
+
A: chalk6.green.bold,
|
|
7182
|
+
B: chalk6.green,
|
|
7183
|
+
C: chalk6.yellow,
|
|
7184
|
+
D: chalk6.red,
|
|
7185
|
+
F: chalk6.red.bold
|
|
8068
7186
|
};
|
|
8069
7187
|
var BANNER = `
|
|
8070
|
-
${
|
|
8071
|
-
${
|
|
8072
|
-
${
|
|
8073
|
-
${
|
|
8074
|
-
${
|
|
7188
|
+
${chalk6.cyan(" ____ _ _ ")}
|
|
7189
|
+
${chalk6.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
|
|
7190
|
+
${chalk6.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
|
|
7191
|
+
${chalk6.cyan("| __/| | | | | (_| | || (_| |")}
|
|
7192
|
+
${chalk6.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
|
|
8075
7193
|
`;
|
|
8076
7194
|
function formatScanTerminal(result, basePath) {
|
|
8077
7195
|
const lines = [];
|
|
8078
7196
|
lines.push(BANNER);
|
|
8079
|
-
lines.push(
|
|
7197
|
+
lines.push(chalk6.gray(`Analyzing: ${result.targetDirectory}`));
|
|
8080
7198
|
const projectTypeLabel = getProjectTypeDescription(result.projectType.type);
|
|
8081
|
-
lines.push(
|
|
8082
|
-
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)}`));
|
|
8083
7201
|
lines.push("");
|
|
8084
7202
|
lines.push(formatScoreBox(result.score));
|
|
8085
7203
|
lines.push("");
|
|
8086
|
-
lines.push(
|
|
7204
|
+
lines.push(chalk6.bold("Domain Coverage:"));
|
|
8087
7205
|
lines.push(formatDomainCoverage(result.coverage));
|
|
8088
7206
|
lines.push("");
|
|
8089
7207
|
if (result.gaps.length > 0) {
|
|
8090
7208
|
lines.push(formatGapsSummary(result.gaps, basePath));
|
|
8091
7209
|
lines.push("");
|
|
8092
7210
|
} else {
|
|
8093
|
-
lines.push(
|
|
7211
|
+
lines.push(chalk6.green.bold("No vulnerabilities detected."));
|
|
8094
7212
|
lines.push("");
|
|
8095
7213
|
}
|
|
8096
7214
|
if (result.gaps.length > 0) {
|
|
8097
|
-
lines.push(
|
|
7215
|
+
lines.push(chalk6.gray("Run `pinata generate --gaps` to create tests for these gaps."));
|
|
8098
7216
|
}
|
|
8099
|
-
lines.push(
|
|
7217
|
+
lines.push(chalk6.gray(`
|
|
8100
7218
|
Scan completed in ${result.durationMs}ms`));
|
|
8101
7219
|
return lines.join("\n");
|
|
8102
7220
|
}
|
|
8103
7221
|
function formatScoreBox(score) {
|
|
8104
|
-
const gradeColor = GRADE_COLORS[score.grade] ??
|
|
7222
|
+
const gradeColor = GRADE_COLORS[score.grade] ?? chalk6.white;
|
|
8105
7223
|
const scoreStr = `Pinata Score: ${score.overall}/100 ${gradeColor(`(${score.grade})`)}`;
|
|
8106
7224
|
const boxWidth = 60;
|
|
8107
7225
|
const padding = Math.floor((boxWidth - scoreStr.length) / 2);
|
|
8108
|
-
const top =
|
|
8109
|
-
const middle =
|
|
8110
|
-
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");
|
|
8111
7229
|
return `${top}
|
|
8112
7230
|
${middle}
|
|
8113
7231
|
${bottom}`;
|
|
@@ -8122,14 +7240,14 @@ function formatDomainCoverage(coverage) {
|
|
|
8122
7240
|
}
|
|
8123
7241
|
const percent = domainCoverage.coveragePercent;
|
|
8124
7242
|
const filledWidth = Math.round(percent / 100 * barWidth);
|
|
8125
|
-
const bar =
|
|
8126
|
-
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;
|
|
8127
7245
|
const domainName = domain.padEnd(15);
|
|
8128
7246
|
const stats = `${domainCoverage.categoriesCovered}/${domainCoverage.categoriesScanned} categories`;
|
|
8129
7247
|
lines.push(` ${domainColor(domainName)} ${bar} ${percent.toString().padStart(3)}% (${stats})`);
|
|
8130
7248
|
}
|
|
8131
7249
|
if (lines.length === 0) {
|
|
8132
|
-
lines.push(
|
|
7250
|
+
lines.push(chalk6.gray(" No domain coverage data available."));
|
|
8133
7251
|
}
|
|
8134
7252
|
return lines.join("\n");
|
|
8135
7253
|
}
|
|
@@ -8140,48 +7258,48 @@ function formatGapsSummary(gaps, basePath) {
|
|
|
8140
7258
|
const medium = gaps.filter((g) => g.severity === "medium");
|
|
8141
7259
|
const low = gaps.filter((g) => g.severity === "low");
|
|
8142
7260
|
if (critical.length > 0) {
|
|
8143
|
-
lines.push(
|
|
7261
|
+
lines.push(chalk6.red.bold(`
|
|
8144
7262
|
Critical Gaps (${critical.length}):`));
|
|
8145
7263
|
for (const gap of critical.slice(0, 5)) {
|
|
8146
7264
|
lines.push(formatGapLine(gap, basePath, "critical"));
|
|
8147
7265
|
}
|
|
8148
7266
|
if (critical.length > 5) {
|
|
8149
|
-
lines.push(
|
|
7267
|
+
lines.push(chalk6.gray(` ... and ${critical.length - 5} more critical gaps`));
|
|
8150
7268
|
}
|
|
8151
7269
|
}
|
|
8152
7270
|
if (high.length > 0) {
|
|
8153
|
-
lines.push(
|
|
7271
|
+
lines.push(chalk6.red(`
|
|
8154
7272
|
High Severity Gaps (${high.length}):`));
|
|
8155
7273
|
for (const gap of high.slice(0, 5)) {
|
|
8156
7274
|
lines.push(formatGapLine(gap, basePath, "high"));
|
|
8157
7275
|
}
|
|
8158
7276
|
if (high.length > 5) {
|
|
8159
|
-
lines.push(
|
|
7277
|
+
lines.push(chalk6.gray(` ... and ${high.length - 5} more high severity gaps`));
|
|
8160
7278
|
}
|
|
8161
7279
|
}
|
|
8162
7280
|
if (medium.length > 0) {
|
|
8163
|
-
lines.push(
|
|
7281
|
+
lines.push(chalk6.yellow(`
|
|
8164
7282
|
Medium Severity Gaps (${medium.length}):`));
|
|
8165
7283
|
for (const gap of medium.slice(0, 3)) {
|
|
8166
7284
|
lines.push(formatGapLine(gap, basePath, "medium"));
|
|
8167
7285
|
}
|
|
8168
7286
|
if (medium.length > 3) {
|
|
8169
|
-
lines.push(
|
|
7287
|
+
lines.push(chalk6.gray(` ... and ${medium.length - 3} more medium severity gaps`));
|
|
8170
7288
|
}
|
|
8171
7289
|
}
|
|
8172
7290
|
if (low.length > 0) {
|
|
8173
|
-
lines.push(
|
|
7291
|
+
lines.push(chalk6.gray(`
|
|
8174
7292
|
Low Severity: ${low.length} gaps`));
|
|
8175
7293
|
}
|
|
8176
7294
|
return lines.join("\n");
|
|
8177
7295
|
}
|
|
8178
7296
|
function formatGapLine(gap, basePath, severity) {
|
|
8179
|
-
const severityColor = SEVERITY_COLORS2[severity] ??
|
|
7297
|
+
const severityColor = SEVERITY_COLORS2[severity] ?? chalk6.white;
|
|
8180
7298
|
const icon = severity === "critical" ? "\u26D4" : severity === "high" ? "\u{1F534}" : severity === "medium" ? "\u{1F7E1}" : "\u26AA";
|
|
8181
7299
|
const relPath = relative(basePath, gap.filePath);
|
|
8182
7300
|
const location = `${relPath}:${gap.lineStart}`;
|
|
8183
7301
|
const confidence = gap.confidence.toUpperCase();
|
|
8184
|
-
return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${
|
|
7302
|
+
return ` ${icon} ${severityColor(gap.categoryName.padEnd(20))} ${chalk6.cyan(location.padEnd(30))} ${chalk6.gray(confidence)} confidence`;
|
|
8185
7303
|
}
|
|
8186
7304
|
function formatLanguages(result) {
|
|
8187
7305
|
const languages = [];
|
|
@@ -8284,7 +7402,7 @@ _...and ${gaps.length - 10} more ${severity} gaps_
|
|
|
8284
7402
|
}
|
|
8285
7403
|
} else {
|
|
8286
7404
|
lines.push("## No Gaps Detected\n");
|
|
8287
|
-
lines.push("
|
|
7405
|
+
lines.push("No vulnerabilities detected.\n");
|
|
8288
7406
|
}
|
|
8289
7407
|
lines.push("## Summary\n");
|
|
8290
7408
|
lines.push(`- Total Gaps: ${result.summary.totalGaps}`);
|
|
@@ -8341,7 +7459,7 @@ function buildSarifResults(result, basePath) {
|
|
|
8341
7459
|
ruleId: gap.categoryId,
|
|
8342
7460
|
level: sarifLevel(gap.severity),
|
|
8343
7461
|
message: {
|
|
8344
|
-
text: `
|
|
7462
|
+
text: `Potential vulnerability: ${gap.categoryName}`
|
|
8345
7463
|
},
|
|
8346
7464
|
locations: [
|
|
8347
7465
|
{
|
|
@@ -8406,7 +7524,7 @@ function isValidScanOutputFormat(format) {
|
|
|
8406
7524
|
|
|
8407
7525
|
// src/cli/commands/analyze.ts
|
|
8408
7526
|
function registerAnalyzeCommand(program2) {
|
|
8409
|
-
program2.command("analyze [path]").description("
|
|
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) => {
|
|
8410
7528
|
const isQuiet = Boolean(options["quiet"]);
|
|
8411
7529
|
const isVerbose = Boolean(options["verbose"]);
|
|
8412
7530
|
if (isQuiet) {
|
|
@@ -8496,24 +7614,24 @@ function registerAnalyzeCommand(program2) {
|
|
|
8496
7614
|
let provider = "anthropic";
|
|
8497
7615
|
if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
|
|
8498
7616
|
spinner?.stop();
|
|
8499
|
-
console.log(
|
|
8500
|
-
console.log(
|
|
8501
|
-
console.log(
|
|
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"));
|
|
8502
7620
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
8503
7621
|
const askQuestion = (question) => new Promise((resolve9) => rl.question(question, (answer) => resolve9(answer.trim())));
|
|
8504
|
-
const apiKey = await askQuestion(
|
|
7622
|
+
const apiKey = await askQuestion(chalk6.cyan("Enter your Anthropic or OpenAI API key: "));
|
|
8505
7623
|
rl.close();
|
|
8506
7624
|
if (!apiKey) {
|
|
8507
|
-
console.log(
|
|
7625
|
+
console.log(chalk6.red("No API key provided. Skipping AI verification."));
|
|
8508
7626
|
} else {
|
|
8509
7627
|
if (apiKey.startsWith("sk-ant-")) {
|
|
8510
7628
|
setConfigValue2("anthropicApiKey", apiKey);
|
|
8511
7629
|
provider = "anthropic";
|
|
8512
|
-
console.log(
|
|
7630
|
+
console.log(chalk6.green("Anthropic API key saved to ~/.pinata/config.json\n"));
|
|
8513
7631
|
} else {
|
|
8514
7632
|
setConfigValue2("openaiApiKey", apiKey);
|
|
8515
7633
|
provider = "openai";
|
|
8516
|
-
console.log(
|
|
7634
|
+
console.log(chalk6.green("OpenAI API key saved to ~/.pinata/config.json\n"));
|
|
8517
7635
|
}
|
|
8518
7636
|
}
|
|
8519
7637
|
} else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
|
|
@@ -8544,19 +7662,19 @@ function registerAnalyzeCommand(program2) {
|
|
|
8544
7662
|
`AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
|
|
8545
7663
|
);
|
|
8546
7664
|
if (isVerbose && dismissed.length > 0) {
|
|
8547
|
-
console.log(
|
|
7665
|
+
console.log(chalk6.gray("\nDismissed as false positives:"));
|
|
8548
7666
|
for (const { gap, reason } of dismissed.slice(0, 5)) {
|
|
8549
|
-
console.log(
|
|
8550
|
-
console.log(
|
|
7667
|
+
console.log(chalk6.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
|
|
7668
|
+
console.log(chalk6.gray(` Reason: ${reason.slice(0, 100)}...`));
|
|
8551
7669
|
}
|
|
8552
7670
|
if (dismissed.length > 5) {
|
|
8553
|
-
console.log(
|
|
7671
|
+
console.log(chalk6.gray(` ... and ${dismissed.length - 5} more`));
|
|
8554
7672
|
}
|
|
8555
7673
|
}
|
|
8556
7674
|
} catch (error) {
|
|
8557
7675
|
verifySpinner?.fail("AI verification failed (results unverified)");
|
|
8558
7676
|
if (isVerbose) {
|
|
8559
|
-
console.error(
|
|
7677
|
+
console.error(chalk6.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
|
|
8560
7678
|
}
|
|
8561
7679
|
}
|
|
8562
7680
|
}
|
|
@@ -8568,12 +7686,12 @@ function registerAnalyzeCommand(program2) {
|
|
|
8568
7686
|
const { readFile: readFile7 } = await import('fs/promises');
|
|
8569
7687
|
const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
|
|
8570
7688
|
if (testableGaps.length === 0) {
|
|
8571
|
-
console.log(
|
|
7689
|
+
console.log(chalk6.yellow("\nNo dynamically testable gaps found."));
|
|
8572
7690
|
} else {
|
|
8573
7691
|
const runner = createRunner2(void 0, isDryRun);
|
|
8574
7692
|
const initResult = await runner.initialize();
|
|
8575
7693
|
if (!initResult.ready) {
|
|
8576
|
-
console.log(
|
|
7694
|
+
console.log(chalk6.red(`
|
|
8577
7695
|
Dynamic execution unavailable: ${initResult.error}`));
|
|
8578
7696
|
} else {
|
|
8579
7697
|
const fileContents = /* @__PURE__ */ new Map();
|
|
@@ -8596,7 +7714,7 @@ Dynamic execution unavailable: ${initResult.error}`));
|
|
|
8596
7714
|
}
|
|
8597
7715
|
}
|
|
8598
7716
|
if (executionSummary.confirmed > 0) {
|
|
8599
|
-
console.log(
|
|
7717
|
+
console.log(chalk6.red.bold(`
|
|
8600
7718
|
\u26A0\uFE0F ${executionSummary.confirmed} CONFIRMED vulnerabilities found!`));
|
|
8601
7719
|
}
|
|
8602
7720
|
}
|
|
@@ -8636,289 +7754,463 @@ Dynamic execution unavailable: ${initResult.error}`));
|
|
|
8636
7754
|
}
|
|
8637
7755
|
});
|
|
8638
7756
|
}
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
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"]
|
|
8648
7783
|
}
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
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
|
+
}
|
|
8666
7893
|
}
|
|
8667
|
-
lines.push("");
|
|
8668
7894
|
}
|
|
8669
|
-
|
|
8670
|
-
lines.push("");
|
|
8671
|
-
lines.push(chalk7.gray("\u2500".repeat(60)));
|
|
7895
|
+
if (foundOpenBrace) break;
|
|
8672
7896
|
}
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
|
|
8676
|
-
|
|
8677
|
-
|
|
8678
|
-
if (tests.some((t) => t.result.unresolved.length > 0)) {
|
|
8679
|
-
const unresolvedCount = tests.reduce((acc, t) => acc + t.result.unresolved.length, 0);
|
|
8680
|
-
lines.push(chalk7.yellow(` Unresolved placeholders: ${unresolvedCount}`));
|
|
8681
|
-
lines.push(chalk7.gray(" (Some placeholders need manual completion)"));
|
|
8682
|
-
}
|
|
8683
|
-
lines.push("");
|
|
8684
|
-
lines.push(chalk7.gray("This is a dry run. Use --write to save files."));
|
|
8685
|
-
return lines.join("\n");
|
|
8686
|
-
}
|
|
8687
|
-
function formatGeneratedJson(tests) {
|
|
8688
|
-
const output = tests.map((test) => ({
|
|
8689
|
-
gap: {
|
|
8690
|
-
categoryId: test.gap.categoryId,
|
|
8691
|
-
categoryName: test.gap.categoryName,
|
|
8692
|
-
filePath: test.gap.filePath,
|
|
8693
|
-
lineStart: test.gap.lineStart,
|
|
8694
|
-
severity: test.gap.severity,
|
|
8695
|
-
confidence: test.gap.confidence
|
|
8696
|
-
},
|
|
8697
|
-
template: {
|
|
8698
|
-
id: test.template.id,
|
|
8699
|
-
framework: test.template.framework,
|
|
8700
|
-
language: test.template.language
|
|
8701
|
-
},
|
|
8702
|
-
suggestedPath: test.suggestedPath,
|
|
8703
|
-
content: test.result.content,
|
|
8704
|
-
imports: test.result.imports,
|
|
8705
|
-
fixtures: test.result.fixtures,
|
|
8706
|
-
substituted: test.result.substituted,
|
|
8707
|
-
unresolved: test.result.unresolved
|
|
8708
|
-
}));
|
|
8709
|
-
return JSON.stringify(output, null, 2);
|
|
8710
|
-
}
|
|
8711
|
-
function suggestTestPath(sourceFile, template, basePath) {
|
|
8712
|
-
const dir = dirname(sourceFile);
|
|
8713
|
-
const name = basename(sourceFile);
|
|
8714
|
-
const nameWithoutExt = name.replace(/\.[^.]+$/, "");
|
|
8715
|
-
const extMap = {
|
|
8716
|
-
python: ".py",
|
|
8717
|
-
typescript: ".ts",
|
|
8718
|
-
javascript: ".js",
|
|
8719
|
-
go: "_test.go",
|
|
8720
|
-
java: "Test.java",
|
|
8721
|
-
rust: ".rs"
|
|
8722
|
-
};
|
|
8723
|
-
const ext = extMap[template.language] ?? ".test.ts";
|
|
8724
|
-
let testFileName;
|
|
8725
|
-
switch (template.language) {
|
|
8726
|
-
case "python":
|
|
8727
|
-
testFileName = `test_${nameWithoutExt}${ext}`;
|
|
8728
|
-
break;
|
|
8729
|
-
case "go":
|
|
8730
|
-
testFileName = `${nameWithoutExt}${ext}`;
|
|
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;
|
|
8731
7902
|
break;
|
|
8732
|
-
|
|
8733
|
-
|
|
7903
|
+
}
|
|
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;
|
|
8734
7920
|
break;
|
|
8735
|
-
|
|
8736
|
-
testFileName = `${nameWithoutExt}.test${ext}`;
|
|
7921
|
+
}
|
|
8737
7922
|
}
|
|
8738
|
-
const
|
|
8739
|
-
|
|
8740
|
-
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8744
|
-
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8748
|
-
|
|
8749
|
-
|
|
8750
|
-
fileName,
|
|
8751
|
-
fileNameWithoutExt,
|
|
8752
|
-
lineNumber: gap.lineStart,
|
|
8753
|
-
// Category info
|
|
8754
|
-
categoryId: gap.categoryId,
|
|
8755
|
-
categoryName: gap.categoryName,
|
|
8756
|
-
domain: gap.domain,
|
|
8757
|
-
level: gap.level,
|
|
8758
|
-
severity: gap.severity,
|
|
8759
|
-
confidence: gap.confidence,
|
|
8760
|
-
// Code context
|
|
8761
|
-
codeSnippet: gap.codeSnippet,
|
|
8762
|
-
functionName: funcMatch?.[1] ?? "targetFunction",
|
|
8763
|
-
className: classMatch?.[1] ?? "TargetClass",
|
|
8764
|
-
// Common template variables
|
|
8765
|
-
testName: `test_${gap.categoryId.replace(/-/g, "_")}`,
|
|
8766
|
-
testDescription: `Test for ${gap.categoryName} in ${fileName}:${gap.lineStart}`,
|
|
8767
|
-
// Pattern info
|
|
8768
|
-
patternId: gap.patternId,
|
|
8769
|
-
patternType: gap.patternType
|
|
8770
|
-
};
|
|
8771
|
-
}
|
|
8772
|
-
async function extractVariablesWithAI(gap, templateVariables, aiConfig) {
|
|
8773
|
-
const baseVars = extractVariablesFromGap(gap);
|
|
8774
|
-
const result = await suggestVariables(
|
|
8775
|
-
{
|
|
8776
|
-
codeSnippet: gap.codeSnippet,
|
|
8777
|
-
filePath: gap.filePath,
|
|
8778
|
-
variables: templateVariables,
|
|
8779
|
-
gap,
|
|
8780
|
-
existingValues: baseVars
|
|
8781
|
-
},
|
|
8782
|
-
aiConfig
|
|
8783
|
-
);
|
|
8784
|
-
if (result.success && result.data) {
|
|
8785
|
-
return result.data.values;
|
|
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;
|
|
7933
|
+
}
|
|
7934
|
+
endIdx = i;
|
|
8786
7935
|
}
|
|
8787
|
-
|
|
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 };
|
|
8788
7940
|
}
|
|
8789
|
-
|
|
8790
|
-
|
|
8791
|
-
|
|
8792
|
-
|
|
8793
|
-
|
|
8794
|
-
|
|
8795
|
-
|
|
8796
|
-
|
|
8797
|
-
|
|
8798
|
-
|
|
8799
|
-
if (
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
|
|
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);
|
|
7962
|
+
}
|
|
8803
7963
|
}
|
|
8804
|
-
const existing = testsByFile.get(outputPath) ?? [];
|
|
8805
|
-
existing.push(test);
|
|
8806
|
-
testsByFile.set(outputPath, existing);
|
|
8807
7964
|
}
|
|
8808
|
-
|
|
8809
|
-
|
|
8810
|
-
|
|
8811
|
-
|
|
8812
|
-
|
|
8813
|
-
|
|
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)) {
|
|
8814
7971
|
try {
|
|
8815
|
-
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
allImports.add(imp);
|
|
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
|
+
}
|
|
8824
7980
|
}
|
|
7981
|
+
} catch {
|
|
8825
7982
|
}
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
|
|
8829
|
-
|
|
8830
|
-
|
|
8831
|
-
contentParts.push(`// Test for ${test.gap.categoryName}`);
|
|
8832
|
-
contentParts.push(`// Gap: ${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`);
|
|
8833
|
-
contentParts.push(`// Generated by Pinata`);
|
|
8834
|
-
contentParts.push("");
|
|
8835
|
-
contentParts.push(test.result.content);
|
|
8836
|
-
contentParts.push("");
|
|
8837
|
-
}
|
|
8838
|
-
const newContent = contentParts.join("\n");
|
|
8839
|
-
let finalContent;
|
|
8840
|
-
let appended = false;
|
|
8841
|
-
if (fileExists) {
|
|
8842
|
-
finalContent = existingContent.trimEnd() + "\n\n" + newContent;
|
|
8843
|
-
appended = true;
|
|
8844
|
-
} else {
|
|
8845
|
-
finalContent = newContent;
|
|
8846
|
-
}
|
|
8847
|
-
await writeFile(outputPath, finalContent, "utf-8");
|
|
8848
|
-
for (const test of fileTests) {
|
|
8849
|
-
const result = {
|
|
8850
|
-
path: outputPath,
|
|
8851
|
-
created: !fileExists,
|
|
8852
|
-
appended,
|
|
8853
|
-
categoryId: test.gap.categoryId,
|
|
8854
|
-
gapLocation: `${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`
|
|
8855
|
-
};
|
|
8856
|
-
if (fileExists) {
|
|
8857
|
-
summary.updated.push(result);
|
|
8858
|
-
} else {
|
|
8859
|
-
summary.created.push(result);
|
|
7983
|
+
}
|
|
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];
|
|
8860
7988
|
}
|
|
8861
|
-
summary.totalTests++;
|
|
8862
7989
|
}
|
|
8863
|
-
} catch (error) {
|
|
8864
|
-
summary.failed.push({
|
|
8865
|
-
path: outputPath,
|
|
8866
|
-
error: error instanceof Error ? error.message : String(error)
|
|
8867
|
-
});
|
|
8868
7990
|
}
|
|
7991
|
+
return language === "typescript" ? FRAMEWORK_DEFAULTS["vitest"] : FRAMEWORK_DEFAULTS["jest"];
|
|
8869
7992
|
}
|
|
8870
|
-
|
|
7993
|
+
if (language === "python") return FRAMEWORK_DEFAULTS["pytest"];
|
|
7994
|
+
if (language === "go") return FRAMEWORK_DEFAULTS["go-test"];
|
|
7995
|
+
return FRAMEWORK_DEFAULTS["vitest"];
|
|
8871
7996
|
}
|
|
8872
|
-
function
|
|
8873
|
-
const
|
|
8874
|
-
|
|
8875
|
-
|
|
8876
|
-
|
|
8877
|
-
|
|
8878
|
-
|
|
8879
|
-
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
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
|
+
}
|
|
8885
8015
|
}
|
|
8886
8016
|
}
|
|
8887
|
-
|
|
8888
|
-
|
|
8889
|
-
|
|
8890
|
-
|
|
8891
|
-
|
|
8892
|
-
|
|
8893
|
-
|
|
8894
|
-
|
|
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;
|
|
8029
|
+
}
|
|
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 {
|
|
8895
8065
|
}
|
|
8896
8066
|
}
|
|
8897
|
-
|
|
8898
|
-
|
|
8899
|
-
|
|
8900
|
-
|
|
8901
|
-
|
|
8902
|
-
|
|
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}`);
|
|
8088
|
+
}
|
|
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}"`);
|
|
8151
|
+
}
|
|
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
|
|
8171
|
+
};
|
|
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);
|
|
8903
8199
|
}
|
|
8904
8200
|
}
|
|
8905
|
-
|
|
8906
|
-
|
|
8907
|
-
lines.push(chalk7.bold(`Total: ${summary.totalTests} test(s) written to ${(/* @__PURE__ */ new Set([...summary.created.map((r) => r.path), ...summary.updated.map((r) => r.path)])).size} file(s)`));
|
|
8908
|
-
if (summary.failed.length > 0) {
|
|
8909
|
-
lines.push(chalk7.red(`Failures: ${summary.failed.length}`));
|
|
8201
|
+
if (result.endsWith("```")) {
|
|
8202
|
+
result = result.slice(0, -3).trimEnd();
|
|
8910
8203
|
}
|
|
8911
|
-
return
|
|
8204
|
+
return result;
|
|
8912
8205
|
}
|
|
8913
8206
|
|
|
8914
8207
|
// src/cli/commands/generate.ts
|
|
8915
|
-
init_results_cache();
|
|
8916
8208
|
function registerGenerateCommand(program2) {
|
|
8917
|
-
program2.command("generate").description("Generate tests for
|
|
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) => {
|
|
8918
8210
|
const isQuiet = Boolean(options["quiet"]);
|
|
8919
8211
|
const isVerbose = Boolean(options["verbose"]);
|
|
8920
|
-
const
|
|
8921
|
-
const
|
|
8212
|
+
const shouldWrite = Boolean(options["write"]);
|
|
8213
|
+
const withProperty = Boolean(options["property"]);
|
|
8922
8214
|
const aiProvider = String(options["aiProvider"] ?? "anthropic");
|
|
8923
8215
|
const outputFormat = String(options["output"] ?? "terminal");
|
|
8924
8216
|
if (isQuiet) {
|
|
@@ -8938,32 +8230,28 @@ function registerGenerateCommand(program2) {
|
|
|
8938
8230
|
process.exit(1);
|
|
8939
8231
|
}
|
|
8940
8232
|
if (domainFilter && !RISK_DOMAINS.includes(domainFilter)) {
|
|
8941
|
-
console.error(formatError(new Error(`Invalid domain: ${domainFilter}. Valid
|
|
8233
|
+
console.error(formatError(new Error(`Invalid domain: ${domainFilter}. Valid: ${RISK_DOMAINS.join(", ")}`)));
|
|
8942
8234
|
process.exit(1);
|
|
8943
8235
|
}
|
|
8944
8236
|
const validSeverities = ["critical", "high", "medium", "low"];
|
|
8945
8237
|
const minSeverity = String(options["severity"] ?? "medium");
|
|
8946
8238
|
if (!validSeverities.includes(minSeverity)) {
|
|
8947
|
-
console.error(formatError(new Error(`Invalid severity: ${minSeverity}
|
|
8239
|
+
console.error(formatError(new Error(`Invalid severity: ${minSeverity}`)));
|
|
8948
8240
|
process.exit(1);
|
|
8949
8241
|
}
|
|
8950
8242
|
const severityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
8951
8243
|
const showSpinner = outputFormat === "terminal" && !isQuiet;
|
|
8952
|
-
const spinner = showSpinner ? ora3("Loading
|
|
8244
|
+
const spinner = showSpinner ? ora3("Loading scan results...").start() : null;
|
|
8953
8245
|
try {
|
|
8954
8246
|
const projectRoot = process.cwd();
|
|
8955
8247
|
const cacheResult = await loadScanResults(projectRoot);
|
|
8956
8248
|
if (!cacheResult.success) {
|
|
8957
8249
|
spinner?.fail("No cached results");
|
|
8958
8250
|
console.error(formatError(cacheResult.error));
|
|
8959
|
-
console.error(
|
|
8251
|
+
console.error(chalk6.yellow("\nRun `pinata analyze` first."));
|
|
8960
8252
|
process.exit(1);
|
|
8961
8253
|
}
|
|
8962
|
-
|
|
8963
|
-
let gaps = cached.gaps;
|
|
8964
|
-
if (spinner) {
|
|
8965
|
-
spinner.text = `Loaded ${gaps.length} gaps from cache. Filtering...`;
|
|
8966
|
-
}
|
|
8254
|
+
let gaps = cacheResult.data.gaps;
|
|
8967
8255
|
if (categoryId) {
|
|
8968
8256
|
gaps = gaps.filter((g) => g.categoryId === categoryId);
|
|
8969
8257
|
}
|
|
@@ -8971,90 +8259,111 @@ function registerGenerateCommand(program2) {
|
|
|
8971
8259
|
gaps = gaps.filter((g) => g.domain === domainFilter);
|
|
8972
8260
|
}
|
|
8973
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
|
+
});
|
|
8974
8269
|
if (gaps.length === 0) {
|
|
8975
|
-
spinner?.succeed("No gaps match
|
|
8976
|
-
console.log(
|
|
8270
|
+
spinner?.succeed("No gaps match filters");
|
|
8271
|
+
console.log(chalk6.yellow("\nNo gaps to generate tests for."));
|
|
8977
8272
|
process.exit(0);
|
|
8978
8273
|
}
|
|
8979
8274
|
if (spinner) {
|
|
8980
|
-
spinner.text = `
|
|
8275
|
+
spinner.text = `Extracting context for ${gaps.length} findings...`;
|
|
8981
8276
|
}
|
|
8982
|
-
const
|
|
8983
|
-
|
|
8984
|
-
|
|
8985
|
-
if (!loadResult.success) {
|
|
8986
|
-
spinner?.fail("Failed to load categories");
|
|
8987
|
-
console.error(formatError(loadResult.error));
|
|
8277
|
+
const contexts = await extractTestContexts(gaps, projectRoot);
|
|
8278
|
+
if (contexts.length === 0) {
|
|
8279
|
+
spinner?.fail("Failed to extract context from any finding");
|
|
8988
8280
|
process.exit(1);
|
|
8989
8281
|
}
|
|
8990
8282
|
if (spinner) {
|
|
8991
|
-
spinner.text = `Generating tests for ${
|
|
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);
|
|
8992
8292
|
}
|
|
8993
|
-
const
|
|
8994
|
-
const
|
|
8293
|
+
const apiKey = getApiKey2(aiProvider) ?? "";
|
|
8294
|
+
const callAI = buildAICaller(aiProvider, apiKey);
|
|
8295
|
+
const generated = [];
|
|
8995
8296
|
const errors = [];
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
gapsByCategory.set(gap.categoryId, existing);
|
|
9001
|
-
}
|
|
9002
|
-
for (const [catId, categoryGaps] of gapsByCategory) {
|
|
9003
|
-
const categoryResult = store.get(catId);
|
|
9004
|
-
if (!categoryResult.success) {
|
|
9005
|
-
errors.push(`Category not found: ${catId}`);
|
|
9006
|
-
continue;
|
|
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)}`;
|
|
9007
8301
|
}
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
|
|
9013
|
-
|
|
9014
|
-
|
|
9015
|
-
|
|
9016
|
-
|
|
9017
|
-
|
|
9018
|
-
errors.push(`No templates available for ${catId}`);
|
|
9019
|
-
continue;
|
|
9020
|
-
}
|
|
9021
|
-
let variables;
|
|
9022
|
-
if (useAI) {
|
|
9023
|
-
variables = await extractVariablesWithAI(gap, template.variables, { provider: aiProvider });
|
|
9024
|
-
} else {
|
|
9025
|
-
variables = extractVariablesFromGap(gap);
|
|
9026
|
-
}
|
|
9027
|
-
const renderResult = renderer.renderTemplate(template, variables);
|
|
9028
|
-
if (!renderResult.success) {
|
|
9029
|
-
errors.push(`Failed to render ${catId}: ${renderResult.error.message}`);
|
|
9030
|
-
continue;
|
|
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
|
+
}
|
|
9031
8312
|
}
|
|
9032
|
-
|
|
9033
|
-
|
|
8313
|
+
} catch (err2) {
|
|
8314
|
+
errors.push(`Failed ${ctx.gap.categoryId}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
9034
8315
|
}
|
|
9035
8316
|
}
|
|
9036
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}`));
|
|
8322
|
+
}
|
|
8323
|
+
process.exit(1);
|
|
8324
|
+
}
|
|
9037
8325
|
if (outputFormat === "json") {
|
|
9038
|
-
console.log(
|
|
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));
|
|
9039
8336
|
} else {
|
|
9040
|
-
console.log(
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
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}`));
|
|
9046
8345
|
}
|
|
8346
|
+
console.log();
|
|
9047
8347
|
}
|
|
9048
|
-
if (
|
|
9049
|
-
|
|
9050
|
-
const
|
|
9051
|
-
|
|
9052
|
-
|
|
9053
|
-
|
|
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
|
+
}
|
|
9054
8358
|
}
|
|
9055
|
-
console.log(
|
|
9056
|
-
|
|
9057
|
-
|
|
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."));
|
|
8362
|
+
}
|
|
8363
|
+
if (errors.length > 0 && isVerbose) {
|
|
8364
|
+
console.error(chalk6.yellow("\nWarnings:"));
|
|
8365
|
+
for (const error of errors) {
|
|
8366
|
+
console.error(chalk6.gray(` ${error}`));
|
|
9058
8367
|
}
|
|
9059
8368
|
}
|
|
9060
8369
|
process.exit(0);
|
|
@@ -9065,10 +8374,57 @@ function registerGenerateCommand(program2) {
|
|
|
9065
8374
|
}
|
|
9066
8375
|
});
|
|
9067
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}`);
|
|
8397
|
+
}
|
|
8398
|
+
const data2 = await response2.json();
|
|
8399
|
+
return data2.content[0]?.text ?? "";
|
|
8400
|
+
}
|
|
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
|
+
}
|
|
9068
8424
|
|
|
9069
8425
|
// src/cli/index.ts
|
|
9070
8426
|
var program = new Command();
|
|
9071
|
-
program.name("pinata").description("AI-powered
|
|
8427
|
+
program.name("pinata").description("AI-powered security vulnerability detection").version(VERSION);
|
|
9072
8428
|
registerAnalyzeCommand(program);
|
|
9073
8429
|
registerGenerateCommand(program);
|
|
9074
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) => {
|
|
@@ -9093,7 +8449,7 @@ program.command("explain").description("Get natural language explanations for de
|
|
|
9093
8449
|
const cacheResult = await loadScanResults(projectRoot);
|
|
9094
8450
|
if (!cacheResult.success) {
|
|
9095
8451
|
console.error(formatError(cacheResult.error));
|
|
9096
|
-
console.error(
|
|
8452
|
+
console.error(chalk6.yellow("\nRun `pinata analyze` first to scan for gaps."));
|
|
9097
8453
|
process.exit(1);
|
|
9098
8454
|
}
|
|
9099
8455
|
const cached = cacheResult.data;
|
|
@@ -9110,7 +8466,7 @@ program.command("explain").description("Get natural language explanations for de
|
|
|
9110
8466
|
}
|
|
9111
8467
|
gaps = gaps.slice(0, topN);
|
|
9112
8468
|
if (gaps.length === 0) {
|
|
9113
|
-
console.log(
|
|
8469
|
+
console.log(chalk6.yellow("No gaps to explain."));
|
|
9114
8470
|
process.exit(0);
|
|
9115
8471
|
}
|
|
9116
8472
|
let explanations;
|
|
@@ -9137,7 +8493,7 @@ program.command("explain").description("Get natural language explanations for de
|
|
|
9137
8493
|
spinner.succeed(`Generated ${explanations.length} explanations`);
|
|
9138
8494
|
} catch (error) {
|
|
9139
8495
|
spinner.fail("AI explanation failed");
|
|
9140
|
-
console.error(
|
|
8496
|
+
console.error(chalk6.yellow(`
|
|
9141
8497
|
Set ${aiProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} for AI explanations.
|
|
9142
8498
|
`));
|
|
9143
8499
|
explanations = gaps.map((g) => generateFallbackExplanation(g));
|
|
@@ -9170,16 +8526,16 @@ Set ${aiProvider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY"} for A
|
|
|
9170
8526
|
for (let i = 0; i < explanations.length; i++) {
|
|
9171
8527
|
const exp = explanations[i];
|
|
9172
8528
|
const gap = gaps[i];
|
|
9173
|
-
const severityColor = gap.severity === "critical" ?
|
|
9174
|
-
console.log(`${severityColor.bold(`[${gap.severity.toUpperCase()}]`)} ${
|
|
9175
|
-
console.log(
|
|
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}`));
|
|
9176
8532
|
console.log();
|
|
9177
8533
|
for (const line of exp.explanation.split("\n")) {
|
|
9178
8534
|
console.log(` ${line}`);
|
|
9179
8535
|
}
|
|
9180
8536
|
if (exp.remediation) {
|
|
9181
8537
|
console.log();
|
|
9182
|
-
console.log(
|
|
8538
|
+
console.log(chalk6.green(` Fix: ${exp.remediation}`));
|
|
9183
8539
|
}
|
|
9184
8540
|
console.log();
|
|
9185
8541
|
}
|
|
@@ -9223,7 +8579,7 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
|
|
|
9223
8579
|
);
|
|
9224
8580
|
if (!result.success || !result.data) {
|
|
9225
8581
|
spinner.fail("Pattern suggestion failed");
|
|
9226
|
-
console.error(
|
|
8582
|
+
console.error(chalk6.red(result.error ?? "Unknown error"));
|
|
9227
8583
|
process.exit(1);
|
|
9228
8584
|
}
|
|
9229
8585
|
const suggestions = result.data.suggestions;
|
|
@@ -9243,15 +8599,15 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
|
|
|
9243
8599
|
}
|
|
9244
8600
|
} else {
|
|
9245
8601
|
console.log();
|
|
9246
|
-
console.log(
|
|
8602
|
+
console.log(chalk6.bold(`Suggested Patterns for ${categoryId}`));
|
|
9247
8603
|
console.log();
|
|
9248
8604
|
for (const s of suggestions) {
|
|
9249
|
-
const confColor = s.confidence === "high" ?
|
|
9250
|
-
console.log(` ${
|
|
9251
|
-
console.log(` Pattern: ${
|
|
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)}`);
|
|
9252
8608
|
console.log(` ${s.description}`);
|
|
9253
|
-
console.log(` Match: ${
|
|
9254
|
-
console.log(` Safe: ${
|
|
8609
|
+
console.log(` Match: ${chalk6.gray(s.matchExample.slice(0, 80))}`);
|
|
8610
|
+
console.log(` Safe: ${chalk6.gray(s.safeExample.slice(0, 80))}`);
|
|
9255
8611
|
console.log();
|
|
9256
8612
|
}
|
|
9257
8613
|
}
|
|
@@ -9333,18 +8689,18 @@ ${cat.description}
|
|
|
9333
8689
|
}
|
|
9334
8690
|
} else {
|
|
9335
8691
|
console.log();
|
|
9336
|
-
console.log(
|
|
9337
|
-
console.log(
|
|
8692
|
+
console.log(chalk6.bold(`Search Results for "${query}"`));
|
|
8693
|
+
console.log(chalk6.gray(`Found ${results.length} matching categories.`));
|
|
9338
8694
|
console.log();
|
|
9339
8695
|
if (results.length === 0) {
|
|
9340
|
-
console.log(
|
|
8696
|
+
console.log(chalk6.yellow("No categories match your search."));
|
|
9341
8697
|
} else {
|
|
9342
8698
|
for (const cat of results) {
|
|
9343
|
-
const domainColor = cat.domain === "security" ?
|
|
9344
|
-
console.log(` ${
|
|
8699
|
+
const domainColor = cat.domain === "security" ? chalk6.red : chalk6.blue;
|
|
8700
|
+
console.log(` ${chalk6.cyan(cat.id)} - ${chalk6.bold(cat.name)}`);
|
|
9345
8701
|
console.log(` ${domainColor(cat.domain)} | ${cat.level} | ${cat.priority}`);
|
|
9346
8702
|
if (options["verbose"]) {
|
|
9347
|
-
console.log(` ${
|
|
8703
|
+
console.log(` ${chalk6.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
|
|
9348
8704
|
}
|
|
9349
8705
|
console.log();
|
|
9350
8706
|
}
|
|
@@ -9412,8 +8768,8 @@ program.command("init").description("Initialize Pinata configuration in project"
|
|
|
9412
8768
|
const configPath = resolve(process.cwd(), ".pinata.yml");
|
|
9413
8769
|
const cacheDir = resolve(process.cwd(), ".pinata");
|
|
9414
8770
|
if (existsSync(configPath) && !options["force"]) {
|
|
9415
|
-
console.log(
|
|
9416
|
-
console.log(
|
|
8771
|
+
console.log(chalk6.yellow("Configuration file already exists at .pinata.yml"));
|
|
8772
|
+
console.log(chalk6.gray("Use --force to overwrite."));
|
|
9417
8773
|
process.exit(0);
|
|
9418
8774
|
}
|
|
9419
8775
|
const defaultConfig = `# Pinata Configuration
|
|
@@ -9456,23 +8812,23 @@ thresholds:
|
|
|
9456
8812
|
const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
|
|
9457
8813
|
try {
|
|
9458
8814
|
await writeFileAsync(configPath, defaultConfig, "utf8");
|
|
9459
|
-
console.log(
|
|
8815
|
+
console.log(chalk6.green("Created .pinata.yml"));
|
|
9460
8816
|
await mkdir4(cacheDir, { recursive: true });
|
|
9461
|
-
console.log(
|
|
8817
|
+
console.log(chalk6.green("Created .pinata/ directory"));
|
|
9462
8818
|
const gitignorePath = resolve(process.cwd(), ".gitignore");
|
|
9463
8819
|
if (existsSync(gitignorePath)) {
|
|
9464
8820
|
const { readFile: readFile7, appendFile } = await import('fs/promises');
|
|
9465
8821
|
const gitignore = await readFile7(gitignorePath, "utf8");
|
|
9466
8822
|
if (!gitignore.includes(".pinata/")) {
|
|
9467
8823
|
await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
|
|
9468
|
-
console.log(
|
|
8824
|
+
console.log(chalk6.green("Added .pinata/ to .gitignore"));
|
|
9469
8825
|
}
|
|
9470
8826
|
}
|
|
9471
8827
|
console.log();
|
|
9472
|
-
console.log(
|
|
9473
|
-
console.log(
|
|
9474
|
-
console.log(
|
|
9475
|
-
console.log(
|
|
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"));
|
|
9476
8832
|
} catch (error) {
|
|
9477
8833
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9478
8834
|
process.exit(1);
|
|
@@ -9485,15 +8841,15 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9485
8841
|
const checkAge = Boolean(options["checkAge"]);
|
|
9486
8842
|
const strictMode = Boolean(options["strict"]);
|
|
9487
8843
|
const doAllChecks = !checkRegistry && !checkDownloads && !checkAge;
|
|
9488
|
-
console.log(
|
|
8844
|
+
console.log(chalk6.bold("\nPinata Dependency Audit\n"));
|
|
9489
8845
|
if (!existsSync(packagePath)) {
|
|
9490
|
-
console.error(
|
|
8846
|
+
console.error(chalk6.red(`Error: ${packagePath} not found`));
|
|
9491
8847
|
process.exit(1);
|
|
9492
8848
|
}
|
|
9493
8849
|
const packageJson = JSON.parse(readFileSync(packagePath, "utf-8"));
|
|
9494
8850
|
const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
9495
8851
|
const packages = Object.keys(allDeps);
|
|
9496
|
-
console.log(
|
|
8852
|
+
console.log(chalk6.gray(`Found ${packages.length} dependencies
|
|
9497
8853
|
`));
|
|
9498
8854
|
const issues = [];
|
|
9499
8855
|
const KNOWN_MALWARE = /* @__PURE__ */ new Set([
|
|
@@ -9552,24 +8908,24 @@ program.command("audit-deps").description("Audit npm dependencies for supply cha
|
|
|
9552
8908
|
const criticals = issues.filter((i) => i.severity === "critical");
|
|
9553
8909
|
const warnings = issues.filter((i) => i.severity === "warning");
|
|
9554
8910
|
if (criticals.length > 0) {
|
|
9555
|
-
console.log(
|
|
8911
|
+
console.log(chalk6.red.bold(`
|
|
9556
8912
|
Critical Issues (${criticals.length}):`));
|
|
9557
8913
|
for (const issue of criticals) {
|
|
9558
|
-
console.log(
|
|
8914
|
+
console.log(chalk6.red(` \u2717 ${issue.pkg}: ${issue.message}`));
|
|
9559
8915
|
}
|
|
9560
8916
|
}
|
|
9561
8917
|
if (warnings.length > 0) {
|
|
9562
|
-
console.log(
|
|
8918
|
+
console.log(chalk6.yellow.bold(`
|
|
9563
8919
|
Warnings (${warnings.length}):`));
|
|
9564
8920
|
for (const issue of warnings.slice(0, 20)) {
|
|
9565
|
-
console.log(
|
|
8921
|
+
console.log(chalk6.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
|
|
9566
8922
|
}
|
|
9567
8923
|
if (warnings.length > 20) {
|
|
9568
|
-
console.log(
|
|
8924
|
+
console.log(chalk6.gray(` ... and ${warnings.length - 20} more`));
|
|
9569
8925
|
}
|
|
9570
8926
|
}
|
|
9571
8927
|
if (issues.length === 0) {
|
|
9572
|
-
console.log(
|
|
8928
|
+
console.log(chalk6.green("\u2713 No dependency issues found"));
|
|
9573
8929
|
}
|
|
9574
8930
|
console.log();
|
|
9575
8931
|
if (criticals.length > 0 || strictMode && warnings.length > 0) {
|
|
@@ -9582,7 +8938,7 @@ program.command("feedback").description("View pattern performance feedback (Laye
|
|
|
9582
8938
|
const shouldReset = Boolean(options["reset"]);
|
|
9583
8939
|
if (shouldReset) {
|
|
9584
8940
|
await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
|
|
9585
|
-
console.log(
|
|
8941
|
+
console.log(chalk6.green("Feedback data reset."));
|
|
9586
8942
|
return;
|
|
9587
8943
|
}
|
|
9588
8944
|
const state = await loadFeedback2();
|
|
@@ -9594,20 +8950,20 @@ program.command("feedback").description("View pattern performance feedback (Laye
|
|
|
9594
8950
|
console.log(generateReport2(state));
|
|
9595
8951
|
return;
|
|
9596
8952
|
}
|
|
9597
|
-
console.log(
|
|
8953
|
+
console.log(chalk6.bold("\nPinata Feedback Report\n"));
|
|
9598
8954
|
console.log(`Total scans: ${state.totalScans}`);
|
|
9599
8955
|
console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
|
|
9600
8956
|
if (state.totalScans === 0) {
|
|
9601
|
-
console.log(
|
|
8957
|
+
console.log(chalk6.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
|
|
9602
8958
|
return;
|
|
9603
8959
|
}
|
|
9604
8960
|
const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
|
|
9605
8961
|
if (patterns.length > 0) {
|
|
9606
|
-
console.log(
|
|
8962
|
+
console.log(chalk6.bold("\nPattern Performance:"));
|
|
9607
8963
|
for (const p of patterns.slice(0, 15)) {
|
|
9608
8964
|
const total = p.confirmedCount + p.unconfirmedCount;
|
|
9609
8965
|
const precisionPct = (p.precision * 100).toFixed(0);
|
|
9610
|
-
const color = p.precision >= 0.7 ?
|
|
8966
|
+
const color = p.precision >= 0.7 ? chalk6.green : p.precision >= 0.4 ? chalk6.yellow : chalk6.red;
|
|
9611
8967
|
console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
|
|
9612
8968
|
}
|
|
9613
8969
|
}
|
|
@@ -9629,74 +8985,74 @@ Examples:
|
|
|
9629
8985
|
case "anthropic-api-key": {
|
|
9630
8986
|
const validation = validateApiKey2("anthropic", value);
|
|
9631
8987
|
if (!validation.valid) {
|
|
9632
|
-
console.log(
|
|
8988
|
+
console.log(chalk6.red(`Invalid API key: ${validation.error}`));
|
|
9633
8989
|
process.exit(1);
|
|
9634
8990
|
}
|
|
9635
8991
|
setConfigValue2("anthropicApiKey", value);
|
|
9636
|
-
console.log(
|
|
8992
|
+
console.log(chalk6.green(`Anthropic API key set: ${maskApiKey2(value)}`));
|
|
9637
8993
|
break;
|
|
9638
8994
|
}
|
|
9639
8995
|
case "openai-api-key": {
|
|
9640
8996
|
const validation = validateApiKey2("openai", value);
|
|
9641
8997
|
if (!validation.valid) {
|
|
9642
|
-
console.log(
|
|
8998
|
+
console.log(chalk6.red(`Invalid API key: ${validation.error}`));
|
|
9643
8999
|
process.exit(1);
|
|
9644
9000
|
}
|
|
9645
9001
|
setConfigValue2("openaiApiKey", value);
|
|
9646
|
-
console.log(
|
|
9002
|
+
console.log(chalk6.green(`OpenAI API key set: ${maskApiKey2(value)}`));
|
|
9647
9003
|
break;
|
|
9648
9004
|
}
|
|
9649
9005
|
case "default-provider": {
|
|
9650
9006
|
if (value !== "anthropic" && value !== "openai") {
|
|
9651
|
-
console.log(
|
|
9007
|
+
console.log(chalk6.red("Provider must be 'anthropic' or 'openai'"));
|
|
9652
9008
|
process.exit(1);
|
|
9653
9009
|
}
|
|
9654
9010
|
setConfigValue2("defaultProvider", value);
|
|
9655
|
-
console.log(
|
|
9011
|
+
console.log(chalk6.green(`Default provider set to: ${value}`));
|
|
9656
9012
|
break;
|
|
9657
9013
|
}
|
|
9658
9014
|
default:
|
|
9659
|
-
console.log(
|
|
9660
|
-
console.log(
|
|
9015
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
9016
|
+
console.log(chalk6.gray("Run 'pinata config set --help' for available keys"));
|
|
9661
9017
|
process.exit(1);
|
|
9662
9018
|
}
|
|
9663
|
-
console.log(
|
|
9019
|
+
console.log(chalk6.gray(`Config stored at: ${getConfigPath2()}`));
|
|
9664
9020
|
});
|
|
9665
9021
|
config.command("get <key>").description("Get a configuration value").action(async (key) => {
|
|
9666
9022
|
const { loadConfig: loadConfig2, maskApiKey: maskApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9667
9023
|
const cfg = loadConfig2();
|
|
9668
9024
|
switch (key) {
|
|
9669
9025
|
case "anthropic-api-key":
|
|
9670
|
-
console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) :
|
|
9026
|
+
console.log(cfg.anthropicApiKey ? maskApiKey2(cfg.anthropicApiKey) : chalk6.gray("(not set)"));
|
|
9671
9027
|
break;
|
|
9672
9028
|
case "openai-api-key":
|
|
9673
|
-
console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) :
|
|
9029
|
+
console.log(cfg.openaiApiKey ? maskApiKey2(cfg.openaiApiKey) : chalk6.gray("(not set)"));
|
|
9674
9030
|
break;
|
|
9675
9031
|
case "default-provider":
|
|
9676
|
-
console.log(cfg.defaultProvider ??
|
|
9032
|
+
console.log(cfg.defaultProvider ?? chalk6.gray("anthropic (default)"));
|
|
9677
9033
|
break;
|
|
9678
9034
|
default:
|
|
9679
|
-
console.log(
|
|
9035
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
9680
9036
|
process.exit(1);
|
|
9681
9037
|
}
|
|
9682
9038
|
});
|
|
9683
9039
|
config.command("list").description("List all configuration values").action(async () => {
|
|
9684
9040
|
const { loadConfig: loadConfig2, maskApiKey: maskApiKey2, getConfigPath: getConfigPath2, hasApiKey: hasApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
9685
9041
|
const cfg = loadConfig2();
|
|
9686
|
-
console.log(
|
|
9687
|
-
console.log(
|
|
9042
|
+
console.log(chalk6.bold("Pinata Configuration"));
|
|
9043
|
+
console.log(chalk6.gray(`Config file: ${getConfigPath2()}`));
|
|
9688
9044
|
console.log();
|
|
9689
9045
|
console.log("AI Providers:");
|
|
9690
|
-
const anthropicStatus = hasApiKey2("anthropic") ?
|
|
9691
|
-
const openaiStatus = hasApiKey2("openai") ?
|
|
9692
|
-
console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ?
|
|
9693
|
-
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)})`) : ""}`);
|
|
9694
9050
|
console.log(` Default provider: ${cfg.defaultProvider ?? "anthropic"}`);
|
|
9695
9051
|
console.log();
|
|
9696
9052
|
if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
|
|
9697
|
-
console.log(
|
|
9698
|
-
console.log(
|
|
9699
|
-
console.log(
|
|
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"));
|
|
9700
9056
|
}
|
|
9701
9057
|
});
|
|
9702
9058
|
config.command("unset <key>").description("Remove a configuration value").action(async (key) => {
|
|
@@ -9704,18 +9060,18 @@ config.command("unset <key>").description("Remove a configuration value").action
|
|
|
9704
9060
|
switch (key) {
|
|
9705
9061
|
case "anthropic-api-key":
|
|
9706
9062
|
deleteConfigValue2("anthropicApiKey");
|
|
9707
|
-
console.log(
|
|
9063
|
+
console.log(chalk6.green("Anthropic API key removed"));
|
|
9708
9064
|
break;
|
|
9709
9065
|
case "openai-api-key":
|
|
9710
9066
|
deleteConfigValue2("openaiApiKey");
|
|
9711
|
-
console.log(
|
|
9067
|
+
console.log(chalk6.green("OpenAI API key removed"));
|
|
9712
9068
|
break;
|
|
9713
9069
|
case "default-provider":
|
|
9714
9070
|
deleteConfigValue2("defaultProvider");
|
|
9715
|
-
console.log(
|
|
9071
|
+
console.log(chalk6.green("Default provider reset to: anthropic"));
|
|
9716
9072
|
break;
|
|
9717
9073
|
default:
|
|
9718
|
-
console.log(
|
|
9074
|
+
console.log(chalk6.red(`Unknown config key: ${key}`));
|
|
9719
9075
|
process.exit(1);
|
|
9720
9076
|
}
|
|
9721
9077
|
});
|
|
@@ -9723,13 +9079,13 @@ var auth = program.command("auth").description("Manage API key authentication");
|
|
|
9723
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) => {
|
|
9724
9080
|
const apiKey = options["key"] ?? process.env["PINATA_API_KEY"];
|
|
9725
9081
|
if (!apiKey) {
|
|
9726
|
-
console.log(
|
|
9727
|
-
console.log(
|
|
9728
|
-
console.log(
|
|
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"));
|
|
9729
9085
|
process.exit(1);
|
|
9730
9086
|
}
|
|
9731
9087
|
if (apiKey.length < 20 || !apiKey.startsWith("pk_")) {
|
|
9732
|
-
console.log(
|
|
9088
|
+
console.log(chalk6.red("Invalid API key format. Keys should start with 'pk_'."));
|
|
9733
9089
|
process.exit(1);
|
|
9734
9090
|
}
|
|
9735
9091
|
const configDir = resolve(process.cwd(), ".pinata");
|
|
@@ -9742,9 +9098,9 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
|
|
|
9742
9098
|
const envPath = resolve(configDir, ".env");
|
|
9743
9099
|
await writeFileAsync(envPath, `PINATA_API_KEY=${apiKey}
|
|
9744
9100
|
`, { mode: 384 });
|
|
9745
|
-
console.log(
|
|
9746
|
-
console.log(
|
|
9747
|
-
console.log(
|
|
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"));
|
|
9748
9104
|
} catch (error) {
|
|
9749
9105
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9750
9106
|
process.exit(1);
|
|
@@ -9765,7 +9121,7 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
9765
9121
|
await rm2(envPath);
|
|
9766
9122
|
removed = true;
|
|
9767
9123
|
}
|
|
9768
|
-
console.log(removed ?
|
|
9124
|
+
console.log(removed ? chalk6.green("API key removed successfully.") : chalk6.yellow("No stored API key found."));
|
|
9769
9125
|
} catch (error) {
|
|
9770
9126
|
console.error(formatError(error instanceof Error ? error : new Error(String(error))));
|
|
9771
9127
|
process.exit(1);
|
|
@@ -9774,19 +9130,19 @@ auth.command("logout").description("Remove stored API key").action(async () => {
|
|
|
9774
9130
|
auth.command("status").description("Check authentication status").action(async () => {
|
|
9775
9131
|
const authPath = resolve(process.cwd(), ".pinata", "auth.json");
|
|
9776
9132
|
if (!existsSync(authPath)) {
|
|
9777
|
-
console.log(
|
|
9778
|
-
console.log(
|
|
9133
|
+
console.log(chalk6.yellow("Not authenticated."));
|
|
9134
|
+
console.log(chalk6.gray("Run: pinata auth login --key <your-api-key>"));
|
|
9779
9135
|
process.exit(0);
|
|
9780
9136
|
}
|
|
9781
9137
|
try {
|
|
9782
9138
|
const { readFile: readFile7 } = await import('fs/promises');
|
|
9783
9139
|
const authData = JSON.parse(await readFile7(authPath, "utf8"));
|
|
9784
|
-
console.log(
|
|
9785
|
-
console.log(
|
|
9786
|
-
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"}`));
|
|
9787
9143
|
} catch {
|
|
9788
|
-
console.log(
|
|
9789
|
-
console.log(
|
|
9144
|
+
console.log(chalk6.yellow("Authentication status unknown."));
|
|
9145
|
+
console.log(chalk6.gray("Run: pinata auth login to reconfigure."));
|
|
9790
9146
|
}
|
|
9791
9147
|
});
|
|
9792
9148
|
program.parse();
|