@saiteja1123/mcp-server 1.1.4 → 1.1.6
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 -55
- package/src/api-scan.mjs +362 -93
- package/src/cli.js +771 -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 -5
- package/src/lock.mjs +55 -14
- package/src/mcp-config.mjs +161 -0
- 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 +2 -1
- package/src/rule-engine/localScan.js +39 -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 +109 -150
- 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,507 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fg from 'fast-glob';
|
|
5
|
+
import { createArtifactRef, validateRunIdSegment } from './contracts.js';
|
|
6
|
+
import { assertSourceSafePayload, cloneSourceSafe } from './sourceSafe.js';
|
|
7
|
+
|
|
8
|
+
export const PROJECT_MAP_SCHEMA_VERSION = 'project_map.v1';
|
|
9
|
+
export const PROJECT_MAP_STEPPER_ID = 'project.map';
|
|
10
|
+
export const PROJECT_MAP_STEPPER_VERSION = '1.0.0';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_IGNORE = [
|
|
13
|
+
'**/node_modules/**',
|
|
14
|
+
'**/.git/**',
|
|
15
|
+
'**/dist/**',
|
|
16
|
+
'**/build/**',
|
|
17
|
+
'**/.next/**',
|
|
18
|
+
'**/.nuxt/**',
|
|
19
|
+
'**/.svelte-kit/**',
|
|
20
|
+
'**/.venv/**',
|
|
21
|
+
'**/venv/**',
|
|
22
|
+
'**/.vibesecur/**',
|
|
23
|
+
'**/coverage/**',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const DEPLOYMENT_FILES = new Map([
|
|
27
|
+
['render.yaml', 'render'],
|
|
28
|
+
['render.yml', 'render'],
|
|
29
|
+
['vercel.json', 'vercel'],
|
|
30
|
+
['netlify.toml', 'netlify'],
|
|
31
|
+
['railway.json', 'railway'],
|
|
32
|
+
['fly.toml', 'fly'],
|
|
33
|
+
['dockerfile', 'docker'],
|
|
34
|
+
['docker-compose.yml', 'docker_compose'],
|
|
35
|
+
['docker-compose.yaml', 'docker_compose'],
|
|
36
|
+
['procfile', 'procfile'],
|
|
37
|
+
['app.yaml', 'google_app_engine'],
|
|
38
|
+
['cloudbuild.yaml', 'google_cloud_build'],
|
|
39
|
+
['cloudbuild.yml', 'google_cloud_build'],
|
|
40
|
+
['serverless.yml', 'serverless'],
|
|
41
|
+
['serverless.yaml', 'serverless'],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const LANGUAGE_BY_EXTENSION = new Map([
|
|
45
|
+
['.js', 'JavaScript/TypeScript'],
|
|
46
|
+
['.jsx', 'JavaScript/TypeScript'],
|
|
47
|
+
['.mjs', 'JavaScript/TypeScript'],
|
|
48
|
+
['.cjs', 'JavaScript/TypeScript'],
|
|
49
|
+
['.ts', 'JavaScript/TypeScript'],
|
|
50
|
+
['.tsx', 'JavaScript/TypeScript'],
|
|
51
|
+
['.py', 'Python'],
|
|
52
|
+
['.go', 'Go'],
|
|
53
|
+
['.rs', 'Rust'],
|
|
54
|
+
['.rb', 'Ruby'],
|
|
55
|
+
['.php', 'PHP'],
|
|
56
|
+
['.java', 'Java'],
|
|
57
|
+
['.kt', 'Kotlin'],
|
|
58
|
+
['.kts', 'Kotlin'],
|
|
59
|
+
['.cs', 'C#'],
|
|
60
|
+
['.swift', 'Swift'],
|
|
61
|
+
['.scala', 'Scala'],
|
|
62
|
+
['.sh', 'Shell'],
|
|
63
|
+
['.bash', 'Shell'],
|
|
64
|
+
['.zsh', 'Shell'],
|
|
65
|
+
['.sql', 'SQL'],
|
|
66
|
+
['.html', 'HTML'],
|
|
67
|
+
['.css', 'CSS'],
|
|
68
|
+
['.md', 'Markdown'],
|
|
69
|
+
['.yml', 'YAML'],
|
|
70
|
+
['.yaml', 'YAML'],
|
|
71
|
+
['.toml', 'TOML'],
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
const FRAMEWORK_DETECTORS = [
|
|
75
|
+
{ name: 'express', packages: ['express'], runtime: 'node', category: 'api' },
|
|
76
|
+
{ name: 'fastify', packages: ['fastify'], runtime: 'node', category: 'api' },
|
|
77
|
+
{ name: 'koa', packages: ['koa'], runtime: 'node', category: 'api' },
|
|
78
|
+
{ name: 'nestjs', packages: ['@nestjs/core'], runtime: 'node', category: 'api' },
|
|
79
|
+
{ name: 'next', packages: ['next'], runtime: 'node', category: 'web' },
|
|
80
|
+
{ name: 'remix', packages: ['@remix-run/react', '@remix-run/node'], runtime: 'node', category: 'web' },
|
|
81
|
+
{ name: 'vite', packages: ['vite', '@vitejs/plugin-react'], runtime: 'node', category: 'web' },
|
|
82
|
+
{ name: 'react', packages: ['react'], runtime: 'browser', category: 'web' },
|
|
83
|
+
{ name: 'vue', packages: ['vue'], runtime: 'browser', category: 'web' },
|
|
84
|
+
{ name: 'svelte', packages: ['svelte', '@sveltejs/kit'], runtime: 'browser', category: 'web' },
|
|
85
|
+
{ name: 'astro', packages: ['astro'], runtime: 'node', category: 'web' },
|
|
86
|
+
{ name: 'angular', packages: ['@angular/core'], runtime: 'browser', category: 'web' },
|
|
87
|
+
{ name: 'mcp', packages: ['@modelcontextprotocol/sdk'], runtime: 'node', category: 'tooling' },
|
|
88
|
+
{ name: 'stripe', packages: ['stripe'], runtime: 'node', category: 'payments' },
|
|
89
|
+
{ name: 'supabase', packages: ['@supabase/supabase-js'], runtime: 'node', category: 'data' },
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
const SECRETISH_PATTERNS = [
|
|
93
|
+
/\b(?:sk|pk)_(?:live|test)_[a-zA-Z0-9_=-]{6,}\b/gi,
|
|
94
|
+
/\bgh[pousr]_[a-zA-Z0-9_]{10,}\b/g,
|
|
95
|
+
/\bAKIA[0-9A-Z]{12,}\b/g,
|
|
96
|
+
/\bxox[baprs]-[a-zA-Z0-9-]{10,}\b/gi,
|
|
97
|
+
/-----BEGIN [A-Z ]*(?:PRIVATE KEY|SECRET|TOKEN|CERTIFICATE)-----[\s\S]*?-----END [A-Z ]*-----/gi,
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const nowIso = () => new Date().toISOString();
|
|
101
|
+
|
|
102
|
+
const toPosix = (value) => String(value || '').replace(/\\/g, '/');
|
|
103
|
+
|
|
104
|
+
const sortByPath = (items) => [...items].sort((a, b) => String(a.path).localeCompare(String(b.path)));
|
|
105
|
+
|
|
106
|
+
function stableSort(value) {
|
|
107
|
+
if (Array.isArray(value)) return value.map(stableSort);
|
|
108
|
+
if (!value || typeof value !== 'object') return value;
|
|
109
|
+
return Object.keys(value)
|
|
110
|
+
.sort()
|
|
111
|
+
.reduce((acc, key) => {
|
|
112
|
+
acc[key] = stableSort(value[key]);
|
|
113
|
+
return acc;
|
|
114
|
+
}, {});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stableStringify(value) {
|
|
118
|
+
return JSON.stringify(stableSort(value));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function redactString(value) {
|
|
122
|
+
if (typeof value !== 'string') return value;
|
|
123
|
+
let redacted = value;
|
|
124
|
+
for (const pattern of SECRETISH_PATTERNS) redacted = redacted.replace(pattern, '[redacted]');
|
|
125
|
+
redacted = redacted.replace(
|
|
126
|
+
/\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|API_KEY|PRIVATE_KEY)[A-Z0-9_]*)=("[^"]*"|'[^']*'|\S+)/gi,
|
|
127
|
+
'$1=[redacted]',
|
|
128
|
+
);
|
|
129
|
+
return redacted;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function redactCommand(value) {
|
|
133
|
+
return redactString(value)
|
|
134
|
+
.replace(
|
|
135
|
+
/(Authorization:\s*(?:Bearer|Basic)\s+)([^"'\s]+)/gi,
|
|
136
|
+
'$1[redacted]',
|
|
137
|
+
)
|
|
138
|
+
.replace(
|
|
139
|
+
/\b([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^@\s/]+)@/gi,
|
|
140
|
+
'$1[redacted]@',
|
|
141
|
+
)
|
|
142
|
+
.replace(
|
|
143
|
+
/(\s--(?:token|api-key|apikey|password|secret|client-secret|access-token|auth-token)(?:=|\s+))(?:"[^"]*"|'[^']*'|[^\s]+)/gi,
|
|
144
|
+
'$1[redacted]',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function redactJson(value) {
|
|
149
|
+
if (typeof value === 'string') return redactString(value);
|
|
150
|
+
if (Array.isArray(value)) return value.map(redactJson);
|
|
151
|
+
if (!value || typeof value !== 'object') return value;
|
|
152
|
+
return Object.entries(value).reduce((acc, [key, child]) => {
|
|
153
|
+
acc[key] = redactJson(child);
|
|
154
|
+
return acc;
|
|
155
|
+
}, {});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function readJsonFile(filePath) {
|
|
159
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
160
|
+
return redactJson(JSON.parse(raw));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function collectDependencyNames(manifest) {
|
|
164
|
+
const groups = [
|
|
165
|
+
manifest.dependencies,
|
|
166
|
+
manifest.devDependencies,
|
|
167
|
+
manifest.peerDependencies,
|
|
168
|
+
manifest.optionalDependencies,
|
|
169
|
+
];
|
|
170
|
+
return [...new Set(groups.flatMap((group) => Object.keys(group || {})))].sort();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function safePackageName(value) {
|
|
174
|
+
return typeof value === 'string' && value.trim() ? redactString(value.trim()).slice(0, 180) : undefined;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function scriptCommands(manifestPath, manifest) {
|
|
178
|
+
return Object.entries(manifest.scripts || {})
|
|
179
|
+
.filter(([name]) => /(^|:)(test|spec|check|lint|verify|e2e|integration|unit)(:|$)/i.test(name))
|
|
180
|
+
.map(([name, command]) => ({
|
|
181
|
+
source: manifestPath,
|
|
182
|
+
name: redactString(name),
|
|
183
|
+
command: redactCommand(String(command || '')).slice(0, 300),
|
|
184
|
+
}))
|
|
185
|
+
.sort((a, b) => `${a.source}:${a.name}`.localeCompare(`${b.source}:${b.name}`));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function packageManagerFromFiles(files, packageJsons) {
|
|
189
|
+
const names = new Set();
|
|
190
|
+
const basenames = new Set(files.map((file) => path.basename(file).toLowerCase()));
|
|
191
|
+
if (basenames.has('pnpm-lock.yaml') || basenames.has('pnpm-workspace.yaml')) names.add('pnpm');
|
|
192
|
+
if (basenames.has('yarn.lock')) names.add('yarn');
|
|
193
|
+
if (basenames.has('bun.lockb') || basenames.has('bun.lock')) names.add('bun');
|
|
194
|
+
if (basenames.has('package-lock.json') || packageJsons.length > 0) names.add('npm');
|
|
195
|
+
if (basenames.has('requirements.txt') || basenames.has('pyproject.toml')) names.add('pip');
|
|
196
|
+
if (basenames.has('poetry.lock')) names.add('poetry');
|
|
197
|
+
if (basenames.has('cargo.lock') || basenames.has('cargo.toml')) names.add('cargo');
|
|
198
|
+
if (basenames.has('go.mod')) names.add('go');
|
|
199
|
+
return [...names].sort();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function detectFrameworks(manifests) {
|
|
203
|
+
const packageNames = new Set(manifests.flatMap((manifest) => manifest.dependencyNames || []));
|
|
204
|
+
return FRAMEWORK_DETECTORS
|
|
205
|
+
.filter((detector) => detector.packages.some((packageName) => packageNames.has(packageName)))
|
|
206
|
+
.map((detector) => ({
|
|
207
|
+
name: detector.name,
|
|
208
|
+
category: detector.category,
|
|
209
|
+
runtime: detector.runtime,
|
|
210
|
+
evidence: detector.packages.filter((packageName) => packageNames.has(packageName)).sort(),
|
|
211
|
+
}))
|
|
212
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function detectRuntimes(files, manifests, frameworks) {
|
|
216
|
+
const runtimes = new Set(frameworks.map((framework) => framework.runtime).filter(Boolean));
|
|
217
|
+
if (manifests.some((manifest) => manifest.type === 'node')) runtimes.add('node');
|
|
218
|
+
const lowerFiles = files.map((file) => file.toLowerCase());
|
|
219
|
+
if (lowerFiles.some((file) => file.endsWith('requirements.txt') || file.endsWith('pyproject.toml') || path.extname(file) === '.py')) {
|
|
220
|
+
runtimes.add('python');
|
|
221
|
+
}
|
|
222
|
+
if (lowerFiles.some((file) => file.endsWith('go.mod') || path.extname(file) === '.go')) runtimes.add('go');
|
|
223
|
+
if (lowerFiles.some((file) => file.endsWith('cargo.toml') || path.extname(file) === '.rs')) runtimes.add('rust');
|
|
224
|
+
if (runtimes.has('browser')) runtimes.add('browser');
|
|
225
|
+
return [...runtimes].sort();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function detectLanguages(files) {
|
|
229
|
+
const counts = new Map();
|
|
230
|
+
for (const file of files) {
|
|
231
|
+
const ext = path.extname(file).toLowerCase();
|
|
232
|
+
if (path.basename(file).startsWith('.env')) continue;
|
|
233
|
+
const language = LANGUAGE_BY_EXTENSION.get(ext);
|
|
234
|
+
if (!language) continue;
|
|
235
|
+
const current = counts.get(language) || { language, files: 0, extensions: new Set() };
|
|
236
|
+
current.files += 1;
|
|
237
|
+
current.extensions.add(ext);
|
|
238
|
+
counts.set(language, current);
|
|
239
|
+
}
|
|
240
|
+
return [...counts.values()]
|
|
241
|
+
.map((item) => ({
|
|
242
|
+
language: item.language,
|
|
243
|
+
files: item.files,
|
|
244
|
+
extensions: [...item.extensions].sort(),
|
|
245
|
+
}))
|
|
246
|
+
.sort((a, b) => b.files - a.files || a.language.localeCompare(b.language));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function isRouteLike(relativePath) {
|
|
250
|
+
const normalized = toPosix(relativePath).toLowerCase();
|
|
251
|
+
const ext = path.extname(normalized);
|
|
252
|
+
if (!['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.py', '.rb', '.php', '.go'].includes(ext)) return false;
|
|
253
|
+
return (
|
|
254
|
+
/(^|\/)(routes?|controllers?|handlers?|endpoints?)(\/|$)/.test(normalized)
|
|
255
|
+
|| /(^|\/)(pages\/api|app\/api|api)(\/|$)/.test(normalized)
|
|
256
|
+
|| /(^|\/)(route|router|controller|server)\.[a-z0-9]+$/.test(normalized)
|
|
257
|
+
|| /(^|\/)app\.(js|ts|mjs|cjs|py|rb|php|go)$/.test(normalized)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function routeKind(relativePath) {
|
|
262
|
+
const normalized = toPosix(relativePath).toLowerCase();
|
|
263
|
+
if (normalized.includes('/pages/api/')) return 'pages_api';
|
|
264
|
+
if (normalized.includes('/app/api/')) return 'app_api';
|
|
265
|
+
if (normalized.includes('/routes/') || normalized.includes('/route.')) return 'route_file';
|
|
266
|
+
if (normalized.includes('/controllers/') || normalized.includes('/controller.')) return 'controller_file';
|
|
267
|
+
if (/(^|\/)(server|app)\.(js|ts|mjs|cjs|py|rb|php|go)$/.test(normalized)) return 'server_entry';
|
|
268
|
+
return 'route_like';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function detectDeploymentFiles(files) {
|
|
272
|
+
return files
|
|
273
|
+
.map((file) => {
|
|
274
|
+
const base = path.basename(file).toLowerCase();
|
|
275
|
+
const type = DEPLOYMENT_FILES.get(base);
|
|
276
|
+
return type ? { path: file, type } : null;
|
|
277
|
+
})
|
|
278
|
+
.filter(Boolean)
|
|
279
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function detectEnvFiles(files) {
|
|
283
|
+
return files
|
|
284
|
+
.filter((file) => {
|
|
285
|
+
const base = path.basename(file).toLowerCase();
|
|
286
|
+
return base === '.env' || base.startsWith('.env.') || base.endsWith('.env') || base.includes('.env.');
|
|
287
|
+
})
|
|
288
|
+
.map((file) => ({
|
|
289
|
+
path: file,
|
|
290
|
+
kind: path.basename(file).toLowerCase().includes('example') ? 'example' : 'env',
|
|
291
|
+
valuesCaptured: false,
|
|
292
|
+
}))
|
|
293
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function collectFiles(rootPath) {
|
|
297
|
+
const files = await fg(['**/*', '**/.env*'], {
|
|
298
|
+
cwd: rootPath,
|
|
299
|
+
onlyFiles: true,
|
|
300
|
+
dot: true,
|
|
301
|
+
absolute: false,
|
|
302
|
+
ignore: DEFAULT_IGNORE,
|
|
303
|
+
suppressErrors: true,
|
|
304
|
+
followSymbolicLinks: false,
|
|
305
|
+
});
|
|
306
|
+
return [...new Set(files.map(toPosix))].sort();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function collectPackageManifests(rootPath, files) {
|
|
310
|
+
const packageJsons = files.filter((file) => path.basename(file) === 'package.json');
|
|
311
|
+
const manifests = [];
|
|
312
|
+
for (const relativePath of packageJsons) {
|
|
313
|
+
const absolutePath = path.join(rootPath, relativePath);
|
|
314
|
+
const manifest = await readJsonFile(absolutePath);
|
|
315
|
+
const dependencyNames = collectDependencyNames(manifest);
|
|
316
|
+
const workspaces = Array.isArray(manifest.workspaces)
|
|
317
|
+
? manifest.workspaces
|
|
318
|
+
: manifest.workspaces?.packages;
|
|
319
|
+
manifests.push({
|
|
320
|
+
path: relativePath,
|
|
321
|
+
type: 'node',
|
|
322
|
+
packageName: safePackageName(manifest.name),
|
|
323
|
+
private: manifest.private === true,
|
|
324
|
+
scriptNames: Object.keys(manifest.scripts || {}).sort().map(redactString),
|
|
325
|
+
dependencyNames,
|
|
326
|
+
workspaces: Array.isArray(workspaces) ? workspaces.map(redactString).sort() : [],
|
|
327
|
+
packageManager: typeof manifest.packageManager === 'string' ? redactString(manifest.packageManager) : undefined,
|
|
328
|
+
scripts: scriptCommands(relativePath, manifest),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return sortByPath(manifests);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function collectOtherManifests(files) {
|
|
335
|
+
const manifestNames = new Set([
|
|
336
|
+
'requirements.txt',
|
|
337
|
+
'pyproject.toml',
|
|
338
|
+
'go.mod',
|
|
339
|
+
'cargo.toml',
|
|
340
|
+
'gemfile',
|
|
341
|
+
'composer.json',
|
|
342
|
+
]);
|
|
343
|
+
return files
|
|
344
|
+
.filter((file) => manifestNames.has(path.basename(file).toLowerCase()))
|
|
345
|
+
.map((file) => ({
|
|
346
|
+
path: file,
|
|
347
|
+
type: path.basename(file).replace(/\..*$/, '').toLowerCase(),
|
|
348
|
+
scriptNames: [],
|
|
349
|
+
dependencyNames: [],
|
|
350
|
+
workspaces: [],
|
|
351
|
+
scripts: [],
|
|
352
|
+
}))
|
|
353
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function buildWorkspaceSummary(manifests, files) {
|
|
357
|
+
const workspaceManifests = manifests.filter((manifest) => manifest.workspaces?.length);
|
|
358
|
+
const hasPnpmWorkspace = files.includes('pnpm-workspace.yaml');
|
|
359
|
+
return {
|
|
360
|
+
detected: workspaceManifests.length > 0 || hasPnpmWorkspace,
|
|
361
|
+
manifests: workspaceManifests.map((manifest) => manifest.path),
|
|
362
|
+
patterns: [...new Set(workspaceManifests.flatMap((manifest) => manifest.workspaces || []))].sort(),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function hashProjectMapArtifact(artifact) {
|
|
367
|
+
assertSourceSafePayload(artifact, 'projectMapArtifact');
|
|
368
|
+
return crypto.createHash('sha256').update(stableStringify(artifact)).digest('hex');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export async function buildProjectMapArtifact({
|
|
372
|
+
rootPath,
|
|
373
|
+
generatedAt = nowIso(),
|
|
374
|
+
maxFiles = 5000,
|
|
375
|
+
} = {}) {
|
|
376
|
+
if (!rootPath || typeof rootPath !== 'string') throw new Error('Project Map requires rootPath');
|
|
377
|
+
const resolvedRoot = path.resolve(rootPath);
|
|
378
|
+
const stat = await fs.stat(resolvedRoot);
|
|
379
|
+
if (!stat.isDirectory()) throw new Error(`Project Map rootPath is not a directory: ${resolvedRoot}`);
|
|
380
|
+
|
|
381
|
+
const discoveredFiles = await collectFiles(resolvedRoot);
|
|
382
|
+
const files = discoveredFiles.slice(0, maxFiles);
|
|
383
|
+
const nodeManifests = await collectPackageManifests(resolvedRoot, files);
|
|
384
|
+
const otherManifests = await collectOtherManifests(files);
|
|
385
|
+
const manifests = sortByPath([...nodeManifests, ...otherManifests]);
|
|
386
|
+
const frameworks = detectFrameworks(manifests);
|
|
387
|
+
const artifact = {
|
|
388
|
+
schemaVersion: PROJECT_MAP_SCHEMA_VERSION,
|
|
389
|
+
generatedAt,
|
|
390
|
+
root: {
|
|
391
|
+
name: path.basename(resolvedRoot),
|
|
392
|
+
pathHash: crypto.createHash('sha256').update(resolvedRoot.toLowerCase()).digest('hex'),
|
|
393
|
+
},
|
|
394
|
+
limits: {
|
|
395
|
+
maxFiles,
|
|
396
|
+
filesObserved: files.length,
|
|
397
|
+
filesDiscovered: discoveredFiles.length,
|
|
398
|
+
truncated: discoveredFiles.length > maxFiles,
|
|
399
|
+
},
|
|
400
|
+
packageManagers: packageManagerFromFiles(files, nodeManifests),
|
|
401
|
+
workspace: buildWorkspaceSummary(manifests, files),
|
|
402
|
+
manifests: manifests.map(({ scripts, ...manifest }) => manifest),
|
|
403
|
+
frameworks,
|
|
404
|
+
runtimes: detectRuntimes(files, manifests, frameworks),
|
|
405
|
+
languages: detectLanguages(files),
|
|
406
|
+
routeFiles: files
|
|
407
|
+
.filter(isRouteLike)
|
|
408
|
+
.map((file) => ({ path: file, kind: routeKind(file) }))
|
|
409
|
+
.sort((a, b) => a.path.localeCompare(b.path)),
|
|
410
|
+
testCommands: nodeManifests.flatMap((manifest) => manifest.scripts || [])
|
|
411
|
+
.sort((a, b) => `${a.source}:${a.name}`.localeCompare(`${b.source}:${b.name}`)),
|
|
412
|
+
deploymentFiles: detectDeploymentFiles(files),
|
|
413
|
+
envFiles: detectEnvFiles(files),
|
|
414
|
+
privacy: {
|
|
415
|
+
envValuesCaptured: false,
|
|
416
|
+
sourceBodiesCaptured: false,
|
|
417
|
+
rawSourceStored: false,
|
|
418
|
+
artifactStorage: 'local_metadata_and_hash_refs',
|
|
419
|
+
redactionStatus: 'manifest_commands_redacted_env_values_unread',
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
assertSourceSafePayload(artifact, 'projectMapArtifact');
|
|
424
|
+
return cloneSourceSafe(artifact, 'projectMapArtifact');
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export async function writeProjectMapArtifact({ rootPath, runId, artifact }) {
|
|
428
|
+
assertSourceSafePayload(artifact, 'projectMapArtifact');
|
|
429
|
+
const safeRunId = validateRunIdSegment(runId, 'runId');
|
|
430
|
+
const relativeUri = `.vibesecur/deep-scans/${safeRunId}/project-map.json`;
|
|
431
|
+
const target = path.join(path.resolve(rootPath), relativeUri);
|
|
432
|
+
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
433
|
+
const serialized = `${JSON.stringify(stableSort(artifact), null, 2)}\n`;
|
|
434
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
435
|
+
await fs.writeFile(tmp, serialized, 'utf8');
|
|
436
|
+
await fs.rename(tmp, target);
|
|
437
|
+
return {
|
|
438
|
+
uri: relativeUri,
|
|
439
|
+
hash: crypto.createHash('sha256').update(serialized).digest('hex'),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function projectMapStepper() {
|
|
444
|
+
return {
|
|
445
|
+
id: PROJECT_MAP_STEPPER_ID,
|
|
446
|
+
version: PROJECT_MAP_STEPPER_VERSION,
|
|
447
|
+
title: 'Project Map Stepper',
|
|
448
|
+
category: 'project_map',
|
|
449
|
+
requiredInputs: ['bound_project_root'],
|
|
450
|
+
producedArtifacts: ['project_map'],
|
|
451
|
+
defaultTimeoutMs: 30000,
|
|
452
|
+
async run({ runId, config = {}, tools = {} }) {
|
|
453
|
+
const rootPath = tools.rootPath || config.rootPath || '.';
|
|
454
|
+
const startedAt = nowIso();
|
|
455
|
+
const artifact = await buildProjectMapArtifact({
|
|
456
|
+
rootPath,
|
|
457
|
+
generatedAt: config.generatedAt || startedAt,
|
|
458
|
+
maxFiles: Number.isFinite(Number(config.maxFiles)) ? Number(config.maxFiles) : 5000,
|
|
459
|
+
});
|
|
460
|
+
const contentHash = hashProjectMapArtifact(artifact);
|
|
461
|
+
const written = await writeProjectMapArtifact({ rootPath, runId, artifact });
|
|
462
|
+
const safeRunId = validateRunIdSegment(runId, 'runId');
|
|
463
|
+
const ref = createArtifactRef({
|
|
464
|
+
id: `artifact-${safeRunId}-project-map`,
|
|
465
|
+
type: 'project_map',
|
|
466
|
+
storage: 'local',
|
|
467
|
+
uri: written.uri,
|
|
468
|
+
hash: written.hash,
|
|
469
|
+
preview: 'Project Map metadata: manifests, frameworks, languages, route/deploy/env filenames only.',
|
|
470
|
+
metadata: {
|
|
471
|
+
schemaVersion: PROJECT_MAP_SCHEMA_VERSION,
|
|
472
|
+
contentHash,
|
|
473
|
+
generatedBy: PROJECT_MAP_STEPPER_ID,
|
|
474
|
+
redactionStatus: artifact.privacy.redactionStatus,
|
|
475
|
+
packageManagers: artifact.packageManagers,
|
|
476
|
+
frameworkCount: artifact.frameworks.length,
|
|
477
|
+
routeFileCount: artifact.routeFiles.length,
|
|
478
|
+
envFileCount: artifact.envFiles.length,
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
stepperId: PROJECT_MAP_STEPPER_ID,
|
|
484
|
+
version: PROJECT_MAP_STEPPER_VERSION,
|
|
485
|
+
status: 'passed',
|
|
486
|
+
startedAt,
|
|
487
|
+
finishedAt: nowIso(),
|
|
488
|
+
evidence: [{
|
|
489
|
+
type: 'hash',
|
|
490
|
+
label: 'Project Map artifact hash',
|
|
491
|
+
hash: ref.hash,
|
|
492
|
+
preview: 'Metadata-only Project Map artifact written locally.',
|
|
493
|
+
summary: `${artifact.manifests.length} manifest(s), ${artifact.frameworks.length} framework signal(s), ${artifact.routeFiles.length} route-like file(s).`,
|
|
494
|
+
metadata: {
|
|
495
|
+
contentHash,
|
|
496
|
+
envValuesCaptured: false,
|
|
497
|
+
sourceBodiesCaptured: false,
|
|
498
|
+
},
|
|
499
|
+
}],
|
|
500
|
+
findings: [],
|
|
501
|
+
artifacts: [ref],
|
|
502
|
+
summary: 'Project Map artifact generated with manifest, framework, route, test command, deployment, and env filename metadata.',
|
|
503
|
+
nextActions: ['Use the Project Map artifact to target deterministic scan, test planning, report, and passport steppers.'],
|
|
504
|
+
};
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|