claude-flow 3.5.71 → 3.5.73
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 +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/plugins.js +6 -0
- package/v3/@claude-flow/cli/dist/src/commands/security.js +350 -34
- package/v3/@claude-flow/cli/dist/src/commands/swarm.js +100 -15
- package/v3/@claude-flow/cli/dist/src/init/claudemd-generator.js +15 -17
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +58 -13
- package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +49 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/performance-tools.js +7 -5
- package/v3/@claude-flow/cli/dist/src/mcp-tools/system-tools.js +53 -18
- package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.d.ts +21 -3
- package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.js +74 -7
- package/v3/@claude-flow/cli/dist/src/ruvector/vector-db.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/vector-db.js +22 -1
- package/v3/@claude-flow/cli/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-flow",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.73",
|
|
4
4
|
"description": "Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -113,12 +113,14 @@ const listCommand = {
|
|
|
113
113
|
output.writeln(output.dim('─'.repeat(70)));
|
|
114
114
|
// Fetch real ratings from Cloud Function (non-blocking)
|
|
115
115
|
let realRatings = {};
|
|
116
|
+
let ratingsSource = 'live';
|
|
116
117
|
try {
|
|
117
118
|
const pluginIds = plugins.map(p => p.name);
|
|
118
119
|
realRatings = await getBulkRatings(pluginIds, 'plugin');
|
|
119
120
|
}
|
|
120
121
|
catch {
|
|
121
122
|
// Fall back to static ratings if Cloud Function unavailable
|
|
123
|
+
ratingsSource = 'unavailable';
|
|
122
124
|
}
|
|
123
125
|
if (ctx.flags.format === 'json') {
|
|
124
126
|
// Merge real ratings into plugin data
|
|
@@ -126,6 +128,7 @@ const listCommand = {
|
|
|
126
128
|
...p,
|
|
127
129
|
rating: realRatings[p.name]?.average || p.rating,
|
|
128
130
|
ratingCount: realRatings[p.name]?.count || 0,
|
|
131
|
+
...(ratingsSource === 'unavailable' ? { ratingsSource: 'cached' } : {}),
|
|
129
132
|
}));
|
|
130
133
|
output.printJson(pluginsWithRatings);
|
|
131
134
|
return { success: true, data: pluginsWithRatings };
|
|
@@ -157,6 +160,9 @@ const listCommand = {
|
|
|
157
160
|
}),
|
|
158
161
|
});
|
|
159
162
|
output.writeln();
|
|
163
|
+
if (ratingsSource === 'unavailable') {
|
|
164
|
+
output.writeln(output.dim('(ratings: cached — cloud unavailable)'));
|
|
165
|
+
}
|
|
160
166
|
output.writeln(output.dim(`Source: ${result.source}${result.fromCache ? ' (cached)' : ''}`));
|
|
161
167
|
if (result.cid) {
|
|
162
168
|
output.writeln(output.dim(`Registry CID: ${result.cid.slice(0, 30)}...`));
|
|
@@ -293,11 +293,201 @@ const threatsCommand = {
|
|
|
293
293
|
],
|
|
294
294
|
action: async (ctx) => {
|
|
295
295
|
const model = ctx.flags.model || 'stride';
|
|
296
|
+
const scope = ctx.flags.scope || '.';
|
|
297
|
+
const exportFormat = ctx.flags.export;
|
|
296
298
|
output.writeln();
|
|
297
299
|
output.writeln(output.bold(`Threat Model: ${model.toUpperCase()}`));
|
|
298
300
|
output.writeln(output.dim('─'.repeat(50)));
|
|
299
|
-
output.
|
|
300
|
-
|
|
301
|
+
const spinner = output.createSpinner({ text: `Scanning ${scope} for threat indicators...`, spinner: 'dots' });
|
|
302
|
+
spinner.start();
|
|
303
|
+
const fs = await import('fs');
|
|
304
|
+
const path = await import('path');
|
|
305
|
+
const rootDir = path.resolve(scope);
|
|
306
|
+
const findings = [];
|
|
307
|
+
const extensions = new Set(['.ts', '.js', '.json', '.yaml', '.yml', '.tsx', '.jsx']);
|
|
308
|
+
const skipDirs = new Set(['node_modules', 'dist', '.git']);
|
|
309
|
+
let filesScanned = 0;
|
|
310
|
+
const MAX_FILES = 500;
|
|
311
|
+
// Threat indicator patterns mapped to STRIDE categories
|
|
312
|
+
const threatPatterns = [
|
|
313
|
+
// Spoofing — weak/missing authentication
|
|
314
|
+
{ pattern: /(?:app|router|server)\s*\.\s*(?:get|post|put|patch|delete)\s*\(\s*['"][^'"]+['"]\s*,\s*(?:async\s+)?\(?(?:req|request)/g, category: 'Spoofing', severity: 'medium', description: 'HTTP endpoint without auth middleware' },
|
|
315
|
+
// Tampering — code injection vectors
|
|
316
|
+
{ pattern: /\beval\s*\(/g, category: 'Tampering', severity: 'high', description: 'eval() usage — arbitrary code execution risk' },
|
|
317
|
+
{ pattern: /\bexecSync\s*\(/g, category: 'Tampering', severity: 'high', description: 'execSync() usage — command injection risk' },
|
|
318
|
+
{ pattern: /\bexec\s*\(\s*[^)]*\$\{/g, category: 'Tampering', severity: 'high', description: 'exec() with template literal — injection risk' },
|
|
319
|
+
{ pattern: /child_process.*\bexec\b/g, category: 'Tampering', severity: 'medium', description: 'child_process exec import — review for injection' },
|
|
320
|
+
{ pattern: /new\s+Function\s*\(/g, category: 'Tampering', severity: 'high', description: 'new Function() — dynamic code execution risk' },
|
|
321
|
+
// Repudiation — missing audit/logging
|
|
322
|
+
// (checked via absence of logging imports, handled separately)
|
|
323
|
+
// Info Disclosure — secrets and data leaks
|
|
324
|
+
{ pattern: /(?:api[_-]?key|secret|token|password|passwd|credential)\s*[:=]\s*['"][^'"]{8,}['"]/gi, category: 'Info Disclosure', severity: 'high', description: 'Hardcoded credential or secret' },
|
|
325
|
+
{ pattern: /AKIA[0-9A-Z]{16}/g, category: 'Info Disclosure', severity: 'critical', description: 'AWS Access Key ID detected' },
|
|
326
|
+
{ pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g, category: 'Info Disclosure', severity: 'high', description: 'GitHub token detected' },
|
|
327
|
+
{ pattern: /-----BEGIN (?:RSA|EC|DSA|OPENSSH) PRIVATE KEY-----/g, category: 'Info Disclosure', severity: 'critical', description: 'Private key detected' },
|
|
328
|
+
{ pattern: /http:\/\/(?!localhost|127\.0\.0\.1|0\.0\.0\.0)/g, category: 'Info Disclosure', severity: 'medium', description: 'Non-localhost HTTP URL — should use HTTPS' },
|
|
329
|
+
// DoS — missing rate limiting / resource protection
|
|
330
|
+
{ pattern: /require\s*\(\s*['"]express['"]\s*\)/g, category: 'DoS', severity: 'low', description: 'Express detected — verify rate-limiting is configured' },
|
|
331
|
+
{ pattern: /require\s*\(\s*['"]fastify['"]\s*\)/g, category: 'DoS', severity: 'low', description: 'Fastify detected — verify rate-limiting is configured' },
|
|
332
|
+
// Elevation of privilege — unsafe deserialization, prototype pollution
|
|
333
|
+
{ pattern: /JSON\.parse\s*\(\s*(?:req\.|request\.)/g, category: 'Elevation', severity: 'medium', description: 'Unsanitized JSON.parse from request — validate input' },
|
|
334
|
+
{ pattern: /\.__proto__/g, category: 'Elevation', severity: 'high', description: '__proto__ access — prototype pollution risk' },
|
|
335
|
+
{ pattern: /Object\.assign\s*\(\s*\{\s*\}\s*,\s*(?:req|request)\./g, category: 'Elevation', severity: 'medium', description: 'Object.assign from request — prototype pollution risk' },
|
|
336
|
+
];
|
|
337
|
+
// Check for .env files committed to git
|
|
338
|
+
const checkEnvInGit = () => {
|
|
339
|
+
try {
|
|
340
|
+
const { execSync } = require('child_process');
|
|
341
|
+
const tracked = execSync('git ls-files --cached', { cwd: rootDir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] });
|
|
342
|
+
const envFiles = tracked.split('\n').filter((f) => /(?:^|\/)\.env(?:\.|$)/.test(f));
|
|
343
|
+
for (const envFile of envFiles) {
|
|
344
|
+
findings.push({
|
|
345
|
+
category: 'Info Disclosure',
|
|
346
|
+
severity: output.error('CRITICAL'),
|
|
347
|
+
location: envFile,
|
|
348
|
+
description: '.env file tracked in git — secrets may be exposed',
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
catch { /* not a git repo or git not available */ }
|
|
353
|
+
};
|
|
354
|
+
// Recursive file scanner
|
|
355
|
+
const scanDir = (dir) => {
|
|
356
|
+
if (filesScanned >= MAX_FILES)
|
|
357
|
+
return;
|
|
358
|
+
let entries;
|
|
359
|
+
try {
|
|
360
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
for (const entry of entries) {
|
|
366
|
+
if (filesScanned >= MAX_FILES)
|
|
367
|
+
break;
|
|
368
|
+
if (skipDirs.has(entry.name) || entry.name.startsWith('.'))
|
|
369
|
+
continue;
|
|
370
|
+
const fullPath = path.join(dir, entry.name);
|
|
371
|
+
if (entry.isDirectory()) {
|
|
372
|
+
scanDir(fullPath);
|
|
373
|
+
}
|
|
374
|
+
else if (entry.isFile() && extensions.has(path.extname(entry.name)) && !entry.name.endsWith('.d.ts')) {
|
|
375
|
+
filesScanned++;
|
|
376
|
+
try {
|
|
377
|
+
const stat = fs.statSync(fullPath);
|
|
378
|
+
if (stat.size > 1024 * 1024)
|
|
379
|
+
continue; // skip files > 1MB
|
|
380
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
381
|
+
const lines = content.split('\n');
|
|
382
|
+
const relPath = path.relative(rootDir, fullPath);
|
|
383
|
+
for (let i = 0; i < lines.length; i++) {
|
|
384
|
+
for (const tp of threatPatterns) {
|
|
385
|
+
tp.pattern.lastIndex = 0;
|
|
386
|
+
if (tp.pattern.test(lines[i])) {
|
|
387
|
+
const sevLabel = tp.severity === 'critical' ? output.error('CRITICAL') :
|
|
388
|
+
tp.severity === 'high' ? output.warning('HIGH') :
|
|
389
|
+
tp.severity === 'medium' ? output.warning('MEDIUM') : output.info('LOW');
|
|
390
|
+
findings.push({
|
|
391
|
+
category: tp.category,
|
|
392
|
+
severity: sevLabel,
|
|
393
|
+
location: `${relPath}:${i + 1}`,
|
|
394
|
+
description: tp.description,
|
|
395
|
+
});
|
|
396
|
+
tp.pattern.lastIndex = 0;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
catch { /* file read error */ }
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
// Check for missing security middleware in Express/Fastify apps
|
|
406
|
+
const checkMissingMiddleware = () => {
|
|
407
|
+
const serverFiles = [];
|
|
408
|
+
const collectServerFiles = (dir, depth) => {
|
|
409
|
+
if (depth <= 0 || filesScanned >= MAX_FILES)
|
|
410
|
+
return;
|
|
411
|
+
try {
|
|
412
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
413
|
+
for (const entry of entries) {
|
|
414
|
+
if (skipDirs.has(entry.name) || entry.name.startsWith('.'))
|
|
415
|
+
continue;
|
|
416
|
+
const fullPath = path.join(dir, entry.name);
|
|
417
|
+
if (entry.isDirectory()) {
|
|
418
|
+
collectServerFiles(fullPath, depth - 1);
|
|
419
|
+
}
|
|
420
|
+
else if (/\.(ts|js)$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
|
|
421
|
+
try {
|
|
422
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
423
|
+
if (/require\s*\(\s*['"](?:express|fastify)['"]\s*\)/.test(content) || /from\s+['"](?:express|fastify)['"]/.test(content)) {
|
|
424
|
+
serverFiles.push(fullPath);
|
|
425
|
+
const relPath = path.relative(rootDir, fullPath);
|
|
426
|
+
if (!/(?:helmet|lusca)/.test(content)) {
|
|
427
|
+
findings.push({ category: 'Tampering', severity: output.warning('MEDIUM'), location: relPath, description: 'No helmet/lusca security headers middleware' });
|
|
428
|
+
}
|
|
429
|
+
if (!/(?:cors)/.test(content)) {
|
|
430
|
+
findings.push({ category: 'Spoofing', severity: output.info('LOW'), location: relPath, description: 'No CORS middleware detected' });
|
|
431
|
+
}
|
|
432
|
+
if (!/(?:rate.?limit|throttle)/.test(content)) {
|
|
433
|
+
findings.push({ category: 'DoS', severity: output.warning('MEDIUM'), location: relPath, description: 'No rate-limiting middleware detected' });
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
catch { /* skip */ }
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch { /* skip */ }
|
|
442
|
+
};
|
|
443
|
+
collectServerFiles(rootDir, 5);
|
|
444
|
+
};
|
|
445
|
+
checkEnvInGit();
|
|
446
|
+
scanDir(rootDir);
|
|
447
|
+
checkMissingMiddleware();
|
|
448
|
+
spinner.succeed(`Scanned ${filesScanned} files`);
|
|
449
|
+
// STRIDE reference framework
|
|
450
|
+
const strideRef = [
|
|
451
|
+
{ category: 'Spoofing', description: 'Can an attacker impersonate a user or service?', example: 'Strong authentication, mTLS' },
|
|
452
|
+
{ category: 'Tampering', description: 'Can data or code be modified without detection?', example: 'Input validation, integrity checks' },
|
|
453
|
+
{ category: 'Repudiation', description: 'Can actions be performed without accountability?', example: 'Audit logging, signed commits' },
|
|
454
|
+
{ category: 'Info Disclosure', description: 'Can sensitive data leak to unauthorized parties?', example: 'Encryption at rest and in transit' },
|
|
455
|
+
{ category: 'DoS', description: 'Can service availability be degraded?', example: 'Rate limiting, resource quotas' },
|
|
456
|
+
{ category: 'Elevation', description: 'Can privileges be escalated beyond granted level?', example: 'RBAC, principle of least privilege' },
|
|
457
|
+
];
|
|
458
|
+
// Display real findings
|
|
459
|
+
output.writeln();
|
|
460
|
+
if (findings.length > 0) {
|
|
461
|
+
output.writeln(output.bold(`Findings (${findings.length}):`));
|
|
462
|
+
output.writeln();
|
|
463
|
+
output.printTable({
|
|
464
|
+
columns: [
|
|
465
|
+
{ key: 'category', header: 'STRIDE Category', width: 18 },
|
|
466
|
+
{ key: 'severity', header: 'Severity', width: 12 },
|
|
467
|
+
{ key: 'location', header: 'Location', width: 30 },
|
|
468
|
+
{ key: 'description', header: 'Description', width: 40 },
|
|
469
|
+
],
|
|
470
|
+
data: findings.slice(0, 30),
|
|
471
|
+
});
|
|
472
|
+
if (findings.length > 30) {
|
|
473
|
+
output.writeln(output.dim(`... and ${findings.length - 30} more findings`));
|
|
474
|
+
}
|
|
475
|
+
// Summary by STRIDE category
|
|
476
|
+
const byCat = {};
|
|
477
|
+
for (const f of findings)
|
|
478
|
+
byCat[f.category] = (byCat[f.category] || 0) + 1;
|
|
479
|
+
output.writeln();
|
|
480
|
+
output.writeln(output.bold('Summary by STRIDE category:'));
|
|
481
|
+
for (const [cat, count] of Object.entries(byCat).sort((a, b) => b[1] - a[1])) {
|
|
482
|
+
output.writeln(` ${cat}: ${count} finding${count === 1 ? '' : 's'}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
output.writeln(output.success('No threat indicators detected in scanned files.'));
|
|
487
|
+
}
|
|
488
|
+
// Always show STRIDE reference
|
|
489
|
+
output.writeln();
|
|
490
|
+
output.writeln(output.bold(`${model.toUpperCase()} Reference Framework${findings.length === 0 ? ' (reference only — no issues detected)' : ''}:`));
|
|
301
491
|
output.writeln();
|
|
302
492
|
output.printTable({
|
|
303
493
|
columns: [
|
|
@@ -305,18 +495,26 @@ const threatsCommand = {
|
|
|
305
495
|
{ key: 'description', header: 'What to Assess', width: 40 },
|
|
306
496
|
{ key: 'example', header: 'Example Mitigation', width: 30 },
|
|
307
497
|
],
|
|
308
|
-
data:
|
|
309
|
-
{ category: 'Spoofing', description: 'Can an attacker impersonate a user or service?', example: 'Strong authentication, mTLS' },
|
|
310
|
-
{ category: 'Tampering', description: 'Can data or code be modified without detection?', example: 'Input validation, integrity checks' },
|
|
311
|
-
{ category: 'Repudiation', description: 'Can actions be performed without accountability?', example: 'Audit logging, signed commits' },
|
|
312
|
-
{ category: 'Info Disclosure', description: 'Can sensitive data leak to unauthorized parties?', example: 'Encryption at rest and in transit' },
|
|
313
|
-
{ category: 'DoS', description: 'Can service availability be degraded?', example: 'Rate limiting, resource quotas' },
|
|
314
|
-
{ category: 'Elevation', description: 'Can privileges be escalated beyond granted level?', example: 'RBAC, principle of least privilege' },
|
|
315
|
-
],
|
|
498
|
+
data: strideRef,
|
|
316
499
|
});
|
|
500
|
+
// Export if requested
|
|
501
|
+
if (exportFormat && findings.length > 0) {
|
|
502
|
+
const exportData = {
|
|
503
|
+
model: model.toUpperCase(),
|
|
504
|
+
timestamp: new Date().toISOString(),
|
|
505
|
+
scope,
|
|
506
|
+
filesScanned,
|
|
507
|
+
totalFindings: findings.length,
|
|
508
|
+
findings: findings.map(f => ({ ...f, severity: f.severity.replace(/\x1b\[[0-9;]*m/g, '') })),
|
|
509
|
+
strideReference: strideRef,
|
|
510
|
+
};
|
|
511
|
+
if (exportFormat === 'json') {
|
|
512
|
+
output.writeln();
|
|
513
|
+
output.writeln(JSON.stringify(exportData, null, 2));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
317
516
|
output.writeln();
|
|
318
|
-
output.writeln(output.dim(
|
|
319
|
-
output.writeln(output.dim('Run "claude-flow security scan" for automated vulnerability detection.'));
|
|
517
|
+
output.writeln(output.dim(`Files scanned: ${filesScanned} (max ${MAX_FILES})`));
|
|
320
518
|
return { success: true };
|
|
321
519
|
},
|
|
322
520
|
};
|
|
@@ -401,33 +599,151 @@ const secretsCommand = {
|
|
|
401
599
|
{ command: 'claude-flow security secrets -a rotate', description: 'Rotate compromised secrets' },
|
|
402
600
|
],
|
|
403
601
|
action: async (ctx) => {
|
|
404
|
-
const
|
|
602
|
+
const scanPath = ctx.flags.path || '.';
|
|
603
|
+
const ignorePatterns = ctx.flags.ignore;
|
|
405
604
|
output.writeln();
|
|
406
605
|
output.writeln(output.bold('Secret Detection'));
|
|
407
606
|
output.writeln(output.dim('─'.repeat(50)));
|
|
408
|
-
const spinner = output.createSpinner({ text:
|
|
607
|
+
const spinner = output.createSpinner({ text: `Scanning ${scanPath} for secrets...`, spinner: 'dots' });
|
|
409
608
|
spinner.start();
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
609
|
+
const fs = await import('fs');
|
|
610
|
+
const path = await import('path');
|
|
611
|
+
const rootDir = path.resolve(scanPath);
|
|
612
|
+
const skipDirs = new Set(['node_modules', 'dist', '.git']);
|
|
613
|
+
const extensions = new Set(['.ts', '.js', '.json', '.yaml', '.yml', '.tsx', '.jsx', '.env', '.toml', '.cfg', '.conf', '.ini', '.properties', '.sh', '.bash', '.zsh']);
|
|
614
|
+
const ignoreList = ignorePatterns ? ignorePatterns.split(',').map(p => p.trim()) : [];
|
|
615
|
+
const secretPatterns = [
|
|
616
|
+
{ pattern: /AKIA[0-9A-Z]{16}/g, type: 'AWS Access Key', risk: 'Critical', action: 'Rotate immediately' },
|
|
617
|
+
{ pattern: /gh[ps]_[A-Za-z0-9_]{36,}/g, type: 'GitHub Token', risk: 'Critical', action: 'Revoke and rotate' },
|
|
618
|
+
{ pattern: /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, type: 'JWT Token', risk: 'High', action: 'Remove from source' },
|
|
619
|
+
{ pattern: /-----BEGIN (?:RSA|EC|DSA|OPENSSH) PRIVATE KEY-----/g, type: 'Private Key', risk: 'Critical', action: 'Remove and regenerate' },
|
|
620
|
+
{ pattern: /(?:mongodb|postgres|mysql|redis):\/\/[^\s'"]+/g, type: 'Connection String', risk: 'High', action: 'Use env variable' },
|
|
621
|
+
{ pattern: /['"](?:sk-|sk_live_|sk_test_)[a-zA-Z0-9]{20,}['"]/g, type: 'API Key (Stripe/OpenAI)', risk: 'Critical', action: 'Rotate immediately' },
|
|
622
|
+
{ pattern: /['"]xox[baprs]-[a-zA-Z0-9-]+['"]/g, type: 'Slack Token', risk: 'High', action: 'Revoke and rotate' },
|
|
623
|
+
{ pattern: /[a-zA-Z0-9_-]*(?:api[_-]?key|secret[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key)\s*[:=]\s*['"][^'"]{8,}['"]/gi, type: 'Generic Secret/API Key', risk: 'High', action: 'Use env variable' },
|
|
624
|
+
{ pattern: /(?:password|passwd|pwd)\s*[:=]\s*['"][^'"]{8,}['"]/gi, type: 'Hardcoded Password', risk: 'High', action: 'Use secrets manager' },
|
|
625
|
+
];
|
|
626
|
+
const findings = [];
|
|
627
|
+
let filesScanned = 0;
|
|
628
|
+
const MAX_FILES = 500;
|
|
629
|
+
const shouldIgnore = (filePath) => {
|
|
630
|
+
return ignoreList.some(p => filePath.includes(p));
|
|
631
|
+
};
|
|
632
|
+
const scanDir = (dir) => {
|
|
633
|
+
if (filesScanned >= MAX_FILES)
|
|
634
|
+
return;
|
|
635
|
+
let entries;
|
|
636
|
+
try {
|
|
637
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
638
|
+
}
|
|
639
|
+
catch {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
for (const entry of entries) {
|
|
643
|
+
if (filesScanned >= MAX_FILES)
|
|
644
|
+
break;
|
|
645
|
+
if (skipDirs.has(entry.name))
|
|
646
|
+
continue;
|
|
647
|
+
// Allow dotfiles like .env but skip .git
|
|
648
|
+
const fullPath = path.join(dir, entry.name);
|
|
649
|
+
if (entry.isDirectory()) {
|
|
650
|
+
if (entry.name.startsWith('.') && entry.name !== '.env')
|
|
651
|
+
continue;
|
|
652
|
+
scanDir(fullPath);
|
|
653
|
+
}
|
|
654
|
+
else if (entry.isFile()) {
|
|
655
|
+
const ext = path.extname(entry.name);
|
|
656
|
+
const isEnvFile = entry.name.startsWith('.env');
|
|
657
|
+
if (!extensions.has(ext) && !isEnvFile)
|
|
658
|
+
continue;
|
|
659
|
+
if (entry.name.endsWith('.d.ts'))
|
|
660
|
+
continue;
|
|
661
|
+
const relPath = path.relative(rootDir, fullPath);
|
|
662
|
+
if (shouldIgnore(relPath))
|
|
663
|
+
continue;
|
|
664
|
+
filesScanned++;
|
|
665
|
+
try {
|
|
666
|
+
const stat = fs.statSync(fullPath);
|
|
667
|
+
if (stat.size > 1024 * 1024)
|
|
668
|
+
continue; // skip files > 1MB
|
|
669
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
670
|
+
// Quick binary check — skip if null bytes present
|
|
671
|
+
if (content.includes('\0'))
|
|
672
|
+
continue;
|
|
673
|
+
const lines = content.split('\n');
|
|
674
|
+
for (let i = 0; i < lines.length; i++) {
|
|
675
|
+
const line = lines[i];
|
|
676
|
+
for (const sp of secretPatterns) {
|
|
677
|
+
sp.pattern.lastIndex = 0;
|
|
678
|
+
const match = sp.pattern.exec(line);
|
|
679
|
+
if (match) {
|
|
680
|
+
// Mask the matched secret for safe display
|
|
681
|
+
const matched = match[0];
|
|
682
|
+
const masked = matched.length > 12
|
|
683
|
+
? matched.substring(0, 6) + '***' + matched.substring(matched.length - 3)
|
|
684
|
+
: '***';
|
|
685
|
+
findings.push({
|
|
686
|
+
type: sp.type,
|
|
687
|
+
location: `${relPath}:${i + 1}`,
|
|
688
|
+
risk: sp.risk,
|
|
689
|
+
action: sp.action,
|
|
690
|
+
line: masked,
|
|
691
|
+
});
|
|
692
|
+
sp.pattern.lastIndex = 0;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
catch { /* file read error */ }
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
scanDir(rootDir);
|
|
702
|
+
spinner.succeed(`Scanned ${filesScanned} files`);
|
|
415
703
|
output.writeln();
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
704
|
+
if (findings.length > 0) {
|
|
705
|
+
const criticalCount = findings.filter(f => f.risk === 'Critical').length;
|
|
706
|
+
const highCount = findings.filter(f => f.risk === 'High').length;
|
|
707
|
+
const mediumCount = findings.filter(f => f.risk === 'Medium').length;
|
|
708
|
+
output.printTable({
|
|
709
|
+
columns: [
|
|
710
|
+
{ key: 'type', header: 'Secret Type', width: 25 },
|
|
711
|
+
{ key: 'location', header: 'Location', width: 35 },
|
|
712
|
+
{ key: 'risk', header: 'Risk', width: 12 },
|
|
713
|
+
{ key: 'action', header: 'Recommended', width: 22 },
|
|
714
|
+
],
|
|
715
|
+
data: findings.slice(0, 25).map(f => ({
|
|
716
|
+
type: f.type,
|
|
717
|
+
location: f.location,
|
|
718
|
+
risk: f.risk === 'Critical' ? output.error(f.risk) :
|
|
719
|
+
f.risk === 'High' ? output.warning(f.risk) :
|
|
720
|
+
output.warning(f.risk),
|
|
721
|
+
action: f.action,
|
|
722
|
+
})),
|
|
723
|
+
});
|
|
724
|
+
if (findings.length > 25) {
|
|
725
|
+
output.writeln(output.dim(`... and ${findings.length - 25} more secrets found`));
|
|
726
|
+
}
|
|
727
|
+
output.writeln();
|
|
728
|
+
output.printBox([
|
|
729
|
+
`Path: ${scanPath}`,
|
|
730
|
+
`Files scanned: ${filesScanned}`,
|
|
731
|
+
``,
|
|
732
|
+
`Critical: ${criticalCount} High: ${highCount} Medium: ${mediumCount}`,
|
|
733
|
+
`Total secrets found: ${findings.length}`,
|
|
734
|
+
].join('\n'), 'Secrets Summary');
|
|
735
|
+
}
|
|
736
|
+
else {
|
|
737
|
+
output.writeln(output.success('No secrets detected.'));
|
|
738
|
+
output.writeln();
|
|
739
|
+
output.printBox([
|
|
740
|
+
`Path: ${scanPath}`,
|
|
741
|
+
`Files scanned: ${filesScanned}`,
|
|
742
|
+
``,
|
|
743
|
+
`No hardcoded secrets, API keys, tokens, or credentials found.`,
|
|
744
|
+
].join('\n'), 'Secrets Summary');
|
|
745
|
+
}
|
|
746
|
+
return { success: findings.length === 0 };
|
|
431
747
|
},
|
|
432
748
|
};
|
|
433
749
|
// Defend subcommand (AIDefence integration)
|
|
@@ -145,16 +145,99 @@ function getSwarmStatus(swarmId) {
|
|
|
145
145
|
pending: pendingTasks
|
|
146
146
|
},
|
|
147
147
|
metrics: {
|
|
148
|
-
tokensUsed:
|
|
149
|
-
avgResponseTime:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
148
|
+
tokensUsed: swarmState?.tokensUsed ?? null,
|
|
149
|
+
avgResponseTime: (() => {
|
|
150
|
+
// Calculate average response time from task files with startedAt/completedAt
|
|
151
|
+
const taskTimesMs = [];
|
|
152
|
+
if (fs.existsSync(tasksDir)) {
|
|
153
|
+
try {
|
|
154
|
+
const taskFiles = fs.readdirSync(tasksDir).filter(f => f.endsWith('.json'));
|
|
155
|
+
for (const file of taskFiles) {
|
|
156
|
+
try {
|
|
157
|
+
const task = JSON.parse(fs.readFileSync(path.join(tasksDir, file), 'utf-8'));
|
|
158
|
+
if (task.startedAt && task.completedAt) {
|
|
159
|
+
const elapsed = new Date(task.completedAt).getTime() - new Date(task.startedAt).getTime();
|
|
160
|
+
if (elapsed > 0)
|
|
161
|
+
taskTimesMs.push(elapsed);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch { /* skip malformed task files */ }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
catch { /* skip if dir unreadable */ }
|
|
168
|
+
}
|
|
169
|
+
if (taskTimesMs.length === 0)
|
|
170
|
+
return null;
|
|
171
|
+
const avgMs = Math.round(taskTimesMs.reduce((a, b) => a + b, 0) / taskTimesMs.length);
|
|
172
|
+
return avgMs < 1000 ? `${avgMs}ms` : `${(avgMs / 1000).toFixed(1)}s`;
|
|
173
|
+
})(),
|
|
174
|
+
successRate: totalTasks > 0 ? `${Math.round((completedTasks / totalTasks) * 100)}%` : null,
|
|
175
|
+
elapsedTime: (() => {
|
|
176
|
+
// Calculate from swarm startedAt in state.json
|
|
177
|
+
const startedAt = swarmState?.startedAt
|
|
178
|
+
|| swarmState?.initializedAt;
|
|
179
|
+
if (!startedAt)
|
|
180
|
+
return null;
|
|
181
|
+
const elapsedMs = Date.now() - new Date(startedAt).getTime();
|
|
182
|
+
if (elapsedMs < 0)
|
|
183
|
+
return null;
|
|
184
|
+
const secs = Math.floor(elapsedMs / 1000);
|
|
185
|
+
if (secs < 60)
|
|
186
|
+
return `${secs}s`;
|
|
187
|
+
const mins = Math.floor(secs / 60);
|
|
188
|
+
const remSecs = secs % 60;
|
|
189
|
+
if (mins < 60)
|
|
190
|
+
return `${mins}m ${remSecs}s`;
|
|
191
|
+
const hrs = Math.floor(mins / 60);
|
|
192
|
+
const remMins = mins % 60;
|
|
193
|
+
return `${hrs}h ${remMins}m`;
|
|
194
|
+
})()
|
|
157
195
|
},
|
|
196
|
+
coordination: (() => {
|
|
197
|
+
// Read real coordination counts from .swarm/coordination/ directory
|
|
198
|
+
const coordDir = path.join(swarmDir, 'coordination');
|
|
199
|
+
let consensusRounds = 0;
|
|
200
|
+
let messagesSent = 0;
|
|
201
|
+
let conflictsResolved = 0;
|
|
202
|
+
if (fs.existsSync(coordDir)) {
|
|
203
|
+
try {
|
|
204
|
+
const coordFiles = fs.readdirSync(coordDir).filter(f => f.endsWith('.json'));
|
|
205
|
+
for (const file of coordFiles) {
|
|
206
|
+
try {
|
|
207
|
+
const entry = JSON.parse(fs.readFileSync(path.join(coordDir, file), 'utf-8'));
|
|
208
|
+
if (entry.type === 'consensus')
|
|
209
|
+
consensusRounds++;
|
|
210
|
+
else if (entry.type === 'message')
|
|
211
|
+
messagesSent++;
|
|
212
|
+
else if (entry.type === 'conflict' || entry.type === 'conflict-resolution')
|
|
213
|
+
conflictsResolved++;
|
|
214
|
+
// Also aggregate pre-counted fields if present
|
|
215
|
+
if (typeof entry.consensusRounds === 'number')
|
|
216
|
+
consensusRounds += entry.consensusRounds;
|
|
217
|
+
if (typeof entry.messagesSent === 'number')
|
|
218
|
+
messagesSent += entry.messagesSent;
|
|
219
|
+
if (typeof entry.conflictsResolved === 'number')
|
|
220
|
+
conflictsResolved += entry.conflictsResolved;
|
|
221
|
+
}
|
|
222
|
+
catch { /* skip malformed coordination files */ }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
catch { /* skip if dir unreadable */ }
|
|
226
|
+
}
|
|
227
|
+
// Also check state.json for aggregate coordination stats
|
|
228
|
+
if (swarmState) {
|
|
229
|
+
const coord = swarmState.coordination;
|
|
230
|
+
if (coord) {
|
|
231
|
+
if (typeof coord.consensusRounds === 'number')
|
|
232
|
+
consensusRounds += coord.consensusRounds;
|
|
233
|
+
if (typeof coord.messagesSent === 'number')
|
|
234
|
+
messagesSent += coord.messagesSent;
|
|
235
|
+
if (typeof coord.conflictsResolved === 'number')
|
|
236
|
+
conflictsResolved += coord.conflictsResolved;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return { consensusRounds, messagesSent, conflictsResolved };
|
|
240
|
+
})(),
|
|
158
241
|
hasActiveSwarm: !!swarmState || totalAgents > 0
|
|
159
242
|
};
|
|
160
243
|
}
|
|
@@ -433,8 +516,10 @@ const startCommand = {
|
|
|
433
516
|
fs.writeFileSync(path.join(swarmDir, 'state.json'), JSON.stringify(executionState, null, 2));
|
|
434
517
|
output.writeln();
|
|
435
518
|
output.printSuccess(`Swarm ${swarmId} initialized with ${totalAgents} agent slots`);
|
|
436
|
-
output.writeln(output.dim('
|
|
437
|
-
output.writeln(output.dim(' Claude Code
|
|
519
|
+
output.writeln(output.dim(' This CLI coordinates agent state. Execution happens via:'));
|
|
520
|
+
output.writeln(output.dim(' - Claude Code Agent tool (interactive)'));
|
|
521
|
+
output.writeln(output.dim(' - claude -p (headless background)'));
|
|
522
|
+
output.writeln(output.dim(' - hive-mind spawn --claude (autonomous)'));
|
|
438
523
|
output.writeln(output.dim(` Monitor: claude-flow swarm status ${swarmId}`));
|
|
439
524
|
return { success: true, data: executionState };
|
|
440
525
|
}
|
|
@@ -500,10 +585,10 @@ const statusCommand = {
|
|
|
500
585
|
// Metrics
|
|
501
586
|
output.writeln(output.bold('Performance Metrics'));
|
|
502
587
|
output.printList([
|
|
503
|
-
`Tokens Used: ${status.metrics.tokensUsed.toLocaleString()}`,
|
|
504
|
-
`Avg Response Time: ${status.metrics.avgResponseTime}`,
|
|
505
|
-
`Success Rate: ${status.metrics.successRate}`,
|
|
506
|
-
`Elapsed Time: ${status.metrics.elapsedTime}`
|
|
588
|
+
`Tokens Used: ${status.metrics.tokensUsed != null ? status.metrics.tokensUsed.toLocaleString() : output.dim('unknown')}`,
|
|
589
|
+
`Avg Response Time: ${status.metrics.avgResponseTime ?? output.dim('no data')}`,
|
|
590
|
+
`Success Rate: ${status.metrics.successRate ?? output.dim('no data')}`,
|
|
591
|
+
`Elapsed Time: ${status.metrics.elapsedTime ?? output.dim('no data')}`
|
|
507
592
|
]);
|
|
508
593
|
output.writeln();
|
|
509
594
|
// Coordination stats
|