@vibefoundry/vibe-shield 0.0.0-stage → 1.1.1
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/LICENSE +21 -0
- package/README.md +577 -3
- package/SECURITY.md +110 -0
- package/SKILL.md +124 -0
- package/dist/bin/vibe-shield.d.ts +59 -0
- package/dist/bin/vibe-shield.js +545 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.js +108 -0
- package/dist/src/init/scaffold.d.ts +120 -0
- package/dist/src/init/scaffold.js +556 -0
- package/dist/src/mcp/server.d.ts +127 -0
- package/dist/src/mcp/server.js +479 -0
- package/dist/src/mcp-guard/skill-auditor.d.ts +105 -0
- package/dist/src/mcp-guard/skill-auditor.js +1129 -0
- package/dist/src/reporter/console-reporter.d.ts +58 -0
- package/dist/src/reporter/console-reporter.js +254 -0
- package/dist/src/reporter/formatters.d.ts +38 -0
- package/dist/src/reporter/formatters.js +262 -0
- package/dist/src/reporter/patch-prompt-writer.d.ts +35 -0
- package/dist/src/reporter/patch-prompt-writer.js +238 -0
- package/dist/src/scanner/context.d.ts +51 -0
- package/dist/src/scanner/context.js +568 -0
- package/dist/src/scanner/engine.d.ts +130 -0
- package/dist/src/scanner/engine.js +726 -0
- package/dist/src/scanner/gitignore.d.ts +49 -0
- package/dist/src/scanner/gitignore.js +195 -0
- package/dist/src/scanner/lexicon.d.ts +56 -0
- package/dist/src/scanner/lexicon.js +357 -0
- package/dist/src/scanner/rules/ai-vulnerabilities.d.ts +15 -0
- package/dist/src/scanner/rules/ai-vulnerabilities.js +859 -0
- package/dist/src/scanner/rules/client-exposure.d.ts +18 -0
- package/dist/src/scanner/rules/client-exposure.js +410 -0
- package/dist/src/scanner/rules/database-exposure.d.ts +15 -0
- package/dist/src/scanner/rules/database-exposure.js +626 -0
- package/dist/src/scanner/rules/helpers.d.ts +31 -0
- package/dist/src/scanner/rules/helpers.js +85 -0
- package/dist/src/scanner/rules/index.d.ts +30 -0
- package/dist/src/scanner/rules/index.js +95 -0
- package/dist/src/scanner/rules/secret-entropy.d.ts +17 -0
- package/dist/src/scanner/rules/secret-entropy.js +338 -0
- package/dist/src/scanner/rules/shared.d.ts +100 -0
- package/dist/src/scanner/rules/shared.js +161 -0
- package/dist/src/scanner/rules/unguarded-routes.d.ts +16 -0
- package/dist/src/scanner/rules/unguarded-routes.js +526 -0
- package/dist/src/scanner/text.d.ts +90 -0
- package/dist/src/scanner/text.js +472 -0
- package/dist/src/scanner/types.d.ts +340 -0
- package/dist/src/scanner/types.js +195 -0
- package/dist/src/secrets/patterns.d.ts +63 -0
- package/dist/src/secrets/patterns.js +392 -0
- package/package.json +97 -4
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vibe-shield :: `npx -y @vibefoundry/vibe-shield init`
|
|
4
|
+
* -------------------------------------
|
|
5
|
+
* One command turns any repository into a guarded one. It installs:
|
|
6
|
+
*
|
|
7
|
+
* - `.claude/skills/vibe-shield/SKILL.md` — the Agent Skill, so Claude Code
|
|
8
|
+
* (and any SKILL.md-aware agent) audits before it reports "done".
|
|
9
|
+
* - an `AGENTS.md` section — the same contract for Codex, Cursor, Windsurf and
|
|
10
|
+
* every other agent that reads a repo-level instruction file.
|
|
11
|
+
* - `.github/workflows/vibe-shield.yml` — fails the build on new findings.
|
|
12
|
+
* - `.vibe-shieldignore` — the documented escape hatch for generated code.
|
|
13
|
+
*
|
|
14
|
+
* Nothing is overwritten without `--force`, and every write is reported so the
|
|
15
|
+
* command is safe to re-run.
|
|
16
|
+
*/
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.IGNORE_TEMPLATE = exports.WORKFLOW_TEMPLATE = exports.MCP_SERVER_ENTRY = exports.MCP_SERVER_SPEC = exports.MCP_SERVER_KEY = exports.AGENTS_SECTION = exports.SKILL_TEMPLATE = void 0;
|
|
52
|
+
exports.mcpServerEntry = mcpServerEntry;
|
|
53
|
+
exports.packageVersionFromDisk = packageVersionFromDisk;
|
|
54
|
+
exports.mergeMcpConfig = mergeMcpConfig;
|
|
55
|
+
exports.buildScaffold = buildScaffold;
|
|
56
|
+
exports.installScaffold = installScaffold;
|
|
57
|
+
exports.locateBundledSkill = locateBundledSkill;
|
|
58
|
+
const node_fs_1 = require("node:fs");
|
|
59
|
+
const path = __importStar(require("node:path"));
|
|
60
|
+
const shared_1 = require("../scanner/rules/shared");
|
|
61
|
+
const BANNER = '<!-- Managed by vibe-shield. Edit freely; `npx -y @vibefoundry/vibe-shield init --force` regenerates this file. -->';
|
|
62
|
+
/* -------------------------------------------------------------------------- */
|
|
63
|
+
/* SKILL.md */
|
|
64
|
+
/* -------------------------------------------------------------------------- */
|
|
65
|
+
exports.SKILL_TEMPLATE = `---
|
|
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.
|
|
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
|
+
license: MIT
|
|
70
|
+
metadata:
|
|
71
|
+
author: VibeFoundry
|
|
72
|
+
homepage: https://github.com/avivgerz-hue/vibe-shield
|
|
73
|
+
version: 1.0.0
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
# vibe-shield
|
|
77
|
+
|
|
78
|
+
You are not allowed to say "done" until the code you just wrote passes a security audit.
|
|
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.
|
|
81
|
+
|
|
82
|
+
## When to run
|
|
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
|
|
96
|
+
|
|
97
|
+
\`\`\`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
|
|
110
|
+
\`\`\`
|
|
111
|
+
|
|
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
|
+
\`\`\`
|
|
120
|
+
|
|
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 |
|
|
127
|
+
|
|
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.
|
|
129
|
+
|
|
130
|
+
## Hard rules
|
|
131
|
+
|
|
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.
|
|
137
|
+
|
|
138
|
+
## Agent surface audit
|
|
139
|
+
|
|
140
|
+
When you add or modify a skill, rule file, MCP server or instruction file, also run:
|
|
141
|
+
|
|
142
|
+
\`\`\`bash
|
|
143
|
+
npx -y @vibefoundry/vibe-shield audit-skills
|
|
144
|
+
\`\`\`
|
|
145
|
+
|
|
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
|
|
151
|
+
|
|
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
|
|
180
|
+
|
|
181
|
+
\`\`\`bash
|
|
182
|
+
npx -y @vibefoundry/vibe-shield init
|
|
183
|
+
\`\`\`
|
|
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
|
+
`;
|
|
190
|
+
/* -------------------------------------------------------------------------- */
|
|
191
|
+
/* AGENTS.md section */
|
|
192
|
+
/* -------------------------------------------------------------------------- */
|
|
193
|
+
exports.AGENTS_SECTION = `## Security gate (vibe-shield)
|
|
194
|
+
|
|
195
|
+
This repository is guarded by [vibe-shield](https://github.com/avivgerz-hue/vibe-shield).
|
|
196
|
+
|
|
197
|
+
Before you report any task as complete:
|
|
198
|
+
|
|
199
|
+
1. Run \`npx -y @vibefoundry/vibe-shield scan --fail-on high\` and fix every finding it reports.
|
|
200
|
+
2. Never suppress a finding (\`vibe-shield-disable\`, \`.vibe-shieldignore\` edits,
|
|
201
|
+
\`--ignore\`) to make the gate pass — that is the human's call.
|
|
202
|
+
3. If you touched \`.claude/\`, \`.cursor/\`, \`AGENTS.md\`, \`mcp.json\` or a skill,
|
|
203
|
+
also run \`npx -y @vibefoundry/vibe-shield audit-skills\`.
|
|
204
|
+
4. Surface any exposed credential to the user for rotation. Do not "fix" it by
|
|
205
|
+
deleting the line and moving on.
|
|
206
|
+
|
|
207
|
+
The gate exists because the most common way AI-written code reaches production
|
|
208
|
+
with a leaked \`SUPABASE_SERVICE_ROLE_KEY\` or an unauthenticated \`DELETE\`
|
|
209
|
+
endpoint is that nobody asked it a second question.
|
|
210
|
+
`;
|
|
211
|
+
/* -------------------------------------------------------------------------- */
|
|
212
|
+
/* .mcp.json */
|
|
213
|
+
/* -------------------------------------------------------------------------- */
|
|
214
|
+
/** Key this tool owns inside `mcpServers`; nothing else is ever touched. */
|
|
215
|
+
exports.MCP_SERVER_KEY = 'vibe-shield';
|
|
216
|
+
/**
|
|
217
|
+
* The server entry written into `.mcp.json`.
|
|
218
|
+
*
|
|
219
|
+
* `npx` is used rather than a local path so the config keeps working when the
|
|
220
|
+
* project is cloned elsewhere or the package is upgraded.
|
|
221
|
+
*
|
|
222
|
+
* The spec is always pinned to an exact version — `@vibefoundry/vibe-shield@1.1.0`.
|
|
223
|
+
* Two reasons: a bare name re-resolves to whatever `latest` points at on every
|
|
224
|
+
* launch, and vibe-shield's own `VSB-AGENT-004` rule flags an unpinned `npx -y`
|
|
225
|
+
* server as high severity. Pinning keeps the gate at A+ for projects that adopt
|
|
226
|
+
* the tool, and upgrades become explicit (`init --force` rewrites the pin).
|
|
227
|
+
*/
|
|
228
|
+
exports.MCP_SERVER_SPEC = shared_1.SELF_GITHUB_SPEC;
|
|
229
|
+
/** Build the `.mcp.json` entry, pinned to `version` when one is known. */
|
|
230
|
+
function mcpServerEntry(version) {
|
|
231
|
+
const spec = version ? `${exports.MCP_SERVER_SPEC}@${version}` : exports.MCP_SERVER_SPEC;
|
|
232
|
+
return { command: 'npx', args: ['--yes', spec, 'mcp'] };
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* The entry as this install writes it: pinned to the running package version.
|
|
236
|
+
* Falls back to unpinned only in a stripped checkout where no `package.json`
|
|
237
|
+
* names this package, which is the one case where no version is knowable.
|
|
238
|
+
*/
|
|
239
|
+
exports.MCP_SERVER_ENTRY = mcpServerEntry(packageVersionFromDisk());
|
|
240
|
+
/**
|
|
241
|
+
* Merge the vibe-shield server into an existing `.mcp.json` without disturbing
|
|
242
|
+
* anything already there.
|
|
243
|
+
*
|
|
244
|
+
* Returns the serialised file and whether our key was newly added. Existing
|
|
245
|
+
* servers, other top-level keys and the user's formatting choices are preserved;
|
|
246
|
+
* a malformed file is reported rather than overwritten, because silently
|
|
247
|
+
* rewriting someone's MCP config could break every server they run.
|
|
248
|
+
*/
|
|
249
|
+
/**
|
|
250
|
+
* The version of the running install, used to pin `.mcp.json`.
|
|
251
|
+
*
|
|
252
|
+
* Read from the nearest `package.json` that names this package. Falls back to
|
|
253
|
+
* the range in `package.json` only if nothing is found, which keeps `init`
|
|
254
|
+
* working in a stripped-down checkout rather than writing a nameless spec.
|
|
255
|
+
*/
|
|
256
|
+
function packageVersionFromDisk() {
|
|
257
|
+
let dir = __dirname;
|
|
258
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
259
|
+
try {
|
|
260
|
+
const raw = (0, node_fs_1.readFileSync)(path.join(dir, 'package.json'), 'utf8');
|
|
261
|
+
const parsed = JSON.parse(raw);
|
|
262
|
+
if ((parsed.name === shared_1.SELF_GITHUB_SPEC || parsed.name === 'vibe-shield') && parsed.version) {
|
|
263
|
+
return parsed.version;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// Keep walking up.
|
|
268
|
+
}
|
|
269
|
+
const parent = path.dirname(dir);
|
|
270
|
+
if (parent === dir)
|
|
271
|
+
break;
|
|
272
|
+
dir = parent;
|
|
273
|
+
}
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
function mergeMcpConfig(existing, options = {}) {
|
|
277
|
+
const entry = mcpServerEntry(options.version ?? packageVersionFromDisk());
|
|
278
|
+
if (existing === null || existing.trim() === '') {
|
|
279
|
+
return {
|
|
280
|
+
contents: `${JSON.stringify({ mcpServers: { [exports.MCP_SERVER_KEY]: entry } }, null, 2)}\n`,
|
|
281
|
+
changed: true,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
let parsed;
|
|
285
|
+
try {
|
|
286
|
+
parsed = JSON.parse(existing);
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
return {
|
|
290
|
+
contents: existing,
|
|
291
|
+
changed: false,
|
|
292
|
+
error: `.mcp.json is not valid JSON (${error instanceof Error ? error.message : String(error)}); left untouched.`,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
296
|
+
return { contents: existing, changed: false, error: '.mcp.json is not a JSON object; left untouched.' };
|
|
297
|
+
}
|
|
298
|
+
const config = parsed;
|
|
299
|
+
const servers = config.mcpServers;
|
|
300
|
+
if (servers !== undefined && (typeof servers !== 'object' || servers === null || Array.isArray(servers))) {
|
|
301
|
+
return { contents: existing, changed: false, error: '.mcp.json has a non-object "mcpServers"; left untouched.' };
|
|
302
|
+
}
|
|
303
|
+
const table = (servers ?? {});
|
|
304
|
+
const present = Object.prototype.hasOwnProperty.call(table, exports.MCP_SERVER_KEY);
|
|
305
|
+
if (present && !options.force) {
|
|
306
|
+
return { contents: existing, changed: false };
|
|
307
|
+
}
|
|
308
|
+
const next = { ...config, mcpServers: { ...table, [exports.MCP_SERVER_KEY]: entry } };
|
|
309
|
+
return { contents: `${JSON.stringify(next, null, 2)}\n`, changed: true };
|
|
310
|
+
}
|
|
311
|
+
/* -------------------------------------------------------------------------- */
|
|
312
|
+
/* GitHub Actions workflow */
|
|
313
|
+
/* -------------------------------------------------------------------------- */
|
|
314
|
+
exports.WORKFLOW_TEMPLATE = `name: vibe-shield
|
|
315
|
+
|
|
316
|
+
on:
|
|
317
|
+
push:
|
|
318
|
+
branches: [main, master]
|
|
319
|
+
pull_request:
|
|
320
|
+
workflow_dispatch:
|
|
321
|
+
|
|
322
|
+
permissions:
|
|
323
|
+
contents: read
|
|
324
|
+
security-events: write
|
|
325
|
+
pull-requests: write
|
|
326
|
+
|
|
327
|
+
jobs:
|
|
328
|
+
scan:
|
|
329
|
+
name: Security audit
|
|
330
|
+
runs-on: ubuntu-latest
|
|
331
|
+
timeout-minutes: 10
|
|
332
|
+
steps:
|
|
333
|
+
- uses: actions/checkout@v4
|
|
334
|
+
with:
|
|
335
|
+
fetch-depth: 0
|
|
336
|
+
|
|
337
|
+
- uses: actions/setup-node@v4
|
|
338
|
+
with:
|
|
339
|
+
node-version: 20
|
|
340
|
+
|
|
341
|
+
- name: Scan for vulnerabilities
|
|
342
|
+
run: npx -y @vibefoundry/vibe-shield scan --format sarif --out vibe-shield.sarif --fail-on high
|
|
343
|
+
continue-on-error: true
|
|
344
|
+
|
|
345
|
+
- name: Upload SARIF to code scanning
|
|
346
|
+
if: always()
|
|
347
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
348
|
+
with:
|
|
349
|
+
sarif_file: vibe-shield.sarif
|
|
350
|
+
category: vibe-shield
|
|
351
|
+
|
|
352
|
+
- name: Audit agent surface
|
|
353
|
+
run: npx -y @vibefoundry/vibe-shield audit-skills --fail-on high
|
|
354
|
+
|
|
355
|
+
- name: Enforce the gate
|
|
356
|
+
run: npx -y @vibefoundry/vibe-shield scan --fail-on high --quiet
|
|
357
|
+
`;
|
|
358
|
+
/* -------------------------------------------------------------------------- */
|
|
359
|
+
/* .vibe-shieldignore */
|
|
360
|
+
/* -------------------------------------------------------------------------- */
|
|
361
|
+
exports.IGNORE_TEMPLATE = `# .vibe-shieldignore — gitignore syntax.
|
|
362
|
+
#
|
|
363
|
+
# Keep this file as short as possible: every line hides real code from the
|
|
364
|
+
# security gate. Prefer an inline suppression with a reason where it applies:
|
|
365
|
+
#
|
|
366
|
+
# // vibe-shield-disable-next-line VSB-AI-001 -- path comes from a fixed enum
|
|
367
|
+
#
|
|
368
|
+
# Generated code, vendored third-party sources and build output are the only
|
|
369
|
+
# good reasons to ignore a path wholesale.
|
|
370
|
+
|
|
371
|
+
# Generated / vendored
|
|
372
|
+
# vendor/**
|
|
373
|
+
# **/*.generated.ts
|
|
374
|
+
# **/__generated__/**
|
|
375
|
+
|
|
376
|
+
# Local-only scratch work
|
|
377
|
+
# scratch/**
|
|
378
|
+
`;
|
|
379
|
+
/* -------------------------------------------------------------------------- */
|
|
380
|
+
/* Cursor rules */
|
|
381
|
+
/* -------------------------------------------------------------------------- */
|
|
382
|
+
const CURSOR_RULE = `---
|
|
383
|
+
description: Run vibe-shield before finishing any coding task
|
|
384
|
+
globs: ["**/*"]
|
|
385
|
+
alwaysApply: true
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
${BANNER}
|
|
389
|
+
|
|
390
|
+
# Security gate
|
|
391
|
+
|
|
392
|
+
Before reporting a task complete, run \`npx -y @vibefoundry/vibe-shield scan --fail-on high\` and
|
|
393
|
+
fix every finding. Never suppress a finding to make the gate pass. If the scan
|
|
394
|
+
reports an exposed credential, tell the user to rotate it at the provider.
|
|
395
|
+
`;
|
|
396
|
+
/** Build the full set of files for a given target repository. */
|
|
397
|
+
function buildScaffold(options) {
|
|
398
|
+
const agents = options.agents ?? 'all';
|
|
399
|
+
const files = [];
|
|
400
|
+
if (agents === 'claude' || agents === 'all') {
|
|
401
|
+
files.push({
|
|
402
|
+
relativePath: '.claude/skills/vibe-shield/SKILL.md',
|
|
403
|
+
contents: exports.SKILL_TEMPLATE,
|
|
404
|
+
mode: 'write',
|
|
405
|
+
description: 'Agent Skill — forces Claude Code to audit before finishing',
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
if (agents === 'cursor' || agents === 'all') {
|
|
409
|
+
files.push({
|
|
410
|
+
relativePath: '.cursor/rules/vibe-shield.mdc',
|
|
411
|
+
contents: CURSOR_RULE,
|
|
412
|
+
mode: 'write',
|
|
413
|
+
description: 'Cursor rule — same gate for Cursor/Windsurf',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (agents === 'generic' || agents === 'all') {
|
|
417
|
+
files.push({
|
|
418
|
+
relativePath: 'AGENTS.md',
|
|
419
|
+
contents: exports.AGENTS_SECTION,
|
|
420
|
+
mode: 'append',
|
|
421
|
+
marker: '## Security gate (vibe-shield)',
|
|
422
|
+
description: 'AGENTS.md section — Codex, Cursor, Windsurf, Amp',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (options.workflow !== false) {
|
|
426
|
+
files.push({
|
|
427
|
+
relativePath: '.github/workflows/vibe-shield.yml',
|
|
428
|
+
contents: exports.WORKFLOW_TEMPLATE,
|
|
429
|
+
mode: 'write',
|
|
430
|
+
description: 'GitHub Actions — fails the PR on new high/critical findings',
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
files.push({
|
|
434
|
+
relativePath: '.mcp.json',
|
|
435
|
+
contents: '', // produced by mergeMcpConfig so existing servers survive
|
|
436
|
+
mode: 'merge',
|
|
437
|
+
description: 'MCP server config — lets Claude Code/Desktop, Cursor and Codex call the scanner',
|
|
438
|
+
});
|
|
439
|
+
files.push({
|
|
440
|
+
relativePath: '.vibe-shieldignore',
|
|
441
|
+
contents: exports.IGNORE_TEMPLATE,
|
|
442
|
+
mode: 'write',
|
|
443
|
+
description: 'Ignore file — documented escape hatch',
|
|
444
|
+
});
|
|
445
|
+
return files;
|
|
446
|
+
}
|
|
447
|
+
const exists = async (file) => {
|
|
448
|
+
try {
|
|
449
|
+
await node_fs_1.promises.access(file);
|
|
450
|
+
return true;
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
/** Write the scaffold into `options.rootDir`. Idempotent unless `force`. */
|
|
457
|
+
async function installScaffold(options) {
|
|
458
|
+
const rootDir = path.resolve(options.rootDir);
|
|
459
|
+
const result = { written: [], appended: [], skipped: [], blocked: [], warnings: [] };
|
|
460
|
+
const resolvedVersion = options.version ?? options.env?.VIBE_SHIELD_VERSION ?? packageVersionFromDisk();
|
|
461
|
+
const files = buildScaffold(options);
|
|
462
|
+
for (const file of files) {
|
|
463
|
+
const absolute = path.join(rootDir, file.relativePath);
|
|
464
|
+
const present = await exists(absolute);
|
|
465
|
+
if (file.mode === 'append') {
|
|
466
|
+
let current = '';
|
|
467
|
+
if (present) {
|
|
468
|
+
try {
|
|
469
|
+
current = await node_fs_1.promises.readFile(absolute, 'utf8');
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
current = '';
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (file.marker && current.includes(file.marker)) {
|
|
476
|
+
result.skipped.push(file.relativePath);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
if (options.dryRun) {
|
|
480
|
+
result.appended.push(file.relativePath);
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
await node_fs_1.promises.mkdir(path.dirname(absolute), { recursive: true });
|
|
484
|
+
const separator = current.trim().length ? (current.endsWith('\n') ? '\n' : '\n\n') : '';
|
|
485
|
+
await node_fs_1.promises.appendFile(absolute, `${separator}${file.contents}`, 'utf8');
|
|
486
|
+
result.appended.push(file.relativePath);
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
if (file.mode === 'merge') {
|
|
490
|
+
let existing = null;
|
|
491
|
+
if (present) {
|
|
492
|
+
try {
|
|
493
|
+
existing = await node_fs_1.promises.readFile(absolute, 'utf8');
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
existing = null;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
const merged = mergeMcpConfig(existing, { force: options.force, version: resolvedVersion });
|
|
500
|
+
if (merged.error) {
|
|
501
|
+
// Not a `skipped` entry: `--force` cannot repair invalid JSON, so the
|
|
502
|
+
// summary must not suggest it can.
|
|
503
|
+
result.warnings.push(merged.error);
|
|
504
|
+
result.blocked.push(file.relativePath);
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (!merged.changed) {
|
|
508
|
+
result.skipped.push(file.relativePath);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
if (options.dryRun) {
|
|
512
|
+
result.written.push(file.relativePath);
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
await node_fs_1.promises.mkdir(path.dirname(absolute), { recursive: true });
|
|
516
|
+
await node_fs_1.promises.writeFile(absolute, merged.contents, 'utf8');
|
|
517
|
+
result.written.push(file.relativePath);
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
if (present && !options.force) {
|
|
521
|
+
result.skipped.push(file.relativePath);
|
|
522
|
+
continue;
|
|
523
|
+
}
|
|
524
|
+
if (options.dryRun) {
|
|
525
|
+
result.written.push(file.relativePath);
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
await node_fs_1.promises.mkdir(path.dirname(absolute), { recursive: true });
|
|
529
|
+
await node_fs_1.promises.writeFile(absolute, file.contents, 'utf8');
|
|
530
|
+
result.written.push(file.relativePath);
|
|
531
|
+
}
|
|
532
|
+
// A repository without a package.json will not be able to `npx -y @vibefoundry/vibe-shield`
|
|
533
|
+
// as a dev dependency; it still works through npx, so this is only a warning.
|
|
534
|
+
if (!(await exists(path.join(rootDir, 'package.json')))) {
|
|
535
|
+
result.warnings.push('No package.json found — run the gate with `npx -y @vibefoundry/vibe-shield scan`.');
|
|
536
|
+
}
|
|
537
|
+
if (!(await exists(path.join(rootDir, '.git')))) {
|
|
538
|
+
result.warnings.push('No .git directory found — the workflow only runs once this is a git repository.');
|
|
539
|
+
}
|
|
540
|
+
return result;
|
|
541
|
+
}
|
|
542
|
+
/** Locate the shipped `SKILL.md` (used by `init` to copy the canonical file). */
|
|
543
|
+
async function locateBundledSkill(startDir) {
|
|
544
|
+
let dir = startDir;
|
|
545
|
+
for (let depth = 0; depth < 8; depth += 1) {
|
|
546
|
+
const candidate = path.join(dir, 'SKILL.md');
|
|
547
|
+
const packageJson = path.join(dir, 'package.json');
|
|
548
|
+
if ((await exists(candidate)) && (await exists(packageJson)))
|
|
549
|
+
return candidate;
|
|
550
|
+
const parent = path.dirname(dir);
|
|
551
|
+
if (parent === dir)
|
|
552
|
+
break;
|
|
553
|
+
dir = parent;
|
|
554
|
+
}
|
|
555
|
+
return null;
|
|
556
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe-shield :: Model Context Protocol server
|
|
3
|
+
* -------------------------------------------
|
|
4
|
+
* A zero-dependency MCP server over stdio, so Claude Code, Claude Desktop,
|
|
5
|
+
* Cursor and Codex can call the scanner as a tool instead of shelling out and
|
|
6
|
+
* parsing console output.
|
|
7
|
+
*
|
|
8
|
+
* Transport is newline-delimited JSON-RPC 2.0. Requests are handled in arrival
|
|
9
|
+
* order and every response carries the matching `id`; notifications (no `id`)
|
|
10
|
+
* are never answered, per the JSON-RPC spec.
|
|
11
|
+
*
|
|
12
|
+
* Three tools are exposed:
|
|
13
|
+
* - `vibe_shield_scan` run the 60-rule scanner over a project
|
|
14
|
+
* - `vibe_shield_audit_skills` audit the agent surface for poisoning
|
|
15
|
+
* - `vibe_shield_explain_rule` full CWE + remediation detail for one rule
|
|
16
|
+
*
|
|
17
|
+
* Deliberate design constraints:
|
|
18
|
+
* - stdout is protocol-only. Diagnostics go to stderr, because a stray
|
|
19
|
+
* `console.log` corrupts the stream and breaks the client session.
|
|
20
|
+
* - No request is ever executed against a path outside the client's `roots`
|
|
21
|
+
* or the server's own cwd tree without an absolute path being explicit.
|
|
22
|
+
* - Every tool returns `isError` on failure rather than throwing, so a bad
|
|
23
|
+
* argument degrades one call instead of killing the session.
|
|
24
|
+
*/
|
|
25
|
+
/** JSON-RPC 2.0 error codes used by this server. */
|
|
26
|
+
export declare const RpcError: {
|
|
27
|
+
readonly parseError: -32700;
|
|
28
|
+
readonly invalidRequest: -32600;
|
|
29
|
+
readonly methodNotFound: -32601;
|
|
30
|
+
readonly invalidParams: -32602;
|
|
31
|
+
readonly internalError: -32603;
|
|
32
|
+
};
|
|
33
|
+
export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
34
|
+
export declare const SERVER_NAME = "vibe-shield";
|
|
35
|
+
/** The subset of `InitializeResult` this server advertises. */
|
|
36
|
+
export interface McpInitializeResult {
|
|
37
|
+
protocolVersion: string;
|
|
38
|
+
capabilities: {
|
|
39
|
+
tools: {
|
|
40
|
+
listChanged: boolean;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
serverInfo: {
|
|
44
|
+
name: string;
|
|
45
|
+
version: string;
|
|
46
|
+
title: string;
|
|
47
|
+
};
|
|
48
|
+
instructions: string;
|
|
49
|
+
}
|
|
50
|
+
export interface JsonRpcRequest {
|
|
51
|
+
jsonrpc: '2.0';
|
|
52
|
+
id?: string | number | null;
|
|
53
|
+
method: string;
|
|
54
|
+
params?: Record<string, unknown>;
|
|
55
|
+
}
|
|
56
|
+
export interface JsonRpcSuccess {
|
|
57
|
+
jsonrpc: '2.0';
|
|
58
|
+
id: string | number | null;
|
|
59
|
+
result: unknown;
|
|
60
|
+
}
|
|
61
|
+
export interface JsonRpcFailure {
|
|
62
|
+
jsonrpc: '2.0';
|
|
63
|
+
id: string | number | null;
|
|
64
|
+
error: {
|
|
65
|
+
code: number;
|
|
66
|
+
message: string;
|
|
67
|
+
data?: unknown;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure;
|
|
71
|
+
/** A single MCP tool description, as returned by `tools/list`. */
|
|
72
|
+
export interface McpToolDefinition {
|
|
73
|
+
name: string;
|
|
74
|
+
title: string;
|
|
75
|
+
description: string;
|
|
76
|
+
inputSchema: Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
export interface McpTextContent {
|
|
79
|
+
type: 'text';
|
|
80
|
+
text: string;
|
|
81
|
+
}
|
|
82
|
+
export interface McpToolResult {
|
|
83
|
+
content: McpTextContent[];
|
|
84
|
+
structuredContent?: Record<string, unknown>;
|
|
85
|
+
isError?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Tool catalogue. Descriptions are written for a model deciding *whether* to
|
|
89
|
+
* call, not for a human reading docs — they say when to reach for the tool.
|
|
90
|
+
*/
|
|
91
|
+
export declare const MCP_TOOLS: readonly McpToolDefinition[];
|
|
92
|
+
export interface McpServerOptions {
|
|
93
|
+
/** Directory that relative `target` values resolve against. */
|
|
94
|
+
cwd?: string;
|
|
95
|
+
/** Version string reported in `initialize` and scan results. */
|
|
96
|
+
version?: string;
|
|
97
|
+
}
|
|
98
|
+
/** Execute one tool by name. Never throws: failures become `isError` results. */
|
|
99
|
+
export declare function callTool(name: string, args: Record<string, unknown>, options?: McpServerOptions): Promise<McpToolResult>;
|
|
100
|
+
/**
|
|
101
|
+
* Handle one decoded JSON-RPC message.
|
|
102
|
+
* Returns `null` for notifications, which the spec forbids answering.
|
|
103
|
+
*/
|
|
104
|
+
export declare function handleMessage(message: unknown, options?: McpServerOptions): Promise<JsonRpcResponse | null>;
|
|
105
|
+
export interface McpServerIo {
|
|
106
|
+
stdin: NodeJS.ReadableStream;
|
|
107
|
+
stdout: {
|
|
108
|
+
write(chunk: string): unknown;
|
|
109
|
+
};
|
|
110
|
+
stderr: {
|
|
111
|
+
write(chunk: string): unknown;
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export interface McpServeOptions extends McpServerOptions {
|
|
115
|
+
/** Called after the transport stops, with the number of messages handled. */
|
|
116
|
+
onClose?: (handled: number) => void;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Serve MCP over newline-delimited JSON-RPC until stdin closes.
|
|
120
|
+
*
|
|
121
|
+
* Messages are processed sequentially so responses preserve request order and a
|
|
122
|
+
* slow scan cannot interleave with a later `tools/call`.
|
|
123
|
+
*/
|
|
124
|
+
export declare function serveMcp(io: McpServerIo, options?: McpServeOptions): Promise<number>;
|
|
125
|
+
/** Convenience wrapper used by `vibe-shield mcp`. */
|
|
126
|
+
export declare const startMcpServer: (options?: McpServerOptions) => Promise<number>;
|
|
127
|
+
export declare const MCP_TOOL_NAMES: readonly ["vibe_shield_scan", "vibe_shield_audit_skills", "vibe_shield_explain_rule"];
|