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/dist/cli/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import fs, { readFile, mkdir, writeFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
3
- import path, { dirname, resolve, join, basename, relative, extname } from 'path';
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 chalk7 from 'chalk';
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", (err3) => {
1488
+ proc.on("error", (err2) => {
1489
1489
  clearTimeout(timer);
1490
1490
  resolve9({
1491
1491
  stdout,
1492
- stderr: stderr + "\n" + err3.message,
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(chalk7.gray(this.format(message)), ...args);
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(chalk7.yellow(this.format(message)), ...args);
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(chalk7.red(this.format(message)), ...args);
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(chalk7.green(this.format(message)), ...args);
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.7,
5702
- low: 0.4
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 test coverage gaps
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
- const basePenalty = 2;
5924
- const penalty = basePenalty * severityWeight * confidenceWeight * Math.sqrt(priorityWeight) * projectTypeWeight;
5925
- if (projectTypeWeight === 0) {
5926
- continue;
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(gap.domain) ?? 100;
5930
- domainScores.set(gap.domain, Math.max(0, currentDomainScore - penalty * 2));
5931
- if (penalty >= 5) {
5932
- const weightNote = projectTypeWeight !== 1 ? ` [${projectType} weight: ${projectTypeWeight}x]` : "";
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: `${gap.severity} ${gap.domain} gap: ${gap.categoryName}${weightNote}`,
6003
+ reason: `${representative.severity} ${representative.domain}: ${representative.categoryName} (${count} findings)${weightNote}`,
5935
6004
  points: Math.round(penalty),
5936
- categoryId: gap.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: chalk7.red.bold,
7082
- high: chalk7.red,
7083
- medium: chalk7.yellow,
7084
- low: chalk7.gray
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: chalk7.red.bold,
7088
- P1: chalk7.yellow,
7089
- P2: chalk7.gray
6432
+ P0: chalk6.red.bold,
6433
+ P1: chalk6.yellow,
6434
+ P2: chalk6.gray
7090
6435
  };
7091
6436
  var DOMAIN_COLORS = {
7092
- security: chalk7.red,
7093
- data: chalk7.blue,
7094
- concurrency: chalk7.magenta,
7095
- input: chalk7.cyan,
7096
- resource: chalk7.yellow,
7097
- reliability: chalk7.green,
7098
- performance: chalk7.yellowBright,
7099
- platform: chalk7.gray,
7100
- business: chalk7.white,
7101
- compliance: chalk7.blueBright
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 chalk7.yellow("No categories found matching the filters.");
6450
+ return chalk6.yellow("No categories found matching the filters.");
7106
6451
  }
7107
6452
  const lines = [];
7108
- lines.push(chalk7.bold.underline(`Found ${categories.length} categories:
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] ?? chalk7.white;
6464
+ const domainColor = DOMAIN_COLORS[domain] ?? chalk6.white;
7120
6465
  lines.push(domainColor.bold(`
7121
6466
  ${domain.toUpperCase()} (${domainCategories.length})`));
7122
- lines.push(chalk7.gray("\u2500".repeat(40)));
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 = chalk7.cyan(`${cat.level}`);
7129
- const name = chalk7.white.bold(cat.name);
7130
- const id = chalk7.gray(`(${cat.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(chalk7.gray(` ${desc}`));
6479
+ lines.push(chalk6.gray(` ${desc}`));
7135
6480
  lines.push("");
7136
6481
  }
7137
6482
  }
7138
- lines.push(chalk7.gray("\u2500".repeat(40)));
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(chalk7.gray("|"));
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 chalk7.red(`Error: ${error.message}`);
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: existsSync6, readFileSync: readFileSync3 } = __require("fs");
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 (!existsSync6(configPath)) {
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 { suggestions, unfilled, values };
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 toPascalCase(str) {
7856
- return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
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 SYSTEM_PROMPT3 = `You are an expert at creating regex patterns for detecting security vulnerabilities in code.
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: SYSTEM_PROMPT3,
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: chalk7.red.bold,
8046
- high: chalk7.red,
8047
- medium: chalk7.yellow,
8048
- low: chalk7.gray
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: chalk7.red,
8052
- data: chalk7.blue,
8053
- concurrency: chalk7.magenta,
8054
- input: chalk7.cyan,
8055
- resource: chalk7.yellow,
8056
- reliability: chalk7.green,
8057
- performance: chalk7.yellowBright,
8058
- platform: chalk7.gray,
8059
- business: chalk7.white,
8060
- compliance: chalk7.blueBright
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: chalk7.green.bold,
8064
- B: chalk7.green,
8065
- C: chalk7.yellow,
8066
- D: chalk7.red,
8067
- F: chalk7.red.bold
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
- ${chalk7.cyan(" ____ _ _ ")}
8071
- ${chalk7.cyan("| _ \\(_)_ __ __ _| |_ __ _ ")}
8072
- ${chalk7.cyan("| |_) | | '_ \\ / _` | __/ _` |")}
8073
- ${chalk7.cyan("| __/| | | | | (_| | || (_| |")}
8074
- ${chalk7.cyan("|_| |_|_| |_|\\__,_|\\__\\__,_|")}
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(chalk7.gray(`Analyzing: ${result.targetDirectory}`));
7197
+ lines.push(chalk6.gray(`Analyzing: ${result.targetDirectory}`));
8080
7198
  const projectTypeLabel = getProjectTypeDescription(result.projectType.type);
8081
- lines.push(chalk7.gray(`Project: ${projectTypeLabel} (${result.projectType.confidence} confidence)`));
8082
- lines.push(chalk7.gray(`Files: ${result.fileStats.totalFiles} | Languages: ${formatLanguages(result)}`));
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(chalk7.bold("Domain Coverage:"));
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(chalk7.green.bold("No gaps detected! Your codebase has good test coverage."));
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(chalk7.gray("Run `pinata generate --gaps` to create tests for these gaps."));
7215
+ lines.push(chalk6.gray("Run `pinata generate --gaps` to create tests for these gaps."));
8098
7216
  }
8099
- lines.push(chalk7.gray(`
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] ?? chalk7.white;
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 = chalk7.cyan("\u2554" + "\u2550".repeat(boxWidth) + "\u2557");
8109
- const middle = chalk7.cyan("\u2551") + " ".repeat(padding) + scoreStr + " ".repeat(boxWidth - padding - scoreStr.length) + chalk7.cyan("\u2551");
8110
- const bottom = chalk7.cyan("\u255A" + "\u2550".repeat(boxWidth) + "\u255D");
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 = chalk7.green("\u2588".repeat(filledWidth)) + chalk7.gray("\u2591".repeat(barWidth - filledWidth));
8126
- const domainColor = DOMAIN_COLORS2[domain] ?? chalk7.white;
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(chalk7.gray(" No domain coverage data available."));
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(chalk7.red.bold(`
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(chalk7.gray(` ... and ${critical.length - 5} more critical gaps`));
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(chalk7.red(`
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(chalk7.gray(` ... and ${high.length - 5} more high severity gaps`));
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(chalk7.yellow(`
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(chalk7.gray(` ... and ${medium.length - 3} more medium severity gaps`));
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(chalk7.gray(`
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] ?? chalk7.white;
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))} ${chalk7.cyan(location.padEnd(30))} ${chalk7.gray(confidence)} confidence`;
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("Your codebase has good test coverage.\n");
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: `Missing test coverage for ${gap.categoryName}`
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("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("--output-file <path>", "Write output to file (useful for SARIF upload)").option("-d, --domains <domains>", "Filter to specific domains (comma-separated)").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "low").option("-c, --confidence <level>", "Minimum confidence: high, medium, low", "high").option("--fail-on <level>", "Exit non-zero if gaps at level: critical, high, medium").option("--exclude <dirs>", "Directories to exclude (comma-separated)").option("--verify", "Use AI to verify each match (reduces false positives)").option("--execute", "Run dynamic tests in Docker sandbox to confirm vulnerabilities").option("--dry-run", "Preview generated tests without executing (use with --execute)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (targetPath, options) => {
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(chalk7.yellow("\nAI verification requires an API key."));
8500
- console.log(chalk7.gray("Get one at: https://console.anthropic.com/settings/keys"));
8501
- console.log(chalk7.gray("Or: https://platform.openai.com/api-keys\n"));
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(chalk7.cyan("Enter your Anthropic or OpenAI API key: "));
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(chalk7.red("No API key provided. Skipping AI verification."));
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(chalk7.green("Anthropic API key saved to ~/.pinata/config.json\n"));
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(chalk7.green("OpenAI API key saved to ~/.pinata/config.json\n"));
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(chalk7.gray("\nDismissed as false positives:"));
7665
+ console.log(chalk6.gray("\nDismissed as false positives:"));
8548
7666
  for (const { gap, reason } of dismissed.slice(0, 5)) {
8549
- console.log(chalk7.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
8550
- console.log(chalk7.gray(` Reason: ${reason.slice(0, 100)}...`));
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(chalk7.gray(` ... and ${dismissed.length - 5} more`));
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(chalk7.yellow(`Verification error: ${error instanceof Error ? error.message : String(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(chalk7.yellow("\nNo dynamically testable gaps found."));
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(chalk7.red(`
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(chalk7.red.bold(`
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
- // src/cli/generate-formatters.ts
8641
- init_errors();
8642
- init_result();
8643
- function formatGeneratedTerminal(tests, basePath) {
8644
- const lines = [];
8645
- if (tests.length === 0) {
8646
- lines.push(chalk7.yellow("No tests generated."));
8647
- return lines.join("\n");
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
- lines.push(chalk7.bold.cyan(`
8650
- Generated ${tests.length} test(s):
8651
- `));
8652
- lines.push(chalk7.gray("\u2500".repeat(60)));
8653
- for (const test of tests) {
8654
- const relGapPath = relative(basePath, test.gap.filePath);
8655
- lines.push("");
8656
- lines.push(chalk7.bold.white(`Test for: ${test.gap.categoryName}`));
8657
- lines.push(chalk7.gray(` Gap location: ${relGapPath}:${test.gap.lineStart}`));
8658
- lines.push(chalk7.gray(` Template: ${test.template.id}`));
8659
- lines.push(chalk7.gray(` Output: ${test.suggestedPath}`));
8660
- lines.push("");
8661
- lines.push(chalk7.cyan(`// --- ${test.suggestedPath} ---`));
8662
- lines.push("");
8663
- if (test.result.imports.length > 0) {
8664
- for (const imp of test.result.imports) {
8665
- lines.push(chalk7.gray(imp));
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
- lines.push(test.result.content);
8670
- lines.push("");
8671
- lines.push(chalk7.gray("\u2500".repeat(60)));
7895
+ if (foundOpenBrace) break;
8672
7896
  }
8673
- lines.push("");
8674
- lines.push(chalk7.bold("Summary:"));
8675
- lines.push(` Tests generated: ${chalk7.green(tests.length.toString())}`);
8676
- const categories = new Set(tests.map((t) => t.category.id));
8677
- lines.push(` Categories covered: ${chalk7.cyan(categories.size.toString())}`);
8678
- if (tests.some((t) => t.result.unresolved.length > 0)) {
8679
- const unresolvedCount = tests.reduce((acc, t) => acc + t.result.unresolved.length, 0);
8680
- lines.push(chalk7.yellow(` Unresolved placeholders: ${unresolvedCount}`));
8681
- lines.push(chalk7.gray(" (Some placeholders need manual completion)"));
8682
- }
8683
- lines.push("");
8684
- lines.push(chalk7.gray("This is a dry run. Use --write to save files."));
8685
- return lines.join("\n");
8686
- }
8687
- function formatGeneratedJson(tests) {
8688
- const output = tests.map((test) => ({
8689
- gap: {
8690
- categoryId: test.gap.categoryId,
8691
- categoryName: test.gap.categoryName,
8692
- filePath: test.gap.filePath,
8693
- lineStart: test.gap.lineStart,
8694
- severity: test.gap.severity,
8695
- confidence: test.gap.confidence
8696
- },
8697
- template: {
8698
- id: test.template.id,
8699
- framework: test.template.framework,
8700
- language: test.template.language
8701
- },
8702
- suggestedPath: test.suggestedPath,
8703
- content: test.result.content,
8704
- imports: test.result.imports,
8705
- fixtures: test.result.fixtures,
8706
- substituted: test.result.substituted,
8707
- unresolved: test.result.unresolved
8708
- }));
8709
- return JSON.stringify(output, null, 2);
8710
- }
8711
- function suggestTestPath(sourceFile, template, basePath) {
8712
- const dir = dirname(sourceFile);
8713
- const name = basename(sourceFile);
8714
- const nameWithoutExt = name.replace(/\.[^.]+$/, "");
8715
- const extMap = {
8716
- python: ".py",
8717
- typescript: ".ts",
8718
- javascript: ".js",
8719
- go: "_test.go",
8720
- java: "Test.java",
8721
- rust: ".rs"
8722
- };
8723
- const ext = extMap[template.language] ?? ".test.ts";
8724
- let testFileName;
8725
- switch (template.language) {
8726
- case "python":
8727
- testFileName = `test_${nameWithoutExt}${ext}`;
8728
- break;
8729
- case "go":
8730
- testFileName = `${nameWithoutExt}${ext}`;
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
- case "java":
8733
- testFileName = `${nameWithoutExt}${ext}`;
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
- default:
8736
- testFileName = `${nameWithoutExt}.test${ext}`;
7921
+ }
8737
7922
  }
8738
- const relativeSrc = relative(basePath, dir);
8739
- const testDir = relativeSrc.replace(/^src/, "tests");
8740
- return `${testDir}/${testFileName}`;
8741
- }
8742
- function extractVariablesFromGap(gap) {
8743
- const fileName = basename(gap.filePath);
8744
- const fileNameWithoutExt = fileName.replace(/\.[^.]+$/, "");
8745
- const funcMatch = gap.codeSnippet.match(/(?:def|function|async function|const|let|var)\s+(\w+)/);
8746
- const classMatch = gap.codeSnippet.match(/(?:class)\s+(\w+)/);
8747
- return {
8748
- // File info
8749
- filePath: gap.filePath,
8750
- fileName,
8751
- fileNameWithoutExt,
8752
- lineNumber: gap.lineStart,
8753
- // Category info
8754
- categoryId: gap.categoryId,
8755
- categoryName: gap.categoryName,
8756
- domain: gap.domain,
8757
- level: gap.level,
8758
- severity: gap.severity,
8759
- confidence: gap.confidence,
8760
- // Code context
8761
- codeSnippet: gap.codeSnippet,
8762
- functionName: funcMatch?.[1] ?? "targetFunction",
8763
- className: classMatch?.[1] ?? "TargetClass",
8764
- // Common template variables
8765
- testName: `test_${gap.categoryId.replace(/-/g, "_")}`,
8766
- testDescription: `Test for ${gap.categoryName} in ${fileName}:${gap.lineStart}`,
8767
- // Pattern info
8768
- patternId: gap.patternId,
8769
- patternType: gap.patternType
8770
- };
8771
- }
8772
- async function extractVariablesWithAI(gap, templateVariables, aiConfig) {
8773
- const baseVars = extractVariablesFromGap(gap);
8774
- const result = await suggestVariables(
8775
- {
8776
- codeSnippet: gap.codeSnippet,
8777
- filePath: gap.filePath,
8778
- variables: templateVariables,
8779
- gap,
8780
- existingValues: baseVars
8781
- },
8782
- aiConfig
8783
- );
8784
- if (result.success && result.data) {
8785
- return result.data.values;
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
- return baseVars;
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
- async function writeGeneratedTests(tests, basePath, outputDir) {
8790
- const summary = {
8791
- created: [],
8792
- updated: [],
8793
- failed: [],
8794
- totalTests: 0
8795
- };
8796
- const testsByFile = /* @__PURE__ */ new Map();
8797
- for (const test of tests) {
8798
- let outputPath;
8799
- if (outputDir) {
8800
- outputPath = resolve(basePath, outputDir, test.suggestedPath);
8801
- } else {
8802
- outputPath = resolve(basePath, test.suggestedPath);
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
- for (const [outputPath, fileTests] of testsByFile) {
8809
- try {
8810
- const dir = dirname(outputPath);
8811
- await mkdir(dir, { recursive: true });
8812
- let existingContent = "";
8813
- let fileExists = false;
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
- existingContent = await readFile(outputPath, "utf-8");
8816
- fileExists = true;
8817
- } catch {
8818
- }
8819
- const contentParts = [];
8820
- const allImports = /* @__PURE__ */ new Set();
8821
- for (const test of fileTests) {
8822
- for (const imp of test.result.imports) {
8823
- allImports.add(imp);
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
- if (!fileExists && allImports.size > 0) {
8827
- contentParts.push(Array.from(allImports).join("\n"));
8828
- contentParts.push("");
8829
- }
8830
- for (const test of fileTests) {
8831
- contentParts.push(`// Test for ${test.gap.categoryName}`);
8832
- contentParts.push(`// Gap: ${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`);
8833
- contentParts.push(`// Generated by Pinata`);
8834
- contentParts.push("");
8835
- contentParts.push(test.result.content);
8836
- contentParts.push("");
8837
- }
8838
- const newContent = contentParts.join("\n");
8839
- let finalContent;
8840
- let appended = false;
8841
- if (fileExists) {
8842
- finalContent = existingContent.trimEnd() + "\n\n" + newContent;
8843
- appended = true;
8844
- } else {
8845
- finalContent = newContent;
8846
- }
8847
- await writeFile(outputPath, finalContent, "utf-8");
8848
- for (const test of fileTests) {
8849
- const result = {
8850
- path: outputPath,
8851
- created: !fileExists,
8852
- appended,
8853
- categoryId: test.gap.categoryId,
8854
- gapLocation: `${relative(basePath, test.gap.filePath)}:${test.gap.lineStart}`
8855
- };
8856
- if (fileExists) {
8857
- summary.updated.push(result);
8858
- } else {
8859
- summary.created.push(result);
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
- return ok(summary);
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 formatWriteSummary(summary, basePath) {
8873
- const lines = [];
8874
- lines.push("");
8875
- lines.push(chalk7.bold.cyan("Write Summary:"));
8876
- lines.push(chalk7.gray("\u2500".repeat(60)));
8877
- if (summary.created.length > 0) {
8878
- const uniquePaths = new Set(summary.created.map((r) => r.path));
8879
- lines.push("");
8880
- lines.push(chalk7.green.bold(`Created ${uniquePaths.size} file(s):`));
8881
- for (const path2 of uniquePaths) {
8882
- const relPath = relative(basePath, path2);
8883
- const testsInFile = summary.created.filter((r) => r.path === path2).length;
8884
- lines.push(chalk7.green(` + ${relPath} (${testsInFile} test(s))`));
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
- if (summary.updated.length > 0) {
8888
- const uniquePaths = new Set(summary.updated.map((r) => r.path));
8889
- lines.push("");
8890
- lines.push(chalk7.yellow.bold(`Updated ${uniquePaths.size} file(s):`));
8891
- for (const path2 of uniquePaths) {
8892
- const relPath = relative(basePath, path2);
8893
- const testsInFile = summary.updated.filter((r) => r.path === path2).length;
8894
- lines.push(chalk7.yellow(` ~ ${relPath} (${testsInFile} test(s) appended)`));
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
- if (summary.failed.length > 0) {
8898
- lines.push("");
8899
- lines.push(chalk7.red.bold(`Failed to write ${summary.failed.length} file(s):`));
8900
- for (const fail of summary.failed) {
8901
- const relPath = relative(basePath, fail.path);
8902
- lines.push(chalk7.red(` \u2717 ${relPath}: ${fail.error}`));
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
- lines.push("");
8906
- lines.push(chalk7.gray("\u2500".repeat(60)));
8907
- lines.push(chalk7.bold(`Total: ${summary.totalTests} test(s) written to ${(/* @__PURE__ */ new Set([...summary.created.map((r) => r.path), ...summary.updated.map((r) => r.path)])).size} file(s)`));
8908
- if (summary.failed.length > 0) {
8909
- lines.push(chalk7.red(`Failures: ${summary.failed.length}`));
8201
+ if (result.endsWith("```")) {
8202
+ result = result.slice(0, -3).trimEnd();
8910
8203
  }
8911
- return lines.join("\n");
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 identified gaps").option("--gaps", "Generate tests for all identified gaps").option("-c, --category <id>", "Generate tests for specific category").option("-d, --domain <domain>", "Generate tests for all categories in domain").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "medium").option("--output-dir <dir>", "Directory for generated test files").option("--write", "Write files to disk (default is dry-run)").option("--ai", "Use AI for smarter template variable filling").option("--ai-provider <provider>", "AI provider: anthropic, openai", "anthropic").option("-o, --output <format>", "Output format: terminal, json", "terminal").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (options) => {
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 dryRun = !options["write"];
8921
- const useAI = Boolean(options["ai"]);
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 domains: ${RISK_DOMAINS.join(", ")}`)));
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}. Use: critical, high, medium, low`)));
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 cached scan results...").start() : null;
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(chalk7.yellow("\nRun `pinata analyze` first to scan for gaps."));
8251
+ console.error(chalk6.yellow("\nRun `pinata analyze` first."));
8960
8252
  process.exit(1);
8961
8253
  }
8962
- const cached = cacheResult.data;
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 the filters");
8976
- console.log(chalk7.yellow("\nNo gaps found matching the specified filters."));
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 = `Found ${gaps.length} gaps. Loading categories...`;
8275
+ spinner.text = `Extracting context for ${gaps.length} findings...`;
8981
8276
  }
8982
- const store = createCategoryStore();
8983
- const definitionsPath = getDefinitionsPath();
8984
- const loadResult = await store.loadFromDirectory(definitionsPath);
8985
- if (!loadResult.success) {
8986
- spinner?.fail("Failed to load categories");
8987
- console.error(formatError(loadResult.error));
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 ${gaps.length} gaps...`;
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 renderer = createRenderer({ strict: false, allowUnresolved: true });
8994
- const generatedTests = [];
8293
+ const apiKey = getApiKey2(aiProvider) ?? "";
8294
+ const callAI = buildAICaller(aiProvider, apiKey);
8295
+ const generated = [];
8995
8296
  const errors = [];
8996
- const gapsByCategory = /* @__PURE__ */ new Map();
8997
- for (const gap of gaps) {
8998
- const existing = gapsByCategory.get(gap.categoryId) ?? [];
8999
- existing.push(gap);
9000
- gapsByCategory.set(gap.categoryId, existing);
9001
- }
9002
- for (const [catId, categoryGaps] of gapsByCategory) {
9003
- const categoryResult = store.get(catId);
9004
- if (!categoryResult.success) {
9005
- errors.push(`Category not found: ${catId}`);
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
- const category = categoryResult.data;
9009
- for (const gap of categoryGaps) {
9010
- const gapExt = gap.filePath.split(".").pop() ?? "";
9011
- const langMap = { py: "python", ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript", go: "go", java: "java", rs: "rust" };
9012
- const gapLang = langMap[gapExt];
9013
- let template = category.testTemplates.find((t) => t.language === gapLang);
9014
- if (!template) {
9015
- template = category.testTemplates[0];
9016
- }
9017
- if (!template) {
9018
- errors.push(`No templates available for ${catId}`);
9019
- continue;
9020
- }
9021
- let variables;
9022
- if (useAI) {
9023
- variables = await extractVariablesWithAI(gap, template.variables, { provider: aiProvider });
9024
- } else {
9025
- variables = extractVariablesFromGap(gap);
9026
- }
9027
- const renderResult = renderer.renderTemplate(template, variables);
9028
- if (!renderResult.success) {
9029
- errors.push(`Failed to render ${catId}: ${renderResult.error.message}`);
9030
- continue;
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
- const suggestedPath = suggestTestPath(gap.filePath, template, cached.targetDirectory);
9033
- generatedTests.push({ gap, category, template, result: renderResult.data, suggestedPath });
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(formatGeneratedJson(generatedTests));
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(formatGeneratedTerminal(generatedTests, cached.targetDirectory));
9041
- }
9042
- if (isVerbose && errors.length > 0) {
9043
- console.error(chalk7.yellow("\nWarnings:"));
9044
- for (const error of errors) {
9045
- console.error(chalk7.gray(` - ${error}`));
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 (!dryRun) {
9049
- const outputDirOption = options["outputDir"];
9050
- const writeResult = await writeGeneratedTests(generatedTests, cached.targetDirectory, outputDirOption);
9051
- if (!writeResult.success) {
9052
- console.error(formatError(writeResult.error));
9053
- process.exit(1);
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(formatWriteSummary(writeResult.data, cached.targetDirectory));
9056
- if (writeResult.data.failed.length > 0) {
9057
- process.exit(1);
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 test coverage analysis and generation").version(VERSION);
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(chalk7.yellow("\nRun `pinata analyze` first to scan for gaps."));
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(chalk7.yellow("No gaps to explain."));
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(chalk7.yellow(`
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" ? chalk7.red : gap.severity === "high" ? chalk7.yellow : chalk7.blue;
9174
- console.log(`${severityColor.bold(`[${gap.severity.toUpperCase()}]`)} ${chalk7.bold(exp.summary)}`);
9175
- console.log(chalk7.gray(` Category: ${gap.categoryId} | ${gap.filePath}:${gap.lineStart}`));
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(chalk7.green(` Fix: ${exp.remediation}`));
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(chalk7.red(result.error ?? "Unknown 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(chalk7.bold(`Suggested Patterns for ${categoryId}`));
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" ? chalk7.green : s.confidence === "medium" ? chalk7.yellow : chalk7.red;
9250
- console.log(` ${chalk7.cyan(s.id)} [${confColor(s.confidence)}]`);
9251
- console.log(` Pattern: ${chalk7.gray(s.pattern)}`);
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: ${chalk7.gray(s.matchExample.slice(0, 80))}`);
9254
- console.log(` Safe: ${chalk7.gray(s.safeExample.slice(0, 80))}`);
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(chalk7.bold(`Search Results for "${query}"`));
9337
- console.log(chalk7.gray(`Found ${results.length} matching categories.`));
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(chalk7.yellow("No categories match your search."));
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" ? chalk7.red : chalk7.blue;
9344
- console.log(` ${chalk7.cyan(cat.id)} - ${chalk7.bold(cat.name)}`);
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(` ${chalk7.gray(cat.description.slice(0, 100))}${cat.description.length > 100 ? "..." : ""}`);
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(chalk7.yellow("Configuration file already exists at .pinata.yml"));
9416
- console.log(chalk7.gray("Use --force to overwrite."));
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(chalk7.green("Created .pinata.yml"));
8815
+ console.log(chalk6.green("Created .pinata.yml"));
9460
8816
  await mkdir4(cacheDir, { recursive: true });
9461
- console.log(chalk7.green("Created .pinata/ directory"));
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(chalk7.green("Added .pinata/ to .gitignore"));
8824
+ console.log(chalk6.green("Added .pinata/ to .gitignore"));
9469
8825
  }
9470
8826
  }
9471
8827
  console.log();
9472
- console.log(chalk7.bold("Pinata initialized successfully!"));
9473
- console.log(chalk7.gray(" 1. Review and customize .pinata.yml"));
9474
- console.log(chalk7.gray(" 2. Run: pinata analyze"));
9475
- console.log(chalk7.gray(" 3. Generate tests: pinata generate"));
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(chalk7.bold("\nPinata Dependency Audit\n"));
8844
+ console.log(chalk6.bold("\nPinata Dependency Audit\n"));
9489
8845
  if (!existsSync(packagePath)) {
9490
- console.error(chalk7.red(`Error: ${packagePath} not found`));
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(chalk7.gray(`Found ${packages.length} dependencies
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(chalk7.red.bold(`
8911
+ console.log(chalk6.red.bold(`
9556
8912
  Critical Issues (${criticals.length}):`));
9557
8913
  for (const issue of criticals) {
9558
- console.log(chalk7.red(` \u2717 ${issue.pkg}: ${issue.message}`));
8914
+ console.log(chalk6.red(` \u2717 ${issue.pkg}: ${issue.message}`));
9559
8915
  }
9560
8916
  }
9561
8917
  if (warnings.length > 0) {
9562
- console.log(chalk7.yellow.bold(`
8918
+ console.log(chalk6.yellow.bold(`
9563
8919
  Warnings (${warnings.length}):`));
9564
8920
  for (const issue of warnings.slice(0, 20)) {
9565
- console.log(chalk7.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
8921
+ console.log(chalk6.yellow(` \u26A0 ${issue.pkg}: ${issue.message}`));
9566
8922
  }
9567
8923
  if (warnings.length > 20) {
9568
- console.log(chalk7.gray(` ... and ${warnings.length - 20} more`));
8924
+ console.log(chalk6.gray(` ... and ${warnings.length - 20} more`));
9569
8925
  }
9570
8926
  }
9571
8927
  if (issues.length === 0) {
9572
- console.log(chalk7.green("\u2713 No dependency issues found"));
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(chalk7.green("Feedback data reset."));
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(chalk7.bold("\nPinata Feedback Report\n"));
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(chalk7.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
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(chalk7.bold("\nPattern Performance:"));
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 ? chalk7.green : p.precision >= 0.4 ? chalk7.yellow : chalk7.red;
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(chalk7.red(`Invalid API key: ${validation.error}`));
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(chalk7.green(`Anthropic API key set: ${maskApiKey2(value)}`));
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(chalk7.red(`Invalid API key: ${validation.error}`));
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(chalk7.green(`OpenAI API key set: ${maskApiKey2(value)}`));
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(chalk7.red("Provider must be 'anthropic' or 'openai'"));
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(chalk7.green(`Default provider set to: ${value}`));
9011
+ console.log(chalk6.green(`Default provider set to: ${value}`));
9656
9012
  break;
9657
9013
  }
9658
9014
  default:
9659
- console.log(chalk7.red(`Unknown config key: ${key}`));
9660
- console.log(chalk7.gray("Run 'pinata config set --help' for available keys"));
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(chalk7.gray(`Config stored at: ${getConfigPath2()}`));
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) : chalk7.gray("(not set)"));
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) : chalk7.gray("(not set)"));
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 ?? chalk7.gray("anthropic (default)"));
9032
+ console.log(cfg.defaultProvider ?? chalk6.gray("anthropic (default)"));
9677
9033
  break;
9678
9034
  default:
9679
- console.log(chalk7.red(`Unknown config key: ${key}`));
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(chalk7.bold("Pinata Configuration"));
9687
- console.log(chalk7.gray(`Config file: ${getConfigPath2()}`));
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") ? chalk7.green("configured") : chalk7.gray("not set");
9691
- const openaiStatus = hasApiKey2("openai") ? chalk7.green("configured") : chalk7.gray("not set");
9692
- console.log(` Anthropic API key: ${anthropicStatus} ${cfg.anthropicApiKey ? chalk7.gray(`(${maskApiKey2(cfg.anthropicApiKey)})`) : ""}`);
9693
- console.log(` OpenAI API key: ${openaiStatus} ${cfg.openaiApiKey ? chalk7.gray(`(${maskApiKey2(cfg.openaiApiKey)})`) : ""}`);
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(chalk7.yellow("No AI provider configured."));
9698
- console.log(chalk7.gray(" pinata config set anthropic-api-key sk-ant-xxx"));
9699
- console.log(chalk7.gray(" export ANTHROPIC_API_KEY=sk-ant-xxx"));
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(chalk7.green("Anthropic API key removed"));
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(chalk7.green("OpenAI API key removed"));
9067
+ console.log(chalk6.green("OpenAI API key removed"));
9712
9068
  break;
9713
9069
  case "default-provider":
9714
9070
  deleteConfigValue2("defaultProvider");
9715
- console.log(chalk7.green("Default provider reset to: anthropic"));
9071
+ console.log(chalk6.green("Default provider reset to: anthropic"));
9716
9072
  break;
9717
9073
  default:
9718
- console.log(chalk7.red(`Unknown config key: ${key}`));
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(chalk7.yellow("No API key provided."));
9727
- console.log(chalk7.gray(" pinata auth login --key <your-api-key>"));
9728
- console.log(chalk7.gray(" PINATA_API_KEY=<your-api-key> pinata auth login"));
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(chalk7.red("Invalid API key format. Keys should start with 'pk_'."));
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(chalk7.green("API key configured successfully!"));
9746
- console.log(chalk7.gray(`Key ID: ${maskedKey}`));
9747
- console.log(chalk7.yellow("Important: Add .pinata/.env to your .gitignore"));
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 ? chalk7.green("API key removed successfully.") : chalk7.yellow("No stored API key found."));
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(chalk7.yellow("Not authenticated."));
9778
- console.log(chalk7.gray("Run: pinata auth login --key <your-api-key>"));
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(chalk7.green("Authenticated"));
9785
- console.log(chalk7.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
9786
- console.log(chalk7.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));
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(chalk7.yellow("Authentication status unknown."));
9789
- console.log(chalk7.gray("Run: pinata auth login to reconfigure."));
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();