pinata-security-cli 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,7 +90,8 @@ dist/
90
90
  --dry-run # Preview generated tests without running
91
91
  --confidence <level> # high (default), medium, low
92
92
  --output <format> # terminal, json, sarif, junit, markdown
93
- --domain <domain> # security, data, concurrency, etc.
93
+ --output-file <path> # Write results to file (for SARIF upload)
94
+ --domains <domains> # security, data, concurrency, etc.
94
95
  --severity <level> # critical, high, medium, low
95
96
  --exclude <dirs> # Comma-separated directories to skip
96
97
  ```
@@ -149,21 +150,50 @@ pinata analyze . --execute --dry-run
149
150
 
150
151
  ## CI/CD Integration
151
152
 
152
- **GitHub Actions**
153
+ **GitHub Action (recommended)**
154
+
153
155
  ```yaml
154
156
  name: Security Scan
155
157
  on: [push, pull_request]
156
158
 
157
159
  jobs:
158
- pinata:
160
+ security:
159
161
  runs-on: ubuntu-latest
162
+ permissions:
163
+ contents: read
164
+ security-events: write
160
165
  steps:
161
166
  - uses: actions/checkout@v4
162
- - name: Run Pinata
163
- run: npx --yes pinata-security-cli@latest analyze . --output sarif > results.sarif
164
- - uses: github/codeql-action/upload-sarif@v3
167
+ - uses: christiancattaneo/pinata-security@v1
165
168
  with:
