guardvibe 3.4.0 → 3.6.0

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/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to GuardVibe are documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.6.0] - 2026-06-07
9
+
10
+ ### Fixed — VG120 SSRF false-positive narrowing (sustain 0-FP) (438 rules / 37 tools)
11
+ - **VG120 (SSRF) no longer fires on URLs that are provably not request-controlled.** The regex flags `fetch(variable)` for any bare identifier; it now skips when the URL variable is assigned from a **literal `https://` constant** or **`process.env`** (including an env default parameter, e.g. `webhook = process.env.SOLUTIONS_WEBHOOK`), and skips **minified bundles**. `new URL(...)` is deliberately NOT treated as safe (it may wrap user input).
12
+ - **Validated against the corpus (clean old-vs-new diff): 1 false positive removed, 0 true positives lost, 0 new findings, 0 drift in any other rule.** Recall on genuinely user-controlled URLs is preserved (covered by tests).
13
+ - **Honest limitation:** URLs built from a constant *base variable* (`` `${apiBase}/path` ``) or returned from a helper still need real dataflow to classify safely, so they are intentionally left as-is for a future AST/dataflow engine rather than narrowed by regex (which would risk hiding a real SSRF). The precise signal for user-input→request flows already exists via the SSRF taint sink.
14
+ - No rule or tool changes (438 / 37).
15
+
16
+ Gate green (build / lint / test / self-audit PASS / A / 0).
17
+
18
+ ## [3.5.0] - 2026-06-07
19
+
20
+ ### Added — agent-native structured output (`guardvibe.agent.v1`) (438 rules / 37 tools)
21
+ - **New `agent` output format** that returns one stable, documented contract per finding so a coding agent can act on data instead of parsing prose: `{ id, name, severity, owasp, file, line, confidence, autoFixable, exactEdit, manualFix, verify }`.
22
+ - **`exactEdit`** is the structured line edit when the finding is auto-applicable (from `fix_code`); **`confidence`** is surfaced per finding; **`verify`** is a deterministic, runnable step (`guardvibe check … --format json`, expect the rule absent) so the agent can *prove* the fix landed.
23
+ - Exposed via CLI `guardvibe check <file> --format agent` and the MCP `scan_file` tool (`format: "agent"`). New module `src/tools/agent-output.ts` (`buildAgentReport`); deterministic, no new dependency.
24
+ - This unifies what was scattered across `fix_code`, `scan_file` and per-finding confidence into a single agent contract; complements `secure_this` (auto-apply loop) with a structured manual-fix path.
25
+ - No rule or tool changes (438 / 37).
26
+
27
+ Gate green (build / lint / test / self-audit PASS / A / 0).
28
+
8
29
  ## [3.4.0] - 2026-06-07
9
30
 
10
31
  ### Added — dependency reachability: is the vulnerable package actually imported? (438 rules / 37 tools)
package/README.md CHANGED
@@ -330,7 +330,9 @@ npx guardvibe-scan # Scan staged files (for pre-commit)
330
330
  npx guardvibe-scan --format sarif --output results.sarif # CI mode
331
331
 
332
332
  # Options (all scan commands)
333
- # --format markdown|json|sarif|buddy
333
+ # --format markdown|json|sarif|buddy|agent
334
+ # agent = guardvibe.agent.v1 — per finding: { id, severity, confidence, exactEdit, manualFix, verify }
335
+ # so an AI agent can apply the exact edit and run the verify step to prove the fix
334
336
  # --output <file> Write results to file
335
337
  # --fail-on <level> Exit 1 on findings: critical|high|medium|low|none
336
338
  # --full Bypass response-size caps (50 JSON / 30 markdown / 200-file taint)
package/build/cli/args.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * CLI argument parsing utilities
3
3
  */
