guardvibe 3.8.0 → 3.10.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,26 @@ 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.10.0] - 2026-06-07
9
+
10
+ ### Added — Season 3 S3-1: autonomous, prioritized threat intel (441 rules / 37 tools)
11
+ - **`npm run intel --scaffold`** now drafts a review-ready `cve-versions.ts` rule object **and** a version-range test stub for each uncovered advisory — turning "here's a gap" into "here's a rule + test to review." Drafts are printed only; nothing is auto-committed (the standing rule: never auto-commit untested rules).
12
+ - **CISA KEV prioritization:** the intel gap report cross-references the CISA Known-Exploited-Vulnerabilities catalog and surfaces actively-exploited CVEs first (🔥 marker), so what's being exploited in the wild — past your model's training cutoff — rises to the top. Degrades gracefully if the catalog is unreachable.
13
+ - New tested module `src/lib/cve-scaffold.ts`: `versionRangeRegex(introduced, fixed)` generalizes the hand-rolled 0-FP semver→regex work (single version / patch-range / minor-range / from-zero), and `scaffoldCveRule` emits the rule + test. Exact/`=` pins by default (a caret/tilde that resolves to the fix is left for the reviewer). The intel report (and `--json`) now also carries each gap's package + affected/fixed range + `kev` flag.
14
+ - No rule or tool changes (441 / 37) — this drafts rules; humans + the gate still decide.
15
+
16
+ Gate green (build / lint / test / self-audit PASS / A / 0).
17
+
18
+ ## [3.9.0] - 2026-06-07
19
+
20
+ ### Changed — diff-aware is now the default across every gating surface (441 rules / 37 tools)
21
+ - FAZ 2a made `guardvibe diff` diff-aware; this extends it to the surfaces that actually gate commits and PRs:
22
+ - **Pre-commit (`scan_staged` / `guardvibe-scan`)** now reports only findings on **newly-staged lines** by default — the hook blocks what you just wrote, not pre-existing debt in a file you touched. Opt out with `--all-lines` (CLI) or `diff_aware:false` (MCP).
23
+ - **`scan_changed_files` (MCP)** now reports only findings on **newly-added lines** vs the base by default (`diff_aware:false` for whole changed files).
24
+ - **Transparent, never silent:** both report how many pre-existing findings on unchanged lines were hidden (`preExistingHidden`; a note in the pre-commit markdown). Reuses the FAZ 2a git-free hunk parser and `getAddedLinesStaged`/`getAddedLinesForDiff`. Verified end-to-end in a temp repo. No rule or tool changes (441 / 37).
25
+
26
+ Gate green (build / lint / test / self-audit PASS / A / 0).
27
+
8
28
  ## [3.8.0] - 2026-06-07
9
29
 
10
30
  ### Fixed — auth-coverage no longer crashes on (and now understands) Clerk/Next.js middleware (441 rules / 37 tools)
package/README.md CHANGED
@@ -218,7 +218,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, CI `npm` provenance / `-
218
218
  | `check_code` | Analyze a code snippet for security issues |
219
219
  | `check_project` | Scan multiple files with security scoring (A-F) |
220
220
  | `scan_directory` | Scan a project directory from disk |
221
- | `scan_staged` | Pre-commit scan of git-staged files |
221
+ | `scan_staged` | Pre-commit scan of git-staged files — **diff-aware** (blocks only newly-staged lines; `diff_aware:false` for whole files) |
222
222
  | `scan_dependencies` | Check all dependencies for known CVEs (OSV) — annotates each vulnerable package with **reachability** (is it actually imported in your source?) |
223
223
  | `scan_secrets` | Detect leaked secrets, API keys, tokens |
224
224
  | `check_dependencies` | Check individual packages against OSV |
@@ -240,7 +240,7 @@ Malicious postinstall scripts, unpinned GitHub Actions, CI `npm` provenance / `-
240
240
  | `repo_security_posture` | Assess overall repository security posture and map sensitive areas |
241
241
  | `explain_remediation` | Get detailed remediation guidance with exploit scenarios and fix strategies |
242
242
  | `scan_file` | Real-time single-file scan — designed for post-edit hooks |
243
- | `scan_changed_files` | Scan only git-changed files — for PRs and incremental CI |
243
+ | `scan_changed_files` | Scan only git-changed files — for PRs and incremental CI; **diff-aware** (only newly-added lines; `diff_aware:false` for whole files) |
244
244
  | `security_stats` | Cumulative security dashboard — scans, fixes, grade trend over time |
