pinata-security-cli 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { z } from 'zod';
3
3
  import fs, { mkdir, writeFile, readFile, stat, readdir } from 'fs/promises';
4
- import path, { dirname, resolve, basename, relative, join, extname } from 'path';
4
+ import path, { dirname, resolve, join, basename, relative, extname } from 'path';
5
+ import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
6
+ import { homedir } from 'os';
5
7
  import { useState } from 'react';
6
8
  import { render, useApp, useInput, Box, Text } from 'ink';
7
9
  import Spinner from 'ink-spinner';
8
10
  import { jsx, jsxs } from 'react/jsx-runtime';
9
- import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
10
- import { homedir } from 'os';
11
11
  import { fileURLToPath } from 'url';
12
12
  import chalk5 from 'chalk';
13
13
  import { Command } from 'commander';
@@ -742,6 +742,109 @@ var init_junit_formatter = __esm({
742
742
  }
743
743
  });
744
744
 
745
+ // src/cli/config.ts
746
+ var config_exports = {};
747
+ __export(config_exports, {
748
+ deleteConfigValue: () => deleteConfigValue,
749
+ getApiKey: () => getApiKey,
750
+ getConfigPath: () => getConfigPath,
751
+ getConfigValue: () => getConfigValue,
752
+ getDefaultProvider: () => getDefaultProvider,
753
+ hasApiKey: () => hasApiKey,
754
+ loadConfig: () => loadConfig,
755
+ maskApiKey: () => maskApiKey,
756
+ saveConfig: () => saveConfig,
757
+ setConfigValue: () => setConfigValue,
758
+ validateApiKey: () => validateApiKey
759
+ });
760
+ function ensureConfigDir() {
761
+ mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
762
+ }
763
+ function loadConfig() {
764
+ try {
765
+ const content = readFileSync(CONFIG_FILE, "utf-8");
766
+ const parsed = JSON.parse(content);
767
+ const result = ConfigSchema.safeParse(parsed);
768
+ return result.success ? result.data : {};
769
+ } catch {
770
+ return {};
771
+ }
772
+ }
773
+ function saveConfig(config2) {
774
+ ensureConfigDir();
775
+ const content = JSON.stringify(config2, null, 2);
776
+ writeFileSync(CONFIG_FILE, content, { mode: 384 });
777
+ chmodSync(CONFIG_FILE, 384);
778
+ }
779
+ function setConfigValue(key, value) {
780
+ const config2 = loadConfig();
781
+ config2[key] = value;
782
+ saveConfig(config2);
783
+ }
784
+ function getConfigValue(key) {
785
+ const config2 = loadConfig();
786
+ return config2[key];
787
+ }
788
+ function deleteConfigValue(key) {
789
+ const config2 = loadConfig();
790
+ delete config2[key];
791
+ saveConfig(config2);
792
+ }
793
+ function getApiKey(provider) {
794
+ const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
795
+ const envValue = process.env[envVar];
796
+ if (envValue !== void 0 && envValue.length > 0) {
797
+ return envValue;
798
+ }
799
+ const config2 = loadConfig();
800
+ return provider === "anthropic" ? config2.anthropicApiKey : config2.openaiApiKey;
801
+ }
802
+ function hasApiKey(provider) {
803
+ const key = getApiKey(provider);
804
+ return key !== void 0 && key.length > 0;
805
+ }
806
+ function getDefaultProvider() {
807
+ const config2 = loadConfig();
808
+ return config2.defaultProvider ?? "anthropic";
809
+ }
810
+ function maskApiKey(key) {
811
+ if (key.length <= 12) {
812
+ return "****";
813
+ }
814
+ return `${key.slice(0, 4)}...${key.slice(-4)}`;
815
+ }
816
+ function validateApiKey(provider, key) {
817
+ if (key.length === 0) {
818
+ return { valid: false, error: "API key cannot be empty" };
819
+ }
820
+ if (provider === "anthropic") {
821
+ if (!key.startsWith("sk-ant-")) {
822
+ return { valid: false, error: "Anthropic API keys should start with 'sk-ant-'" };
823
+ }
824
+ } else if (provider === "openai") {
825
+ if (!key.startsWith("sk-")) {
826
+ return { valid: false, error: "OpenAI API keys should start with 'sk-'" };
827
+ }
828
+ }
829
+ return { valid: true };
830
+ }
831
+ function getConfigPath() {
832
+ return CONFIG_FILE;
833
+ }
834
+ var CONFIG_DIR, CONFIG_FILE, ConfigSchema;
835
+ var init_config = __esm({
836
+ "src/cli/config.ts"() {
837
+ CONFIG_DIR = join(homedir(), ".pinata");
838
+ CONFIG_FILE = join(CONFIG_DIR, "config.json");
839
+ ConfigSchema = z.object({
840
+ anthropicApiKey: z.string().optional(),
841
+ openaiApiKey: z.string().optional(),
842
+ defaultProvider: z.enum(["anthropic", "openai"]).optional(),
843
+ telemetry: z.boolean().optional()
844
+ });
845
+ }
846
+ });
847
+
745
848
  // src/core/verifier/ai-verifier.ts
