@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,392 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vibe-shield :: credential signature library
|
|
4
|
+
* -------------------------------------------
|
|
5
|
+
* Vendor-specific secret signatures plus a Shannon-entropy fallback. The same
|
|
6
|
+
* table powers `VSB-SECRET-*` (source scanner) and the agent-skill auditor, so
|
|
7
|
+
* a leaked key is reported identically no matter where it is found.
|
|
8
|
+
*
|
|
9
|
+
* Every regex is *deliberately* written so it cannot match its own source text
|
|
10
|
+
* (that is why character classes appear as `[0-9A-Z]{16}` and never as a
|
|
11
|
+
* literal example key) — vibe-shield scans itself in CI.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.GENERIC_ASSIGNMENT_REGEX = exports.SECRET_PATTERNS = void 0;
|
|
15
|
+
exports.findSecrets = findSecrets;
|
|
16
|
+
exports.decodeJwtPayload = decodeJwtPayload;
|
|
17
|
+
exports.jwtHasPrivilegedRole = jwtHasPrivilegedRole;
|
|
18
|
+
const text_1 = require("../scanner/text");
|
|
19
|
+
const q = (value) => value;
|
|
20
|
+
exports.SECRET_PATTERNS = [
|
|
21
|
+
{
|
|
22
|
+
id: 'private-key',
|
|
23
|
+
vendor: 'Private key material',
|
|
24
|
+
family: 'private-key',
|
|
25
|
+
severity: 'critical',
|
|
26
|
+
regex: /-----BEGIN[ \t]+(?:(?:RSA|EC|DSA|OPENSSH|PGP|ENCRYPTED)[ \t]+)?PRIVATE[ \t]+KEY(?:[ \t]+BLOCK)?-----/g,
|
|
27
|
+
raw: true,
|
|
28
|
+
note: 'An unencrypted private key is committed to the repository.',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'anthropic',
|
|
32
|
+
vendor: 'Anthropic',
|
|
33
|
+
family: 'anthropic',
|
|
34
|
+
severity: 'critical',
|
|
35
|
+
regex: /\bsk-ant-(?:api|sid)\d{2}-[A-Za-z0-9_-]{20,}(?:-[A-Za-z0-9_-]{8,})?/g,
|
|
36
|
+
minEntropy: 3,
|
|
37
|
+
note: 'Anthropic API keys can be replayed for unlimited model spend.',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'openai-project',
|
|
41
|
+
vendor: 'OpenAI',
|
|
42
|
+
family: 'openai',
|
|
43
|
+
severity: 'critical',
|
|
44
|
+
regex: /\bsk-(?:proj|svcacct)-[A-Za-z0-9_-]{20,}/g,
|
|
45
|
+
minEntropy: 3,
|
|
46
|
+
note: 'OpenAI project/service-account key.',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'openai',
|
|
50
|
+
vendor: 'OpenAI',
|
|
51
|
+
family: 'openai',
|
|
52
|
+
severity: 'critical',
|
|
53
|
+
regex: /\bsk-[A-Za-z0-9_-]{24,}\b/g,
|
|
54
|
+
minEntropy: 3.2,
|
|
55
|
+
note: 'OpenAI-compatible API key.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'aws-access-key',
|
|
59
|
+
vendor: 'AWS',
|
|
60
|
+
family: 'aws',
|
|
61
|
+
severity: 'critical',
|
|
62
|
+
regex: /\b(?:A3T[A-Z0-9]|AKIA|ASIA|ABIA|ACCA|AGPA|AIDA|AIPA|ANPA|ANVA|APKA)[0-9A-Z]{16}\b/g,
|
|
63
|
+
minEntropy: 3,
|
|
64
|
+
note: 'AWS access key ID.',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'aws-secret-key',
|
|
68
|
+
vendor: 'AWS',
|
|
69
|
+
family: 'aws',
|
|
70
|
+
severity: 'critical',
|
|
71
|
+
regex: /\b(?:aws[_-]?secret[_-]?access[_-]?key|aws[_-]?secret[_-]?key|secret[_-]?access[_-]?key)\b["']?\s*[:=]\s*["']?([A-Za-z0-9/+=]{40})\b/gi,
|
|
72
|
+
minEntropy: 3.5,
|
|
73
|
+
note: 'AWS secret access key (40-char base64).',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'stripe-live-secret',
|
|
77
|
+
vendor: 'Stripe',
|
|
78
|
+
family: 'stripe',
|
|
79
|
+
severity: 'critical',
|
|
80
|
+
regex: /\b(?:sk|rk)_live_[0-9A-Za-z]{16,}\b/g,
|
|
81
|
+
minEntropy: 3,
|
|
82
|
+
note: 'Live-mode Stripe secret/restricted key: full access to charges and refunds.',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'stripe-webhook-secret',
|
|
86
|
+
vendor: 'Stripe',
|
|
87
|
+
family: 'stripe',
|
|
88
|
+
severity: 'high',
|
|
89
|
+
regex: /\bwhsec_[0-9A-Za-z]{16,}\b/g,
|
|
90
|
+
minEntropy: 3,
|
|
91
|
+
note: 'Stripe webhook signing secret: allows forging webhook events.',
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'github-token',
|
|
95
|
+
vendor: 'GitHub',
|
|
96
|
+
family: 'github',
|
|
97
|
+
severity: 'critical',
|
|
98
|
+
regex: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{36}\b|\bgithub_pat_[A-Za-z0-9_]{22,}\b/g,
|
|
99
|
+
minEntropy: 3,
|
|
100
|
+
note: 'GitHub personal access / OAuth / installation token.',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'supabase-access-token',
|
|
104
|
+
vendor: 'Supabase',
|
|
105
|
+
family: 'supabase',
|
|
106
|
+
severity: 'critical',
|
|
107
|
+
regex: /\bsbp_[a-f0-9]{40}\b|\bsb_secret_[A-Za-z0-9_]{20,}\b/g,
|
|
108
|
+
minEntropy: 2.5,
|
|
109
|
+
note: 'Supabase management API token / secret key.',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: 'google-api-key',
|
|
113
|
+
vendor: 'Google',
|
|
114
|
+
family: 'saas',
|
|
115
|
+
severity: 'high',
|
|
116
|
+
regex: /\bAIza[0-9A-Za-z_-]{35}\b/g,
|
|
117
|
+
minEntropy: 3,
|
|
118
|
+
note: 'Google API key (Maps / Gemini / Firebase).',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'slack-token',
|
|
122
|
+
vendor: 'Slack',
|
|
123
|
+
family: 'saas',
|
|
124
|
+
severity: 'high',
|
|
125
|
+
regex: /\bxox[baprs]-[0-9A-Za-z-]{10,}\b/g,
|
|
126
|
+
minEntropy: 3,
|
|
127
|
+
note: 'Slack bot/user/app token.',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'twilio',
|
|
131
|
+
vendor: 'Twilio',
|
|
132
|
+
family: 'saas',
|
|
133
|
+
severity: 'high',
|
|
134
|
+
regex: /\bAC[0-9a-fA-F]{32}\b|\bSK[0-9a-fA-F]{32}\b/g,
|
|
135
|
+
minEntropy: 3,
|
|
136
|
+
note: 'Twilio Account SID / API key.',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'sendgrid',
|
|
140
|
+
vendor: 'SendGrid',
|
|
141
|
+
family: 'saas',
|
|
142
|
+
severity: 'high',
|
|
143
|
+
regex: /\bSG\.[A-Za-z0-9_-]{16,32}\.[A-Za-z0-9_-]{24,}\b/g,
|
|
144
|
+
minEntropy: 3.5,
|
|
145
|
+
note: 'SendGrid API key.',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
id: 'telegram-bot',
|
|
149
|
+
vendor: 'Telegram',
|
|
150
|
+
family: 'saas',
|
|
151
|
+
severity: 'high',
|
|
152
|
+
regex: /\b\d{8,10}:AA[0-9A-Za-z_-]{33}\b/g,
|
|
153
|
+
minEntropy: 3,
|
|
154
|
+
note: 'Telegram bot token.',
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
id: 'npm-token',
|
|
158
|
+
vendor: 'npm',
|
|
159
|
+
family: 'registry',
|
|
160
|
+
severity: 'critical',
|
|
161
|
+
regex: /\bnpm_[A-Za-z0-9]{36}\b/g,
|
|
162
|
+
minEntropy: 3,
|
|
163
|
+
note: 'npm automation/publish token (supply-chain risk).',
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
id: 'pypi-token',
|
|
167
|
+
vendor: 'PyPI',
|
|
168
|
+
family: 'registry',
|
|
169
|
+
severity: 'critical',
|
|
170
|
+
regex: /\bpypi-AgEI[A-Za-z0-9_-]{20,}\b/g,
|
|
171
|
+
minEntropy: 3,
|
|
172
|
+
note: 'PyPI API token.',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'huggingface-token',
|
|
176
|
+
vendor: 'Hugging Face',
|
|
177
|
+
family: 'registry',
|
|
178
|
+
severity: 'high',
|
|
179
|
+
regex: /\bhf_[A-Za-z0-9]{30,}\b/g,
|
|
180
|
+
minEntropy: 3,
|
|
181
|
+
note: 'Hugging Face access token.',
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
id: 'docker-token',
|
|
185
|
+
vendor: 'Docker Hub',
|
|
186
|
+
family: 'registry',
|
|
187
|
+
severity: 'high',
|
|
188
|
+
regex: /\bdckr_pat_[A-Za-z0-9_-]{20,}\b/g,
|
|
189
|
+
minEntropy: 3,
|
|
190
|
+
note: 'Docker Hub personal access token.',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'vercel-token',
|
|
194
|
+
vendor: 'Vercel',
|
|
195
|
+
family: 'registry',
|
|
196
|
+
severity: 'high',
|
|
197
|
+
regex: /\bvc_[A-Za-z0-9]{24,}\b/g,
|
|
198
|
+
minEntropy: 3,
|
|
199
|
+
note: 'Vercel access token.',
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'mailgun',
|
|
203
|
+
vendor: 'Mailgun',
|
|
204
|
+
family: 'saas',
|
|
205
|
+
severity: 'high',
|
|
206
|
+
regex: /\bkey-[0-9a-f]{32}\b/g,
|
|
207
|
+
minEntropy: 3,
|
|
208
|
+
note: 'Mailgun API key.',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: 'square',
|
|
212
|
+
vendor: 'Square',
|
|
213
|
+
family: 'saas',
|
|
214
|
+
severity: 'high',
|
|
215
|
+
regex: /\bsq0atp-[0-9A-Za-z_-]{22}\b|\bsq0csp-[0-9A-Za-z_-]{43}\b/g,
|
|
216
|
+
minEntropy: 3,
|
|
217
|
+
note: 'Square access token.',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'shopify',
|
|
221
|
+
vendor: 'Shopify',
|
|
222
|
+
family: 'saas',
|
|
223
|
+
severity: 'high',
|
|
224
|
+
regex: /\bshp(?:at|ca|pa)_[0-9A-Za-z]{32}\b/g,
|
|
225
|
+
minEntropy: 3,
|
|
226
|
+
note: 'Shopify access token.',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: 'datadog',
|
|
230
|
+
vendor: 'Datadog',
|
|
231
|
+
family: 'saas',
|
|
232
|
+
severity: 'high',
|
|
233
|
+
regex: /\b(?:api|app)[_-]?key\b["']?\s*[:=]\s*["']?([a-f0-9]{32,40})\b/gi,
|
|
234
|
+
minEntropy: 3,
|
|
235
|
+
note: 'Datadog API/application key.',
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
id: 'jwt',
|
|
239
|
+
vendor: 'JSON Web Token',
|
|
240
|
+
family: 'jwt',
|
|
241
|
+
severity: 'high',
|
|
242
|
+
regex: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g,
|
|
243
|
+
minEntropy: 3.4,
|
|
244
|
+
note: 'Signed JWT — decode it to check for `service_role` / `admin` claims.',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: 'database-url',
|
|
248
|
+
vendor: 'Database connection string',
|
|
249
|
+
family: 'database-url',
|
|
250
|
+
severity: 'critical',
|
|
251
|
+
regex: /\b(?:postgres(?:ql)?|mysql|mssql|mongodb(?:\+srv)?|redis|rediss|amqps?|cockroachdb|clickhouse):\/\/[^\s:/@'"]+:([^\s@/'"]{6,})@[^\s'"`)]+/gi,
|
|
252
|
+
minEntropy: 2.2,
|
|
253
|
+
note: 'Connection string with an embedded password.',
|
|
254
|
+
},
|
|
255
|
+
];
|
|
256
|
+
/** Loose "assignment of a long opaque value" probe used by the generic rule. */
|
|
257
|
+
exports.GENERIC_ASSIGNMENT_REGEX = /(?:^|[^\w.])(?:"|')?([A-Za-z0-9_.\-\[\] ]{3,64}?(?:password|passwd|pwd|secret|token|api[-_]?key|apikey|access[-_]?key|private[-_]?key|client[-_]?secret|auth[-_]?token|session[-_]?key|signing[-_]?key|encryption[-_]?key|master[-_]?key|webhook[-_]?secret|credential|passphrase|mnemonic|seed[-_]?phrase))(?:"|')?\s*[:=]\s*(?:"([^"\n]{8,256})"|'([^'\n]{8,256})'|([^\s"'`,;#)\]}]{8,256}))/gi;
|
|
258
|
+
const containsOverlap = (accepted, offset, length) => accepted.some((m) => offset < m.offset + m.length && offset + length > m.offset);
|
|
259
|
+
/** Find every credential in `text`, deduplicating overlapping matches. */
|
|
260
|
+
function findSecrets(text, options = {}) {
|
|
261
|
+
const matches = [];
|
|
262
|
+
for (const pattern of exports.SECRET_PATTERNS) {
|
|
263
|
+
const regex = new RegExp(pattern.regex.source, pattern.regex.flags);
|
|
264
|
+
let match;
|
|
265
|
+
let guard = 0;
|
|
266
|
+
while ((match = regex.exec(text)) !== null && guard < 5000) {
|
|
267
|
+
guard += 1;
|
|
268
|
+
const captured = (match[1] ?? match[0]).trim();
|
|
269
|
+
if (match[0].length === 0) {
|
|
270
|
+
regex.lastIndex += 1;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
if (!pattern.raw && (0, text_1.isPlaceholderSecret)(captured))
|
|
274
|
+
continue;
|
|
275
|
+
const entropy = (0, text_1.shannonEntropy)(captured);
|
|
276
|
+
if (!pattern.raw && pattern.minEntropy !== undefined && entropy < pattern.minEntropy)
|
|
277
|
+
continue;
|
|
278
|
+
if (!pattern.raw && pattern.minEntropy !== undefined && (0, text_1.alphabetSize)(captured) < 4)
|
|
279
|
+
continue;
|
|
280
|
+
matches.push({
|
|
281
|
+
patternId: pattern.id,
|
|
282
|
+
vendor: pattern.vendor,
|
|
283
|
+
family: pattern.family,
|
|
284
|
+
severity: pattern.severity,
|
|
285
|
+
value: captured,
|
|
286
|
+
display: (0, text_1.redactSecret)(captured),
|
|
287
|
+
offset: match.index,
|
|
288
|
+
length: match[0].length,
|
|
289
|
+
entropy: Number(entropy.toFixed(2)),
|
|
290
|
+
note: pattern.note,
|
|
291
|
+
});
|
|
292
|
+
if (regex.lastIndex === match.index)
|
|
293
|
+
regex.lastIndex += 1;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (!options.skipGeneric) {
|
|
297
|
+
const regex = new RegExp(exports.GENERIC_ASSIGNMENT_REGEX.source, exports.GENERIC_ASSIGNMENT_REGEX.flags);
|
|
298
|
+
let match;
|
|
299
|
+
let guard = 0;
|
|
300
|
+
while ((match = regex.exec(text)) !== null && guard < 5000) {
|
|
301
|
+
guard += 1;
|
|
302
|
+
const value = (match[2] ?? match[3] ?? match[4] ?? '').trim();
|
|
303
|
+
const name = (match[1] ?? '').trim();
|
|
304
|
+
if (!value || (0, text_1.isPlaceholderSecret)(value))
|
|
305
|
+
continue;
|
|
306
|
+
const entropy = (0, text_1.shannonEntropy)(value);
|
|
307
|
+
const longValue = value.length >= 20;
|
|
308
|
+
const threshold = longValue ? 3 : 3.6;
|
|
309
|
+
if (entropy < threshold || (0, text_1.alphabetSize)(value) < 4)
|
|
310
|
+
continue;
|
|
311
|
+
const valueOffset = match.index + match[0].lastIndexOf(value);
|
|
312
|
+
matches.push({
|
|
313
|
+
patternId: 'generic',
|
|
314
|
+
vendor: q(name.replace(/\s+/g, ' ')),
|
|
315
|
+
family: 'generic',
|
|
316
|
+
severity: longValue ? 'high' : 'medium',
|
|
317
|
+
value,
|
|
318
|
+
display: (0, text_1.redactSecret)(value),
|
|
319
|
+
offset: valueOffset,
|
|
320
|
+
length: value.length,
|
|
321
|
+
entropy: Number(entropy.toFixed(2)),
|
|
322
|
+
note: `High-entropy value assigned to \`${name}\` (Shannon entropy ${entropy.toFixed(2)}).`,
|
|
323
|
+
});
|
|
324
|
+
if (regex.lastIndex === match.index)
|
|
325
|
+
regex.lastIndex += 1;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
// Specific vendor signatures win over generic/loose ones.
|
|
329
|
+
const priority = (m) => {
|
|
330
|
+
if (m.family === 'generic')
|
|
331
|
+
return 0;
|
|
332
|
+
if (m.family === 'jwt')
|
|
333
|
+
return 1;
|
|
334
|
+
return 2;
|
|
335
|
+
};
|
|
336
|
+
matches.sort((a, b) => priority(b) - priority(a) || b.length - a.length || a.offset - b.offset);
|
|
337
|
+
const accepted = [];
|
|
338
|
+
for (const candidate of matches) {
|
|
339
|
+
if (containsOverlap(accepted, candidate.offset, candidate.length))
|
|
340
|
+
continue;
|
|
341
|
+
accepted.push(candidate);
|
|
342
|
+
}
|
|
343
|
+
accepted.sort((a, b) => a.offset - b.offset);
|
|
344
|
+
return accepted;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Decode a JWT payload without verifying it — used to spot `service_role`,
|
|
348
|
+
* `admin` or `exp: never` claims sitting in source code.
|
|
349
|
+
*/
|
|
350
|
+
function decodeJwtPayload(token) {
|
|
351
|
+
const parts = token.split('.');
|
|
352
|
+
if (parts.length < 2)
|
|
353
|
+
return null;
|
|
354
|
+
const payload = parts[1];
|
|
355
|
+
if (!payload)
|
|
356
|
+
return null;
|
|
357
|
+
try {
|
|
358
|
+
const normalised = payload.replace(/-/g, '+').replace(/_/g, '/');
|
|
359
|
+
const padded = normalised + '='.repeat((4 - (normalised.length % 4)) % 4);
|
|
360
|
+
const json = Buffer.from(padded, 'base64').toString('utf8');
|
|
361
|
+
const parsed = JSON.parse(json);
|
|
362
|
+
return parsed && typeof parsed === 'object' ? parsed : null;
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
/** True when a JWT carries a privileged role claim. */
|
|
369
|
+
function jwtHasPrivilegedRole(token) {
|
|
370
|
+
const payload = decodeJwtPayload(token);
|
|
371
|
+
if (!payload)
|
|
372
|
+
return { privileged: false, role: null };
|
|
373
|
+
const candidates = [
|
|
374
|
+
payload['role'],
|
|
375
|
+
payload['roles'],
|
|
376
|
+
payload['app_metadata']?.['role'],
|
|
377
|
+
payload['user_metadata']?.['role'],
|
|
378
|
+
payload['claims']?.['role'],
|
|
379
|
+
];
|
|
380
|
+
for (const candidate of candidates) {
|
|
381
|
+
const values = Array.isArray(candidate) ? candidate : [candidate];
|
|
382
|
+
for (const value of values) {
|
|
383
|
+
if (typeof value !== 'string')
|
|
384
|
+
continue;
|
|
385
|
+
const lower = value.toLowerCase();
|
|
386
|
+
if (['service_role', 'servicerole', 'admin', 'superadmin', 'root', 'staff', 'owner'].includes(lower)) {
|
|
387
|
+
return { privileged: true, role: value };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { privileged: false, role: null };
|
|
392
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,96 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibefoundry/vibe-shield",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.1.0",
|
|
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
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"mcp-server",
|
|
8
|
+
"model-context-protocol",
|
|
9
|
+
"mcp-tools",
|
|
10
|
+
"mcp-security",
|
|
11
|
+
"mcp-client",
|
|
12
|
+
"json-rpc",
|
|
13
|
+
"claude-code",
|
|
14
|
+
"claude-desktop",
|
|
15
|
+
"codex",
|
|
16
|
+
"cursor",
|
|
17
|
+
"windsurf",
|
|
18
|
+
"ai-agents",
|
|
19
|
+
"agent-skills",
|
|
20
|
+
"agents-md",
|
|
21
|
+
"ai-security",
|
|
22
|
+
"ai-generated-code",
|
|
23
|
+
"vibe-coding",
|
|
24
|
+
"security-scanner",
|
|
25
|
+
"vulnerability-scanner",
|
|
26
|
+
"static-analysis",
|
|
27
|
+
"sast",
|
|
28
|
+
"appsec",
|
|
29
|
+
"cybersecurity",
|
|
30
|
+
"devsecops",
|
|
31
|
+
"owasp",
|
|
32
|
+
"cwe",
|
|
33
|
+
"sarif",
|
|
34
|
+
"prompt-injection",
|
|
35
|
+
"secret-scanner",
|
|
36
|
+
"secrets-detection",
|
|
37
|
+
"security-audit",
|
|
38
|
+
"supabase-rls",
|
|
39
|
+
"nextjs-security",
|
|
40
|
+
"row-level-security",
|
|
41
|
+
"sql-injection",
|
|
42
|
+
"agent-skill-audit",
|
|
43
|
+
"github-actions",
|
|
44
|
+
"code-scanning",
|
|
45
|
+
"ci-security-gate",
|
|
46
|
+
"npx",
|
|
47
|
+
"typescript",
|
|
48
|
+
"zero-dependencies"
|
|
49
|
+
],
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"author": "VibeFoundry",
|
|
52
|
+
"homepage": "https://github.com/avivgerz-hue/vibe-shield#readme",
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/avivgerz-hue/vibe-shield.git"
|
|
56
|
+
},
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/avivgerz-hue/vibe-shield/issues"
|
|
59
|
+
},
|
|
60
|
+
"main": "./dist/src/index.js",
|
|
61
|
+
"types": "./dist/src/index.d.ts",
|
|
62
|
+
"bin": {
|
|
63
|
+
"vibe-shield": "dist/bin/vibe-shield.js"
|
|
64
|
+
},
|
|
65
|
+
"files": [
|
|
66
|
+
"dist/bin",
|
|
67
|
+
"dist/src",
|
|
68
|
+
"SKILL.md",
|
|
69
|
+
"README.md",
|
|
70
|
+
"SECURITY.md",
|
|
71
|
+
"LICENSE"
|
|
72
|
+
],
|
|
73
|
+
"engines": {
|
|
74
|
+
"node": ">=18.17.0"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
|
|
78
|
+
"build": "npm run clean && tsc -p tsconfig.build.json && node scripts/postbuild.mjs",
|
|
79
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
80
|
+
"test": "vitest run",
|
|
81
|
+
"test:watch": "vitest",
|
|
82
|
+
"coverage": "vitest run --coverage",
|
|
83
|
+
"scan:self": "node dist/bin/vibe-shield.js scan",
|
|
84
|
+
"audit:self": "node dist/bin/vibe-shield.js audit-skills",
|
|
85
|
+
"verify": "npm run typecheck && npm run build && npm test && npm run scan:self && npm run audit:self",
|
|
86
|
+
"prepare": "node scripts/prepare.mjs",
|
|
87
|
+
"prepublishOnly": "npm run verify"
|
|
88
|
+
},
|
|
89
|
+
"dependencies": {},
|
|
90
|
+
"devDependencies": {
|
|
91
|
+
"@types/node": "^22.20.5",
|
|
92
|
+
"@vitest/coverage-v8": "^5.1.0",
|
|
93
|
+
"typescript": "^5.9.3",
|
|
94
|
+
"vitest": "^5.1.0"
|
|
95
|
+
}
|
|
96
|
+
}
|