@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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +577 -3
  3. package/SECURITY.md +110 -0
  4. package/SKILL.md +124 -0
  5. package/dist/bin/vibe-shield.d.ts +59 -0
  6. package/dist/bin/vibe-shield.js +545 -0
  7. package/dist/src/index.d.ts +25 -0
  8. package/dist/src/index.js +108 -0
  9. package/dist/src/init/scaffold.d.ts +120 -0
  10. package/dist/src/init/scaffold.js +556 -0
  11. package/dist/src/mcp/server.d.ts +127 -0
  12. package/dist/src/mcp/server.js +479 -0
  13. package/dist/src/mcp-guard/skill-auditor.d.ts +105 -0
  14. package/dist/src/mcp-guard/skill-auditor.js +1129 -0
  15. package/dist/src/reporter/console-reporter.d.ts +58 -0
  16. package/dist/src/reporter/console-reporter.js +254 -0
  17. package/dist/src/reporter/formatters.d.ts +38 -0
  18. package/dist/src/reporter/formatters.js +262 -0
  19. package/dist/src/reporter/patch-prompt-writer.d.ts +35 -0
  20. package/dist/src/reporter/patch-prompt-writer.js +238 -0
  21. package/dist/src/scanner/context.d.ts +51 -0
  22. package/dist/src/scanner/context.js +568 -0
  23. package/dist/src/scanner/engine.d.ts +130 -0
  24. package/dist/src/scanner/engine.js +726 -0
  25. package/dist/src/scanner/gitignore.d.ts +49 -0
  26. package/dist/src/scanner/gitignore.js +195 -0
  27. package/dist/src/scanner/lexicon.d.ts +56 -0
  28. package/dist/src/scanner/lexicon.js +357 -0
  29. package/dist/src/scanner/rules/ai-vulnerabilities.d.ts +15 -0
  30. package/dist/src/scanner/rules/ai-vulnerabilities.js +859 -0
  31. package/dist/src/scanner/rules/client-exposure.d.ts +18 -0
  32. package/dist/src/scanner/rules/client-exposure.js +410 -0
  33. package/dist/src/scanner/rules/database-exposure.d.ts +15 -0
  34. package/dist/src/scanner/rules/database-exposure.js +626 -0
  35. package/dist/src/scanner/rules/helpers.d.ts +31 -0
  36. package/dist/src/scanner/rules/helpers.js +85 -0
  37. package/dist/src/scanner/rules/index.d.ts +30 -0
  38. package/dist/src/scanner/rules/index.js +95 -0
  39. package/dist/src/scanner/rules/secret-entropy.d.ts +17 -0
  40. package/dist/src/scanner/rules/secret-entropy.js +338 -0
  41. package/dist/src/scanner/rules/shared.d.ts +100 -0
  42. package/dist/src/scanner/rules/shared.js +161 -0
  43. package/dist/src/scanner/rules/unguarded-routes.d.ts +16 -0
  44. package/dist/src/scanner/rules/unguarded-routes.js +526 -0
  45. package/dist/src/scanner/text.d.ts +90 -0
  46. package/dist/src/scanner/text.js +472 -0
  47. package/dist/src/scanner/types.d.ts +340 -0
  48. package/dist/src/scanner/types.js +195 -0
  49. package/dist/src/secrets/patterns.d.ts +63 -0
  50. package/dist/src/secrets/patterns.js +392 -0
  51. package/package.json +97 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 VibeFoundry
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,577 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ <div align="center">
2
+
3
+ # 🛡️ vibe-shield
4
+
5
+ **The security gate for AI-written code.**
6
+
7
+ Your agent ships fast. It also ships `NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY`, a `DELETE` route with no session check, and a `CREATE TABLE` with no Row Level Security — because it was optimising for "it works".
8
+
9
+ vibe-shield is a **static security scanner built for that failure mode**, plus an **Agent Skill** that makes Claude Code, Codex, Cursor and Windsurf audit before they say *"done"*.
10
+
11
+ [![npm version](https://img.shields.io/npm/v/@vibefoundry/vibe-shield.svg)](https://www.npmjs.com/package/@vibefoundry/vibe-shield)
12
+ [![npm downloads](https://img.shields.io/npm/dm/@vibefoundry/vibe-shield.svg)](https://www.npmjs.com/package/@vibefoundry/vibe-shield)
13
+ [![CI](https://img.shields.io/github/actions/workflow/status/avivgerz-hue/vibe-shield/ci.yml?branch=main&style=flat-square&logo=githubactions&label=CI)](https://github.com/avivgerz-hue/vibe-shield/actions/workflows/ci.yml)
14
+ [![node](https://img.shields.io/badge/node-%E2%89%A518.17.0-5fa04e?style=flat-square&logo=nodedotjs)](#-requirements)
15
+ [![license](https://img.shields.io/github/license/avivgerz-hue/vibe-shield?style=flat-square&color=blue)](./LICENSE)
16
+ [![zero dependencies](https://img.shields.io/badge/runtime%20deps-0-brightgreen?style=flat-square)](./package.json)
17
+ [![rules](https://img.shields.io/badge/detection%20signatures-60-8b5cf6?style=flat-square)](./src/scanner/rules)
18
+ [![self-scan](https://img.shields.io/badge/self--scan-100%2F100%20A%2B-16a34a?style=flat-square)](#-it-scans-itself)
19
+
20
+ ```bash
21
+ npx -y @vibefoundry/vibe-shield scan
22
+ ```
23
+
24
+ ![vibe-shield terminal preview](./assets/hero-terminal.svg)
25
+
26
+ </div>
27
+
28
+ ---
29
+
30
+ ## The problem, in one file
31
+
32
+ This is the shape of code an agent writes when you ask for "a delete endpoint":
33
+
34
+ ```ts
35
+ // app/api/users/route.ts
36
+ export async function DELETE(req: Request) {
37
+ const { id, userId } = await req.json();
38
+ await db.query(`DELETE FROM users WHERE id = ${id} AND owner = ${userId}`);
39
+ return new Response('ok');
40
+ }
41
+ ```
42
+
43
+ It works. It also has **three critical vulnerabilities** — no authentication, SQL injection, and an IDOR that lets the caller choose whose data they delete. Nothing in a normal review loop catches all three, and the agent reported the task as complete.
44
+
45
+ `npx -y @vibefoundry/vibe-shield scan` catches all three in **17 milliseconds**:
46
+
47
+ ```
48
+ 🛡️ vibe-shield v1.0.0 · security scan
49
+ target /home/you/app
50
+
51
+ 💀 GRADE F — 0/100 · 3 critical
52
+
53
+ CRITICAL 3 ███
54
+
55
+ Findings
56
+
57
+ app/api/users/route.ts
58
+ CRITICAL 2:1 VSB-AUTH-001 Next.js App Router mutation handler has no authentication check
59
+ export async function DELETE(req: Request) {
60
+ CRITICAL 4:22 VSB-DB-007 SQL statement built by concatenating request data
61
+ await db.query(`DELETE FROM users WHERE id = ${id} AND owner = ${userId}`);
62
+ CRITICAL 4:22 VSB-AUTH-004 Record ownership taken from the request instead of the session (IDOR)
63
+
64
+ 7 files · 1 KiB · 412 files/s · 17 ms · 48 rules
65
+
66
+ ⚠ 3 critical findings — rotate any exposed credential before anything else.
67
+ Fix it npx -y @vibefoundry/vibe-shield scan --fix-prompt # writes SECURITY_PATCH_PROMPT.md for your agent
68
+ Explain npx -y @vibefoundry/vibe-shield explain VSB-AUTH-001
69
+ Gate CI npx -y @vibefoundry/vibe-shield scan --fail-on critical --format github
70
+ ```
71
+
72
+ Then hand the fix list to the agent that wrote the bug:
73
+
74
+ ```bash
75
+ npx -y @vibefoundry/vibe-shield scan --fix-prompt # → SECURITY_PATCH_PROMPT.md
76
+ ```
77
+
78
+ That file contains, for every finding: the severity, the CWE id and link, the exact `file:line`, the vulnerable snippet, the ordered remediation steps, the target code shape, and a verification command. It is written for a machine to execute and a human to audit.
79
+
80
+ ---
81
+
82
+ ## ⚡ Quick start
83
+
84
+ ```bash
85
+ # 1. Scan (no install, no config, no network calls)
86
+ npx -y @vibefoundry/vibe-shield scan
87
+
88
+ # 2. Give your agent the fix list
89
+ npx -y @vibefoundry/vibe-shield scan --fix-prompt
90
+
91
+ # 3. Install the gate permanently — skill + CI + ignore file
92
+ npx -y @vibefoundry/vibe-shield init
93
+ ```
94
+
95
+ > **Use the scoped name.** vibe-shield is published as
96
+ > [`@vibefoundry/vibe-shield`](https://www.npmjs.com/package/@vibefoundry/vibe-shield).
97
+ > The *unscoped* name `vibe-shield` on npm belongs to an unrelated project, so
98
+ > `npx vibe-shield` runs someone else's code. Always include the `@vibefoundry/`
99
+ > scope. The package ships a prebuilt `dist/`, needs **no TypeScript**, and
100
+ > starts in milliseconds.
101
+
102
+ `npx -y @vibefoundry/vibe-shield init` writes:
103
+
104
+ | File | What it does |
105
+ | --- | --- |
106
+ | `.claude/skills/vibe-shield/SKILL.md` | Agent Skill: forces Claude Code (and any SKILL.md-aware agent) to audit before finishing |
107
+ | `AGENTS.md` *(section)* | Same contract for Codex, Cursor, Windsurf, Amp, and anything that reads a repo instruction file |
108
+ | `.cursor/rules/vibe-shield.mdc` | `alwaysApply` Cursor rule |
109
+ | `.mcp.json` | MCP server entry (merged — your existing servers are preserved) |
110
+ | `.github/workflows/vibe-shield.yml` | Fails the pull request on new high/critical findings, uploads SARIF to code scanning |
111
+ | `.vibe-shieldignore` | The documented escape hatch (empty by default, with guidance) |
112
+
113
+ Nothing is overwritten without `--force`, and re-running is a no-op.
114
+
115
+ ---
116
+
117
+ ## 🧠 Why AI code needs its own scanner
118
+
119
+ Traditional SAST rules are built around human mistakes: a forgotten escape, a missing `prepare()`. Agent-written code fails differently — it is *confidently complete* and *structurally plausible*:
120
+
121
+ | What a human does | What an agent does |
122
+ | --- | --- |
123
+ | forgets to validate one input | writes a perfect endpoint with **no auth at all** |
124
+ | uses `eval` in one legacy file | puts a **tool `execute` that shells out** behind an LLM |
125
+ | commits a `.env` by accident | puts `SUPABASE_SERVICE_ROLE_KEY` behind `NEXT_PUBLIC_` because "it needed the env var" |
126
+ | writes `CREATE TABLE` | writes `CREATE TABLE` **and forgets the `ENABLE ROW LEVEL SECURITY`**, so the anon key reads everything |
127
+ | adds `dangerouslySetInnerHTML` to render a doc | renders **model output** with it, creating stored XSS with a step counter |
128
+
129
+ vibe-shield encodes those specific shapes: 60 signatures across five families, each one derived from a real agent-generated bug rather than a generic CWE checklist.
130
+
131
+ ---
132
+
133
+ ## 📊 Vulnerability Coverage Matrix
134
+
135
+ **48 code signatures + 12 agent-integrity signatures = 60.** Every rule has an id, a CWE, a vulnerable example, a fixed example, and ordered remediation steps — run `npx -y @vibefoundry/vibe-shield rules` or `npx -y @vibefoundry/vibe-shield explain <ID>`.
136
+
137
+ ### Client-side secret & data exposure — `VSB-CLIENT-*`
138
+
139
+ | ID | Detects | CWE | Sev |
140
+ | --- | --- | --- | --- |
141
+ | `VSB-CLIENT-001` | `NEXT_PUBLIC_` / `VITE_` / `PUBLIC_` / `REACT_APP_` + a server-only secret name (`SERVICE_ROLE_KEY`, `OPENAI_API_KEY`, `STRIPE_SECRET_KEY`, `DATABASE_URL`…) | [CWE-200](https://cwe.mitre.org/data/definitions/200.html) | critical |
142
+ | `VSB-CLIENT-002` | server-only `process.env.*SECRET*` read inside a `"use client"` module | CWE-200 | high |
143
+ | `VSB-CLIENT-003` | `createClient(url, SERVICE_ROLE_KEY)` — RLS bypass from a client-reachable file | [CWE-250](https://cwe.mitre.org/data/definitions/250.html) | critical |
144
+ | `VSB-CLIENT-004` | `supabaseAdmin` / `adminClient` / `serviceRoleClient` referenced from browser code | CWE-250 | critical |
145
+ | `VSB-CLIENT-005` | `dangerouslyAllowBrowser: true`, provider SDK keyed in the browser | [CWE-749](https://cwe.mitre.org/data/definitions/749.html) | high |
146
+ | `VSB-CLIENT-006` | `.env*` assigns a live-looking credential to a public variable | [CWE-540](https://cwe.mitre.org/data/definitions/540.html) | critical |
147
+ | `VSB-CLIENT-007` | `next.config.js` `env` / `publicRuntimeConfig` / Vite `define` inlining a secret | [CWE-798](https://cwe.mitre.org/data/definitions/798.html) | critical |
148
+
149
+ ### Unguarded mutation routes — `VSB-AUTH-*`
150
+
151
+ | ID | Detects | CWE | Sev |
152
+ | --- | --- | --- | --- |
153
+ | `VSB-AUTH-001` | App Router `POST/PUT/PATCH/DELETE` handler with no session check | [CWE-306](https://cwe.mitre.org/data/definitions/306.html) | critical |
154
+ | `VSB-AUTH-002` | Pages API mutation handler with no session check | CWE-306 | critical |
155
+ | `VSB-AUTH-003` | Express / Fastify / Koa / Hono mutation route with no auth middleware | [CWE-862](https://cwe.mitre.org/data/definitions/862.html) | high |
156
+ | `VSB-AUTH-004` | IDOR — ownership key from `body`/`query`/`params` (including the destructured local variable) | [CWE-639](https://cwe.mitre.org/data/definitions/639.html) | critical |
157
+ | `VSB-AUTH-005` | `"use server"` action mutating data with no auth | CWE-306 | high |
158
+ | `VSB-AUTH-006` | identity from `jwt.decode`, `x-user-id`, `atob(token.split('.')[1])`, `verify:false`, `algorithms:['none']` | [CWE-347](https://cwe.mitre.org/data/definitions/347.html) | critical |
159
+ | `VSB-AUTH-007` | admin/destructive surface that authenticates but never checks a role | [CWE-863](https://cwe.mitre.org/data/definitions/863.html) | high |
160
+ | `VSB-AUTH-008` | `const session = await auth()` captured and never enforced | [CWE-287](https://cwe.mitre.org/data/definitions/287.html) | medium |
161
+ | `VSB-AUTH-009` | cookie-session mutation with no CSRF token protection | [CWE-352](https://cwe.mitre.org/data/definitions/352.html) | medium |
162
+
163
+ ### Database exposure — `VSB-DB-*`
164
+
165
+ | ID | Detects | CWE | Sev |
166
+ | --- | --- | --- | --- |
167
+ | `VSB-DB-001` | `CREATE TABLE` with no `ENABLE ROW LEVEL SECURITY` **anywhere in the project** (cross-file index) | [CWE-284](https://cwe.mitre.org/data/definitions/284.html) | critical\*/high |
168
+ | `VSB-DB-002` | permissive policy — `USING (true)`, `WITH CHECK (true)`, `USING (1=1)` | [CWE-732](https://cwe.mitre.org/data/definitions/732.html) | critical |
169
+ | `VSB-DB-003` | `anon`/`authenticated` policy whose `USING` never references `auth.uid()`/`auth.jwt()` | CWE-639 | high |
170
+ | `VSB-DB-004` | `GRANT … TO anon/public` (writes), incl. `GRANT ALL` | CWE-732 | critical/high |
171
+ | `VSB-DB-005` | `DISABLE ROW LEVEL SECURITY`, `DROP POLICY`, `BYPASSRLS` | CWE-284 | critical/high |
172
+ | `VSB-DB-006` | `$queryRawUnsafe`, `$executeRawUnsafe`, `knex.raw`, `sequelize.query`, `.raw()` with a dynamic string | [CWE-89](https://cwe.mitre.org/data/definitions/89.html) | critical |
173
+ | `VSB-DB-007` | hand-built SQL from request data — `${}`, `+`, Python f-strings, PHP interpolation | CWE-89 | critical |
174
+ | `VSB-DB-008` | Supabase service-role key outside a `server-only` boundary | CWE-250 | critical |
175
+ | `VSB-DB-009` | `createBucket(…, { public: true })` | CWE-732 | medium |
176
+ | `VSB-DB-010` | request data used as a table, column or `ORDER BY` identifier | CWE-89 | high |
177
+
178
+ \* critical when Supabase/PostgREST is detected — the anon key in every browser makes the table publicly addressable.
179
+
180
+ ### AI / LLM & classic web — `VSB-AI-*`
181
+
182
+ | ID | Detects | CWE | Sev |
183
+ | --- | --- | --- | --- |
184
+ | `VSB-AI-001` | `exec`/`execSync`/`spawn({shell:true})`/`Bun.spawn`/`os.system`/`subprocess(shell=True)`/`Runtime.exec`/PHP backticks | [CWE-78](https://cwe.mitre.org/data/definitions/78.html) | critical |
185
+ | `VSB-AI-002` | LLM/MCP tool `execute` reaching a shell, `eval` or filesystem sink | [CWE-94](https://cwe.mitre.org/data/definitions/94.html) | critical |
186
+ | `VSB-AI-003` | model output into `eval`/`new Function`/`vm.runInNewContext` | CWE-94 | critical |
187
+ | `VSB-AI-004` | untrusted/RAG content interpolated into a `system` prompt, no delimiters (OWASP LLM01) | [CWE-74](https://cwe.mitre.org/data/definitions/74.html) | high |
188
+ | `VSB-AI-005` | Stripe webhook without `stripe.webhooks.constructEvent` | [CWE-345](https://cwe.mitre.org/data/definitions/345.html) | critical |
189
+ | `VSB-AI-006` | webhook signing secret read from the request | CWE-345 | high |
190
+ | `VSB-AI-007` | GitHub/Slack/Shopify/Svix webhook with no signature verification | CWE-345 | high |
191
+ | `VSB-AI-008` | `dangerouslySetInnerHTML`, `v-html`, `{@html}`, `innerHTML`, `document.write`, jQuery `.html()` | [CWE-79](https://cwe.mitre.org/data/definitions/79.html) | high |
192
+ | `VSB-AI-009` | client `eval`/`new Function`/string `setTimeout`/`javascript:` URLs | CWE-94 | high |
193
+ | `VSB-AI-010` | open redirect — `redirect(searchParams.get('next'))` | [CWE-601](https://cwe.mitre.org/data/definitions/601.html) | medium |
194
+ | `VSB-AI-011` | SSRF — `fetch(req.body.url)` | [CWE-918](https://cwe.mitre.org/data/definitions/918.html) | high |
195
+ | `VSB-AI-012` | path traversal — `path.join(dir, req.params.name)` | [CWE-22](https://cwe.mitre.org/data/definitions/22.html) | high |
196
+ | `VSB-AI-013` | multer/`formData()` upload with no size or type limit | [CWE-434](https://cwe.mitre.org/data/definitions/434.html) | medium |
197
+ | `VSB-AI-014` | public LLM endpoint with no rate limit or quota | [CWE-770](https://cwe.mitre.org/data/definitions/770.html) | medium |
198
+ | `VSB-AI-015` | `pickle.loads`, `yaml.load` without `SafeLoader`, PHP `unserialize` | [CWE-502](https://cwe.mitre.org/data/definitions/502.html) | high |
199
+
200
+ ### Hardcoded credentials — `VSB-SECRET-*`
201
+
202
+ | ID | Detects | CWE | Sev |
203
+ | --- | --- | --- | --- |
204
+ | `VSB-SECRET-001` | vendor signature in a server file (Stripe, OpenAI, Anthropic, AWS, GitHub, Supabase, Slack, Twilio, SendGrid, npm, PyPI, HuggingFace, Docker, Vercel, Mailgun, Square, Shopify, Datadog) | CWE-798 | high |
205
+ | `VSB-SECRET-002` | generic high-entropy assignment to a credential-shaped name | CWE-798 | medium/high |
206
+ | `VSB-SECRET-003` | PEM private key material | [CWE-321](https://cwe.mitre.org/data/definitions/321.html) | critical |
207
+ | `VSB-SECRET-004` | hardcoded JWT whose payload carries `service_role` / `admin` | CWE-250 | critical |
208
+ | `VSB-SECRET-005` | the same vendor credential in a **browser-reachable** file | CWE-200 | critical |
209
+ | `VSB-SECRET-006` | `postgres:postgres@…`, `password = "admin"`, entropy < 2.5 | [CWE-1392](https://cwe.mitre.org/data/definitions/1392.html) | medium |
210
+ | `VSB-SECRET-007` | `.env` with live credentials — critical when git-tracked | CWE-540 | critical |
211
+
212
+ ### Agent skill & MCP poisoning — `VSB-AGENT-*` (`audit-skills`)
213
+
214
+ | ID | Detects | CWE | Sev |
215
+ | --- | --- | --- | --- |
216
+ | `VSB-AGENT-001` | zero-width, bidi-override, Unicode-tag characters hiding instructions | CWE-74 | critical |
217
+ | `VSB-AGENT-002` | `curl`/`wget`/`nc` exfiltrating env vars or `.env` | CWE-200 | critical |
218
+ | `VSB-AGENT-003` | instructions to read `.env`, `~/.ssh`, `~/.aws/credentials`, `id_rsa`, keychain | [CWE-522](https://cwe.mitre.org/data/definitions/522.html) | critical |
219
+ | `VSB-AGENT-004` | registry-tag `npx -y`/`uvx`, remote MCP URL, `curl \| sh` install (git installs such as `github:owner/repo` are not flagged: they resolve to a commit, not a mutable tag) | [CWE-829](https://cwe.mitre.org/data/definitions/829.html) | high |
220
+ | `VSB-AGENT-005` | literal credentials in an `env` block, or `"*": "*"` env forwarding | [CWE-526](https://cwe.mitre.org/data/definitions/526.html) | critical/high |
221
+ | `VSB-AGENT-006` | "ignore previous instructions", "do not tell the user", "auto-approve", "skip confirmation" | CWE-74 | critical |
222
+ | `VSB-AGENT-007` | `Bash(*)`, `Write(*)`, `autoApprove: ["*"]`, `dangerouslySkipPermissions` | CWE-284 | high |
223
+ | `VSB-AGENT-008` | base64 payload / `eval(atob(…))` / `base64 -d \| sh` | CWE-94 | high |
224
+ | `VSB-AGENT-009` | "fetch <url> and follow every instruction" | CWE-829 | critical |
225
+ | `VSB-AGENT-010` | missing frontmatter, wildcard `allowed-tools` | CWE-250 | medium |
226
+ | `VSB-AGENT-011` | literal credential inside an instruction/MCP file | CWE-798 | critical |
227
+ | `VSB-AGENT-012` | `eval "$1"`, `sh -c "$VAR"` in a skill script | CWE-78 | medium |
228
+
229
+ ---
230
+
231
+ ## 🔌 Native MCP server
232
+
233
+ vibe-shield is itself a [Model Context Protocol](https://modelcontextprotocol.io) server, so
234
+ Claude Code, Claude Desktop, Cursor and Codex can call the scanner as a tool
235
+ instead of shelling out and parsing console output.
236
+
237
+ ```bash
238
+ npx -y @vibefoundry/vibe-shield mcp # JSON-RPC 2.0 over stdio
239
+ npx -y @vibefoundry/vibe-shield --mcp # same thing, if your client prefers a flag
240
+ ```
241
+
242
+ Three tools:
243
+
244
+ | Tool | What it does |
245
+ | --- | --- |
246
+ | `vibe_shield_scan` | Runs the 60 rules over a project or specific paths → grade, score, findings with file:line, CWE and remediation |
247
+ | `vibe_shield_audit_skills` | Audits `.claude/skills`, `.cursor` rules, `AGENTS.md` and MCP configs for prompt injection and exfiltration traps |
248
+ | `vibe_shield_explain_rule` | Full CWE detail, vulnerable/fixed examples and remediation for any rule id or slug |
249
+
250
+ `npx -y @vibefoundry/vibe-shield init` writes the config for you, merging into `.mcp.json`
251
+ **without clobbering** servers you already have:
252
+
253
+ ```jsonc
254
+ {
255
+ "mcpServers": {
256
+ "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"] },
257
+ "vibe-shield": { "command": "npx", "args": ["--yes", "@vibefoundry/vibe-shield@1.1.0", "mcp"] }
258
+ }
259
+ }
260
+ ```
261
+
262
+ The version pin is not cosmetic: an unpinned `npx -y` server is exactly what
263
+ `VSB-AGENT-004` flags as high severity, so vibe-shield's own audit would write
264
+ itself a finding. `init` pins the running version automatically.
265
+
266
+ Or add it by hand:
267
+
268
+ ```bash
269
+ claude mcp add vibe-shield -- npx -y @vibefoundry/vibe-shield mcp
270
+ ```
271
+
272
+ The server is zero-dependency, writes **only** JSON-RPC to stdout, honours
273
+ JSON-RPC notifications (no spurious replies), and returns tool errors as
274
+ `isError` results so a bad argument degrades one call instead of killing the
275
+ session. Requests are handled in arrival order.
276
+
277
+ An empty target is reported as `No files were scanned` rather than a green pass —
278
+ a scanner that says "clean" when it read nothing is worse than no scanner.
279
+
280
+ ---
281
+
282
+ ## 🤖 Use it as an Agent Skill
283
+
284
+ `npx -y @vibefoundry/vibe-shield init` installs the skill. Once it is in a repository, every
285
+ agent session starts with a contract:
286
+
287
+ > You are not allowed to say "done" until the code you just wrote passes a security audit.
288
+
289
+ ```bash
290
+ npx -y @vibefoundry/vibe-shield init # Claude Code + Cursor + AGENTS.md + CI
291
+ npx -y @vibefoundry/vibe-shield init --agent claude # just the skill
292
+ npx -y @vibefoundry/vibe-shield init --no-workflow # skip the GitHub Action
293
+ ```
294
+
295
+ **Why this works better than a prompt snippet:** it names the three commands, gives the agent a reading guide for a finding (`line:col`, rule id, message, CWE), states five hard rules (never suppress, never weaken, always rotate, report honestly, re-verify), and defines "done" as an exit code rather than an opinion.
296
+
297
+ The skill itself is [SKILL.md](./SKILL.md) — read it, it is short.
298
+
299
+ ### Anthropic Agent Skills quickstart
300
+
301
+ ```bash
302
+ # marketplace-style install
303
+ npx -y @vibefoundry/vibe-shield init --agent claude
304
+ # → .claude/skills/vibe-shield/SKILL.md
305
+
306
+ # or globally, for every project
307
+ curl -sL https://raw.githubusercontent.com/avivgerz-hue/vibe-shield/main/SKILL.md \
308
+ -o ~/.claude/skills/vibe-shield/SKILL.md
309
+ ```
310
+
311
+ ---
312
+
313
+ ## 🔁 CI/CD: fail the PR before the bug ships
314
+
315
+ ### GitHub Actions (recommended)
316
+
317
+ `npx -y @vibefoundry/vibe-shield init` installs a workflow that runs the scan, uploads SARIF to
318
+ code scanning, audits the agent surface, and fails the job:
319
+
320
+ ```yaml
321
+ name: vibe-shield
322
+ on: [push, pull_request]
323
+
324
+ permissions:
325
+ contents: read
326
+ security-events: write
327
+
328
+ jobs:
329
+ scan:
330
+ runs-on: ubuntu-latest
331
+ steps:
332
+ - uses: actions/checkout@v4
333
+ - uses: actions/setup-node@v4
334
+ with:
335
+ node-version: 20
336
+
337
+ - name: Scan and upload SARIF
338
+ run: npx -y @vibefoundry/vibe-shield scan --format sarif --out vibe-shield.sarif --fail-on high
339
+ continue-on-error: true
340
+
341
+ - uses: github/codeql-action/upload-sarif@v3
342
+ if: always()
343
+ with:
344
+ sarif_file: vibe-shield.sarif
345
+ category: vibe-shield
346
+
347
+ - name: Enforce the gate
348
+ run: npx -y @vibefoundry/vibe-shield scan --fail-on high
349
+ ```
350
+
351
+ Findings appear **inline on the pull request diff** (GitHub annotations) and in
352
+ the **Security → Code scanning** tab (SARIF), so a reviewer sees the vulnerability
353
+ on the line that introduced it.
354
+
355
+ ### Any other CI
356
+
357
+ ```bash
358
+ npx -y @vibefoundry/vibe-shield scan --fail-on high # exit 1 on findings
359
+ npx -y @vibefoundry/vibe-shield scan --format compact # one line per finding
360
+ npx -y @vibefoundry/vibe-shield scan --format json > vibe-shield.json # full report
361
+ npx -y @vibefoundry/vibe-shield scan --format summary # JSON, counts + grade only
362
+ npx -y @vibefoundry/vibe-shield scan --format github # workflow annotations
363
+ ```
364
+
365
+ Exit codes: **0** clean · **1** findings at/above `--fail-on` · **2** usage error
366
+ (a target path that does not exist, or a target that matches no scannable files).
367
+ An empty scan prints `NO FILES SCANNED — this is not a pass` and is never
368
+ reported as a grade — a gate pointed at an ignored path fails instead of
369
+ succeeding forever.
370
+
371
+ ---
372
+
373
+ ## 🎯 The scorecard
374
+
375
+ Grades are **deliberately unforgiving**, because "B+" for a leaked production key is how security tools lose credibility:
376
+
377
+ | Finding present | Maximum score | Best possible grade |
378
+ | --- | --- | --- |
379
+ | any **critical** | 29 | **F** |
380
+ | any **high** | 58 | C |
381
+ | any **medium** | 76 | B |
382
+ | any **low** | 88 | A- |
383
+ | nothing | 100 | **A+** |
384
+
385
+ Penalties per finding: critical −30, high −12, medium −5, low −2, info −0.5. One leaked live key fails the build on its own; a handful of informational notes never will.
386
+
387
+ ---
388
+
389
+ ## 🪄 It scans itself
390
+
391
+ This repository runs its own gate on every commit and in CI:
392
+
393
+ ```console
394
+ $ npx -y @vibefoundry/vibe-shield scan
395
+ 🛡️ vibe-shield v1.0.0 · security scan
396
+ target /you/vibe-shield
397
+
398
+ 🏆 GRADE A+ — 100/100
399
+
400
+ 21 files · 172 KiB · 236 files/s · 89 ms · 48 rules
401
+
402
+ Nothing to fix. Keep the shield up. 🛡️
403
+
404
+ $ npx -y @vibefoundry/vibe-shield audit-skills
405
+ 🏆 GRADE A+ — 100/100
406
+
407
+ 1 file · 6 KiB · 14 files/s · 69 ms · 12 rules
408
+
409
+ Nothing to fix. Keep the shield up. 🛡️
410
+ ```
411
+
412
+ The detector catalogue is the one place that must contain the exact vulnerable
413
+ shapes and vendor key formats the rules match on — exactly like gitleaks and
414
+ semgrep. Those files are listed explicitly in [`.vibe-shieldignore`](./.vibe-shieldignore)
415
+ with a comment explaining why, instead of weakening a rule.
416
+
417
+ ---
418
+
419
+ ## 🔇 Suppressing a finding (legitimately)
420
+
421
+ Inline, with a rule id and a reason — the reason is required by review culture even though the parser does not enforce it:
422
+
423
+ ```ts
424
+ // vibe-shield-disable-next-line VSB-AI-001 -- path comes from a closed enum
425
+ exec(`convert ${KNOWN_FORMATS[format]} out.png`);
426
+ ```
427
+
428
+ ```ts
429
+ // vibe-shield-disable-line VSB-AUTH-008
430
+ // vibe-shield-disable VSB-AI-014
431
+ // vibe-shield-enable
432
+ // vibe-shield-disable-file VSB-DB-004 -- generated edge schema
433
+ ```
434
+
435
+ Suppressed findings are **counted and reported** in the scorecard, never silently dropped. Whole-path exclusions belong in `.vibe-shieldignore` (gitignore syntax) and should be reserved for generated code.
436
+
437
+ ---
438
+
439
+ ## ⚙️ How it works
440
+
441
+ ```
442
+ discover ──▶ project context ──▶ rule gate ──▶ detect ──▶ suppress ──▶ score ──▶ report
443
+ │ │ │ │
444
+ │ │ │ └─ 60 rules, each a pure (FileContext) => RuleHit[]
445
+ │ │ └─ cheap content hints: most rules skip most files
446
+ │ └─ manifests, framework flags, cross-file RLS index, git index, middleware matchers
447
+ └─ one tree walk, gitignore/.vibe-shieldignore layering, binary + size + test/doc pruning
448
+ ```
449
+
450
+ **Design constraints, in priority order:**
451
+
452
+ 1. **Fast.** One `readdir` walk, one read per file, one comment-masking pass, and a cheap gate before any rule body runs. A 400-file project scans in ~200 ms; a typical Next.js app in under 100 ms. No AST, no parser, no dependency.
453
+ 2. **Honest.** Every finding carries the exact snippet the developer wrote. Nothing is inferred from a filename alone, and every suppression is counted, not hidden.
454
+ 3. **Composable.** `scanProject()` for the CLI, `runRulesOnFile()` for the auditor, `scoreFindings()` for anything that produces findings. The whole API is exported from `vibe-shield`.
455
+
456
+ **Zero runtime dependencies.** No network calls, no telemetry, no language server, no native binaries. `npx -y @vibefoundry/vibe-shield scan` works offline and in an air-gapped CI runner.
457
+
458
+ ---
459
+
460
+ ## 📖 CLI reference
461
+
462
+ ```
463
+ vibe-shield scan [paths...] Scan the project (default: `.`)
464
+ vibe-shield audit-skills [paths...] Audit agent skills, rules, AGENTS.md and MCP config
465
+ vibe-shield init Install the Agent Skill, AGENTS.md, .mcp.json and CI gate
466
+ vibe-shield mcp Serve the MCP tools over stdio
467
+ vibe-shield rules List every detection rule
468
+ vibe-shield explain <RULE_ID> Full documentation for one rule
469
+ vibe-shield version | help
470
+ ```
471
+
472
+ | Flag | Default | Meaning |
473
+ | --- | --- | --- |
474
+ | `--format <fmt>` | `console` | `console` `compact` `summary` `json` `sarif` `github` |
475
+ | `--fail-on <sev>` | `critical` | Exit 1 at/above this severity |
476
+ | `--severity <sev>` | — | Only report at/above this severity |
477
+ | `--fix-prompt` | off | Write `SECURITY_PATCH_PROMPT.md` |
478
+ | `--out <file>` | — | Write the report to a file |
479
+ | `--rules <ids>` | all | Comma-separated ids/slugs, supports `VSB-DB-*` |
480
+ | `--ignore <glob>` | — | Extra ignore pattern (repeatable) |
481
+ | `--include-tests` | off | Include test/fixture files |
482
+ | `--include-docs` | off | Include Markdown and `docs/` |
483
+ | `--cache` | off | Reuse `.vibe-shield-cache.json` |
484
+ | `--max-findings <n>` | 50 | Console list cap (`0` = all) |
485
+ | `--quiet` | off | One-line verdict (`100/100 A+ — 0 critical, …`) |
486
+ | `--no-color` | auto | Disable ANSI (also honours `NO_COLOR`) |
487
+ | `--json` | off | Shorthand for `--format json` |
488
+ | `--no-gitignore` | off | Ignore `.gitignore` while discovering files |
489
+ | `--no-shield-ignore` | off | Ignore `.vibe-shieldignore` (e.g. to scan excluded fixtures) |
490
+
491
+ `init` takes `--target <dir>`, `--agent claude / cursor / generic / all`, `--no-workflow`,
492
+ `--force` and `--yes`.
493
+
494
+ ---
495
+
496
+ ## 🧩 Programmatic API
497
+
498
+ ```ts
499
+ import { scanProject, formatSarif, buildPatchPrompt, auditSkills } from 'vibe-shield';
500
+
501
+ const result = await scanProject({ rootDir: process.cwd(), cache: true });
502
+
503
+ console.log(result.score.score, result.score.grade); // 0 'F'
504
+ console.log(result.findings[0].ruleId); // 'VSB-AUTH-001'
505
+ console.log(result.findings[0].cwe.id); // 'CWE-306'
506
+
507
+ process.stdout.write(formatSarif(result)); // SARIF 2.1.0
508
+ process.stdout.write(buildPatchPrompt(result)); // agent-ready fix list
509
+
510
+ const agentReport = await auditSkills(process.cwd()); // VSB-AGENT-* surface
511
+ ```
512
+
513
+ ---
514
+
515
+ ## ❓ FAQ
516
+
517
+ **Does it replace a real SAST tool?**
518
+ No. It replaces the *absence* of one. vibe-shield is a fast, zero-dependency gate you can put in front of every agent-written commit today. Pair it with a deeper scanner (CodeQL, Semgrep) if you have one; the rules overlap far less than you would expect, because these are agent-specific shapes.
519
+
520
+ **Will it flag my secure code?**
521
+ The secure fixture (`tests/fixtures/secure/`) is a real Next.js + Supabase + Stripe app with auth, RLS, sanitisation, rate limiting and webhook verification — it scores 100/100 and the suite asserts **zero findings** on it. Each rule has guard clauses for the legitimate patterns: `import "server-only"` suppresses the service-role rules, Prisma tagged templates suppress the SQLi rule, `DOMPurify` suppresses the XSS rule, `auth.uid()` policies suppress the RLS rules.
522
+
523
+ **Does it send my code anywhere?**
524
+ No. There is no network code in the runtime at all — the package has zero dependencies and makes no outbound requests.
525
+
526
+ **Why not an AST?**
527
+ Because a 1.5 MB minified bundle, a Vue SFC, a PHP legacy file and a SQL migration all need to be scanned with the same tool, and a parser handles exactly one of those. The rules use structural brace matching plus a comment-masker that preserves offsets, which is enough for the shapes that matter and roughly two orders of magnitude faster.
528
+
529
+ **Why "vibe"?**
530
+ Because the code was written by vibe and reviewed by nobody. This is the reviewer.
531
+
532
+ ---
533
+
534
+ ## 🛠 Development
535
+
536
+ ```bash
537
+ git clone https://github.com/avivgerz-hue/vibe-shield
538
+ cd vibe-shield
539
+ npm ci
540
+ npm run typecheck # tsc --noEmit (strict, noUnusedLocals)
541
+ npm test # 224 Vitest tests
542
+ npm run build # tsc + postbuild (shebang, chmod, CJS marker)
543
+ npm run verify # typecheck + test + build + self-scan + self-audit
544
+ ```
545
+
546
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the rule-authoring guide, and
547
+ [SECURITY.md](./SECURITY.md) for the threat model and disclosure policy.
548
+
549
+ ---
550
+
551
+ ## 📋 Requirements
552
+
553
+ - **Node.js ≥ 18.17** (uses `node:` builtins only — no polyfills, no native modules)
554
+ - Any OS: Linux, macOS, Windows
555
+ - No config file required; `package.json` is optional
556
+
557
+ ---
558
+
559
+ ## 📄 License
560
+
561
+ MIT © [VibeFoundry](./LICENSE) — use it, fork it, ship it.
562
+
563
+ ---
564
+
565
+ ## Part of the VibeFoundry Agent Suite
566
+
567
+ Three tools for the same problem: agents move fast, and something has to keep the output honest.
568
+
569
+ | | Tool | What it does |
570
+ | :---: | --- | --- |
571
+ | 🎨 | **[unslop](https://github.com/avivgerz-hue/unslop)** | Studio-grade UI design systems & Anti-Slop engine — stops agents from generating generic, interchangeable interfaces |
572
+ | 🛡️ | **[vibe-shield](https://github.com/avivgerz-hue/vibe-shield)** | Security scanner & MCP guard — stops agents from shipping leaked keys, unguarded routes and disabled RLS |
573
+ | 👻 | **[ghost-qa](https://github.com/avivgerz-hue/ghost-qa)** | Autonomous headless-browser QA & self-healing verification loop — stops agents from claiming a UI works when it does not |
574
+
575
+ ```bash
576
+ npx -y @vibefoundry/vibe-shield init # security gate
577
+ ```