@vibefoundry/vibe-shield 1.1.1 → 2.0.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/README.md +31 -11
- package/SKILL.md +26 -93
- package/dist/bin/vibe-shield.d.ts +1 -1
- package/dist/bin/vibe-shield.js +38 -11
- package/dist/src/init/scaffold.d.ts +1 -1
- package/dist/src/init/scaffold.js +26 -93
- package/dist/src/mcp/server.js +5 -1
- package/dist/src/mcp-guard/skill-auditor.js +11 -3
- package/dist/src/reporter/console-reporter.js +14 -0
- package/dist/src/reporter/executive.d.ts +4 -0
- package/dist/src/reporter/executive.js +58 -0
- package/dist/src/reporter/formatters.d.ts +1 -1
- package/dist/src/reporter/formatters.js +10 -3
- package/dist/src/reporter/patch-prompt-writer.js +3 -3
- package/dist/src/scanner/attack-surface.d.ts +4 -0
- package/dist/src/scanner/attack-surface.js +50 -0
- package/dist/src/scanner/engine.d.ts +2 -3
- package/dist/src/scanner/engine.js +21 -39
- package/dist/src/scanner/mappings.d.ts +17 -0
- package/dist/src/scanner/mappings.js +39 -0
- package/dist/src/scanner/redact.d.ts +1 -0
- package/dist/src/scanner/redact.js +12 -0
- package/dist/src/scanner/rules/hardening.d.ts +5 -0
- package/dist/src/scanner/rules/hardening.js +116 -0
- package/dist/src/scanner/rules/helpers.js +2 -0
- package/dist/src/scanner/rules/index.js +6 -0
- package/dist/src/scanner/rules/response-hardening.d.ts +2 -0
- package/dist/src/scanner/rules/response-hardening.js +120 -0
- package/dist/src/scanner/rules/secret-entropy.js +0 -2
- package/dist/src/scanner/types.d.ts +33 -1
- package/dist/src/scanner/types.js +6 -2
- package/dist/src/scanner/version.d.ts +2 -0
- package/dist/src/scanner/version.js +69 -0
- package/dist/src/secrets/patterns.js +40 -0
- package/package.json +99 -99
|
@@ -64,128 +64,61 @@ const BANNER = '<!-- Managed by vibe-shield. Edit freely; `npx -y @vibefoundry/v
|
|
|
64
64
|
/* -------------------------------------------------------------------------- */
|
|
65
65
|
exports.SKILL_TEMPLATE = `---
|
|
66
66
|
name: vibe-shield
|
|
67
|
-
description:
|
|
67
|
+
description: Apply a six-phase defensive security review to changes affecting applications, APIs, data, AI agents, and MCP configuration. Use static evidence, preserve behavior, and report limitations.
|
|
68
68
|
allowed-tools: Bash(npx -y @vibefoundry/vibe-shield scan*), Bash(npx -y @vibefoundry/vibe-shield audit-skills*), Bash(npx -y @vibefoundry/vibe-shield explain*), Read(./**), Grep, Glob
|
|
69
69
|
license: MIT
|
|
70
70
|
metadata:
|
|
71
71
|
author: VibeFoundry
|
|
72
72
|
homepage: https://github.com/avivgerz-hue/vibe-shield
|
|
73
|
-
version:
|
|
73
|
+
version: 2.0.0
|
|
74
74
|
---
|
|
75
75
|
|
|
76
|
-
#
|
|
76
|
+
# Defensive AppSec architect protocol
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
Work only within the authorized project. Static findings are evidence for review, not proof of exploitability or a guarantee of safety. Do not transmit source code or secrets. Ask before changing production services, rotating credentials, or making destructive changes.
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
## Phase 1: Attack-surface reconnaissance
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
Map entrypoints and trust boundaries: API methods, authentication and authorization, middleware matchers, database tables and policies, storage, LLM inputs and tools, webhooks, and agent/MCP configuration. Check deployment configuration and exclusions. Identify what the scanner cannot see.
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
- an API route, route handler, server action, RPC endpoint or GraphQL mutation
|
|
87
|
-
- authentication, session, role, permission or ownership logic
|
|
88
|
-
- a database migration, policy, grant, schema or Supabase table
|
|
89
|
-
- an environment variable, config file, \`.env*\` file or build config
|
|
90
|
-
- anything calling an LLM, a tool, a webhook, a shell command or the filesystem
|
|
91
|
-
- \`.claude/\`, \`.cursor/\`, \`AGENTS.md\`, \`mcp.json\` or any skill/config file
|
|
92
|
-
|
|
93
|
-
If you did not touch any of those, you do not need to run it.
|
|
94
|
-
|
|
95
|
-
## The loop
|
|
84
|
+
## Phase 2: Automated static scan and skill audit
|
|
96
85
|
|
|
97
86
|
\`\`\`bash
|
|
98
|
-
|
|
99
|
-
npx -y @vibefoundry/vibe-shield
|
|
100
|
-
|
|
101
|
-
# 2. If it fails, generate the machine-readable fix list
|
|
102
|
-
npx -y @vibefoundry/vibe-shield scan --fix-prompt
|
|
103
|
-
|
|
104
|
-
# 3. Read SECURITY_PATCH_PROMPT.md and fix every finding, critical first
|
|
105
|
-
# Each entry has the exact file:line, the vulnerable snippet, the CWE and
|
|
106
|
-
# the required change. Apply it literally.
|
|
107
|
-
|
|
108
|
-
# 4. Re-run until it exits 0
|
|
109
|
-
npx -y @vibefoundry/vibe-shield scan --fail-on high
|
|
87
|
+
npx -y @vibefoundry/vibe-shield scan . --format json
|
|
88
|
+
npx -y @vibefoundry/vibe-shield audit-skills . --format json
|
|
110
89
|
\`\`\`
|
|
111
90
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
## Reading a finding
|
|
115
|
-
|
|
116
|
-
\`\`\`
|
|
117
|
-
CRITICAL 47:15 VSB-CLIENT-001 Server-only secret exposed through a public environment prefix
|
|
118
|
-
const key = process.env.NEXT_PUBLIC_OPENAI_API_KEY;
|
|
119
|
-
\`\`\`
|
|
91
|
+
Inspect findings, inventory, ignored paths, and the files scanned. The explicit \`.\` includes test and documentation files: review fixtures as examples rather than blindly changing them. Use \`explain <RULE_ID>\` to investigate a rule. These commands run locally; first-time installation with npx requires registry access.
|
|
120
92
|
|
|
121
|
-
|
|
122
|
-
| --- | --- |
|
|
123
|
-
| \`47:15\` | exact line and column to edit |
|
|
124
|
-
| \`VSB-CLIENT-001\` | rule id — use it with \`explain\` for full docs |
|
|
125
|
-
| message | why this specific line is exploitable |
|
|
126
|
-
| \`(CWE-200)\` | bug class, for a deeper search if the fix is unclear |
|
|
93
|
+
## Phase 3: Deep contextual threat modeling
|
|
127
94
|
|
|
128
|
-
|
|
95
|
+
Trace untrusted input through authentication, ownership checks and tenant boundaries to database, filesystem, network, HTML, and LLM sinks. Inspect IDOR, business-logic bypasses, webhook signature verification and asynchronous jobs. Check cross-file guards and deployed database policies; absence of a static finding is not evidence of protection.
|
|
129
96
|
|
|
130
|
-
##
|
|
97
|
+
## Phase 4: Surgical zero-regression remediation
|
|
131
98
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
99
|
+
- On Next.js mutation routes, authenticate the session on the server and authorize the target resource against the session principal before writes. Do not trust identity from request bodies.
|
|
100
|
+
- Enable Supabase RLS and write action-specific policies using the authenticated principal; test both permitted and denied tenants before deploying migrations.
|
|
101
|
+
- Validate inputs at the boundary with a schema such as Zod, then use parameterized database queries. Treat LLM output as untrusted.
|
|
102
|
+
- Use explicit CORS allowlists, secure cookie flags and a CSP appropriate for the application. Test login, redirects, and browser behavior.
|
|
103
|
+
- Report exposed credentials to the owner for provider-side rotation; avoid reproducing their values in reports. Do not suppress findings or weaken controls to obtain a higher score. Ask for missing business or deployment decisions.
|
|
137
104
|
|
|
138
|
-
##
|
|
105
|
+
## Phase 5: Verification gate
|
|
139
106
|
|
|
140
|
-
|
|
107
|
+
Run project tests and both audits again. Aim for 100/100 Grade A+ with no high or critical findings, but report any unresolved finding, exclusion, false positive, or verification limitation honestly rather than claiming success. Never equate a clean score with a complete security assessment.
|
|
141
108
|
|
|
142
109
|
\`\`\`bash
|
|
143
|
-
npx -y @vibefoundry/vibe-shield
|
|
110
|
+
npx -y @vibefoundry/vibe-shield scan --fail-on high
|
|
111
|
+
npx -y @vibefoundry/vibe-shield audit-skills --fail-on high
|
|
144
112
|
\`\`\`
|
|
145
113
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Each finding carries a \`triage\` field: \`block\` means refuse and escalate, \`review\` means ask the user, \`report\` means mention it and continue. Do not blanket-refuse a repository because a rule fired — read the triage.
|
|
149
|
-
|
|
150
|
-
## Flags worth knowing
|
|
114
|
+
## Phase 6: Executive security sign-off
|
|
151
115
|
|
|
152
|
-
|
|
153
|
-
| --- | --- |
|
|
154
|
-
| \`--format json\` | full machine-readable report (schemaVersion 1) |
|
|
155
|
-
| \`--format sarif\` | SARIF 2.1.0 for GitHub code scanning |
|
|
156
|
-
| \`--format compact\` | one line per finding |
|
|
157
|
-
| \`--format github\` | inline GitHub Actions annotations |
|
|
158
|
-
| \`--severity high\` | only report high and critical |
|
|
159
|
-
| \`--fail-on medium\` | stricter gate |
|
|
160
|
-
| \`--include-tests\` | include test and fixture files |
|
|
161
|
-
| \`--cache\` | reuse results between runs |
|
|
162
|
-
| \`--quiet\` | one-line verdict (ideal for piping) |
|
|
163
|
-
| \`rules\` | list every rule |
|
|
164
|
-
| \`explain <RULE_ID>\` | full documentation for one rule |
|
|
165
|
-
|
|
166
|
-
If the repository has a \`.mcp.json\`, the same three capabilities are also
|
|
167
|
-
available as MCP tools — \`vibe_shield_scan\`, \`vibe_shield_audit_skills\` and
|
|
168
|
-
\`vibe_shield_explain_rule\`. Prefer those when they are listed, because they
|
|
169
|
-
return structured findings instead of text you have to parse.
|
|
170
|
-
|
|
171
|
-
## Definition of done
|
|
172
|
-
|
|
173
|
-
- [ ] \`npx -y @vibefoundry/vibe-shield scan --fail-on high\` exits \`0\`
|
|
174
|
-
- [ ] \`npx -y @vibefoundry/vibe-shield audit-skills\` exits \`0\` if you touched agent files
|
|
175
|
-
- [ ] every credential the scan surfaced has been flagged to the user for rotation
|
|
176
|
-
- [ ] no suppression directive was added
|
|
177
|
-
- [ ] the feature still works and the existing tests still pass
|
|
178
|
-
|
|
179
|
-
## Install it into a repository
|
|
116
|
+
Produce a Markdown or HTML report with scope, inventory, risks prioritized by severity, evidence, actions, test results and remaining uncertainty. Illustrative code examples are not project-specific patches; review and adapt them. Obtain human approval before deployment or provider-side action.
|
|
180
117
|
|
|
181
118
|
\`\`\`bash
|
|
182
|
-
npx -y @vibefoundry/vibe-shield
|
|
119
|
+
npx -y @vibefoundry/vibe-shield scan --report SECURITY-REPORT.md --patch-prompt PATCH-PROMPT.md
|
|
120
|
+
npx -y @vibefoundry/vibe-shield audit-skills --report AGENT-REPORT.html
|
|
183
121
|
\`\`\`
|
|
184
|
-
|
|
185
|
-
That writes this file to \`.claude/skills/vibe-shield/SKILL.md\`, appends a security
|
|
186
|
-
gate section to \`AGENTS.md\`, adds a \`.cursor/rules/vibe-shield.mdc\`, and installs a
|
|
187
|
-
GitHub Actions workflow that fails the pull request on new high or critical
|
|
188
|
-
findings. Every future task in that repository starts guarded.
|
|
189
122
|
`;
|
|
190
123
|
/* -------------------------------------------------------------------------- */
|
|
191
124
|
/* AGENTS.md section */
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -230,6 +230,7 @@ const toToolResult = (result, options) => {
|
|
|
230
230
|
filesScanned: result.stats.filesScanned,
|
|
231
231
|
durationMs: result.stats.durationMs,
|
|
232
232
|
rootDir: result.rootDir,
|
|
233
|
+
attackSurface: result.attackSurface,
|
|
233
234
|
findings: shown.map((finding) => ({
|
|
234
235
|
ruleId: finding.ruleId,
|
|
235
236
|
slug: finding.slug,
|
|
@@ -239,10 +240,12 @@ const toToolResult = (result, options) => {
|
|
|
239
240
|
title: finding.title,
|
|
240
241
|
message: finding.message,
|
|
241
242
|
filePath: finding.filePath,
|
|
243
|
+
file: finding.file,
|
|
242
244
|
line: finding.line,
|
|
243
245
|
column: finding.column,
|
|
244
246
|
snippet: finding.snippet,
|
|
245
247
|
cwe: finding.cwe,
|
|
248
|
+
owasp: finding.owasp,
|
|
246
249
|
remediation: finding.remediation,
|
|
247
250
|
secureExample: finding.secureExample,
|
|
248
251
|
references: finding.references,
|
|
@@ -281,6 +284,7 @@ const explainRuleResult = (rule) => {
|
|
|
281
284
|
severity: rule.severity,
|
|
282
285
|
confidence: rule.confidence,
|
|
283
286
|
cwe: rule.cwe,
|
|
287
|
+
owasp: rule.owasp,
|
|
284
288
|
description: rule.description,
|
|
285
289
|
insecureExample: rule.insecureExample,
|
|
286
290
|
secureExample: rule.secureExample,
|
|
@@ -393,7 +397,7 @@ async function handleMessage(message, options = {}) {
|
|
|
393
397
|
serverInfo: {
|
|
394
398
|
name: exports.SERVER_NAME,
|
|
395
399
|
title: 'vibe-shield',
|
|
396
|
-
version: options.version ??
|
|
400
|
+
version: options.version ?? await (0, engine_1.packageVersion)(),
|
|
397
401
|
},
|
|
398
402
|
instructions: 'vibe-shield finds security bugs that AI coding agents ship: leaked secrets, unguarded API routes, missing Postgres RLS, SQL injection, prompt injection and poisoned agent skills. Call vibe_shield_scan after writing application code and before reporting a task as done, and vibe_shield_audit_skills after touching any agent instruction or MCP config file.',
|
|
399
403
|
});
|
|
@@ -59,6 +59,9 @@ const path = __importStar(require("node:path"));
|
|
|
59
59
|
const patterns_1 = require("../secrets/patterns");
|
|
60
60
|
const context_1 = require("../scanner/context");
|
|
61
61
|
const gitignore_1 = require("../scanner/gitignore");
|
|
62
|
+
const attack_surface_1 = require("../scanner/attack-surface");
|
|
63
|
+
const version_1 = require("../scanner/version");
|
|
64
|
+
const redact_1 = require("../scanner/redact");
|
|
62
65
|
const helpers_1 = require("../scanner/rules/helpers");
|
|
63
66
|
const shared_1 = require("../scanner/rules/shared");
|
|
64
67
|
const types_1 = require("../scanner/types");
|
|
@@ -1031,6 +1034,7 @@ async function auditSkills(rootDir, options = {}) {
|
|
|
1031
1034
|
// auditor's own discovery rules so `audit-skills .claude` works as expected.
|
|
1032
1035
|
const files = options.paths?.length ? await resolveAuditPaths(rootDir, options.paths) : await discoverAgentFiles(rootDir);
|
|
1033
1036
|
const findings = [];
|
|
1037
|
+
const attackSurface = (0, attack_surface_1.emptyAttackSurface)();
|
|
1034
1038
|
const scanned = [];
|
|
1035
1039
|
let bytesScanned = 0;
|
|
1036
1040
|
for (const absolutePath of files) {
|
|
@@ -1042,6 +1046,7 @@ async function auditSkills(rootDir, options = {}) {
|
|
|
1042
1046
|
continue;
|
|
1043
1047
|
}
|
|
1044
1048
|
const ctx = (0, context_1.createFileContext)(absolutePath, rootDir, content, project);
|
|
1049
|
+
(0, attack_surface_1.inventoryFile)(ctx, attackSurface);
|
|
1045
1050
|
if (ctx.flags.isLockfile || ctx.flags.isGenerated)
|
|
1046
1051
|
continue;
|
|
1047
1052
|
scanned.push(ctx.relativePath);
|
|
@@ -1069,12 +1074,14 @@ async function auditSkills(rootDir, options = {}) {
|
|
|
1069
1074
|
severity: hit.severity ?? rule.severity,
|
|
1070
1075
|
confidence: hit.confidence ?? rule.confidence,
|
|
1071
1076
|
cwe: rule.cwe,
|
|
1077
|
+
owasp: rule.owasp,
|
|
1072
1078
|
filePath: ctx.relativePath,
|
|
1079
|
+
file: ctx.relativePath,
|
|
1073
1080
|
absolutePath: ctx.absolutePath,
|
|
1074
1081
|
line: located.line,
|
|
1075
1082
|
column: located.column,
|
|
1076
|
-
snippet: ctx.lineText(located.line).trim().slice(0, 200),
|
|
1077
|
-
matched: hit.matched ?? ctx.lineText(located.line).trim().slice(0, 200),
|
|
1083
|
+
snippet: (0, redact_1.redactReportText)(ctx.lineText(located.line).trim()).slice(0, 200),
|
|
1084
|
+
matched: (0, redact_1.redactReportText)(hit.matched ?? ctx.lineText(located.line).trim()).slice(0, 200),
|
|
1078
1085
|
message: hit.message ?? rule.description,
|
|
1079
1086
|
remediation: rule.remediate ? rule.remediate(ctx, hit) : rule.remediation,
|
|
1080
1087
|
insecureExample: rule.insecureExample,
|
|
@@ -1101,6 +1108,7 @@ async function auditSkills(rootDir, options = {}) {
|
|
|
1101
1108
|
return {
|
|
1102
1109
|
rootDir,
|
|
1103
1110
|
command: 'audit-skills',
|
|
1111
|
+
attackSurface,
|
|
1104
1112
|
findings: unique,
|
|
1105
1113
|
score: scoreFindings(unique),
|
|
1106
1114
|
stats: {
|
|
@@ -1118,7 +1126,7 @@ async function auditSkills(rootDir, options = {}) {
|
|
|
1118
1126
|
scannedPaths: scanned,
|
|
1119
1127
|
startedAt,
|
|
1120
1128
|
finishedAt: new Date(finished).toISOString(),
|
|
1121
|
-
version: options.version ??
|
|
1129
|
+
version: options.version ?? await (0, version_1.packageVersion)(),
|
|
1122
1130
|
ruleCount: rules.length,
|
|
1123
1131
|
suppressedCount: 0,
|
|
1124
1132
|
notices: scanned.length ? [] : ['No agent instruction or MCP configuration files were found.'],
|
|
@@ -235,6 +235,20 @@ function renderConsoleReport(result, options = {}) {
|
|
|
235
235
|
lines.push(...bars);
|
|
236
236
|
}
|
|
237
237
|
lines.push(...renderCategories(result, color));
|
|
238
|
+
if (result.attackSurface) {
|
|
239
|
+
const surface = result.attackSurface;
|
|
240
|
+
lines.push('', color.bold('Attack surface (static signals; review required)'));
|
|
241
|
+
for (const route of surface.routes)
|
|
242
|
+
lines.push(` route ${route.method} ${route.path} — ${route.file}:${route.line} (${route.auth})`);
|
|
243
|
+
for (const table of surface.tables)
|
|
244
|
+
lines.push(` table ${table.table} — ${table.file}:${table.line} (${table.rls})`);
|
|
245
|
+
for (const ai of surface.aiEntrypoints)
|
|
246
|
+
lines.push(` AI entrypoint ${ai.file}:${ai.line}`);
|
|
247
|
+
for (const agent of surface.agentFiles)
|
|
248
|
+
lines.push(` ${agent.kind} ${agent.name ?? ''} — ${agent.file}`);
|
|
249
|
+
if (!surface.routes.length && !surface.tables.length && !surface.aiEntrypoints.length && !surface.agentFiles.length)
|
|
250
|
+
lines.push(' No inventory signals observed.');
|
|
251
|
+
}
|
|
238
252
|
if (result.findings.length) {
|
|
239
253
|
lines.push('');
|
|
240
254
|
lines.push(color.bold('Findings'));
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatMarkdown = formatMarkdown;
|
|
4
|
+
exports.formatHtml = formatHtml;
|
|
5
|
+
const mappings_1 = require("../scanner/mappings");
|
|
6
|
+
const engine_1 = require("../scanner/engine");
|
|
7
|
+
const types_1 = require("../scanner/types");
|
|
8
|
+
const clean = (s) => s.replace(/[\r\n]+/g, ' ').replace(/\|/g, '\\|');
|
|
9
|
+
const fence = (s) => {
|
|
10
|
+
const marker = '`'.repeat(Math.max(3, ...Array.from(s.matchAll(/`+/g), m => m[0].length + 1)));
|
|
11
|
+
return `${marker}\n${s}\n${marker}`;
|
|
12
|
+
};
|
|
13
|
+
// The CWE mapping is based on OWASP's published 2025 category lists.
|
|
14
|
+
function mappings(ruleId, cwe) {
|
|
15
|
+
const mapping = (0, mappings_1.owaspMappings)(ruleId, cwe);
|
|
16
|
+
const web = mapping.web ? `${mapping.web.id} ${mapping.web.name} (${mapping.web.basis})` : 'not mapped';
|
|
17
|
+
const llm = mapping.llm.length ? mapping.llm.map(m => `${m.id} ${m.name} (contextual)`).join(', ') : 'not applicable or not assigned';
|
|
18
|
+
return `OWASP Top 10 2025: ${web}; OWASP LLM Top 10 2025: ${llm}. Validate applicability in context.`;
|
|
19
|
+
}
|
|
20
|
+
function formatMarkdown(result) {
|
|
21
|
+
const lines = ['# Executive Security Report', '', `**Score: ${result.score.score}/100 (${result.score.grade})** · ${result.stats.filesScanned} files scanned · ${result.command}`, '',
|
|
22
|
+
'Static signals require manual validation. A clean score is not proof of security. Ignored, unavailable, and unscanned files are outside this report.', '',
|
|
23
|
+
'## Category scorecard', '', '| Vector | Score | Findings |', '| --- | --- | ---: |'];
|
|
24
|
+
for (const category of types_1.RULE_CATEGORIES) {
|
|
25
|
+
const subset = result.findings.filter(f => f.category === category);
|
|
26
|
+
const applicable = result.command === 'audit-skills' ? category === 'agent-integrity' : category !== 'agent-integrity';
|
|
27
|
+
lines.push(`| ${types_1.CATEGORY_TITLES[category]} | ${applicable ? `${(0, engine_1.scoreFindings)(subset).score}/100` : 'Not assessed'} | ${subset.length} |`);
|
|
28
|
+
}
|
|
29
|
+
lines.push('', '## Attack-surface inventory', '', 'Observed signals only; guard and RLS signals do not prove enforcement. Absence of a guard signal is not proof a route is unguarded.', '', '| Type | Location | Detail | Signal |', '| --- | --- | --- | --- |');
|
|
30
|
+
const surface = result.attackSurface;
|
|
31
|
+
if (surface) {
|
|
32
|
+
for (const r of surface.routes)
|
|
33
|
+
lines.push(`| Route | ${clean(r.file)}:${r.line} | ${clean(r.method)} ${clean(r.path)} | ${r.auth} |`);
|
|
34
|
+
for (const t of surface.tables)
|
|
35
|
+
lines.push(`| Table | ${clean(t.file)}:${t.line} | ${clean(t.table)} | ${t.rls} |`);
|
|
36
|
+
for (const a of surface.aiEntrypoints)
|
|
37
|
+
lines.push(`| AI entrypoint | ${clean(a.file)}:${a.line} | LLM reference | review |`);
|
|
38
|
+
for (const a of surface.agentFiles)
|
|
39
|
+
lines.push(`| Agent | ${clean(a.file)} | ${a.kind}${a.name ? ': ' + clean(a.name) : ''} | review |`);
|
|
40
|
+
}
|
|
41
|
+
else
|
|
42
|
+
lines.push('| — | — | Inventory not available for this command | — |');
|
|
43
|
+
lines.push('', '## Prioritized findings', '');
|
|
44
|
+
const rank = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
45
|
+
for (const f of [...result.findings].sort((a, b) => (rank[a.severity] ?? 5) - (rank[b.severity] ?? 5))) {
|
|
46
|
+
lines.push(`### ${clean(f.severity.toUpperCase())}: ${clean(f.title)}`, '', `**${f.ruleId} · ${clean(f.filePath)}:${f.line}:${f.column} · ${f.cwe.id} · confidence ${f.confidence}**`, '', f.message, '', mappings(f.ruleId, f.cwe.id), '', '**Observed code**', '', fence(f.snippet), '', '**Suggested remediation**', '', ...f.remediation.map(x => `- ${x}`), '', '**Illustrative before / after (adapt and validate; not an automatic patch)**', '', fence(f.insecureExample), '', fence(f.secureExample), '');
|
|
47
|
+
}
|
|
48
|
+
if (!result.findings.length)
|
|
49
|
+
lines.push('No findings reported for the selected rules and files.', '');
|
|
50
|
+
return lines.join('\n') + '\n';
|
|
51
|
+
}
|
|
52
|
+
const escapeHtml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
53
|
+
/** All project content is escaped. The only script is a fixed local filter. */
|
|
54
|
+
function formatHtml(result) {
|
|
55
|
+
const md = formatMarkdown(result);
|
|
56
|
+
const entries = result.findings.map(f => `<article data-severity="${escapeHtml(f.severity)}"><h3>${escapeHtml(f.severity.toUpperCase() + ': ' + f.title)}</h3><p>${escapeHtml(`${f.filePath}:${f.line}:${f.column} · ${f.ruleId} · ${f.cwe.id}`)}</p><pre>${escapeHtml(f.snippet)}</pre><p>${escapeHtml(f.message)}</p><ul>${f.remediation.map(s => `<li>${escapeHtml(s)}</li>`).join('')}</ul></article>`).join('\n');
|
|
57
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Executive Security Report</title><style>body{background:#101722;color:#e8effa;font:16px system-ui;max-width:1000px;margin:auto;padding:2rem}pre{white-space:pre-wrap;overflow-wrap:anywhere;background:#1b2738;padding:1rem;border-radius:6px}article{border:1px solid #34455b;padding:1rem;margin:1rem 0;border-radius:8px}select{background:#1b2738;color:white;padding:.5rem}</style></head><body><h1>Executive Security Report</h1><label>Filter findings by severity <select id="severity"><option value="all">All</option><option>critical</option><option>high</option><option>medium</option><option>low</option><option>info</option></select></label><section>${entries}</section><details open><summary>Full report</summary><pre>${escapeHtml(md)}</pre></details><script>document.getElementById('severity').addEventListener('change',function(){document.querySelectorAll('article[data-severity]').forEach(function(e){e.hidden=this.value!=='all'&&e.dataset.severity!==this.value},this)})</script></body></html>\n`;
|
|
58
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* - `github` GitHub Actions workflow commands (annotations in the diff)
|
|
12
12
|
*/
|
|
13
13
|
import type { Finding, ScanResult } from '../scanner/types';
|
|
14
|
-
export declare const OUTPUT_FORMATS: readonly ["console", "compact", "summary", "json", "sarif", "github"];
|
|
14
|
+
export declare const OUTPUT_FORMATS: readonly ["console", "compact", "summary", "json", "sarif", "github", "markdown", "html"];
|
|
15
15
|
export type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|
16
16
|
export declare const isOutputFormat: (value: unknown) => value is OutputFormat;
|
|
17
17
|
/** Bump when the JSON shape changes in a breaking way. */
|
|
@@ -20,7 +20,9 @@ exports.formatGithub = formatGithub;
|
|
|
20
20
|
exports.formatSarif = formatSarif;
|
|
21
21
|
exports.formatResult = formatResult;
|
|
22
22
|
exports.groupByFile = groupByFile;
|
|
23
|
-
|
|
23
|
+
const node_url_1 = require("node:url");
|
|
24
|
+
const executive_1 = require("./executive");
|
|
25
|
+
exports.OUTPUT_FORMATS = ['console', 'compact', 'summary', 'json', 'sarif', 'github', 'markdown', 'html'];
|
|
24
26
|
const isOutputFormat = (value) => typeof value === 'string' && exports.OUTPUT_FORMATS.includes(value);
|
|
25
27
|
exports.isOutputFormat = isOutputFormat;
|
|
26
28
|
/** Bump when the JSON shape changes in a breaking way. */
|
|
@@ -193,7 +195,7 @@ function formatSarif(result) {
|
|
|
193
195
|
},
|
|
194
196
|
],
|
|
195
197
|
originalUriBaseIds: {
|
|
196
|
-
'%SRCROOT%': { uri:
|
|
198
|
+
'%SRCROOT%': { uri: (0, node_url_1.pathToFileURL)(result.rootDir.endsWith('/') ? result.rootDir : result.rootDir + '/').href },
|
|
197
199
|
},
|
|
198
200
|
results: result.findings.map((finding) => ({
|
|
199
201
|
ruleId: finding.ruleId,
|
|
@@ -203,7 +205,7 @@ function formatSarif(result) {
|
|
|
203
205
|
locations: [
|
|
204
206
|
{
|
|
205
207
|
physicalLocation: {
|
|
206
|
-
artifactLocation: { uri: finding.filePath, uriBaseId: '%SRCROOT%' },
|
|
208
|
+
artifactLocation: { uri: finding.filePath.split('/').map(encodeURIComponent).join('/'), uriBaseId: '%SRCROOT%' },
|
|
207
209
|
region: {
|
|
208
210
|
startLine: finding.line,
|
|
209
211
|
startColumn: finding.column,
|
|
@@ -221,6 +223,7 @@ function formatSarif(result) {
|
|
|
221
223
|
severity: finding.severity,
|
|
222
224
|
confidence: finding.confidence,
|
|
223
225
|
cwe: finding.cwe.id,
|
|
226
|
+
owasp: finding.owasp,
|
|
224
227
|
remediation: finding.remediation,
|
|
225
228
|
secureExample: finding.secureExample,
|
|
226
229
|
},
|
|
@@ -236,6 +239,10 @@ function formatSarif(result) {
|
|
|
236
239
|
/** Render `result` in the requested format. */
|
|
237
240
|
function formatResult(result, format) {
|
|
238
241
|
switch (format) {
|
|
242
|
+
case 'markdown':
|
|
243
|
+
return (0, executive_1.formatMarkdown)(result);
|
|
244
|
+
case 'html':
|
|
245
|
+
return (0, executive_1.formatHtml)(result);
|
|
239
246
|
case 'json':
|
|
240
247
|
return formatJson(result);
|
|
241
248
|
case 'summary':
|
|
@@ -87,12 +87,12 @@ function buildPatchPrompt(result, options = {}) {
|
|
|
87
87
|
lines.push('# Security patch prompt');
|
|
88
88
|
lines.push('');
|
|
89
89
|
lines.push('> Generated by [vibe-shield](https://github.com/avivgerz-hue/vibe-shield). Hand this file to your coding agent (Claude Code, Codex, Cursor, Windsurf).');
|
|
90
|
-
lines.push('>
|
|
90
|
+
lines.push('> Findings are static signals with source locations; validate each in context before making changes.');
|
|
91
91
|
lines.push('');
|
|
92
92
|
lines.push('## Mission');
|
|
93
93
|
lines.push('');
|
|
94
94
|
lines.push(`Fix the ${selected.length} security finding${selected.length === 1 ? '' : 's'} listed below in the repository at \`${result.rootDir}\`. ` +
|
|
95
|
-
'
|
|
95
|
+
'Review each finding, adapt the guidance to the project, then re-run the scanner and relevant tests.');
|
|
96
96
|
lines.push('');
|
|
97
97
|
lines.push('## Scanner verdict');
|
|
98
98
|
lines.push('');
|
|
@@ -175,7 +175,7 @@ function buildPatchPrompt(result, options = {}) {
|
|
|
175
175
|
lines.push(`${stepIndex + 1}. ${step}`);
|
|
176
176
|
});
|
|
177
177
|
lines.push('');
|
|
178
|
-
lines.push('**
|
|
178
|
+
lines.push('**Illustrative remediation example (adapt to the project)**');
|
|
179
179
|
lines.push('');
|
|
180
180
|
lines.push(...fence(finding.secureExample, languageFor(filePath)));
|
|
181
181
|
lines.push('');
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AttackSurface, FileContext, ProjectContext } from './types';
|
|
2
|
+
export declare const emptyAttackSurface: () => AttackSurface;
|
|
3
|
+
export declare function inventoryFile(ctx: FileContext, surface: AttackSurface): void;
|
|
4
|
+
export declare function inventoryTables(surface: AttackSurface, project: ProjectContext, scannedPaths: Set<string>): void;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.emptyAttackSurface = void 0;
|
|
4
|
+
exports.inventoryFile = inventoryFile;
|
|
5
|
+
exports.inventoryTables = inventoryTables;
|
|
6
|
+
/** Heuristic inventory of observed surface, not evidence of effective protection. */
|
|
7
|
+
const lexicon_1 = require("./lexicon");
|
|
8
|
+
const emptyAttackSurface = () => ({ routes: [], tables: [], aiEntrypoints: [], agentFiles: [] });
|
|
9
|
+
exports.emptyAttackSurface = emptyAttackSurface;
|
|
10
|
+
function inventoryFile(ctx, surface) {
|
|
11
|
+
const { relativePath: file, code } = ctx;
|
|
12
|
+
if (ctx.flags.isMcpConfig) {
|
|
13
|
+
try {
|
|
14
|
+
const config = JSON.parse(ctx.content);
|
|
15
|
+
const servers = Object.keys(config.mcpServers ?? {});
|
|
16
|
+
if (servers.length)
|
|
17
|
+
for (const name of servers)
|
|
18
|
+
surface.agentFiles.push({ file, kind: 'mcp', name });
|
|
19
|
+
else
|
|
20
|
+
surface.agentFiles.push({ file, kind: 'mcp' });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
surface.agentFiles.push({ file, kind: 'mcp' });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
else if (ctx.flags.isAgentInstructionFile)
|
|
27
|
+
surface.agentFiles.push({ file, kind: 'skill' });
|
|
28
|
+
if (ctx.flags.mentionsLlm && (ctx.flags.isAppRouterRoute || ctx.flags.isPagesApiRoute || ctx.flags.isServerFrameworkFile)) {
|
|
29
|
+
surface.aiEntrypoints.push({ file, line: 1 });
|
|
30
|
+
}
|
|
31
|
+
const auth = lexicon_1.AUTH_VERIFICATION_REGEX.test(code) ? 'guard-signal' : 'unknown';
|
|
32
|
+
const add = (method, route, offset) => {
|
|
33
|
+
surface.routes.push({ file, line: ctx.locate(offset).line, method: method.toUpperCase(), path: route, auth });
|
|
34
|
+
};
|
|
35
|
+
if (ctx.flags.isAppRouterRoute) {
|
|
36
|
+
const route = '/' + file.replace(/^.*?app\//, '').replace(/\/route\.[^.]+$/, '').replace(/\([^/]+\)\/?/g, '');
|
|
37
|
+
for (const match of code.matchAll(/\bexport\s+(?:async\s+function|const|function)\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\b/g))
|
|
38
|
+
add(match[1], route.replace(/\/$/, '') || '/', match.index);
|
|
39
|
+
}
|
|
40
|
+
else if (ctx.flags.isPagesApiRoute) {
|
|
41
|
+
add('ANY', '/' + file.replace(/^.*?pages\//, '').replace(/\.[^.]+$/, '').replace(/\/index$/, ''), 0);
|
|
42
|
+
}
|
|
43
|
+
for (const match of code.matchAll(/\b(?:app|router|server|fastify)\.(get|post|put|patch|delete|all)\s*\(\s*['"]([^'"]+)['"]/gi))
|
|
44
|
+
add(match[1], match[2], match.index);
|
|
45
|
+
}
|
|
46
|
+
function inventoryTables(surface, project, scannedPaths) {
|
|
47
|
+
surface.tables = project.sqlTables.filter(t => scannedPaths.has(t.file) && !t.internal).map(t => ({
|
|
48
|
+
file: t.file, line: t.line, table: t.table, rls: t.rlsEnabled ? 'enabled-signal' : 'not-observed',
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
@@ -107,7 +107,7 @@ export declare function applySuppressions(findings: readonly Finding[], parsed:
|
|
|
107
107
|
/** Turn a finding list into a 0-100 score with an A+ → F grade. */
|
|
108
108
|
export declare function scoreFindings(findings: readonly Finding[]): SecurityScore;
|
|
109
109
|
export declare const CACHE_FILE_NAME = ".vibe-shield-cache.json";
|
|
110
|
-
export declare const CACHE_VERSION =
|
|
110
|
+
export declare const CACHE_VERSION = 2;
|
|
111
111
|
/** Content hash used as the cache key for one file. */
|
|
112
112
|
export declare const contentHash: (content: string) => string;
|
|
113
113
|
/**
|
|
@@ -115,8 +115,7 @@ export declare const contentHash: (content: string) => string;
|
|
|
115
115
|
* Exported because the MCP auditor reuses it: two scanners, one rule contract.
|
|
116
116
|
*/
|
|
117
117
|
export declare function runRulesOnFile(ctx: FileContext, rules: readonly Rule[]): RuleHit[];
|
|
118
|
-
|
|
119
|
-
export declare function packageVersion(): Promise<string>;
|
|
118
|
+
export { packageVersion } from './version';
|
|
120
119
|
/**
|
|
121
120
|
* Scan a project. This is the one function the CLI, the tests and any embedding
|
|
122
121
|
* application call; everything else in this module is an implementation detail.
|