746
849
  var SKIP_PATTERNS, BATCH_PROMPT, SINGLE_ITEM_TEMPLATE, AIVerifier;
747
850
  var init_ai_verifier = __esm({
@@ -1456,109 +1559,6 @@ var init_tui = __esm({
1456
1559
  init_App();
1457
1560
  }
1458
1561
  });
1459
-
1460
- // src/cli/config.ts
1461
- var config_exports = {};
1462
- __export(config_exports, {
1463
- deleteConfigValue: () => deleteConfigValue,
1464
- getApiKey: () => getApiKey,
1465
- getConfigPath: () => getConfigPath,
1466
- getConfigValue: () => getConfigValue,
1467
- getDefaultProvider: () => getDefaultProvider,
1468
- hasApiKey: () => hasApiKey,
1469
- loadConfig: () => loadConfig,
1470
- maskApiKey: () => maskApiKey,
1471
- saveConfig: () => saveConfig,
1472
- setConfigValue: () => setConfigValue,
1473
- validateApiKey: () => validateApiKey
1474
- });
1475
- function ensureConfigDir() {
1476
- mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
1477
- }
1478
- function loadConfig() {
1479
- try {
1480
- const content = readFileSync(CONFIG_FILE, "utf-8");
1481
- const parsed = JSON.parse(content);
1482
- const result = ConfigSchema.safeParse(parsed);
1483
- return result.success ? result.data : {};
1484
- } catch {
1485
- return {};
1486
- }
1487
- }
1488
- function saveConfig(config2) {
1489
- ensureConfigDir();
1490
- const content = JSON.stringify(config2, null, 2);
1491
- writeFileSync(CONFIG_FILE, content, { mode: 384 });
1492
- chmodSync(CONFIG_FILE, 384);
1493
- }
1494
- function setConfigValue(key, value) {
1495
- const config2 = loadConfig();
1496
- config2[key] = value;
1497
- saveConfig(config2);
1498
- }
1499
- function getConfigValue(key) {
1500
- const config2 = loadConfig();
1501
- return config2[key];
1502
- }
1503
- function deleteConfigValue(key) {
1504
- const config2 = loadConfig();
1505
- delete config2[key];
1506
- saveConfig(config2);
1507
- }
1508
- function getApiKey(provider) {
1509
- const envVar = provider === "anthropic" ? "ANTHROPIC_API_KEY" : "OPENAI_API_KEY";
1510
- const envValue = process.env[envVar];
1511
- if (envValue !== void 0 && envValue.length > 0) {
1512
- return envValue;
1513
- }
1514
- const config2 = loadConfig();
1515
- return provider === "anthropic" ? config2.anthropicApiKey : config2.openaiApiKey;
1516
- }
1517
- function hasApiKey(provider) {
1518
- const key = getApiKey(provider);
1519
- return key !== void 0 && key.length > 0;
1520
- }
1521
- function getDefaultProvider() {
1522
- const config2 = loadConfig();
1523
- return config2.defaultProvider ?? "anthropic";
1524
- }
1525
- function maskApiKey(key) {
1526
- if (key.length <= 12) {
1527
- return "****";
1528
- }
1529
- return `${key.slice(0, 4)}...${key.slice(-4)}`;
1530
- }
1531
- function validateApiKey(provider, key) {
1532
- if (key.length === 0) {
1533
- return { valid: false, error: "API key cannot be empty" };
1534
- }
1535
- if (provider === "anthropic") {
1536
- if (!key.startsWith("sk-ant-")) {
1537
- return { valid: false, error: "Anthropic API keys should start with 'sk-ant-'" };
1538
- }
1539
- } else if (provider === "openai") {
1540
- if (!key.startsWith("sk-")) {
1541
- return { valid: false, error: "OpenAI API keys should start with 'sk-'" };
1542
- }
1543
- }
1544
- return { valid: true };
1545
- }
1546
- function getConfigPath() {
1547
- return CONFIG_FILE;
1548
- }
1549
- var CONFIG_DIR, CONFIG_FILE, ConfigSchema;
1550
- var init_config = __esm({
1551
- "src/cli/config.ts"() {
1552
- CONFIG_DIR = join(homedir(), ".pinata");
1553
- CONFIG_FILE = join(CONFIG_DIR, "config.json");
1554
- ConfigSchema = z.object({
1555
- anthropicApiKey: z.string().optional(),
1556
- openaiApiKey: z.string().optional(),
1557
- defaultProvider: z.enum(["anthropic", "openai"]).optional(),
1558
- telemetry: z.boolean().optional()
1559
- });
1560
- }
1561
- });
1562
1562
  var RiskDomainSchema = z.enum([
1563
1563
  "security",
1564
1564
  "data",
@@ -3576,7 +3576,7 @@ function createScanner(categoryStore) {
3576
3576
  init_types();
3577
3577
 
3578
3578
  // src/core/index.ts
3579
- var VERSION = "0.2.2";
3579
+ var VERSION = "0.2.3";
3580
3580
 
3581
3581
  // src/lib/index.ts
3582
3582
  init_errors();
@@ -5974,42 +5974,78 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
5974
5974
  spinner?.stop();
5975
5975
  const shouldVerify = Boolean(options["verify"]);
5976
5976
  if (shouldVerify && scanResult.data.gaps.length > 0) {
5977
- const verifySpinner = showSpinner ? ora("Verifying gaps with AI...").start() : null;
5978
- try {
5979
- const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
5980
- const { readFile: readFile5 } = await import('fs/promises');
5981
- const verifier = new AIVerifier2({ provider: "anthropic" });
5982
- const { verified, dismissed, stats } = await verifier.verifyAll(
5983
- scanResult.data.gaps,
5984
- async (path2) => readFile5(path2, "utf-8")
5985
- );
5986
- scanResult.data.gaps = verified;
5987
- const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
5988
- let deduction = 0;
5989
- for (const gap of verified) {
5990
- deduction += severityWeights[gap.severity] ?? 1;
5977
+ const { hasApiKey: hasApiKey2, setConfigValue: setConfigValue2, getApiKey: getApiKey2 } = await Promise.resolve().then(() => (init_config(), config_exports));
5978
+ const { createInterface } = await import('readline');
5979
+ let provider = "anthropic";
5980
+ if (!hasApiKey2("anthropic") && !hasApiKey2("openai")) {
5981
+ spinner?.stop();
5982
+ console.log(chalk5.yellow("\nAI verification requires an API key."));
5983
+ console.log(chalk5.gray("Get one at: https://console.anthropic.com/settings/keys"));
5984
+ console.log(chalk5.gray("Or: https://platform.openai.com/api-keys\n"));
5985
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
5986
+ const askQuestion = (question) => {
5987
+ return new Promise((resolve6) => {
5988
+ rl.question(question, (answer) => resolve6(answer.trim()));
5989
+ });
5990
+ };
5991
+ const apiKey = await askQuestion(chalk5.cyan("Enter your Anthropic or OpenAI API key: "));
5992
+ rl.close();
5993
+ if (!apiKey) {
5994
+ console.log(chalk5.red("No API key provided. Skipping AI verification."));
5995
+ } else {
5996
+ if (apiKey.startsWith("sk-ant-")) {
5997
+ setConfigValue2("anthropicApiKey", apiKey);
5998
+ provider = "anthropic";
5999
+ console.log(chalk5.green("Anthropic API key saved to ~/.pinata/config.json\n"));
6000
+ } else {
6001
+ setConfigValue2("openaiApiKey", apiKey);
6002
+ provider = "openai";
6003
+ console.log(chalk5.green("OpenAI API key saved to ~/.pinata/config.json\n"));
6004
+ }
5991
6005
  }
5992
- const newOverall = Math.max(0, 100 - deduction);
5993
- const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
5994
- scanResult.data.score.overall = newOverall;
5995
- scanResult.data.score.grade = newGrade;
5996
- verifySpinner?.succeed(
5997
- `AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
5998
- );
5999
- if (isVerbose && dismissed.length > 0) {
6000
- console.log(chalk5.gray("\nDismissed as false positives:"));
6001
- for (const { gap, reason } of dismissed.slice(0, 5)) {
6002
- console.log(chalk5.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
6003
- console.log(chalk5.gray(` Reason: ${reason.slice(0, 100)}...`));
6006
+ } else if (hasApiKey2("openai") && !hasApiKey2("anthropic")) {
6007
+ provider = "openai";
6008
+ }
6009
+ if (!hasApiKey2(provider)) {
6010
+ } else {
6011
+ const verifySpinner = showSpinner ? ora("Verifying gaps with AI...").start() : null;
6012
+ try {
6013
+ const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
6014
+ const { readFile: readFile5 } = await import('fs/promises');
6015
+ const apiKey = getApiKey2(provider);
6016
+ const verifier = new AIVerifier2({ provider, ...apiKey ? { apiKey } : {} });
6017
+ const { verified, dismissed, stats } = await verifier.verifyAll(
6018
+ scanResult.data.gaps,
6019
+ async (path2) => readFile5(path2, "utf-8")
6020
+ );
6021
+ scanResult.data.gaps = verified;
6022
+ const severityWeights = { critical: 10, high: 5, medium: 2, low: 1 };
6023
+ let deduction = 0;
6024
+ for (const gap of verified) {
6025
+ deduction += severityWeights[gap.severity] ?? 1;
6004
6026
  }
6005
- if (dismissed.length > 5) {
6006
- console.log(chalk5.gray(` ... and ${dismissed.length - 5} more`));
6027
+ const newOverall = Math.max(0, 100 - deduction);
6028
+ const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
6029
+ scanResult.data.score.overall = newOverall;
6030
+ scanResult.data.score.grade = newGrade;
6031
+ verifySpinner?.succeed(
6032
+ `AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
6033
+ );
6034
+ if (isVerbose && dismissed.length > 0) {
6035
+ console.log(chalk5.gray("\nDismissed as false positives:"));
6036
+ for (const { gap, reason } of dismissed.slice(0, 5)) {
6037
+ console.log(chalk5.gray(` - ${gap.categoryName} at ${gap.filePath}:${gap.lineStart}`));
6038
+ console.log(chalk5.gray(` Reason: ${reason.slice(0, 100)}...`));
6039
+ }
6040
+ if (dismissed.length > 5) {
6041
+ console.log(chalk5.gray(` ... and ${dismissed.length - 5} more`));
6042
+ }
6043
+ }
6044
+ } catch (error) {
6045
+ verifySpinner?.fail("AI verification failed (results unverified)");
6046
+ if (isVerbose) {
6047
+ console.error(chalk5.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
6007
6048
  }
6008
- }
6009
- } catch (error) {
6010
- verifySpinner?.fail("AI verification failed (results unverified)");
6011
- if (isVerbose) {
6012
- console.error(chalk5.yellow(`Verification error: ${error instanceof Error ? error.message : String(error)}`));
6013
6049
  }
6014
6050
  }
6015
6051
  }