@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.
Files changed (35) hide show
  1. package/README.md +31 -11
  2. package/SKILL.md +26 -93
  3. package/dist/bin/vibe-shield.d.ts +1 -1
  4. package/dist/bin/vibe-shield.js +38 -11
  5. package/dist/src/init/scaffold.d.ts +1 -1
  6. package/dist/src/init/scaffold.js +26 -93
  7. package/dist/src/mcp/server.js +5 -1
  8. package/dist/src/mcp-guard/skill-auditor.js +11 -3
  9. package/dist/src/reporter/console-reporter.js +14 -0
  10. package/dist/src/reporter/executive.d.ts +4 -0
  11. package/dist/src/reporter/executive.js +58 -0
  12. package/dist/src/reporter/formatters.d.ts +1 -1
  13. package/dist/src/reporter/formatters.js +10 -3
  14. package/dist/src/reporter/patch-prompt-writer.js +3 -3
  15. package/dist/src/scanner/attack-surface.d.ts +4 -0
  16. package/dist/src/scanner/attack-surface.js +50 -0
  17. package/dist/src/scanner/engine.d.ts +2 -3
  18. package/dist/src/scanner/engine.js +21 -39
  19. package/dist/src/scanner/mappings.d.ts +17 -0
  20. package/dist/src/scanner/mappings.js +39 -0
  21. package/dist/src/scanner/redact.d.ts +1 -0
  22. package/dist/src/scanner/redact.js +12 -0
  23. package/dist/src/scanner/rules/hardening.d.ts +5 -0
  24. package/dist/src/scanner/rules/hardening.js +116 -0
  25. package/dist/src/scanner/rules/helpers.js +2 -0
  26. package/dist/src/scanner/rules/index.js +6 -0
  27. package/dist/src/scanner/rules/response-hardening.d.ts +2 -0
  28. package/dist/src/scanner/rules/response-hardening.js +120 -0
  29. package/dist/src/scanner/rules/secret-entropy.js +0 -2
  30. package/dist/src/scanner/types.d.ts +33 -1
  31. package/dist/src/scanner/types.js +6 -2
  32. package/dist/src/scanner/version.d.ts +2 -0
  33. package/dist/src/scanner/version.js +69 -0
  34. package/dist/src/secrets/patterns.js +40 -0
  35. 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: Audit any change for AI-introduced security bugs (leaked keys, unguarded API routes, disabled Row Level Security, SQL injection, prompt injection, poisoned MCP servers) and fix them before reporting a task as done. Use whenever you write or modify application code, API routes, server actions, database migrations, auth logic, environment variables, LLM calls, webhook handlers, agent skills or MCP configuration.
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: 1.0.0
73
+ version: 2.0.0
74
74
  ---
75
75
 
76
- # vibe-shield
76
+ # Defensive AppSec architect protocol
77
77
 
78
- You are not allowed to say "done" until the code you just wrote passes a security audit.
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
- AI coding agents ship a specific, repeatable class of vulnerability: a \`SUPABASE_SERVICE_ROLE_KEY\` behind a \`NEXT_PUBLIC_\` prefix, a \`DELETE\` route handler with no session check, a \`CREATE TABLE\` with no \`ENABLE ROW LEVEL SECURITY\`, a tool whose \`execute\` forwards model output into \`exec\`. These are not exotic — they are the default shape of code written quickly by something optimising for "it works". This skill is the second pair of eyes.
80
+ ## Phase 1: Attack-surface reconnaissance
81
81
 
82
- ## When to run
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
- Run the audit **after every change that touches**:
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
- # 1. Scan (a whole-repo scan finishes in well under a second)
99
- npx -y @vibefoundry/vibe-shield scan --fail-on high
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
- \`--fail-on high\` exits non-zero while any high or critical finding exists. That is the gate.
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
- | Part | Meaning |
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
- Need the full picture? \`npx -y @vibefoundry/vibe-shield explain VSB-CLIENT-001\` prints the vulnerable shape, the fixed shape, remediation steps and references.
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
- ## Hard rules
97
+ ## Phase 4: Surgical zero-regression remediation
131
98
 
132
- 1. **Fix the root cause.** Never delete an auth check, disable RLS, silence a log or add a catch-all \`catch {}\` to make the finding disappear.
133
- 2. **Never suppress.** Do not add \`// vibe-shield-disable\`, do not edit \`.vibe-shieldignore\`, do not pass \`--ignore\`. Suppression is a decision for the human.
134
- 3. **Rotate exposed credentials.** If the scan reported a live key, tell the user to rotate it at the provider in the same message. Removing the line does not un-leak it — the value may already be in a bundle, a cache or a git history.
135
- 4. **Report honestly.** If you cannot fix a finding (you need a decision, a credential or a provider change), say so explicitly, name the rule id, and stop.
136
- 5. **Re-verify.** A fix is not finished until the gate exits 0.
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
- ## Agent surface audit
105
+ ## Phase 5: Verification gate
139
106
 
140
- When you add or modify a skill, rule file, MCP server or instruction file, also run:
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 audit-skills
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
- Twelve \`VSB-AGENT-*\` rules check for hidden Unicode (zero-width and bidi characters used to smuggle instructions past review), \`curl\`/\`wget\` exfiltration of environment variables, credential-harvesting instructions (\`.env\`, \`~/.ssh\`, \`~/.aws\`), registry-tag or remote MCP servers, literal tokens in an \`env\` block, safety-override language, over-broad tool permissions, base64 \`eval\` payloads, remote-instruction fetching, wildcard \`allowed-tools\`, and shell injection in skill scripts.
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
- | Flag | Effect |
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 init
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 */
@@ -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 ?? '1.1.0',
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 ?? process.env.npm_package_version ?? '1.0.0',
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,4 @@
1
+ import { type ScanResult } from '../scanner/types';
2
+ export declare function formatMarkdown(result: ScanResult): string;
3
+ /** All project content is escaped. The only script is a fixed local filter. */
4
+ export declare function formatHtml(result: ScanResult): string;
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
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
- exports.OUTPUT_FORMATS = ['console', 'compact', 'summary', 'json', 'sarif', 'github'];
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: `${result.rootDir.split('\\').join('/')}/` },
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('> Every finding below was detected statically and cites the exact line that must change.');
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
- 'Apply every remediation exactly as written, then re-run the scanner to prove the finding is gone.');
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('**Target shape after the fix**');
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 = 1;
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
- /** Package version, resolved once from the nearest package.json. */
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.