guardvibe 3.2.0 → 3.3.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 +10 -0
- package/README.md +2 -1
- package/build/cli/scan.js +18 -3
- package/build/cli.js +2 -1
- package/build/tools/diff-aware.d.ts +13 -0
- package/build/tools/diff-aware.js +78 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ 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.3.0] - 2026-06-07
|
|
9
|
+
|
|
10
|
+
### Added — diff-aware scanning: block what you just wrote, not the backlog (438 rules / 37 tools)
|
|
11
|
+
- **`guardvibe diff [base]` is now diff-aware by default** — it reports only findings on lines the change actually **added**, instead of re-reporting pre-existing debt in every file you touched. This makes the gate actionable: it blocks the issues newly introduced vs the base, the ones an AI agent just wrote.
|
|
12
|
+
- **Transparent, never silent:** the report states the mode and how many pre-existing findings on unchanged lines were hidden (`preExistingHidden` in JSON; a note in markdown). `--all-lines` restores the whole-changed-file view.
|
|
13
|
+
- New module `src/tools/diff-aware.ts` — a pure, git-free unified-diff hunk parser (`addedLinesFromUnifiedDiff`) plus thin `git diff` wrappers (`getAddedLinesForDiff`, `getAddedLinesStaged`) and a `filterToAddedLines` helper. Unit-tested independent of git; works at any `--unified` context level; counts newly-added files as fully added; ignores deletions.
|
|
14
|
+
- No rule or tool changes (438 / 37).
|
|
15
|
+
|
|
16
|
+
Gate green (build / lint / test / self-audit PASS / A / 0).
|
|
17
|
+
|
|
8
18
|
## [3.2.0] - 2026-06-07
|
|
9
19
|
|
|
10
20
|
### Added — `secure_this`: close the loop from "warns" to "guarantees the fix landed" (438 rules / 37 tools)
|
package/README.md
CHANGED
|
@@ -292,7 +292,8 @@ All scanning tools support `format: "json"` for machine-readable output.
|
|
|
292
292
|
npx guardvibe scan [path] # Scan a directory for security issues
|
|
293
293
|
npx guardvibe scan . --format json # JSON output for automation
|
|
294
294
|
npx guardvibe check <file> # Scan a single file
|
|
295
|
-
npx guardvibe diff [base] # Scan
|
|
295
|
+
npx guardvibe diff [base] # Scan changed files — reports only newly-introduced issues
|
|
296
|
+
npx guardvibe diff [base] --all-lines # Include pre-existing findings in changed files too
|
|
296
297
|
|
|
297
298
|
# Close the loop — scan, apply verified fixes, re-verify
|
|
298
299
|
npx guardvibe secure-this <file> # Dry run: show the fixes that would land + remaining manual work
|
package/build/cli/scan.js
CHANGED
|
@@ -78,9 +78,13 @@ export async function runDiffScan(base, flags) {
|
|
|
78
78
|
const { execFileSync } = await import("child_process");
|
|
79
79
|
const { analyzeFileSecurity } = await import("../tools/file-security.js");
|
|
80
80
|
const { EXTENSION_MAP, CONFIG_FILE_MAP } = await import("../utils/constants.js");
|
|
81
|
+
const { getAddedLinesForDiff, filterToAddedLines } = await import("../tools/diff-aware.js");
|
|
81
82
|
const format = validateFormat(flags);
|
|
82
83
|
const outputFile = getOutputPath(flags);
|
|
83
84
|
const root = resolve(".");
|
|
85
|
+
// Diff-aware by default: report only issues on newly-added lines. --all-lines
|
|
86
|
+
// restores the old whole-changed-file behavior (surfaces pre-existing debt too).
|
|
87
|
+
const allLines = flags["all-lines"] === true;
|
|
84
88
|
let changedFiles;
|
|
85
89
|
try {
|
|
86
90
|
const output = execFileSync("git", ["diff", "--name-only", "--diff-filter=ACMR", base], { cwd: root, encoding: "utf-8" });
|
|
@@ -95,6 +99,7 @@ export async function runDiffScan(base, flags) {
|
|
|
95
99
|
return;
|
|
96
100
|
}
|
|
97
101
|
const allFindings = [];
|
|
102
|
+
let preExistingHidden = 0;
|
|
98
103
|
for (const relPath of changedFiles) {
|
|
99
104
|
const fullPath = resolve(root, relPath);
|
|
100
105
|
if (!existsSync(fullPath))
|
|
@@ -110,24 +115,34 @@ export async function runDiffScan(base, flags) {
|
|
|
110
115
|
try {
|
|
111
116
|
const content = readFileSync(fullPath, "utf-8");
|
|
112
117
|
const findings = analyzeFileSecurity(content, language, undefined, fullPath, root);
|
|
113
|
-
|
|
118
|
+
let kept = findings;
|
|
119
|
+
if (!allLines) {
|
|
120
|
+
const added = getAddedLinesForDiff(base, relPath, root);
|
|
121
|
+
kept = filterToAddedLines(findings, added);
|
|
122
|
+
preExistingHidden += findings.length - kept.length;
|
|
123
|
+
}
|
|
124
|
+
for (const f of kept) {
|
|
114
125
|
allFindings.push({ file: relPath, severity: f.rule.severity, name: f.rule.name, id: f.rule.id, line: f.line, fix: f.rule.fix });
|
|
115
126
|
}
|
|
116
127
|
}
|
|
117
128
|
catch { /* skip */ }
|
|
118
129
|
}
|
|
130
|
+
const mode = allLines ? "all changed lines" : "newly-introduced lines only";
|
|
119
131
|
let result;
|
|
120
132
|
if (format === "json") {
|
|
121
133
|
const critical = allFindings.filter(f => f.severity === "critical").length;
|
|
122
134
|
const high = allFindings.filter(f => f.severity === "high").length;
|
|
123
135
|
const medium = allFindings.filter(f => f.severity === "medium").length;
|
|
124
136
|
result = JSON.stringify({
|
|
125
|
-
summary: { total: allFindings.length, critical, high, medium, changedFiles: changedFiles.length, blocked: critical > 0 || high > 0 },
|
|
137
|
+
summary: { total: allFindings.length, critical, high, medium, changedFiles: changedFiles.length, blocked: critical > 0 || high > 0, diffAware: !allLines, preExistingHidden },
|
|
126
138
|
findings: allFindings,
|
|
127
139
|
});
|
|
128
140
|
}
|
|
129
141
|
else {
|
|
130
|
-
const lines = [`# GuardVibe Diff Report`, ``, `Base: ${base}`, `Changed files: ${changedFiles.length}`, `Issues: ${allFindings.length}`, ``];
|
|
142
|
+
const lines = [`# GuardVibe Diff Report`, ``, `Base: ${base}`, `Mode: ${mode}`, `Changed files: ${changedFiles.length}`, `Issues: ${allFindings.length}`, ``];
|
|
143
|
+
if (!allLines && preExistingHidden > 0) {
|
|
144
|
+
lines.push(`> ${preExistingHidden} pre-existing finding(s) on unchanged lines hidden — re-run with \`--all-lines\` to see them.`, ``);
|
|
145
|
+
}
|
|
131
146
|
if (allFindings.length === 0) {
|
|
132
147
|
lines.push(`All changed files passed security checks.`);
|
|
133
148
|
}
|
package/build/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ function printUsage() {
|
|
|
23
23
|
|
|
24
24
|
Commands:
|
|
25
25
|
npx guardvibe scan [path] Scan a directory for security issues
|
|
26
|
-
npx guardvibe diff [base] Scan
|
|
26
|
+
npx guardvibe diff [base] Scan changed files; reports only newly-introduced issues (--all-lines for whole files)
|
|
27
27
|
npx guardvibe check <file> Scan a single file for security issues
|
|
28
28
|
npx guardvibe doctor [path] Run host security audit
|
|
29
29
|
npx guardvibe audit [path] Full security audit with PASS/FAIL verdict
|
|
@@ -50,6 +50,7 @@ function printUsage() {
|
|
|
50
50
|
critical (default) | high | medium | low | none
|
|
51
51
|
--baseline <file> Compare against a previous scan JSON for fix tracking
|
|
52
52
|
--save-baseline Save current scan as baseline (.guardvibe-baseline.json)
|
|
53
|
+
--all-lines (diff) Report all findings in changed files, not just newly-added lines
|
|
53
54
|
--version, -V Print version and exit
|
|
54
55
|
--help, -h Show this help message
|
|
55
56
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse unified-diff text and return the set of 1-based line numbers that are
|
|
3
|
+
* ADDED in the new version of the file. Works at any `--unified` context level.
|
|
4
|
+
*/
|
|
5
|
+
export declare function addedLinesFromUnifiedDiff(diff: string): Set<number>;
|
|
6
|
+
/** Keep only findings whose line number is in the added-line set. */
|
|
7
|
+
export declare function filterToAddedLines<T extends {
|
|
8
|
+
line: number;
|
|
9
|
+
}>(findings: T[], added: Set<number>): T[];
|
|
10
|
+
/** Lines added in `relPath` relative to a git base ref (branch/commit/HEAD~N). */
|
|
11
|
+
export declare function getAddedLinesForDiff(base: string, relPath: string, cwd: string): Set<number>;
|
|
12
|
+
/** Lines added in `relPath` in the staged (index) changes — for pre-commit gating. */
|
|
13
|
+
export declare function getAddedLinesStaged(relPath: string, cwd: string): Set<number>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff-aware scanning — surface only the issues that were NEWLY introduced.
|
|
3
|
+
*
|
|
4
|
+
* Scanning a changed file whole re-reports pre-existing debt the author didn't
|
|
5
|
+
* touch, which trains agents (and people) to ignore the output. Diff-aware
|
|
6
|
+
* filtering keeps only findings that land on lines the current change actually
|
|
7
|
+
* ADDED, so the gate blocks what you just wrote — not the backlog.
|
|
8
|
+
*
|
|
9
|
+
* The hunk parser is pure and git-free (unit-tested); a thin wrapper shells out
|
|
10
|
+
* to `git diff` to obtain the unified diff for a file.
|
|
11
|
+
*/
|
|
12
|
+
import { execFileSync } from "child_process";
|
|
13
|
+
/**
|
|
14
|
+
* Parse unified-diff text and return the set of 1-based line numbers that are
|
|
15
|
+
* ADDED in the new version of the file. Works at any `--unified` context level.
|
|
16
|
+
*/
|
|
17
|
+
export function addedLinesFromUnifiedDiff(diff) {
|
|
18
|
+
const added = new Set();
|
|
19
|
+
let newLine = 0;
|
|
20
|
+
let inHunk = false;
|
|
21
|
+
for (const raw of diff.split("\n")) {
|
|
22
|
+
if (raw.startsWith("@@")) {
|
|
23
|
+
// @@ -oldStart,oldCount +newStart,newCount @@
|
|
24
|
+
const m = raw.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
25
|
+
if (m) {
|
|
26
|
+
newLine = parseInt(m[1], 10);
|
|
27
|
+
inHunk = true;
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!inHunk || raw === "")
|
|
32
|
+
continue;
|
|
33
|
+
if (raw.startsWith("+++") || raw.startsWith("---"))
|
|
34
|
+
continue;
|
|
35
|
+
const c = raw[0];
|
|
36
|
+
if (c === "+") {
|
|
37
|
+
added.add(newLine);
|
|
38
|
+
newLine++;
|
|
39
|
+
}
|
|
40
|
+
else if (c === "-") {
|
|
41
|
+
// deletion — present only in the old file, does not advance the new line
|
|
42
|
+
}
|
|
43
|
+
else if (c === "\\") {
|
|
44
|
+
// "" — metadata, ignore
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
// context line (leading space) — advances the new-file line counter
|
|
48
|
+
newLine++;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return added;
|
|
52
|
+
}
|
|
53
|
+
/** Keep only findings whose line number is in the added-line set. */
|
|
54
|
+
export function filterToAddedLines(findings, added) {
|
|
55
|
+
return findings.filter(f => added.has(f.line));
|
|
56
|
+
}
|
|
57
|
+
function gitDiffAddedLines(args, relPath, cwd) {
|
|
58
|
+
try {
|
|
59
|
+
const out = execFileSync("git", ["diff", "--no-color", "--unified=0", ...args, "--", relPath], {
|
|
60
|
+
cwd,
|
|
61
|
+
encoding: "utf-8",
|
|
62
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
63
|
+
});
|
|
64
|
+
return addedLinesFromUnifiedDiff(out);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Not a git repo, untracked path, or git unavailable — caller decides fallback.
|
|
68
|
+
return new Set();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Lines added in `relPath` relative to a git base ref (branch/commit/HEAD~N). */
|
|
72
|
+
export function getAddedLinesForDiff(base, relPath, cwd) {
|
|
73
|
+
return gitDiffAddedLines([base], relPath, cwd);
|
|
74
|
+
}
|
|
75
|
+
/** Lines added in `relPath` in the staged (index) changes — for pre-commit gating. */
|
|
76
|
+
export function getAddedLinesStaged(relPath, cwd) {
|
|
77
|
+
return gitDiffAddedLines(["--cached"], relPath, cwd);
|
|
78
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.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",
|