guardvibe 3.0.57 → 3.1.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/README.md +1 -1
- package/build/cli/deep-scan.d.ts +1 -0
- package/build/cli/deep-scan.js +79 -0
- package/build/cli.js +9 -0
- package/build/index.js +12 -5
- package/build/tools/deep-scan.d.ts +13 -2
- package/build/tools/deep-scan.js +50 -9
- package/build/utils/update-check.d.ts +27 -0
- package/build/utils/update-check.js +125 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -228,7 +228,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, typosquat detection
|
|
|
228
228
|
| `verify_fix` | Verify a security fix was applied correctly — returns fixed/still_vulnerable/new_issues |
|
|
229
229
|
| `security_workflow` | Get recommended tool workflow for your current task (writing, pre-commit, PR review, etc.) |
|
|
230
230
|
| `auth_coverage` | **Auth coverage map** — enumerate routes, parse middleware matchers, detect auth guards, report coverage % |
|
|
231
|
-
| `deep_scan` | **LLM-powered deep analysis** — IDOR, business logic, race conditions,
|
|
231
|
+
| `deep_scan` | **LLM-powered deep analysis** — IDOR, business logic, race conditions, auth bypass. Defaults to Claude Haiku 4.5 (~cents/scan). Pass `model: 'sonnet'` for deeper analysis. CLI: `npx guardvibe deep-scan <file> --focus idor` |
|
|
232
232
|
| `full_audit` | **Single source of truth** — runs ALL checks in one call, returns PASS/FAIL/WARN verdict + score + coverage % + deterministic result hash |
|
|
233
233
|
| `remediation_plan` | **Remediation plan** — generates section-by-section fix checklist after audit |
|
|
234
234
|
| `verify_remediation` | **Remediation verification** — compares before/after audit, flags skipped sections |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDeepScan(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI: guardvibe deep-scan <file>
|
|
3
|
+
* LLM-powered deep security analysis.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { resolve, extname } from "node:path";
|
|
7
|
+
import { parseArgs } from "./args.js";
|
|
8
|
+
import { buildDeepScanPrompt, callLLM, parseDeepScanResult, formatDeepScanFindings, DEFAULT_MAX_BYTES, } from "../tools/deep-scan.js";
|
|
9
|
+
const EXT_TO_LANG = {
|
|
10
|
+
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
11
|
+
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
12
|
+
".py": "python", ".go": "go", ".rb": "ruby", ".java": "java",
|
|
13
|
+
".rs": "rust", ".php": "php", ".cs": "csharp",
|
|
14
|
+
};
|
|
15
|
+
const VALID_FOCUS = ["all", "idor", "business-logic", "auth-bypass", "race-condition"];
|
|
16
|
+
export async function runDeepScan(args) {
|
|
17
|
+
const { flags, positional } = parseArgs(args);
|
|
18
|
+
const file = positional[0];
|
|
19
|
+
if (!file) {
|
|
20
|
+
console.error(" [ERR] Please specify a file: npx guardvibe deep-scan <file>");
|
|
21
|
+
console.error("");
|
|
22
|
+
console.error(" Options:");
|
|
23
|
+
console.error(" --focus <area> all (default) | idor | business-logic | auth-bypass | race-condition");
|
|
24
|
+
console.error(" --model <model> haiku (default, ~cents/scan) | sonnet (deeper, more expensive)");
|
|
25
|
+
console.error(" --max-bytes <n> Truncate input to N bytes (default 10000)");
|
|
26
|
+
console.error(" --format <type> markdown (default) | json");
|
|
27
|
+
console.error("");
|
|
28
|
+
console.error(" Requires ANTHROPIC_API_KEY (or OPENAI_API_KEY) environment variable.");
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
const path = resolve(file);
|
|
32
|
+
let content;
|
|
33
|
+
try {
|
|
34
|
+
const stat = statSync(path);
|
|
35
|
+
if (!stat.isFile()) {
|
|
36
|
+
console.error(` [ERR] Not a file: ${path}`);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
content = readFileSync(path, "utf-8");
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
console.error(` [ERR] Cannot read file: ${path}`);
|
|
43
|
+
console.error(` ${e.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) {
|
|
47
|
+
console.error(" [ERR] No LLM API key. Set ANTHROPIC_API_KEY or OPENAI_API_KEY in your environment.");
|
|
48
|
+
console.error(" Default model is Claude Haiku 4.5 — typically ~cents per scan.");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const focusArg = flags.focus ?? "all";
|
|
52
|
+
if (!VALID_FOCUS.includes(focusArg)) {
|
|
53
|
+
console.error(` [ERR] Invalid --focus: ${focusArg}. Use one of: ${VALID_FOCUS.join(", ")}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
const focus = focusArg;
|
|
57
|
+
const modelArg = flags.model ?? "haiku";
|
|
58
|
+
if (modelArg !== "haiku" && modelArg !== "sonnet") {
|
|
59
|
+
console.error(` [ERR] Invalid --model: ${modelArg}. Use haiku or sonnet.`);
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
const model = modelArg;
|
|
63
|
+
const maxBytes = flags["max-bytes"] != null ? Number(flags["max-bytes"]) : DEFAULT_MAX_BYTES;
|
|
64
|
+
if (!Number.isFinite(maxBytes) || maxBytes < 500 || maxBytes > 50_000) {
|
|
65
|
+
console.error(` [ERR] --max-bytes must be 500..50000 (got ${flags["max-bytes"]})`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const format = (flags.format === "json" ? "json" : "markdown");
|
|
69
|
+
const language = EXT_TO_LANG[extname(path).toLowerCase()] ?? "unknown";
|
|
70
|
+
const prompt = buildDeepScanPrompt(content, language, [], focus);
|
|
71
|
+
const llmResponse = await callLLM(prompt, { model, maxBytes });
|
|
72
|
+
if (llmResponse === null) {
|
|
73
|
+
console.error(" [ERR] LLM call failed — check API key validity and network.");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
const findings = parseDeepScanResult(llmResponse);
|
|
77
|
+
const output = formatDeepScanFindings(findings, format);
|
|
78
|
+
console.log(output);
|
|
79
|
+
}
|
package/build/cli.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "module";
|
|
3
|
+
import { checkForUpdate } from "./utils/update-check.js";
|
|
3
4
|
const require = createRequire(import.meta.url);
|
|
4
5
|
const pkg = require("../package.json");
|
|
6
|
+
// Fire-and-forget update notification (writes to stderr if newer version exists).
|
|
7
|
+
// Skipped when GUARDVIBE_NO_UPDATE_CHECK=1, NO_UPDATE_NOTIFIER=1, or CI=true.
|
|
8
|
+
checkForUpdate(pkg.version);
|
|
5
9
|
// ── Scan entry point detection ──────────────────────────────────────
|
|
6
10
|
const SCAN_SCRIPT_DETECTED = process.argv[1]?.endsWith("guardvibe-scan") ||
|
|
7
11
|
process.argv[1]?.endsWith("guardvibe-scan.js");
|
|
@@ -28,6 +32,7 @@ function printUsage() {
|
|
|
28
32
|
npx guardvibe check-cmd "<cmd>" Check if a shell command is safe to execute
|
|
29
33
|
npx guardvibe auth-coverage [path] Auth coverage analysis (Next.js routes)
|
|
30
34
|
npx guardvibe compliance [path] Compliance report (--framework SOC2|GDPR|...)
|
|
35
|
+
npx guardvibe deep-scan <file> LLM-powered deep scan (IDOR, business logic, race conditions)
|
|
31
36
|
npx guardvibe init <platform> Setup MCP server configuration
|
|
32
37
|
npx guardvibe hook install Install pre-commit security hook
|
|
33
38
|
npx guardvibe hook uninstall Remove pre-commit security hook
|
|
@@ -152,6 +157,10 @@ async function main() {
|
|
|
152
157
|
const { runCompliance } = await import("./cli/compliance.js");
|
|
153
158
|
await runCompliance(subArgs);
|
|
154
159
|
}
|
|
160
|
+
else if (command === "deep-scan") {
|
|
161
|
+
const { runDeepScan } = await import("./cli/deep-scan.js");
|
|
162
|
+
await runDeepScan(subArgs);
|
|
163
|
+
}
|
|
155
164
|
else {
|
|
156
165
|
console.error(` Unknown command: ${command}`);
|
|
157
166
|
printUsage();
|
package/build/index.js
CHANGED
|
@@ -863,20 +863,24 @@ server.tool("auth_coverage", "Analyze authentication coverage across Next.js App
|
|
|
863
863
|
return { content: [{ type: "text", text: output }] };
|
|
864
864
|
});
|
|
865
865
|
// Tool 32: LLM-powered deep scan
|
|
866
|
-
server.tool("deep_scan", "LLM-powered deep security analysis for vulnerabilities that pattern-matching cannot detect: IDOR, business logic flaws, race conditions, stale auth, mass assignment, privilege escalation.
|
|
866
|
+
server.tool("deep_scan", "LLM-powered deep security analysis for vulnerabilities that pattern-matching cannot detect: IDOR, business logic flaws, race conditions, stale auth, mass assignment, privilege escalation. Defaults to Claude Haiku 4.5 (~cents per scan); pass `model: 'sonnet'` for deeper analysis at higher cost. Requires ANTHROPIC_API_KEY or OPENAI_API_KEY env var.", {
|
|
867
867
|
code: z.string().describe("Code to analyze"),
|
|
868
868
|
language: z.string().describe("Programming language"),
|
|
869
869
|
context: z.string().optional().describe("Additional context (e.g., 'This is a payment endpoint')"),
|
|
870
870
|
existingFindings: z.array(z.string()).default([]).describe("Already-detected findings to avoid duplicating"),
|
|
871
|
+
focus: z.enum(["all", "idor", "business-logic", "auth-bypass", "race-condition"]).default("all").describe("Focus area — narrows the prompt to a specific vulnerability class"),
|
|
872
|
+
model: z.enum(["haiku", "sonnet"]).default("haiku").describe("LLM model. haiku = fast & cheap (default), sonnet = deeper analysis"),
|
|
873
|
+
maxBytes: z.number().int().min(500).max(50_000).default(10_000).describe("Max prompt size in bytes — caps cost. Code over this limit is truncated."),
|
|
871
874
|
format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
|
|
872
|
-
}, async ({ code, language, context, existingFindings, format }) => {
|
|
873
|
-
const prompt = buildDeepScanPrompt(code, language, existingFindings);
|
|
874
|
-
const
|
|
875
|
+
}, async ({ code, language, context, existingFindings, focus, model, maxBytes, format }) => {
|
|
876
|
+
const prompt = buildDeepScanPrompt(code, language, existingFindings, focus);
|
|
877
|
+
const fullPrompt = context ? `${prompt}\n\nAdditional context: ${context}` : prompt;
|
|
878
|
+
const llmResponse = await callLLM(fullPrompt, { model, maxBytes });
|
|
875
879
|
if (llmResponse === null) {
|
|
876
880
|
return {
|
|
877
881
|
content: [{
|
|
878
882
|
type: "text",
|
|
879
|
-
text: "## Deep Scan — Setup Required\n\nNo LLM API key found. Set one of:\n- `ANTHROPIC_API_KEY` — uses Claude\n- `OPENAI_API_KEY` — uses GPT-4o\n\nThe deep scan sends code to the LLM API for semantic vulnerability analysis.",
|
|
883
|
+
text: "## Deep Scan — Setup Required\n\nNo LLM API key found. Set one of:\n- `ANTHROPIC_API_KEY` — uses Claude (default: Haiku 4.5; pass `model: 'sonnet'` for deeper analysis)\n- `OPENAI_API_KEY` — uses GPT-4o-mini / GPT-4o\n\nThe deep scan sends code to the LLM API for semantic vulnerability analysis. Default cost is a few cents per scan with Haiku.",
|
|
880
884
|
}],
|
|
881
885
|
};
|
|
882
886
|
}
|
|
@@ -994,6 +998,9 @@ export async function startMcpServer() {
|
|
|
994
998
|
return main();
|
|
995
999
|
}
|
|
996
1000
|
async function main() {
|
|
1001
|
+
// Fire-and-forget npm update check (writes a banner to stderr if newer version exists).
|
|
1002
|
+
const { checkForUpdate } = await import("./utils/update-check.js");
|
|
1003
|
+
checkForUpdate(pkg.version);
|
|
997
1004
|
// Load plugins
|
|
998
1005
|
const config = loadConfig(process.cwd());
|
|
999
1006
|
const plugins = await discoverPlugins(process.cwd(), config.plugins);
|
|
@@ -12,10 +12,14 @@ export interface DeepScanFinding {
|
|
|
12
12
|
location: string;
|
|
13
13
|
fix: string;
|
|
14
14
|
}
|
|
15
|
+
export type DeepScanFocus = "all" | "idor" | "business-logic" | "auth-bypass" | "race-condition";
|
|
16
|
+
export type DeepScanModel = "haiku" | "sonnet";
|
|
17
|
+
export declare const MODEL_IDS: Record<DeepScanModel, string>;
|
|
18
|
+
export declare const DEFAULT_MAX_BYTES = 10000;
|
|
15
19
|
/**
|
|
16
20
|
* Build a structured prompt for the LLM to analyze code.
|
|
17
21
|
*/
|
|
18
|
-
export declare function buildDeepScanPrompt(code: string, language: string, existingFindings: string[]): string;
|
|
22
|
+
export declare function buildDeepScanPrompt(code: string, language: string, existingFindings: string[], focus?: DeepScanFocus): string;
|
|
19
23
|
/**
|
|
20
24
|
* Parse LLM response into structured findings.
|
|
21
25
|
* Handles raw JSON, JSON in markdown code blocks, and malformed responses.
|
|
@@ -25,9 +29,16 @@ export declare function parseDeepScanResult(response: string): DeepScanFinding[]
|
|
|
25
29
|
* Format deep scan findings as markdown or JSON.
|
|
26
30
|
*/
|
|
27
31
|
export declare function formatDeepScanFindings(findings: DeepScanFinding[], format: "markdown" | "json"): string;
|
|
32
|
+
export interface CallLLMOptions {
|
|
33
|
+
model?: DeepScanModel;
|
|
34
|
+
maxBytes?: number;
|
|
35
|
+
}
|
|
28
36
|
/**
|
|
29
37
|
* Call an LLM API for deep analysis. Uses native fetch.
|
|
30
38
|
* Supports Anthropic (ANTHROPIC_API_KEY) or OpenAI (OPENAI_API_KEY).
|
|
31
39
|
* Returns null if no API key is available.
|
|
40
|
+
*
|
|
41
|
+
* Defaults to Haiku 4.5 for cost; pass `model: "sonnet"` for higher-quality analysis.
|
|
42
|
+
* `maxBytes` truncates the prompt to keep cost bounded (default 10 KB).
|
|
32
43
|
*/
|
|
33
|
-
export declare function callLLM(prompt: string): Promise<string | null>;
|
|
44
|
+
export declare function callLLM(prompt: string, options?: CallLLMOptions): Promise<string | null>;
|
package/build/tools/deep-scan.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Uses native fetch — no extra dependencies.
|
|
7
7
|
*/
|
|
8
|
-
const
|
|
8
|
+
const ALL_AREAS = [
|
|
9
9
|
"IDOR (Insecure Direct Object Reference) — can users access resources belonging to other users?",
|
|
10
10
|
"Business logic flaws — are there authorization bypasses, price manipulation, or state machine violations?",
|
|
11
11
|
"Race conditions — are there TOCTOU issues, double-spend, or concurrent mutation without locking?",
|
|
@@ -13,18 +13,50 @@ const FOCUS_AREAS = [
|
|
|
13
13
|
"Mass assignment — can users set fields they shouldn't (role, isAdmin, price)?",
|
|
14
14
|
"Privilege escalation — can a regular user perform admin actions through parameter manipulation?",
|
|
15
15
|
];
|
|
16
|
+
const FOCUS_AREAS = {
|
|
17
|
+
all: ALL_AREAS,
|
|
18
|
+
idor: [
|
|
19
|
+
"IDOR (Insecure Direct Object Reference) — can users access resources belonging to other users?",
|
|
20
|
+
"Missing ownership scope on database queries (where: { id } instead of { id, userId })",
|
|
21
|
+
"URL/path parameters used directly as DB keys without authorization gate",
|
|
22
|
+
],
|
|
23
|
+
"business-logic": [
|
|
24
|
+
"Authorization bypass via parameter manipulation (e.g., role/isAdmin in body)",
|
|
25
|
+
"Price/amount manipulation or coupon stacking",
|
|
26
|
+
"State machine violations (skip steps, replay completed actions)",
|
|
27
|
+
"Idempotency / replay protection on payment / order paths",
|
|
28
|
+
],
|
|
29
|
+
"auth-bypass": [
|
|
30
|
+
"Missing or insufficient auth check before sensitive operations",
|
|
31
|
+
"Stale tokens / sessions still accepted after revoke",
|
|
32
|
+
"Cookie / JWT validation skipped on a subset of routes",
|
|
33
|
+
"Privilege-elevation through parameter manipulation",
|
|
34
|
+
],
|
|
35
|
+
"race-condition": [
|
|
36
|
+
"TOCTOU between read and write (check-then-act without locking)",
|
|
37
|
+
"Concurrent rate-limit increments without atomic ops",
|
|
38
|
+
"Double-spend / double-grant via parallel requests",
|
|
39
|
+
"Optimistic-update races on shared mutable state",
|
|
40
|
+
],
|
|
41
|
+
};
|
|
42
|
+
export const MODEL_IDS = {
|
|
43
|
+
haiku: "claude-haiku-4-5-20251001",
|
|
44
|
+
sonnet: "claude-sonnet-4-6",
|
|
45
|
+
};
|
|
46
|
+
export const DEFAULT_MAX_BYTES = 10_000;
|
|
16
47
|
/**
|
|
17
48
|
* Build a structured prompt for the LLM to analyze code.
|
|
18
49
|
*/
|
|
19
|
-
export function buildDeepScanPrompt(code, language, existingFindings) {
|
|
50
|
+
export function buildDeepScanPrompt(code, language, existingFindings, focus = "all") {
|
|
51
|
+
const areas = FOCUS_AREAS[focus] ?? FOCUS_AREAS.all;
|
|
20
52
|
const lines = [
|
|
21
53
|
"You are a senior application security engineer performing a deep code review.",
|
|
22
54
|
"Analyze the following code for security vulnerabilities that automated pattern-matching scanners miss.",
|
|
23
55
|
"",
|
|
24
|
-
|
|
56
|
+
`## Focus Areas (${focus})`,
|
|
25
57
|
"",
|
|
26
58
|
];
|
|
27
|
-
for (const area of
|
|
59
|
+
for (const area of areas) {
|
|
28
60
|
lines.push(`- ${area}`);
|
|
29
61
|
}
|
|
30
62
|
lines.push("");
|
|
@@ -123,11 +155,20 @@ export function formatDeepScanFindings(findings, format) {
|
|
|
123
155
|
* Call an LLM API for deep analysis. Uses native fetch.
|
|
124
156
|
* Supports Anthropic (ANTHROPIC_API_KEY) or OpenAI (OPENAI_API_KEY).
|
|
125
157
|
* Returns null if no API key is available.
|
|
158
|
+
*
|
|
159
|
+
* Defaults to Haiku 4.5 for cost; pass `model: "sonnet"` for higher-quality analysis.
|
|
160
|
+
* `maxBytes` truncates the prompt to keep cost bounded (default 10 KB).
|
|
126
161
|
*/
|
|
127
|
-
export async function callLLM(prompt) {
|
|
162
|
+
export async function callLLM(prompt, options = {}) {
|
|
128
163
|
// guardvibe-ignore — API URLs are hardcoded trusted endpoints, not user-controlled
|
|
129
164
|
const anthropicKey = process.env.ANTHROPIC_API_KEY;
|
|
130
165
|
const openaiKey = process.env.OPENAI_API_KEY;
|
|
166
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
167
|
+
const model = options.model ?? "haiku";
|
|
168
|
+
// Truncate prompt to keep token budget bounded
|
|
169
|
+
const trimmedPrompt = prompt.length > maxBytes
|
|
170
|
+
? prompt.slice(0, maxBytes) + "\n\n[truncated by GuardVibe to stay within budget]"
|
|
171
|
+
: prompt;
|
|
131
172
|
if (anthropicKey) {
|
|
132
173
|
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
133
174
|
method: "POST",
|
|
@@ -137,9 +178,9 @@ export async function callLLM(prompt) {
|
|
|
137
178
|
"anthropic-version": "2023-06-01",
|
|
138
179
|
},
|
|
139
180
|
body: JSON.stringify({
|
|
140
|
-
model:
|
|
181
|
+
model: MODEL_IDS[model],
|
|
141
182
|
max_tokens: 2048,
|
|
142
|
-
messages: [{ role: "user", content:
|
|
183
|
+
messages: [{ role: "user", content: trimmedPrompt }],
|
|
143
184
|
}),
|
|
144
185
|
});
|
|
145
186
|
if (!res.ok)
|
|
@@ -155,9 +196,9 @@ export async function callLLM(prompt) {
|
|
|
155
196
|
"Authorization": `Bearer ${openaiKey}`,
|
|
156
197
|
},
|
|
157
198
|
body: JSON.stringify({
|
|
158
|
-
model: "gpt-4o",
|
|
199
|
+
model: model === "sonnet" ? "gpt-4o" : "gpt-4o-mini",
|
|
159
200
|
max_tokens: 2048,
|
|
160
|
-
messages: [{ role: "user", content:
|
|
201
|
+
messages: [{ role: "user", content: trimmedPrompt }],
|
|
161
202
|
}),
|
|
162
203
|
});
|
|
163
204
|
if (!res.ok)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking npm update notification.
|
|
3
|
+
*
|
|
4
|
+
* On startup of the CLI or MCP server, fires an async GET against
|
|
5
|
+
* https://registry.npmjs.org/guardvibe/latest. Result is cached for 24h
|
|
6
|
+
* in ~/.cache/guardvibe/version-check.json so we never hit npm twice in
|
|
7
|
+
* a day from one machine. If a newer version is available, a 5-line
|
|
8
|
+
* banner is written to stderr — never stdout, so the MCP JSON-RPC stream
|
|
9
|
+
* is untouched.
|
|
10
|
+
*
|
|
11
|
+
* Disable with GUARDVIBE_NO_UPDATE_CHECK=1, NO_UPDATE_NOTIFIER=1,
|
|
12
|
+
* or CI=true (CI runners don't need version banners).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Compare two semver strings. Returns true if `latest` is strictly newer.
|
|
16
|
+
* Handles plain MAJOR.MINOR.PATCH (no pre-release / build metadata).
|
|
17
|
+
*/
|
|
18
|
+
export declare function isNewer(latest: string, current: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Fire-and-forget version check. Never throws, never blocks.
|
|
21
|
+
*
|
|
22
|
+
* Behavior:
|
|
23
|
+
* - If env var disables it → no-op.
|
|
24
|
+
* - If cache is fresh (< 24h) and indicates newer version → announce immediately.
|
|
25
|
+
* - If cache is stale → fire async fetch, update cache, announce if newer.
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkForUpdate(currentVersion: string): void;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-blocking npm update notification.
|
|
3
|
+
*
|
|
4
|
+
* On startup of the CLI or MCP server, fires an async GET against
|
|
5
|
+
* https://registry.npmjs.org/guardvibe/latest. Result is cached for 24h
|
|
6
|
+
* in ~/.cache/guardvibe/version-check.json so we never hit npm twice in
|
|
7
|
+
* a day from one machine. If a newer version is available, a 5-line
|
|
8
|
+
* banner is written to stderr — never stdout, so the MCP JSON-RPC stream
|
|
9
|
+
* is untouched.
|
|
10
|
+
*
|
|
11
|
+
* Disable with GUARDVIBE_NO_UPDATE_CHECK=1, NO_UPDATE_NOTIFIER=1,
|
|
12
|
+
* or CI=true (CI runners don't need version banners).
|
|
13
|
+
*/
|
|
14
|
+
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
15
|
+
import { join, dirname } from "node:path";
|
|
16
|
+
import { homedir, tmpdir } from "node:os";
|
|
17
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24h
|
|
18
|
+
const NPM_URL = "https://registry.npmjs.org/guardvibe/latest";
|
|
19
|
+
const FETCH_TIMEOUT_MS = 2000;
|
|
20
|
+
function cachePath() {
|
|
21
|
+
const home = process.env.HOME ?? homedir();
|
|
22
|
+
const baseDir = home && home.length > 0 ? join(home, ".cache", "guardvibe") : join(tmpdir(), "guardvibe");
|
|
23
|
+
return join(baseDir, "version-check.json");
|
|
24
|
+
}
|
|
25
|
+
function readCache() {
|
|
26
|
+
try {
|
|
27
|
+
const raw = readFileSync(cachePath(), "utf-8");
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function writeCache(data) {
|
|
35
|
+
try {
|
|
36
|
+
const path = cachePath();
|
|
37
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
38
|
+
writeFileSync(path, JSON.stringify(data));
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// Cache write failure is non-fatal; silently swallow.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compare two semver strings. Returns true if `latest` is strictly newer.
|
|
46
|
+
* Handles plain MAJOR.MINOR.PATCH (no pre-release / build metadata).
|
|
47
|
+
*/
|
|
48
|
+
export function isNewer(latest, current) {
|
|
49
|
+
const parse = (v) => v.split(".").map(n => parseInt(n, 10));
|
|
50
|
+
const la = parse(latest);
|
|
51
|
+
const ca = parse(current);
|
|
52
|
+
for (let i = 0; i < 3; i++) {
|
|
53
|
+
const l = la[i] ?? 0;
|
|
54
|
+
const c = ca[i] ?? 0;
|
|
55
|
+
if (l !== c)
|
|
56
|
+
return l > c;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
async function fetchLatest() {
|
|
61
|
+
try {
|
|
62
|
+
const ctrl = new AbortController();
|
|
63
|
+
const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
|
|
64
|
+
// guardvibe-ignore VG120 — NPM_URL is a hardcoded module-level constant, not user input
|
|
65
|
+
const res = await fetch(NPM_URL, { signal: ctrl.signal });
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
if (!res.ok)
|
|
68
|
+
return null;
|
|
69
|
+
const data = (await res.json());
|
|
70
|
+
return typeof data.version === "string" ? data.version : null;
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function announceUpdate(latest, current) {
|
|
77
|
+
const lines = [
|
|
78
|
+
"",
|
|
79
|
+
" ┌──────────────────────────────────────────────────────────┐",
|
|
80
|
+
` │ GuardVibe ${current} → ${latest} available`,
|
|
81
|
+
" │ Upgrade: re-run `npx guardvibe init <host>` to pin the new",
|
|
82
|
+
" │ version into your .mcp.json (or `npx guardvibe@latest`)",
|
|
83
|
+
" │ Silence: set GUARDVIBE_NO_UPDATE_CHECK=1",
|
|
84
|
+
" └──────────────────────────────────────────────────────────┘",
|
|
85
|
+
"",
|
|
86
|
+
];
|
|
87
|
+
process.stderr.write(lines.join("\n"));
|
|
88
|
+
}
|
|
89
|
+
function isDisabled() {
|
|
90
|
+
return (process.env.GUARDVIBE_NO_UPDATE_CHECK === "1" ||
|
|
91
|
+
process.env.NO_UPDATE_NOTIFIER === "1" ||
|
|
92
|
+
process.env.CI === "true" ||
|
|
93
|
+
process.env.CI === "1");
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Fire-and-forget version check. Never throws, never blocks.
|
|
97
|
+
*
|
|
98
|
+
* Behavior:
|
|
99
|
+
* - If env var disables it → no-op.
|
|
100
|
+
* - If cache is fresh (< 24h) and indicates newer version → announce immediately.
|
|
101
|
+
* - If cache is stale → fire async fetch, update cache, announce if newer.
|
|
102
|
+
*/
|
|
103
|
+
export function checkForUpdate(currentVersion) {
|
|
104
|
+
if (isDisabled())
|
|
105
|
+
return;
|
|
106
|
+
const cache = readCache();
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
if (cache && cache.latest && now - cache.checkedAt < CACHE_TTL_MS) {
|
|
109
|
+
if (isNewer(cache.latest, currentVersion)) {
|
|
110
|
+
announceUpdate(cache.latest, currentVersion);
|
|
111
|
+
}
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
// Cache missing or stale — refresh in background, don't await.
|
|
115
|
+
fetchLatest()
|
|
116
|
+
.then(latest => {
|
|
117
|
+
writeCache({ checkedAt: now, latest });
|
|
118
|
+
if (latest && isNewer(latest, currentVersion)) {
|
|
119
|
+
announceUpdate(latest, currentVersion);
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
.catch(() => {
|
|
123
|
+
// Non-fatal.
|
|
124
|
+
});
|
|
125
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
5
|
"description": "Security MCP for vibe coding. 390 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis, +25 AI-native rules (MCP supply-chain, RAG/vector poisoning, agent loop DoS, public-prefix LLM keys, sandbox bypass). Plus Next.js, Supabase, Clerk, Stripe, Prisma, tRPC, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK, and the full AI-generated stack.",
|
|
6
6
|
"type": "module",
|