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 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("0.1.1");
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
@@ -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)
@@ -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
+ }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securevibe",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Autonomous AI security engineer for AI generated apps: scan, fix, verify, and gate every commit before you launch",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",