@saiteja1123/mcp-server 1.1.3 → 1.1.5
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/package.json +59 -54
- package/src/api-scan.mjs +362 -93
- package/src/cli.js +713 -322
- package/src/deep-scan/contracts.js +201 -0
- package/src/deep-scan/deterministic-scan.js +337 -0
- package/src/deep-scan/index.js +109 -0
- package/src/deep-scan/project-map.js +507 -0
- package/src/deep-scan/ralph-accept.js +510 -0
- package/src/deep-scan/ralph-compare.js +498 -0
- package/src/deep-scan/ralph-tasks.js +598 -0
- package/src/deep-scan/ralph-track.js +548 -0
- package/src/deep-scan/registry.js +159 -0
- package/src/deep-scan/runtime.js +275 -0
- package/src/deep-scan/sample-steppers.js +128 -0
- package/src/deep-scan/sourceSafe.js +73 -0
- package/src/deep-scan/status.js +70 -0
- package/src/deep-scan/store.js +57 -0
- package/src/deep-scan/test-plan.js +760 -0
- package/src/index.js +6 -6
- package/src/lock.mjs +55 -14
- package/src/middleware/governance.js +135 -0
- package/src/orchestrator/runScan.js +211 -0
- package/src/project-bindings.mjs +215 -0
- package/src/rule-engine/index.js +3 -2
- package/src/rule-engine/localScan.js +41 -12
- package/src/rule-engine/metadata.js +20 -0
- package/src/rule-engine/prompt.js +6 -5
- package/src/rule-engine/rules.js +71 -43
- package/src/rule-engine/score.js +5 -4
- package/src/security/pathGuard.js +170 -0
- package/src/selftest.js +2473 -0
- package/src/server.js +161 -145
- package/src/tools/deepScan.js +286 -0
- package/src/tools/localScan.js +85 -0
- package/src/tools/projects.js +124 -0
- package/src/tools/scanFile.js +131 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
listProjects,
|
|
6
|
+
upsertProject,
|
|
7
|
+
normalizeLockedRoot,
|
|
8
|
+
verifyInstallBinding,
|
|
9
|
+
} from './api-scan.mjs';
|
|
10
|
+
import { createLock, isRuntimeCompatibleLock } from './lock.mjs';
|
|
11
|
+
|
|
12
|
+
const bindingCache = new Map();
|
|
13
|
+
let projectsCache = { at: 0, projects: [] };
|
|
14
|
+
const PROJECTS_CACHE_MS = 30_000;
|
|
15
|
+
|
|
16
|
+
function cacheKey(normalizedRoot) {
|
|
17
|
+
return normalizeLockedRoot(normalizedRoot);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function findProjectForPath(projects, targetPath) {
|
|
21
|
+
const norm = normalizeLockedRoot(targetPath);
|
|
22
|
+
if (!norm) return null;
|
|
23
|
+
let best = null;
|
|
24
|
+
let bestLen = -1;
|
|
25
|
+
for (const project of projects) {
|
|
26
|
+
const hint = normalizeLockedRoot(project.lockedRootHint || '');
|
|
27
|
+
if (!hint) continue;
|
|
28
|
+
if (norm === hint || norm.startsWith(`${hint}/`)) {
|
|
29
|
+
if (hint.length > bestLen) {
|
|
30
|
+
best = project;
|
|
31
|
+
bestLen = hint.length;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return best;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function inferProjectRoot(targetPath) {
|
|
39
|
+
let dir = path.resolve(targetPath);
|
|
40
|
+
try {
|
|
41
|
+
const stat = await fs.stat(dir);
|
|
42
|
+
if (stat.isFile()) dir = path.dirname(dir);
|
|
43
|
+
} catch {
|
|
44
|
+
dir = path.resolve(targetPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let current = dir;
|
|
48
|
+
for (let depth = 0; depth < 24; depth += 1) {
|
|
49
|
+
try {
|
|
50
|
+
await fs.access(path.join(current, '.git'));
|
|
51
|
+
return current;
|
|
52
|
+
} catch {
|
|
53
|
+
// continue walking up
|
|
54
|
+
}
|
|
55
|
+
const parent = path.dirname(current);
|
|
56
|
+
if (parent === current) break;
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
return dir;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function refreshProjects(authToken) {
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
if (now - projectsCache.at < PROJECTS_CACHE_MS && projectsCache.projects.length) {
|
|
65
|
+
return projectsCache.projects;
|
|
66
|
+
}
|
|
67
|
+
const res = await listProjects({ authToken });
|
|
68
|
+
if (!res.ok || !res.json?.success) {
|
|
69
|
+
throw new Error(res.json?.error || res.error || 'Unable to list projects');
|
|
70
|
+
}
|
|
71
|
+
projectsCache = {
|
|
72
|
+
at: now,
|
|
73
|
+
projects: res.json.data?.projects || [],
|
|
74
|
+
};
|
|
75
|
+
return projectsCache.projects;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function invalidateProjectsListCache() {
|
|
79
|
+
projectsCache = { at: 0, projects: [] };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function writeServerLock(projectRoot, { installToken, lockedRootHash, account = 'server' }) {
|
|
83
|
+
await createLock({
|
|
84
|
+
rootPath: projectRoot,
|
|
85
|
+
account,
|
|
86
|
+
installToken,
|
|
87
|
+
lockedRootHash,
|
|
88
|
+
source: 'server',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Register or refresh a project binding and cache install credentials for scans.
|
|
94
|
+
*/
|
|
95
|
+
export async function ensureProjectBinding({
|
|
96
|
+
lockedRootPath,
|
|
97
|
+
name,
|
|
98
|
+
projectId,
|
|
99
|
+
authToken,
|
|
100
|
+
apiBase,
|
|
101
|
+
} = {}) {
|
|
102
|
+
const resolvedRoot = path.resolve(lockedRootPath);
|
|
103
|
+
const res = await upsertProject({
|
|
104
|
+
lockedRootPath: resolvedRoot,
|
|
105
|
+
name,
|
|
106
|
+
projectId,
|
|
107
|
+
authToken,
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok || !res.json?.success) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
code: 'PROJECT_UPSERT_FAILED',
|
|
113
|
+
message: res.json?.error || res.error || `Project upsert failed (${res.status || 'unknown'})`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const bind = res.json.data?.bind || {};
|
|
118
|
+
const project = res.json.data?.project || {};
|
|
119
|
+
const installToken = bind.installToken;
|
|
120
|
+
const lockedRootHash = bind.lockedRootHash;
|
|
121
|
+
if (!installToken || !lockedRootHash) {
|
|
122
|
+
return { ok: false, code: 'PROJECT_BIND_INCOMPLETE', message: 'Server did not return install credentials' };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
await writeServerLock(resolvedRoot, { installToken, lockedRootHash });
|
|
126
|
+
const verify = await verifyInstallBinding({ installToken, lockedRootHash, apiBase });
|
|
127
|
+
if (!verify.ok || !verify.json?.success) {
|
|
128
|
+
return {
|
|
129
|
+
ok: false,
|
|
130
|
+
code: verify.json?.code || 'SERVER_BINDING_INVALID',
|
|
131
|
+
message: verify.json?.error || verify.error || 'Unable to verify project binding',
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const entry = {
|
|
136
|
+
projectRoot: resolvedRoot,
|
|
137
|
+
normalizedRoot: normalizeLockedRoot(resolvedRoot),
|
|
138
|
+
installToken,
|
|
139
|
+
lockedRootHash,
|
|
140
|
+
projectHash: bind.projectHash || project.projectHash || null,
|
|
141
|
+
projectId: project.id || bind.projectId || null,
|
|
142
|
+
lock: {
|
|
143
|
+
source: 'server',
|
|
144
|
+
installToken,
|
|
145
|
+
lockedRootHash,
|
|
146
|
+
rootHash: lockedRootHash,
|
|
147
|
+
boundRoot: resolvedRoot,
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
bindingCache.set(cacheKey(resolvedRoot), entry);
|
|
151
|
+
projectsCache.at = 0;
|
|
152
|
+
return { ok: true, ...entry, action: res.json.data?.action || 'upserted' };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Resolve install credentials for a scan path in universal (account-wide) mode.
|
|
157
|
+
* Creates or updates the project when no matching binding exists.
|
|
158
|
+
*/
|
|
159
|
+
export async function resolveBindingForScanPath({
|
|
160
|
+
requestedPath,
|
|
161
|
+
authToken,
|
|
162
|
+
apiBase,
|
|
163
|
+
autoUpsert = true,
|
|
164
|
+
} = {}) {
|
|
165
|
+
const target = path.resolve(requestedPath);
|
|
166
|
+
const cached = [...bindingCache.values()].find((entry) => {
|
|
167
|
+
const root = path.resolve(entry.projectRoot);
|
|
168
|
+
return target.startsWith(root + path.sep) || target === root;
|
|
169
|
+
});
|
|
170
|
+
if (cached) return { ok: true, ...cached, resolvedRoot: target };
|
|
171
|
+
|
|
172
|
+
let projects = [];
|
|
173
|
+
try {
|
|
174
|
+
projects = await refreshProjects(authToken);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
if (!autoUpsert) {
|
|
177
|
+
return { ok: false, code: 'PROJECT_LIST_FAILED', message: err.message };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const matched = findProjectForPath(projects, target);
|
|
182
|
+
if (matched?.lockedRootHint) {
|
|
183
|
+
return ensureProjectBinding({
|
|
184
|
+
lockedRootPath: matched.lockedRootHint,
|
|
185
|
+
projectId: matched.id,
|
|
186
|
+
authToken,
|
|
187
|
+
apiBase,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!autoUpsert) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
code: 'PROJECT_NOT_REGISTERED',
|
|
195
|
+
message:
|
|
196
|
+
`No project registered for "${target}". ` +
|
|
197
|
+
'Run projectUpsert with the codebase root before scanning.',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const inferredRoot = await inferProjectRoot(target);
|
|
202
|
+
return ensureProjectBinding({
|
|
203
|
+
lockedRootPath: inferredRoot,
|
|
204
|
+
authToken,
|
|
205
|
+
apiBase,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function getCachedBinding(projectRoot) {
|
|
210
|
+
return bindingCache.get(cacheKey(projectRoot)) || null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function isRuntimeCompatibleBinding(lock) {
|
|
214
|
+
return isRuntimeCompatibleLock(lock);
|
|
215
|
+
}
|
package/src/rule-engine/index.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* rule-engine/index.js
|
|
3
3
|
* Bundled inline - no external @vibesecur/rule-engine dep needed.
|
|
4
|
-
* This allows `npx @
|
|
4
|
+
* This allows `npx @saiteja1123/mcp-server` to work standalone.
|
|
5
5
|
*/
|
|
6
6
|
export { JS_RULES, PY_RULES, CHECKLIST } from './rules.js';
|
|
7
|
+
export { getRuleEngineMetadata } from './metadata.js';
|
|
8
|
+
export { calculateScore, getGrade, getVerdict } from './score.js';
|
|
7
9
|
export { localScan } from './localScan.js';
|
|
8
10
|
export { buildClaudePrompt } from './prompt.js';
|
|
9
|
-
export { calculateScore, getGrade, getVerdict } from './score.js';
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import crypto from 'crypto';
|
|
2
2
|
import { JS_RULES, PY_RULES, CHECKLIST } from './rules.js';
|
|
3
3
|
import { calculateScore, getGrade, getVerdict } from './score.js';
|
|
4
|
+
import { getRuleEngineMetadata } from './metadata.js';
|
|
4
5
|
|
|
6
|
+
// ── Main Local Scan ──────────────────────────────────────────────────────────
|
|
5
7
|
export function localScan(code, lang = 'js') {
|
|
6
8
|
const rules = lang === 'py'
|
|
7
9
|
? PY_RULES
|
|
@@ -10,34 +12,60 @@ export function localScan(code, lang = 'js') {
|
|
|
10
12
|
: JS_RULES;
|
|
11
13
|
|
|
12
14
|
const findings = [];
|
|
15
|
+
const lines = code.split('\n');
|
|
13
16
|
|
|
14
17
|
for (const rule of rules) {
|
|
15
18
|
const matches = [...code.matchAll(rule.re)];
|
|
16
19
|
for (const match of matches) {
|
|
17
|
-
|
|
20
|
+
// A003 can over-match valid jwt.sign() calls. Skip when expiry is present.
|
|
21
|
+
if (rule.id === 'A003') {
|
|
22
|
+
const matchedCall = String(match[0] || '');
|
|
23
|
+
if (/expiresIn\s*:|exp\s*:/i.test(matchedCall)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
18
27
|
|
|
19
28
|
const lineNumber = code.substring(0, match.index).split('\n').length;
|
|
29
|
+
|
|
30
|
+
if (rule.cat === 'XSS' || rule.cat === 'Path Traversal' || rule.cat === 'SSRF') {
|
|
31
|
+
const matchedLine = (lines[lineNumber - 1] || '').trimStart();
|
|
32
|
+
if (matchedLine.startsWith('//') || matchedLine.startsWith('*') || matchedLine.startsWith('/*')) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (rule.cat === 'Path Traversal') {
|
|
38
|
+
const matchedLine = lines[lineNumber - 1] || '';
|
|
39
|
+
if (rule.id === 'PT002' && /\bres\.sendFile\s*\(/.test(matchedLine)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (rule.id === 'PT004' && /\bpath\.normalize\s*\(/.test(matchedLine)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
20
46
|
const snippet = match[0].substring(0, 80) + (match[0].length > 80 ? '...' : '');
|
|
21
47
|
findings.push({
|
|
22
|
-
ruleId:
|
|
23
|
-
ruleName:
|
|
24
|
-
severity:
|
|
25
|
-
category:
|
|
48
|
+
ruleId: rule.id,
|
|
49
|
+
ruleName: rule.name,
|
|
50
|
+
severity: rule.sev,
|
|
51
|
+
category: rule.cat,
|
|
26
52
|
lineNumber,
|
|
53
|
+
endLineNumber: lineNumber,
|
|
27
54
|
snippet,
|
|
28
|
-
|
|
55
|
+
snippetPreview: snippet,
|
|
56
|
+
fix: rule.fix,
|
|
29
57
|
});
|
|
30
58
|
}
|
|
31
59
|
}
|
|
32
60
|
|
|
33
|
-
const score
|
|
34
|
-
const grade
|
|
35
|
-
const verdict
|
|
36
|
-
const codeHash
|
|
61
|
+
const score = calculateScore(findings);
|
|
62
|
+
const grade = getGrade(score);
|
|
63
|
+
const verdict = getVerdict(score);
|
|
64
|
+
const codeHash = crypto.createHash('sha256').update(code).digest('hex');
|
|
37
65
|
|
|
38
|
-
const checklist = CHECKLIST.map(
|
|
66
|
+
const checklist = CHECKLIST.map(cl => ({
|
|
39
67
|
...cl,
|
|
40
|
-
pass: !cl.ruleIds.some(
|
|
68
|
+
pass: !cl.ruleIds.some(rid => findings.find(f => f.ruleId === rid)),
|
|
41
69
|
}));
|
|
42
70
|
|
|
43
71
|
return {
|
|
@@ -49,6 +77,7 @@ export function localScan(code, lang = 'js') {
|
|
|
49
77
|
codeHash,
|
|
50
78
|
linesAnalysed: code.split('\n').length,
|
|
51
79
|
engine: 'local',
|
|
80
|
+
engineMetadata: getRuleEngineMetadata('mcp-bundled'),
|
|
52
81
|
summary: `Local engine found ${findings.length} issue${findings.length !== 1 ? 's' : ''}. ${
|
|
53
82
|
findings.length === 0
|
|
54
83
|
? 'No common vibe coding vulnerabilities detected.'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CHECKLIST, JS_RULES, PY_RULES } from './rules.js';
|
|
2
|
+
|
|
3
|
+
export const RULE_ENGINE_NAME = 'vibesecur-rule-engine';
|
|
4
|
+
export const RULE_ENGINE_PACKAGE = '@saiteja1123/rule-engine';
|
|
5
|
+
export const RULE_ENGINE_VERSION = '1.0.1';
|
|
6
|
+
|
|
7
|
+
export function getRuleEngineMetadata(surface = 'mcp-bundled') {
|
|
8
|
+
return {
|
|
9
|
+
name: RULE_ENGINE_NAME,
|
|
10
|
+
packageName: RULE_ENGINE_PACKAGE,
|
|
11
|
+
version: RULE_ENGINE_VERSION,
|
|
12
|
+
surface,
|
|
13
|
+
counts: {
|
|
14
|
+
javascript: JS_RULES.length,
|
|
15
|
+
python: PY_RULES.length,
|
|
16
|
+
deterministic: JS_RULES.length + PY_RULES.length,
|
|
17
|
+
checklist: CHECKLIST.length,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
// ── Claude AI Prompt Builder ─────────────────────────────────────────────────
|
|
1
2
|
export function buildClaudePrompt(code, platform, mode, lang) {
|
|
2
3
|
const isSupabase = mode === 'supabase' || code.toLowerCase().includes('supabase');
|
|
3
|
-
const isPython
|
|
4
|
-
const deepMode
|
|
5
|
-
const sbMode
|
|
4
|
+
const isPython = lang === 'py' || code.includes('import os') || code.includes('def ');
|
|
5
|
+
const deepMode = mode === 'deep' ? 'DEEP MODE: find subtle and complex issues.' : '';
|
|
6
|
+
const sbMode = isSupabase ? 'Focus especially on Supabase RLS policies and service key exposure.' : '';
|
|
6
7
|
|
|
7
8
|
return `You are Vibesecur, an expert AI security scanner for AI-generated code. Analyse this ${lang.toUpperCase()} code from ${platform}.
|
|
8
|
-
Return ONLY valid JSON
|
|
9
|
+
Return ONLY valid JSON — no markdown, no explanation, no backticks.
|
|
9
10
|
|
|
10
11
|
\`\`\`${lang}
|
|
11
12
|
${code.substring(0, 8000)}
|
|
@@ -24,7 +25,7 @@ Return exactly this JSON structure:
|
|
|
24
25
|
"severity": "<critical|high|medium|low>",
|
|
25
26
|
"lineNumber": <integer or null>,
|
|
26
27
|
"category": "<Secrets|Auth|Injection|CORS|RLS|Exposure|Python|SSRF>",
|
|
27
|
-
"description": "<what was found
|
|
28
|
+
"description": "<what was found — be specific>",
|
|
28
29
|
"fix": "<exact actionable code fix>"
|
|
29
30
|
}
|
|
30
31
|
],
|
package/src/rule-engine/rules.js
CHANGED
|
@@ -1,51 +1,79 @@
|
|
|
1
|
+
// ── Rule Definitions ────────────────────────────────────────────────────────
|
|
1
2
|
export const JS_RULES = [
|
|
2
|
-
|
|
3
|
-
{ id:
|
|
4
|
-
{ id:
|
|
5
|
-
{ id:
|
|
6
|
-
{ id:
|
|
7
|
-
{ id:
|
|
8
|
-
{ id:
|
|
9
|
-
{ id:
|
|
10
|
-
{ id:
|
|
11
|
-
|
|
12
|
-
{ id:
|
|
13
|
-
{ id:
|
|
14
|
-
{ id:
|
|
15
|
-
{ id:
|
|
16
|
-
{ id:
|
|
17
|
-
{ id:
|
|
18
|
-
{ id:
|
|
19
|
-
{ id:
|
|
20
|
-
{ id:
|
|
21
|
-
|
|
22
|
-
{ id:
|
|
23
|
-
{ id:
|
|
3
|
+
// Secrets
|
|
4
|
+
{ id:'S001', name:'Hardcoded API Key', sev:'critical', re:/api[_-]?key\s*[:=]\s*['"][a-zA-Z0-9_\-]{16,}['"]/gi, fix:'Move to .env: process.env.API_KEY', cat:'Secrets' },
|
|
5
|
+
{ id:'S002', name:'Hardcoded Password', sev:'critical', re:/password\s*[:=]\s*['"][^'"]{4,}['"]/gi, fix:'Use env var: process.env.PASSWORD', cat:'Secrets' },
|
|
6
|
+
{ id:'S003', name:'JWT Secret Hardcoded', sev:'critical', re:/jwt[_-]?secret\s*[:=]\s*['"][^'"]{6,}['"]/gi, fix:'Move to .env: JWT_SECRET=...', cat:'Secrets' },
|
|
7
|
+
{ id:'S004', name:'Database URL Exposed', sev:'critical', re:/(mongodb|mysql|postgres|supabase):\/\/[^\s'"]{8,}/gi, fix:'Move to .env: DATABASE_URL=...', cat:'Secrets' },
|
|
8
|
+
{ id:'S005', name:'AWS Access Key', sev:'critical', re:/AKIA[0-9A-Z]{16}/g, fix:'Rotate immediately. Use IAM roles.', cat:'Secrets' },
|
|
9
|
+
{ id:'S006', name:'Stripe Key Exposed', sev:'critical', re:/(sk_live|sk_test)_[a-zA-Z0-9]{20,}/g, fix:'Move to .env: STRIPE_SECRET_KEY=...', cat:'Secrets' },
|
|
10
|
+
{ id:'S007', name:'Private Key in Code', sev:'critical', re:/-----BEGIN[A-Z ]*PRIVATE KEY-----/g, fix:'Never commit private keys. Use a secrets manager.', cat:'Secrets' },
|
|
11
|
+
{ id:'S008', name:'Stripe Key via Concatenation', sev:'critical', re:/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*['"`]sk_(?:live|test)_['"`]\s*;[\s\S]{0,240}?\b[A-Za-z_$][\w$]*\s*=\s*\1\s*\+\s*[A-Za-z_$][\w$]*/g, fix:'Do not split secrets in source. Move the full Stripe key to server-side environment storage.', cat:'Secrets' },
|
|
12
|
+
// Auth
|
|
13
|
+
{ id:'A001', name:'MD5 Password Hashing', sev:'critical', re:/md5\s*\(/gi, fix:'Use bcrypt: await bcrypt.hash(password, 12)', cat:'Auth' },
|
|
14
|
+
{ id:'A002', name:'SHA1 Password Hashing', sev:'critical', re:/sha1\s*\(/gi, fix:'Use bcrypt or argon2 for passwords', cat:'Auth' },
|
|
15
|
+
{ id:'A003', name:'JWT Without Expiry', sev:'critical', re:/jwt\.sign\s*\([^)]{0,200}\)/g, fix:'Add: { expiresIn: "1h" }', cat:'Auth' },
|
|
16
|
+
{ id:'A004', name:'eval() Usage', sev:'critical', re:/\beval\s*\(/g, fix:'Never use eval() — arbitrary code execution', cat:'Injection'},
|
|
17
|
+
{ id:'A005', name:'SQL Injection Risk', sev:'critical', re:/(SELECT|INSERT|UPDATE|DELETE)[^;]{0,140}(\+\s*(req\.|user\.)|\$\{\s*(req|user)\.)/gi, fix:'Use parameterized queries: db.query("?", [val])', cat:'Injection'},
|
|
18
|
+
{ id:'A006', name:'Wildcard CORS', sev:'critical', re:/origin\s*:\s*['"]\*['"]/gi, fix:'cors({ origin: process.env.ALLOWED_ORIGIN })', cat:'CORS' },
|
|
19
|
+
{ id:'A007', name:'Missing Rate Limit', sev:'high', re:/app\.(post|put)\s*\(['"]\/(login|auth|register)/gi, fix:'Add rateLimit({ windowMs:900000, max:5 }) middleware', cat:'Auth' },
|
|
20
|
+
{ id:'A013', name:'eval via concatenated name', sev:'critical', re:/\b(?:window|globalThis|global)\s*\[\s*[A-Za-z_$][\w$]*\s*\+\s*[A-Za-z_$][\w$]*\s*\]/g, fix:'Never invoke dynamic globals built from strings; use explicit allowlisted functions.', cat:'Injection'},
|
|
21
|
+
{ id:'A014', name:'SQL keyword via concatenation', sev:'critical', re:/['"`]SEL['"`]\s*\+\s*['"`]ECT['"`]/gi, fix:'Avoid assembling SQL keywords or queries through string concatenation; use parameterized query builders.', cat:'Injection'},
|
|
22
|
+
// RLS / Firebase
|
|
23
|
+
{ id:'RLS1', name:'Supabase — Missing RLS', sev:'critical', re:/supabase\.from\(['"`][^'"`]+['"`]\)\.(select|insert|update|delete)/gi, fix:'ENABLE ROW LEVEL SECURITY on this table', cat:'RLS' },
|
|
24
|
+
{ id:'RLS2', name:'Supabase Service Key FE', sev:'critical', re:/service_role|supabase_service/gi, fix:'Service key must be server-side only — never in browser',cat:'RLS' },
|
|
25
|
+
{ id:'RLS3', name:'Firebase Open Rules', sev:'critical', re:/allow read, write:\s*if true/gi, fix:'Restrict: allow read if request.auth != null', cat:'RLS' },
|
|
26
|
+
// Exposure
|
|
27
|
+
{ id:'E001', name:'Credentials in Logs', sev:'high', re:/console\.log\(.*?(password|token|secret|key)/gi, fix:'Remove console.log with sensitive data', cat:'Exposure'},
|
|
28
|
+
{ id:'E002', name:'Debug Mode On', sev:'high', re:/debug\s*[:=]\s*true/gi, fix:'Set debug: false in production', cat:'Exposure'},
|
|
29
|
+
{ id:'E003', name:'Stack Trace Exposed', sev:'high', re:/res\.(send|json)\(.*?err\.(stack|message)/gi, fix:'return { error: "Server error" } only', cat:'Exposure'},
|
|
30
|
+
{ id:'E004', name:'TODO Security Note', sev:'medium', re:/\/\/\s*(TODO|FIXME).*?(auth|security|password|token)/gi, fix:'Resolve all security TODOs before deploy', cat:'Exposure'},
|
|
31
|
+
{ id:'E005', name:'Sensitive GET Params', sev:'medium', re:/\?.*?(token|password|secret|key)=/gi, fix:'Use POST body — never sensitive data in URL params', cat:'Exposure'},
|
|
32
|
+
// XSS / DOM injection
|
|
33
|
+
{ id:'XSS01', name:'innerHTML/outerHTML Assignment', sev:'high', re:/\.(innerHTML|outerHTML)\s*[+]?=/g, fix:'Use textContent or a sanitization library (DOMPurify) instead of innerHTML', cat:'XSS'},
|
|
34
|
+
{ id:'XSS02', name:'document.write Usage', sev:'high', re:/document\.write(?:ln)?\s*\(/g, fix:'Never use document.write; use DOM APIs (createElement, appendChild)', cat:'XSS'},
|
|
35
|
+
{ id:'XSS03', name:'dangerouslySetInnerHTML', sev:'high', re:/dangerouslySetInnerHTML/g, fix:'Sanitize HTML with DOMPurify before using dangerouslySetInnerHTML', cat:'XSS'},
|
|
36
|
+
{ id:'XSS04', name:'jQuery .html() Injection', sev:'medium', re:/\.html\s*\(\s*[^)]+\)/g, fix:'Use .text() for user data, or sanitize before .html()', cat:'XSS'},
|
|
37
|
+
{ id:'XSS05', name:'insertAdjacentHTML Usage', sev:'high', re:/\.insertAdjacentHTML\s*\(/g, fix:'Sanitize HTML content before insertAdjacentHTML, or use insertAdjacentText', cat:'XSS'},
|
|
38
|
+
// Path Traversal
|
|
39
|
+
{ id:'PT001', name:'fs.readFile with Request Path', sev:'critical', re:/\bfs(?:\.promises)?\.(?:readFile|readFileSync)\s*\(\s*(?:req|request)\.(?:query|params|body)\.[A-Za-z_$][\w$]*/g, fix:'Validate file names against an allowlist and resolve inside a fixed root before reading', cat:'Path Traversal'},
|
|
40
|
+
{ id:'PT002', name:'path.join/resolve with Request Path', sev:'high', re:/\bpath\.(?:join|resolve)\s*\([^)]*(?:req|request)\.(?:query|params|body)\.[A-Za-z_$][\w$][^)]*\)/g, fix:'Use a fixed root plus allowlisted or basename-normalized file names', cat:'Path Traversal'},
|
|
41
|
+
{ id:'PT003', name:'sendFile with Request Path', sev:'high', re:/\bres\.sendFile\s*\(\s*path\.(?:join|resolve)\s*\([^)]*(?:req|request)\.(?:query|params|body)\.[A-Za-z_$][\w$][^)]*\)/g, fix:'Serve only allowlisted files from a configured root directory', cat:'Path Traversal'},
|
|
42
|
+
{ id:'PT004', name:'Archive Extraction Path Join', sev:'high', re:/\bpath\.(?:join|resolve)\s*\([^)]*\b(?:entry|file|zipEntry|archiveEntry)\.(?:path|fileName|filename|name)\b[^)]*\)/g, fix:'Normalize extraction targets and reject paths outside the destination root', cat:'Path Traversal'},
|
|
43
|
+
// SSRF
|
|
44
|
+
{ id:'SSRF01', name:'fetch with Request URL', sev:'high', re:/\bfetch\s*\(\s*(?:req|request)\.(?:query|params|body)\.[A-Za-z_$][\w$]*/g, fix:'Validate URL host against an allowlist before fetching', cat:'SSRF'},
|
|
45
|
+
{ id:'SSRF02', name:'HTTP Client with Request URL', sev:'high', re:/\b(?:axios\.(?:get|post|put|patch|delete|request)|got|request(?:\.(?:get|post|put|patch|delete))?)\s*\(\s*(?:req|request)\.(?:query|params|body)\.[A-Za-z_$][\w$]*/g, fix:'Allowlist destination hosts before making outbound HTTP requests', cat:'SSRF'},
|
|
46
|
+
{ id:'SSRF03', name:'http/https Request with Request URL', sev:'high', re:/\bhttps?\.(?:get|request)\s*\(\s*(?:req|request)\.(?:query|params|body)\.[A-Za-z_$][\w$]*/g, fix:'Allowlist destination hosts before making server-side HTTP requests', cat:'SSRF'},
|
|
47
|
+
{ id:'SSRF04', name:'URL Constructor with Request Input', sev:'medium', re:/\bnew\s+URL\s*\(\s*(?:req|request)\.(?:query|params|body)\.[A-Za-z_$][\w$]*/g, fix:'Validate URL host and protocol immediately after construction', cat:'SSRF'},
|
|
48
|
+
{ id:'SSRF05', name:'Cloud Metadata Endpoint Request', sev:'critical', re:/\b(?:fetch|got|request|axios\.(?:get|post|put|patch|delete|request)|https?\.(?:get|request))\s*\(\s*['"`]https?:\/\/(?:169\.254\.169\.254|metadata\.google\.internal)\b/g, fix:'Block cloud metadata endpoints and private network targets from server-side fetches', cat:'SSRF'},
|
|
24
49
|
];
|
|
25
50
|
|
|
26
51
|
export const PY_RULES = [
|
|
27
|
-
{ id:
|
|
28
|
-
{ id:
|
|
29
|
-
{ id:
|
|
30
|
-
{ id:
|
|
31
|
-
{ id:
|
|
32
|
-
{ id:
|
|
33
|
-
{ id:
|
|
34
|
-
{ id:
|
|
35
|
-
{ id:
|
|
52
|
+
{ id:'P001', name:'Python eval()', sev:'critical', re:/\beval\s*\(/g, fix:'Never use eval() — arbitrary code execution', cat:'Injection'},
|
|
53
|
+
{ id:'P002', name:'Python pickle.loads()', sev:'critical', re:/pickle\.loads?\s*\(/g, fix:'Never unpickle untrusted data — use JSON', cat:'Injection'},
|
|
54
|
+
{ id:'P003', name:'Python SQL Concatenation', sev:'critical', re:/cursor\.execute\s*\([^)]*%\s*|f"SELECT.*\{/gi, fix:'Use parameterized queries: cursor.execute("?", (v,))', cat:'Injection'},
|
|
55
|
+
{ id:'P004', name:'Python Hardcoded Secret', sev:'critical', re:/(password|api_key|secret|token)\s*=\s*['"][^'"]{6,}['"]/gi, fix:'Use os.environ.get("SECRET")', cat:'Secrets' },
|
|
56
|
+
{ id:'P005', name:'Python subprocess shell', sev:'high', re:/subprocess\.[^(]+\([^)]*shell\s*=\s*True/gi, fix:'Avoid shell=True — use list args', cat:'Injection'},
|
|
57
|
+
{ id:'P006', name:'Python MD5 Passwords', sev:'critical', re:/hashlib\.(md5|sha1)/gi, fix:'Use bcrypt or argon2-cffi for passwords', cat:'Auth' },
|
|
58
|
+
{ id:'P007', name:'Python DEBUG=True', sev:'high', re:/DEBUG\s*=\s*True/g, fix:'Set DEBUG=False in production settings', cat:'Exposure'},
|
|
59
|
+
{ id:'P008', name:'Python Open Redirect', sev:'medium', re:/(?:redirect\s*\([^)]*(?:request|[A-Za-z_]\w*)\.(?:args|form|params)(?:\.get\s*\([^)]*\)|\.[A-Za-z_]\w*)|([A-Za-z_]\w*)\s*=\s*(?:request|[A-Za-z_]\w*)\.(?:args|form|params)\.get\s*\([^)]*\)[\s\S]{0,240}?redirect\s*\(\s*\1\b)/gi, fix:'Validate redirect URLs against allowlist', cat:'Auth' },
|
|
60
|
+
{ id:'P009', name:'Python SSRF Risk', sev:'high', re:/(?:([A-Za-z_]\w*)\s*=\s*(?:request|[A-Za-z_]\w*)\.(?:args|form|params)\.get\s*\([^)]*\)[\s\S]{0,240}?requests\.(?:get|post)\s*\(\s*\1\b|requests\.(?:get|post)\s*\(\s*(?:request|[A-Za-z_]\w*)\.(?:args|form|params)(?:\.get\s*\([^)]*\)|\.[A-Za-z_]\w*))/gi, fix:'Validate and allowlist URLs before fetching', cat:'SSRF' },
|
|
36
61
|
];
|
|
37
62
|
|
|
38
63
|
export const CHECKLIST = [
|
|
39
|
-
{ id:
|
|
40
|
-
{ id:
|
|
41
|
-
{ id:
|
|
42
|
-
{ id:
|
|
43
|
-
{ id:
|
|
44
|
-
{ id:
|
|
45
|
-
{ id:
|
|
46
|
-
{ id:
|
|
47
|
-
{ id:
|
|
48
|
-
{ id:
|
|
49
|
-
{ id:
|
|
50
|
-
{ id:
|
|
64
|
+
{ id:'CL01', item:'No API keys hardcoded', critical:true, ruleIds:['S001','S002','S003','S005','S006','S008','P004'] },
|
|
65
|
+
{ id:'CL02', item:'.env in .gitignore', critical:true, ruleIds:[] },
|
|
66
|
+
{ id:'CL03', item:'bcrypt/argon2 for passwords', critical:true, ruleIds:['A001','A002','P006'] },
|
|
67
|
+
{ id:'CL04', item:'JWT expiry set', critical:true, ruleIds:['A003'] },
|
|
68
|
+
{ id:'CL05', item:'Rate limiting on auth routes', critical:true, ruleIds:['A007'] },
|
|
69
|
+
{ id:'CL06', item:'CORS restricted', critical:true, ruleIds:['A006'] },
|
|
70
|
+
{ id:'CL07', item:'No SQL injection patterns', critical:true, ruleIds:['A005','A014','P003'] },
|
|
71
|
+
{ id:'CL08', item:'Supabase RLS enabled', critical:true, ruleIds:['RLS1','RLS2'] },
|
|
72
|
+
{ id:'CL09', item:'Firebase rules restricted', critical:false, ruleIds:['RLS3'] },
|
|
73
|
+
{ id:'CL10', item:'No stack traces exposed', critical:false, ruleIds:['E003'] },
|
|
74
|
+
{ id:'CL11', item:'eval() not used', critical:true, ruleIds:['A004','A013','P001','P002'] },
|
|
75
|
+
{ id:'CL12', item:'No debug mode in production', critical:false, ruleIds:['E002','P007'] },
|
|
76
|
+
{ id:'CL13', item:'No XSS / DOM injection sinks', critical:true, ruleIds:['XSS01','XSS02','XSS03','XSS04','XSS05'] },
|
|
77
|
+
{ id:'CL14', item:'No path traversal file access', critical:true, ruleIds:['PT001','PT002','PT003','PT004'] },
|
|
78
|
+
{ id:'CL15', item:'No obvious SSRF network sinks', critical:true, ruleIds:['SSRF01','SSRF02','SSRF03','SSRF04','SSRF05'] },
|
|
51
79
|
];
|
package/src/rule-engine/score.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
// ── Score Calculation ────────────────────────────────────────────────────────
|
|
1
2
|
export function calculateScore(findings) {
|
|
2
3
|
const WEIGHTS = { critical: 20, high: 10, medium: 5, low: 2 };
|
|
3
4
|
let score = 100;
|
|
4
5
|
for (const f of findings) score -= (WEIGHTS[f.severity] || 2);
|
|
5
|
-
const cats = new Set(findings.map(
|
|
6
|
+
const cats = new Set(findings.map(f => f.category));
|
|
6
7
|
if (cats.size >= 4) score -= 5;
|
|
7
8
|
return Math.max(0, Math.min(100, Math.round(score)));
|
|
8
9
|
}
|
|
@@ -16,7 +17,7 @@ export function getGrade(score) {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export function getVerdict(score) {
|
|
19
|
-
if (score >= 80) return 'Safe to Deploy';
|
|
20
|
-
if (score >= 60) return 'Review Before Deploy';
|
|
21
|
-
return 'Critical Issues
|
|
20
|
+
if (score >= 80) return '✅ Safe to Deploy';
|
|
21
|
+
if (score >= 60) return '⚠️ Review Before Deploy';
|
|
22
|
+
return '🚫 Critical Issues — Do Not Ship';
|
|
22
23
|
}
|