166
- sarif_file: results.sarif
169
+ confidence: high
170
+ sarif-output: pinata.sarif
171
+ # Optional: AI verification
172
+ # with:
173
+ # verify: true
174
+ # env:
175
+ # ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
176
+ ```
177
+
178
+ **Action inputs:**
179
+ - `path` - Directory to scan (default: `.`)
180
+ - `confidence` - high, medium, low (default: `high`)
181
+ - `domains` - Comma-separated domains to scan
182
+ - `verify` - Enable AI verification (default: `false`)
183
+ - `fail-on-gaps` - Fail if gaps found (default: `true`)
184
+ - `sarif-output` - Path for SARIF file (auto-uploads to GitHub Security)
185
+
186
+ **Action outputs:**
187
+ - `score` - Pinata score (0-100)
188
+ - `gaps` - Number of gaps found
189
+ - `sarif-file` - Path to SARIF file
190
+
191
+ **Manual workflow (any CI)**
192
+ ```yaml
193
+ - run: npx --yes pinata-security-cli@latest analyze . --output sarif --output-file results.sarif
194
+ - uses: github/codeql-action/upload-sarif@v3
195
+ with:
196
+ sarif_file: results.sarif
167
197
  ```
168
198
 
169
199
  **GitLab CI**
package/dist/cli/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  import { z } from 'zod';
3
3
  import fs, { mkdir, writeFile, readFile, stat, readdir, mkdtemp, rm } from 'fs/promises';
4
4
  import path, { dirname, resolve, join, basename, relative, extname } from 'path';
5
- import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
5
+ import { existsSync, writeFileSync, readFileSync, chmodSync, mkdirSync } from 'fs';
6
6
  import { homedir, tmpdir } from 'os';
7
7
  import { spawn } from 'child_process';
8
8
  import { useState } from 'react';
@@ -2553,6 +2553,174 @@ var init_tui = __esm({
2553
2553
  init_App();
2554
2554
  }
2555
2555
  });
2556
+
2557
+ // src/feedback/types.ts
2558
+ function suggestConfidence(precision) {
2559
+ if (precision >= CONFIDENCE_THRESHOLDS.high) return "high";
2560
+ if (precision >= CONFIDENCE_THRESHOLDS.medium) return "medium";
2561
+ return "low";
2562
+ }
2563
+ var EMPTY_FEEDBACK_STATE, CONFIDENCE_THRESHOLDS;
2564
+ var init_types3 = __esm({
2565
+ "src/feedback/types.ts"() {
2566
+ EMPTY_FEEDBACK_STATE = {
2567
+ version: 1,
2568
+ patterns: {},
2569
+ totalScans: 0,
2570
+ lastScanAt: (/* @__PURE__ */ new Date()).toISOString()
2571
+ };
2572
+ CONFIDENCE_THRESHOLDS = {
2573
+ /** Precision >= 0.7 → high confidence */
2574
+ high: 0.7,
2575
+ /** Precision >= 0.4 → medium confidence */
2576
+ medium: 0.4,
2577
+ /** Precision < 0.4 → low confidence */
2578
+ low: 0
2579
+ };
2580
+ }
2581
+ });
2582
+ async function loadFeedback() {
2583
+ try {
2584
+ const content = await readFile(FEEDBACK_FILE, "utf-8");
2585
+ const state = JSON.parse(content);
2586
+ if (state.version !== 1) {
2587
+ console.warn("Feedback version mismatch, resetting...");
2588
+ return { ...EMPTY_FEEDBACK_STATE };
2589
+ }
2590
+ return state;
2591
+ } catch {
2592
+ return { ...EMPTY_FEEDBACK_STATE };
2593
+ }
2594
+ }
2595
+ async function saveFeedback(state) {
2596
+ try {
2597
+ await mkdir(FEEDBACK_DIR, { recursive: true });
2598
+ await writeFile(FEEDBACK_FILE, JSON.stringify(state, null, 2));
2599
+ } catch (error) {
2600
+ console.warn(`Failed to save feedback: ${error instanceof Error ? error.message : String(error)}`);
2601
+ }
2602
+ }
2603
+ function applyUpdates(state, updates) {
2604
+ const newState = { ...state };
2605
+ newState.patterns = { ...state.patterns };
2606
+ for (const update of updates) {
2607
+ const existing = newState.patterns[update.patternId];
2608
+ const pattern = existing ?? {
2609
+ patternId: update.patternId,
2610
+ categoryId: update.categoryId,
2611
+ totalMatches: 0,
2612
+ confirmedCount: 0,
2613
+ unconfirmedCount: 0,
2614
+ aiDismissedCount: 0,
2615
+ aiVerifiedCount: 0,
2616
+ precision: 0,
2617
+ suggestedConfidence: "medium",
2618
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2619
+ };
2620
+ switch (update.outcome) {
2621
+ case "matched":
2622
+ pattern.totalMatches++;
2623
+ break;
2624
+ case "confirmed":
2625
+ pattern.confirmedCount++;
2626
+ break;
2627
+ case "unconfirmed":
2628
+ pattern.unconfirmedCount++;
2629
+ break;
2630
+ case "ai_verified":
2631
+ pattern.aiVerifiedCount++;
2632
+ break;
2633
+ case "ai_dismissed":
2634
+ pattern.aiDismissedCount++;
2635
+ break;
2636
+ }
2637
+ const total = pattern.confirmedCount + pattern.unconfirmedCount;
2638
+ pattern.precision = total > 0 ? pattern.confirmedCount / total : 0.5;
2639
+ pattern.suggestedConfidence = suggestConfidence(pattern.precision);
2640
+ pattern.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2641
+ newState.patterns[update.patternId] = pattern;
2642
+ }
2643
+ newState.totalScans++;
2644
+ newState.lastScanAt = (/* @__PURE__ */ new Date()).toISOString();
2645
+ return newState;
2646
+ }
2647
+ function getConfidenceAdjustment(state, patternId) {
2648
+ const pattern = state.patterns[patternId];
2649
+ if (!pattern) return null;
2650
+ const totalExecutions = pattern.confirmedCount + pattern.unconfirmedCount;
2651
+ if (totalExecutions < 5) return null;
2652
+ return pattern.suggestedConfidence;
2653
+ }
2654
+ function getLowPrecisionPatterns(state, threshold = 0.3) {
2655
+ return Object.values(state.patterns).filter((p) => {
2656
+ const total = p.confirmedCount + p.unconfirmedCount;
2657
+ return total >= 5 && p.precision < threshold;
2658
+ }).sort((a, b) => a.precision - b.precision);
2659
+ }
2660
+ function getHighPrecisionPatterns(state, threshold = 0.8) {
2661
+ return Object.values(state.patterns).filter((p) => {
2662
+ const total = p.confirmedCount + p.unconfirmedCount;
2663
+ return total >= 5 && p.precision >= threshold;
2664
+ }).sort((a, b) => b.precision - a.precision);
2665
+ }
2666
+ function generateReport(state) {
2667
+ const lines = [
2668
+ "# Pinata Feedback Report",
2669
+ "",
2670
+ `Total scans: ${state.totalScans}`,
2671
+ `Last scan: ${state.lastScanAt}`,
2672
+ `Patterns tracked: ${Object.keys(state.patterns).length}`,
2673
+ ""
2674
+ ];
2675
+ const lowPrecision = getLowPrecisionPatterns(state);
2676
+ if (lowPrecision.length > 0) {
2677
+ lines.push("## Low Precision Patterns (potential false positive sources)");
2678
+ lines.push("");
2679
+ for (const p of lowPrecision.slice(0, 10)) {
2680
+ lines.push(`- ${p.patternId}: ${(p.precision * 100).toFixed(1)}% precision (${p.confirmedCount}/${p.confirmedCount + p.unconfirmedCount})`);
2681
+ }
2682
+ lines.push("");
2683
+ }
2684
+ const highPrecision = getHighPrecisionPatterns(state);
2685
+ if (highPrecision.length > 0) {
2686
+ lines.push("## High Precision Patterns");
2687
+ lines.push("");
2688
+ for (const p of highPrecision.slice(0, 10)) {
2689
+ lines.push(`- ${p.patternId}: ${(p.precision * 100).toFixed(1)}% precision (${p.confirmedCount}/${p.confirmedCount + p.unconfirmedCount})`);
2690
+ }
2691
+ lines.push("");
2692
+ }
2693
+ return lines.join("\n");
2694
+ }
2695
+ var FEEDBACK_DIR, FEEDBACK_FILE;
2696
+ var init_store = __esm({
2697
+ "src/feedback/store.ts"() {
2698
+ init_types3();
2699
+ FEEDBACK_DIR = join(homedir(), ".pinata");
2700
+ FEEDBACK_FILE = join(FEEDBACK_DIR, "feedback.json");
2701
+ }
2702
+ });
2703
+
2704
+ // src/feedback/index.ts
2705
+ var feedback_exports = {};
2706
+ __export(feedback_exports, {
2707
+ CONFIDENCE_THRESHOLDS: () => CONFIDENCE_THRESHOLDS,
2708
+ EMPTY_FEEDBACK_STATE: () => EMPTY_FEEDBACK_STATE,
2709
+ applyUpdates: () => applyUpdates,
2710
+ generateReport: () => generateReport,
2711
+ getConfidenceAdjustment: () => getConfidenceAdjustment,
2712
+ getHighPrecisionPatterns: () => getHighPrecisionPatterns,
2713
+ getLowPrecisionPatterns: () => getLowPrecisionPatterns,
2714
+ loadFeedback: () => loadFeedback,
2715
+ saveFeedback: () => saveFeedback,
2716
+ suggestConfidence: () => suggestConfidence
2717
+ });
2718
+ var init_feedback = __esm({
2719
+ "src/feedback/index.ts"() {
2720
+ init_types3();
2721
+ init_store();
2722
+ }
2723
+ });
2556
2724
  var RiskDomainSchema = z.enum([
2557
2725
  "security",
2558
2726
  "data",
@@ -5492,9 +5660,9 @@ var AIService = class {
5492
5660
  getApiKeyFromConfig(provider) {
5493
5661
  try {
5494
5662
  const { existsSync: existsSync4, readFileSync: readFileSync3 } = __require("fs");
5495
- const { homedir: homedir2 } = __require("os");
5496
- const { join: join4 } = __require("path");
5497
- const configPath = join4(homedir2(), ".pinata", "config.json");
5663
+ const { homedir: homedir3 } = __require("os");
5664
+ const { join: join5 } = __require("path");
5665
+ const configPath = join5(homedir3(), ".pinata", "config.json");
5498
5666
  if (!existsSync4(configPath)) {
5499
5667
  return "";
5500
5668
  }
@@ -6882,7 +7050,7 @@ function getDefinitionsPath() {
6882
7050
  }
6883
7051
  var program = new Command();
6884
7052
  program.name("pinata").description("AI-powered test coverage analysis and generation").version(VERSION);
6885
- program.command("analyze [path]").description("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("-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) => {
7053
+ program.command("analyze [path]").description("Analyze codebase for test coverage gaps").option("-o, --output <format>", "Output format: terminal, json, markdown, sarif, html, junit-xml", "terminal").option("--output-file <path>", "Write output to file (useful for SARIF upload)").option("-d, --domains <domains>", "Filter to specific domains (comma-separated)").option("-s, --severity <level>", "Minimum severity: critical, high, medium, low", "low").option("-c, --confidence <level>", "Minimum confidence: high, medium, low", "high").option("--fail-on <level>", "Exit non-zero if gaps at level: critical, high, medium").option("--exclude <dirs>", "Directories to exclude (comma-separated)").option("--verify", "Use AI to verify each match (reduces false positives)").option("--execute", "Run dynamic tests in Docker sandbox to confirm vulnerabilities").option("--dry-run", "Preview generated tests without executing (use with --execute)").option("-v, --verbose", "Verbose output").option("-q, --quiet", "Quiet mode (errors only)").action(async (targetPath, options) => {
6886
7054
  const isQuiet = Boolean(options["quiet"]);
6887
7055
  const isVerbose = Boolean(options["verbose"]);
6888
7056
  if (isQuiet) {
@@ -7005,12 +7173,12 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
7005
7173
  const verifySpinner = showSpinner ? ora("Verifying gaps with AI...").start() : null;
7006
7174
  try {
7007
7175
  const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
7008
- const { readFile: readFile5 } = await import('fs/promises');
7176
+ const { readFile: readFile6 } = await import('fs/promises');
7009
7177
  const apiKey = getApiKey2(provider);
7010
7178
  const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
7011
7179
  const { verified, dismissed, stats } = await verifier.verifyAll(
7012
7180
  scanResult.data.gaps,
7013
- async (path2) => readFile5(path2, "utf-8")
7181
+ async (path2) => readFile6(path2, "utf-8")
7014
7182
  );
7015
7183
  scanResult.data.gaps = verified;
7016
7184
  const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
@@ -7047,7 +7215,7 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
7047
7215
  const isDryRun = Boolean(options["dryRun"]);
7048
7216
  if (shouldExecute && scanResult.data.gaps.length > 0) {
7049
7217
  const { createRunner: createRunner2, isTestable: isTestable2 } = await Promise.resolve().then(() => (init_execution(), execution_exports));
7050
- const { readFile: readFile5 } = await import('fs/promises');
7218
+ const { readFile: readFile6 } = await import('fs/promises');
7051
7219
  const testableGaps = scanResult.data.gaps.filter((g) => isTestable2(g.categoryId));
7052
7220
  if (testableGaps.length === 0) {
7053
7221
  console.log(chalk5.yellow("\nNo dynamically testable gaps found."));
@@ -7063,7 +7231,7 @@ Dynamic execution unavailable: ${initResult.error}`));
7063
7231
  for (const gap of testableGaps) {
7064
7232
  if (!fileContents.has(gap.filePath)) {
7065
7233
  try {
7066
- fileContents.set(gap.filePath, await readFile5(gap.filePath, "utf-8"));
7234
+ fileContents.set(gap.filePath, await readFile6(gap.filePath, "utf-8"));
7067
7235
  } catch {
7068
7236
  }
7069
7237
  }
@@ -7090,7 +7258,14 @@ Dynamic execution unavailable: ${initResult.error}`));
7090
7258
  logger.debug(`Failed to cache results: ${cacheResult.error.message}`);
7091
7259
  }
7092
7260
  const output = formatScanResult(scanResult.data, outputFormat, targetDirectory);
7093
- console.log(output);
7261
+ const outputFile = options["outputFile"];
7262
+ if (outputFile) {
7263
+ const outputPath = resolve(outputFile);
7264
+ writeFileSync(outputPath, output, "utf-8");
7265
+ logger.info(`Results written to: ${outputPath}`);
7266
+ } else {
7267
+ console.log(output);
7268
+ }
7094
7269
  if (isVerbose && scanResult.data.warnings.length > 0) {
7095
7270
  console.error("\nWarnings:");
7096
7271
  for (const warning of scanResult.data.warnings) {
@@ -7497,8 +7672,8 @@ program.command("suggest-patterns").description("Use AI to suggest new detection
7497
7672
  let vulnerableCode = [...codeSnippets];
7498
7673
  if (filePath) {
7499
7674
  try {
7500
- const { readFile: readFile5 } = await import('fs/promises');
7501
- const content = await readFile5(filePath, "utf-8");
7675
+ const { readFile: readFile6 } = await import('fs/promises');
7676
+ const content = await readFile6(filePath, "utf-8");
7502
7677
  vulnerableCode = [...vulnerableCode, ...content.split("\n---\n").filter(Boolean)];
7503
7678
  } catch (error) {
7504
7679
  console.error(formatError(new Error(`Failed to read file: ${filePath}`)));
@@ -7797,16 +7972,16 @@ thresholds:
7797
7972
  high: 5
7798
7973
  medium: 20
7799
7974
  `;
7800
- const { writeFile: writeFileAsync, mkdir: mkdir3 } = await import('fs/promises');
7975
+ const { writeFile: writeFileAsync, mkdir: mkdir4 } = await import('fs/promises');
7801
7976
  try {
7802
7977
  await writeFileAsync(configPath, defaultConfig, "utf8");
7803
7978
  console.log(chalk5.green("Created .pinata.yml"));
7804
- await mkdir3(cacheDir, { recursive: true });
7979
+ await mkdir4(cacheDir, { recursive: true });
7805
7980
  console.log(chalk5.green("Created .pinata/ directory"));
7806
7981
  const gitignorePath = resolve(process.cwd(), ".gitignore");
7807
7982
  if (existsSync(gitignorePath)) {
7808
- const { readFile: readFile5, appendFile } = await import('fs/promises');
7809
- const gitignore = await readFile5(gitignorePath, "utf8");
7983
+ const { readFile: readFile6, appendFile } = await import('fs/promises');
7984
+ const gitignore = await readFile6(gitignorePath, "utf8");
7810
7985
  if (!gitignore.includes(".pinata/")) {
7811
7986
  await appendFile(gitignorePath, "\n# Pinata cache\n.pinata/\n");
7812
7987
  console.log(chalk5.green("Added .pinata/ to .gitignore"));
@@ -7950,6 +8125,43 @@ Warnings (${warnings.length}):`));
7950
8125
  process.exit(1);
7951
8126
  }
7952
8127
  });
8128
+ program.command("feedback").description("View pattern performance feedback (Layer 6)").option("--reset", "Reset all feedback data").option("-o, --output <format>", "Output format: terminal, json, markdown", "terminal").action(async (options) => {
8129
+ const { loadFeedback: loadFeedback2, saveFeedback: saveFeedback2, generateReport: generateReport2, EMPTY_FEEDBACK_STATE: EMPTY_FEEDBACK_STATE2 } = await Promise.resolve().then(() => (init_feedback(), feedback_exports));
8130
+ const outputFormat = String(options["output"] ?? "terminal");
8131
+ const shouldReset = Boolean(options["reset"]);
8132
+ if (shouldReset) {
8133
+ await saveFeedback2({ ...EMPTY_FEEDBACK_STATE2 });
8134
+ console.log(chalk5.green("Feedback data reset."));
8135
+ return;
8136
+ }
8137
+ const state = await loadFeedback2();
8138
+ if (outputFormat === "json") {
8139
+ console.log(JSON.stringify(state, null, 2));
8140
+ return;
8141
+ }
8142
+ if (outputFormat === "markdown") {
8143
+ console.log(generateReport2(state));
8144
+ return;
8145
+ }
8146
+ console.log(chalk5.bold("\nPinata Feedback Report\n"));
8147
+ console.log(`Total scans: ${state.totalScans}`);
8148
+ console.log(`Patterns tracked: ${Object.keys(state.patterns).length}`);
8149
+ if (state.totalScans === 0) {
8150
+ console.log(chalk5.gray("\nNo feedback data yet. Run scans with --execute to collect data.\n"));
8151
+ return;
8152
+ }
8153
+ const patterns = Object.values(state.patterns).filter((p) => p.confirmedCount + p.unconfirmedCount >= 1).sort((a, b) => b.precision - a.precision);
8154
+ if (patterns.length > 0) {
8155
+ console.log(chalk5.bold("\nPattern Performance:"));
8156
+ for (const p of patterns.slice(0, 15)) {
8157
+ const total = p.confirmedCount + p.unconfirmedCount;
8158
+ const precisionPct = (p.precision * 100).toFixed(0);
8159
+ const color = p.precision >= 0.7 ? chalk5.green : p.precision >= 0.4 ? chalk5.yellow : chalk5.red;
8160
+ console.log(` ${color(`${precisionPct}%`)} ${p.patternId} (${p.confirmedCount}/${total} confirmed)`);
8161
+ }
8162
+ }
8163
+ console.log();
8164
+ });
7953
8165
  var config = program.command("config").description("Manage AI provider configuration");
7954
8166
  config.command("set <key> <value>").description("Set a configuration value").addHelpText("after", `
7955
8167
  Available keys:
@@ -8078,9 +8290,9 @@ auth.command("login").description("Set API key for Pinata Cloud").option("-k, --
8078
8290
  }
8079
8291
  const configDir = resolve(process.cwd(), ".pinata");
8080
8292
  const authPath = resolve(configDir, "auth.json");
8081
- const { mkdir: mkdir3, writeFile: writeFileAsync } = await import('fs/promises');
8293
+ const { mkdir: mkdir4, writeFile: writeFileAsync } = await import('fs/promises');
8082
8294
  try {
8083
- await mkdir3(configDir, { recursive: true });
8295
+ await mkdir4(configDir, { recursive: true });
8084
8296
  const maskedKey = `****${apiKey.slice(-8)}`;
8085
8297
  const authData = {
8086
8298
  configured: true,
@@ -8133,8 +8345,8 @@ auth.command("status").description("Check authentication status").action(async (
8133
8345
  process.exit(0);
8134
8346
  }
8135
8347
  try {
8136
- const { readFile: readFile5 } = await import('fs/promises');
8137
- const authData = JSON.parse(await readFile5(authPath, "utf8"));
8348
+ const { readFile: readFile6 } = await import('fs/promises');
8349
+ const authData = JSON.parse(await readFile6(authPath, "utf8"));
8138
8350
  console.log(chalk5.green("Authenticated"));
8139
8351
  console.log(chalk5.gray(`Key ID: ${authData.keyId ?? "unknown"}`));
8140
8352
  console.log(chalk5.gray(`Configured: ${authData.configuredAt ?? "unknown"}`));