barrikade-lens 0.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 +117 -0
- package/bin/cli.js +37 -0
- package/package.json +51 -0
- package/src/exporters/html-exporter.js +683 -0
- package/src/exporters/json-exporter.js +17 -0
- package/src/runner.js +189 -0
- package/src/scanners/config-auditor.js +550 -0
- package/src/scanners/env-scanner.js +72 -0
- package/src/scanners/history-scanner.js +107 -0
- package/src/scanners/port-scanner.js +129 -0
- package/src/scanners/process-scanner.js +103 -0
- package/src/scanners/secret-scanner.js +88 -0
- package/src/ui/banner.js +26 -0
- package/src/ui/dashboard.js +67 -0
- package/src/ui/summary.js +139 -0
- package/src/ui/tables.js +258 -0
- package/src/utils/analyzer.js +418 -0
- package/src/utils/paths.js +293 -0
- package/src/utils/patterns.js +148 -0
- package/src/utils/telemetry.js +217 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getScanPaths, getAgentStateDirs, getAgentRuleFiles, getModelDirs } from '../utils/paths.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parses simple TOML content line by line (used for Codex CLI config.toml).
|
|
8
|
+
* Extracts mcp_servers blocks.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} tomlContent
|
|
11
|
+
* @returns {any}
|
|
12
|
+
*/
|
|
13
|
+
function parseToml(tomlContent) {
|
|
14
|
+
try {
|
|
15
|
+
const lines = tomlContent.split(/\r?\n/);
|
|
16
|
+
const data = { mcpServers: {} };
|
|
17
|
+
let currentServer = null;
|
|
18
|
+
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
const trimmed = line.trim();
|
|
21
|
+
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith(';')) continue;
|
|
22
|
+
|
|
23
|
+
const headerMatch = trimmed.match(/^\[mcp_servers\.([^\]]+)\]/);
|
|
24
|
+
if (headerMatch) {
|
|
25
|
+
currentServer = headerMatch[1].trim();
|
|
26
|
+
data.mcpServers[currentServer] = { command: '', args: [], env: {} };
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
31
|
+
currentServer = null;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (currentServer && data.mcpServers[currentServer]) {
|
|
36
|
+
const eqIdx = trimmed.indexOf('=');
|
|
37
|
+
if (eqIdx !== -1) {
|
|
38
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
39
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
40
|
+
|
|
41
|
+
if (key === 'command') {
|
|
42
|
+
data.mcpServers[currentServer].command = val.replace(/^['"]|['"]$/g, '');
|
|
43
|
+
} else if (key === 'args') {
|
|
44
|
+
if (val.startsWith('[') && val.endsWith(']')) {
|
|
45
|
+
data.mcpServers[currentServer].args = val
|
|
46
|
+
.slice(1, -1)
|
|
47
|
+
.split(',')
|
|
48
|
+
.map(s => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
49
|
+
.filter(s => s !== '');
|
|
50
|
+
}
|
|
51
|
+
} else if (key === 'env') {
|
|
52
|
+
if (val.startsWith('{') && val.endsWith('}')) {
|
|
53
|
+
const body = val.slice(1, -1);
|
|
54
|
+
const pairs = body.split(',');
|
|
55
|
+
for (const pair of pairs) {
|
|
56
|
+
const eq = pair.indexOf('=');
|
|
57
|
+
if (eq !== -1) {
|
|
58
|
+
const k = pair.slice(0, eq).trim();
|
|
59
|
+
const v = pair.slice(eq + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
60
|
+
data.mcpServers[currentServer].env[k] = v;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return data;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
throw new Error('Malformed TOML: ' + err.message);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parses simple YAML content line by line (used for Goose, Aider, and Continue).
|
|
76
|
+
* Extracts mcpServers or extensions blocks.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} yamlContent
|
|
79
|
+
* @returns {any}
|
|
80
|
+
*/
|
|
81
|
+
function parseYaml(yamlContent) {
|
|
82
|
+
try {
|
|
83
|
+
const lines = yamlContent.split(/\r?\n/);
|
|
84
|
+
const data = { mcpServers: {} };
|
|
85
|
+
let currentServer = null;
|
|
86
|
+
let serverIndent = -1;
|
|
87
|
+
let inMcp = false;
|
|
88
|
+
let mcpIndent = -1;
|
|
89
|
+
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
const trimmed = line.trim();
|
|
92
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
93
|
+
|
|
94
|
+
const indent = line.length - line.trimStart().length;
|
|
95
|
+
|
|
96
|
+
if (inMcp && indent <= mcpIndent && trimmed && !trimmed.startsWith('-') && !trimmed.includes(':')) {
|
|
97
|
+
inMcp = false;
|
|
98
|
+
currentServer = null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!inMcp) {
|
|
102
|
+
const colonIdx = trimmed.indexOf(':');
|
|
103
|
+
if (colonIdx !== -1) {
|
|
104
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
105
|
+
if (key === 'mcpServers' || key === 'mcp_servers' || key === 'servers' || key === 'extensions') {
|
|
106
|
+
inMcp = true;
|
|
107
|
+
mcpIndent = indent;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Inside MCP block
|
|
114
|
+
if (currentServer && indent <= serverIndent && !trimmed.startsWith('-')) {
|
|
115
|
+
currentServer = null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!currentServer && trimmed.endsWith(':')) {
|
|
119
|
+
currentServer = trimmed.slice(0, -1).trim();
|
|
120
|
+
serverIndent = indent;
|
|
121
|
+
data.mcpServers[currentServer] = { command: '', args: [], env: {} };
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (currentServer) {
|
|
126
|
+
const srv = data.mcpServers[currentServer];
|
|
127
|
+
|
|
128
|
+
if (trimmed.startsWith('-')) {
|
|
129
|
+
const val = trimmed.slice(1).trim().replace(/^['"]|['"]$/g, '');
|
|
130
|
+
if (val) {
|
|
131
|
+
srv.args.push(val);
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const colonIdx = trimmed.indexOf(':');
|
|
137
|
+
if (colonIdx !== -1) {
|
|
138
|
+
const key = trimmed.slice(0, colonIdx).trim();
|
|
139
|
+
const val = trimmed.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, '');
|
|
140
|
+
|
|
141
|
+
if (key === 'command' || key === 'cmd') {
|
|
142
|
+
srv.command = val;
|
|
143
|
+
} else if (key === 'args') {
|
|
144
|
+
if (val && val.startsWith('[') && val.endsWith(']')) {
|
|
145
|
+
srv.args = val
|
|
146
|
+
.slice(1, -1)
|
|
147
|
+
.split(',')
|
|
148
|
+
.map(s => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
149
|
+
.filter(s => s);
|
|
150
|
+
}
|
|
151
|
+
} else if (key === 'enabled' && val === 'false') {
|
|
152
|
+
srv.disabled = true;
|
|
153
|
+
} else if (key === 'url' || key === 'serverUrl' || key === 'server_url') {
|
|
154
|
+
srv.url = val;
|
|
155
|
+
} else if (indent > serverIndent) {
|
|
156
|
+
// Check for env properties
|
|
157
|
+
if (key !== 'env') {
|
|
158
|
+
srv.env[key] = val;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return data;
|
|
165
|
+
} catch (err) {
|
|
166
|
+
throw new Error('Malformed YAML: ' + err.message);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Sweeps the filesystem to locate JetBrains configuration files.
|
|
172
|
+
*/
|
|
173
|
+
async function discoverJetBrainsPaths() {
|
|
174
|
+
const home = os.homedir();
|
|
175
|
+
const platform = os.platform();
|
|
176
|
+
let jbBase = '';
|
|
177
|
+
|
|
178
|
+
if (platform === 'darwin') {
|
|
179
|
+
jbBase = path.join(home, 'Library', 'Application Support', 'JetBrains');
|
|
180
|
+
} else if (platform === 'win32') {
|
|
181
|
+
jbBase = path.join(process.env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'JetBrains');
|
|
182
|
+
} else {
|
|
183
|
+
jbBase = path.join(home, '.config', 'JetBrains');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const jbPaths = [];
|
|
187
|
+
try {
|
|
188
|
+
const entries = await fs.readdir(jbBase, { withFileTypes: true });
|
|
189
|
+
for (const entry of entries) {
|
|
190
|
+
if (entry.isDirectory()) {
|
|
191
|
+
const xmlPath = path.join(jbBase, entry.name, 'options', 'llm.mcpServers.xml');
|
|
192
|
+
try {
|
|
193
|
+
const stats = await fs.stat(xmlPath);
|
|
194
|
+
if (stats.isFile()) {
|
|
195
|
+
jbPaths.push({
|
|
196
|
+
tool: `JetBrains (${entry.name})`,
|
|
197
|
+
path: xmlPath,
|
|
198
|
+
scope: 'global',
|
|
199
|
+
type: 'jetbrains'
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
} catch {
|
|
203
|
+
// Skip
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
} catch {
|
|
208
|
+
// JetBrains folder doesn't exist
|
|
209
|
+
}
|
|
210
|
+
return jbPaths;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Dynamically checks for server configurations inside .continue/mcpServers/ directory.
|
|
215
|
+
*
|
|
216
|
+
* @param {string} cwd
|
|
217
|
+
* @returns {Promise<Array<{ tool: string, path: string, scope: 'global' | 'project', type: string }>>}
|
|
218
|
+
*/
|
|
219
|
+
async function discoverContinueMcpServers(cwd = process.cwd()) {
|
|
220
|
+
const home = os.homedir();
|
|
221
|
+
const dirsToCheck = [
|
|
222
|
+
{ dir: path.join(home, '.continue', 'mcpServers'), scope: 'global' },
|
|
223
|
+
{ dir: path.join(cwd, '.continue', 'mcpServers'), scope: 'project' }
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
const foundConfigs = [];
|
|
227
|
+
|
|
228
|
+
for (const { dir, scope } of dirsToCheck) {
|
|
229
|
+
try {
|
|
230
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
if (entry.isFile()) {
|
|
233
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
234
|
+
if (ext === '.json' || ext === '.yaml' || ext === '.yml' || ext === '.jsonc') {
|
|
235
|
+
const fullPath = path.join(dir, entry.name);
|
|
236
|
+
const parserType = (ext === '.yaml' || ext === '.yml') ? 'yaml' : (ext === '.jsonc' ? 'jsonc' : 'mcpServers');
|
|
237
|
+
foundConfigs.push({
|
|
238
|
+
tool: `Continue MCP Server Config (${entry.name})`,
|
|
239
|
+
path: fullPath,
|
|
240
|
+
scope,
|
|
241
|
+
type: parserType
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// Directory doesn't exist, skip
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return foundConfigs;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Audits all configuration files on the workstation.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} [cwd=process.cwd()]
|
|
258
|
+
*/
|
|
259
|
+
export async function auditConfigs(cwd = process.cwd()) {
|
|
260
|
+
const resolvedPaths = getScanPaths(cwd);
|
|
261
|
+
const jbPaths = await discoverJetBrainsPaths();
|
|
262
|
+
const continuePaths = await discoverContinueMcpServers(cwd);
|
|
263
|
+
const allScanPaths = [...resolvedPaths, ...jbPaths, ...continuePaths];
|
|
264
|
+
|
|
265
|
+
const results = [];
|
|
266
|
+
|
|
267
|
+
for (const scanConfig of allScanPaths) {
|
|
268
|
+
const targetPath = scanConfig.path;
|
|
269
|
+
const result = {
|
|
270
|
+
tool: scanConfig.tool,
|
|
271
|
+
filePath: targetPath,
|
|
272
|
+
scope: scanConfig.scope,
|
|
273
|
+
exists: false,
|
|
274
|
+
malformed: false,
|
|
275
|
+
rawContent: '',
|
|
276
|
+
servers: []
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const content = await fs.readFile(targetPath, 'utf8');
|
|
281
|
+
result.exists = true;
|
|
282
|
+
result.rawContent = content;
|
|
283
|
+
|
|
284
|
+
if (scanConfig.type === 'jetbrains') {
|
|
285
|
+
const braveModeMatches = content.includes('braveMode" value="true"') || content.includes('name="braveMode" value="true"');
|
|
286
|
+
const mcpServerInfos = content.match(/<McpServerInfo>([\s\S]*?)<\/McpServerInfo>/g) || [];
|
|
287
|
+
|
|
288
|
+
const servers = [];
|
|
289
|
+
for (const info of mcpServerInfos) {
|
|
290
|
+
const nameMatch = info.match(/name="name" value="([^"]+)"/);
|
|
291
|
+
const commandMatch = info.match(/name="command" value="([^"]+)"/);
|
|
292
|
+
const braveModeMatch = info.match(/name="braveMode" value="([^"]+)"/);
|
|
293
|
+
|
|
294
|
+
servers.push({
|
|
295
|
+
name: nameMatch ? nameMatch[1] : 'JetBrains MCP Server',
|
|
296
|
+
type: 'jetbrains',
|
|
297
|
+
command: commandMatch ? commandMatch[1] : undefined,
|
|
298
|
+
braveMode: braveModeMatch ? braveModeMatch[1] === 'true' : braveModeMatches
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (servers.length === 0 && content.includes('braveMode')) {
|
|
303
|
+
servers.push({
|
|
304
|
+
name: 'JetBrains Global Config',
|
|
305
|
+
type: 'jetbrains',
|
|
306
|
+
braveMode: braveModeMatches
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
result.servers = servers;
|
|
311
|
+
} else {
|
|
312
|
+
// Parse YAML, TOML, or JSON
|
|
313
|
+
let json;
|
|
314
|
+
try {
|
|
315
|
+
if (scanConfig.type === 'toml') {
|
|
316
|
+
json = parseToml(content);
|
|
317
|
+
} else if (scanConfig.type === 'yaml') {
|
|
318
|
+
json = parseYaml(content);
|
|
319
|
+
} else {
|
|
320
|
+
// Strip JSON/JSONC comments and trailing commas
|
|
321
|
+
const cleanContent = content
|
|
322
|
+
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '$1')
|
|
323
|
+
.replace(/,(\s*[\]}])/g, '$1');
|
|
324
|
+
json = JSON.parse(cleanContent);
|
|
325
|
+
}
|
|
326
|
+
} catch {
|
|
327
|
+
result.malformed = true;
|
|
328
|
+
results.push(result);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
let mcpConfig = null;
|
|
333
|
+
|
|
334
|
+
if (scanConfig.type === 'toml' || scanConfig.type === 'yaml') {
|
|
335
|
+
mcpConfig = json.mcpServers;
|
|
336
|
+
} else if (scanConfig.type === 'mcpServers') {
|
|
337
|
+
mcpConfig = json.mcpServers;
|
|
338
|
+
} else if (scanConfig.type === 'vscodeServers') {
|
|
339
|
+
mcpConfig = json.servers || json.mcpServers;
|
|
340
|
+
} else if (scanConfig.type === 'vscodeSettings') {
|
|
341
|
+
// VS Code settings can have mcp config in custom properties
|
|
342
|
+
mcpConfig = json['mcp.servers'] || json['mcpServers'] || json['augment.advanced.mcpServers'] || (json['augment.advanced'] && json['augment.advanced'].mcpServers);
|
|
343
|
+
} else if (scanConfig.type === 'continue') {
|
|
344
|
+
mcpConfig = json.mcpServers;
|
|
345
|
+
} else if (scanConfig.type === 'zed') {
|
|
346
|
+
mcpConfig = json.context_servers || json.mcp;
|
|
347
|
+
} else if (scanConfig.type === 'antigravity' || scanConfig.type === 'antigravityCli') {
|
|
348
|
+
mcpConfig = json.mcp_servers || json.mcpServers || json.servers;
|
|
349
|
+
} else if (scanConfig.type === 'opencode' || scanConfig.type === 'jsonc') {
|
|
350
|
+
mcpConfig = json.mcpServers || json.mcp_servers || json.servers;
|
|
351
|
+
} else if (scanConfig.type === 'openclaw') {
|
|
352
|
+
mcpConfig = json.mcpServers || json.mcp_servers || json.servers;
|
|
353
|
+
} else if (scanConfig.type === 'codex') {
|
|
354
|
+
mcpConfig = json.mcpServers || json.mcp_servers || json.servers;
|
|
355
|
+
} else if (scanConfig.type === 'amazonq') {
|
|
356
|
+
mcpConfig = json.mcpServers || json.mcp_servers || json.servers;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (mcpConfig) {
|
|
360
|
+
if (Array.isArray(mcpConfig)) {
|
|
361
|
+
mcpConfig.forEach((srv, idx) => {
|
|
362
|
+
const name = srv.name || `server-${idx}`;
|
|
363
|
+
const envKeys = srv.env ? Object.keys(srv.env) : [];
|
|
364
|
+
const type = srv.url || srv.serverUrl ? 'sse' : 'stdio';
|
|
365
|
+
|
|
366
|
+
result.servers.push({
|
|
367
|
+
name,
|
|
368
|
+
type,
|
|
369
|
+
command: srv.command || srv.cmd,
|
|
370
|
+
args: srv.args || [],
|
|
371
|
+
envVars: envKeys,
|
|
372
|
+
url: srv.url || srv.serverUrl,
|
|
373
|
+
disabled: srv.disabled === true || srv.enabled === false
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
} else if (typeof mcpConfig === 'object') {
|
|
377
|
+
for (const [name, srv] of Object.entries(mcpConfig)) {
|
|
378
|
+
if (srv && typeof srv === 'object') {
|
|
379
|
+
const envKeys = srv.env ? Object.keys(srv.env) : [];
|
|
380
|
+
const sseUrl = srv.url || srv.serverUrl || srv.server_url;
|
|
381
|
+
const type = sseUrl ? 'sse' : 'stdio';
|
|
382
|
+
|
|
383
|
+
result.servers.push({
|
|
384
|
+
name,
|
|
385
|
+
type,
|
|
386
|
+
command: srv.command || srv.cmd,
|
|
387
|
+
args: srv.args || [],
|
|
388
|
+
envVars: envKeys,
|
|
389
|
+
url: sseUrl,
|
|
390
|
+
disabled: srv.disabled === true || srv.enabled === false,
|
|
391
|
+
autoApprove: Array.isArray(srv.autoApprove) ? srv.autoApprove : undefined
|
|
392
|
+
});
|
|
393
|
+
} else if (typeof srv === 'string') {
|
|
394
|
+
result.servers.push({
|
|
395
|
+
name,
|
|
396
|
+
type: 'sse',
|
|
397
|
+
url: srv
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
results.push(result);
|
|
406
|
+
} catch {
|
|
407
|
+
// File does not exist, skip
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return results;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Checks for the existence of agent state directories, rule files, and model directories.
|
|
416
|
+
*
|
|
417
|
+
* @param {string} [cwd=process.cwd()]
|
|
418
|
+
* @returns {Promise<{
|
|
419
|
+
* detectedStateDirs: string[],
|
|
420
|
+
* detectedRuleFiles: string[],
|
|
421
|
+
* detectedModelDirs: string[]
|
|
422
|
+
* }>}
|
|
423
|
+
*/
|
|
424
|
+
export async function auditWorkspaceArtifacts(cwd = process.cwd()) {
|
|
425
|
+
const stateDirs = getAgentStateDirs(cwd);
|
|
426
|
+
const ruleFiles = getAgentRuleFiles(cwd);
|
|
427
|
+
const modelDirs = getModelDirs();
|
|
428
|
+
|
|
429
|
+
const detectedStateDirs = [];
|
|
430
|
+
const detectedRuleFiles = [];
|
|
431
|
+
const detectedModelDirs = [];
|
|
432
|
+
|
|
433
|
+
// Check state dirs
|
|
434
|
+
for (const dir of stateDirs) {
|
|
435
|
+
try {
|
|
436
|
+
const stat = await fs.stat(dir.path);
|
|
437
|
+
if (stat.isDirectory()) {
|
|
438
|
+
detectedStateDirs.push(`${dir.name} directory (${dir.scope === 'global' ? 'Global' : 'Project'})`);
|
|
439
|
+
}
|
|
440
|
+
} catch {
|
|
441
|
+
// Doesn't exist
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Check rule files
|
|
446
|
+
for (const file of ruleFiles) {
|
|
447
|
+
try {
|
|
448
|
+
const stat = await fs.stat(file.path);
|
|
449
|
+
if (stat.isFile()) {
|
|
450
|
+
detectedRuleFiles.push(file.name);
|
|
451
|
+
} else if (stat.isDirectory()) {
|
|
452
|
+
detectedRuleFiles.push(`${file.name}/ (rules folder)`);
|
|
453
|
+
}
|
|
454
|
+
} catch {
|
|
455
|
+
// Doesn't exist
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// Check model dirs
|
|
460
|
+
for (const dir of modelDirs) {
|
|
461
|
+
try {
|
|
462
|
+
const stat = await fs.stat(dir.path);
|
|
463
|
+
if (stat.isDirectory()) {
|
|
464
|
+
detectedModelDirs.push(dir.name);
|
|
465
|
+
}
|
|
466
|
+
} catch {
|
|
467
|
+
// Doesn't exist
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
detectedStateDirs,
|
|
473
|
+
detectedRuleFiles,
|
|
474
|
+
detectedModelDirs
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Known agent frameworks to match in dependencies
|
|
479
|
+
const AGENT_FRAMEWORKS = [
|
|
480
|
+
'langchain',
|
|
481
|
+
'langgraph',
|
|
482
|
+
'crewai',
|
|
483
|
+
'autogen',
|
|
484
|
+
'pydanticai',
|
|
485
|
+
'smolagents',
|
|
486
|
+
'semantic-kernel',
|
|
487
|
+
'haystack',
|
|
488
|
+
'llama-index',
|
|
489
|
+
'mastra',
|
|
490
|
+
'voltagent'
|
|
491
|
+
];
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Inspects project files for agent framework dependencies (Tier 2).
|
|
495
|
+
*
|
|
496
|
+
* @param {string} [cwd=process.cwd()]
|
|
497
|
+
* @returns {Promise<string[]>} List of discovered agent frameworks
|
|
498
|
+
*/
|
|
499
|
+
export async function auditDependencies(cwd = process.cwd()) {
|
|
500
|
+
const discovered = [];
|
|
501
|
+
|
|
502
|
+
// 1. Check package.json (JS/TS)
|
|
503
|
+
try {
|
|
504
|
+
const pkgContent = await fs.readFile(path.join(cwd, 'package.json'), 'utf8');
|
|
505
|
+
const pkg = JSON.parse(pkgContent);
|
|
506
|
+
const deps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
507
|
+
|
|
508
|
+
for (const name of Object.keys(deps)) {
|
|
509
|
+
const normalized = name.toLowerCase();
|
|
510
|
+
for (const fw of AGENT_FRAMEWORKS) {
|
|
511
|
+
if (normalized.includes(fw)) {
|
|
512
|
+
discovered.push(`${fw} (JS/TS package)`);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} catch {
|
|
517
|
+
// package.json doesn't exist or is malformed
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// 2. Check requirements.txt (Python)
|
|
521
|
+
try {
|
|
522
|
+
const reqs = await fs.readFile(path.join(cwd, 'requirements.txt'), 'utf8');
|
|
523
|
+
const lines = reqs.toLowerCase().split('\n');
|
|
524
|
+
for (const line of lines) {
|
|
525
|
+
const trimmed = line.split(/[=<>]/)[0].trim();
|
|
526
|
+
for (const fw of AGENT_FRAMEWORKS) {
|
|
527
|
+
if (trimmed === fw || trimmed.replace('-', '') === fw) {
|
|
528
|
+
discovered.push(`${fw} (Python library)`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
// requirements.txt doesn't exist
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// 3. Check pyproject.toml (Python)
|
|
537
|
+
try {
|
|
538
|
+
const toml = await fs.readFile(path.join(cwd, 'pyproject.toml'), 'utf8');
|
|
539
|
+
const tomlLower = toml.toLowerCase();
|
|
540
|
+
for (const fw of AGENT_FRAMEWORKS) {
|
|
541
|
+
if (tomlLower.includes(fw)) {
|
|
542
|
+
discovered.push(`${fw} (Python dependency)`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
} catch {
|
|
546
|
+
// pyproject.toml doesn't exist
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return Array.from(new Set(discovered));
|
|
550
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { scanStringForSecrets } from '../utils/patterns.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scans workspace .env files for hardcoded API keys and secrets.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} [cwd=process.cwd()] Working directory to scan
|
|
9
|
+
* @returns {Promise<Array<{
|
|
10
|
+
* filePath: string,
|
|
11
|
+
* tool: string,
|
|
12
|
+
* type: string,
|
|
13
|
+
* matched: string,
|
|
14
|
+
* line: number,
|
|
15
|
+
* risk: 'CRITICAL' | 'HIGH' | 'MEDIUM',
|
|
16
|
+
* remediation: string
|
|
17
|
+
* }>>}
|
|
18
|
+
*/
|
|
19
|
+
export async function scanEnvFiles(cwd = process.cwd()) {
|
|
20
|
+
const findings = [];
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const files = await fs.readdir(cwd);
|
|
24
|
+
const envFiles = files.filter(f => f === '.env' || (f.startsWith('.env.') && !f.endsWith('.example')));
|
|
25
|
+
|
|
26
|
+
for (const fileName of envFiles) {
|
|
27
|
+
const filePath = path.join(cwd, fileName);
|
|
28
|
+
try {
|
|
29
|
+
const stats = await fs.stat(filePath);
|
|
30
|
+
if (!stats.isFile()) continue;
|
|
31
|
+
|
|
32
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
33
|
+
const lines = content.split('\n');
|
|
34
|
+
|
|
35
|
+
lines.forEach((lineText, lineIdx) => {
|
|
36
|
+
const lineNum = lineIdx + 1;
|
|
37
|
+
const trimmed = lineText.trim();
|
|
38
|
+
|
|
39
|
+
// Skip comments and empty lines
|
|
40
|
+
if (trimmed.startsWith('#') || trimmed === '') return;
|
|
41
|
+
|
|
42
|
+
// Parse key=value
|
|
43
|
+
const eqIdx = trimmed.indexOf('=');
|
|
44
|
+
if (eqIdx === -1) return;
|
|
45
|
+
|
|
46
|
+
const key = trimmed.substring(0, eqIdx).trim();
|
|
47
|
+
const val = trimmed.substring(eqIdx + 1).trim().replace(/^['"]|['"]$/g, ''); // strip quotes
|
|
48
|
+
|
|
49
|
+
// Search value for credentials
|
|
50
|
+
const secretsFound = scanStringForSecrets(val);
|
|
51
|
+
for (const s of secretsFound) {
|
|
52
|
+
findings.push({
|
|
53
|
+
filePath,
|
|
54
|
+
tool: `.env file (${fileName})`,
|
|
55
|
+
type: `${s.type} in ${key}`,
|
|
56
|
+
matched: s.matched,
|
|
57
|
+
line: lineNum,
|
|
58
|
+
risk: s.risk,
|
|
59
|
+
remediation: `Remove hardcoded credentials from ${fileName}. Read keys from system env variables instead.`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
} catch {
|
|
64
|
+
// Skip file if can't read
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// Current directory can't be read or has no files
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return findings;
|
|
72
|
+
}
|