forked from KeygraphHQ/shannon
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathshannon.mjs
More file actions
executable file
·503 lines (421 loc) · 18.8 KB
/
shannon.mjs
File metadata and controls
executable file
·503 lines (421 loc) · 18.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
#!/usr/bin/env zx
// Copyright (C) 2025 Keygraph, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3
// as published by the Free Software Foundation.
import { path, fs } from 'zx';
import chalk from 'chalk';
import dotenv from 'dotenv';
dotenv.config();
// Config and Tools
import { parseConfig, distributeConfig } from './src/config-parser.js';
import { checkToolAvailability, handleMissingTools } from './src/tool-checker.js';
// Session and Checkpoints
import { createSession, updateSession, getSession, AGENTS } from './src/session-manager.js';
import { runPhase, getGitCommitHash } from './src/checkpoint-manager.js';
// Setup and Deliverables
import { setupLocalRepo } from './src/setup/environment.js';
// AI and Prompts
import { runClaudePromptWithRetry } from './src/ai/claude-executor.js';
import { loadPrompt } from './src/prompts/prompt-manager.js';
// Phases
import { executePreReconPhase } from './src/phases/pre-recon.js';
import { assembleFinalReport } from './src/phases/reporting.js';
// Utils
import { timingResults, costResults, displayTimingSummary, Timer } from './src/utils/metrics.js';
import { formatDuration, generateAuditPath } from './src/audit/utils.js';
// CLI
import { handleDeveloperCommand } from './src/cli/command-handler.js';
import { showHelp, displaySplashScreen } from './src/cli/ui.js';
import { validateWebUrl, validateRepoPath } from './src/cli/input-validator.js';
// Error Handling
import { PentestError, logError } from './src/error-handling.js';
// Session Manager Functions
import {
calculateVulnerabilityAnalysisSummary,
calculateExploitationSummary,
getNextAgent
} from './src/session-manager.js';
// Configure zx to disable timeouts (let tools run as long as needed)
$.timeout = 0;
// Setup graceful cleanup on process signals
process.on('SIGINT', async () => {
console.log(chalk.yellow('\n⚠️ Received SIGINT, cleaning up...'));
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log(chalk.yellow('\n⚠️ Received SIGTERM, cleaning up...'));
process.exit(0);
});
// Main orchestration function
async function main(webUrl, repoPath, configPath = null, pipelineTestingMode = false, disableLoader = false) {
// Set global flag for loader control
global.SHANNON_DISABLE_LOADER = disableLoader;
const totalTimer = new Timer('total-execution');
timingResults.total = totalTimer;
// Display splash screen
await displaySplashScreen();
console.log(chalk.cyan.bold('🚀 AI PENETRATION TESTING AGENT'));
console.log(chalk.cyan(`🎯 Target: ${webUrl}`));
console.log(chalk.cyan(`📁 Source: ${repoPath}`));
if (configPath) {
console.log(chalk.cyan(`⚙️ Config: ${configPath}`));
}
console.log(chalk.gray('─'.repeat(60)));
// Parse configuration if provided
let config = null;
let distributedConfig = null;
if (configPath) {
try {
// Resolve config path - check configs folder if relative path
let resolvedConfigPath = configPath;
if (!path.isAbsolute(configPath)) {
const configsDir = path.join(process.cwd(), 'configs');
const configInConfigsDir = path.join(configsDir, configPath);
// Check if file exists in configs directory, otherwise use original path
if (await fs.pathExists(configInConfigsDir)) {
resolvedConfigPath = configInConfigsDir;
}
}
config = await parseConfig(resolvedConfigPath);
distributedConfig = distributeConfig(config);
console.log(chalk.green(`✅ Configuration loaded successfully`));
} catch (error) {
await logError(error, `Configuration loading from ${configPath}`);
throw error; // Let the main error boundary handle it
}
}
// Check tool availability
const toolAvailability = await checkToolAvailability();
handleMissingTools(toolAvailability);
// Setup local repository
console.log(chalk.blue('📁 Setting up local repository...'));
let sourceDir;
try {
sourceDir = await setupLocalRepo(repoPath);
const variables = { webUrl, repoPath, sourceDir };
console.log(chalk.green('✅ Local repository setup successfully'));
} catch (error) {
console.log(chalk.red(`❌ Failed to setup local repository: ${error.message}`));
console.log(chalk.gray('This could be due to:'));
console.log(chalk.gray(' - Insufficient permissions'));
console.log(chalk.gray(' - Repository path not accessible'));
console.log(chalk.gray(' - Git initialization issues'));
console.log(chalk.gray(' - Insufficient disk space'));
process.exit(1);
}
const variables = { webUrl, repoPath, sourceDir };
// Create session for tracking (in normal mode)
const session = await createSession(webUrl, repoPath, configPath, sourceDir);
console.log(chalk.blue(`📝 Session created: ${session.id.substring(0, 8)}...`));
// If setup-only mode, exit after session creation
if (process.argv.includes('--setup-only')) {
console.log(chalk.green('✅ Setup complete! Local repository setup and session created.'));
console.log(chalk.gray('Use developer commands to run individual agents:'));
console.log(chalk.gray(' ./shannon.mjs --run-agent pre-recon'));
console.log(chalk.gray(' ./shannon.mjs --status'));
process.exit(0);
}
// Helper function to update session progress
const updateSessionProgress = async (agentName, commitHash = null) => {
try {
const updates = {
completedAgents: [...new Set([...session.completedAgents, agentName])],
failedAgents: session.failedAgents.filter(name => name !== agentName), // Remove from failed if it was there
status: 'in-progress'
};
if (commitHash) {
updates.checkpoints = { ...session.checkpoints, [agentName]: commitHash };
}
await updateSession(session.id, updates);
// Update local session object for subsequent updates
Object.assign(session, updates);
console.log(chalk.gray(` 📝 Session updated: ${agentName} completed`));
} catch (error) {
console.log(chalk.yellow(` ⚠️ Failed to update session: ${error.message}`));
}
};
// Create outputs directory in source directory
try {
const outputsDir = path.join(sourceDir, 'outputs');
await fs.ensureDir(outputsDir);
await fs.ensureDir(path.join(outputsDir, 'schemas'));
await fs.ensureDir(path.join(outputsDir, 'scans'));
} catch (error) {
throw new PentestError(
`Failed to create output directories: ${error.message}`,
'filesystem',
false,
{ sourceDir, originalError: error.message }
);
}
// Check if we should continue from where session left off
const nextAgent = getNextAgent(session);
if (!nextAgent) {
console.log(chalk.green(`✅ All agents completed! Session is finished.`));
await displayTimingSummary(timingResults, costResults, session.completedAgents);
process.exit(0);
}
console.log(chalk.blue(`🔄 Continuing from ${nextAgent.displayName} (${session.completedAgents.length}/${Object.keys(AGENTS).length} agents completed)`));
// Determine which phase to start from based on next agent
const startPhase = nextAgent.name === 'pre-recon' ? 1
: nextAgent.name === 'recon' ? 2
: ['injection-vuln', 'xss-vuln', 'auth-vuln', 'ssrf-vuln', 'authz-vuln'].includes(nextAgent.name) ? 3
: ['injection-exploit', 'xss-exploit', 'auth-exploit', 'ssrf-exploit', 'authz-exploit'].includes(nextAgent.name) ? 4
: nextAgent.name === 'report' ? 5 : 1;
// PHASE 1: PRE-RECONNAISSANCE
if (startPhase <= 1) {
const { duration: preReconDuration } = await executePreReconPhase(
webUrl,
sourceDir,
variables,
distributedConfig,
toolAvailability,
pipelineTestingMode,
session.id // Pass session ID for logging
);
timingResults.phases['pre-recon'] = preReconDuration;
await updateSessionProgress('pre-recon');
}
// PHASE 2: RECONNAISSANCE
if (startPhase <= 2) {
console.log(chalk.magenta.bold('\n🔎 PHASE 2: RECONNAISSANCE'));
console.log(chalk.magenta('Analyzing initial findings...'));
const reconTimer = new Timer('phase-2-recon');
const recon = await runClaudePromptWithRetry(
await loadPrompt('recon', variables, distributedConfig, pipelineTestingMode),
sourceDir,
'*',
'',
AGENTS['recon'].displayName,
'recon', // Agent name for snapshot creation
chalk.cyan,
{ id: session.id, webUrl } // Session metadata for audit logging (STANDARD: use 'id' field)
);
const reconDuration = reconTimer.stop();
timingResults.phases['recon'] = reconDuration;
console.log(chalk.green(`✅ Reconnaissance complete in ${formatDuration(reconDuration)}`));
await updateSessionProgress('recon');
}
// PHASE 3: VULNERABILITY ANALYSIS
if (startPhase <= 3) {
const vulnTimer = new Timer('phase-3-vulnerability-analysis');
console.log(chalk.red.bold('\n🚨 PHASE 3: VULNERABILITY ANALYSIS'));
await runPhase('vulnerability-analysis', session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
// Display vulnerability analysis summary
const currentSession = await getSession(session.id);
const vulnSummary = calculateVulnerabilityAnalysisSummary(currentSession);
console.log(chalk.blue(`\n📊 Vulnerability Analysis Summary: ${vulnSummary.totalAnalyses} analyses, ${vulnSummary.totalVulnerabilities} vulnerabilities found, ${vulnSummary.exploitationCandidates} ready for exploitation`));
const vulnDuration = vulnTimer.stop();
timingResults.phases['vulnerability-analysis'] = vulnDuration;
console.log(chalk.green(`✅ Vulnerability analysis phase complete in ${formatDuration(vulnDuration)}`));
}
// PHASE 4: EXPLOITATION
if (startPhase <= 4) {
const exploitTimer = new Timer('phase-4-exploitation');
console.log(chalk.red.bold('\n💥 PHASE 4: EXPLOITATION'));
// Get fresh session data to ensure we have latest vulnerability analysis results
const freshSession = await getSession(session.id);
await runPhase('exploitation', freshSession, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
// Display exploitation summary
const finalSession = await getSession(session.id);
const exploitSummary = calculateExploitationSummary(finalSession);
if (exploitSummary.eligibleExploits > 0) {
console.log(chalk.blue(`\n🎯 Exploitation Summary: ${exploitSummary.totalAttempts}/${exploitSummary.eligibleExploits} attempted, ${exploitSummary.skippedExploits} skipped (no vulnerabilities)`));
} else {
console.log(chalk.gray(`\n🎯 Exploitation Summary: No exploitation attempts (no vulnerabilities found)`));
}
const exploitDuration = exploitTimer.stop();
timingResults.phases['exploitation'] = exploitDuration;
console.log(chalk.green(`✅ Exploitation phase complete in ${formatDuration(exploitDuration)}`));
}
// PHASE 5: REPORTING
if (startPhase <= 5) {
console.log(chalk.greenBright.bold('\n📊 PHASE 5: REPORTING'));
console.log(chalk.greenBright('Generating executive summary and assembling final report...'));
const reportTimer = new Timer('phase-5-reporting');
// First, assemble all deliverables into a single concatenated report
console.log(chalk.blue('📝 Assembling deliverables from specialist agents...'));
try {
await assembleFinalReport(sourceDir);
} catch (error) {
console.log(chalk.red(`❌ Error assembling final report: ${error.message}`));
}
// Then run reporter agent to create executive summary and clean up hallucinations
console.log(chalk.blue('📋 Generating executive summary and cleaning up report...'));
const execSummary = await runClaudePromptWithRetry(
await loadPrompt('report-executive', variables, distributedConfig, pipelineTestingMode),
sourceDir,
'*',
'',
'Executive Summary and Report Cleanup',
'report', // Agent name for snapshot creation
chalk.cyan,
{ id: session.id, webUrl } // Session metadata for audit logging (STANDARD: use 'id' field)
);
const reportDuration = reportTimer.stop();
timingResults.phases['reporting'] = reportDuration;
console.log(chalk.green(`✅ Final report generated in ${formatDuration(reportDuration)}`));
// Get the commit hash after successful report generation for checkpoint
try {
const reportCommitHash = await getGitCommitHash(sourceDir);
await updateSessionProgress('report', reportCommitHash);
console.log(chalk.gray(` 📍 Report checkpoint saved: ${reportCommitHash.substring(0, 8)}`));
} catch (error) {
console.log(chalk.yellow(` ⚠️ Failed to save report checkpoint: ${error.message}`));
await updateSessionProgress('report'); // Fallback without checkpoint
}
}
// Calculate final timing and cost data
const totalDuration = timingResults.total.stop();
const timingBreakdown = {
total: totalDuration,
phases: { ...timingResults.phases },
agents: { ...timingResults.agents },
commands: { ...timingResults.commands }
};
// Use accumulated cost data
const costBreakdown = {
total: costResults.total,
agents: { ...costResults.agents }
};
// Mark session as completed with timing and cost data
await updateSession(session.id, {
status: 'completed',
timingBreakdown,
costBreakdown
});
// Display comprehensive timing summary
displayTimingSummary();
console.log(chalk.cyan.bold('\n🎉 PENETRATION TESTING COMPLETE!'));
console.log(chalk.gray('─'.repeat(60)));
// Calculate audit logs path
const auditLogsPath = generateAuditPath(session);
// Return final report path and audit logs path for clickable output
return {
reportPath: path.join(sourceDir, 'deliverables', 'comprehensive_security_assessment_report.md'),
auditLogsPath
};
}
// Entry point - handle both direct node execution and shebang execution
let args = process.argv.slice(2);
// If first arg is the script name (from shebang), remove it
if (args[0] && args[0].includes('shannon.mjs')) {
args = args.slice(1);
}
// Parse flags and arguments
let configPath = null;
let pipelineTestingMode = false;
let disableLoader = false;
const nonFlagArgs = [];
let developerCommand = null;
const developerCommands = ['--run-phase', '--run-all', '--rollback-to', '--rerun', '--status', '--list-agents', '--cleanup'];
for (let i = 0; i < args.length; i++) {
if (args[i] === '--config') {
if (i + 1 < args.length) {
configPath = args[i + 1];
i++; // Skip the next argument
} else {
console.log(chalk.red('❌ --config flag requires a file path'));
process.exit(1);
}
} else if (args[i] === '--pipeline-testing') {
pipelineTestingMode = true;
} else if (args[i] === '--disable-loader') {
disableLoader = true;
} else if (developerCommands.includes(args[i])) {
developerCommand = args[i];
// Collect remaining args for the developer command
const remainingArgs = args.slice(i + 1).filter(arg => !arg.startsWith('--') || arg === '--pipeline-testing' || arg === '--disable-loader');
// Check for --pipeline-testing in remaining args
if (remainingArgs.includes('--pipeline-testing')) {
pipelineTestingMode = true;
}
// Check for --disable-loader in remaining args
if (remainingArgs.includes('--disable-loader')) {
disableLoader = true;
}
// Add non-flag args (excluding --pipeline-testing and --disable-loader)
nonFlagArgs.push(...remainingArgs.filter(arg => arg !== '--pipeline-testing' && arg !== '--disable-loader'));
break; // Stop parsing after developer command
} else if (!args[i].startsWith('-')) {
nonFlagArgs.push(args[i]);
}
}
// Handle help flag
if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
showHelp();
process.exit(0);
}
// Handle developer commands
if (developerCommand) {
// Set global flag for loader control in developer mode too
global.SHANNON_DISABLE_LOADER = disableLoader;
await handleDeveloperCommand(developerCommand, nonFlagArgs, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
process.exit(0);
}
// Handle no arguments - show help
if (nonFlagArgs.length === 0) {
console.log(chalk.red.bold('❌ Error: No arguments provided\n'));
showHelp();
process.exit(1);
}
// Handle insufficient arguments
if (nonFlagArgs.length < 2) {
console.log(chalk.red('❌ Both WEB_URL and REPO_PATH are required'));
console.log(chalk.gray('Usage: ./shannon.mjs <WEB_URL> <REPO_PATH> [--config config.yaml]'));
console.log(chalk.gray('Help: ./shannon.mjs --help'));
process.exit(1);
}
const [webUrl, repoPath] = nonFlagArgs;
// Validate web URL
const webUrlValidation = validateWebUrl(webUrl);
if (!webUrlValidation.valid) {
console.log(chalk.red(`❌ Invalid web URL: ${webUrlValidation.error}`));
console.log(chalk.gray(`Expected format: https://example.com`));
process.exit(1);
}
// Validate repository path
const repoPathValidation = await validateRepoPath(repoPath);
if (!repoPathValidation.valid) {
console.log(chalk.red(`❌ Invalid repository path: ${repoPathValidation.error}`));
console.log(chalk.gray(`Expected: Accessible local directory path`));
process.exit(1);
}
// Success - show validated inputs
console.log(chalk.green('✅ Input validation passed:'));
console.log(chalk.gray(` Target Web URL: ${webUrl}`));
console.log(chalk.gray(` Target Repository: ${repoPathValidation.path}\n`));
console.log(chalk.gray(` Config Path: ${configPath}\n`));
if (pipelineTestingMode) {
console.log(chalk.yellow('⚡ PIPELINE TESTING MODE ENABLED - Using minimal test prompts for fast pipeline validation\n'));
}
if (disableLoader) {
console.log(chalk.yellow('⚙️ LOADER DISABLED - Progress indicator will not be shown\n'));
}
try {
const result = await main(webUrl, repoPathValidation.path, configPath, pipelineTestingMode, disableLoader);
console.log(chalk.green.bold('\n📄 FINAL REPORT AVAILABLE:'));
console.log(chalk.cyan(result.reportPath));
console.log(chalk.green.bold('\n📂 AUDIT LOGS AVAILABLE:'));
console.log(chalk.cyan(result.auditLogsPath));
} catch (error) {
// Enhanced error boundary with proper logging
if (error instanceof PentestError) {
await logError(error, 'Main execution failed');
console.log(chalk.red.bold('\n🚨 PENTEST EXECUTION FAILED'));
console.log(chalk.red(` Type: ${error.type}`));
console.log(chalk.red(` Retryable: ${error.retryable ? 'Yes' : 'No'}`));
if (error.retryable) {
console.log(chalk.yellow(' Consider running the command again or checking network connectivity.'));
}
} else {
console.log(chalk.red.bold('\n🚨 UNEXPECTED ERROR OCCURRED'));
console.log(chalk.red(` Error: ${error?.message || error?.toString() || 'Unknown error'}`));
if (process.env.DEBUG) {
console.log(chalk.gray(` Stack: ${error?.stack || 'No stack trace available'}`));
}
}
process.exit(1);
}