securevibe 0.1.8 → 0.1.10
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 +15 -5
- package/dist/engine/explain.js +79 -0
- package/dist/engine/fix/llm.js +21 -9
- package/dist/engine/init.js +37 -0
- package/dist/index-support.js +29 -0
- package/dist/index.js +32 -0
- package/dist/repl.js +25 -0
- package/dist/ui/explain.js +72 -0
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,6 +27,7 @@ midnight). The `--staged` commit guard is never limited. See [`LICENSE`](LICENSE
|
|
|
27
27
|
| `securevibe scan [path]` | Full scan → findings + security score (default command) |
|
|
28
28
|
| `securevibe init [path]` | **Wire SecureVibe in: pre-commit guard + GitHub Action + gitignore** (doc 12) |
|
|
29
29
|
| `securevibe fix [path]` | **Autonomously fix findings, then re-scan to verify** (doc 05) |
|
|
30
|
+
| `securevibe explain [path]` | AI-powered plain-language explanations for critical/high findings |
|
|
30
31
|
| `securevibe deps [path]` | Audit dependencies for known CVEs against the local OSV database (SCA) |
|
|
31
32
|
| `securevibe db <update\|status>` | Manage the local OSV database — `update` is the only networked command |
|
|
32
33
|
| `securevibe ready [path]` | Launch readiness scorecard: pass/fail gates + go/no-go verdict |
|
|
@@ -78,11 +79,20 @@ Anything that fails the gate is rolled back and surfaced as a manual instruction
|
|
|
78
79
|
is kept **only if it passes the verification gate**, otherwise it falls back to a precise manual
|
|
79
80
|
instruction. Groq is used by default when both keys are present (lower bar to try it at all);
|
|
80
81
|
set `SECUREVIBE_LLM_PROVIDER=anthropic` to force Claude instead.
|
|
81
|
-
- **Setting a key:**
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
- **Setting a key:** `securevibe init` is the primary path — if it detects an LLM SDK dependency
|
|
83
|
+
(openai, `@anthropic-ai/sdk`, langchain, ...) and no key is already set, it offers to set one up
|
|
84
|
+
on the spot, once per project (declining is remembered so it won't ask again). You can also
|
|
85
|
+
export a key yourself (`GROQ_API_KEY=...`), or run `securevibe config set-key groq` any time — it
|
|
86
|
+
prompts interactively (never as a command argument, so it never lands in shell history) and saves
|
|
87
|
+
it to `~/.securevibe/config.json`, used automatically whenever the env var isn't already set.
|
|
88
|
+
- **A second AI feature, `securevibe explain`:** for critical/high findings, asks
|
|
89
|
+
the same configured provider for a plain-language explanation grounded in the
|
|
90
|
+
actual flagged code (not just the generic why/fix text every finding of that
|
|
91
|
+
detector shares). Capped at the top 10 qualifying findings per run. Falls back to
|
|
92
|
+
the static why/fix text with no key configured, or if a call fails — an
|
|
93
|
+
explanation is always additional context, never required to use the command.
|
|
94
|
+
`fix --apply` with no key configured still offers to set one up as a fallback.
|
|
95
|
+
`securevibe config show` lists what's stored (masked).
|
|
86
96
|
|
|
87
97
|
**Honesty (read this):**
|
|
88
98
|
- A "fixed" finding means *our detector no longer flags it* — not that the app is proven secure.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `securevibe explain` (AI feature #2, 2026-07-10). Turns a critical/high
|
|
3
|
+
* finding's generic why/fix text into a plain-language explanation grounded
|
|
4
|
+
* in the actual flagged code. Read-only — no files are modified, so there is
|
|
5
|
+
* no verification/rollback gate the way `fix` needs one.
|
|
6
|
+
*
|
|
7
|
+
* Capped at EXPLAIN_MAX_FINDINGS per run to bound worst-case AI cost/latency:
|
|
8
|
+
* a scan with dozens of criticals should never turn into dozens of API calls
|
|
9
|
+
* without the caller asking for it. Degrades gracefully with no provider
|
|
10
|
+
* configured (or a failed call) — every finding still gets a result, with
|
|
11
|
+
* explanation: null, so the UI layer falls back to the static why/fix text.
|
|
12
|
+
*/
|
|
13
|
+
import { promises as fs } from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { SEVERITY_ORDER } from "./types.js";
|
|
16
|
+
import { llmChat, llmAvailability } from "./fix/llm.js";
|
|
17
|
+
export const EXPLAIN_MAX_FINDINGS = 10;
|
|
18
|
+
const EXPLAIN_SEVERITIES = new Set(["critical", "high"]);
|
|
19
|
+
const SNIPPET_CONTEXT_LINES = 4;
|
|
20
|
+
const EXPLAIN_SYSTEM = [
|
|
21
|
+
"You are a senior application-security engineer explaining one specific vulnerability finding to the developer who wrote the code.",
|
|
22
|
+
"You will receive the finding's detector, severity, generic why/fix guidance, and a snippet of the actual flagged code (the vulnerable line is marked with >>).",
|
|
23
|
+
"Write a short plain-language explanation (3-5 sentences) of the concrete risk IN THIS CODE specifically — reference the actual variable, function, or endpoint names visible in the snippet, not generic advice.",
|
|
24
|
+
"Do not repeat the generic why/fix text verbatim. Do not add a heading, bullet list, or markdown formatting. Return prose only.",
|
|
25
|
+
].join("\n");
|
|
26
|
+
function explainUserPrompt(finding, snippet) {
|
|
27
|
+
return [
|
|
28
|
+
`Finding: ${finding.title}`,
|
|
29
|
+
`Detector: ${finding.detector} Severity: ${finding.severity}${finding.cwe ? ` ${finding.cwe}` : ""}`,
|
|
30
|
+
`File: ${finding.file}:${finding.line}`,
|
|
31
|
+
"",
|
|
32
|
+
"Generic why:",
|
|
33
|
+
finding.why,
|
|
34
|
+
"",
|
|
35
|
+
"Generic fix:",
|
|
36
|
+
finding.fix,
|
|
37
|
+
"",
|
|
38
|
+
"Code snippet (flagged line marked with >>):",
|
|
39
|
+
snippet,
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
42
|
+
/** A fixed window of lines around `line` (1-indexed), clamped to file bounds. */
|
|
43
|
+
export function extractSnippet(code, line) {
|
|
44
|
+
const lines = code.split(/\r?\n/);
|
|
45
|
+
const idx = Math.min(Math.max(line - 1, 0), lines.length - 1);
|
|
46
|
+
const start = Math.max(0, idx - SNIPPET_CONTEXT_LINES);
|
|
47
|
+
const end = Math.min(lines.length, idx + SNIPPET_CONTEXT_LINES + 1);
|
|
48
|
+
const out = [];
|
|
49
|
+
for (let i = start; i < end; i++) {
|
|
50
|
+
out.push(`${i === idx ? ">>" : " "} ${i + 1}: ${lines[i]}`);
|
|
51
|
+
}
|
|
52
|
+
return out.join("\n");
|
|
53
|
+
}
|
|
54
|
+
export async function explainFindings(scanResult, opts = {}) {
|
|
55
|
+
const qualifying = scanResult.findings
|
|
56
|
+
.filter((f) => EXPLAIN_SEVERITIES.has(f.severity))
|
|
57
|
+
.sort((a, b) => SEVERITY_ORDER[b.severity] - SEVERITY_ORDER[a.severity] || b.confidence - a.confidence);
|
|
58
|
+
const capped = qualifying.slice(0, EXPLAIN_MAX_FINDINGS);
|
|
59
|
+
const truncatedCount = qualifying.length - capped.length;
|
|
60
|
+
const avail = llmAvailability(false);
|
|
61
|
+
const results = [];
|
|
62
|
+
for (const finding of capped) {
|
|
63
|
+
opts.onProgress?.(`Explaining ${finding.file}:${finding.line}…`);
|
|
64
|
+
let explanation = null;
|
|
65
|
+
if (avail.available) {
|
|
66
|
+
let snippet;
|
|
67
|
+
try {
|
|
68
|
+
const code = await fs.readFile(path.join(scanResult.root, finding.file), "utf8");
|
|
69
|
+
snippet = extractSnippet(code, finding.line);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
snippet = "(source unavailable)";
|
|
73
|
+
}
|
|
74
|
+
explanation = await llmChat(EXPLAIN_SYSTEM, explainUserPrompt(finding, snippet));
|
|
75
|
+
}
|
|
76
|
+
results.push({ finding, explanation });
|
|
77
|
+
}
|
|
78
|
+
return { root: scanResult.root, results, truncatedCount, llmAvailable: avail.available };
|
|
79
|
+
}
|
package/dist/engine/fix/llm.js
CHANGED
|
@@ -94,18 +94,30 @@ function userPrompt(input) {
|
|
|
94
94
|
export async function llmFix(input) {
|
|
95
95
|
if (input.code.length > MAX_FILE_CHARS)
|
|
96
96
|
return null;
|
|
97
|
+
const text = await llmChat(SYSTEM, userPrompt(input));
|
|
98
|
+
if (text == null)
|
|
99
|
+
return null;
|
|
100
|
+
return parseFile(text, input.code);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generic single-turn chat call against whichever provider is configured.
|
|
104
|
+
* Shared by llmFix (full-file rewrites) and engine/explain.ts (plain-language
|
|
105
|
+
* finding explanations) so the Groq/Anthropic HTTP plumbing lives in one place.
|
|
106
|
+
* Returns null on no-provider / network / auth / quota failure — callers degrade
|
|
107
|
+
* gracefully and must never throw because of this.
|
|
108
|
+
*/
|
|
109
|
+
export async function llmChat(system, user) {
|
|
97
110
|
const provider = selectProvider();
|
|
98
111
|
if (!provider)
|
|
99
112
|
return null;
|
|
100
113
|
try {
|
|
101
|
-
|
|
102
|
-
return parseFile(text, input.code);
|
|
114
|
+
return provider === "groq" ? await callGroq(system, user) : await callAnthropic(system, user);
|
|
103
115
|
}
|
|
104
116
|
catch {
|
|
105
|
-
return null;
|
|
117
|
+
return null;
|
|
106
118
|
}
|
|
107
119
|
}
|
|
108
|
-
async function callAnthropic(
|
|
120
|
+
async function callAnthropic(system, user) {
|
|
109
121
|
let Anthropic;
|
|
110
122
|
try {
|
|
111
123
|
({ default: Anthropic } = await import("@anthropic-ai/sdk"));
|
|
@@ -117,13 +129,13 @@ async function callAnthropic(input) {
|
|
|
117
129
|
const msg = await client.messages.create({
|
|
118
130
|
model: modelFor("anthropic"),
|
|
119
131
|
max_tokens: 8192,
|
|
120
|
-
system
|
|
121
|
-
messages: [{ role: "user", content:
|
|
132
|
+
system,
|
|
133
|
+
messages: [{ role: "user", content: user }],
|
|
122
134
|
});
|
|
123
135
|
return extractAnthropicText(msg);
|
|
124
136
|
}
|
|
125
137
|
/** Groq's chat-completions endpoint is OpenAI-compatible — plain fetch, no SDK. */
|
|
126
|
-
async function callGroq(
|
|
138
|
+
async function callGroq(system, user) {
|
|
127
139
|
const res = await fetch("https://api.groq.com/openai/v1/chat/completions", {
|
|
128
140
|
method: "POST",
|
|
129
141
|
headers: {
|
|
@@ -134,8 +146,8 @@ async function callGroq(input) {
|
|
|
134
146
|
model: modelFor("groq"),
|
|
135
147
|
max_tokens: 8192,
|
|
136
148
|
messages: [
|
|
137
|
-
{ role: "system", content:
|
|
138
|
-
{ role: "user", content:
|
|
149
|
+
{ role: "system", content: system },
|
|
150
|
+
{ role: "user", content: user },
|
|
139
151
|
],
|
|
140
152
|
}),
|
|
141
153
|
});
|
package/dist/engine/init.js
CHANGED
|
@@ -9,6 +9,43 @@
|
|
|
9
9
|
import { promises as fs } from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { isGitRepo, gitDir } from "./git.js";
|
|
12
|
+
export function projectConfigPath(root) {
|
|
13
|
+
return path.join(path.resolve(root), "securevibe.config.json");
|
|
14
|
+
}
|
|
15
|
+
/** Tolerant read: a missing or corrupt config is treated as empty. */
|
|
16
|
+
export async function readProjectConfig(root) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(projectConfigPath(root), "utf8");
|
|
19
|
+
const parsed = JSON.parse(raw);
|
|
20
|
+
return typeof parsed === "object" && parsed ? parsed : {};
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return {};
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function patchProjectConfig(root, patch) {
|
|
27
|
+
const current = await readProjectConfig(root);
|
|
28
|
+
const next = { ...current, ...patch };
|
|
29
|
+
await fs.writeFile(projectConfigPath(root), JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
30
|
+
}
|
|
31
|
+
// Package names/imports that mark a repo as "uses an LLM SDK" — matched against
|
|
32
|
+
// package.json (dependency keys) and requirements.txt (package names) so the
|
|
33
|
+
// init-time AI-fixer offer only fires where it's actually relevant.
|
|
34
|
+
const AI_PACKAGE_RE = /"(openai|@anthropic-ai\/sdk|anthropic|langchain|@langchain\/[\w.-]+|cohere-ai|@google\/generative-ai|llamaindex|@modelcontextprotocol\/sdk)"\s*:|^\s*(openai|anthropic|langchain|cohere|google-generativeai|llama-index)\b/im;
|
|
35
|
+
export async function looksAiRelated(root) {
|
|
36
|
+
const absRoot = path.resolve(root);
|
|
37
|
+
for (const rel of ["package.json", "requirements.txt"]) {
|
|
38
|
+
try {
|
|
39
|
+
const content = await fs.readFile(path.join(absRoot, rel), "utf8");
|
|
40
|
+
if (AI_PACKAGE_RE.test(content))
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* file absent — try the next */
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
12
49
|
const GITIGNORE_LINES = [".env", ".env.local", ".env.*.local", ".securevibe-backup/"];
|
|
13
50
|
const CONFIG = `{
|
|
14
51
|
"failOn": "block",
|
package/dist/index-support.js
CHANGED
|
@@ -30,3 +30,32 @@ export async function maybeOfferAiFixerSetup() {
|
|
|
30
30
|
process.env.GROQ_API_KEY = key;
|
|
31
31
|
process.stderr.write(` Saved to ${configFilePath()}. This run will use it.\n\n`);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Runs once at the end of `init`, interactive-only. Users were learning a
|
|
35
|
+
* provider key was needed only mid-`fix` — too late, past the point of
|
|
36
|
+
* expecting the fixer to already work. `init` already runs once per project
|
|
37
|
+
* before any `fix`, so it's the right place to surface this early — but only
|
|
38
|
+
* for repos that actually use an LLM SDK, and only once (a decline is
|
|
39
|
+
* remembered per-project so we don't nag).
|
|
40
|
+
*/
|
|
41
|
+
export async function maybeOfferAiFixerSetupDuringInit(root, deps) {
|
|
42
|
+
const { applyStoredKeysToEnv } = await import("./config.js");
|
|
43
|
+
await applyStoredKeysToEnv(deps?.configFile);
|
|
44
|
+
if (process.env.GROQ_API_KEY || process.env.ANTHROPIC_API_KEY)
|
|
45
|
+
return;
|
|
46
|
+
const { readProjectConfig, patchProjectConfig, looksAiRelated } = await import("./engine/init.js");
|
|
47
|
+
const cfg = await readProjectConfig(root);
|
|
48
|
+
if (cfg.aiFixerPromptDismissed)
|
|
49
|
+
return;
|
|
50
|
+
if (!(await looksAiRelated(root)))
|
|
51
|
+
return;
|
|
52
|
+
const isInteractive = deps?.isInteractive ?? (await import("./ui/prompt.js")).isInteractive;
|
|
53
|
+
if (!isInteractive())
|
|
54
|
+
return;
|
|
55
|
+
process.stderr.write("\n This project uses an LLM SDK — SecureVibe can auto-fix AI-specific findings\n (tool-hijack, prompt-injection sinks) with a free key.\n");
|
|
56
|
+
const offer = deps?.offer ?? maybeOfferAiFixerSetup;
|
|
57
|
+
await offer();
|
|
58
|
+
if (!(process.env.GROQ_API_KEY || process.env.ANTHROPIC_API_KEY)) {
|
|
59
|
+
await patchProjectConfig(root, { aiFixerPromptDismissed: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -128,6 +128,8 @@ program
|
|
|
128
128
|
const { renderInit } = await import("./ui/init.js");
|
|
129
129
|
const result = await runInit(pathArg ?? ".", { force: opts.force });
|
|
130
130
|
process.stdout.write(renderInit(result) + "\n");
|
|
131
|
+
const { maybeOfferAiFixerSetupDuringInit } = await import("./index-support.js");
|
|
132
|
+
await maybeOfferAiFixerSetupDuringInit(pathArg ?? ".");
|
|
131
133
|
});
|
|
132
134
|
program
|
|
133
135
|
.command("fix")
|
|
@@ -169,6 +171,36 @@ program
|
|
|
169
171
|
}
|
|
170
172
|
await maybeNotifyUpdate({ json: opts.json });
|
|
171
173
|
});
|
|
174
|
+
program
|
|
175
|
+
.command("explain")
|
|
176
|
+
.description("AI-powered plain-language explanations for critical/high findings")
|
|
177
|
+
.argument("[path]", "path to the repository", ".")
|
|
178
|
+
.option("--json", "output machine-readable JSON")
|
|
179
|
+
.option("--no-color", "disable coloured output")
|
|
180
|
+
.action(async (pathArg, opts) => {
|
|
181
|
+
if (opts.color === false)
|
|
182
|
+
process.env.NO_COLOR = "1";
|
|
183
|
+
const { applyStoredKeysToEnv } = await import("./config.js");
|
|
184
|
+
await applyStoredKeysToEnv();
|
|
185
|
+
if (!opts.json) {
|
|
186
|
+
const { maybeOfferAiFixerSetup } = await import("./index-support.js");
|
|
187
|
+
await maybeOfferAiFixerSetup();
|
|
188
|
+
}
|
|
189
|
+
const { result, usage } = await runScan(pathArg, { json: opts.json, color: opts.color });
|
|
190
|
+
const { explainFindings } = await import("./engine/explain.js");
|
|
191
|
+
const report = await explainFindings(result, {
|
|
192
|
+
onProgress: opts.json ? undefined : (m) => process.stderr.write(` · ${m}\n`),
|
|
193
|
+
});
|
|
194
|
+
if (opts.json) {
|
|
195
|
+
const { explainToJson } = await import("./ui/explain.js");
|
|
196
|
+
process.stdout.write(explainToJson(report) + "\n");
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const { renderExplain } = await import("./ui/explain.js");
|
|
200
|
+
process.stdout.write(renderExplain(report, usage) + "\n");
|
|
201
|
+
}
|
|
202
|
+
await maybeNotifyUpdate({ json: opts.json });
|
|
203
|
+
});
|
|
172
204
|
addCommon(program
|
|
173
205
|
.command("ai-audit")
|
|
174
206
|
.description("Focus on the AI-application / agent attack surface (doc 04)")
|
package/dist/repl.js
CHANGED
|
@@ -44,6 +44,7 @@ const HELP = [
|
|
|
44
44
|
" Commands:",
|
|
45
45
|
" scan [--json] full scan",
|
|
46
46
|
" fix [--apply] [--yes] [--no-llm] [--json] fix findings",
|
|
47
|
+
" explain [--json] AI explanations for critical/high findings",
|
|
47
48
|
" ai-audit | protect | attack-map | score | deps | ready [--json]",
|
|
48
49
|
" init [--force] wire up the pre-commit guard + CI",
|
|
49
50
|
" config show | set-key <groq|anthropic> | unset-key <groq|anthropic>",
|
|
@@ -149,11 +150,35 @@ async function dispatch(cmd, tokens, state, io) {
|
|
|
149
150
|
}
|
|
150
151
|
return;
|
|
151
152
|
}
|
|
153
|
+
if (cmd === "explain") {
|
|
154
|
+
if (!asJson) {
|
|
155
|
+
const { maybeOfferAiFixerSetup } = await import("./index-support.js");
|
|
156
|
+
await maybeOfferAiFixerSetup();
|
|
157
|
+
}
|
|
158
|
+
const { explainFindings } = await import("./engine/explain.js");
|
|
159
|
+
const usage = await meterOrPrint(io);
|
|
160
|
+
const { scan } = await import("./engine/scan.js");
|
|
161
|
+
const scanResult = await scan(state.root, { onProgress: asJson ? undefined : (m) => write(io, ` · ${m}\n`) });
|
|
162
|
+
state.lastResult = scanResult;
|
|
163
|
+
const report = await explainFindings(scanResult, {
|
|
164
|
+
onProgress: asJson ? undefined : (m) => write(io, ` · ${m}\n`),
|
|
165
|
+
});
|
|
166
|
+
if (asJson) {
|
|
167
|
+
const { explainToJson } = await import("./ui/explain.js");
|
|
168
|
+
write(io, explainToJson(report) + "\n\n");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const { renderExplain } = await import("./ui/explain.js");
|
|
172
|
+
write(io, renderExplain(report, usage) + "\n");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
152
175
|
if (cmd === "init") {
|
|
153
176
|
const { runInit } = await import("./engine/init.js");
|
|
154
177
|
const { renderInit } = await import("./ui/init.js");
|
|
155
178
|
const result = await runInit(state.root, { force: hasFlag(tokens, "--force") });
|
|
156
179
|
write(io, renderInit(result) + "\n");
|
|
180
|
+
const { maybeOfferAiFixerSetupDuringInit } = await import("./index-support.js");
|
|
181
|
+
await maybeOfferAiFixerSetupDuringInit(state.root);
|
|
157
182
|
return;
|
|
158
183
|
}
|
|
159
184
|
if (cmd === "config") {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `explain` view. Shows the AI-generated explanation per finding when
|
|
3
|
+
* available, falling back to the existing static why/fix text otherwise —
|
|
4
|
+
* an AI explanation is additional context, never a replacement, so the
|
|
5
|
+
* static text stays a guaranteed field in both the human and JSON output.
|
|
6
|
+
*/
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
import { renderBanner } from "./banner.js";
|
|
9
|
+
import { renderUsageLine } from "./usage.js";
|
|
10
|
+
const SEV_TAG = {
|
|
11
|
+
critical: (s) => pc.bgRed(pc.white(pc.bold(s))),
|
|
12
|
+
high: (s) => pc.red(pc.bold(s)),
|
|
13
|
+
medium: (s) => pc.yellow(s),
|
|
14
|
+
low: (s) => pc.blue(s),
|
|
15
|
+
info: (s) => pc.dim(s),
|
|
16
|
+
};
|
|
17
|
+
export function renderExplain(report, usage) {
|
|
18
|
+
const L = [];
|
|
19
|
+
L.push(renderBanner({ subtitle: "AI-powered finding explanations" }));
|
|
20
|
+
L.push(pc.dim(` ${report.root}`));
|
|
21
|
+
L.push("");
|
|
22
|
+
if (usage) {
|
|
23
|
+
L.push(renderUsageLine(usage));
|
|
24
|
+
L.push("");
|
|
25
|
+
}
|
|
26
|
+
if (report.results.length === 0) {
|
|
27
|
+
L.push(pc.green(" No critical/high findings to explain."));
|
|
28
|
+
L.push("");
|
|
29
|
+
return L.join("\n");
|
|
30
|
+
}
|
|
31
|
+
for (const r of report.results) {
|
|
32
|
+
const f = r.finding;
|
|
33
|
+
const sev = SEV_TAG[f.severity](` ${f.severity.toUpperCase()} `);
|
|
34
|
+
L.push(` ${sev} ${pc.bold(f.title)} ${pc.dim(`(${f.file}:${f.line})`)}`);
|
|
35
|
+
if (r.explanation) {
|
|
36
|
+
L.push(` ${pc.cyan("AI:")} ${r.explanation.trim()}`);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
L.push(` ${pc.dim("why:")} ${f.why}`);
|
|
40
|
+
L.push(` ${pc.dim("fix:")} ${f.fix}`);
|
|
41
|
+
}
|
|
42
|
+
L.push("");
|
|
43
|
+
}
|
|
44
|
+
if (report.truncatedCount > 0) {
|
|
45
|
+
const total = report.results.length + report.truncatedCount;
|
|
46
|
+
L.push(pc.dim(` Showing the top ${report.results.length} of ${total} critical/high findings — re-run \`scan\` to see the rest.`));
|
|
47
|
+
L.push("");
|
|
48
|
+
}
|
|
49
|
+
if (!report.llmAvailable) {
|
|
50
|
+
L.push(pc.dim(" Set GROQ_API_KEY (free tier) or ANTHROPIC_API_KEY for AI-generated explanations — showing static guidance above. Run `securevibe config set-key groq`."));
|
|
51
|
+
L.push("");
|
|
52
|
+
}
|
|
53
|
+
return L.join("\n");
|
|
54
|
+
}
|
|
55
|
+
export function explainToJson(report) {
|
|
56
|
+
return JSON.stringify({
|
|
57
|
+
root: report.root,
|
|
58
|
+
llmAvailable: report.llmAvailable,
|
|
59
|
+
truncatedCount: report.truncatedCount,
|
|
60
|
+
results: report.results.map((r) => ({
|
|
61
|
+
id: r.finding.id,
|
|
62
|
+
detector: r.finding.detector,
|
|
63
|
+
severity: r.finding.severity,
|
|
64
|
+
file: r.finding.file,
|
|
65
|
+
line: r.finding.line,
|
|
66
|
+
title: r.finding.title,
|
|
67
|
+
why: r.finding.why,
|
|
68
|
+
fix: r.finding.fix,
|
|
69
|
+
explanation: r.explanation,
|
|
70
|
+
})),
|
|
71
|
+
}, null, 2);
|
|
72
|
+
}
|
package/dist/version.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// Single source of truth for the CLI's own version, so it can't drift from
|
|
3
3
|
// what commander reports and what the update-check compares against.
|
|
4
4
|
// Kept in sync with the "version" field in package.json by hand at release time.
|
|
5
|
-
export const VERSION = "0.1.
|
|
5
|
+
export const VERSION = "0.1.10";
|
package/package.json
CHANGED