245
245
  | `guardvibe_doctor` | **Host security audit** — CVE-2025-59536, CVE-2026-21852, MCP config, env scanner |
246
246
  | `audit_mcp_config` | Audit MCP server configurations for hook injection, file:// abuse, sensitive paths |
package/build/cli/scan.js CHANGED
@@ -26,7 +26,7 @@ export async function runScan() {
26
26
  }
27
27
  else {
28
28
  const { scanStaged } = await import("../tools/scan-staged.js");
29
- result = scanStaged(process.cwd(), format === "json" ? "json" : "markdown");
29
+ result = scanStaged(process.cwd(), format === "json" ? "json" : "markdown", undefined, { diffAware: flags["all-lines"] !== true });
30
30
  }
31
31
  if (outputFile) {
32
32
  safeWriteOutput(outputFile, result);
package/build/index.js CHANGED
@@ -238,12 +238,13 @@ server.tool("scan_secrets", "Scan files and directories for leaked secrets, API
238
238
  return { content: [{ type: "text", text: results }] };
239
239
  });
240
240
  // Tool 8: Scan git-staged files before committing
241
- server.tool("scan_staged", "Scan git-staged files for security vulnerabilities before committing. Run this before every commit to catch issues early. No input needed — automatically reads staged files.", {
241
+ server.tool("scan_staged", "Scan git-staged files for security vulnerabilities before committing. Run this before every commit to catch issues early. No input needed — automatically reads staged files. Diff-aware by default: reports only issues on newly-staged lines (set diff_aware:false for whole staged files).", {
242
242
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format: markdown (human) or json (machine-readable for agents)"),
243
- }, async ({ format }) => {
243
+ diff_aware: z.boolean().default(true).describe("Report only findings on newly-staged lines (true, default) vs. all lines in staged files (false)"),
244
+ }, async ({ format, diff_aware }) => {
244
245
  const rules = getRules();
245
246
  const cwd = process.cwd();
246
- const results = scanStaged(cwd, format, rules);
247
+ const results = scanStaged(cwd, format, rules, { diffAware: diff_aware });
247
248
  let findingCount = 0;
248
249
  try {
249
250
  const parsed = JSON.parse(results);
@@ -556,15 +557,17 @@ server.tool("scan_file", "Scan a single file on disk by path for security vulner
556
557
  return { content: [{ type: "text", text: mergeStatsIntoOutput(result, summary, format) }] };
557
558
  });
558
559
  // Tool 24: Scan changed files only — for incremental CI/CD and PR workflows
559
- server.tool("scan_changed_files", "Scan only files that have changed since a given git ref (branch, commit, or HEAD~N). Ideal for PR checks, pre-push hooks, and incremental CI. Returns findings only for modified/added files.", {
560
+ server.tool("scan_changed_files", "Scan only files that have changed since a given git ref (branch, commit, or HEAD~N). Ideal for PR checks, pre-push hooks, and incremental CI. Diff-aware by default: returns only findings on newly-added lines (set diff_aware:false for whole changed files).", {
560
561
  path: z.string().default(".").describe("Repository root path"),
561
562
  base: z.string().default("HEAD~1").describe("Git ref to diff against (e.g. 'main', 'HEAD~3', commit SHA)"),
562
563
  format: z.enum(["markdown", "json"]).default("markdown").describe("Output format"),
563
- }, async ({ path: repoPath, base, format }) => {
564
+ diff_aware: z.boolean().default(true).describe("Report only newly-introduced findings on added lines (true, default) vs. all findings in changed files (false)"),
565
+ }, async ({ path: repoPath, base, format, diff_aware }) => {
564
566
  const { execFileSync } = await import("child_process");
565
567
  const { readFileSync, existsSync } = await import("fs");
566
568
  const { resolve, extname, basename } = await import("path");
567
569
  const { EXTENSION_MAP, CONFIG_FILE_MAP } = await import("./utils/constants.js");
570
+ const { getAddedLinesForDiff, filterToAddedLines } = await import("./tools/diff-aware.js");
568
571
  const root = resolve(repoPath);
569
572
  let changedFiles;
570
573
  try {
@@ -582,6 +585,7 @@ server.tool("scan_changed_files", "Scan only files that have changed since a giv
582
585
  }
583
586
  const rules = getRules();
584
587
  const allFindings = [];
588
+ let preExistingHidden = 0;
585
589
  for (const relPath of changedFiles) {
586
590
  const fullPath = resolve(root, relPath);
587
591
  if (!existsSync(fullPath))
@@ -596,7 +600,13 @@ server.tool("scan_changed_files", "Scan only files that have changed since a giv
596
600
  continue;
597
601
  try {
598
602
  const content = readFileSync(fullPath, "utf-8");
599
- const findings = analyzeFileSecurity(content, language, undefined, fullPath, root, rules);
603
+ let findings = analyzeFileSecurity(content, language, undefined, fullPath, root, rules);
604
+ if (diff_aware) {
605
+ const added = getAddedLinesForDiff(base, relPath, root);
606
+ const kept = filterToAddedLines(findings, added);
607
+ preExistingHidden += findings.length - kept.length;
608
+ findings = kept;
609
+ }
600
610
  for (const f of findings) {
601
611
  allFindings.push({
602
612
  file: relPath, id: f.rule.id, name: f.rule.name,
@@ -615,7 +625,7 @@ server.tool("scan_changed_files", "Scan only files that have changed since a giv
615
625
  const high = allFindings.filter(f => f.severity === "high").length;
616
626
  const medium = allFindings.filter(f => f.severity === "medium").length;
617
627
  return { content: [{ type: "text", text: mergeStatsIntoOutput(JSON.stringify({
618
- summary: { total: allFindings.length, critical, high, medium, low: 0, blocked: critical > 0 || high > 0, changedFiles: changedFiles.length },
628
+ summary: { total: allFindings.length, critical, high, medium, low: 0, blocked: critical > 0 || high > 0, changedFiles: changedFiles.length, diffAware: diff_aware, preExistingHidden },
619
629
  findings: allFindings,
620
630
  }), statsSummary, format) }] };
621
631
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * CVE rule scaffolding (Season 3, S3-1).
3
+ *
4
+ * Turns a "package X is vulnerable in [introduced, fixed)" advisory into a
5
+ * review-ready `cve-versions.ts` rule object + a version-range test stub, so the
6
+ * daily intel pipeline drafts rules instead of just reporting gaps. The output is
7
+ * a DRAFT for human review + the normal gate/corpus validation — never
8
+ * auto-committed (the hard-won "never auto-commit untested rules" rule stands).
9
+ *
10
+ * 0-FP-leaning by default: the generated pin matcher only accepts exact / `=`
11
+ * pins (a `^`/`~` range usually resolves to the fixed patch and is left for the
12
+ * reviewer to add where the range spans minors). Pure + deterministic.
13
+ */
14
+ /**
15
+ * Regex fragment matching a semver version string in [introduced, fixed).
16
+ * Handles the shapes CVE advisories use: single version, a patch range within a
17
+ * minor, a minor range within a major, and from-zero. Multi-major ranges get a
18
+ * best-effort pattern (flagged for review by the scaffold).
19
+ */
20
+ export declare function versionRangeRegex(introduced: string, fixed: string): string;
21
+ export interface AdvisoryInput {
22
+ ruleId: string;
23
+ pkg: string;
24
+ introduced: string;
25
+ fixed: string;
26
+ cve?: string;
27
+ ghsa?: string;
28
+ severity: string;
29
+ summary: string;
30
+ }
31
+ /**
32
+ * Build a review-ready cve-versions.ts rule object + a matching test stub for an
33
+ * advisory. Returns source strings to paste (after review) — NOT auto-applied.
34
+ */
35
+ export declare function scaffoldCveRule(a: AdvisoryInput): {
36
+ rule: string;
37
+ test: string;
38
+ };
@@ -0,0 +1,110 @@
1
+ // guardvibe-ignore — builds CVE-rule regex *strings* from semver ranges; the
2
+ // `["']`/`\s*` fragments here are detector-pattern templates, not vulnerable code.
3
+ /**
4
+ * CVE rule scaffolding (Season 3, S3-1).
5
+ *
6
+ * Turns a "package X is vulnerable in [introduced, fixed)" advisory into a
7
+ * review-ready `cve-versions.ts` rule object + a version-range test stub, so the
8
+ * daily intel pipeline drafts rules instead of just reporting gaps. The output is
9
+ * a DRAFT for human review + the normal gate/corpus validation — never
10
+ * auto-committed (the hard-won "never auto-commit untested rules" rule stands).
11
+ *
12
+ * 0-FP-leaning by default: the generated pin matcher only accepts exact / `=`
13
+ * pins (a `^`/`~` range usually resolves to the fixed patch and is left for the
14
+ * reviewer to add where the range spans minors). Pure + deterministic.
15
+ */
16
+ function parseVer(v) {
17
+ if (!v || v === "0")
18
+ return [0, 0, 0];
19
+ const m = v.match(/^(\d+)(?:\.(\d+))?(?:\.(\d+))?/);
20
+ if (!m)
21
+ return [0, 0, 0];
22
+ return [Number(m[1] || 0), Number(m[2] || 0), Number(m[3] || 0)];
23
+ }
24
+ /** Regex fragment matching integers in [min, max] (enumerated for the small ranges CVEs use). */
25
+ function numRange(min, max) {
26
+ if (max < min)
27
+ return "(?!)"; // never matches (empty range)
28
+ if (min === max)
29
+ return String(min);
30
+ if (max - min <= 40) {
31
+ const alts = [];
32
+ for (let n = min; n <= max; n++)
33
+ alts.push(String(n));
34
+ return "(?:" + alts.join("|") + ")";
35
+ }
36
+ return "\\d+"; // unexpected for a CVE range — fall back to any
37
+ }
38
+ /**
39
+ * Regex fragment matching a semver version string in [introduced, fixed).
40
+ * Handles the shapes CVE advisories use: single version, a patch range within a
41
+ * minor, a minor range within a major, and from-zero. Multi-major ranges get a
42
+ * best-effort pattern (flagged for review by the scaffold).
43
+ */
44
+ export function versionRangeRegex(introduced, fixed) {
45
+ const [iM, im, ip] = parseVer(introduced);
46
+ const [fM, fm, fp] = parseVer(fixed);
47
+ const alts = [];
48
+ if (iM === fM) {
49
+ if (im === fm) {
50
+ // Same minor: patches ip .. fp-1
51
+ alts.push(`${iM}\\.${im}\\.${numRange(ip, fp - 1)}`);
52
+ }
53
+ else {
54
+ if (ip === 0) {
55
+ // Introduced minor starts at .0 → folds into the full-minor sweep im..fm-1
56
+ alts.push(`${iM}\\.${numRange(im, fm - 1)}\\.\\d+`);
57
+ }
58
+ else {
59
+ // ip > 0: approximate the introduced minor as full (reviewer tightens)
60
+ alts.push(`${iM}\\.${im}\\.\\d+`);
61
+ if (fm - 1 >= im + 1)
62
+ alts.push(`${iM}\\.${numRange(im + 1, fm - 1)}\\.\\d+`);
63
+ }
64
+ if (fp > 0)
65
+ alts.push(`${iM}\\.${fm}\\.${numRange(0, fp - 1)}`);
66
+ }
67
+ }
68
+ else {
69
+ // Multi-major (rare): majors iM..fM-1 in full, then the start of the fixed major.
70
+ alts.push(`${numRange(iM, fM - 1)}\\.\\d+\\.\\d+`);
71
+ if (fm > 0)
72
+ alts.push(`${fM}\\.${numRange(0, fm - 1)}\\.\\d+`);
73
+ if (fp > 0)
74
+ alts.push(`${fM}\\.${fm}\\.${numRange(0, fp - 1)}`);
75
+ }
76
+ const parts = alts.filter(Boolean);
77
+ return parts.length === 1 ? parts[0] : "(?:" + parts.join("|") + ")";
78
+ }
79
+ /** Escape a package name for use inside a regex literal. */
80
+ function escapePkg(pkg) {
81
+ return pkg.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
82
+ }
83
+ /**
84
+ * Build a review-ready cve-versions.ts rule object + a matching test stub for an
85
+ * advisory. Returns source strings to paste (after review) — NOT auto-applied.
86
+ */
87
+ export function scaffoldCveRule(a) {
88
+ const frag = versionRangeRegex(a.introduced, a.fixed);
89
+ const ids = [a.cve, a.ghsa].filter(Boolean).join(" / ");
90
+ const pattern = `/["']${escapePkg(a.pkg)}["']\\s*:\\s*["']=?${frag}["']/g`;
91
+ const safeSummary = a.summary.replace(/"/g, "'").replace(/\s+/g, " ").trim();
92
+ const rule = ` {
93
+ id: "${a.ruleId}",
94
+ name: "${a.pkg} vulnerability${ids ? ` (${ids})` : ""}",
95
+ severity: "${a.severity}",
96
+ owasp: "A06:2025 Vulnerable and Outdated Components",
97
+ description:
98
+ "${safeSummary} Affected: ${a.pkg} ${a.introduced}–<${a.fixed}; fixed in ${a.fixed}. REVIEW: confirm the affected range, and add ~/^ prefixes only where they don't resolve to the fix, before merging.",
99
+ pattern:
100
+ ${pattern},
101
+ languages: ["json"],
102
+ fix: "Upgrade ${a.pkg} to ${a.fixed}+: npm install ${a.pkg}@latest.",
103
+ compliance: ["SOC2:CC7.1"],
104
+ },`;
105
+ const test = ` describe("${a.ruleId} - ${a.pkg} (${ids})", () => {
106
+ it("detects affected ${a.pkg}", () => testRule("${a.ruleId}", '"${a.pkg}": "${a.introduced}"', true));
107
+ it("ignores fixed ${a.pkg}", () => testRule("${a.ruleId}", '"${a.pkg}": "${a.fixed}"', false));
108
+ });`;
109
+ return { rule, test };
110
+ }
@@ -1,2 +1,4 @@
1
1
  import type { SecurityRule } from "../data/rules/types.js";
2
- export declare function scanStaged(cwd?: string, format?: "markdown" | "json", rules?: SecurityRule[]): string;
2
+ export declare function scanStaged(cwd?: string, format?: "markdown" | "json", rules?: SecurityRule[], opts?: {
3
+ diffAware?: boolean;
4
+ }): string;
@@ -2,6 +2,7 @@ import { execFileSync } from "child_process";
2
2
  import { extname, basename } from "path";
3
3
  import { formatFindingsJson } from "./check-code.js";
4
4
  import { analyzeFileSecurity } from "./file-security.js";
5
+ import { getAddedLinesStaged, filterToAddedLines } from "./diff-aware.js";
5
6
  import { securityBanner } from "../utils/banner.js";
6
7
  const EXTENSION_MAP = {
7
8
  ".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
@@ -58,7 +59,10 @@ function detectLanguage(filePath) {
58
59
  return configLang;
59
60
  return null;
60
61
  }
61
- export function scanStaged(cwd = process.cwd(), format = "markdown", rules) {
62
+ export function scanStaged(cwd = process.cwd(), format = "markdown", rules, opts = {}) {
63
+ // Diff-aware by default: the pre-commit gate blocks issues on lines you just
64
+ // staged, not pre-existing debt in files you happened to touch. --all-lines opts out.
65
+ const diffAware = opts.diffAware !== false;
62
66
  const stagedFiles = getStagedFiles(cwd);
63
67
  if (stagedFiles.length === 0) {
64
68
  return [
@@ -69,6 +73,7 @@ export function scanStaged(cwd = process.cwd(), format = "markdown", rules) {
69
73
  }
70
74
  const results = [];
71
75
  const skippedFiles = [];
76
+ let preExistingHidden = 0;
72
77
  for (const filePath of stagedFiles) {
73
78
  const language = detectLanguage(filePath);
74
79
  if (!language) {
@@ -80,7 +85,13 @@ export function scanStaged(cwd = process.cwd(), format = "markdown", rules) {
80
85
  skippedFiles.push(filePath);
81
86
  continue;
82
87
  }
83
- const findings = analyzeFileSecurity(content, language, undefined, filePath, cwd, rules);
88
+ let findings = analyzeFileSecurity(content, language, undefined, filePath, cwd, rules);
89
+ if (diffAware) {
90
+ const added = getAddedLinesStaged(filePath, cwd);
91
+ const kept = filterToAddedLines(findings, added);
92
+ preExistingHidden += findings.length - kept.length;
93
+ findings = kept;
94
+ }
84
95
  if (findings.length > 0) {
85
96
  results.push({ path: filePath, findings });
86
97
  }
@@ -102,10 +113,14 @@ export function scanStaged(cwd = process.cwd(), format = "markdown", rules) {
102
113
  "# GuardVibe Pre-Commit Report",
103
114
  "",
104
115
  `Staged files scanned: ${scannedCount}`,
116
+ `Mode: ${diffAware ? "newly-staged lines only" : "all lines"}`,
105
117
  `Total issues: ${totalIssues}`,
106
118
  `Security Score: ${grade} (${score}/100)`,
107
119
  "",
108
120
  ];
121
+ if (diffAware && preExistingHidden > 0) {
122
+ lines.push(`> ${preExistingHidden} pre-existing finding(s) on unchanged lines hidden — re-run with \`--all-lines\` to see them.`, "");
123
+ }
109
124
  if (totalIssues > 0) {
110
125
  lines.push("## Summary", "", "| Severity | Count |", "|----------|-------|");
111
126
  if (totalCritical > 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "guardvibe",
3
- "version": "3.8.0",
3
+ "version": "3.10.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. 441 rules, 37 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 70 CVE rules refreshed daily from GHSA/OSV/CISA KEV — React Router 7 cluster, DOMPurify XSS, Better Auth bypass, 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",