@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,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe-shield :: scanner type system
|
|
3
|
+
* ----------------------------------
|
|
4
|
+
* Every rule, finding and scan result in vibe-shield is described by the
|
|
5
|
+
* interfaces below. They are intentionally serialisable (plain JSON) so that a
|
|
6
|
+
* `ScanResult` can be cached to disk, emitted as SARIF, or handed to an agent
|
|
7
|
+
* as a patch prompt without any transformation.
|
|
8
|
+
*/
|
|
9
|
+
/** Ordered list of severities, most severe first. */
|
|
10
|
+
export declare const SEVERITIES: readonly ["critical", "high", "medium", "low", "info"];
|
|
11
|
+
export type Severity = (typeof SEVERITIES)[number];
|
|
12
|
+
/** Detection confidence reported alongside every finding. */
|
|
13
|
+
export declare const CONFIDENCES: readonly ["high", "medium", "low"];
|
|
14
|
+
export type Confidence = (typeof CONFIDENCES)[number];
|
|
15
|
+
/**
|
|
16
|
+
* Rule families. Each family maps 1:1 to a module in `src/scanner/rules`
|
|
17
|
+
* (plus `agent-integrity`, which is produced by the MCP / skill auditor).
|
|
18
|
+
*/
|
|
19
|
+
export declare const RULE_CATEGORIES: readonly ["client-exposure", "unguarded-routes", "database-exposure", "ai-vulnerabilities", "secret-entropy", "agent-integrity"];
|
|
20
|
+
export type RuleCategory = (typeof RULE_CATEGORIES)[number];
|
|
21
|
+
export declare const CATEGORY_TITLES: Record<RuleCategory, string>;
|
|
22
|
+
/** Relative severity weight used by the scoring model. */
|
|
23
|
+
export declare const SEVERITY_WEIGHT: Record<Severity, number>;
|
|
24
|
+
/**
|
|
25
|
+
* Points deducted from the 100-point security score per finding. The curve is
|
|
26
|
+
* deliberately steep: one leaked live key must be able to fail a build on its
|
|
27
|
+
* own, while a handful of informational notes must not.
|
|
28
|
+
*/
|
|
29
|
+
export declare const SEVERITY_SCORE_PENALTY: Record<Severity, number>;
|
|
30
|
+
/**
|
|
31
|
+
* Score ceilings: the worst severity present caps the achievable score, so a
|
|
32
|
+
* single severe finding can never yield a good grade no matter how clean the
|
|
33
|
+
* rest of the repository is.
|
|
34
|
+
*
|
|
35
|
+
* - any critical -> at most 29 (grade F)
|
|
36
|
+
* - any high -> at most 58 (grade C)
|
|
37
|
+
* - any medium -> at most 76 (grade B)
|
|
38
|
+
* - any low -> at most 88 (grade A-)
|
|
39
|
+
*/
|
|
40
|
+
export declare const SEVERITY_SCORE_CEILING: Partial<Record<Severity, number>>;
|
|
41
|
+
export declare const GRADES: readonly ["A+", "A", "A-", "B+", "B", "B-", "C+", "C", "C-", "D+", "D", "D-", "F"];
|
|
42
|
+
export type Grade = (typeof GRADES)[number];
|
|
43
|
+
/** Minimum score required for each grade. */
|
|
44
|
+
export declare const GRADE_THRESHOLDS: ReadonlyArray<{
|
|
45
|
+
grade: Grade;
|
|
46
|
+
min: number;
|
|
47
|
+
}>;
|
|
48
|
+
/** A CWE reference (https://cwe.mitre.org). */
|
|
49
|
+
export interface Cwe {
|
|
50
|
+
/** e.g. `CWE-79` */
|
|
51
|
+
id: string;
|
|
52
|
+
/** Short human readable name, e.g. `Cross-site Scripting`. */
|
|
53
|
+
name: string;
|
|
54
|
+
/** Direct link to the CWE entry. */
|
|
55
|
+
url: string;
|
|
56
|
+
}
|
|
57
|
+
/** Source language buckets used for cheap rule pre-filtering. */
|
|
58
|
+
export type Language = 'typescript' | 'javascript' | 'jsx' | 'vue' | 'svelte' | 'html' | 'css' | 'sql' | 'prisma' | 'python' | 'ruby' | 'php' | 'go' | 'rust' | 'java' | 'shell' | 'yaml' | 'toml' | 'json' | 'markdown' | 'dotenv' | 'dockerfile' | 'terraform' | 'graphql' | 'xml' | 'other';
|
|
59
|
+
export declare const LANGUAGE_EXTENSIONS: Readonly<Record<string, Language>>;
|
|
60
|
+
/** Structural hints computed once per file and shared by every rule. */
|
|
61
|
+
export interface FileFlags {
|
|
62
|
+
/** File starts with (or contains at top level) a `"use client"` directive. */
|
|
63
|
+
isClientComponent: boolean;
|
|
64
|
+
/** File contains a `"use server"` directive (Next.js server action module). */
|
|
65
|
+
isServerActionModule: boolean;
|
|
66
|
+
/** Next.js App Router handler: `app/**\/route.(ts|js)`. */
|
|
67
|
+
isAppRouterRoute: boolean;
|
|
68
|
+
/** Next.js Pages Router handler: `pages/api/**`. */
|
|
69
|
+
isPagesApiRoute: boolean;
|
|
70
|
+
/** Express / Fastify / Koa / Hono style server file. */
|
|
71
|
+
isServerFrameworkFile: boolean;
|
|
72
|
+
/** Path lives in a directory that is shipped to the browser verbatim. */
|
|
73
|
+
isStaticAssetDir: boolean;
|
|
74
|
+
/** Path matches a client-side source location (components/, pages/, app/, src/client...). */
|
|
75
|
+
isClientPath: boolean;
|
|
76
|
+
/** SQL migration file (Supabase / Prisma / Drizzle / raw migrations dir). */
|
|
77
|
+
isMigration: boolean;
|
|
78
|
+
/** `.env*` file. */
|
|
79
|
+
isEnvFile: boolean;
|
|
80
|
+
/** Test / spec / fixture / storybook file. */
|
|
81
|
+
isTestFile: boolean;
|
|
82
|
+
/** Documentation file (`*.md`, `docs/**`). */
|
|
83
|
+
isDocumentation: boolean;
|
|
84
|
+
/** Lockfile (never scanned for logic rules). */
|
|
85
|
+
isLockfile: boolean;
|
|
86
|
+
/** Generated / vendored file. */
|
|
87
|
+
isGenerated: boolean;
|
|
88
|
+
/** Agent instruction file (CLAUDE.md, AGENTS.md, .cursorrules, SKILL.md...). */
|
|
89
|
+
isAgentInstructionFile: boolean;
|
|
90
|
+
/** MCP configuration file (`mcp.json`, `.mcp.json`, `.cursor/mcp.json`...). */
|
|
91
|
+
isMcpConfig: boolean;
|
|
92
|
+
/** CI workflow file (`.github/workflows/*`). */
|
|
93
|
+
isCiWorkflow: boolean;
|
|
94
|
+
/** File mentions authentication/session symbols anywhere. */
|
|
95
|
+
mentionsAuth: boolean;
|
|
96
|
+
/** File performs a state mutation (DB write, HTTP mutation, fs write). */
|
|
97
|
+
mentionsMutation: boolean;
|
|
98
|
+
/** File talks to an LLM provider or the Vercel AI SDK. */
|
|
99
|
+
mentionsLlm: boolean;
|
|
100
|
+
}
|
|
101
|
+
/** A table parsed out of the project's SQL files, with its RLS posture. */
|
|
102
|
+
export interface SqlTableInfo {
|
|
103
|
+
/** Schema-qualified name as written, e.g. `public.accounts`. */
|
|
104
|
+
table: string;
|
|
105
|
+
/** Bare table name without the schema, e.g. `accounts`. */
|
|
106
|
+
name: string;
|
|
107
|
+
/** SQL file (relative to the scan root) that created the table. */
|
|
108
|
+
file: string;
|
|
109
|
+
/** 1-based line of the `CREATE TABLE` statement. */
|
|
110
|
+
line: number;
|
|
111
|
+
rlsEnabled: boolean;
|
|
112
|
+
rlsForced: boolean;
|
|
113
|
+
/** Names of every policy created for this table, across all SQL files. */
|
|
114
|
+
policies: string[];
|
|
115
|
+
/** True when `GRANT ... TO anon|authenticated|public` touches this table. */
|
|
116
|
+
grantedToPublic: boolean;
|
|
117
|
+
/** True for `CREATE TEMP/TEMPORARY/UNLOGGED TABLE` and catalog schemas. */
|
|
118
|
+
internal: boolean;
|
|
119
|
+
}
|
|
120
|
+
/** Whole-project context: framework detection plus resolved config. */
|
|
121
|
+
export interface ProjectContext {
|
|
122
|
+
/** Absolute path of the scan root. */
|
|
123
|
+
rootDir: string;
|
|
124
|
+
/** Frameworks/SDKs detected from package manifests and file layout. */
|
|
125
|
+
frameworks: string[];
|
|
126
|
+
/** `dependencies` + `devDependencies` names across discovered manifests. */
|
|
127
|
+
dependencies: string[];
|
|
128
|
+
/** True when any manifest declares an Express-family server. */
|
|
129
|
+
hasExpressServer: boolean;
|
|
130
|
+
hasFastifyServer: boolean;
|
|
131
|
+
hasCookieSession: boolean;
|
|
132
|
+
hasRateLimiting: boolean;
|
|
133
|
+
hasCsrfProtection: boolean;
|
|
134
|
+
hasSanitizer: boolean;
|
|
135
|
+
hasAuthLibrary: boolean;
|
|
136
|
+
hasAiSdk: boolean;
|
|
137
|
+
hasSupabase: boolean;
|
|
138
|
+
hasStripe: boolean;
|
|
139
|
+
hasPrisma: boolean;
|
|
140
|
+
/** File-relative list of SQL migration files discovered in the project. */
|
|
141
|
+
migrationFiles: string[];
|
|
142
|
+
/**
|
|
143
|
+
* Cross-file RLS index. Row Level Security is frequently enabled in a
|
|
144
|
+
* *later* migration than the `CREATE TABLE`, so the posture of a table can
|
|
145
|
+
* only be judged project-wide.
|
|
146
|
+
*/
|
|
147
|
+
sqlTables: SqlTableInfo[];
|
|
148
|
+
/** True when a root-level `middleware.ts` verifies auth for every request. */
|
|
149
|
+
hasGlobalAuthMiddleware: boolean;
|
|
150
|
+
/** `matcher` patterns declared by that middleware (empty = matches all). */
|
|
151
|
+
authMiddlewareMatchers: string[];
|
|
152
|
+
/** Paths tracked by git, relative to the scan root (empty without git). */
|
|
153
|
+
trackedPaths: string[];
|
|
154
|
+
/** True when the scan root sits inside a readable git work tree. */
|
|
155
|
+
gitAvailable: boolean;
|
|
156
|
+
}
|
|
157
|
+
/** An empty project context, used by tests and single-file scans. */
|
|
158
|
+
export declare const emptyProjectContext: (rootDir: string) => ProjectContext;
|
|
159
|
+
/** Everything a rule is allowed to look at for a single file. */
|
|
160
|
+
export interface FileContext {
|
|
161
|
+
/** Absolute path. */
|
|
162
|
+
absolutePath: string;
|
|
163
|
+
/** POSIX-style path relative to the scan root (what gets printed). */
|
|
164
|
+
relativePath: string;
|
|
165
|
+
fileName: string;
|
|
166
|
+
/** Lower-cased extension without the dot (`.env.local` -> `local`). */
|
|
167
|
+
extension: string;
|
|
168
|
+
language: Language;
|
|
169
|
+
/** Raw file content. */
|
|
170
|
+
content: string;
|
|
171
|
+
/** Content lower-cased once, for cheap case-insensitive hint tests. */
|
|
172
|
+
lowerContent: string;
|
|
173
|
+
/** Content with comments blanked out (same length, offsets preserved). */
|
|
174
|
+
code: string;
|
|
175
|
+
/** Content with comments blanked out, lower-cased. */
|
|
176
|
+
lowerCode: string;
|
|
177
|
+
/** Split lines of `content` (no trailing newlines). */
|
|
178
|
+
lines: string[];
|
|
179
|
+
/** Byte length of the file. */
|
|
180
|
+
bytes: number;
|
|
181
|
+
flags: FileFlags;
|
|
182
|
+
project: ProjectContext;
|
|
183
|
+
/** Convert a character offset into 1-based `{ line, column }`. */
|
|
184
|
+
locate(offset: number): {
|
|
185
|
+
line: number;
|
|
186
|
+
column: number;
|
|
187
|
+
};
|
|
188
|
+
/** 1-based line text accessor (returns '' for out-of-range lines). */
|
|
189
|
+
lineText(line: number): string;
|
|
190
|
+
}
|
|
191
|
+
/** A single detection produced by a rule. */
|
|
192
|
+
export interface RuleHit {
|
|
193
|
+
/** Character offset of the match inside `FileContext.content`. */
|
|
194
|
+
offset?: number;
|
|
195
|
+
/** 1-based line (used when `offset` is absent). */
|
|
196
|
+
line?: number;
|
|
197
|
+
/** 1-based column (used when `offset` is absent). */
|
|
198
|
+
column?: number;
|
|
199
|
+
/** Override for the printed match text (secrets must be redacted). */
|
|
200
|
+
matched?: string;
|
|
201
|
+
/** Override for the human readable message. */
|
|
202
|
+
message?: string;
|
|
203
|
+
/** Override severity for this specific hit. */
|
|
204
|
+
severity?: Severity;
|
|
205
|
+
/** Override confidence for this specific hit. */
|
|
206
|
+
confidence?: Confidence;
|
|
207
|
+
/** Extra structured data carried into the finding / patch prompt. */
|
|
208
|
+
data?: Record<string, unknown>;
|
|
209
|
+
}
|
|
210
|
+
/** A fully implemented vibe-shield detection rule. */
|
|
211
|
+
export interface Rule {
|
|
212
|
+
/** Stable identifier, e.g. `VSB-DB-001`. */
|
|
213
|
+
id: string;
|
|
214
|
+
/** Kebab-case slug used in config files and SARIF, e.g. `migration-table-without-rls`. */
|
|
215
|
+
slug: string;
|
|
216
|
+
title: string;
|
|
217
|
+
category: RuleCategory;
|
|
218
|
+
severity: Severity;
|
|
219
|
+
confidence: Confidence;
|
|
220
|
+
cwe: Cwe;
|
|
221
|
+
/** Why this matters — one or two sentences, written for the coding agent. */
|
|
222
|
+
description: string;
|
|
223
|
+
/** Code shape that triggers the rule. */
|
|
224
|
+
insecureExample: string;
|
|
225
|
+
/** Code shape that resolves the finding. */
|
|
226
|
+
secureExample: string;
|
|
227
|
+
/** Ordered remediation steps injected into `SECURITY_PATCH_PROMPT.md`. */
|
|
228
|
+
remediation: string[];
|
|
229
|
+
references: string[];
|
|
230
|
+
/** File extensions the rule applies to (empty = any). */
|
|
231
|
+
extensions?: string[];
|
|
232
|
+
/** Languages the rule applies to (empty = any). */
|
|
233
|
+
languages?: Language[];
|
|
234
|
+
/** Path regexes; when present at least one must match `relativePath`. */
|
|
235
|
+
pathPatterns?: RegExp[];
|
|
236
|
+
/** Path regexes that disqualify a file even when `pathPatterns` matched. */
|
|
237
|
+
excludePathPatterns?: RegExp[];
|
|
238
|
+
/**
|
|
239
|
+
* Cheap content probes. The engine only calls `detect` when at least one
|
|
240
|
+
* hint matches, which keeps a full-repo scan in the millisecond range.
|
|
241
|
+
*/
|
|
242
|
+
contentHints?: RegExp[];
|
|
243
|
+
/** Files for which the rule should never run (tests, docs, lockfiles...). */
|
|
244
|
+
skipFlags?: Array<keyof FileFlags>;
|
|
245
|
+
detect(ctx: FileContext): RuleHit[];
|
|
246
|
+
/**
|
|
247
|
+
* Scope predicate. `defineRule` derives it from the declarative fields above
|
|
248
|
+
* (plus an optional custom predicate); the engine calls it before `detect`.
|
|
249
|
+
*/
|
|
250
|
+
appliesTo?: (ctx: FileContext) => boolean;
|
|
251
|
+
/** Free-form tags used for filtering and documentation (`owasp-llm01`...). */
|
|
252
|
+
tags?: readonly string[];
|
|
253
|
+
/** Optional per-hit message refinement. */
|
|
254
|
+
message?(ctx: FileContext, hit: RuleHit): string;
|
|
255
|
+
/** Optional per-hit remediation refinement (replaces `remediation`). */
|
|
256
|
+
remediate?(ctx: FileContext, hit: RuleHit): string[];
|
|
257
|
+
}
|
|
258
|
+
/** A reported vulnerability with exact `file:line` citation. */
|
|
259
|
+
export interface Finding {
|
|
260
|
+
ruleId: string;
|
|
261
|
+
slug: string;
|
|
262
|
+
title: string;
|
|
263
|
+
category: RuleCategory;
|
|
264
|
+
severity: Severity;
|
|
265
|
+
confidence: Confidence;
|
|
266
|
+
cwe: Cwe;
|
|
267
|
+
/** POSIX path relative to the scan root. */
|
|
268
|
+
filePath: string;
|
|
269
|
+
absolutePath: string;
|
|
270
|
+
/** 1-based. */
|
|
271
|
+
line: number;
|
|
272
|
+
/** 1-based. */
|
|
273
|
+
column: number;
|
|
274
|
+
/** The offending source line, trimmed and truncated for display. */
|
|
275
|
+
snippet: string;
|
|
276
|
+
/** Exact matched text (secrets are redacted). */
|
|
277
|
+
matched: string;
|
|
278
|
+
message: string;
|
|
279
|
+
remediation: string[];
|
|
280
|
+
insecureExample: string;
|
|
281
|
+
secureExample: string;
|
|
282
|
+
references: string[];
|
|
283
|
+
data?: Record<string, unknown>;
|
|
284
|
+
}
|
|
285
|
+
/** Scan timing / throughput statistics shown in the scorecard. */
|
|
286
|
+
export interface ScanStats {
|
|
287
|
+
filesDiscovered: number;
|
|
288
|
+
filesScanned: number;
|
|
289
|
+
filesSkipped: number;
|
|
290
|
+
bytesScanned: number;
|
|
291
|
+
rulesExecuted: number;
|
|
292
|
+
ruleInvocations: number;
|
|
293
|
+
cacheHits: number;
|
|
294
|
+
durationMs: number;
|
|
295
|
+
filesPerSecond: number;
|
|
296
|
+
megabytesPerSecond: number;
|
|
297
|
+
}
|
|
298
|
+
/** Severity histogram. */
|
|
299
|
+
export type SeverityCounts = Record<Severity, number>;
|
|
300
|
+
/** Final scorecard. */
|
|
301
|
+
export interface SecurityScore {
|
|
302
|
+
/** 0-100, rounded to one decimal. */
|
|
303
|
+
score: number;
|
|
304
|
+
grade: Grade;
|
|
305
|
+
counts: SeverityCounts;
|
|
306
|
+
/** Rule ids responsible for the grade ceiling, if any. */
|
|
307
|
+
ceiling: {
|
|
308
|
+
severity: Severity;
|
|
309
|
+
limit: number;
|
|
310
|
+
} | null;
|
|
311
|
+
total: number;
|
|
312
|
+
}
|
|
313
|
+
export interface ScanResult {
|
|
314
|
+
rootDir: string;
|
|
315
|
+
command: 'scan' | 'audit-skills';
|
|
316
|
+
findings: Finding[];
|
|
317
|
+
score: SecurityScore;
|
|
318
|
+
stats: ScanStats;
|
|
319
|
+
scannedPaths: string[];
|
|
320
|
+
startedAt: string;
|
|
321
|
+
finishedAt: string;
|
|
322
|
+
version: string;
|
|
323
|
+
ruleCount: number;
|
|
324
|
+
suppressedCount: number;
|
|
325
|
+
/** Human readable notes about the run (empty results, skipped config...). */
|
|
326
|
+
notices: string[];
|
|
327
|
+
}
|
|
328
|
+
/** Suppression directive parsed from source comments. */
|
|
329
|
+
export interface Suppression {
|
|
330
|
+
line: number;
|
|
331
|
+
ruleIds: string[];
|
|
332
|
+
reason: string;
|
|
333
|
+
}
|
|
334
|
+
export declare const emptySeverityCounts: () => SeverityCounts;
|
|
335
|
+
/** Numeric rank so severities can be compared (`critical` = 5). */
|
|
336
|
+
export declare const severityRank: (severity: Severity) => number;
|
|
337
|
+
/** True when `severity` is at least as severe as `threshold`. */
|
|
338
|
+
export declare const meetsSeverity: (severity: Severity, threshold: Severity) => boolean;
|
|
339
|
+
export declare const isSeverity: (value: unknown) => value is Severity;
|
|
340
|
+
export declare const isRuleCategory: (value: unknown) => value is RuleCategory;
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vibe-shield :: scanner type system
|
|
4
|
+
* ----------------------------------
|
|
5
|
+
* Every rule, finding and scan result in vibe-shield is described by the
|
|
6
|
+
* interfaces below. They are intentionally serialisable (plain JSON) so that a
|
|
7
|
+
* `ScanResult` can be cached to disk, emitted as SARIF, or handed to an agent
|
|
8
|
+
* as a patch prompt without any transformation.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.isRuleCategory = exports.isSeverity = exports.meetsSeverity = exports.severityRank = exports.emptySeverityCounts = exports.emptyProjectContext = exports.LANGUAGE_EXTENSIONS = exports.GRADE_THRESHOLDS = exports.GRADES = exports.SEVERITY_SCORE_CEILING = exports.SEVERITY_SCORE_PENALTY = exports.SEVERITY_WEIGHT = exports.CATEGORY_TITLES = exports.RULE_CATEGORIES = exports.CONFIDENCES = exports.SEVERITIES = void 0;
|
|
12
|
+
/** Ordered list of severities, most severe first. */
|
|
13
|
+
exports.SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
14
|
+
/** Detection confidence reported alongside every finding. */
|
|
15
|
+
exports.CONFIDENCES = ['high', 'medium', 'low'];
|
|
16
|
+
/**
|
|
17
|
+
* Rule families. Each family maps 1:1 to a module in `src/scanner/rules`
|
|
18
|
+
* (plus `agent-integrity`, which is produced by the MCP / skill auditor).
|
|
19
|
+
*/
|
|
20
|
+
exports.RULE_CATEGORIES = [
|
|
21
|
+
'client-exposure',
|
|
22
|
+
'unguarded-routes',
|
|
23
|
+
'database-exposure',
|
|
24
|
+
'ai-vulnerabilities',
|
|
25
|
+
'secret-entropy',
|
|
26
|
+
'agent-integrity',
|
|
27
|
+
];
|
|
28
|
+
exports.CATEGORY_TITLES = {
|
|
29
|
+
'client-exposure': 'Client-side secret & data exposure',
|
|
30
|
+
'unguarded-routes': 'Unguarded mutation routes (authZ / authN)',
|
|
31
|
+
'database-exposure': 'Database exposure (RLS, SQLi, service role)',
|
|
32
|
+
'ai-vulnerabilities': 'AI / LLM & classic web vulnerabilities',
|
|
33
|
+
'secret-entropy': 'Hardcoded credentials & high-entropy secrets',
|
|
34
|
+
'agent-integrity': 'Agent skill & MCP poisoning',
|
|
35
|
+
};
|
|
36
|
+
/** Relative severity weight used by the scoring model. */
|
|
37
|
+
exports.SEVERITY_WEIGHT = {
|
|
38
|
+
critical: 5,
|
|
39
|
+
high: 4,
|
|
40
|
+
medium: 3,
|
|
41
|
+
low: 2,
|
|
42
|
+
info: 1,
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Points deducted from the 100-point security score per finding. The curve is
|
|
46
|
+
* deliberately steep: one leaked live key must be able to fail a build on its
|
|
47
|
+
* own, while a handful of informational notes must not.
|
|
48
|
+
*/
|
|
49
|
+
exports.SEVERITY_SCORE_PENALTY = {
|
|
50
|
+
critical: 30,
|
|
51
|
+
high: 12,
|
|
52
|
+
medium: 5,
|
|
53
|
+
low: 2,
|
|
54
|
+
info: 0.5,
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Score ceilings: the worst severity present caps the achievable score, so a
|
|
58
|
+
* single severe finding can never yield a good grade no matter how clean the
|
|
59
|
+
* rest of the repository is.
|
|
60
|
+
*
|
|
61
|
+
* - any critical -> at most 29 (grade F)
|
|
62
|
+
* - any high -> at most 58 (grade C)
|
|
63
|
+
* - any medium -> at most 76 (grade B)
|
|
64
|
+
* - any low -> at most 88 (grade A-)
|
|
65
|
+
*/
|
|
66
|
+
exports.SEVERITY_SCORE_CEILING = {
|
|
67
|
+
critical: 29,
|
|
68
|
+
high: 58,
|
|
69
|
+
medium: 76,
|
|
70
|
+
low: 88,
|
|
71
|
+
};
|
|
72
|
+
exports.GRADES = [
|
|
73
|
+
'A+',
|
|
74
|
+
'A',
|
|
75
|
+
'A-',
|
|
76
|
+
'B+',
|
|
77
|
+
'B',
|
|
78
|
+
'B-',
|
|
79
|
+
'C+',
|
|
80
|
+
'C',
|
|
81
|
+
'C-',
|
|
82
|
+
'D+',
|
|
83
|
+
'D',
|
|
84
|
+
'D-',
|
|
85
|
+
'F',
|
|
86
|
+
];
|
|
87
|
+
/** Minimum score required for each grade. */
|
|
88
|
+
exports.GRADE_THRESHOLDS = [
|
|
89
|
+
{ grade: 'A+', min: 98 },
|
|
90
|
+
{ grade: 'A', min: 93 },
|
|
91
|
+
{ grade: 'A-', min: 88 },
|
|
92
|
+
{ grade: 'B+', min: 82 },
|
|
93
|
+
{ grade: 'B', min: 76 },
|
|
94
|
+
{ grade: 'B-', min: 70 },
|
|
95
|
+
{ grade: 'C+', min: 64 },
|
|
96
|
+
{ grade: 'C', min: 58 },
|
|
97
|
+
{ grade: 'C-', min: 52 },
|
|
98
|
+
{ grade: 'D+', min: 45 },
|
|
99
|
+
{ grade: 'D', min: 38 },
|
|
100
|
+
{ grade: 'D-', min: 30 },
|
|
101
|
+
{ grade: 'F', min: -Infinity },
|
|
102
|
+
];
|
|
103
|
+
exports.LANGUAGE_EXTENSIONS = {
|
|
104
|
+
ts: 'typescript',
|
|
105
|
+
mts: 'typescript',
|
|
106
|
+
cts: 'typescript',
|
|
107
|
+
tsx: 'jsx',
|
|
108
|
+
js: 'javascript',
|
|
109
|
+
mjs: 'javascript',
|
|
110
|
+
cjs: 'javascript',
|
|
111
|
+
jsx: 'jsx',
|
|
112
|
+
vue: 'vue',
|
|
113
|
+
svelte: 'svelte',
|
|
114
|
+
html: 'html',
|
|
115
|
+
htm: 'html',
|
|
116
|
+
ejs: 'html',
|
|
117
|
+
handlebars: 'html',
|
|
118
|
+
hbs: 'html',
|
|
119
|
+
css: 'css',
|
|
120
|
+
scss: 'css',
|
|
121
|
+
less: 'css',
|
|
122
|
+
sql: 'sql',
|
|
123
|
+
prisma: 'prisma',
|
|
124
|
+
py: 'python',
|
|
125
|
+
rb: 'ruby',
|
|
126
|
+
php: 'php',
|
|
127
|
+
go: 'go',
|
|
128
|
+
rs: 'rust',
|
|
129
|
+
java: 'java',
|
|
130
|
+
kt: 'java',
|
|
131
|
+
sh: 'shell',
|
|
132
|
+
bash: 'shell',
|
|
133
|
+
zsh: 'shell',
|
|
134
|
+
yml: 'yaml',
|
|
135
|
+
yaml: 'yaml',
|
|
136
|
+
toml: 'toml',
|
|
137
|
+
json: 'json',
|
|
138
|
+
jsonc: 'json',
|
|
139
|
+
json5: 'json',
|
|
140
|
+
map: 'json',
|
|
141
|
+
md: 'markdown',
|
|
142
|
+
mdx: 'markdown',
|
|
143
|
+
markdown: 'markdown',
|
|
144
|
+
env: 'dotenv',
|
|
145
|
+
tf: 'terraform',
|
|
146
|
+
tfvars: 'terraform',
|
|
147
|
+
graphql: 'graphql',
|
|
148
|
+
gql: 'graphql',
|
|
149
|
+
xml: 'xml',
|
|
150
|
+
plist: 'xml',
|
|
151
|
+
csproj: 'xml',
|
|
152
|
+
dockerfile: 'dockerfile',
|
|
153
|
+
};
|
|
154
|
+
/** An empty project context, used by tests and single-file scans. */
|
|
155
|
+
const emptyProjectContext = (rootDir) => ({
|
|
156
|
+
rootDir,
|
|
157
|
+
frameworks: [],
|
|
158
|
+
dependencies: [],
|
|
159
|
+
hasExpressServer: false,
|
|
160
|
+
hasFastifyServer: false,
|
|
161
|
+
hasCookieSession: false,
|
|
162
|
+
hasRateLimiting: false,
|
|
163
|
+
hasCsrfProtection: false,
|
|
164
|
+
hasSanitizer: false,
|
|
165
|
+
hasAuthLibrary: false,
|
|
166
|
+
hasAiSdk: false,
|
|
167
|
+
hasSupabase: false,
|
|
168
|
+
hasStripe: false,
|
|
169
|
+
hasPrisma: false,
|
|
170
|
+
migrationFiles: [],
|
|
171
|
+
sqlTables: [],
|
|
172
|
+
hasGlobalAuthMiddleware: false,
|
|
173
|
+
authMiddlewareMatchers: [],
|
|
174
|
+
trackedPaths: [],
|
|
175
|
+
gitAvailable: false,
|
|
176
|
+
});
|
|
177
|
+
exports.emptyProjectContext = emptyProjectContext;
|
|
178
|
+
const emptySeverityCounts = () => ({
|
|
179
|
+
critical: 0,
|
|
180
|
+
high: 0,
|
|
181
|
+
medium: 0,
|
|
182
|
+
low: 0,
|
|
183
|
+
info: 0,
|
|
184
|
+
});
|
|
185
|
+
exports.emptySeverityCounts = emptySeverityCounts;
|
|
186
|
+
/** Numeric rank so severities can be compared (`critical` = 5). */
|
|
187
|
+
const severityRank = (severity) => exports.SEVERITY_WEIGHT[severity];
|
|
188
|
+
exports.severityRank = severityRank;
|
|
189
|
+
/** True when `severity` is at least as severe as `threshold`. */
|
|
190
|
+
const meetsSeverity = (severity, threshold) => (0, exports.severityRank)(severity) >= (0, exports.severityRank)(threshold);
|
|
191
|
+
exports.meetsSeverity = meetsSeverity;
|
|
192
|
+
const isSeverity = (value) => typeof value === 'string' && exports.SEVERITIES.includes(value);
|
|
193
|
+
exports.isSeverity = isSeverity;
|
|
194
|
+
const isRuleCategory = (value) => typeof value === 'string' && exports.RULE_CATEGORIES.includes(value);
|
|
195
|
+
exports.isRuleCategory = isRuleCategory;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe-shield :: credential signature library
|
|
3
|
+
* -------------------------------------------
|
|
4
|
+
* Vendor-specific secret signatures plus a Shannon-entropy fallback. The same
|
|
5
|
+
* table powers `VSB-SECRET-*` (source scanner) and the agent-skill auditor, so
|
|
6
|
+
* a leaked key is reported identically no matter where it is found.
|
|
7
|
+
*
|
|
8
|
+
* Every regex is *deliberately* written so it cannot match its own source text
|
|
9
|
+
* (that is why character classes appear as `[0-9A-Z]{16}` and never as a
|
|
10
|
+
* literal example key) — vibe-shield scans itself in CI.
|
|
11
|
+
*/
|
|
12
|
+
import type { Severity } from '../scanner/types';
|
|
13
|
+
export type SecretFamily = 'openai' | 'anthropic' | 'aws' | 'stripe' | 'github' | 'supabase' | 'private-key' | 'saas' | 'registry' | 'jwt' | 'database-url' | 'generic';
|
|
14
|
+
export interface SecretPattern {
|
|
15
|
+
/** Stable id used by config overrides, e.g. `openai`. */
|
|
16
|
+
id: string;
|
|
17
|
+
/** Human readable vendor, e.g. `OpenAI`. */
|
|
18
|
+
vendor: string;
|
|
19
|
+
family: SecretFamily;
|
|
20
|
+
severity: Severity;
|
|
21
|
+
/** Global regex; group 1 (when present) holds the credential. */
|
|
22
|
+
regex: RegExp;
|
|
23
|
+
/** Minimum Shannon entropy for the captured value. */
|
|
24
|
+
minEntropy?: number;
|
|
25
|
+
/**
|
|
26
|
+
* The match *is* the finding (PEM headers, connection strings): skip the
|
|
27
|
+
* placeholder/entropy gates, which exist to filter out opaque token bodies.
|
|
28
|
+
*/
|
|
29
|
+
raw?: boolean;
|
|
30
|
+
/** Short note shown in the finding message. */
|
|
31
|
+
note: string;
|
|
32
|
+
}
|
|
33
|
+
export declare const SECRET_PATTERNS: SecretPattern[];
|
|
34
|
+
/** Loose "assignment of a long opaque value" probe used by the generic rule. */
|
|
35
|
+
export declare const GENERIC_ASSIGNMENT_REGEX: RegExp;
|
|
36
|
+
export interface SecretMatch {
|
|
37
|
+
patternId: string;
|
|
38
|
+
vendor: string;
|
|
39
|
+
family: SecretFamily;
|
|
40
|
+
severity: Severity;
|
|
41
|
+
/** The raw credential. Never print this — use `display`. */
|
|
42
|
+
value: string;
|
|
43
|
+
/** Masked value safe for terminals, logs and reports. */
|
|
44
|
+
display: string;
|
|
45
|
+
offset: number;
|
|
46
|
+
length: number;
|
|
47
|
+
entropy: number;
|
|
48
|
+
note: string;
|
|
49
|
+
}
|
|
50
|
+
/** Find every credential in `text`, deduplicating overlapping matches. */
|
|
51
|
+
export declare function findSecrets(text: string, options?: {
|
|
52
|
+
skipGeneric?: boolean;
|
|
53
|
+
}): SecretMatch[];
|
|
54
|
+
/**
|
|
55
|
+
* Decode a JWT payload without verifying it — used to spot `service_role`,
|
|
56
|
+
* `admin` or `exp: never` claims sitting in source code.
|
|
57
|
+
*/
|
|
58
|
+
export declare function decodeJwtPayload(token: string): Record<string, unknown> | null;
|
|
59
|
+
/** True when a JWT carries a privileged role claim. */
|
|
60
|
+
export declare function jwtHasPrivilegedRole(token: string): {
|
|
61
|
+
privileged: boolean;
|
|
62
|
+
role: string | null;
|
|
63
|
+
};
|