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 +176 -140
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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,
|
|
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.
|
|
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
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
);
|
|
5986
|
-
|
|
5987
|
-
|
|
5988
|
-
|
|
5989
|
-
|
|
5990
|
-
|
|
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
|
-
|
|
5993
|
-
|
|
5994
|
-
|
|
5995
|
-
|
|
5996
|
-
|
|
5997
|
-
|
|
5998
|
-
|
|
5999
|
-
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
|
|
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
|
-
|
|
6006
|
-
|
|
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
|
}
|