pinata-security-cli 0.2.0 → 0.2.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/dist/cli/index.js +275 -130
- 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 +2 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
-
import fs, { mkdir, writeFile, readFile, readdir, stat } from 'fs/promises';
|
|
3
|
+
import fs, { mkdir, writeFile, readFile, stat, readdir } from 'fs/promises';
|
|
5
4
|
import path, { dirname, resolve, basename, relative, join, extname } from 'path';
|
|
6
5
|
import { useState } from 'react';
|
|
7
6
|
import { render, useApp, useInput, Box, Text } from 'ink';
|
|
8
7
|
import Spinner from 'ink-spinner';
|
|
9
8
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
9
|
+
import { existsSync, readFileSync, writeFileSync, chmodSync, mkdirSync } from 'fs';
|
|
10
10
|
import { homedir } from 'os';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
12
|
import chalk5 from 'chalk';
|
|
@@ -160,9 +160,7 @@ async function saveScanResults(projectRoot, result) {
|
|
|
160
160
|
try {
|
|
161
161
|
const cacheDir = resolve(projectRoot, CACHE_DIR);
|
|
162
162
|
const cachePath = getCachePath(projectRoot);
|
|
163
|
-
|
|
164
|
-
await mkdir(cacheDir, { recursive: true });
|
|
165
|
-
}
|
|
163
|
+
await mkdir(cacheDir, { recursive: true });
|
|
166
164
|
const cached = {
|
|
167
165
|
timestamp: result.completedAt.toISOString(),
|
|
168
166
|
targetDirectory: result.targetDirectory,
|
|
@@ -187,7 +185,10 @@ async function saveScanResults(projectRoot, result) {
|
|
|
187
185
|
async function loadScanResults(projectRoot) {
|
|
188
186
|
try {
|
|
189
187
|
const cachePath = getCachePath(projectRoot);
|
|
190
|
-
|
|
188
|
+
let content;
|
|
189
|
+
try {
|
|
190
|
+
content = await readFile(cachePath, "utf-8");
|
|
191
|
+
} catch {
|
|
191
192
|
return err(
|
|
192
193
|
new PinataError(
|
|
193
194
|
"No cached scan results found. Run `pinata analyze` first.",
|
|
@@ -195,7 +196,6 @@ async function loadScanResults(projectRoot) {
|
|
|
195
196
|
)
|
|
196
197
|
);
|
|
197
198
|
}
|
|
198
|
-
const content = await readFile(cachePath, "utf-8");
|
|
199
199
|
const cached = JSON.parse(content);
|
|
200
200
|
if (cached.version !== CACHE_VERSION) {
|
|
201
201
|
return err(
|
|
@@ -743,97 +743,233 @@ var init_junit_formatter = __esm({
|
|
|
743
743
|
});
|
|
744
744
|
|
|
745
745
|
// src/core/verifier/ai-verifier.ts
|
|
746
|
-
var
|
|
746
|
+
var SKIP_PATTERNS, BATCH_PROMPT, SINGLE_ITEM_TEMPLATE, AIVerifier;
|
|
747
747
|
var init_ai_verifier = __esm({
|
|
748
748
|
"src/core/verifier/ai-verifier.ts"() {
|
|
749
749
|
init_types();
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
750
|
+
SKIP_PATTERNS = {
|
|
751
|
+
paths: [
|
|
752
|
+
/\.test\.(ts|js|tsx|jsx)$/,
|
|
753
|
+
/\.spec\.(ts|js|tsx|jsx)$/,
|
|
754
|
+
/tests?\//i,
|
|
755
|
+
/fixtures?\//i,
|
|
756
|
+
/mocks?\//i,
|
|
757
|
+
/__tests__\//,
|
|
758
|
+
/node_modules\//,
|
|
759
|
+
/dist\//,
|
|
760
|
+
/\.d\.ts$/,
|
|
761
|
+
/examples?\//i
|
|
762
|
+
],
|
|
763
|
+
// Content patterns that indicate false positive
|
|
764
|
+
content: [
|
|
765
|
+
/\/\/ SAFE:/i,
|
|
766
|
+
// Explicit safe marker
|
|
767
|
+
/\/\/ nosec/i,
|
|
768
|
+
// Security ignore
|
|
769
|
+
/eslint-disable/i,
|
|
770
|
+
/sanitized?|escaped?|validated?/i
|
|
771
|
+
// Near sanitization
|
|
772
|
+
]
|
|
773
|
+
};
|
|
774
|
+
BATCH_PROMPT = `You are a security code reviewer. Analyze these potential vulnerabilities and determine which are real issues vs false positives.
|
|
774
775
|
|
|
775
|
-
|
|
776
|
+
For each item, consider:
|
|
776
777
|
- Is user input actually reaching this code?
|
|
777
778
|
- Is there sanitization, validation, or encoding nearby?
|
|
778
779
|
- Is this test code, example code, or production code?
|
|
779
780
|
- Is there context that makes this safe?
|
|
780
781
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
782
|
+
Be rigorous. Most pattern matches are false positives.
|
|
783
|
+
|
|
784
|
+
ITEMS TO ANALYZE:
|
|
785
|
+
{{items}}
|
|
786
|
+
|
|
787
|
+
Respond with a JSON array. Each object MUST have these exact fields:
|
|
788
|
+
[
|
|
789
|
+
{
|
|
790
|
+
"id": "1",
|
|
791
|
+
"isVulnerable": true/false,
|
|
792
|
+
"confidence": "high"/"medium"/"low",
|
|
793
|
+
"reasoning": "brief explanation"
|
|
794
|
+
},
|
|
795
|
+
...
|
|
796
|
+
]
|
|
797
|
+
|
|
798
|
+
Only return the JSON array, no other text.`;
|
|
799
|
+
SINGLE_ITEM_TEMPLATE = `
|
|
800
|
+
---
|
|
801
|
+
ID: {{id}}
|
|
802
|
+
CATEGORY: {{category}}
|
|
803
|
+
FILE: {{filePath}}:{{lineNumber}}
|
|
804
|
+
CODE:
|
|
805
|
+
\`\`\`{{language}}
|
|
806
|
+
{{codeContext}}
|
|
807
|
+
\`\`\`
|
|
808
|
+
FLAGGED LINE: {{flaggedLine}}
|
|
809
|
+
---`;
|
|
790
810
|
AIVerifier = class {
|
|
791
811
|
config;
|
|
812
|
+
batchSize;
|
|
813
|
+
concurrency;
|
|
792
814
|
constructor(config2) {
|
|
793
815
|
this.config = config2;
|
|
816
|
+
this.batchSize = config2.batchSize ?? 10;
|
|
817
|
+
this.concurrency = config2.concurrency ?? 3;
|
|
794
818
|
}
|
|
795
819
|
/**
|
|
796
|
-
* Verify
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
return this.parseResponse(response);
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* Verify multiple gaps, filtering out false positives.
|
|
820
|
+
* Verify multiple gaps efficiently using filtering, batching, and parallelism.
|
|
821
|
+
*
|
|
822
|
+
* Flow:
|
|
823
|
+
* 1. Pre-filter obvious false positives (test files, etc.)
|
|
824
|
+
* 2. Group remaining gaps into batches of 10
|
|
825
|
+
* 3. Process 3 batches in parallel
|
|
826
|
+
* 4. Return verified gaps and dismissed with reasons
|
|
807
827
|
*/
|
|
808
828
|
async verifyAll(gaps, getFileContent) {
|
|
809
829
|
const verified = [];
|
|
810
830
|
const dismissed = [];
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
reason: result.reasoning
|
|
823
|
-
});
|
|
831
|
+
const { toVerify, preFiltered } = this.preFilter(gaps);
|
|
832
|
+
dismissed.push(...preFiltered);
|
|
833
|
+
if (toVerify.length === 0) {
|
|
834
|
+
return {
|
|
835
|
+
verified: [],
|
|
836
|
+
dismissed,
|
|
837
|
+
stats: {
|
|
838
|
+
total: gaps.length,
|
|
839
|
+
preFiltered: preFiltered.length,
|
|
840
|
+
aiDismissed: 0,
|
|
841
|
+
aiVerified: 0
|
|
824
842
|
}
|
|
825
|
-
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
console.log(`Pre-filtered ${preFiltered.length} gaps. Verifying ${toVerify.length} with AI...`);
|
|
846
|
+
const fileContents = /* @__PURE__ */ new Map();
|
|
847
|
+
const uniquePaths = [...new Set(toVerify.map((g) => g.filePath))];
|
|
848
|
+
await Promise.all(
|
|
849
|
+
uniquePaths.map(async (path2) => {
|
|
850
|
+
try {
|
|
851
|
+
fileContents.set(path2, await getFileContent(path2));
|
|
852
|
+
} catch {
|
|
853
|
+
fileContents.set(path2, "");
|
|
829
854
|
}
|
|
855
|
+
})
|
|
856
|
+
);
|
|
857
|
+
const batches = this.createBatches(toVerify, fileContents);
|
|
858
|
+
console.log(`Created ${batches.length} batches of ~${this.batchSize} gaps each`);
|
|
859
|
+
const results = await this.processParallel(batches, toVerify);
|
|
860
|
+
let aiVerified = 0;
|
|
861
|
+
let aiDismissed = 0;
|
|
862
|
+
for (const gap of toVerify) {
|
|
863
|
+
const gapId = `${gap.filePath}:${gap.lineStart}`;
|
|
864
|
+
const result = results.get(gapId);
|
|
865
|
+
if (!result || result.isVulnerable) {
|
|
830
866
|
verified.push(gap);
|
|
867
|
+
aiVerified++;
|
|
868
|
+
} else {
|
|
869
|
+
dismissed.push({
|
|
870
|
+
gap,
|
|
871
|
+
reason: result.reasoning
|
|
872
|
+
});
|
|
873
|
+
aiDismissed++;
|
|
831
874
|
}
|
|
832
875
|
}
|
|
833
|
-
|
|
834
|
-
|
|
876
|
+
return {
|
|
877
|
+
verified,
|
|
878
|
+
dismissed,
|
|
879
|
+
stats: {
|
|
880
|
+
total: gaps.length,
|
|
881
|
+
preFiltered: preFiltered.length,
|
|
882
|
+
aiDismissed,
|
|
883
|
+
aiVerified
|
|
884
|
+
}
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* Pre-filter gaps that are obviously false positives without needing AI.
|
|
889
|
+
*/
|
|
890
|
+
preFilter(gaps) {
|
|
891
|
+
const toVerify = [];
|
|
892
|
+
const preFiltered = [];
|
|
893
|
+
for (const gap of gaps) {
|
|
894
|
+
const pathMatch = SKIP_PATTERNS.paths.find((p) => p.test(gap.filePath));
|
|
895
|
+
if (pathMatch) {
|
|
896
|
+
preFiltered.push({
|
|
897
|
+
gap,
|
|
898
|
+
reason: `Skipped: test/example file (${pathMatch.source})`
|
|
899
|
+
});
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (gap.categoryId === "precision-loss" && gap.filePath.endsWith(".ts")) {
|
|
903
|
+
preFiltered.push({
|
|
904
|
+
gap,
|
|
905
|
+
reason: "TypeScript type annotation, not runtime code"
|
|
906
|
+
});
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
toVerify.push(gap);
|
|
910
|
+
}
|
|
911
|
+
return { toVerify, preFiltered };
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Create batches of gaps for batch API calls.
|
|
915
|
+
*/
|
|
916
|
+
createBatches(gaps, fileContents) {
|
|
917
|
+
const batches = [];
|
|
918
|
+
for (let i = 0; i < gaps.length; i += this.batchSize) {
|
|
919
|
+
const batchGaps = gaps.slice(i, i + this.batchSize);
|
|
920
|
+
const items = [];
|
|
921
|
+
const gapIds = [];
|
|
922
|
+
for (let j = 0; j < batchGaps.length; j++) {
|
|
923
|
+
const gap = batchGaps[j];
|
|
924
|
+
const content = fileContents.get(gap.filePath) ?? "";
|
|
925
|
+
const gapId = `${gap.filePath}:${gap.lineStart}`;
|
|
926
|
+
gapIds.push(gapId);
|
|
927
|
+
const codeContext = this.extractContext(content, gap.lineStart, 10);
|
|
928
|
+
const flaggedLine = this.extractLine(content, gap.lineStart);
|
|
929
|
+
items.push(
|
|
930
|
+
SINGLE_ITEM_TEMPLATE.replace("{{id}}", String(j + 1)).replace("{{category}}", gap.categoryName).replace("{{filePath}}", gap.filePath).replace("{{lineNumber}}", String(gap.lineStart)).replace("{{language}}", this.getLanguage(gap.filePath)).replace("{{codeContext}}", codeContext).replace("{{flaggedLine}}", flaggedLine)
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
const prompt = BATCH_PROMPT.replace("{{items}}", items.join("\n"));
|
|
934
|
+
batches.push({ prompt, gapIds });
|
|
835
935
|
}
|
|
836
|
-
return
|
|
936
|
+
return batches;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Process batches in parallel with limited concurrency.
|
|
940
|
+
*/
|
|
941
|
+
async processParallel(batches, gaps) {
|
|
942
|
+
const results = /* @__PURE__ */ new Map();
|
|
943
|
+
let completed = 0;
|
|
944
|
+
for (let i = 0; i < batches.length; i += this.concurrency) {
|
|
945
|
+
const wave = batches.slice(i, i + this.concurrency);
|
|
946
|
+
const waveResults = await Promise.all(
|
|
947
|
+
wave.map(async (batch) => {
|
|
948
|
+
try {
|
|
949
|
+
const response = await this.callAI(batch.prompt);
|
|
950
|
+
return { gapIds: batch.gapIds, response };
|
|
951
|
+
} catch (error) {
|
|
952
|
+
console.error(`Batch failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
953
|
+
return { gapIds: batch.gapIds, response: null };
|
|
954
|
+
}
|
|
955
|
+
})
|
|
956
|
+
);
|
|
957
|
+
for (const { gapIds, response } of waveResults) {
|
|
958
|
+
if (response) {
|
|
959
|
+
const parsed = this.parseBatchResponse(response);
|
|
960
|
+
for (let j = 0; j < gapIds.length && j < parsed.length; j++) {
|
|
961
|
+
const gapId = gapIds[j];
|
|
962
|
+
const result = parsed[j];
|
|
963
|
+
if (result) {
|
|
964
|
+
results.set(gapId, result);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
completed += wave.length;
|
|
970
|
+
console.log(`Processed ${completed}/${batches.length} batches...`);
|
|
971
|
+
}
|
|
972
|
+
return results;
|
|
837
973
|
}
|
|
838
974
|
extractContext(content, lineNumber, radius) {
|
|
839
975
|
const lines = content.split("\n");
|
|
@@ -842,9 +978,13 @@ Respond in JSON format:
|
|
|
842
978
|
return lines.slice(start, end).map((line, i) => {
|
|
843
979
|
const num = start + i + 1;
|
|
844
980
|
const marker = num === lineNumber ? ">" : " ";
|
|
845
|
-
return `${marker}
|
|
981
|
+
return `${marker}${num.toString().padStart(4)}| ${line}`;
|
|
846
982
|
}).join("\n");
|
|
847
983
|
}
|
|
984
|
+
extractLine(content, lineNumber) {
|
|
985
|
+
const lines = content.split("\n");
|
|
986
|
+
return lines[lineNumber - 1] ?? "";
|
|
987
|
+
}
|
|
848
988
|
getLanguage(filePath) {
|
|
849
989
|
if (filePath.endsWith(".ts") || filePath.endsWith(".tsx")) return "typescript";
|
|
850
990
|
if (filePath.endsWith(".js") || filePath.endsWith(".jsx")) return "javascript";
|
|
@@ -852,21 +992,6 @@ Respond in JSON format:
|
|
|
852
992
|
if (filePath.endsWith(".go")) return "go";
|
|
853
993
|
return "text";
|
|
854
994
|
}
|
|
855
|
-
getCategoryDescription(categoryId) {
|
|
856
|
-
const descriptions = {
|
|
857
|
-
"sql-injection": "SQL queries built with string concatenation allowing attackers to inject malicious SQL",
|
|
858
|
-
"xss": "User input rendered in HTML without sanitization, allowing script injection",
|
|
859
|
-
"command-injection": "Shell commands built with user input, allowing arbitrary command execution",
|
|
860
|
-
"path-traversal": "File paths built with user input, allowing access to files outside intended directory",
|
|
861
|
-
"hardcoded-secrets": "API keys, passwords, or tokens embedded in source code",
|
|
862
|
-
"timing-attack": "Non-constant-time comparison of secrets, leaking information via timing",
|
|
863
|
-
"memory-bloat": "Unbounded memory growth from accumulating data or inefficient patterns",
|
|
864
|
-
"precision-loss": "Floating-point arithmetic for currency causing rounding errors",
|
|
865
|
-
"ssrf": "Server-side requests with user-controlled URLs, allowing internal network access",
|
|
866
|
-
"deserialization": "Deserializing untrusted data, potentially leading to code execution"
|
|
867
|
-
};
|
|
868
|
-
return descriptions[categoryId] ?? "Potential security or reliability issue";
|
|
869
|
-
}
|
|
870
995
|
async callAI(prompt) {
|
|
871
996
|
const apiKey = this.config.apiKey ?? this.getApiKeyFromEnv();
|
|
872
997
|
if (!apiKey) {
|
|
@@ -880,7 +1005,7 @@ Respond in JSON format:
|
|
|
880
1005
|
}
|
|
881
1006
|
async callAnthropic(prompt, apiKey) {
|
|
882
1007
|
const controller = new AbortController();
|
|
883
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
1008
|
+
const timeout = setTimeout(() => controller.abort(), 6e4);
|
|
884
1009
|
try {
|
|
885
1010
|
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
886
1011
|
method: "POST",
|
|
@@ -891,13 +1016,15 @@ Respond in JSON format:
|
|
|
891
1016
|
},
|
|
892
1017
|
body: JSON.stringify({
|
|
893
1018
|
model: this.config.model ?? "claude-sonnet-4-20250514",
|
|
894
|
-
max_tokens:
|
|
1019
|
+
max_tokens: 4096,
|
|
1020
|
+
// Larger for batch responses
|
|
895
1021
|
messages: [{ role: "user", content: prompt }]
|
|
896
1022
|
}),
|
|
897
1023
|
signal: controller.signal
|
|
898
1024
|
});
|
|
899
1025
|
if (!response.ok) {
|
|
900
|
-
|
|
1026
|
+
const body = await response.text();
|
|
1027
|
+
throw new Error(`Anthropic API error: ${response.status} - ${body}`);
|
|
901
1028
|
}
|
|
902
1029
|
const data = await response.json();
|
|
903
1030
|
return data.content[0]?.text ?? "";
|
|
@@ -907,7 +1034,7 @@ Respond in JSON format:
|
|
|
907
1034
|
}
|
|
908
1035
|
async callOpenAI(prompt, apiKey) {
|
|
909
1036
|
const controller = new AbortController();
|
|
910
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
1037
|
+
const timeout = setTimeout(() => controller.abort(), 6e4);
|
|
911
1038
|
try {
|
|
912
1039
|
const response = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
913
1040
|
method: "POST",
|
|
@@ -918,12 +1045,13 @@ Respond in JSON format:
|
|
|
918
1045
|
body: JSON.stringify({
|
|
919
1046
|
model: this.config.model ?? "gpt-4o",
|
|
920
1047
|
messages: [{ role: "user", content: prompt }],
|
|
921
|
-
max_tokens:
|
|
1048
|
+
max_tokens: 4096
|
|
922
1049
|
}),
|
|
923
1050
|
signal: controller.signal
|
|
924
1051
|
});
|
|
925
1052
|
if (!response.ok) {
|
|
926
|
-
|
|
1053
|
+
const body = await response.text();
|
|
1054
|
+
throw new Error(`OpenAI API error: ${response.status} - ${body}`);
|
|
927
1055
|
}
|
|
928
1056
|
const data = await response.json();
|
|
929
1057
|
return data.choices[0]?.message?.content ?? "";
|
|
@@ -937,31 +1065,49 @@ Respond in JSON format:
|
|
|
937
1065
|
}
|
|
938
1066
|
return process.env["OPENAI_API_KEY"] ?? "";
|
|
939
1067
|
}
|
|
940
|
-
|
|
1068
|
+
parseBatchResponse(response) {
|
|
941
1069
|
try {
|
|
942
|
-
const jsonMatch = response.match(/\
|
|
1070
|
+
const jsonMatch = response.match(/\[[\s\S]*\]/);
|
|
943
1071
|
if (!jsonMatch) {
|
|
944
|
-
|
|
1072
|
+
console.error("No JSON array found in batch response");
|
|
1073
|
+
return [];
|
|
945
1074
|
}
|
|
946
1075
|
const parsed = JSON.parse(jsonMatch[0]);
|
|
947
|
-
return {
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
};
|
|
955
|
-
|
|
1076
|
+
return parsed.map((item) => ({
|
|
1077
|
+
id: String(item.id),
|
|
1078
|
+
isVulnerable: Boolean(item.isVulnerable),
|
|
1079
|
+
confidence: item.confidence ?? "medium",
|
|
1080
|
+
reasoning: item.reasoning ?? "No reasoning provided"
|
|
1081
|
+
}));
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
console.error(`Failed to parse batch response: ${error instanceof Error ? error.message : String(error)}`);
|
|
1084
|
+
return [];
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/**
|
|
1088
|
+
* Legacy single-gap verification (kept for backwards compatibility).
|
|
1089
|
+
*/
|
|
1090
|
+
async verify(gap, fileContent) {
|
|
1091
|
+
const result = await this.verifyAll([gap], async () => fileContent);
|
|
1092
|
+
if (result.verified.length > 0) {
|
|
956
1093
|
return {
|
|
957
1094
|
isVulnerable: true,
|
|
958
|
-
confidence: "
|
|
959
|
-
reasoning: "AI
|
|
1095
|
+
confidence: "high",
|
|
1096
|
+
reasoning: "AI confirmed vulnerability",
|
|
960
1097
|
mitigatingFactors: [],
|
|
961
1098
|
exploitScenario: null,
|
|
962
|
-
recommendation: "
|
|
1099
|
+
recommendation: "Fix this issue"
|
|
963
1100
|
};
|
|
964
1101
|
}
|
|
1102
|
+
const dismissal = result.dismissed[0];
|
|
1103
|
+
return {
|
|
1104
|
+
isVulnerable: false,
|
|
1105
|
+
confidence: "high",
|
|
1106
|
+
reasoning: dismissal?.reason ?? "AI dismissed as false positive",
|
|
1107
|
+
mitigatingFactors: [],
|
|
1108
|
+
exploitScenario: null,
|
|
1109
|
+
recommendation: "No action needed"
|
|
1110
|
+
};
|
|
965
1111
|
}
|
|
966
1112
|
};
|
|
967
1113
|
}
|
|
@@ -1327,15 +1473,10 @@ __export(config_exports, {
|
|
|
1327
1473
|
validateApiKey: () => validateApiKey
|
|
1328
1474
|
});
|
|
1329
1475
|
function ensureConfigDir() {
|
|
1330
|
-
|
|
1331
|
-
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
1332
|
-
}
|
|
1476
|
+
mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
1333
1477
|
}
|
|
1334
1478
|
function loadConfig() {
|
|
1335
1479
|
try {
|
|
1336
|
-
if (!existsSync(CONFIG_FILE)) {
|
|
1337
|
-
return {};
|
|
1338
|
-
}
|
|
1339
1480
|
const content = readFileSync(CONFIG_FILE, "utf-8");
|
|
1340
1481
|
const parsed = JSON.parse(content);
|
|
1341
1482
|
const result = ConfigSchema.safeParse(parsed);
|
|
@@ -2896,7 +3037,12 @@ var Scanner = class {
|
|
|
2896
3037
|
const startedAt = /* @__PURE__ */ new Date();
|
|
2897
3038
|
const opts = this.mergeOptions(targetDirectory, options);
|
|
2898
3039
|
this.log.info(`Starting scan of ${targetDirectory}`);
|
|
2899
|
-
|
|
3040
|
+
try {
|
|
3041
|
+
const dirStat = await stat(targetDirectory);
|
|
3042
|
+
if (!dirStat.isDirectory()) {
|
|
3043
|
+
return err(new AnalysisError(`Not a directory: ${targetDirectory}`));
|
|
3044
|
+
}
|
|
3045
|
+
} catch {
|
|
2900
3046
|
return err(new AnalysisError(`Directory not found: ${targetDirectory}`));
|
|
2901
3047
|
}
|
|
2902
3048
|
const categoriesResult = this.getCategoriesToScan(opts);
|
|
@@ -3092,9 +3238,6 @@ var Scanner = class {
|
|
|
3092
3238
|
*/
|
|
3093
3239
|
readPinataIgnore(targetDirectory) {
|
|
3094
3240
|
const ignorePath = resolve(targetDirectory, ".pinataignore");
|
|
3095
|
-
if (!existsSync(ignorePath)) {
|
|
3096
|
-
return [];
|
|
3097
|
-
}
|
|
3098
3241
|
try {
|
|
3099
3242
|
const { readFileSync: readFileSync2 } = __require("fs");
|
|
3100
3243
|
const content = readFileSync2(ignorePath, "utf-8");
|
|
@@ -3433,7 +3576,7 @@ function createScanner(categoryStore) {
|
|
|
3433
3576
|
init_types();
|
|
3434
3577
|
|
|
3435
3578
|
// src/core/index.ts
|
|
3436
|
-
var VERSION = "0.2.
|
|
3579
|
+
var VERSION = "0.2.1";
|
|
3437
3580
|
|
|
3438
3581
|
// src/lib/index.ts
|
|
3439
3582
|
init_errors();
|
|
@@ -4354,11 +4497,11 @@ var AIService = class {
|
|
|
4354
4497
|
*/
|
|
4355
4498
|
getApiKeyFromConfig(provider) {
|
|
4356
4499
|
try {
|
|
4357
|
-
const { existsSync:
|
|
4500
|
+
const { existsSync: existsSync4, readFileSync: readFileSync2 } = __require("fs");
|
|
4358
4501
|
const { homedir: homedir2 } = __require("os");
|
|
4359
4502
|
const { join: join3 } = __require("path");
|
|
4360
4503
|
const configPath = join3(homedir2(), ".pinata", "config.json");
|
|
4361
|
-
if (!
|
|
4504
|
+
if (!existsSync4(configPath)) {
|
|
4362
4505
|
return "";
|
|
4363
4506
|
}
|
|
4364
4507
|
const content = readFileSync2(configPath, "utf-8");
|
|
@@ -5005,13 +5148,13 @@ async function writeGeneratedTests(tests, basePath, outputDir) {
|
|
|
5005
5148
|
for (const [outputPath, fileTests] of testsByFile) {
|
|
5006
5149
|
try {
|
|
5007
5150
|
const dir = dirname(outputPath);
|
|
5008
|
-
|
|
5009
|
-
await mkdir(dir, { recursive: true });
|
|
5010
|
-
}
|
|
5011
|
-
const fileExists = existsSync(outputPath);
|
|
5151
|
+
await mkdir(dir, { recursive: true });
|
|
5012
5152
|
let existingContent = "";
|
|
5013
|
-
|
|
5153
|
+
let fileExists = false;
|
|
5154
|
+
try {
|
|
5014
5155
|
existingContent = await readFile(outputPath, "utf-8");
|
|
5156
|
+
fileExists = true;
|
|
5157
|
+
} catch {
|
|
5015
5158
|
}
|
|
5016
5159
|
const contentParts = [];
|
|
5017
5160
|
const allImports = /* @__PURE__ */ new Set();
|
|
@@ -5836,7 +5979,7 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
|
|
|
5836
5979
|
const { AIVerifier: AIVerifier2 } = await Promise.resolve().then(() => (init_verifier(), verifier_exports));
|
|
5837
5980
|
const { readFile: readFile5 } = await import('fs/promises');
|
|
5838
5981
|
const verifier = new AIVerifier2({ provider: "anthropic" });
|
|
5839
|
-
const { verified, dismissed } = await verifier.verifyAll(
|
|
5982
|
+
const { verified, dismissed, stats } = await verifier.verifyAll(
|
|
5840
5983
|
scanResult.data.gaps,
|
|
5841
5984
|
async (path2) => readFile5(path2, "utf-8")
|
|
5842
5985
|
);
|
|
@@ -5850,7 +5993,9 @@ program.command("analyze [path]").description("Analyze codebase for test coverag
|
|
|
5850
5993
|
const newGrade = newOverall >= 90 ? "A" : newOverall >= 80 ? "B" : newOverall >= 70 ? "C" : newOverall >= 60 ? "D" : "F";
|
|
5851
5994
|
scanResult.data.score.overall = newOverall;
|
|
5852
5995
|
scanResult.data.score.grade = newGrade;
|
|
5853
|
-
verifySpinner?.succeed(
|
|
5996
|
+
verifySpinner?.succeed(
|
|
5997
|
+
`AI Verification: ${stats.total} total \u2192 ${stats.preFiltered} pre-filtered \u2192 ${stats.aiVerified} verified, ${stats.aiDismissed} AI-dismissed`
|
|
5998
|
+
);
|
|
5854
5999
|
if (isVerbose && dismissed.length > 0) {
|
|
5855
6000
|
console.log(chalk5.gray("\nDismissed as false positives:"));
|
|
5856
6001
|
for (const { gap, reason } of dismissed.slice(0, 5)) {
|