@vibefoundry/vibe-shield 1.1.0 → 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/README.md +45 -39
- package/SECURITY.md +1 -1
- package/SKILL.md +9 -9
- package/dist/bin/vibe-shield.d.ts +1 -1
- package/dist/bin/vibe-shield.js +545 -541
- package/dist/src/init/scaffold.d.ts +29 -31
- package/dist/src/init/scaffold.js +51 -68
- package/dist/src/mcp/server.js +1 -1
- package/dist/src/reporter/console-reporter.js +3 -3
- package/dist/src/reporter/patch-prompt-writer.js +2 -2
- package/dist/src/scanner/engine.js +5 -1
- package/dist/src/scanner/rules/database-exposure.js +1 -1
- package/dist/src/scanner/rules/shared.d.ts +1 -1
- package/dist/src/scanner/rules/shared.js +1 -1
- package/package.json +4 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* vibe-shield :: `npx
|
|
2
|
+
* vibe-shield :: `npx -y @vibefoundry/vibe-shield init`
|
|
3
3
|
* -------------------------------------
|
|
4
4
|
* One command turns any repository into a guarded one. It installs:
|
|
5
5
|
*
|
|
@@ -36,14 +36,14 @@ export interface InitOptions {
|
|
|
36
36
|
force?: boolean;
|
|
37
37
|
/** Print what would happen without writing. */
|
|
38
38
|
dryRun?: boolean;
|
|
39
|
-
/** Pin a specific version in the workflow (default `latest`). */
|
|
40
|
-
version?: string;
|
|
41
39
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
* Version to pin in both the workflow and the `.mcp.json` entry.
|
|
41
|
+
*
|
|
42
|
+
* Resolved from the installed package when omitted; see
|
|
43
|
+
* `packageVersionFromDisk`.
|
|
44
44
|
*/
|
|
45
|
-
|
|
46
|
-
/** Environment to read the
|
|
45
|
+
version?: string | null;
|
|
46
|
+
/** Environment to read the version override from. Defaults to `process.env`. */
|
|
47
47
|
env?: NodeJS.ProcessEnv;
|
|
48
48
|
}
|
|
49
49
|
export interface InitResult {
|
|
@@ -54,8 +54,8 @@ export interface InitResult {
|
|
|
54
54
|
blocked: string[];
|
|
55
55
|
warnings: string[];
|
|
56
56
|
}
|
|
57
|
-
export declare const SKILL_TEMPLATE = "---\nname: vibe-shield\ndescription: 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.\nallowed-tools: Bash(npx
|
|
58
|
-
export declare const AGENTS_SECTION = "## Security gate (vibe-shield)\n\nThis repository is guarded by [vibe-shield](https://github.com/avivgerz-hue/vibe-shield).\n\nBefore you report any task as complete:\n\n1. Run `npx
|
|
57
|
+
export declare const SKILL_TEMPLATE = "---\nname: vibe-shield\ndescription: 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.\nallowed-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\nlicense: MIT\nmetadata:\n author: VibeFoundry\n homepage: https://github.com/avivgerz-hue/vibe-shield\n version: 1.0.0\n---\n\n# vibe-shield\n\nYou are not allowed to say \"done\" until the code you just wrote passes a security audit.\n\nAI 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 \u2014 they are the default shape of code written quickly by something optimising for \"it works\". This skill is the second pair of eyes.\n\n## When to run\n\nRun the audit **after every change that touches**:\n\n- an API route, route handler, server action, RPC endpoint or GraphQL mutation\n- authentication, session, role, permission or ownership logic\n- a database migration, policy, grant, schema or Supabase table\n- an environment variable, config file, `.env*` file or build config\n- anything calling an LLM, a tool, a webhook, a shell command or the filesystem\n- `.claude/`, `.cursor/`, `AGENTS.md`, `mcp.json` or any skill/config file\n\nIf you did not touch any of those, you do not need to run it.\n\n## The loop\n\n```bash\n# 1. Scan (a whole-repo scan finishes in well under a second)\nnpx -y @vibefoundry/vibe-shield scan --fail-on high\n\n# 2. If it fails, generate the machine-readable fix list\nnpx -y @vibefoundry/vibe-shield scan --fix-prompt\n\n# 3. Read SECURITY_PATCH_PROMPT.md and fix every finding, critical first\n# Each entry has the exact file:line, the vulnerable snippet, the CWE and\n# the required change. Apply it literally.\n\n# 4. Re-run until it exits 0\nnpx -y @vibefoundry/vibe-shield scan --fail-on high\n```\n\n`--fail-on high` exits non-zero while any high or critical finding exists. That is the gate.\n\n## Reading a finding\n\n```\nCRITICAL 47:15 VSB-CLIENT-001 Server-only secret exposed through a public environment prefix\n const key = process.env.NEXT_PUBLIC_OPENAI_API_KEY;\n```\n\n| Part | Meaning |\n| --- | --- |\n| `47:15` | exact line and column to edit |\n| `VSB-CLIENT-001` | rule id \u2014 use it with `explain` for full docs |\n| message | why this specific line is exploitable |\n| `(CWE-200)` | bug class, for a deeper search if the fix is unclear |\n\nNeed the full picture? `npx -y @vibefoundry/vibe-shield explain VSB-CLIENT-001` prints the vulnerable shape, the fixed shape, remediation steps and references.\n\n## Hard rules\n\n1. **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.\n2. **Never suppress.** Do not add `// vibe-shield-disable`, do not edit `.vibe-shieldignore`, do not pass `--ignore`. Suppression is a decision for the human.\n3. **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 \u2014 the value may already be in a bundle, a cache or a git history.\n4. **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.\n5. **Re-verify.** A fix is not finished until the gate exits 0.\n\n## Agent surface audit\n\nWhen you add or modify a skill, rule file, MCP server or instruction file, also run:\n\n```bash\nnpx -y @vibefoundry/vibe-shield audit-skills\n```\n\nTwelve `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.\n\nEach 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 \u2014 read the triage.\n\n## Flags worth knowing\n\n| Flag | Effect |\n| --- | --- |\n| `--format json` | full machine-readable report (schemaVersion 1) |\n| `--format sarif` | SARIF 2.1.0 for GitHub code scanning |\n| `--format compact` | one line per finding |\n| `--format github` | inline GitHub Actions annotations |\n| `--severity high` | only report high and critical |\n| `--fail-on medium` | stricter gate |\n| `--include-tests` | include test and fixture files |\n| `--cache` | reuse results between runs |\n| `--quiet` | one-line verdict (ideal for piping) |\n| `rules` | list every rule |\n| `explain <RULE_ID>` | full documentation for one rule |\n\nIf the repository has a `.mcp.json`, the same three capabilities are also\navailable as MCP tools \u2014 `vibe_shield_scan`, `vibe_shield_audit_skills` and\n`vibe_shield_explain_rule`. Prefer those when they are listed, because they\nreturn structured findings instead of text you have to parse.\n\n## Definition of done\n\n- [ ] `npx -y @vibefoundry/vibe-shield scan --fail-on high` exits `0`\n- [ ] `npx -y @vibefoundry/vibe-shield audit-skills` exits `0` if you touched agent files\n- [ ] every credential the scan surfaced has been flagged to the user for rotation\n- [ ] no suppression directive was added\n- [ ] the feature still works and the existing tests still pass\n\n## Install it into a repository\n\n```bash\nnpx -y @vibefoundry/vibe-shield init\n```\n\nThat writes this file to `.claude/skills/vibe-shield/SKILL.md`, appends a security\ngate section to `AGENTS.md`, adds a `.cursor/rules/vibe-shield.mdc`, and installs a\nGitHub Actions workflow that fails the pull request on new high or critical\nfindings. Every future task in that repository starts guarded.\n";
|
|
58
|
+
export declare const AGENTS_SECTION = "## Security gate (vibe-shield)\n\nThis repository is guarded by [vibe-shield](https://github.com/avivgerz-hue/vibe-shield).\n\nBefore you report any task as complete:\n\n1. Run `npx -y @vibefoundry/vibe-shield scan --fail-on high` and fix every finding it reports.\n2. Never suppress a finding (`vibe-shield-disable`, `.vibe-shieldignore` edits,\n `--ignore`) to make the gate pass \u2014 that is the human's call.\n3. If you touched `.claude/`, `.cursor/`, `AGENTS.md`, `mcp.json` or a skill,\n also run `npx -y @vibefoundry/vibe-shield audit-skills`.\n4. Surface any exposed credential to the user for rotation. Do not \"fix\" it by\n deleting the line and moving on.\n\nThe gate exists because the most common way AI-written code reaches production\nwith a leaked `SUPABASE_SERVICE_ROLE_KEY` or an unauthenticated `DELETE`\nendpoint is that nobody asked it a second question.\n";
|
|
59
59
|
/** Key this tool owns inside `mcpServers`; nothing else is ever touched. */
|
|
60
60
|
export declare const MCP_SERVER_KEY = "vibe-shield";
|
|
61
61
|
/**
|
|
@@ -64,19 +64,23 @@ export declare const MCP_SERVER_KEY = "vibe-shield";
|
|
|
64
64
|
* `npx` is used rather than a local path so the config keeps working when the
|
|
65
65
|
* project is cloned elsewhere or the package is upgraded.
|
|
66
66
|
*
|
|
67
|
-
* The spec
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* (`init --force` rewrites
|
|
67
|
+
* The spec is always pinned to an exact version — `@vibefoundry/vibe-shield@1.1.0`.
|
|
68
|
+
* Two reasons: a bare name re-resolves to whatever `latest` points at on every
|
|
69
|
+
* launch, and vibe-shield's own `VSB-AGENT-004` rule flags an unpinned `npx -y`
|
|
70
|
+
* server as high severity. Pinning keeps the gate at A+ for projects that adopt
|
|
71
|
+
* the tool, and upgrades become explicit (`init --force` rewrites the pin).
|
|
72
72
|
*/
|
|
73
|
-
export declare const MCP_SERVER_SPEC = "
|
|
74
|
-
/** Build the `.mcp.json` entry, pinned to `
|
|
75
|
-
export declare function mcpServerEntry(
|
|
73
|
+
export declare const MCP_SERVER_SPEC = "@vibefoundry/vibe-shield";
|
|
74
|
+
/** Build the `.mcp.json` entry, pinned to `version` when one is known. */
|
|
75
|
+
export declare function mcpServerEntry(version?: string | null): {
|
|
76
76
|
command: string;
|
|
77
77
|
args: string[];
|
|
78
78
|
};
|
|
79
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* The entry as this install writes it: pinned to the running package version.
|
|
81
|
+
* Falls back to unpinned only in a stripped checkout where no `package.json`
|
|
82
|
+
* names this package, which is the one case where no version is knowable.
|
|
83
|
+
*/
|
|
80
84
|
export declare const MCP_SERVER_ENTRY: {
|
|
81
85
|
command: string;
|
|
82
86
|
args: string[];
|
|
@@ -91,28 +95,22 @@ export declare const MCP_SERVER_ENTRY: {
|
|
|
91
95
|
* rewriting someone's MCP config could break every server they run.
|
|
92
96
|
*/
|
|
93
97
|
/**
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* Deliberately conservative: returns `null` rather than guessing, because a
|
|
97
|
-
* wrong pin is worse than no pin (it would silently freeze the server on an
|
|
98
|
-
* unrelated commit). The revision is only derivable when the CLI is running
|
|
99
|
-
* from inside an npm install of this package, which is the `npx github:` case.
|
|
98
|
+
* The version of the running install, used to pin `.mcp.json`.
|
|
100
99
|
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* 3. `/app/.rev` — written into the container image at build time.
|
|
100
|
+
* Read from the nearest `package.json` that names this package. Falls back to
|
|
101
|
+
* the range in `package.json` only if nothing is found, which keeps `init`
|
|
102
|
+
* working in a stripped-down checkout rather than writing a nameless spec.
|
|
105
103
|
*/
|
|
106
|
-
export declare function
|
|
104
|
+
export declare function packageVersionFromDisk(): string | null;
|
|
107
105
|
export declare function mergeMcpConfig(existing: string | null, options?: {
|
|
108
106
|
force?: boolean;
|
|
109
|
-
|
|
107
|
+
version?: string | null;
|
|
110
108
|
}): {
|
|
111
109
|
contents: string;
|
|
112
110
|
changed: boolean;
|
|
113
111
|
error?: string;
|
|
114
112
|
};
|
|
115
|
-
export declare const WORKFLOW_TEMPLATE = "name: vibe-shield\n\non:\n push:\n branches: [main, master]\n pull_request:\n workflow_dispatch:\n\npermissions:\n contents: read\n security-events: write\n pull-requests: write\n\njobs:\n scan:\n name: Security audit\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - uses: actions/setup-node@v4\n with:\n node-version: 20\n\n - name: Scan for vulnerabilities\n run: npx
|
|
113
|
+
export declare const WORKFLOW_TEMPLATE = "name: vibe-shield\n\non:\n push:\n branches: [main, master]\n pull_request:\n workflow_dispatch:\n\npermissions:\n contents: read\n security-events: write\n pull-requests: write\n\njobs:\n scan:\n name: Security audit\n runs-on: ubuntu-latest\n timeout-minutes: 10\n steps:\n - uses: actions/checkout@v4\n with:\n fetch-depth: 0\n\n - uses: actions/setup-node@v4\n with:\n node-version: 20\n\n - name: Scan for vulnerabilities\n run: npx -y @vibefoundry/vibe-shield scan --format sarif --out vibe-shield.sarif --fail-on high\n continue-on-error: true\n\n - name: Upload SARIF to code scanning\n if: always()\n uses: github/codeql-action/upload-sarif@v3\n with:\n sarif_file: vibe-shield.sarif\n category: vibe-shield\n\n - name: Audit agent surface\n run: npx -y @vibefoundry/vibe-shield audit-skills --fail-on high\n\n - name: Enforce the gate\n run: npx -y @vibefoundry/vibe-shield scan --fail-on high --quiet\n";
|
|
116
114
|
export declare const IGNORE_TEMPLATE = "# .vibe-shieldignore \u2014 gitignore syntax.\n#\n# Keep this file as short as possible: every line hides real code from the\n# security gate. Prefer an inline suppression with a reason where it applies:\n#\n# // vibe-shield-disable-next-line VSB-AI-001 -- path comes from a fixed enum\n#\n# Generated code, vendored third-party sources and build output are the only\n# good reasons to ignore a path wholesale.\n\n# Generated / vendored\n# vendor/**\n# **/*.generated.ts\n# **/__generated__/**\n\n# Local-only scratch work\n# scratch/**\n";
|
|
117
115
|
/** Build the full set of files for a given target repository. */
|
|
118
116
|
export declare function buildScaffold(options: InitOptions): ScaffoldFile[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* vibe-shield :: `npx
|
|
3
|
+
* vibe-shield :: `npx -y @vibefoundry/vibe-shield init`
|
|
4
4
|
* -------------------------------------
|
|
5
5
|
* One command turns any repository into a guarded one. It installs:
|
|
6
6
|
*
|
|
@@ -50,7 +50,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
50
50
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
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
52
|
exports.mcpServerEntry = mcpServerEntry;
|
|
53
|
-
exports.
|
|
53
|
+
exports.packageVersionFromDisk = packageVersionFromDisk;
|
|
54
54
|
exports.mergeMcpConfig = mergeMcpConfig;
|
|
55
55
|
exports.buildScaffold = buildScaffold;
|
|
56
56
|
exports.installScaffold = installScaffold;
|
|
@@ -58,14 +58,14 @@ exports.locateBundledSkill = locateBundledSkill;
|
|
|
58
58
|
const node_fs_1 = require("node:fs");
|
|
59
59
|
const path = __importStar(require("node:path"));
|
|
60
60
|
const shared_1 = require("../scanner/rules/shared");
|
|
61
|
-
const BANNER = '<!-- Managed by vibe-shield. Edit freely; `npx
|
|
61
|
+
const BANNER = '<!-- Managed by vibe-shield. Edit freely; `npx -y @vibefoundry/vibe-shield init --force` regenerates this file. -->';
|
|
62
62
|
/* -------------------------------------------------------------------------- */
|
|
63
63
|
/* SKILL.md */
|
|
64
64
|
/* -------------------------------------------------------------------------- */
|
|
65
65
|
exports.SKILL_TEMPLATE = `---
|
|
66
66
|
name: vibe-shield
|
|
67
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
|
|
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
|
|
@@ -96,17 +96,17 @@ If you did not touch any of those, you do not need to run it.
|
|
|
96
96
|
|
|
97
97
|
\`\`\`bash
|
|
98
98
|
# 1. Scan (a whole-repo scan finishes in well under a second)
|
|
99
|
-
npx
|
|
99
|
+
npx -y @vibefoundry/vibe-shield scan --fail-on high
|
|
100
100
|
|
|
101
101
|
# 2. If it fails, generate the machine-readable fix list
|
|
102
|
-
npx
|
|
102
|
+
npx -y @vibefoundry/vibe-shield scan --fix-prompt
|
|
103
103
|
|
|
104
104
|
# 3. Read SECURITY_PATCH_PROMPT.md and fix every finding, critical first
|
|
105
105
|
# Each entry has the exact file:line, the vulnerable snippet, the CWE and
|
|
106
106
|
# the required change. Apply it literally.
|
|
107
107
|
|
|
108
108
|
# 4. Re-run until it exits 0
|
|
109
|
-
npx
|
|
109
|
+
npx -y @vibefoundry/vibe-shield scan --fail-on high
|
|
110
110
|
\`\`\`
|
|
111
111
|
|
|
112
112
|
\`--fail-on high\` exits non-zero while any high or critical finding exists. That is the gate.
|
|
@@ -125,7 +125,7 @@ CRITICAL 47:15 VSB-CLIENT-001 Server-only secret exposed through a public en
|
|
|
125
125
|
| message | why this specific line is exploitable |
|
|
126
126
|
| \`(CWE-200)\` | bug class, for a deeper search if the fix is unclear |
|
|
127
127
|
|
|
128
|
-
Need the full picture? \`npx
|
|
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
129
|
|
|
130
130
|
## Hard rules
|
|
131
131
|
|
|
@@ -140,7 +140,7 @@ Need the full picture? \`npx github:avivgerz-hue/vibe-shield explain VSB-CLIENT-
|
|
|
140
140
|
When you add or modify a skill, rule file, MCP server or instruction file, also run:
|
|
141
141
|
|
|
142
142
|
\`\`\`bash
|
|
143
|
-
npx
|
|
143
|
+
npx -y @vibefoundry/vibe-shield audit-skills
|
|
144
144
|
\`\`\`
|
|
145
145
|
|
|
146
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.
|
|
@@ -170,8 +170,8 @@ return structured findings instead of text you have to parse.
|
|
|
170
170
|
|
|
171
171
|
## Definition of done
|
|
172
172
|
|
|
173
|
-
- [ ] \`npx
|
|
174
|
-
- [ ] \`npx
|
|
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
175
|
- [ ] every credential the scan surfaced has been flagged to the user for rotation
|
|
176
176
|
- [ ] no suppression directive was added
|
|
177
177
|
- [ ] the feature still works and the existing tests still pass
|
|
@@ -179,7 +179,7 @@ return structured findings instead of text you have to parse.
|
|
|
179
179
|
## Install it into a repository
|
|
180
180
|
|
|
181
181
|
\`\`\`bash
|
|
182
|
-
npx
|
|
182
|
+
npx -y @vibefoundry/vibe-shield init
|
|
183
183
|
\`\`\`
|
|
184
184
|
|
|
185
185
|
That writes this file to \`.claude/skills/vibe-shield/SKILL.md\`, appends a security
|
|
@@ -196,11 +196,11 @@ This repository is guarded by [vibe-shield](https://github.com/avivgerz-hue/vibe
|
|
|
196
196
|
|
|
197
197
|
Before you report any task as complete:
|
|
198
198
|
|
|
199
|
-
1. Run \`npx
|
|
199
|
+
1. Run \`npx -y @vibefoundry/vibe-shield scan --fail-on high\` and fix every finding it reports.
|
|
200
200
|
2. Never suppress a finding (\`vibe-shield-disable\`, \`.vibe-shieldignore\` edits,
|
|
201
201
|
\`--ignore\`) to make the gate pass — that is the human's call.
|
|
202
202
|
3. If you touched \`.claude/\`, \`.cursor/\`, \`AGENTS.md\`, \`mcp.json\` or a skill,
|
|
203
|
-
also run \`npx
|
|
203
|
+
also run \`npx -y @vibefoundry/vibe-shield audit-skills\`.
|
|
204
204
|
4. Surface any exposed credential to the user for rotation. Do not "fix" it by
|
|
205
205
|
deleting the line and moving on.
|
|
206
206
|
|
|
@@ -219,20 +219,24 @@ exports.MCP_SERVER_KEY = 'vibe-shield';
|
|
|
219
219
|
* `npx` is used rather than a local path so the config keeps working when the
|
|
220
220
|
* project is cloned elsewhere or the package is upgraded.
|
|
221
221
|
*
|
|
222
|
-
* The spec
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* (`init --force` rewrites
|
|
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
227
|
*/
|
|
228
228
|
exports.MCP_SERVER_SPEC = shared_1.SELF_GITHUB_SPEC;
|
|
229
|
-
/** Build the `.mcp.json` entry, pinned to `
|
|
230
|
-
function mcpServerEntry(
|
|
231
|
-
const 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
232
|
return { command: 'npx', args: ['--yes', spec, 'mcp'] };
|
|
233
233
|
}
|
|
234
|
-
/**
|
|
235
|
-
|
|
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());
|
|
236
240
|
/**
|
|
237
241
|
* Merge the vibe-shield server into an existing `.mcp.json` without disturbing
|
|
238
242
|
* anything already there.
|
|
@@ -243,55 +247,34 @@ exports.MCP_SERVER_ENTRY = mcpServerEntry();
|
|
|
243
247
|
* rewriting someone's MCP config could break every server they run.
|
|
244
248
|
*/
|
|
245
249
|
/**
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* Deliberately conservative: returns `null` rather than guessing, because a
|
|
249
|
-
* wrong pin is worse than no pin (it would silently freeze the server on an
|
|
250
|
-
* unrelated commit). The revision is only derivable when the CLI is running
|
|
251
|
-
* from inside an npm install of this package, which is the `npx github:` case.
|
|
250
|
+
* The version of the running install, used to pin `.mcp.json`.
|
|
252
251
|
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* 3. `/app/.rev` — written into the container image at build time.
|
|
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.
|
|
257
255
|
*/
|
|
258
|
-
|
|
259
|
-
const explicit = env.VIBE_SHIELD_REV?.trim();
|
|
260
|
-
if (explicit)
|
|
261
|
-
return explicit;
|
|
262
|
-
const shaPattern = /(?:[?#]([0-9a-f]{40}))\b/i;
|
|
263
|
-
// A lockfile one or two directories above this file, in an npm install.
|
|
256
|
+
function packageVersionFromDisk() {
|
|
264
257
|
let dir = __dirname;
|
|
265
|
-
for (let depth = 0; depth <
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
return match[1];
|
|
272
|
-
}
|
|
273
|
-
catch {
|
|
274
|
-
// Keep walking.
|
|
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;
|
|
275
264
|
}
|
|
276
265
|
}
|
|
266
|
+
catch {
|
|
267
|
+
// Keep walking up.
|
|
268
|
+
}
|
|
277
269
|
const parent = path.dirname(dir);
|
|
278
270
|
if (parent === dir)
|
|
279
271
|
break;
|
|
280
272
|
dir = parent;
|
|
281
273
|
}
|
|
282
|
-
try {
|
|
283
|
-
const rev = (await node_fs_1.promises.readFile('/app/.rev', 'utf8')).trim();
|
|
284
|
-
if (/^[0-9a-f]{40}$/i.test(rev))
|
|
285
|
-
return rev;
|
|
286
|
-
}
|
|
287
|
-
catch {
|
|
288
|
-
// Not in the container image.
|
|
289
|
-
}
|
|
290
|
-
void rootDir;
|
|
291
274
|
return null;
|
|
292
275
|
}
|
|
293
276
|
function mergeMcpConfig(existing, options = {}) {
|
|
294
|
-
const entry = mcpServerEntry(options.
|
|
277
|
+
const entry = mcpServerEntry(options.version ?? packageVersionFromDisk());
|
|
295
278
|
if (existing === null || existing.trim() === '') {
|
|
296
279
|
return {
|
|
297
280
|
contents: `${JSON.stringify({ mcpServers: { [exports.MCP_SERVER_KEY]: entry } }, null, 2)}\n`,
|
|
@@ -356,7 +339,7 @@ jobs:
|
|
|
356
339
|
node-version: 20
|
|
357
340
|
|
|
358
341
|
- name: Scan for vulnerabilities
|
|
359
|
-
run: npx
|
|
342
|
+
run: npx -y @vibefoundry/vibe-shield scan --format sarif --out vibe-shield.sarif --fail-on high
|
|
360
343
|
continue-on-error: true
|
|
361
344
|
|
|
362
345
|
- name: Upload SARIF to code scanning
|
|
@@ -367,10 +350,10 @@ jobs:
|
|
|
367
350
|
category: vibe-shield
|
|
368
351
|
|
|
369
352
|
- name: Audit agent surface
|
|
370
|
-
run: npx
|
|
353
|
+
run: npx -y @vibefoundry/vibe-shield audit-skills --fail-on high
|
|
371
354
|
|
|
372
355
|
- name: Enforce the gate
|
|
373
|
-
run: npx
|
|
356
|
+
run: npx -y @vibefoundry/vibe-shield scan --fail-on high --quiet
|
|
374
357
|
`;
|
|
375
358
|
/* -------------------------------------------------------------------------- */
|
|
376
359
|
/* .vibe-shieldignore */
|
|
@@ -406,7 +389,7 @@ ${BANNER}
|
|
|
406
389
|
|
|
407
390
|
# Security gate
|
|
408
391
|
|
|
409
|
-
Before reporting a task complete, run \`npx
|
|
392
|
+
Before reporting a task complete, run \`npx -y @vibefoundry/vibe-shield scan --fail-on high\` and
|
|
410
393
|
fix every finding. Never suppress a finding to make the gate pass. If the scan
|
|
411
394
|
reports an exposed credential, tell the user to rotate it at the provider.
|
|
412
395
|
`;
|
|
@@ -474,7 +457,7 @@ const exists = async (file) => {
|
|
|
474
457
|
async function installScaffold(options) {
|
|
475
458
|
const rootDir = path.resolve(options.rootDir);
|
|
476
459
|
const result = { written: [], appended: [], skipped: [], blocked: [], warnings: [] };
|
|
477
|
-
const
|
|
460
|
+
const resolvedVersion = options.version ?? options.env?.VIBE_SHIELD_VERSION ?? packageVersionFromDisk();
|
|
478
461
|
const files = buildScaffold(options);
|
|
479
462
|
for (const file of files) {
|
|
480
463
|
const absolute = path.join(rootDir, file.relativePath);
|
|
@@ -513,7 +496,7 @@ async function installScaffold(options) {
|
|
|
513
496
|
existing = null;
|
|
514
497
|
}
|
|
515
498
|
}
|
|
516
|
-
const merged = mergeMcpConfig(existing, { force: options.force,
|
|
499
|
+
const merged = mergeMcpConfig(existing, { force: options.force, version: resolvedVersion });
|
|
517
500
|
if (merged.error) {
|
|
518
501
|
// Not a `skipped` entry: `--force` cannot repair invalid JSON, so the
|
|
519
502
|
// summary must not suggest it can.
|
|
@@ -546,10 +529,10 @@ async function installScaffold(options) {
|
|
|
546
529
|
await node_fs_1.promises.writeFile(absolute, file.contents, 'utf8');
|
|
547
530
|
result.written.push(file.relativePath);
|
|
548
531
|
}
|
|
549
|
-
// A repository without a package.json will not be able to `npx
|
|
532
|
+
// A repository without a package.json will not be able to `npx -y @vibefoundry/vibe-shield`
|
|
550
533
|
// as a dev dependency; it still works through npx, so this is only a warning.
|
|
551
534
|
if (!(await exists(path.join(rootDir, 'package.json')))) {
|
|
552
|
-
result.warnings.push('No package.json found — run the gate with `npx
|
|
535
|
+
result.warnings.push('No package.json found — run the gate with `npx -y @vibefoundry/vibe-shield scan`.');
|
|
553
536
|
}
|
|
554
537
|
if (!(await exists(path.join(rootDir, '.git')))) {
|
|
555
538
|
result.warnings.push('No .git directory found — the workflow only runs once this is a git repository.');
|
package/dist/src/mcp/server.js
CHANGED
|
@@ -334,7 +334,7 @@ async function callTool(name, args, options = {}) {
|
|
|
334
334
|
return errorResult('`ruleId` is required, e.g. "VSB-CLIENT-001".');
|
|
335
335
|
const rule = (0, rules_1.getRule)(ruleId);
|
|
336
336
|
if (!rule)
|
|
337
|
-
return errorResult(`Unknown rule "${ruleId}". Call tools/list or \`npx
|
|
337
|
+
return errorResult(`Unknown rule "${ruleId}". Call tools/list or \`npx -y @vibefoundry/vibe-shield rules\` for the catalogue.`);
|
|
338
338
|
return explainRuleResult(rule);
|
|
339
339
|
}
|
|
340
340
|
return errorResult(`Unknown tool "${name}".`);
|
|
@@ -216,9 +216,9 @@ function renderFooter(result, color) {
|
|
|
216
216
|
if (critical > 0) {
|
|
217
217
|
lines.push(color.red(` ⚠ ${critical} critical finding${critical === 1 ? '' : 's'} — rotate any exposed credential before anything else.`));
|
|
218
218
|
}
|
|
219
|
-
lines.push(` ${color.bold('Fix it')} npx
|
|
220
|
-
lines.push(` ${color.bold('Explain')} npx
|
|
221
|
-
lines.push(` ${color.bold('Gate CI')} npx
|
|
219
|
+
lines.push(` ${color.bold('Fix it')} npx -y @vibefoundry/vibe-shield scan --fix-prompt ${color.dim('# writes SECURITY_PATCH_PROMPT.md for your agent')}`);
|
|
220
|
+
lines.push(` ${color.bold('Explain')} npx -y @vibefoundry/vibe-shield explain ${result.findings[0]?.ruleId ?? 'VSB-AI-001'}`);
|
|
221
|
+
lines.push(` ${color.bold('Gate CI')} npx -y @vibefoundry/vibe-shield scan --fail-on ${worst} --format github`);
|
|
222
222
|
for (const notice of result.notices)
|
|
223
223
|
lines.push(color.dim(` ${notice}`));
|
|
224
224
|
return lines;
|
|
@@ -119,7 +119,7 @@ function buildPatchPrompt(result, options = {}) {
|
|
|
119
119
|
lines.push('');
|
|
120
120
|
lines.push('After each group of fixes run:');
|
|
121
121
|
lines.push('');
|
|
122
|
-
lines.push(...fence('npx
|
|
122
|
+
lines.push(...fence('npx -y @vibefoundry/vibe-shield scan --fail-on high', 'bash'));
|
|
123
123
|
lines.push('');
|
|
124
124
|
lines.push('The task is complete only when this command exits `0` and every finding below is gone.');
|
|
125
125
|
lines.push('');
|
|
@@ -195,7 +195,7 @@ function buildPatchPrompt(result, options = {}) {
|
|
|
195
195
|
lines.push('## Final checklist');
|
|
196
196
|
lines.push('');
|
|
197
197
|
lines.push('- [ ] Every credential that appeared in this report has been rotated by the user.');
|
|
198
|
-
lines.push('- [ ] `npx
|
|
198
|
+
lines.push('- [ ] `npx -y @vibefoundry/vibe-shield scan --fail-on high` exits `0`.');
|
|
199
199
|
lines.push('- [ ] No `vibe-shield-disable` comments were added.');
|
|
200
200
|
lines.push('- [ ] The application still builds and its tests still pass.');
|
|
201
201
|
lines.push('');
|
|
@@ -521,7 +521,11 @@ async function packageVersion() {
|
|
|
521
521
|
try {
|
|
522
522
|
const raw = await node_fs_1.promises.readFile(path.join(dir, 'package.json'), 'utf8');
|
|
523
523
|
const parsed = JSON.parse(raw);
|
|
524
|
-
|
|
524
|
+
// Match the scoped name *or* the legacy bare one, so an install made
|
|
525
|
+
// before the package was published under `@vibefoundry/` still resolves
|
|
526
|
+
// its own version instead of reporting the fallback.
|
|
527
|
+
const isThisPackage = parsed.name === '@vibefoundry/vibe-shield' || parsed.name === 'vibe-shield';
|
|
528
|
+
if (isThisPackage && parsed.version) {
|
|
525
529
|
cachedVersion = parsed.version;
|
|
526
530
|
return cachedVersion;
|
|
527
531
|
}
|
|
@@ -60,7 +60,7 @@ const tableWithoutRls = (0, helpers_1.defineRule)({
|
|
|
60
60
|
'Add `alter table public.<table> enable row level security;` to the migration that creates the table (never a later one).',
|
|
61
61
|
'Add a policy that scopes rows to the caller, e.g. `using (auth.uid() = user_id)` for each of select/insert/update/delete.',
|
|
62
62
|
'Use the service-role key only in server code, and only where bypassing RLS is genuinely required.',
|
|
63
|
-
'Run `npx
|
|
63
|
+
'Run `npx -y @vibefoundry/vibe-shield scan` again — the table should disappear from this report even if no policy was added yet (deny-all is safe).',
|
|
64
64
|
],
|
|
65
65
|
references: [
|
|
66
66
|
'https://supabase.com/docs/guides/database/postgres/row-level-security',
|
|
@@ -81,7 +81,7 @@ export declare const confident: (value: Confidence, boost?: boolean) => Confiden
|
|
|
81
81
|
* Single source of truth: `init` writes it into `.mcp.json`, and the auditor
|
|
82
82
|
* recognises it there. Keeping one constant means the two can never drift.
|
|
83
83
|
*/
|
|
84
|
-
export declare const SELF_GITHUB_SPEC = "
|
|
84
|
+
export declare const SELF_GITHUB_SPEC = "@vibefoundry/vibe-shield";
|
|
85
85
|
/** First-party install specs the auditor vouches for without a `@sha`. */
|
|
86
86
|
export declare const SELF_INSTALL_SPECS: readonly string[];
|
|
87
87
|
/**
|
|
@@ -137,7 +137,7 @@ exports.confident = confident;
|
|
|
137
137
|
* Single source of truth: `init` writes it into `.mcp.json`, and the auditor
|
|
138
138
|
* recognises it there. Keeping one constant means the two can never drift.
|
|
139
139
|
*/
|
|
140
|
-
exports.SELF_GITHUB_SPEC = '
|
|
140
|
+
exports.SELF_GITHUB_SPEC = '@vibefoundry/vibe-shield';
|
|
141
141
|
/** First-party install specs the auditor vouches for without a `@sha`. */
|
|
142
142
|
exports.SELF_INSTALL_SPECS = [exports.SELF_GITHUB_SPEC];
|
|
143
143
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibefoundry/vibe-shield",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "High-speed static security analyzer and MCP/agent-skill guard for AI-generated codebases. Catches the vulnerabilities autonomous coding agents ship: leaked secrets, unguarded mutation routes, missing Postgres RLS, unverified webhooks, prompt injection sinks and XSS.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -92,5 +92,8 @@
|
|
|
92
92
|
"@vitest/coverage-v8": "^5.1.0",
|
|
93
93
|
"typescript": "^5.9.3",
|
|
94
94
|
"vitest": "^5.1.0"
|
|
95
|
+
},
|
|
96
|
+
"publishConfig": {
|
|
97
|
+
"access": "public"
|
|
95
98
|
}
|
|
96
99
|
}
|