securevibe 0.1.1 → 0.1.2
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/index.js +30 -9
- package/dist/ui/attackmap.js +6 -1
- package/dist/ui/deps.js +6 -1
- package/dist/ui/protect.js +11 -2
- package/dist/update-check.js +101 -0
- package/dist/version.js +5 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,11 +10,12 @@
|
|
|
10
10
|
import { Command } from "commander";
|
|
11
11
|
import { scan } from "./engine/scan.js";
|
|
12
12
|
import { toJson, toSarif } from "./ui/emit.js";
|
|
13
|
+
import { VERSION } from "./version.js";
|
|
13
14
|
const program = new Command();
|
|
14
15
|
program
|
|
15
16
|
.name("securevibe")
|
|
16
17
|
.description("Autonomous AI security engineer for AI-generated applications")
|
|
17
|
-
.version(
|
|
18
|
+
.version(VERSION);
|
|
18
19
|
function addCommon(cmd) {
|
|
19
20
|
return cmd
|
|
20
21
|
.option("--json", "output machine-readable JSON")
|
|
@@ -79,6 +80,18 @@ function emitMachine(result, opts) {
|
|
|
79
80
|
}
|
|
80
81
|
return false;
|
|
81
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* Best-effort "a newer version exists" nudge on human-readable output only.
|
|
85
|
+
* Cached for a day, never blocks, never throws — see update-check.ts.
|
|
86
|
+
*/
|
|
87
|
+
async function maybeNotifyUpdate(opts) {
|
|
88
|
+
if (opts.json || opts.sarif || opts.staged)
|
|
89
|
+
return;
|
|
90
|
+
const { checkForUpdate, updateNotice } = await import("./update-check.js");
|
|
91
|
+
const latest = await checkForUpdate(VERSION);
|
|
92
|
+
if (latest)
|
|
93
|
+
process.stderr.write("\n" + updateNotice(VERSION, latest) + "\n");
|
|
94
|
+
}
|
|
82
95
|
/** Exit code policy for CI gating (doc 08/12). */
|
|
83
96
|
function ciExit(result, opts) {
|
|
84
97
|
if (!opts.ci)
|
|
@@ -99,6 +112,7 @@ addCommon(program
|
|
|
99
112
|
const { renderReport } = await import("./ui/report.js");
|
|
100
113
|
process.stdout.write(renderReport(result, { usage }) + "\n");
|
|
101
114
|
}
|
|
115
|
+
await maybeNotifyUpdate(opts);
|
|
102
116
|
ciExit(result, opts);
|
|
103
117
|
});
|
|
104
118
|
program
|
|
@@ -145,6 +159,7 @@ program
|
|
|
145
159
|
if (decision.notice) {
|
|
146
160
|
process.stderr.write(" Not applied: no interactive terminal. Re-run with --yes to apply non-interactively.\n");
|
|
147
161
|
}
|
|
162
|
+
await maybeNotifyUpdate({ json: opts.json });
|
|
148
163
|
});
|
|
149
164
|
addCommon(program
|
|
150
165
|
.command("ai-audit")
|
|
@@ -155,50 +170,55 @@ addCommon(program
|
|
|
155
170
|
const { renderReport } = await import("./ui/report.js");
|
|
156
171
|
process.stdout.write(renderReport(result, { aiFocus: true, usage }) + "\n");
|
|
157
172
|
}
|
|
173
|
+
await maybeNotifyUpdate(opts);
|
|
158
174
|
ciExit(result, opts);
|
|
159
175
|
});
|
|
160
176
|
addCommon(program
|
|
161
177
|
.command("protect")
|
|
162
178
|
.description("Remediation-first view: the fixes for every finding")
|
|
163
179
|
.argument("[path]", "path to the repository", ".")).action(async (pathArg, opts) => {
|
|
164
|
-
const { result } = await runScan(pathArg, opts);
|
|
180
|
+
const { result, usage } = await runScan(pathArg, opts);
|
|
165
181
|
if (!emitMachine(result, opts)) {
|
|
166
182
|
const { renderRemediations } = await import("./ui/protect.js");
|
|
167
|
-
process.stdout.write(renderRemediations(result) + "\n");
|
|
183
|
+
process.stdout.write(renderRemediations(result, usage) + "\n");
|
|
168
184
|
}
|
|
185
|
+
await maybeNotifyUpdate(opts);
|
|
169
186
|
ciExit(result, opts);
|
|
170
187
|
});
|
|
171
188
|
addCommon(program
|
|
172
189
|
.command("attack-map")
|
|
173
190
|
.description("Show exploit paths from reachable findings (doc 06)")
|
|
174
191
|
.argument("[path]", "path to the repository", ".")).action(async (pathArg, opts) => {
|
|
175
|
-
const { result } = await runScan(pathArg, opts);
|
|
192
|
+
const { result, usage } = await runScan(pathArg, opts);
|
|
176
193
|
if (!emitMachine(result, opts)) {
|
|
177
194
|
const { renderAttackMap } = await import("./ui/attackmap.js");
|
|
178
|
-
process.stdout.write(renderAttackMap(result) + "\n");
|
|
195
|
+
process.stdout.write(renderAttackMap(result, usage) + "\n");
|
|
179
196
|
}
|
|
197
|
+
await maybeNotifyUpdate(opts);
|
|
180
198
|
ciExit(result, opts);
|
|
181
199
|
});
|
|
182
200
|
addCommon(program
|
|
183
201
|
.command("score")
|
|
184
202
|
.description("Print the security score and deployment-readiness verdict")
|
|
185
203
|
.argument("[path]", "path to the repository", ".")).action(async (pathArg, opts) => {
|
|
186
|
-
const { result } = await runScan(pathArg, opts);
|
|
204
|
+
const { result, usage } = await runScan(pathArg, opts);
|
|
187
205
|
if (!emitMachine(result, opts)) {
|
|
188
206
|
const { renderScoreOnly } = await import("./ui/protect.js");
|
|
189
|
-
process.stdout.write(renderScoreOnly(result) + "\n");
|
|
207
|
+
process.stdout.write(renderScoreOnly(result, usage) + "\n");
|
|
190
208
|
}
|
|
209
|
+
await maybeNotifyUpdate(opts);
|
|
191
210
|
ciExit(result, opts);
|
|
192
211
|
});
|
|
193
212
|
addCommon(program
|
|
194
213
|
.command("deps")
|
|
195
214
|
.description("Audit dependencies for known CVEs (SCA, offline against the local OSV db)")
|
|
196
215
|
.argument("[path]", "path to the repository", ".")).action(async (pathArg, opts) => {
|
|
197
|
-
const { result } = await runScan(pathArg, opts);
|
|
216
|
+
const { result, usage } = await runScan(pathArg, opts);
|
|
198
217
|
if (!emitMachine(result, opts)) {
|
|
199
218
|
const { renderDeps } = await import("./ui/deps.js");
|
|
200
|
-
process.stdout.write(renderDeps(result) + "\n");
|
|
219
|
+
process.stdout.write(renderDeps(result, usage) + "\n");
|
|
201
220
|
}
|
|
221
|
+
await maybeNotifyUpdate(opts);
|
|
202
222
|
ciExit(result, opts);
|
|
203
223
|
});
|
|
204
224
|
addCommon(program
|
|
@@ -210,6 +230,7 @@ addCommon(program
|
|
|
210
230
|
const { renderScorecard } = await import("./ui/readiness.js");
|
|
211
231
|
process.stdout.write(renderScorecard(result, usage) + "\n");
|
|
212
232
|
}
|
|
233
|
+
await maybeNotifyUpdate(opts);
|
|
213
234
|
ciExit(result, opts);
|
|
214
235
|
});
|
|
215
236
|
program
|
package/dist/ui/attackmap.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import pc from "picocolors";
|
|
8
8
|
import { renderBanner } from "./banner.js";
|
|
9
|
+
import { renderUsageLine } from "./usage.js";
|
|
9
10
|
function impactOf(f) {
|
|
10
11
|
switch (f.detector) {
|
|
11
12
|
case "sql-injection":
|
|
@@ -26,10 +27,14 @@ function impactOf(f) {
|
|
|
26
27
|
return "compromise of " + f.file;
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
|
-
export function renderAttackMap(result) {
|
|
30
|
+
export function renderAttackMap(result, usage) {
|
|
30
31
|
const lines = [];
|
|
31
32
|
lines.push(renderBanner({ subtitle: "attack map · exploit paths" }));
|
|
32
33
|
lines.push("");
|
|
34
|
+
if (usage) {
|
|
35
|
+
lines.push(renderUsageLine(usage));
|
|
36
|
+
lines.push("");
|
|
37
|
+
}
|
|
33
38
|
const chains = result.findings.filter((f) => f.reachable && (f.severity === "critical" || f.severity === "high"));
|
|
34
39
|
if (chains.length === 0) {
|
|
35
40
|
lines.push(pc.green(" ✓ No reachable critical/high exploit paths found."));
|
package/dist/ui/deps.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import pc from "picocolors";
|
|
8
8
|
import { renderBanner } from "./banner.js";
|
|
9
|
+
import { renderUsageLine } from "./usage.js";
|
|
9
10
|
const SEV_COLOR = {
|
|
10
11
|
critical: (s) => pc.bgRed(pc.white(pc.bold(` ${s} `))),
|
|
11
12
|
high: (s) => pc.red(pc.bold(s)),
|
|
@@ -25,12 +26,16 @@ export function dependencyLine(result) {
|
|
|
25
26
|
.map((s) => `${d.bySeverity[s]} ${s}`);
|
|
26
27
|
return `${d.total} known ${d.total === 1 ? "CVE" : "CVEs"} (${parts.join(", ")}) · ${date}`;
|
|
27
28
|
}
|
|
28
|
-
export function renderDeps(result) {
|
|
29
|
+
export function renderDeps(result, usage) {
|
|
29
30
|
const lines = [];
|
|
30
31
|
const deps = result.findings.filter((f) => f.detector === "vulnerable-dependency");
|
|
31
32
|
lines.push(renderBanner({ subtitle: "dependency audit (SCA)" }));
|
|
32
33
|
lines.push(pc.dim(` ${result.root}`));
|
|
33
34
|
lines.push("");
|
|
35
|
+
if (usage) {
|
|
36
|
+
lines.push(renderUsageLine(usage));
|
|
37
|
+
lines.push("");
|
|
38
|
+
}
|
|
34
39
|
lines.push(" " + (deps.length === 0 ? pc.green("✓ ") : "") + dependencyLine(result));
|
|
35
40
|
lines.push("");
|
|
36
41
|
for (const f of deps)
|
package/dist/ui/protect.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import pc from "picocolors";
|
|
6
6
|
import { renderBanner } from "./banner.js";
|
|
7
|
+
import { renderUsageLine } from "./usage.js";
|
|
7
8
|
const SEV_COLOR = {
|
|
8
9
|
critical: (s) => pc.bgRed(pc.white(pc.bold(s))),
|
|
9
10
|
high: (s) => pc.red(pc.bold(s)),
|
|
@@ -21,10 +22,14 @@ function readinessLine(r) {
|
|
|
21
22
|
return pc.red(pc.bold("✗ BLOCK DEPLOY — fix blocking issues first"));
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
|
-
export function renderRemediations(result) {
|
|
25
|
+
export function renderRemediations(result, usage) {
|
|
25
26
|
const lines = [];
|
|
26
27
|
lines.push(renderBanner({ subtitle: "remediation plan" }));
|
|
27
28
|
lines.push("");
|
|
29
|
+
if (usage) {
|
|
30
|
+
lines.push(renderUsageLine(usage));
|
|
31
|
+
lines.push("");
|
|
32
|
+
}
|
|
28
33
|
if (result.findings.length === 0) {
|
|
29
34
|
lines.push(pc.green(" ✓ Nothing to remediate."));
|
|
30
35
|
lines.push("");
|
|
@@ -53,12 +58,16 @@ export function renderRemediations(result) {
|
|
|
53
58
|
lines.push("");
|
|
54
59
|
return lines.join("\n");
|
|
55
60
|
}
|
|
56
|
-
export function renderScoreOnly(result) {
|
|
61
|
+
export function renderScoreOnly(result, usage) {
|
|
57
62
|
const s = result.score;
|
|
58
63
|
const gc = s.grade === "A" || s.grade === "B" ? pc.green : s.grade === "C" ? pc.yellow : pc.red;
|
|
59
64
|
const lines = [];
|
|
60
65
|
lines.push(renderBanner({ subtitle: "security score" }));
|
|
61
66
|
lines.push("");
|
|
67
|
+
if (usage) {
|
|
68
|
+
lines.push(renderUsageLine(usage));
|
|
69
|
+
lines.push("");
|
|
70
|
+
}
|
|
62
71
|
lines.push(` ${gc(pc.bold(` ${s.grade} `))} ${pc.bold(`${s.composite}/100`)} ${readinessLine(s.readiness)}`);
|
|
63
72
|
lines.push("");
|
|
64
73
|
lines.push(` app ${s.subScores.appSecurity} · ${pc.magenta(`ai ${s.subScores.aiSecurity}`)} · secrets ${s.subScores.dependencies} · deploy ${s.subScores.deployment}`);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort "a newer version is published" notice. There is no auto-update —
|
|
3
|
+
* users install a CLI globally and nothing tells them it went stale. This
|
|
4
|
+
* checks the npm registry at most once a day (cached in ~/.securevibe), never
|
|
5
|
+
* throws, and never slows a command down on a broken network (short timeout,
|
|
6
|
+
* fire-and-forget failure).
|
|
7
|
+
*/
|
|
8
|
+
import { promises as fs } from "node:fs";
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
const REGISTRY_URL = "https://registry.npmjs.org/securevibe/latest";
|
|
12
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const FETCH_TIMEOUT_MS = 1500;
|
|
14
|
+
export function cacheFilePath() {
|
|
15
|
+
return path.join(os.homedir(), ".securevibe", "update-check.json");
|
|
16
|
+
}
|
|
17
|
+
/** Simple major.minor.patch compare; true if `latest` is strictly newer than `current`. */
|
|
18
|
+
export function isNewer(current, latest) {
|
|
19
|
+
const a = current.split(".").map((n) => parseInt(n, 10) || 0);
|
|
20
|
+
const b = latest.split(".").map((n) => parseInt(n, 10) || 0);
|
|
21
|
+
for (let i = 0; i < 3; i++) {
|
|
22
|
+
const ai = a[i] ?? 0;
|
|
23
|
+
const bi = b[i] ?? 0;
|
|
24
|
+
if (bi > ai)
|
|
25
|
+
return true;
|
|
26
|
+
if (bi < ai)
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
async function readCache(file) {
|
|
32
|
+
try {
|
|
33
|
+
const raw = JSON.parse(await fs.readFile(file, "utf8"));
|
|
34
|
+
if (typeof raw?.checkedAt === "number" && typeof raw?.latest === "string")
|
|
35
|
+
return raw;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
/* missing or corrupt → treated as no cache */
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
async function writeCache(file, state) {
|
|
43
|
+
try {
|
|
44
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
45
|
+
await fs.writeFile(file, JSON.stringify(state) + "\n", "utf8");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* best effort — a failed write must never break the command */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function fetchLatest(fetchImpl) {
|
|
52
|
+
const controller = new AbortController();
|
|
53
|
+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
|
|
54
|
+
try {
|
|
55
|
+
const res = await fetchImpl(REGISTRY_URL, { signal: controller.signal });
|
|
56
|
+
if (!res.ok)
|
|
57
|
+
return null;
|
|
58
|
+
const json = await res.json();
|
|
59
|
+
return typeof json?.version === "string" ? json.version : null;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Returns the newer published version if one exists, else null. Never
|
|
70
|
+
* throws — offline, a down registry, or a corrupt cache all resolve to null
|
|
71
|
+
* rather than interrupting the command.
|
|
72
|
+
*/
|
|
73
|
+
export async function checkForUpdate(currentVersion, opts = {}) {
|
|
74
|
+
const file = opts.file ?? cacheFilePath();
|
|
75
|
+
const now = opts.now ?? Date.now();
|
|
76
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
77
|
+
try {
|
|
78
|
+
const cached = await readCache(file);
|
|
79
|
+
let latest = null;
|
|
80
|
+
if (cached && now - cached.checkedAt < CACHE_TTL_MS) {
|
|
81
|
+
latest = cached.latest;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
latest = await fetchLatest(fetchImpl);
|
|
85
|
+
if (latest)
|
|
86
|
+
await writeCache(file, { checkedAt: now, latest });
|
|
87
|
+
else if (cached)
|
|
88
|
+
latest = cached.latest;
|
|
89
|
+
}
|
|
90
|
+
return latest && isNewer(currentVersion, latest) ? latest : null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function updateNotice(currentVersion, latestVersion) {
|
|
97
|
+
return [
|
|
98
|
+
` A new version of securevibe is available: ${latestVersion} (you have ${currentVersion}).`,
|
|
99
|
+
" Update: npm install -g securevibe",
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// packages/cli/src/version.ts
|
|
2
|
+
// Single source of truth for the CLI's own version, so it can't drift from
|
|
3
|
+
// what commander reports and what the update-check compares against.
|
|
4
|
+
// Kept in sync with the "version" field in package.json by hand at release time.
|
|
5
|
+
export const VERSION = "0.1.2";
|
package/package.json
CHANGED