Files
shannon/shannon.mjs
ajmallesh d372f87297 refactor: remove ~500 lines of dead code and consolidate duplicates
Comprehensive codebase cleanup based on parallel agent analysis and automated
dead code detection (knip, depcheck). Reduces codebase by ~10% with zero
functional changes.

## Phase 1: Obsolete MCP Setup Removal (~82 lines)
- Delete setupMCP() and cleanupMCP() functions from environment.js
- Remove all calls to cleanupMCP() (8 instances across 3 files)
- Migrate from claude CLI to SDK's mcpServers option
- Remove --log flag (obsolete logging system)

## Phase 2: Dead Code Removal (~317 lines)
- Delete src/utils/logger.js entirely (127 lines, superseded by audit system)
- Remove handleConfigError() and handleError() from error-handling.js
- Remove isToolAvailable() from tool-checker.js
- Remove 5 dead methods from audit-session.js (logSessionFailure, logMessage,
  markRolledBack, updateValidation, getValidation)
- Remove 6 wrapper methods from audit/logger.js (all callers use logEvent directly)
- Remove formatCost(), updateMessage(), compose() utilities (unused)

## Phase 3: Consolidation (~195 lines)
- Extract SessionMutex to src/utils/concurrency.js (was duplicated in 2 files)
- Consolidate formatDuration to src/audit/utils.js (was in 3 files)
- Extract readline prompts to src/cli/prompts.js (was duplicated in 2 files)
- Create validator factories in constants.js (reduce 72 lines to 30)

## Impact
- Total reduction: 488 lines (20 files modified, 2 created, 1 deleted)
- Codebase: ~4,900 → ~4,400 LOC (10% reduction)
- Zero functional changes, all tests pass
- Improved maintainability and DRY compliance

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-23 17:01:17 -07:00

470 lines
18 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env zx
import { path, fs } from 'zx';
import chalk from 'chalk';
// 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 } 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) {
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)));
// Return final report path for clickable output
return path.join(sourceDir, 'deliverables', 'comprehensive_security_assessment_report.md');
}
// 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;
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 (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');
// Check for --pipeline-testing in remaining args
if (remainingArgs.includes('--pipeline-testing')) {
pipelineTestingMode = true;
}
// Add non-flag args (excluding --pipeline-testing)
nonFlagArgs.push(...remainingArgs.filter(arg => arg !== '--pipeline-testing'));
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) {
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'));
}
try {
const finalReportPath = await main(webUrl, repoPathValidation.path, configPath, pipelineTestingMode);
console.log(chalk.green.bold('\n📄 FINAL REPORT AVAILABLE:'));
console.log(chalk.cyan(finalReportPath));
} 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);
}