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.
@@ -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
+ }