@vibefoundry/vibe-shield 0.0.0-stage → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +577 -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 +545 -0
- package/dist/src/index.d.ts +25 -0
- package/dist/src/index.js +108 -0
- package/dist/src/init/scaffold.d.ts +120 -0
- package/dist/src/init/scaffold.js +556 -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 +726 -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 +97 -4
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe-shield :: .gitignore engine
|
|
3
|
+
* --------------------------------
|
|
4
|
+
* A dependency-free implementation of the gitignore specification:
|
|
5
|
+
* negation (`!`), directory-only patterns (`foo/`), anchoring (`/foo`,
|
|
6
|
+
* `a/b`), `**` wildcards, character classes and nested ignore files.
|
|
7
|
+
*
|
|
8
|
+
* Every ignore file is compiled once into an ordered rule list; lookups are
|
|
9
|
+
* O(rules) with an early prefix check, which keeps a 100k-file tree fast.
|
|
10
|
+
*/
|
|
11
|
+
export interface IgnoreRule {
|
|
12
|
+
/** Directory the rule is relative to (POSIX, '' = repo root). */
|
|
13
|
+
base: string;
|
|
14
|
+
regex: RegExp;
|
|
15
|
+
negated: boolean;
|
|
16
|
+
dirOnly: boolean;
|
|
17
|
+
/** Original pattern, used in `--explain-ignores` style debugging. */
|
|
18
|
+
pattern: string;
|
|
19
|
+
/** Source file the rule came from. */
|
|
20
|
+
source: string;
|
|
21
|
+
}
|
|
22
|
+
/** Compile a single gitignore pattern into a rule. Returns null for comments/blanks. */
|
|
23
|
+
export declare function compileIgnorePattern(rawPattern: string, base: string, source: string): IgnoreRule | null;
|
|
24
|
+
/** Parse the text of an ignore file into rules. */
|
|
25
|
+
export declare function parseIgnoreFile(text: string, base: string, source: string): IgnoreRule[];
|
|
26
|
+
export interface IgnoreMatcherOptions {
|
|
27
|
+
/** Always-ignored directory names, regardless of ignore files. */
|
|
28
|
+
alwaysIgnore?: string[];
|
|
29
|
+
/** Extra CLI/config globs (treated as ignore rules rooted at the scan root). */
|
|
30
|
+
extraPatterns?: string[];
|
|
31
|
+
}
|
|
32
|
+
/** Ordered, immutable ignore rule set for one scan. */
|
|
33
|
+
export declare class IgnoreMatcher {
|
|
34
|
+
private readonly rules;
|
|
35
|
+
private readonly alwaysIgnore;
|
|
36
|
+
constructor(options?: IgnoreMatcherOptions);
|
|
37
|
+
/** Add rules from an ignore file located at `base` (POSIX, '' for root). */
|
|
38
|
+
addIgnoreFile(text: string, base: string, source: string): void;
|
|
39
|
+
addRule(pattern: string, base?: string, source?: string): void;
|
|
40
|
+
get ruleCount(): number;
|
|
41
|
+
/** Result of a lookup: ignored, kept, or undecided (no rule matched). */
|
|
42
|
+
private match;
|
|
43
|
+
/**
|
|
44
|
+
* True when `relPath` (POSIX, relative to the scan root) must be skipped.
|
|
45
|
+
* Implements the git rule that a file inside an excluded directory can never
|
|
46
|
+
* be re-included.
|
|
47
|
+
*/
|
|
48
|
+
isIgnored(relPath: string, isDirectory: boolean): boolean;
|
|
49
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vibe-shield :: .gitignore engine
|
|
4
|
+
* --------------------------------
|
|
5
|
+
* A dependency-free implementation of the gitignore specification:
|
|
6
|
+
* negation (`!`), directory-only patterns (`foo/`), anchoring (`/foo`,
|
|
7
|
+
* `a/b`), `**` wildcards, character classes and nested ignore files.
|
|
8
|
+
*
|
|
9
|
+
* Every ignore file is compiled once into an ordered rule list; lookups are
|
|
10
|
+
* O(rules) with an early prefix check, which keeps a 100k-file tree fast.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.IgnoreMatcher = void 0;
|
|
14
|
+
exports.compileIgnorePattern = compileIgnorePattern;
|
|
15
|
+
exports.parseIgnoreFile = parseIgnoreFile;
|
|
16
|
+
const globToRegExpSource = (pattern) => {
|
|
17
|
+
let source = '';
|
|
18
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
19
|
+
const char = pattern[i];
|
|
20
|
+
if (char === '*') {
|
|
21
|
+
if (pattern[i + 1] === '*') {
|
|
22
|
+
// consume the whole `**` run
|
|
23
|
+
let j = i;
|
|
24
|
+
while (pattern[j] === '*')
|
|
25
|
+
j += 1;
|
|
26
|
+
const followedBySlash = pattern[j] === '/';
|
|
27
|
+
const precededBySlash = i === 0 || pattern[i - 1] === '/';
|
|
28
|
+
if (precededBySlash && followedBySlash) {
|
|
29
|
+
// `a/**/b` -> zero or more directories
|
|
30
|
+
source += '(?:[^/]+/)*';
|
|
31
|
+
i = j; // skip the trailing slash, it is part of the group
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (precededBySlash && j === pattern.length) {
|
|
35
|
+
// `a/**` -> everything below a
|
|
36
|
+
source += '.*';
|
|
37
|
+
i = j - 1;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (i === 0 && followedBySlash) {
|
|
41
|
+
// `**/foo` -> foo at any depth
|
|
42
|
+
source += '(?:.*/)?';
|
|
43
|
+
i = j;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
source += '.*';
|
|
47
|
+
i = j - 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
source += '[^/]*';
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (char === '?') {
|
|
54
|
+
source += '[^/]';
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (char === '[') {
|
|
58
|
+
const end = pattern.indexOf(']', i + 1);
|
|
59
|
+
if (end !== -1) {
|
|
60
|
+
let body = pattern.slice(i + 1, end);
|
|
61
|
+
if (body.startsWith('!') || body.startsWith('^'))
|
|
62
|
+
body = `^${body.slice(1)}`;
|
|
63
|
+
source += `[${body.replace(/\\/g, '\\\\')}]`;
|
|
64
|
+
i = end;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
source += '\\[';
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (char === '\\') {
|
|
71
|
+
const nxt = pattern[i + 1];
|
|
72
|
+
if (nxt !== undefined) {
|
|
73
|
+
source += nxt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
74
|
+
i += 1;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
source += char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
79
|
+
}
|
|
80
|
+
return source;
|
|
81
|
+
};
|
|
82
|
+
/** Compile a single gitignore pattern into a rule. Returns null for comments/blanks. */
|
|
83
|
+
function compileIgnorePattern(rawPattern, base, source) {
|
|
84
|
+
let pattern = rawPattern.replace(/\s+$/, '');
|
|
85
|
+
if (!pattern.length || pattern.startsWith('#'))
|
|
86
|
+
return null;
|
|
87
|
+
let negated = false;
|
|
88
|
+
if (pattern.startsWith('!')) {
|
|
89
|
+
negated = true;
|
|
90
|
+
pattern = pattern.slice(1);
|
|
91
|
+
}
|
|
92
|
+
else if (pattern.startsWith('\\#') || pattern.startsWith('\\!')) {
|
|
93
|
+
pattern = pattern.slice(1);
|
|
94
|
+
}
|
|
95
|
+
if (!pattern.length)
|
|
96
|
+
return null;
|
|
97
|
+
let dirOnly = false;
|
|
98
|
+
if (pattern.endsWith('/')) {
|
|
99
|
+
dirOnly = true;
|
|
100
|
+
pattern = pattern.slice(0, -1);
|
|
101
|
+
}
|
|
102
|
+
if (!pattern.length)
|
|
103
|
+
return null;
|
|
104
|
+
const anchored = pattern.startsWith('/') || pattern.slice(0, -1).includes('/');
|
|
105
|
+
if (pattern.startsWith('/'))
|
|
106
|
+
pattern = pattern.slice(1);
|
|
107
|
+
if (!pattern.length)
|
|
108
|
+
return null;
|
|
109
|
+
const body = globToRegExpSource(pattern);
|
|
110
|
+
const prefix = anchored ? '' : '(?:.*/)?';
|
|
111
|
+
const regex = new RegExp(`^${prefix}${body}${dirOnly ? '(?:/.*)?' : '(?:/.*)?'}$`);
|
|
112
|
+
return { base, regex, negated, dirOnly, pattern: rawPattern, source };
|
|
113
|
+
}
|
|
114
|
+
/** Parse the text of an ignore file into rules. */
|
|
115
|
+
function parseIgnoreFile(text, base, source) {
|
|
116
|
+
const rules = [];
|
|
117
|
+
for (const line of text.split(/\r?\n/)) {
|
|
118
|
+
const rule = compileIgnorePattern(line, base, source);
|
|
119
|
+
if (rule)
|
|
120
|
+
rules.push(rule);
|
|
121
|
+
}
|
|
122
|
+
return rules;
|
|
123
|
+
}
|
|
124
|
+
/** Ordered, immutable ignore rule set for one scan. */
|
|
125
|
+
class IgnoreMatcher {
|
|
126
|
+
rules = [];
|
|
127
|
+
alwaysIgnore;
|
|
128
|
+
constructor(options = {}) {
|
|
129
|
+
this.alwaysIgnore = new Set(options.alwaysIgnore ?? ['.git']);
|
|
130
|
+
for (const pattern of options.extraPatterns ?? []) {
|
|
131
|
+
const rule = compileIgnorePattern(pattern, '', '<cli>');
|
|
132
|
+
if (rule)
|
|
133
|
+
this.rules.push(rule);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Add rules from an ignore file located at `base` (POSIX, '' for root). */
|
|
137
|
+
addIgnoreFile(text, base, source) {
|
|
138
|
+
this.rules.push(...parseIgnoreFile(text, base, source));
|
|
139
|
+
}
|
|
140
|
+
addRule(pattern, base = '', source = '<config>') {
|
|
141
|
+
const rule = compileIgnorePattern(pattern, base, source);
|
|
142
|
+
if (rule)
|
|
143
|
+
this.rules.push(rule);
|
|
144
|
+
}
|
|
145
|
+
get ruleCount() {
|
|
146
|
+
return this.rules.length;
|
|
147
|
+
}
|
|
148
|
+
/** Result of a lookup: ignored, kept, or undecided (no rule matched). */
|
|
149
|
+
match(relPath, isDirectory) {
|
|
150
|
+
let result = null;
|
|
151
|
+
for (const rule of this.rules) {
|
|
152
|
+
if (rule.base) {
|
|
153
|
+
if (!relPath.startsWith(`${rule.base}/`))
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const scoped = rule.base ? relPath.slice(rule.base.length + 1) : relPath;
|
|
157
|
+
if (!scoped.length)
|
|
158
|
+
continue;
|
|
159
|
+
if (rule.dirOnly && !isDirectory) {
|
|
160
|
+
// A directory-only rule still excludes everything below that directory.
|
|
161
|
+
const segments = scoped.split('/');
|
|
162
|
+
let matchedParent = false;
|
|
163
|
+
for (let i = 1; i < segments.length; i += 1) {
|
|
164
|
+
if (rule.regex.test(segments.slice(0, i).join('/'))) {
|
|
165
|
+
matchedParent = true;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (!matchedParent)
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
else if (!rule.regex.test(scoped)) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
result = !rule.negated;
|
|
176
|
+
}
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* True when `relPath` (POSIX, relative to the scan root) must be skipped.
|
|
181
|
+
* Implements the git rule that a file inside an excluded directory can never
|
|
182
|
+
* be re-included.
|
|
183
|
+
*/
|
|
184
|
+
isIgnored(relPath, isDirectory) {
|
|
185
|
+
const segments = relPath.split('/');
|
|
186
|
+
if (segments.some((segment) => this.alwaysIgnore.has(segment)))
|
|
187
|
+
return true;
|
|
188
|
+
for (let i = 1; i < segments.length; i += 1) {
|
|
189
|
+
if (this.match(segments.slice(0, i).join('/'), true) === true)
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return this.match(relPath, isDirectory) === true;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exports.IgnoreMatcher = IgnoreMatcher;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe-shield :: detection lexicon
|
|
3
|
+
* --------------------------------
|
|
4
|
+
* The vocabulary every rule agrees on: what counts as "authentication was
|
|
5
|
+
* verified", what counts as "this route mutates state", which helpers count as
|
|
6
|
+
* sanitisation, and which identifiers are attacker-controlled.
|
|
7
|
+
*
|
|
8
|
+
* Centralising it means adding support for a new auth library (Auth.js, Clerk,
|
|
9
|
+
* Lucia, Supabase SSR, Passport, Firebase, Kinvey...) is a one-line change
|
|
10
|
+
* that instantly improves six different rules.
|
|
11
|
+
*/
|
|
12
|
+
/** Server-side session / identity verification helpers. */
|
|
13
|
+
export declare const AUTH_VERIFICATION_TOKENS: readonly ["getServerSession", "getSession", "auth()", "auth(req", "await auth", "getAuth", "authOptions", "withAuth", "withPageAuth", "withApiAuth", "getServerSideSession", "currentUser", "auth().userId", "clerkClient", "getAuth(", "requireAuth", "protect", "verifyToken", "validateSession", "getSessionUser", "sessionUser", "requireUser", "requireLogin", "requireAdmin", "assertAuthenticated", "assertUser", "isAuthenticated", "authenticate", "authenticated", "verifySession", "resolveSession", "readSessionCookie", "parseSessionToken", "getUser()", "supabase.auth.getUser", "auth.getUser", "createServerClient", "createRouteHandlerClient", "getSessionFromRequest", "passport.authenticate", "ensureLoggedIn", "ensureAuthenticated", "req.isAuthenticated", "req.user", "request.user", "ctx.state.user", "c.get('user')", "c.var.user", "verifyIdToken", "getAuth().verifyIdToken", "jwt.verify", "jwtVerify", "jsonwebtoken.verify", "decodeJwt", "validateJwt", "session.user", "session.userId", "user.id", "userId = session", "bearerToken", "authorizationHeader", "checkPermission", "hasPermission", "hasRole", "can(", "authorize(", "guardRoute", "routeGuard", "middlewareAuth", "authMiddleware", "authGuard"];
|
|
14
|
+
/** Compiled matcher: does this file verify the caller's identity? */
|
|
15
|
+
export declare const AUTH_VERIFICATION_REGEX: RegExp;
|
|
16
|
+
/** Middleware names commonly mounted in front of a route handler. */
|
|
17
|
+
export declare const ROUTE_AUTH_MIDDLEWARE_REGEX: RegExp;
|
|
18
|
+
/** State-mutating operations (database writes, HTTP mutations, fs writes). */
|
|
19
|
+
export declare const MUTATION_TOKENS: readonly [".insert(", ".upsert(", ".update(", ".updateMany(", ".delete(", ".deleteMany(", ".destroy(", ".remove(", ".create(", ".createMany(", ".save(", ".patch(", ".write(", ".rpc(", ".execute(", "INSERT INTO", "UPDATE ", "DELETE FROM", "DROP TABLE", "ALTER TABLE", "TRUNCATE", "method: 'POST'", "method: \"POST\"", "method: 'PUT'", "method: \"PUT\"", "method: 'PATCH'", "method: \"PATCH\"", "method: 'DELETE'", "method: \"DELETE\"", "writeFile", "appendFile", "unlink", "mkdir", "rm(", "rmdir", "rename", "send(", "transfer(", "checkout.sessions.create", "refunds.create", "publish(", "emit("];
|
|
20
|
+
export declare const MUTATION_REGEX: RegExp;
|
|
21
|
+
/** SQL mutation verbs (uppercase-insensitive, used by the SQL rules). */
|
|
22
|
+
export declare const SQL_MUTATION_REGEX: RegExp;
|
|
23
|
+
/** Libraries / helpers that mark output as sanitised. */
|
|
24
|
+
export declare const SANITIZER_TOKENS: string[];
|
|
25
|
+
export declare const SANITIZER_REGEX: RegExp;
|
|
26
|
+
/** Values that are provably constant (and therefore safe) inside a sink. */
|
|
27
|
+
export declare const CONSTANT_VALUE_REGEX: RegExp;
|
|
28
|
+
/** LLM provider / SDK markers. */
|
|
29
|
+
export declare const LLM_TOKENS: string[];
|
|
30
|
+
export declare const LLM_REGEX: RegExp;
|
|
31
|
+
/** Identifiers whose value is controlled by the caller of an HTTP endpoint. */
|
|
32
|
+
export declare const UNTRUSTED_SOURCES: string[];
|
|
33
|
+
export declare const UNTRUSTED_SOURCE_REGEX: RegExp;
|
|
34
|
+
/** Object-identity keys that must come from the session, never the request. */
|
|
35
|
+
export declare const OWNERSHIP_KEYS: string[];
|
|
36
|
+
export declare const OWNERSHIP_KEY_REGEX: RegExp;
|
|
37
|
+
/** Session-derived ownership expressions (the safe counterpart). */
|
|
38
|
+
export declare const SESSION_OWNERSHIP_REGEX: RegExp;
|
|
39
|
+
/** Role / permission checks that protect administrative surfaces. */
|
|
40
|
+
export declare const ROLE_CHECK_REGEX: RegExp;
|
|
41
|
+
/** CSRF protection markers. */
|
|
42
|
+
export declare const CSRF_REGEX: RegExp;
|
|
43
|
+
/** Rate limiting markers. */
|
|
44
|
+
export declare const RATE_LIMIT_REGEX: RegExp;
|
|
45
|
+
/** Client-only environment variable prefixes that get inlined into bundles. */
|
|
46
|
+
export declare const PUBLIC_ENV_PREFIXES: readonly ["NEXT_PUBLIC_", "VITE_", "PUBLIC_", "NUXT_PUBLIC_", "SVELTEKIT_PUBLIC_", "REACT_APP_", "GATSBY_", "EXPO_PUBLIC_", "RSLIB_PUBLIC_", "RSBUILD_PUBLIC_", "WXT_PUBLIC_", "ANGULAR_"];
|
|
47
|
+
export declare const PUBLIC_ENV_PREFIX_REGEX: RegExp;
|
|
48
|
+
/**
|
|
49
|
+
* Names that must never appear behind a public prefix. Matched
|
|
50
|
+
* case-insensitively against the *suffix* of the variable name.
|
|
51
|
+
*/
|
|
52
|
+
export declare const SERVER_ONLY_SECRET_NAMES: readonly ["SERVICE_ROLE_KEY", "SERVICE_ROLE", "SERVICE_KEY", "SECRET_KEY", "SECRETS", "SECRET", "PRIVATE_KEY", "PRIVKEY", "API_KEY", "APIKEY", "ACCESS_KEY", "ACCESS_TOKEN", "AUTH_TOKEN", "ADMIN_KEY", "DATABASE_URL", "DB_URL", "DB_PASSWORD", "POSTGRES_PASSWORD", "CONNECTION_STRING", "PASSWORD", "PASSWD", "PWD", "SIGNING_KEY", "JWT_SECRET", "ENCRYPTION_KEY", "MASTER_KEY", "WEBHOOK_SECRET", "WHSEC", "SESSION_SECRET", "CLIENT_SECRET", "MNEMONIC", "SEED", "TOKEN"];
|
|
53
|
+
/** Names that are *expected* to be public even though they contain a keyword. */
|
|
54
|
+
export declare const PUBLIC_SAFE_NAME_SUFFIXES: string[];
|
|
55
|
+
/** Well-known server-only providers that must never reach the browser. */
|
|
56
|
+
export declare const SERVER_ONLY_PROVIDERS: readonly ["OPENAI_API_KEY", "ANTHROPIC_API_KEY", "STRIPE_SECRET_KEY", "STRIPE_WEBHOOK_SECRET", "SUPABASE_SERVICE_ROLE_KEY", "SUPABASE_SECRET_KEY", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN", "GITHUB_TOKEN", "DATABASE_URL", "POSTGRES_URL", "SENDGRID_API_KEY", "TWILIO_AUTH_TOKEN", "RESEND_API_KEY", "REPLICATE_API_TOKEN", "HF_TOKEN", "GOOGLE_API_KEY", "AZURE_OPENAI_API_KEY", "JWT_SECRET", "SESSION_SECRET", "ENCRYPTION_KEY"];
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* vibe-shield :: detection lexicon
|
|
4
|
+
* --------------------------------
|
|
5
|
+
* The vocabulary every rule agrees on: what counts as "authentication was
|
|
6
|
+
* verified", what counts as "this route mutates state", which helpers count as
|
|
7
|
+
* sanitisation, and which identifiers are attacker-controlled.
|
|
8
|
+
*
|
|
9
|
+
* Centralising it means adding support for a new auth library (Auth.js, Clerk,
|
|
10
|
+
* Lucia, Supabase SSR, Passport, Firebase, Kinvey...) is a one-line change
|
|
11
|
+
* that instantly improves six different rules.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.SERVER_ONLY_PROVIDERS = exports.PUBLIC_SAFE_NAME_SUFFIXES = exports.SERVER_ONLY_SECRET_NAMES = exports.PUBLIC_ENV_PREFIX_REGEX = exports.PUBLIC_ENV_PREFIXES = exports.RATE_LIMIT_REGEX = exports.CSRF_REGEX = exports.ROLE_CHECK_REGEX = exports.SESSION_OWNERSHIP_REGEX = exports.OWNERSHIP_KEY_REGEX = exports.OWNERSHIP_KEYS = exports.UNTRUSTED_SOURCE_REGEX = exports.UNTRUSTED_SOURCES = exports.LLM_REGEX = exports.LLM_TOKENS = exports.CONSTANT_VALUE_REGEX = exports.SANITIZER_REGEX = exports.SANITIZER_TOKENS = exports.SQL_MUTATION_REGEX = exports.MUTATION_REGEX = exports.MUTATION_TOKENS = exports.ROUTE_AUTH_MIDDLEWARE_REGEX = exports.AUTH_VERIFICATION_REGEX = exports.AUTH_VERIFICATION_TOKENS = void 0;
|
|
15
|
+
/** Server-side session / identity verification helpers. */
|
|
16
|
+
exports.AUTH_VERIFICATION_TOKENS = [
|
|
17
|
+
// NextAuth / Auth.js
|
|
18
|
+
'getServerSession',
|
|
19
|
+
'getSession',
|
|
20
|
+
'auth()',
|
|
21
|
+
'auth(req',
|
|
22
|
+
'await auth',
|
|
23
|
+
'getAuth',
|
|
24
|
+
'authOptions',
|
|
25
|
+
'withAuth',
|
|
26
|
+
'withPageAuth',
|
|
27
|
+
'withApiAuth',
|
|
28
|
+
'getServerSideSession',
|
|
29
|
+
// Clerk
|
|
30
|
+
'currentUser',
|
|
31
|
+
'auth().userId',
|
|
32
|
+
'clerkClient',
|
|
33
|
+
'getAuth(',
|
|
34
|
+
'requireAuth',
|
|
35
|
+
'protect',
|
|
36
|
+
'verifyToken',
|
|
37
|
+
// Lucia / custom
|
|
38
|
+
'validateSession',
|
|
39
|
+
'getSessionUser',
|
|
40
|
+
'sessionUser',
|
|
41
|
+
'requireUser',
|
|
42
|
+
'requireLogin',
|
|
43
|
+
'requireAdmin',
|
|
44
|
+
'assertAuthenticated',
|
|
45
|
+
'assertUser',
|
|
46
|
+
'isAuthenticated',
|
|
47
|
+
'authenticate',
|
|
48
|
+
'authenticated',
|
|
49
|
+
'verifySession',
|
|
50
|
+
'resolveSession',
|
|
51
|
+
'readSessionCookie',
|
|
52
|
+
'parseSessionToken',
|
|
53
|
+
// Supabase
|
|
54
|
+
'getUser()',
|
|
55
|
+
'supabase.auth.getUser',
|
|
56
|
+
'auth.getUser',
|
|
57
|
+
'createServerClient',
|
|
58
|
+
'createRouteHandlerClient',
|
|
59
|
+
'getSessionFromRequest',
|
|
60
|
+
// Passport / Express
|
|
61
|
+
'passport.authenticate',
|
|
62
|
+
'ensureLoggedIn',
|
|
63
|
+
'ensureAuthenticated',
|
|
64
|
+
'req.isAuthenticated',
|
|
65
|
+
'req.user',
|
|
66
|
+
'request.user',
|
|
67
|
+
'ctx.state.user',
|
|
68
|
+
'c.get(\'user\')',
|
|
69
|
+
'c.var.user',
|
|
70
|
+
// Firebase / JWT
|
|
71
|
+
'verifyIdToken',
|
|
72
|
+
'getAuth().verifyIdToken',
|
|
73
|
+
'jwt.verify',
|
|
74
|
+
'jwtVerify',
|
|
75
|
+
'jsonwebtoken.verify',
|
|
76
|
+
'decodeJwt',
|
|
77
|
+
'validateJwt',
|
|
78
|
+
'session.user',
|
|
79
|
+
'session.userId',
|
|
80
|
+
'user.id',
|
|
81
|
+
'userId = session',
|
|
82
|
+
'bearerToken',
|
|
83
|
+
'authorizationHeader',
|
|
84
|
+
// Generic guard helpers
|
|
85
|
+
'checkPermission',
|
|
86
|
+
'hasPermission',
|
|
87
|
+
'hasRole',
|
|
88
|
+
'can(',
|
|
89
|
+
'authorize(',
|
|
90
|
+
'guardRoute',
|
|
91
|
+
'routeGuard',
|
|
92
|
+
'middlewareAuth',
|
|
93
|
+
'authMiddleware',
|
|
94
|
+
'authGuard',
|
|
95
|
+
];
|
|
96
|
+
/** Compiled matcher: does this file verify the caller's identity? */
|
|
97
|
+
exports.AUTH_VERIFICATION_REGEX = new RegExp(exports.AUTH_VERIFICATION_TOKENS.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i');
|
|
98
|
+
/** Middleware names commonly mounted in front of a route handler. */
|
|
99
|
+
exports.ROUTE_AUTH_MIDDLEWARE_REGEX = /\b(?:require|ensure|verify|check|assert|protect|guard)[A-Za-z]*(?:Auth|User|Session|Login|Token|Role|Admin|Permission)[A-Za-z]*\b|\b(?:isAuthenticated|authenticate|authMiddleware|authGuard|withAuth|passport\.authenticate)\b/i;
|
|
100
|
+
/** State-mutating operations (database writes, HTTP mutations, fs writes). */
|
|
101
|
+
exports.MUTATION_TOKENS = [
|
|
102
|
+
'.insert(',
|
|
103
|
+
'.upsert(',
|
|
104
|
+
'.update(',
|
|
105
|
+
'.updateMany(',
|
|
106
|
+
'.delete(',
|
|
107
|
+
'.deleteMany(',
|
|
108
|
+
'.destroy(',
|
|
109
|
+
'.remove(',
|
|
110
|
+
'.create(',
|
|
111
|
+
'.createMany(',
|
|
112
|
+
'.save(',
|
|
113
|
+
'.patch(',
|
|
114
|
+
'.write(',
|
|
115
|
+
'.rpc(',
|
|
116
|
+
'.execute(',
|
|
117
|
+
'INSERT INTO',
|
|
118
|
+
'UPDATE ',
|
|
119
|
+
'DELETE FROM',
|
|
120
|
+
'DROP TABLE',
|
|
121
|
+
'ALTER TABLE',
|
|
122
|
+
'TRUNCATE',
|
|
123
|
+
'method: \'POST\'',
|
|
124
|
+
'method: "POST"',
|
|
125
|
+
'method: \'PUT\'',
|
|
126
|
+
'method: "PUT"',
|
|
127
|
+
'method: \'PATCH\'',
|
|
128
|
+
'method: "PATCH"',
|
|
129
|
+
'method: \'DELETE\'',
|
|
130
|
+
'method: "DELETE"',
|
|
131
|
+
'writeFile',
|
|
132
|
+
'appendFile',
|
|
133
|
+
'unlink',
|
|
134
|
+
'mkdir',
|
|
135
|
+
'rm(',
|
|
136
|
+
'rmdir',
|
|
137
|
+
'rename',
|
|
138
|
+
'send(',
|
|
139
|
+
'transfer(',
|
|
140
|
+
'checkout.sessions.create',
|
|
141
|
+
'refunds.create',
|
|
142
|
+
'publish(',
|
|
143
|
+
'emit(',
|
|
144
|
+
];
|
|
145
|
+
exports.MUTATION_REGEX = new RegExp(exports.MUTATION_TOKENS.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i');
|
|
146
|
+
/** SQL mutation verbs (uppercase-insensitive, used by the SQL rules). */
|
|
147
|
+
exports.SQL_MUTATION_REGEX = /\b(?:INSERT\s+INTO|UPDATE\s+[`"'\w.]+\s+SET|DELETE\s+FROM|DROP\s+TABLE|TRUNCATE\s+TABLE|ALTER\s+TABLE|CREATE\s+POLICY|GRANT\s+|MERGE\s+INTO)\b/i;
|
|
148
|
+
/** Libraries / helpers that mark output as sanitised. */
|
|
149
|
+
exports.SANITIZER_TOKENS = [
|
|
150
|
+
'DOMPurify',
|
|
151
|
+
'dompurify',
|
|
152
|
+
'sanitizeHtml',
|
|
153
|
+
'sanitize-html',
|
|
154
|
+
'xss(',
|
|
155
|
+
'isomorphic-dompurify',
|
|
156
|
+
'escapeHtml',
|
|
157
|
+
'escape(',
|
|
158
|
+
'he.encode',
|
|
159
|
+
'encodeHTML',
|
|
160
|
+
'htmlEntities',
|
|
161
|
+
'bleach.clean',
|
|
162
|
+
'marked.parse',
|
|
163
|
+
'remark',
|
|
164
|
+
'rehype-sanitize',
|
|
165
|
+
'sanitize(',
|
|
166
|
+
'sanitizeMarkdown',
|
|
167
|
+
'purify.sanitize',
|
|
168
|
+
];
|
|
169
|
+
exports.SANITIZER_REGEX = new RegExp(exports.SANITIZER_TOKENS.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'));
|
|
170
|
+
/** Values that are provably constant (and therefore safe) inside a sink. */
|
|
171
|
+
exports.CONSTANT_VALUE_REGEX = /^(['"`][^'"`${]*['"`]|\d+(?:\.\d+)?|true|false|null|undefined|\[\]|\{\}|i18n|t\(|__\(|env\.[A-Z0-9_]+)$/;
|
|
172
|
+
/** LLM provider / SDK markers. */
|
|
173
|
+
exports.LLM_TOKENS = [
|
|
174
|
+
'openai',
|
|
175
|
+
'OpenAI(',
|
|
176
|
+
'anthropic',
|
|
177
|
+
'Anthropic(',
|
|
178
|
+
'generateText',
|
|
179
|
+
'streamText',
|
|
180
|
+
'generateObject',
|
|
181
|
+
'streamObject',
|
|
182
|
+
'tool(',
|
|
183
|
+
'chat.completions.create',
|
|
184
|
+
'messages.create',
|
|
185
|
+
'llm.',
|
|
186
|
+
'groq',
|
|
187
|
+
'mistral',
|
|
188
|
+
'cohere',
|
|
189
|
+
'google/generative-ai',
|
|
190
|
+
'bedrock',
|
|
191
|
+
'langchain',
|
|
192
|
+
'ollama',
|
|
193
|
+
'together.ai',
|
|
194
|
+
'xai',
|
|
195
|
+
'completion',
|
|
196
|
+
'prompt',
|
|
197
|
+
];
|
|
198
|
+
exports.LLM_REGEX = new RegExp(exports.LLM_TOKENS.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'), 'i');
|
|
199
|
+
/** Identifiers whose value is controlled by the caller of an HTTP endpoint. */
|
|
200
|
+
exports.UNTRUSTED_SOURCES = [
|
|
201
|
+
'req.body',
|
|
202
|
+
'req.query',
|
|
203
|
+
'req.params',
|
|
204
|
+
'request.body',
|
|
205
|
+
'request.query',
|
|
206
|
+
'request.params',
|
|
207
|
+
'request.json()',
|
|
208
|
+
'req.json()',
|
|
209
|
+
'searchParams',
|
|
210
|
+
'formData',
|
|
211
|
+
'req.headers',
|
|
212
|
+
'request.headers',
|
|
213
|
+
'cookies()',
|
|
214
|
+
'c.req.',
|
|
215
|
+
'ctx.request.body',
|
|
216
|
+
'ctx.params',
|
|
217
|
+
'event.body',
|
|
218
|
+
'event.queryStringParameters',
|
|
219
|
+
'useBody',
|
|
220
|
+
'readBody',
|
|
221
|
+
'getQuery',
|
|
222
|
+
'body.',
|
|
223
|
+
'payload',
|
|
224
|
+
'input.',
|
|
225
|
+
'args.',
|
|
226
|
+
'variables.',
|
|
227
|
+
];
|
|
228
|
+
exports.UNTRUSTED_SOURCE_REGEX = new RegExp(exports.UNTRUSTED_SOURCES.map((token) => token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|'));
|
|
229
|
+
/** Object-identity keys that must come from the session, never the request. */
|
|
230
|
+
exports.OWNERSHIP_KEYS = [
|
|
231
|
+
'userId',
|
|
232
|
+
'user_id',
|
|
233
|
+
'uid',
|
|
234
|
+
'accountId',
|
|
235
|
+
'account_id',
|
|
236
|
+
'customerId',
|
|
237
|
+
'customer_id',
|
|
238
|
+
'tenantId',
|
|
239
|
+
'tenant_id',
|
|
240
|
+
'organizationId',
|
|
241
|
+
'organization_id',
|
|
242
|
+
'orgId',
|
|
243
|
+
'org_id',
|
|
244
|
+
'teamId',
|
|
245
|
+
'team_id',
|
|
246
|
+
'workspaceId',
|
|
247
|
+
'workspace_id',
|
|
248
|
+
'projectId',
|
|
249
|
+
'project_id',
|
|
250
|
+
'ownerId',
|
|
251
|
+
'owner_id',
|
|
252
|
+
'authorId',
|
|
253
|
+
'author_id',
|
|
254
|
+
'profileId',
|
|
255
|
+
'profile_id',
|
|
256
|
+
];
|
|
257
|
+
exports.OWNERSHIP_KEY_REGEX = new RegExp(`\\b(${exports.OWNERSHIP_KEYS.join('|')})\\b`);
|
|
258
|
+
/** Session-derived ownership expressions (the safe counterpart). */
|
|
259
|
+
exports.SESSION_OWNERSHIP_REGEX = /(?:session|auth|user|currentUser|identity|principal|claims|jwt|token|viewer|me)\s*(?:\.\s*(?:user|data|identity|payload|claims|subject))?\s*\.\s*(?:id|userId|user_id|uid|sub|accountId|customerId|tenantId|orgId|organizationId|teamId|workspaceId|email)\b/i;
|
|
260
|
+
/** Role / permission checks that protect administrative surfaces. */
|
|
261
|
+
exports.ROLE_CHECK_REGEX = /\b(?:isAdmin|is_admin|isOwner|is_owner|isStaff|is_staff|isSuperuser|is_superuser|hasRole|requireRole|requireAdmin|checkRole|assertRole|role\s*===?\s*['"`](?:admin|owner|superadmin|super_admin|staff|root|manager)['"`]|roles?\.\s*includes|permissions?\.\s*includes|hasPermission|checkPermission|can\(['"`]|isAllowedTo|claim\.|app_metadata|user_metadata\.role|plan\s*===?\s*['"`](?:pro|team|enterprise)['"`])\b/i;
|
|
262
|
+
/** CSRF protection markers. */
|
|
263
|
+
exports.CSRF_REGEX = /\bcsrf|xsrf|_token\s*===|doubleSubmit|sameSite\s*[:=]\s*['"`]?(?:strict|lax)|synchronizerToken/i;
|
|
264
|
+
/** Rate limiting markers. */
|
|
265
|
+
exports.RATE_LIMIT_REGEX = /\brateLimit|rate_limit|ratelimiter|upstash|@upstash\/ratelimit|express-rate-limit|slowdown|throttle|tokenBucket|leakyBucket|kv\.incr|redis.*incr|tooManyRequests|429\b/i;
|
|
266
|
+
/** Client-only environment variable prefixes that get inlined into bundles. */
|
|
267
|
+
exports.PUBLIC_ENV_PREFIXES = [
|
|
268
|
+
'NEXT_PUBLIC_',
|
|
269
|
+
'VITE_',
|
|
270
|
+
'PUBLIC_',
|
|
271
|
+
'NUXT_PUBLIC_',
|
|
272
|
+
'SVELTEKIT_PUBLIC_',
|
|
273
|
+
'REACT_APP_',
|
|
274
|
+
'GATSBY_',
|
|
275
|
+
'EXPO_PUBLIC_',
|
|
276
|
+
'RSLIB_PUBLIC_',
|
|
277
|
+
'RSBUILD_PUBLIC_',
|
|
278
|
+
'WXT_PUBLIC_',
|
|
279
|
+
'ANGULAR_',
|
|
280
|
+
];
|
|
281
|
+
exports.PUBLIC_ENV_PREFIX_REGEX = new RegExp(`\\b(${exports.PUBLIC_ENV_PREFIXES.join('|')})`, 'g');
|
|
282
|
+
/**
|
|
283
|
+
* Names that must never appear behind a public prefix. Matched
|
|
284
|
+
* case-insensitively against the *suffix* of the variable name.
|
|
285
|
+
*/
|
|
286
|
+
exports.SERVER_ONLY_SECRET_NAMES = [
|
|
287
|
+
'SERVICE_ROLE_KEY',
|
|
288
|
+
'SERVICE_ROLE',
|
|
289
|
+
'SERVICE_KEY',
|
|
290
|
+
'SECRET_KEY',
|
|
291
|
+
'SECRETS',
|
|
292
|
+
'SECRET',
|
|
293
|
+
'PRIVATE_KEY',
|
|
294
|
+
'PRIVKEY',
|
|
295
|
+
'API_KEY',
|
|
296
|
+
'APIKEY',
|
|
297
|
+
'ACCESS_KEY',
|
|
298
|
+
'ACCESS_TOKEN',
|
|
299
|
+
'AUTH_TOKEN',
|
|
300
|
+
'ADMIN_KEY',
|
|
301
|
+
'DATABASE_URL',
|
|
302
|
+
'DB_URL',
|
|
303
|
+
'DB_PASSWORD',
|
|
304
|
+
'POSTGRES_PASSWORD',
|
|
305
|
+
'CONNECTION_STRING',
|
|
306
|
+
'PASSWORD',
|
|
307
|
+
'PASSWD',
|
|
308
|
+
'PWD',
|
|
309
|
+
'SIGNING_KEY',
|
|
310
|
+
'JWT_SECRET',
|
|
311
|
+
'ENCRYPTION_KEY',
|
|
312
|
+
'MASTER_KEY',
|
|
313
|
+
'WEBHOOK_SECRET',
|
|
314
|
+
'WHSEC',
|
|
315
|
+
'SESSION_SECRET',
|
|
316
|
+
'CLIENT_SECRET',
|
|
317
|
+
'MNEMONIC',
|
|
318
|
+
'SEED',
|
|
319
|
+
'TOKEN',
|
|
320
|
+
];
|
|
321
|
+
/** Names that are *expected* to be public even though they contain a keyword. */
|
|
322
|
+
exports.PUBLIC_SAFE_NAME_SUFFIXES = [
|
|
323
|
+
'ANON_KEY',
|
|
324
|
+
'ANON',
|
|
325
|
+
'PUBLISHABLE_KEY',
|
|
326
|
+
'PUBLIC_KEY',
|
|
327
|
+
'PK_',
|
|
328
|
+
'SITE_KEY',
|
|
329
|
+
'CLIENT_ID',
|
|
330
|
+
'APP_ID',
|
|
331
|
+
'MEASUREMENT_ID',
|
|
332
|
+
'FIREBASE_CONFIG',
|
|
333
|
+
];
|
|
334
|
+
/** Well-known server-only providers that must never reach the browser. */
|
|
335
|
+
exports.SERVER_ONLY_PROVIDERS = [
|
|
336
|
+
'OPENAI_API_KEY',
|
|
337
|
+
'ANTHROPIC_API_KEY',
|
|
338
|
+
'STRIPE_SECRET_KEY',
|
|
339
|
+
'STRIPE_WEBHOOK_SECRET',
|
|
340
|
+
'SUPABASE_SERVICE_ROLE_KEY',
|
|
341
|
+
'SUPABASE_SECRET_KEY',
|
|
342
|
+
'AWS_SECRET_ACCESS_KEY',
|
|
343
|
+
'AWS_SESSION_TOKEN',
|
|
344
|
+
'GITHUB_TOKEN',
|
|
345
|
+
'DATABASE_URL',
|
|
346
|
+
'POSTGRES_URL',
|
|
347
|
+
'SENDGRID_API_KEY',
|
|
348
|
+
'TWILIO_AUTH_TOKEN',
|
|
349
|
+
'RESEND_API_KEY',
|
|
350
|
+
'REPLICATE_API_TOKEN',
|
|
351
|
+
'HF_TOKEN',
|
|
352
|
+
'GOOGLE_API_KEY',
|
|
353
|
+
'AZURE_OPENAI_API_KEY',
|
|
354
|
+
'JWT_SECRET',
|
|
355
|
+
'SESSION_SECRET',
|
|
356
|
+
'ENCRYPTION_KEY',
|
|
357
|
+
];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* vibe-shield :: ai-vulnerabilities rules
|
|
3
|
+
* ---------------------------------------
|
|
4
|
+
* Two families live here because they share a root cause: an agent that
|
|
5
|
+
* "makes it work" on the happy path and never models an attacker.
|
|
6
|
+
*
|
|
7
|
+
* - LLM-specific: tool calls that reach a shell, model output piped into
|
|
8
|
+
* `eval`, untrusted context promoted to the system prompt, expensive routes
|
|
9
|
+
* with no quota.
|
|
10
|
+
* - Classic web: command injection, webhook signature bypass, XSS, open
|
|
11
|
+
* redirect, SSRF, path traversal, unrestricted uploads, insecure
|
|
12
|
+
* deserialization.
|
|
13
|
+
*/
|
|
14
|
+
import type { Rule } from '../types';
|
|
15
|
+
export declare const AI_VULNERABILITY_RULES: readonly Rule[];
|