guardvibe 3.0.49 → 3.0.51
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 +6 -0
- package/build/tools/check-project.js +6 -24
- package/build/tools/scan-directory.js +5 -10
- package/build/utils/config.d.ts +8 -0
- package/build/utils/config.js +3 -0
- package/build/utils/scoring.d.ts +25 -0
- package/build/utils/scoring.js +52 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -278,6 +278,7 @@ npx guardvibe diff [base] # Scan only changed files since git ref
|
|
|
278
278
|
npx guardvibe audit [path] # Full audit with PASS/FAIL verdict + hash
|
|
279
279
|
npx guardvibe audit . --format json # JSON output for CI pipelines
|
|
280
280
|
npx guardvibe audit --skip-deps # Skip dependency CVE check
|
|
281
|
+
npx guardvibe audit --full # Disable MCP-output truncation (full finding set)
|
|
281
282
|
|
|
282
283
|
# Host security audit
|
|
283
284
|
npx guardvibe doctor # Host hardening audit (project scope)
|
|
@@ -299,6 +300,7 @@ npx guardvibe-scan --format sarif --output results.sarif # CI mode
|
|
|
299
300
|
# --format markdown|json|sarif|buddy
|
|
300
301
|
# --output <file> Write results to file
|
|
301
302
|
# --fail-on <level> Exit 1 on findings: critical|high|medium|low|none
|
|
303
|
+
# --full Bypass response-size caps (50 JSON / 30 markdown / 200-file taint)
|
|
302
304
|
```
|
|
303
305
|
|
|
304
306
|
## Plugin System
|
|
@@ -514,6 +516,9 @@ Create a `.guardviberc` JSON file in your project root to customize GuardVibe be
|
|
|
514
516
|
}
|
|
515
517
|
],
|
|
516
518
|
"requiredControls": ["SOC2:CC6.1"]
|
|
519
|
+
},
|
|
520
|
+
"scoring": {
|
|
521
|
+
"densityModel": "exponential"
|
|
517
522
|
}
|
|
518
523
|
}
|
|
519
524
|
```
|
|
@@ -531,6 +536,7 @@ Create a `.guardviberc` JSON file in your project root to customize GuardVibe be
|
|
|
531
536
|
| `compliance.failOn` | `string` | `"high"` | Minimum severity that causes compliance failure |
|
|
532
537
|
| `compliance.exceptions` | `PolicyException[]` | `[]` | Approved exceptions with expiration dates |
|
|
533
538
|
| `compliance.requiredControls` | `string[]` | -- | Controls that must pass regardless of exceptions |
|
|
539
|
+
| `scoring.densityModel` | `"linear" \| "exponential"` | `"linear"` | Score decay curve. `linear` matches pre-v3.0.50 (cliff at density 5). `exponential` keeps resolution past density 5 — smoother decay for large repos. Severity caps (1+ critical → max C/60, 1+ high → max B/75) apply under both. |
|
|
534
540
|
|
|
535
541
|
## Security
|
|
536
542
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { analyzeCode, formatFindingsJson } from "./check-code.js";
|
|
2
|
+
import { calculateScore, scoreToGrade } from "../utils/scoring.js";
|
|
3
|
+
import { loadConfig } from "../utils/config.js";
|
|
2
4
|
const extensionMap = {
|
|
3
5
|
".js": "javascript",
|
|
4
6
|
".jsx": "javascript",
|
|
@@ -42,29 +44,6 @@ function detectLanguage(filePath) {
|
|
|
42
44
|
const ext = filePath.match(/\.[^.]+$/)?.[0]?.toLowerCase();
|
|
43
45
|
return ext ? extensionMap[ext] ?? null : null;
|
|
44
46
|
}
|
|
45
|
-
function calculateScore(critical, high, medium, fileCount = 1) {
|
|
46
|
-
// Calibrated: medium issues are informational (0.5 weight), high issues are real (5x), critical are severe (15x)
|
|
47
|
-
const weighted = critical * 15 + high * 5 + medium * 0.5;
|
|
48
|
-
const density = weighted / Math.max(fileCount, 1);
|
|
49
|
-
let score = Math.max(0, Math.min(100, Math.round(100 - Math.min(density, 5) * 20)));
|
|
50
|
-
// Severity caps: CRITICAL findings can never get A/B, HIGH can never get A
|
|
51
|
-
if (critical > 0)
|
|
52
|
-
score = Math.min(score, 60); // cap at C
|
|
53
|
-
if (high > 0)
|
|
54
|
-
score = Math.min(score, 75); // cap at B
|
|
55
|
-
return score;
|
|
56
|
-
}
|
|
57
|
-
function scoreToGrade(score) {
|
|
58
|
-
if (score >= 90)
|
|
59
|
-
return "A";
|
|
60
|
-
if (score >= 75)
|
|
61
|
-
return "B";
|
|
62
|
-
if (score >= 60)
|
|
63
|
-
return "C";
|
|
64
|
-
if (score >= 40)
|
|
65
|
-
return "D";
|
|
66
|
-
return "F";
|
|
67
|
-
}
|
|
68
47
|
export function checkProject(files, format = "markdown", rules) {
|
|
69
48
|
const results = [];
|
|
70
49
|
const skippedFiles = [];
|
|
@@ -85,7 +64,10 @@ export function checkProject(files, format = "markdown", rules) {
|
|
|
85
64
|
const totalHigh = allFindings.filter((f) => f.rule.severity === "high").length;
|
|
86
65
|
const totalMedium = allFindings.filter((f) => f.rule.severity === "medium").length;
|
|
87
66
|
const totalIssues = totalCritical + totalHigh + totalMedium;
|
|
88
|
-
const
|
|
67
|
+
const config = loadConfig();
|
|
68
|
+
const score = calculateScore(totalCritical, totalHigh, totalMedium, scannedCount, {
|
|
69
|
+
densityModel: config.scoring?.densityModel,
|
|
70
|
+
});
|
|
89
71
|
const grade = scoreToGrade(score);
|
|
90
72
|
if (format === "json") {
|
|
91
73
|
return formatFindingsJson(allFindings, { grade, score });
|
|
@@ -7,6 +7,7 @@ import { loadConfig } from "../utils/config.js";
|
|
|
7
7
|
import { DEFAULT_EXCLUDES, EXTENSION_MAP, CONFIG_FILE_MAP } from "../utils/constants.js";
|
|
8
8
|
import { walkDirectory } from "../utils/walk-directory.js";
|
|
9
9
|
import { securityBanner } from "../utils/banner.js";
|
|
10
|
+
import { calculateScore, scoreToGrade } from "../utils/scoring.js";
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
const pkg = require("../../package.json");
|
|
12
13
|
// GuardVibe version — used in scan metadata
|
|
@@ -110,17 +111,11 @@ export function scanDirectory(path, recursive = true, exclude = [], format = "ma
|
|
|
110
111
|
const totalHigh = allFindings.filter(f => f.rule.severity === "high").length;
|
|
111
112
|
const totalMedium = allFindings.filter(f => f.rule.severity === "medium").length;
|
|
112
113
|
const totalIssues = totalCritical + totalHigh + totalMedium;
|
|
113
|
-
// Density-based scoring calibrated against real Next.js projects.
|
|
114
|
-
// A clean Next.js project with ~200 medium findings in ~800 files should score ~B.
|
|
115
|
-
// Critical issues have the most impact; medium issues are informational.
|
|
116
114
|
const filesScanned = metadata.filesScanned || 1;
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const score = Math.max(0, Math.min(100, Math.round(100 - Math.min(density, 5) * 20)));
|
|
122
|
-
// Grade boundaries match full-audit so the section sub-grade and overall verdict agree.
|
|
123
|
-
const grade = score >= 90 ? "A" : score >= 75 ? "B" : score >= 50 ? "C" : score >= 25 ? "D" : "F";
|
|
115
|
+
const score = calculateScore(totalCritical, totalHigh, totalMedium, filesScanned, {
|
|
116
|
+
densityModel: config.scoring?.densityModel,
|
|
117
|
+
});
|
|
118
|
+
const grade = scoreToGrade(score);
|
|
124
119
|
// Baseline comparison
|
|
125
120
|
let baselineDiff = null;
|
|
126
121
|
let previousBaseline = null;
|
package/build/utils/config.d.ts
CHANGED
|
@@ -33,6 +33,14 @@ export interface GuardVibeConfig {
|
|
|
33
33
|
path: string;
|
|
34
34
|
reason: string;
|
|
35
35
|
}>;
|
|
36
|
+
/** Score calculation tweaks. Default density model is "linear" (matches
|
|
37
|
+
* pre-v3.0.50 behavior). Set to "exponential" for smoother decay past
|
|
38
|
+
* density 5 — projects with many medium findings will see slightly higher
|
|
39
|
+
* scores under exponential, projects with concentrated criticals will see
|
|
40
|
+
* lower. CI gates set on absolute score should keep the default. */
|
|
41
|
+
scoring?: {
|
|
42
|
+
densityModel?: "linear" | "exponential";
|
|
43
|
+
};
|
|
36
44
|
}
|
|
37
45
|
export declare function loadConfig(dir?: string): GuardVibeConfig;
|
|
38
46
|
export declare function resetConfigCache(): void;
|
package/build/utils/config.js
CHANGED
|
@@ -69,6 +69,9 @@ export function loadConfig(dir) {
|
|
|
69
69
|
} : undefined,
|
|
70
70
|
authFunctions: Array.isArray(parsed.authFunctions) ? parsed.authFunctions : undefined,
|
|
71
71
|
authExceptions: Array.isArray(parsed.authExceptions) ? parsed.authExceptions : undefined,
|
|
72
|
+
scoring: parsed.scoring && typeof parsed.scoring === "object" ? {
|
|
73
|
+
densityModel: parsed.scoring.densityModel === "exponential" ? "exponential" : "linear",
|
|
74
|
+
} : undefined,
|
|
72
75
|
};
|
|
73
76
|
}
|
|
74
77
|
catch { }
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for security score / grade calculation.
|
|
3
|
+
*
|
|
4
|
+
* Goals:
|
|
5
|
+
* 1. Same finding counts produce the same score across scan-directory,
|
|
6
|
+
* check-project, full-audit, and CLI output.
|
|
7
|
+
* 2. Severity caps so a critical finding cannot ever look like a clean run
|
|
8
|
+
* (1+ critical → max C/60, 1+ high → max B/75).
|
|
9
|
+
* 3. Optional exponential density decay (`scoring.densityModel: "exponential"`
|
|
10
|
+
* in .guardviberc) for projects that want resolution past density 5
|
|
11
|
+
* instead of the linear cliff.
|
|
12
|
+
*
|
|
13
|
+
* Default density formula stays linear so existing CI thresholds don't shift.
|
|
14
|
+
*/
|
|
15
|
+
export type DensityModel = "linear" | "exponential";
|
|
16
|
+
export interface ScoreOptions {
|
|
17
|
+
/** Density curve. "linear" (default) is `100 - min(density, 5) * 20`.
|
|
18
|
+
* "exponential" is `100 * exp(-density / 3)` — smoother, no cliff. */
|
|
19
|
+
densityModel?: DensityModel;
|
|
20
|
+
}
|
|
21
|
+
/** Severity caps applied AFTER the density-derived score. */
|
|
22
|
+
export declare const CRITICAL_SCORE_CAP = 60;
|
|
23
|
+
export declare const HIGH_SCORE_CAP = 75;
|
|
24
|
+
export declare function calculateScore(critical: number, high: number, medium: number, fileCount?: number, options?: ScoreOptions): number;
|
|
25
|
+
export declare function scoreToGrade(score: number): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for security score / grade calculation.
|
|
3
|
+
*
|
|
4
|
+
* Goals:
|
|
5
|
+
* 1. Same finding counts produce the same score across scan-directory,
|
|
6
|
+
* check-project, full-audit, and CLI output.
|
|
7
|
+
* 2. Severity caps so a critical finding cannot ever look like a clean run
|
|
8
|
+
* (1+ critical → max C/60, 1+ high → max B/75).
|
|
9
|
+
* 3. Optional exponential density decay (`scoring.densityModel: "exponential"`
|
|
10
|
+
* in .guardviberc) for projects that want resolution past density 5
|
|
11
|
+
* instead of the linear cliff.
|
|
12
|
+
*
|
|
13
|
+
* Default density formula stays linear so existing CI thresholds don't shift.
|
|
14
|
+
*/
|
|
15
|
+
/** Severity caps applied AFTER the density-derived score. */
|
|
16
|
+
export const CRITICAL_SCORE_CAP = 60; // 1+ critical → cannot exceed C
|
|
17
|
+
export const HIGH_SCORE_CAP = 75; // 1+ high → cannot exceed B
|
|
18
|
+
/** Severity weights for density. Calibrated against real Next.js projects:
|
|
19
|
+
* a clean Next.js app with ~200 medium findings in ~800 files lands near B. */
|
|
20
|
+
const WEIGHT_CRITICAL = 15;
|
|
21
|
+
const WEIGHT_HIGH = 5;
|
|
22
|
+
const WEIGHT_MEDIUM = 0.5;
|
|
23
|
+
/** Exponential decay constant — density = 3 produces ~37 (D). */
|
|
24
|
+
const EXPONENTIAL_K = 3;
|
|
25
|
+
export function calculateScore(critical, high, medium, fileCount = 1, options) {
|
|
26
|
+
const weighted = critical * WEIGHT_CRITICAL + high * WEIGHT_HIGH + medium * WEIGHT_MEDIUM;
|
|
27
|
+
const density = weighted / Math.max(fileCount, 1);
|
|
28
|
+
let score;
|
|
29
|
+
if (options?.densityModel === "exponential") {
|
|
30
|
+
score = Math.round(100 * Math.exp(-density / EXPONENTIAL_K));
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
score = Math.round(100 - Math.min(density, 5) * 20);
|
|
34
|
+
}
|
|
35
|
+
score = Math.max(0, Math.min(100, score));
|
|
36
|
+
if (critical > 0)
|
|
37
|
+
score = Math.min(score, CRITICAL_SCORE_CAP);
|
|
38
|
+
if (high > 0)
|
|
39
|
+
score = Math.min(score, HIGH_SCORE_CAP);
|
|
40
|
+
return score;
|
|
41
|
+
}
|
|
42
|
+
export function scoreToGrade(score) {
|
|
43
|
+
if (score >= 90)
|
|
44
|
+
return "A";
|
|
45
|
+
if (score >= 75)
|
|
46
|
+
return "B";
|
|
47
|
+
if (score >= 50)
|
|
48
|
+
return "C";
|
|
49
|
+
if (score >= 25)
|
|
50
|
+
return "D";
|
|
51
|
+
return "F";
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardvibe",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.51",
|
|
4
4
|
"mcpName": "io.github.goklab/guardvibe",
|
|
5
5
|
"description": "Security MCP for vibe coding. 365 rules, 36 tools, CLI + doctor. Host security, auth coverage mapping, LLM-powered deep scan (IDOR/business logic), taint analysis. 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",
|