@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.
- package/LICENSE +21 -0
- package/README.md +571 -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 +541 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.js +108 -0
- package/dist/src/init/scaffold.d.ts +122 -0
- package/dist/src/init/scaffold.js +573 -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 +722 -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 +94 -4
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* vibe-shield :: command line
|
|
5
|
+
* ---------------------------
|
|
6
|
+
* vibe-shield scan [paths] scan a project (default: `.`)
|
|
7
|
+
* vibe-shield audit-skills [paths] audit agent skills / MCP config
|
|
8
|
+
* vibe-shield init install the Agent Skill + CI gate
|
|
9
|
+
* vibe-shield rules list every rule
|
|
10
|
+
* vibe-shield explain <RULE_ID> full documentation for one rule
|
|
11
|
+
* vibe-shield version | help
|
|
12
|
+
*
|
|
13
|
+
* The module exports `run(argv, io)` and only executes when invoked directly,
|
|
14
|
+
* which is what lets the test suite drive the real CLI without a build step.
|
|
15
|
+
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
49
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
50
|
+
exports.HELP_TEXT = exports.EXIT_USAGE = exports.EXIT_FINDINGS = exports.EXIT_OK = exports.defaultIo = void 0;
|
|
51
|
+
exports.parseArgs = parseArgs;
|
|
52
|
+
exports.run = run;
|
|
53
|
+
exports.commandScan = commandScan;
|
|
54
|
+
exports.commandAuditSkills = commandAuditSkills;
|
|
55
|
+
exports.commandInit = commandInit;
|
|
56
|
+
exports.commandRules = commandRules;
|
|
57
|
+
exports.commandExplain = commandExplain;
|
|
58
|
+
exports.commandMcp = commandMcp;
|
|
59
|
+
const node_fs_1 = require("node:fs");
|
|
60
|
+
const path = __importStar(require("node:path"));
|
|
61
|
+
const skill_auditor_1 = require("../src/mcp-guard/skill-auditor");
|
|
62
|
+
const server_1 = require("../src/mcp/server");
|
|
63
|
+
const scaffold_1 = require("../src/init/scaffold");
|
|
64
|
+
const console_reporter_1 = require("../src/reporter/console-reporter");
|
|
65
|
+
const formatters_1 = require("../src/reporter/formatters");
|
|
66
|
+
const patch_prompt_writer_1 = require("../src/reporter/patch-prompt-writer");
|
|
67
|
+
const engine_1 = require("../src/scanner/engine");
|
|
68
|
+
const rules_1 = require("../src/scanner/rules");
|
|
69
|
+
const types_1 = require("../src/scanner/types");
|
|
70
|
+
const defaultIo = () => ({
|
|
71
|
+
stdout: process.stdout,
|
|
72
|
+
stderr: process.stderr,
|
|
73
|
+
cwd: process.cwd(),
|
|
74
|
+
env: process.env,
|
|
75
|
+
});
|
|
76
|
+
exports.defaultIo = defaultIo;
|
|
77
|
+
/** Exit codes: 0 clean, 1 findings at/above `--fail-on`, 2 usage error. */
|
|
78
|
+
exports.EXIT_OK = 0;
|
|
79
|
+
exports.EXIT_FINDINGS = 1;
|
|
80
|
+
exports.EXIT_USAGE = 2;
|
|
81
|
+
const HELP = `vibe-shield — security scanner for AI-generated code
|
|
82
|
+
|
|
83
|
+
USAGE
|
|
84
|
+
vibe-shield <command> [options]
|
|
85
|
+
|
|
86
|
+
COMMANDS
|
|
87
|
+
scan [paths...] Scan the project (default: current directory).
|
|
88
|
+
audit-skills [paths...] Audit agent skills, Cursor rules, AGENTS.md and MCP configs.
|
|
89
|
+
init Install the Agent Skill, AGENTS.md section and CI workflow.
|
|
90
|
+
rules List every detection rule.
|
|
91
|
+
explain <RULE_ID> Show full documentation for a rule.
|
|
92
|
+
mcp Serve the Model Context Protocol tools over stdio.
|
|
93
|
+
version Print the version.
|
|
94
|
+
help Print this help.
|
|
95
|
+
|
|
96
|
+
SCAN OPTIONS
|
|
97
|
+
--format <fmt> console | compact | summary | json | sarif | github (console)
|
|
98
|
+
--out <file> Write the report to a file instead of stdout.
|
|
99
|
+
--fix-prompt Write ${patch_prompt_writer_1.PATCH_PROMPT_FILENAME} with exact fix instructions.
|
|
100
|
+
--fail-on <severity> Exit 1 when a finding at/above this severity exists. (critical)
|
|
101
|
+
--severity <severity> Only report findings at/above this severity.
|
|
102
|
+
--rules <ids> Comma-separated rule ids/slugs (supports VSB-DB-*).
|
|
103
|
+
--ignore <glob> Extra gitignore-style pattern (repeatable).
|
|
104
|
+
--include-tests Include test, spec and fixture files.
|
|
105
|
+
--include-docs Include Markdown and docs/.
|
|
106
|
+
--cache Reuse .vibe-shield-cache.json between runs.
|
|
107
|
+
--max-findings <n> Limit the console list (0 = all). (50)
|
|
108
|
+
--quiet Print only the verdict line and the exit code.
|
|
109
|
+
--no-color Disable ANSI colour.
|
|
110
|
+
--no-shield-ignore Ignore .vibe-shieldignore (e.g. to scan excluded fixtures).
|
|
111
|
+
--json Shorthand for --format json.
|
|
112
|
+
|
|
113
|
+
INIT OPTIONS
|
|
114
|
+
--target <dir> Repository to install into. (cwd)
|
|
115
|
+
--agent <kind> claude | cursor | generic | all. (all)
|
|
116
|
+
--no-workflow Skip the GitHub Actions workflow.
|
|
117
|
+
--force Overwrite existing files.
|
|
118
|
+
--yes Accepted for non-interactive CI use (init never prompts).
|
|
119
|
+
|
|
120
|
+
EXAMPLES
|
|
121
|
+
npx github:avivgerz-hue/vibe-shield scan
|
|
122
|
+
npx github:avivgerz-hue/vibe-shield scan --fail-on high --format github
|
|
123
|
+
npx github:avivgerz-hue/vibe-shield scan --fix-prompt
|
|
124
|
+
npx github:avivgerz-hue/vibe-shield init
|
|
125
|
+
npx github:avivgerz-hue/vibe-shield explain VSB-DB-001
|
|
126
|
+
npx github:avivgerz-hue/vibe-shield audit-skills .claude
|
|
127
|
+
`;
|
|
128
|
+
exports.HELP_TEXT = HELP;
|
|
129
|
+
const BOOLEAN_FLAGS = new Set([
|
|
130
|
+
'fix-prompt',
|
|
131
|
+
'include-tests',
|
|
132
|
+
'include-docs',
|
|
133
|
+
'cache',
|
|
134
|
+
'quiet',
|
|
135
|
+
'no-shield-ignore',
|
|
136
|
+
'no-color',
|
|
137
|
+
'json',
|
|
138
|
+
'no-workflow',
|
|
139
|
+
'force',
|
|
140
|
+
'yes',
|
|
141
|
+
'list-rules',
|
|
142
|
+
'help',
|
|
143
|
+
'verbose',
|
|
144
|
+
'strict-gitignore',
|
|
145
|
+
'no-gitignore',
|
|
146
|
+
]);
|
|
147
|
+
const VALUE_FLAGS = new Set([
|
|
148
|
+
'format',
|
|
149
|
+
'out',
|
|
150
|
+
'fail-on',
|
|
151
|
+
'severity',
|
|
152
|
+
'rules',
|
|
153
|
+
'ignore',
|
|
154
|
+
'max-findings',
|
|
155
|
+
'target',
|
|
156
|
+
'agent',
|
|
157
|
+
'category',
|
|
158
|
+
]);
|
|
159
|
+
const SHORT_FLAGS = {
|
|
160
|
+
'-h': 'help',
|
|
161
|
+
'-v': 'version',
|
|
162
|
+
'-f': 'format',
|
|
163
|
+
'-o': 'out',
|
|
164
|
+
'-q': 'quiet',
|
|
165
|
+
};
|
|
166
|
+
/** Parse `argv` without any dependency. Supports `--flag=value` and repeats. */
|
|
167
|
+
function parseArgs(argv) {
|
|
168
|
+
const positionals = [];
|
|
169
|
+
const flags = new Map();
|
|
170
|
+
const unknown = [];
|
|
171
|
+
let command = '';
|
|
172
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
173
|
+
const raw = argv[i] ?? '';
|
|
174
|
+
if (raw.startsWith('-')) {
|
|
175
|
+
const eq = raw.indexOf('=');
|
|
176
|
+
const name = (eq === -1 ? raw : raw.slice(0, eq)).replace(/^--?/, '');
|
|
177
|
+
const normalised = SHORT_FLAGS[raw] ?? name;
|
|
178
|
+
const inlineValue = eq === -1 ? undefined : raw.slice(eq + 1);
|
|
179
|
+
if (BOOLEAN_FLAGS.has(normalised)) {
|
|
180
|
+
flags.set(normalised, inlineValue === undefined ? true : inlineValue !== 'false');
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (VALUE_FLAGS.has(normalised)) {
|
|
184
|
+
const value = inlineValue ?? argv[i + 1];
|
|
185
|
+
if (inlineValue === undefined && value !== undefined)
|
|
186
|
+
i += 1;
|
|
187
|
+
if (value === undefined) {
|
|
188
|
+
unknown.push(raw);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
// Repeatable flag: keep every value.
|
|
192
|
+
const existing = flags.get(normalised);
|
|
193
|
+
if (normalised === 'ignore' && typeof existing === 'string') {
|
|
194
|
+
flags.set(normalised, `${existing}\n${value}`);
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
flags.set(normalised, value);
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
unknown.push(raw);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (!command && !raw.includes(path.sep) && /^[a-z][a-z-]*$/i.test(raw) && !raw.endsWith('.ts')) {
|
|
205
|
+
command = raw;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
positionals.push(raw);
|
|
209
|
+
}
|
|
210
|
+
return { command, positionals, flags, unknown };
|
|
211
|
+
}
|
|
212
|
+
const flagString = (parsed, name) => {
|
|
213
|
+
const value = parsed.flags.get(name);
|
|
214
|
+
return typeof value === 'string' ? value : undefined;
|
|
215
|
+
};
|
|
216
|
+
const flagBool = (parsed, name) => parsed.flags.get(name) === true;
|
|
217
|
+
const flagNumber = (parsed, name, fallback) => {
|
|
218
|
+
const raw = flagString(parsed, name);
|
|
219
|
+
if (raw === undefined)
|
|
220
|
+
return fallback;
|
|
221
|
+
const value = Number.parseInt(raw, 10);
|
|
222
|
+
return Number.isFinite(value) ? value : fallback;
|
|
223
|
+
};
|
|
224
|
+
/* -------------------------------------------------------------------------- */
|
|
225
|
+
/* Output helpers */
|
|
226
|
+
/* -------------------------------------------------------------------------- */
|
|
227
|
+
const write = (io, stream, text) => {
|
|
228
|
+
io[stream].write(text);
|
|
229
|
+
};
|
|
230
|
+
async function emitReport(result, io, options, parsed) {
|
|
231
|
+
const noColor = flagBool(parsed, 'no-color') || io.env.NO_COLOR !== undefined;
|
|
232
|
+
const color = (0, console_reporter_1.createColorizer)(io.stdout);
|
|
233
|
+
if (noColor)
|
|
234
|
+
color.enabled = false;
|
|
235
|
+
let patchPath = null;
|
|
236
|
+
if (options.fixPrompt) {
|
|
237
|
+
patchPath = await (0, patch_prompt_writer_1.writePatchPrompt)(result, {
|
|
238
|
+
outDir: io.cwd,
|
|
239
|
+
minSeverity: ((0, types_1.isSeverity)(flagString(parsed, 'severity')) ? flagString(parsed, 'severity') : 'info'),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
if (options.quiet) {
|
|
243
|
+
// One line: verdict + counts. Machine-friendly and CI-log friendly.
|
|
244
|
+
const counts = result.score.counts;
|
|
245
|
+
const files = `${result.stats.filesScanned} ${result.stats.filesScanned === 1 ? 'file' : 'files'}`;
|
|
246
|
+
const verdict = result.stats.filesScanned === 0 ? '0 files scanned — not a pass' : `${result.score.score}/100 ${result.score.grade}`;
|
|
247
|
+
write(io, 'stdout', `${verdict} — ${counts.critical} critical, ${counts.high} high, ${counts.medium} medium, ${counts.low} low (${files})\n`);
|
|
248
|
+
}
|
|
249
|
+
else if (options.format === 'console') {
|
|
250
|
+
(0, console_reporter_1.reportConsole)(result, io.stdout, {
|
|
251
|
+
color,
|
|
252
|
+
maxFindings: options.maxFindings,
|
|
253
|
+
width: Math.min(io.stdout.columns ?? 100, 140),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
write(io, 'stdout', (0, formatters_1.formatResult)(result, options.format));
|
|
258
|
+
}
|
|
259
|
+
if (patchPath) {
|
|
260
|
+
write(io, 'stderr', `\n📝 Wrote ${path.relative(io.cwd, patchPath) || patchPath} — hand it to your agent, or run \`npx github:avivgerz-hue/vibe-shield scan\` after fixing.\n`);
|
|
261
|
+
}
|
|
262
|
+
if (options.out) {
|
|
263
|
+
const target = path.resolve(io.cwd, options.out);
|
|
264
|
+
const { promises: fs } = await Promise.resolve().then(() => __importStar(require('node:fs')));
|
|
265
|
+
await fs.writeFile(target, (0, formatters_1.formatResult)(result, options.format === 'console' ? 'json' : options.format), 'utf8');
|
|
266
|
+
write(io, 'stderr', `📄 Report written to ${options.out}\n`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/* -------------------------------------------------------------------------- */
|
|
270
|
+
/* Commands */
|
|
271
|
+
/* -------------------------------------------------------------------------- */
|
|
272
|
+
async function commandScan(parsed, io) {
|
|
273
|
+
const format = (flagString(parsed, 'format') ?? (flagBool(parsed, 'json') ? 'json' : 'console')).toLowerCase();
|
|
274
|
+
if (!(0, formatters_1.isOutputFormat)(format)) {
|
|
275
|
+
write(io, 'stderr', `vibe-shield: unknown --format "${format}". Choose one of: ${formatters_1.OUTPUT_FORMATS.join(', ')}\n`);
|
|
276
|
+
return exports.EXIT_USAGE;
|
|
277
|
+
}
|
|
278
|
+
const failOn = (flagString(parsed, 'fail-on') ?? 'critical').toLowerCase();
|
|
279
|
+
if (!(0, types_1.isSeverity)(failOn)) {
|
|
280
|
+
write(io, 'stderr', `vibe-shield: --fail-on expects one of: critical, high, medium, low, info\n`);
|
|
281
|
+
return exports.EXIT_USAGE;
|
|
282
|
+
}
|
|
283
|
+
const severity = flagString(parsed, 'severity');
|
|
284
|
+
if (severity && !(0, types_1.isSeverity)(severity)) {
|
|
285
|
+
write(io, 'stderr', `vibe-shield: --severity expects one of: critical, high, medium, low, info\n`);
|
|
286
|
+
return exports.EXIT_USAGE;
|
|
287
|
+
}
|
|
288
|
+
const ruleSelectors = (flagString(parsed, 'rules') ?? '').split(',').map((entry) => entry.trim()).filter(Boolean);
|
|
289
|
+
const rules = ruleSelectors.length ? (0, rules_1.selectRules)(ruleSelectors) : undefined;
|
|
290
|
+
if (ruleSelectors.length && (!rules || rules.length === 0)) {
|
|
291
|
+
write(io, 'stderr', `vibe-shield: no rules matched --rules "${ruleSelectors.join(',')}"\n`);
|
|
292
|
+
return exports.EXIT_USAGE;
|
|
293
|
+
}
|
|
294
|
+
// A typo in the path must not look like a clean pass.
|
|
295
|
+
const missing = parsed.positionals.filter((target) => !(0, node_fs_1.existsSync)(path.resolve(io.cwd, target)));
|
|
296
|
+
if (missing.length) {
|
|
297
|
+
write(io, 'stderr', `vibe-shield: no such file or directory: ${missing.join(', ')}\n`);
|
|
298
|
+
return exports.EXIT_USAGE;
|
|
299
|
+
}
|
|
300
|
+
const result = await (0, engine_1.scanProject)({
|
|
301
|
+
rootDir: io.cwd,
|
|
302
|
+
paths: parsed.positionals.length ? parsed.positionals : undefined,
|
|
303
|
+
rules,
|
|
304
|
+
ignorePatterns: (flagString(parsed, 'ignore') ?? '').split('\n').filter(Boolean),
|
|
305
|
+
// `undefined` means "use the engine default" (which is: include everything
|
|
306
|
+
// the user named explicitly, exclude tests/docs on an implicit whole-tree walk).
|
|
307
|
+
includeTests: flagBool(parsed, 'include-tests') ? true : undefined,
|
|
308
|
+
includeDocs: flagBool(parsed, 'include-docs') ? true : undefined,
|
|
309
|
+
cache: flagBool(parsed, 'cache'),
|
|
310
|
+
useGitignore: !flagBool(parsed, 'no-gitignore'),
|
|
311
|
+
useShieldIgnore: flagBool(parsed, 'no-shield-ignore') ? false : undefined,
|
|
312
|
+
});
|
|
313
|
+
const filtered = severity && (0, types_1.isSeverity)(severity)
|
|
314
|
+
? {
|
|
315
|
+
...result,
|
|
316
|
+
findings: result.findings.filter((finding) => (0, engine_1.hasFindingsAtOrAbove)({ ...result, findings: [finding] }, severity)),
|
|
317
|
+
}
|
|
318
|
+
: result;
|
|
319
|
+
await emitReport(filtered, io, {
|
|
320
|
+
fixPrompt: flagBool(parsed, 'fix-prompt'),
|
|
321
|
+
format: format,
|
|
322
|
+
out: flagString(parsed, 'out'),
|
|
323
|
+
quiet: flagBool(parsed, 'quiet'),
|
|
324
|
+
maxFindings: flagNumber(parsed, 'max-findings', 50),
|
|
325
|
+
}, parsed);
|
|
326
|
+
// An explicit target that matched nothing is a configuration error, not a
|
|
327
|
+
// pass: a CI gate pointed at a path excluded by an ignore file would
|
|
328
|
+
// otherwise report success forever without scanning a single file.
|
|
329
|
+
if (parsed.positionals.length > 0 && filtered.stats.filesScanned === 0) {
|
|
330
|
+
write(io, 'stderr', `vibe-shield: no scannable files matched ${parsed.positionals.join(', ')} (check ignore files, or pass --no-shield-ignore)\n`);
|
|
331
|
+
return exports.EXIT_USAGE;
|
|
332
|
+
}
|
|
333
|
+
return (0, engine_1.hasFindingsAtOrAbove)(filtered, failOn) ? exports.EXIT_FINDINGS : exports.EXIT_OK;
|
|
334
|
+
}
|
|
335
|
+
async function commandAuditSkills(parsed, io) {
|
|
336
|
+
const format = (flagString(parsed, 'format') ?? (flagBool(parsed, 'json') ? 'json' : 'console')).toLowerCase();
|
|
337
|
+
if (!(0, formatters_1.isOutputFormat)(format)) {
|
|
338
|
+
write(io, 'stderr', `vibe-shield: unknown --format "${format}".\n`);
|
|
339
|
+
return exports.EXIT_USAGE;
|
|
340
|
+
}
|
|
341
|
+
const failOn = (flagString(parsed, 'fail-on') ?? 'high').toLowerCase();
|
|
342
|
+
if (!(0, types_1.isSeverity)(failOn)) {
|
|
343
|
+
write(io, 'stderr', `vibe-shield: --fail-on expects one of: critical, high, medium, low, info\n`);
|
|
344
|
+
return exports.EXIT_USAGE;
|
|
345
|
+
}
|
|
346
|
+
const result = await (0, skill_auditor_1.auditSkills)(io.cwd, {
|
|
347
|
+
paths: parsed.positionals.length ? parsed.positionals : undefined,
|
|
348
|
+
version: await (0, engine_1.packageVersion)(),
|
|
349
|
+
});
|
|
350
|
+
await emitReport(result, io, {
|
|
351
|
+
fixPrompt: flagBool(parsed, 'fix-prompt'),
|
|
352
|
+
format: format,
|
|
353
|
+
out: flagString(parsed, 'out'),
|
|
354
|
+
quiet: flagBool(parsed, 'quiet'),
|
|
355
|
+
maxFindings: flagNumber(parsed, 'max-findings', 50),
|
|
356
|
+
}, parsed);
|
|
357
|
+
return (0, engine_1.hasFindingsAtOrAbove)(result, failOn) ? exports.EXIT_FINDINGS : exports.EXIT_OK;
|
|
358
|
+
}
|
|
359
|
+
async function commandInit(parsed, io) {
|
|
360
|
+
const target = path.resolve(io.cwd, flagString(parsed, 'target') ?? '.');
|
|
361
|
+
const agent = (flagString(parsed, 'agent') ?? 'all').toLowerCase();
|
|
362
|
+
if (!['claude', 'cursor', 'generic', 'all'].includes(agent)) {
|
|
363
|
+
write(io, 'stderr', 'vibe-shield: --agent expects claude, cursor, generic or all\n');
|
|
364
|
+
return exports.EXIT_USAGE;
|
|
365
|
+
}
|
|
366
|
+
const result = await (0, scaffold_1.installScaffold)({
|
|
367
|
+
rootDir: target,
|
|
368
|
+
agents: agent,
|
|
369
|
+
workflow: !flagBool(parsed, 'no-workflow'),
|
|
370
|
+
force: flagBool(parsed, 'force'),
|
|
371
|
+
dryRun: false,
|
|
372
|
+
env: io.env,
|
|
373
|
+
});
|
|
374
|
+
const color = (0, console_reporter_1.createColorizer)(io.stdout);
|
|
375
|
+
if (flagBool(parsed, 'no-color'))
|
|
376
|
+
color.enabled = false;
|
|
377
|
+
write(io, 'stdout', `\n${color.bold(color.cyan('🛡️ vibe-shield init'))} ${color.dim(target)}\n\n`);
|
|
378
|
+
for (const file of result.written)
|
|
379
|
+
write(io, 'stdout', ` ${color.green('created')} ${file}\n`);
|
|
380
|
+
for (const file of result.appended)
|
|
381
|
+
write(io, 'stdout', ` ${color.green('updated')} ${file}\n`);
|
|
382
|
+
for (const file of result.blocked)
|
|
383
|
+
write(io, 'stdout', ` ${color.yellow('blocked')} ${file} ${color.dim('(left untouched — fix the file by hand)')}\n`);
|
|
384
|
+
for (const file of result.skipped)
|
|
385
|
+
write(io, 'stdout', ` ${color.gray('skipped')} ${file} ${color.dim('(exists — use --force)')}\n`);
|
|
386
|
+
for (const warning of result.warnings)
|
|
387
|
+
write(io, 'stdout', ` ${color.yellow('note')} ${warning}\n`);
|
|
388
|
+
write(io, 'stdout', `\n${color.bold('Next steps')}\n`);
|
|
389
|
+
write(io, 'stdout', ` 1. ${color.dim('npx github:avivgerz-hue/vibe-shield scan')} ${color.dim('# baseline your repo')}\n`);
|
|
390
|
+
write(io, 'stdout', ` 2. ${color.dim('npx github:avivgerz-hue/vibe-shield scan --fix-prompt')} ${color.dim('# generate the fix list')}\n`);
|
|
391
|
+
write(io, 'stdout', ` 3. Commit the new files — your agent now audits before it says "done".\n\n`);
|
|
392
|
+
return exports.EXIT_OK;
|
|
393
|
+
}
|
|
394
|
+
function commandRules(parsed, io) {
|
|
395
|
+
const category = flagString(parsed, 'category');
|
|
396
|
+
const rules = category
|
|
397
|
+
? types_1.RULE_CATEGORIES.includes(category)
|
|
398
|
+
? (0, rules_1.rulesByCategory)(category)
|
|
399
|
+
: []
|
|
400
|
+
: [...rules_1.ALL_RULES];
|
|
401
|
+
if (category && !rules.length) {
|
|
402
|
+
write(io, 'stderr', `vibe-shield: unknown --category "${category}". Choose one of: ${types_1.RULE_CATEGORIES.join(', ')}\n`);
|
|
403
|
+
return exports.EXIT_USAGE;
|
|
404
|
+
}
|
|
405
|
+
if (flagBool(parsed, 'json') || flagString(parsed, 'format') === 'json') {
|
|
406
|
+
write(io, 'stdout', `${JSON.stringify(rules.map((rule) => ({
|
|
407
|
+
id: rule.id,
|
|
408
|
+
slug: rule.slug,
|
|
409
|
+
title: rule.title,
|
|
410
|
+
category: rule.category,
|
|
411
|
+
severity: rule.severity,
|
|
412
|
+
confidence: rule.confidence,
|
|
413
|
+
cwe: rule.cwe.id,
|
|
414
|
+
description: rule.description,
|
|
415
|
+
remediation: rule.remediation,
|
|
416
|
+
references: rule.references,
|
|
417
|
+
})), null, 2)}\n`);
|
|
418
|
+
return exports.EXIT_OK;
|
|
419
|
+
}
|
|
420
|
+
const color = (0, console_reporter_1.createColorizer)(io.stdout);
|
|
421
|
+
if (flagBool(parsed, 'no-color'))
|
|
422
|
+
color.enabled = false;
|
|
423
|
+
write(io, 'stdout', `\n${color.bold(`${rules.length} detection rules`)}\n`);
|
|
424
|
+
let currentCategory = '';
|
|
425
|
+
for (const rule of rules) {
|
|
426
|
+
if (rule.category !== currentCategory) {
|
|
427
|
+
currentCategory = rule.category;
|
|
428
|
+
write(io, 'stdout', `\n${color.bold(color.cyan(types_1.CATEGORY_TITLES[rule.category]))}\n`);
|
|
429
|
+
}
|
|
430
|
+
write(io, 'stdout', ` ${color.dim(rule.id.padEnd(14))} ${rule.severity.padEnd(9)} ${rule.title}\n`);
|
|
431
|
+
}
|
|
432
|
+
write(io, 'stdout', `\n${color.dim('Run `npx github:avivgerz-hue/vibe-shield explain <RULE_ID>` for the full documentation.')}\n\n`);
|
|
433
|
+
return exports.EXIT_OK;
|
|
434
|
+
}
|
|
435
|
+
function commandExplain(parsed, io) {
|
|
436
|
+
const id = parsed.positionals[0];
|
|
437
|
+
if (!id) {
|
|
438
|
+
write(io, 'stderr', 'vibe-shield: explain needs a rule id, e.g. `vibe-shield explain VSB-DB-001`\n');
|
|
439
|
+
return exports.EXIT_USAGE;
|
|
440
|
+
}
|
|
441
|
+
const rule = (0, rules_1.getRule)(id);
|
|
442
|
+
if (!rule) {
|
|
443
|
+
write(io, 'stderr', `vibe-shield: unknown rule "${id}". Run \`vibe-shield rules\` to list them.\n`);
|
|
444
|
+
return exports.EXIT_USAGE;
|
|
445
|
+
}
|
|
446
|
+
const color = (0, console_reporter_1.createColorizer)(io.stdout);
|
|
447
|
+
if (flagBool(parsed, 'no-color'))
|
|
448
|
+
color.enabled = false;
|
|
449
|
+
const out = [];
|
|
450
|
+
out.push('');
|
|
451
|
+
out.push(`${color.bold(rule.id)} ${color.bold(rule.title)}`);
|
|
452
|
+
out.push(color.dim(`${rule.category} · ${rule.severity} severity · ${rule.confidence} confidence · ${rule.cwe.id}`));
|
|
453
|
+
out.push('');
|
|
454
|
+
out.push(rule.description);
|
|
455
|
+
out.push('');
|
|
456
|
+
out.push(color.bold('Vulnerable'));
|
|
457
|
+
out.push(color.red(rule.insecureExample));
|
|
458
|
+
out.push('');
|
|
459
|
+
out.push(color.bold('Fixed'));
|
|
460
|
+
out.push(color.green(rule.secureExample));
|
|
461
|
+
out.push('');
|
|
462
|
+
out.push(color.bold('Remediation'));
|
|
463
|
+
rule.remediation.forEach((step, index) => out.push(` ${index + 1}. ${step}`));
|
|
464
|
+
out.push('');
|
|
465
|
+
out.push(color.bold('References'));
|
|
466
|
+
for (const reference of rule.references)
|
|
467
|
+
out.push(` ${color.cyan(reference)}`);
|
|
468
|
+
out.push('');
|
|
469
|
+
out.push(color.dim(`CWE: ${rule.cwe.name} — ${rule.cwe.url}`));
|
|
470
|
+
out.push('');
|
|
471
|
+
write(io, 'stdout', `${out.join('\n')}\n`);
|
|
472
|
+
return exports.EXIT_OK;
|
|
473
|
+
}
|
|
474
|
+
/* -------------------------------------------------------------------------- */
|
|
475
|
+
/* mcp */
|
|
476
|
+
/* -------------------------------------------------------------------------- */
|
|
477
|
+
/**
|
|
478
|
+
* Serve MCP over stdio.
|
|
479
|
+
*
|
|
480
|
+
* Nothing but JSON-RPC may reach stdout here — a banner or progress line would
|
|
481
|
+
* corrupt the client session — so the one diagnostic permitted (an explicit
|
|
482
|
+
* note that the server is ready) goes to stderr.
|
|
483
|
+
*/
|
|
484
|
+
async function commandMcp(parsed, io) {
|
|
485
|
+
if (parsed.positionals.length) {
|
|
486
|
+
write(io, 'stderr', `vibe-shield: mcp takes no positional arguments (got ${parsed.positionals.join(', ')})\n`);
|
|
487
|
+
return exports.EXIT_USAGE;
|
|
488
|
+
}
|
|
489
|
+
write(io, 'stderr', 'vibe-shield: MCP server listening on stdio\n');
|
|
490
|
+
await (0, server_1.serveMcp)({ stdin: process.stdin, stdout: io.stdout, stderr: io.stderr }, { cwd: io.cwd, version: await (0, engine_1.packageVersion)() });
|
|
491
|
+
return exports.EXIT_OK;
|
|
492
|
+
}
|
|
493
|
+
/* -------------------------------------------------------------------------- */
|
|
494
|
+
/* Entry point */
|
|
495
|
+
/* -------------------------------------------------------------------------- */
|
|
496
|
+
/** Run the CLI. Returns the process exit code. */
|
|
497
|
+
async function run(argv, io = (0, exports.defaultIo)()) {
|
|
498
|
+
const parsed = parseArgs(argv);
|
|
499
|
+
const command = parsed.command || (flagBool(parsed, 'version') ? 'version' : flagBool(parsed, 'help') ? 'help' : '');
|
|
500
|
+
if (parsed.unknown.length) {
|
|
501
|
+
write(io, 'stderr', `vibe-shield: unknown option${parsed.unknown.length === 1 ? '' : 's'} ${parsed.unknown.join(', ')}\n`);
|
|
502
|
+
write(io, 'stderr', 'Run `vibe-shield help` for usage.\n');
|
|
503
|
+
return exports.EXIT_USAGE;
|
|
504
|
+
}
|
|
505
|
+
switch (command) {
|
|
506
|
+
case 'scan':
|
|
507
|
+
return commandScan(parsed, io);
|
|
508
|
+
case 'audit-skills':
|
|
509
|
+
case 'audit':
|
|
510
|
+
return commandAuditSkills(parsed, io);
|
|
511
|
+
case 'init':
|
|
512
|
+
return commandInit(parsed, io);
|
|
513
|
+
case 'rules':
|
|
514
|
+
return commandRules(parsed, io);
|
|
515
|
+
case 'explain':
|
|
516
|
+
return commandExplain(parsed, io);
|
|
517
|
+
case 'mcp':
|
|
518
|
+
return commandMcp(parsed, io);
|
|
519
|
+
case 'version':
|
|
520
|
+
write(io, 'stdout', `${await (0, engine_1.packageVersion)()}\n`);
|
|
521
|
+
return exports.EXIT_OK;
|
|
522
|
+
case 'help':
|
|
523
|
+
case '':
|
|
524
|
+
write(io, 'stdout', HELP);
|
|
525
|
+
return command ? exports.EXIT_OK : exports.EXIT_USAGE;
|
|
526
|
+
default:
|
|
527
|
+
write(io, 'stderr', `vibe-shield: unknown command "${command}"\nRun \`vibe-shield help\` for usage.\n`);
|
|
528
|
+
return exports.EXIT_USAGE;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
/* Self-execute only when invoked as a program (never under test). */
|
|
532
|
+
if (require.main === module) {
|
|
533
|
+
void run(process.argv.slice(2))
|
|
534
|
+
.then((code) => {
|
|
535
|
+
process.exitCode = code;
|
|
536
|
+
})
|
|
537
|
+
.catch((error) => {
|
|
538
|
+
process.stderr.write(`vibe-shield: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
539
|
+
process.exitCode = exports.EXIT_USAGE;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe-shield :: public API
|
|
3
|
+
* -------------------------
|
|
4
|
+
* Everything an embedder needs: run a scan, audit agent surface, render a
|
|
5
|
+
* report, or write a patch prompt — without spawning the CLI.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { scanProject, formatSarif } from 'vibe-shield';
|
|
9
|
+
*
|
|
10
|
+
* const result = await scanProject({ rootDir: process.cwd() });
|
|
11
|
+
* process.stdout.write(formatSarif(result));
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export { scanProject, discoverFiles, runRulesOnFile, scoreFindings, applySuppressions, parseSuppressions, suppressionFor, hasFindingsAtOrAbove, dedupe, gitTrackedPaths, packageVersion, contentHash, isTestPath, isDocPath, DEFAULT_IGNORED_DIRS, BINARY_EXTENSIONS, MAX_FILE_SIZE_BYTES, DEFAULT_CONCURRENCY, CACHE_FILE_NAME, type ScanOptions, type DiscoveredFile, type DiscoverResult, } from './scanner/engine';
|
|
15
|
+
export { ALL_RULES, SCAN_RULES, RULES_FINGERPRINT, getRule, rulesByCategory, selectRules, } from './scanner/rules';
|
|
16
|
+
export { AGENT_RULES, auditSkills, discoverAgentFiles, parseFrontmatter, type AuditOptions, type SkillFrontmatter } from './mcp-guard/skill-auditor';
|
|
17
|
+
export { parseMiddlewareMatchers, parseSqlTables, mergeSqlTables, readGitTrackedPaths, detectProject, createFileContext, computeFlags, detectLanguage, tolerantJsonParse } from './scanner/context';
|
|
18
|
+
export { findSecrets, SECRET_PATTERNS, decodeJwtPayload, jwtHasPrivilegedRole, type SecretMatch, type SecretPattern } from './secrets/patterns';
|
|
19
|
+
export { reportConsole, renderConsoleReport, createColorizer, verdictLine, type Colorizer, type ReportOptions } from './reporter/console-reporter';
|
|
20
|
+
export { formatResult, formatJson, formatSummary, formatCompact, formatSarif, formatGithub, groupByFile, isOutputFormat, OUTPUT_FORMATS, SCHEMA_VERSION, type OutputFormat, } from './reporter/formatters';
|
|
21
|
+
export { buildPatchPrompt, writePatchPrompt, PATCH_PROMPT_FILENAME, type PatchPromptOptions, } from './reporter/patch-prompt-writer';
|
|
22
|
+
export { buildScaffold, installScaffold, SKILL_TEMPLATE, AGENTS_SECTION, WORKFLOW_TEMPLATE, IGNORE_TEMPLATE, type ScaffoldFile, type InitOptions, type InitResult, } from './init/scaffold';
|
|
23
|
+
export { SEVERITIES, SEVERITY_WEIGHT, SEVERITY_SCORE_PENALTY, SEVERITY_SCORE_CEILING, GRADES, GRADE_THRESHOLDS, RULE_CATEGORIES, CATEGORY_TITLES, emptyProjectContext, emptySeverityCounts, severityRank, meetsSeverity, isSeverity, isRuleCategory, type Cwe, type Confidence, type FileContext, type FileFlags, type Finding, type Grade, type Language, type ProjectContext, type Rule, type RuleCategory, type RuleHit, type ScanResult, type ScanStats, type SecurityScore, type Severity, type SeverityCounts, type SqlTableInfo, type Suppression, } from './scanner/types';
|
|
24
|
+
/** Package version, injected at build time from `package.json`. */
|
|
25
|
+
export declare const VERSION = "1.1.0";
|