4
- const VALID_FORMATS = new Set(["markdown", "json", "sarif", "buddy"]);
4
+ const VALID_FORMATS = new Set(["markdown", "json", "sarif", "buddy", "agent"]);
5
5
  export function getStringFlag(flags, key) {
6
6
  const val = flags[key];
7
7
  if (val === undefined || val === true)
package/build/cli/scan.js CHANGED
@@ -206,9 +206,17 @@ export async function runFileCheck(filePath, flags) {
206
206
  process.exit(1);
207
207
  }
208
208
  const format = validateFormat(flags);
209
- const formatArg = format === "json" ? "json" : format === "buddy" ? "buddy" : "markdown";
210
209
  const findings = analyzeFileSecurity(content, language, undefined, resolved, undefined);
211
- const result = renderFindings(findings, language, undefined, formatArg, resolved);
210
+ let result;
211
+ if (format === "agent") {
212
+ // Agent-native contract: finding + exact-edit + confidence + verify step.
213
+ const { buildAgentReport } = await import("../tools/agent-output.js");
214
+ result = JSON.stringify(buildAgentReport(findings, content, language, resolved));
215
+ }
216
+ else {
217
+ const formatArg = format === "json" ? "json" : format === "buddy" ? "buddy" : "markdown";
218
+ result = renderFindings(findings, language, undefined, formatArg, resolved);
219
+ }
212
220
  const outputFile = getOutputPath(flags);
213
221
  if (outputFile) {
214
222
  safeWriteOutput(outputFile, result);
package/build/cli.js CHANGED
@@ -44,7 +44,8 @@ function printUsage() {
44
44
  npx guardvibe-scan --format sarif --output results.sarif
45
45
 
46
46
  Options:
47
- --format <type> Output format: markdown (default), json, sarif, buddy
47
+ --format <type> Output format: markdown (default), json, sarif, buddy, agent
48
+ agent = guardvibe.agent.v1 (finding + exact edit + confidence + verify)
48
49
  --output <file> Write results to file instead of stdout
49
50
  --fail-on <level> Exit 1 when findings at this level or above exist
50
51
  critical (default) | high | medium | low | none
package/build/index.js CHANGED
@@ -42,6 +42,7 @@ import { formatHostFindings, redactSecrets } from "./server/types.js";
42
42
  import { verifyFix } from "./tools/verify-fix.js";
43
43
  import { fixCode as fixCodeTool } from "./tools/fix-code.js";
44
44
  import { secureThis } from "./tools/secure-this.js";
45
+ import { buildAgentReport } from "./tools/agent-output.js";
45
46
  import { analyzeAuthCoverage, formatAuthCoverage } from "./tools/auth-coverage.js";
46
47
  import { buildDeepScanPrompt, parseDeepScanResult, formatDeepScanFindings, callLLM } from "./tools/deep-scan.js";
47
48
  import { runFullAudit, formatAuditResult } from "./tools/full-audit.js";
@@ -506,9 +507,9 @@ server.tool("explain_remediation", "Pass a GuardVibe rule ID (e.g. VG154) to get
506
507
  });
507
508
  // Tool 23: Quick file scan — returns structured findings, not raw file contents.
508
509
  // guardvibe-ignore VG880
509
- server.tool("scan_file", "Scan a single file on disk by path for security vulnerabilities. Pass a file path — the tool reads the file itself. For inline code snippets, use check_code instead. Example: scan_file({file_path: 'src/api/route.ts'})", {
510
+ server.tool("scan_file", "Scan a single file on disk by path for security vulnerabilities. Pass a file path — the tool reads the file itself. For inline code snippets, use check_code instead. The 'agent' format returns the structured guardvibe.agent.v1 contract (finding + exact edit + confidence + verify step). Example: scan_file({file_path: 'src/api/route.ts', format: 'agent'})", {
510
511
  file_path: z.string().describe("Absolute or relative path to the file to scan"),
511
- format: z.enum(["markdown", "json"]).default("json").describe("Output format"),
512
+ format: z.enum(["markdown", "json", "agent"]).default("json").describe("Output format. 'agent' = machine-actionable guardvibe.agent.v1 (exact edits + confidence + verify)"),
512
513
  }, async ({ file_path, format }) => {
513
514
  const { readFileSync, existsSync } = await import("fs");
514
515
  const { resolve, extname, basename, dirname } = await import("path");
@@ -529,6 +530,11 @@ server.tool("scan_file", "Scan a single file on disk by path for security vulner
529
530
  }
530
531
  const rules = getRules();
531
532
  const findings = analyzeFileSecurity(content, language, undefined, resolved, dirname(resolved), rules);
533
+ if (format === "agent") {
534
+ const cwdA = dirname(resolved);
535
+ recordScan(cwdA, { toolName: "scan_file", filesScanned: 1, findings: findings.map(f => ({ severity: f.rule.severity, ruleId: f.rule.id })) });
536
+ return { content: [{ type: "text", text: JSON.stringify(buildAgentReport(findings, content, language, resolved, rules)) }] };
537
+ }
532
538
  const result = renderFindings(findings, language, undefined, format, resolved);
533
539
  const cwd = dirname(resolved);
534
540
  recordScan(cwd, { toolName: "scan_file", filesScanned: 1, findings: findings.map(f => ({ severity: f.rule.severity, ruleId: f.rule.id })) });
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Agent-native structured output (FAZ 4).
3
+ *
4
+ * Coding agents act on data, not prose. This builds one stable, documented
5
+ * contract per finding so an agent can: read severity + confidence, apply the
6
+ * exact edit deterministically when one exists, and run a verify step to prove
7
+ * the issue is gone. It unifies what was scattered across fix_code (edits),
8
+ * scan_file (suggested_fixes) and the finding's own confidence into a single
9
+ * `guardvibe.agent.v1` shape.
10
+ *
11
+ * No new dependency, fully deterministic (same findings + code → same report).
12
+ */
13
+ import type { Finding } from "./check-code.js";
14
+ import { type StructuredEdit } from "./fix-code.js";
15
+ import type { SecurityRule } from "../data/rules/types.js";
16
+ export interface AgentFinding {
17
+ id: string;
18
+ name: string;
19
+ severity: string;
20
+ owasp?: string;
21
+ file: string;
22
+ line: number;
23
+ confidence: "high" | "medium" | "low";
24
+ autoFixable: boolean;
25
+ exactEdit: StructuredEdit | null;
26
+ manualFix: string;
27
+ /** A deterministic, runnable step that proves the finding is resolved. */
28
+ verify: {
29
+ command: string;
30
+ expect: string;
31
+ };
32
+ }
33
+ export interface AgentReport {
34
+ schema: "guardvibe.agent.v1";
35
+ file: string;
36
+ total: number;
37
+ autoFixable: number;
38
+ findings: AgentFinding[];
39
+ }
40
+ /**
41
+ * Normalize a file's findings into the agent-native contract. `fixCode` is run
42
+ * once to attach exact edits to the findings that have one.
43
+ */
44
+ export declare function buildAgentReport(findings: Finding[], code: string, language: string, filePath: string, rules?: SecurityRule[]): AgentReport;
@@ -0,0 +1,45 @@
1
+ import { fixCode } from "./fix-code.js";
2
+ /**
3
+ * Normalize a file's findings into the agent-native contract. `fixCode` is run
4
+ * once to attach exact edits to the findings that have one.
5
+ */
6
+ export function buildAgentReport(findings, code, language, filePath, rules) {
7
+ // Map ruleId+line → structured edit (only auto-applicable rules yield one).
8
+ const edits = new Map();
9
+ if (findings.length > 0) {
10
+ try {
11
+ const parsed = JSON.parse(fixCode(code, language, undefined, filePath, "json", rules));
12
+ for (const fx of parsed.fixes ?? []) {
13
+ if (fx.edit)
14
+ edits.set(`${fx.ruleId}:${fx.line}`, fx.edit);
15
+ }
16
+ }
17
+ catch { /* fixCode is best-effort; findings still report without edits */ }
18
+ }
19
+ const agentFindings = findings.map(f => {
20
+ const exactEdit = edits.get(`${f.rule.id}:${f.line}`) ?? null;
21
+ return {
22
+ id: f.rule.id,
23
+ name: f.rule.name,
24
+ severity: f.rule.severity,
25
+ owasp: f.rule.owasp,
26
+ file: filePath,
27
+ line: f.line,
28
+ confidence: f.confidence,
29
+ autoFixable: !!exactEdit,
30
+ exactEdit,
31
+ manualFix: f.rule.fix,
32
+ verify: {
33
+ command: `npx guardvibe check ${filePath} --format json`,
34
+ expect: `${f.rule.id} no longer reported at ${filePath}:${f.line}`,
35
+ },
36
+ };
37
+ });
38
+ return {
39
+ schema: "guardvibe.agent.v1",
40
+ file: filePath,
41
+ total: agentFindings.length,
42
+ autoFixable: agentFindings.filter(f => f.autoFixable).length,
43
+ findings: agentFindings,
44
+ };
45
+ }
@@ -3,6 +3,7 @@ import { owaspRules } from "../data/rules/index.js";
3
3
  import { loadConfig } from "../utils/config.js";
4
4
  import { loadIgnoreFile, isIgnored } from "../utils/ignore.js";
5
5
  import { securityBanner } from "../utils/banner.js";
6
+ import { looksMinified } from "../utils/constants.js";
6
7
  /** CVE version-pin rule IDs are VG900-VG931 (and only these). Other VG9xx IDs
7
8
  * (VG983 Turso, VG990 SVG, VG998 OpenAI browser flag, etc.) are regular code-pattern
8
9
  * rules and should NOT be exempted from comment / string-literal skip logic. */
@@ -873,6 +874,25 @@ export function analyzeCode(code, language, framework, filePath, configDir, rule
873
874
  continue;
874
875
  }
875
876
  }
877
+ // VG120 (SSRF via User-Controlled URL): the regex flags `fetch(variable)` for any
878
+ // bare identifier, so it over-fires on constant/config endpoints. Safely skip the
879
+ // cases that are provably NOT request-controlled: a minified bundle (not real
880
+ // source; taint already skips these), or a URL variable assigned from a literal
881
+ // https:// constant or process.env (incl. an env default parameter). Template URLs
882
+ // built from a constant *base var* (`${apiBase}/path`) and method-returned URLs
883
+ // need real dataflow to classify and are deliberately LEFT for the AST engine —
884
+ // narrowing them by regex would risk hiding a genuine SSRF. `new URL(...)` is NOT
885
+ // treated as safe (it may wrap user input).
886
+ if (rule.id === "VG120") {
887
+ if (looksMinified(code))
888
+ continue;
889
+ const v = match[0].match(/\(\s*([A-Za-z_$]\w*)\s*[,)]/)?.[1];
890
+ if (v) {
891
+ const safeOrigin = new RegExp(`\\b${v}\\s*=\\s*(?:["'\\\`]https?:\\/\\/|process\\.env\\b)`);
892
+ if (safeOrigin.test(code))
893
+ continue;
894
+ }
895
+ }
876
896
  // Skip matches on comment lines and inside string literals.
877
897
  // CVE version-pin rules (VG900-VG931) are exempt — they scan package.json
878
898
  // dependency declarations where these contexts don't apply.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.4.0",
3
+ "version": "3.6.0",
4
4
  "mcpName": "io.github.goklab/guardvibe",
5
5
  "description": "Security infrastructure your AI can't be — deterministic, current past your model's training cutoff, whole-repo-aware, author-independent. Security MCP for vibe coding. 438 rules, 37 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 67 CVE rules refreshed daily from GHSA/OSV/CISA KEV — Miasma @redhat-cloud-services compromise, Next.js May 2026 13-advisory cluster, Drizzle/MikroORM/Kysely SQL injection, Axios proxy-auth redirect leak, Hono setCookie attribute injection, Clerk SSRF, tRPC prototype pollution, @tanstack supply-chain, node-ipc protestware, OpenClaude sandbox bypass, plus the full AI-generated stack (Supabase, Stripe, Prisma, Hono, GraphQL, Convex, Turso, Uploadthing, AI SDK). 68 AI-native rules including OWASP MCP Top 10 tool-description prompt injection (VG1068), model-controlled sandbox-disable flag detection (VG1063), Session messenger exfil endpoint IOC (VG1075), and CI/CD supply-chain hardening (VG1070 npm --expect-provenance / --ignore-scripts enforcement).",
6
6
  "type": "module",