@vibefoundry/vibe-shield 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +571 -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 +541 -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 +122 -0
  10. package/dist/src/init/scaffold.js +573 -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 +722 -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 +94 -4
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ /**
3
+ * vibe-shield :: SECURITY_PATCH_PROMPT.md
4
+ * ---------------------------------------
5
+ * `--fix-prompt` writes the file an autonomous agent can *execute*. That means
6
+ * every finding must be self-contained: exact location, the vulnerable code,
7
+ * why it is exploitable, the exact replacement, and a verification command.
8
+ *
9
+ * The document is ordered by severity and grouped by file so an agent reads one
10
+ * file's problems, patches them, and moves on — instead of thrashing between
11
+ * unrelated modules.
12
+ */
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.PATCH_PROMPT_FILENAME = void 0;
48
+ exports.buildPatchPrompt = buildPatchPrompt;
49
+ exports.writePatchPrompt = writePatchPrompt;
50
+ const node_fs_1 = require("node:fs");
51
+ const path = __importStar(require("node:path"));
52
+ exports.PATCH_PROMPT_FILENAME = 'SECURITY_PATCH_PROMPT.md';
53
+ const SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
54
+ const SEVERITY_HEADING = {
55
+ critical: 'Critical — exploitable now, fix first',
56
+ high: 'High — exploitable with modest effort',
57
+ medium: 'Medium — needs specific conditions, still worth fixing',
58
+ low: 'Low — hardening',
59
+ info: 'Informational',
60
+ };
61
+ const truncate = (value, max) => value.length <= max ? value : `${value.slice(0, Math.max(0, max - 1))}…`;
62
+ /** Fenced code block that cannot be broken by backticks in the code. */
63
+ const fence = (code, language = 'ts') => {
64
+ const longest = Math.max(2, ...(code.match(/`+/g) ?? []).map((run) => run.length));
65
+ const marker = '`'.repeat(longest + 1);
66
+ return [`${marker}${language}`, code.replace(/\r/g, ''), marker];
67
+ };
68
+ /**
69
+ * Build the patch prompt document.
70
+ *
71
+ * The prompt is written *for an agent*: imperative, unambiguous, and explicit
72
+ * about what must not change. It ends with a self-check the agent is told to run
73
+ * before reporting the task as done — the same discipline `SKILL.md` enforces.
74
+ */
75
+ function buildPatchPrompt(result, options = {}) {
76
+ const minSeverity = options.minSeverity ?? 'info';
77
+ const maxFindings = options.maxFindings ?? 200;
78
+ const minRank = SEVERITY_ORDER[minSeverity] ?? 4;
79
+ const selected = result.findings
80
+ .filter((finding) => (SEVERITY_ORDER[finding.severity] ?? 4) <= minRank)
81
+ .sort((a, b) => (SEVERITY_ORDER[a.severity] ?? 4) - (SEVERITY_ORDER[b.severity] ?? 4) ||
82
+ a.filePath.localeCompare(b.filePath) ||
83
+ a.line - b.line)
84
+ .slice(0, maxFindings > 0 ? maxFindings : undefined);
85
+ const counts = result.score.counts;
86
+ const lines = [];
87
+ lines.push('# Security patch prompt');
88
+ lines.push('');
89
+ lines.push('> Generated by [vibe-shield](https://github.com/avivgerz-hue/vibe-shield). Hand this file to your coding agent (Claude Code, Codex, Cursor, Windsurf).');
90
+ lines.push('> Every finding below was detected statically and cites the exact line that must change.');
91
+ lines.push('');
92
+ lines.push('## Mission');
93
+ lines.push('');
94
+ lines.push(`Fix the ${selected.length} security finding${selected.length === 1 ? '' : 's'} listed below in the repository at \`${result.rootDir}\`. ` +
95
+ 'Apply every remediation exactly as written, then re-run the scanner to prove the finding is gone.');
96
+ lines.push('');
97
+ lines.push('## Scanner verdict');
98
+ lines.push('');
99
+ lines.push('| Metric | Value |');
100
+ lines.push('| --- | --- |');
101
+ lines.push(`| Score | **${result.score.score}/100** |`);
102
+ lines.push(`| Grade | **${result.score.grade}** |`);
103
+ lines.push(`| Critical | ${counts.critical} |`);
104
+ lines.push(`| High | ${counts.high} |`);
105
+ lines.push(`| Medium | ${counts.medium} |`);
106
+ lines.push(`| Low | ${counts.low} |`);
107
+ lines.push(`| Files scanned | ${result.stats.filesScanned} |`);
108
+ lines.push(`| Generated | ${result.finishedAt} |`);
109
+ lines.push('');
110
+ lines.push('## Rules of engagement');
111
+ lines.push('');
112
+ lines.push('1. **Rotate before you refactor.** If a finding exposes a credential, tell the user to rotate it at the provider first. Deleting the line does not un-leak it.');
113
+ lines.push('2. **Never weaken a check to silence a finding.** Do not delete an auth guard, disable RLS, or add `// vibe-shield-disable` to make the report green.');
114
+ lines.push('3. **One finding at a time.** Patch it, run the verification command, then move on. Do not batch unrelated files into one edit.');
115
+ lines.push('4. **Preserve behaviour.** The feature must keep working; a fix that breaks the happy path is not a fix.');
116
+ lines.push('5. **Do not invent code.** If a remediation needs a decision you cannot make (which auth provider, which storage bucket), stop and ask the user.');
117
+ lines.push('');
118
+ lines.push('## Verification');
119
+ lines.push('');
120
+ lines.push('After each group of fixes run:');
121
+ lines.push('');
122
+ lines.push(...fence('npx github:avivgerz-hue/vibe-shield scan --fail-on high', 'bash'));
123
+ lines.push('');
124
+ lines.push('The task is complete only when this command exits `0` and every finding below is gone.');
125
+ lines.push('');
126
+ if (!selected.length) {
127
+ lines.push('## Nothing to fix');
128
+ lines.push('');
129
+ lines.push('No findings matched the requested severity threshold. 🛡️');
130
+ lines.push('');
131
+ return `${lines.join('\n')}\n`;
132
+ }
133
+ // Group by severity, then by file: an agent patches file-by-file.
134
+ const bySeverity = new Map();
135
+ for (const finding of selected) {
136
+ const bucket = bySeverity.get(finding.severity) ?? [];
137
+ bucket.push(finding);
138
+ bySeverity.set(finding.severity, bucket);
139
+ }
140
+ let index = 0;
141
+ for (const severity of ['critical', 'high', 'medium', 'low', 'info']) {
142
+ const findings = bySeverity.get(severity);
143
+ if (!findings?.length)
144
+ continue;
145
+ lines.push(`## ${SEVERITY_HEADING[severity] ?? severity} (${findings.length})`);
146
+ lines.push('');
147
+ const byFile = new Map();
148
+ for (const finding of findings) {
149
+ const bucket = byFile.get(finding.filePath) ?? [];
150
+ bucket.push(finding);
151
+ byFile.set(finding.filePath, bucket);
152
+ }
153
+ for (const [filePath, fileFindings] of byFile) {
154
+ lines.push(`### ${filePath}`);
155
+ lines.push('');
156
+ for (const finding of fileFindings) {
157
+ index += 1;
158
+ lines.push(`#### ${index}. ${finding.title} — \`${filePath}:${finding.line}:${finding.column}\``);
159
+ lines.push('');
160
+ lines.push(`- **Rule:** \`${finding.ruleId}\` (${finding.category})`);
161
+ lines.push(`- **Severity:** ${finding.severity} · **Confidence:** ${finding.confidence}`);
162
+ lines.push(`- **CWE:** [${finding.cwe.id} — ${finding.cwe.name}](${finding.cwe.url})`);
163
+ lines.push('');
164
+ lines.push(finding.message);
165
+ lines.push('');
166
+ if (finding.snippet) {
167
+ lines.push('**Vulnerable code**');
168
+ lines.push('');
169
+ lines.push(...fence(truncate(finding.snippet, 400), languageFor(filePath)));
170
+ lines.push('');
171
+ }
172
+ lines.push('**Required change**');
173
+ lines.push('');
174
+ finding.remediation.forEach((step, stepIndex) => {
175
+ lines.push(`${stepIndex + 1}. ${step}`);
176
+ });
177
+ lines.push('');
178
+ lines.push('**Target shape after the fix**');
179
+ lines.push('');
180
+ lines.push(...fence(finding.secureExample, languageFor(filePath)));
181
+ lines.push('');
182
+ if (options.verbose) {
183
+ lines.push('**Anti-pattern to remove**');
184
+ lines.push('');
185
+ lines.push(...fence(finding.insecureExample, languageFor(filePath)));
186
+ lines.push('');
187
+ }
188
+ if (finding.references.length) {
189
+ lines.push(`**Reference:** ${finding.references.slice(0, 3).join(' · ')}`);
190
+ lines.push('');
191
+ }
192
+ }
193
+ }
194
+ }
195
+ lines.push('## Final checklist');
196
+ lines.push('');
197
+ lines.push('- [ ] Every credential that appeared in this report has been rotated by the user.');
198
+ lines.push('- [ ] `npx github:avivgerz-hue/vibe-shield scan --fail-on high` exits `0`.');
199
+ lines.push('- [ ] No `vibe-shield-disable` comments were added.');
200
+ lines.push('- [ ] The application still builds and its tests still pass.');
201
+ lines.push('');
202
+ return `${lines.join('\n')}\n`;
203
+ }
204
+ const languageFor = (filePath) => {
205
+ const extension = (filePath.split('.').pop() ?? '').toLowerCase();
206
+ const map = {
207
+ ts: 'ts',
208
+ tsx: 'tsx',
209
+ js: 'js',
210
+ jsx: 'jsx',
211
+ mjs: 'js',
212
+ cjs: 'js',
213
+ py: 'python',
214
+ rb: 'ruby',
215
+ php: 'php',
216
+ go: 'go',
217
+ rs: 'rust',
218
+ java: 'java',
219
+ sql: 'sql',
220
+ prisma: 'prisma',
221
+ sh: 'bash',
222
+ yml: 'yaml',
223
+ yaml: 'yaml',
224
+ json: 'json',
225
+ md: 'markdown',
226
+ env: 'ini',
227
+ };
228
+ return map[extension] ?? '';
229
+ };
230
+ /** Write the patch prompt and return the absolute path written. */
231
+ async function writePatchPrompt(result, options = {}) {
232
+ const fileName = options.fileName ?? exports.PATCH_PROMPT_FILENAME;
233
+ const target = path.isAbsolute(fileName) ? fileName : path.join(options.outDir ?? result.rootDir, fileName);
234
+ const document = buildPatchPrompt(result, options);
235
+ await node_fs_1.promises.mkdir(path.dirname(target), { recursive: true });
236
+ await node_fs_1.promises.writeFile(target, document, 'utf8');
237
+ return target;
238
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * vibe-shield :: file & project context
3
+ * -------------------------------------
4
+ * Everything a rule needs is computed once per file here: language, structural
5
+ * flags ("is this a client component?", "is this an App Router handler?", "is
6
+ * this a SQL migration?") and the comment-masked code view.
7
+ */
8
+ import { type FileContext, type FileFlags, type Language, type ProjectContext, type SqlTableInfo } from './types';
9
+ export declare const toPosix: (value: string) => string;
10
+ /** Resolve the `Language` bucket for a path. */
11
+ export declare function detectLanguage(relativePath: string, fileName: string, extension: string): Language;
12
+ /** Extension without the dot, lower-cased (`.env.local` -> `local`). */
13
+ export declare const extensionOf: (fileName: string) => string;
14
+ /** Compute the structural flags for one file. */
15
+ export declare function computeFlags(relativePath: string, fileName: string, extension: string, language: Language, content: string): FileFlags;
16
+ /** Build the immutable per-file context handed to every rule. */
17
+ export declare function createFileContext(absolutePath: string, rootDir: string, content: string, project: ProjectContext): FileContext;
18
+ /** Strip JSON comments/trailing commas so `jsonc` manifests still parse. */
19
+ export declare function tolerantJsonParse(text: string): unknown;
20
+ export interface DetectProjectOptions {
21
+ /** Directories (relative) that should not be inspected for manifests. */
22
+ skip?: (relativePath: string, isDirectory: boolean) => boolean;
23
+ /** How deep to look for workspace manifests. Default 8. */
24
+ maxDepth?: number;
25
+ /** Read the git index to learn which paths are actually tracked. Default true. */
26
+ readGitIndex?: boolean;
27
+ /** Maximum number of SQL files parsed for the RLS index. Default 400. */
28
+ maxSqlFiles?: number;
29
+ /** Maximum bytes read per SQL file. Default 512 KiB. */
30
+ maxSqlBytes?: number;
31
+ }
32
+ /** Extract the `matcher` entries declared by a Next.js middleware module. */
33
+ export declare function parseMiddlewareMatchers(source: string): string[];
34
+ /**
35
+ * Parse every `CREATE TABLE` / `ALTER TABLE ... ROW LEVEL SECURITY` /
36
+ * `CREATE POLICY` / `GRANT` statement in a SQL file into table records.
37
+ * Deliberately tolerant: SQL dialects differ, and a partial read is still
38
+ * better than a missed "RLS is enabled three migrations later".
39
+ */
40
+ export declare function parseSqlTables(relativePath: string, source: string): SqlTableInfo[];
41
+ /** Merge per-file table records into the project-wide index. */
42
+ export declare function mergeSqlTables(target: SqlTableInfo[], incoming: SqlTableInfo[]): void;
43
+ /** Paths tracked by git, read from the index in a single subprocess call. */
44
+ export declare function readGitTrackedPaths(rootDir: string): string[];
45
+ /**
46
+ * Cheap project fingerprint: manifests + marker files + SQL RLS posture.
47
+ * Runs before the scan so rules can ask "does this project even have a CSRF
48
+ * library installed?" or "is RLS enabled for this table in a later migration?"
49
+ * without a second pass over the source.
50
+ */
51
+ export declare function detectProject(rootDir: string, options?: DetectProjectOptions): Promise<ProjectContext>;