// 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 { fs, path, $ } from 'zx'; import chalk from 'chalk'; import { PentestError } from './error-handling.js'; import { parseConfig, distributeConfig } from './config-parser.js'; import { executeGitCommandWithRetry } from './utils/git-manager.js'; import { formatDuration } from './audit/utils.js'; import { AGENTS, PHASES, selectSession, validateAgent, validateAgentRange, validatePhase, checkPrerequisites, getNextAgent, markAgentCompleted, markAgentFailed, getSessionStatus, rollbackToAgent, updateSession } from './session-manager.js'; // Check if target repository exists and is accessible const validateTargetRepo = async (targetRepo) => { if (!targetRepo || !await fs.pathExists(targetRepo)) { throw new PentestError( `Target repository '${targetRepo}' not found or not accessible`, 'filesystem', false, { targetRepo } ); } // Check if it's a git repository const gitDir = path.join(targetRepo, '.git'); if (!await fs.pathExists(gitDir)) { throw new PentestError( `Target repository '${targetRepo}' is not a git repository`, 'validation', false, { targetRepo } ); } return true; }; // Get git commit hash for checkpoint export const getGitCommitHash = async (targetRepo) => { try { const result = await executeGitCommandWithRetry(['git', 'rev-parse', 'HEAD'], targetRepo, 'getting commit hash'); return result.stdout.trim(); } catch (error) { throw new PentestError( `Failed to get git commit hash: ${error.message}`, 'git', false, { targetRepo, originalError: error.message } ); } }; // Rollback git workspace to specific commit const rollbackGitToCommit = async (targetRepo, commitHash) => { try { await executeGitCommandWithRetry(['git', 'reset', '--hard', commitHash], targetRepo, 'rollback to commit'); await executeGitCommandWithRetry(['git', 'clean', '-fd'], targetRepo, 'cleaning after rollback'); console.log(chalk.green(`βœ… Git workspace rolled back to commit ${commitHash.substring(0, 8)}`)); } catch (error) { throw new PentestError( `Failed to rollback git workspace: ${error.message}`, 'git', false, { targetRepo, commitHash, originalError: error.message } ); } }; // Run a single agent with retry logic and checkpointing const runSingleAgent = async (agentName, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt, allowRerun = false, skipWorkspaceClean = false) => { // Validate agent first const agent = validateAgent(agentName); console.log(chalk.cyan(`\nπŸ€– Running agent: ${agent.displayName}`)); // Reload session to get latest state (important for agent ranges) const { getSession } = await import('./session-manager.js'); const freshSession = await getSession(session.id); if (!freshSession) { throw new PentestError(`Session ${session.id} not found`, 'validation', false); } // Use fresh session for all subsequent checks session = freshSession; // Warn if session is completed if (session.status === 'completed') { console.log(chalk.yellow('⚠️ This session is already completed. Re-running will modify completed results.')); } // Block re-running completed agents unless explicitly allowed - use --rerun for explicit rollback and re-run if (!allowRerun && session.completedAgents.includes(agentName)) { throw new PentestError( `Agent '${agentName}' has already been completed. Use --rerun ${agentName} for explicit rollback and re-execution.`, 'validation', false, { agentName, suggestion: `--rerun ${agentName}`, completedAgents: session.completedAgents } ); } const targetRepo = session.targetRepo; await validateTargetRepo(targetRepo); // Check prerequisites checkPrerequisites(session, agentName); // Additional safety check: if this agent is not completed but we have uncommitted changes, // it might be from a previous interrupted run. Clean the workspace to be safe. // Skip workspace cleaning during parallel execution to avoid agents interfering with each other if (!session.completedAgents.includes(agentName) && !allowRerun && !skipWorkspaceClean) { try { const status = await executeGitCommandWithRetry(['git', 'status', '--porcelain'], targetRepo, 'checking workspace status'); const hasUncommittedChanges = status.stdout.trim().length > 0; if (hasUncommittedChanges) { console.log(chalk.yellow(` ⚠️ Detected uncommitted changes before running ${agentName}`)); console.log(chalk.yellow(` 🧹 Cleaning workspace to ensure clean agent execution`)); await executeGitCommandWithRetry(['git', 'reset', '--hard', 'HEAD'], targetRepo, 'cleaning workspace'); await executeGitCommandWithRetry(['git', 'clean', '-fd'], targetRepo, 'removing untracked files'); console.log(chalk.green(` βœ… Workspace cleaned successfully`)); } } catch (error) { console.log(chalk.yellow(` ⚠️ Could not check/clean workspace: ${error.message}`)); } } // Create checkpoint before execution const variables = { webUrl: session.webUrl, repoPath: session.repoPath, sourceDir: targetRepo }; // Handle relative config paths - prepend configs/ if needed let configPath = null; if (session.configFile) { configPath = session.configFile.startsWith('configs/') ? session.configFile : path.join('configs', session.configFile); } const config = configPath ? await parseConfig(configPath) : null; const distributedConfig = config ? distributeConfig(config) : null; // Removed prompt snapshotting - using live prompts from repo // Initialize variables that will be used in both try and catch blocks let validationData = null; let timingData = null; let costData = null; try { // Load and run the appropriate prompt let promptName = getPromptName(agentName); const prompt = await loadPrompt(promptName, variables, distributedConfig, pipelineTestingMode); // Get color function for this agent const getAgentColor = (agentName) => { const colorMap = { 'injection-vuln': chalk.red, 'injection-exploit': chalk.red, 'xss-vuln': chalk.yellow, 'xss-exploit': chalk.yellow, 'auth-vuln': chalk.blue, 'auth-exploit': chalk.blue, 'ssrf-vuln': chalk.magenta, 'ssrf-exploit': chalk.magenta, 'authz-vuln': chalk.green, 'authz-exploit': chalk.green }; return colorMap[agentName] || chalk.cyan; }; const result = await runClaudePromptWithRetry( prompt, targetRepo, '*', '', AGENTS[agentName].displayName, agentName, // Pass agent name for snapshot creation getAgentColor(agentName), // Pass color function for this agent { id: session.id, webUrl: session.webUrl, repoPath: session.repoPath } // Session metadata for audit logging ); if (!result.success) { throw new PentestError( `Agent execution failed: ${result.error}`, 'agent', result.retryable || false, { agentName, result } ); } // Get commit hash for checkpoint const commitHash = await getGitCommitHash(targetRepo); // Extract timing and cost data from result if available timingData = result.duration; costData = result.cost || 0; if (agentName.includes('-vuln')) { // Extract vulnerability type from agent name (e.g., 'injection-vuln' -> 'injection') const vulnType = agentName.replace('-vuln', ''); try { const { safeValidateQueueAndDeliverable } = await import('./queue-validation.js'); const validation = await safeValidateQueueAndDeliverable(vulnType, targetRepo); if (validation.success) { // Log validation result (don't store - will be re-validated during exploitation phase) console.log(chalk.blue(`πŸ“‹ Validation: ${validation.data.shouldExploit ? `Ready for exploitation (${validation.data.vulnerabilityCount} vulnerabilities)` : 'No vulnerabilities found'}`)); validationData = { shouldExploit: validation.data.shouldExploit, vulnerabilityCount: validation.data.vulnerabilityCount }; } else { console.log(chalk.yellow(`⚠️ Validation failed: ${validation.error.message}`)); } } catch (validationError) { console.log(chalk.yellow(`⚠️ Could not validate ${vulnType}: ${validationError.message}`)); } } // Mark agent as completed (validation not stored - will be re-checked during exploitation) await markAgentCompleted(session.id, agentName, commitHash); // Only show completion message for sequential execution if (!skipWorkspaceClean) { console.log(chalk.green(`βœ… Agent '${agentName}' completed successfully`)); } // Return immutable result object with enhanced metadata return Object.freeze({ success: true, agentName, result, validation: validationData, timing: timingData, cost: costData, checkpoint: commitHash, completedAt: new Date().toISOString() }); } catch (error) { // Mark agent as failed await markAgentFailed(session.id, agentName); // Only show failure message for sequential execution if (!skipWorkspaceClean) { console.log(chalk.red(`❌ Agent '${agentName}' failed: ${error.message}`)); } // Return immutable error object with enhanced context const errorResult = Object.freeze({ success: false, agentName, error: { message: error.message, type: error.constructor.name, retryable: error.retryable || false, originalError: error }, validation: validationData, timing: timingData, failedAt: new Date().toISOString(), context: { targetRepo, promptName: getPromptName(agentName), sessionId: session.id } }); // Throw enhanced error with preserved context const enhancedError = new PentestError( `Agent '${agentName}' execution failed: ${error.message}`, 'agent', error.retryable || false, { agentName, sessionId: session.id, originalError: error.message, errorResult } ); throw enhancedError; } }; // Run multiple agents in sequence const runAgentRange = async (startAgent, endAgent, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt) => { const agents = validateAgentRange(startAgent, endAgent); console.log(chalk.cyan(`\nπŸ”„ Running agent range: ${startAgent} to ${endAgent} (${agents.length} agents)`)); for (const agent of agents) { // Skip if already completed if (session.completedAgents.includes(agent.name)) { console.log(chalk.gray(`⏭️ Agent '${agent.name}' already completed, skipping`)); continue; } try { await runSingleAgent(agent.name, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt); } catch (error) { console.log(chalk.red(`❌ Agent range execution stopped at '${agent.name}' due to failure`)); throw error; } } console.log(chalk.green(`βœ… Agent range ${startAgent} to ${endAgent} completed successfully`)); }; // Run vulnerability agents in parallel const runParallelVuln = async (session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt) => { const vulnAgents = ['injection-vuln', 'xss-vuln', 'auth-vuln', 'ssrf-vuln', 'authz-vuln']; const activeAgents = vulnAgents.filter(agent => !session.completedAgents.includes(agent)); if (activeAgents.length === 0) { console.log(chalk.gray('⏭️ All vulnerability agents already completed')); return { completed: vulnAgents, failed: [] }; } console.log(chalk.cyan(`\nπŸš€ Starting ${activeAgents.length} vulnerability analysis specialists in parallel...`)); console.log(chalk.gray(' Specialists: ' + activeAgents.join(', '))); console.log(); const startTime = Date.now(); // Collect all results without logging individual completions const results = await Promise.allSettled( activeAgents.map(async (agentName, index) => { // Add 2-second stagger to prevent API overwhelm await new Promise(resolve => setTimeout(resolve, index * 2000)); let lastError; let attempts = 0; const maxAttempts = 3; while (attempts < maxAttempts) { attempts++; try { const result = await runSingleAgent(agentName, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt, false, true); return { agentName, ...result, attempts }; } catch (error) { lastError = error; if (attempts < maxAttempts) { console.log(chalk.yellow(`⚠️ ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`)); await new Promise(resolve => setTimeout(resolve, 5000)); } } } throw { agentName, error: lastError, attempts }; }) ); const totalDuration = Date.now() - startTime; // Process and display results in a nice table console.log(chalk.cyan('\nπŸ“Š Vulnerability Analysis Results')); console.log(chalk.gray('─'.repeat(80))); // Table header console.log(chalk.bold('Agent Status Vulns Attempt Duration Cost')); console.log(chalk.gray('─'.repeat(80))); const completed = []; const failed = []; results.forEach((result, index) => { const agentName = activeAgents[index]; const agentDisplay = agentName.padEnd(22); if (result.status === 'fulfilled') { const data = result.value; completed.push(agentName); const vulnCount = data.validation?.vulnerabilityCount || 0; const duration = formatDuration(data.timing || 0); const cost = `$${(data.cost || 0).toFixed(4)}`; console.log( `${chalk.green(agentDisplay)} ${chalk.green('βœ“ Success')} ${vulnCount.toString().padStart(5)} ` + `${data.attempts}/3 ${duration.padEnd(11)} ${cost}` ); // Show log file path for detailed review if (data.logFile) { const relativePath = path.relative(process.cwd(), data.logFile); console.log(chalk.gray(` └─ Detailed log: ${relativePath}`)); } } else { const error = result.reason.error || result.reason; failed.push({ agent: agentName, error: error.message }); const attempts = result.reason.attempts || 3; // Default to 3 if not available console.log( `${chalk.red(agentDisplay)} ${chalk.red('βœ— Failed ')} - ` + `${attempts}/3 - -` ); console.log(chalk.gray(` └─ ${error.message.substring(0, 60)}...`)); } }); console.log(chalk.gray('─'.repeat(80))); console.log(chalk.cyan(`Summary: ${completed.length}/${activeAgents.length} succeeded in ${formatDuration(totalDuration)}`)); return { completed, failed }; }; // Run exploitation agents in parallel const runParallelExploit = async (session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt) => { const exploitAgents = ['injection-exploit', 'xss-exploit', 'auth-exploit', 'ssrf-exploit', 'authz-exploit']; // Get fresh session data to ensure we have the latest vulnerability analysis results // This prevents race conditions where parallel vuln agents haven't updated session state yet const { getSession } = await import('./session-manager.js'); const freshSession = await getSession(session.id); // Load validation module const { safeValidateQueueAndDeliverable } = await import('./queue-validation.js'); // Only run exploit agents whose vuln counterparts completed successfully AND found vulnerabilities const eligibilityChecks = await Promise.all( exploitAgents.map(async (agentName) => { const vulnAgentName = agentName.replace('-exploit', '-vuln'); // Must have completed the vulnerability analysis if (!freshSession.completedAgents.includes(vulnAgentName)) { return { agentName, eligible: false }; } // Check if vulnerabilities were found by validating the queue file const vulnType = vulnAgentName.replace('-vuln', ''); // "injection-vuln" -> "injection" const validation = await safeValidateQueueAndDeliverable(vulnType, freshSession.targetRepo); if (!validation.success || !validation.data.shouldExploit) { console.log(chalk.gray(`⏭️ Skipping ${agentName} (no vulnerabilities found in ${vulnAgentName})`)); return { agentName, eligible: false }; } console.log(chalk.blue(`βœ“ ${agentName} eligible (${validation.data.vulnerabilityCount} vulnerabilities from ${vulnAgentName})`)); return { agentName, eligible: true }; }) ); const eligibleAgents = eligibilityChecks .filter(check => check.eligible) .map(check => check.agentName); const activeAgents = eligibleAgents.filter(agent => !freshSession.completedAgents.includes(agent)); if (activeAgents.length === 0) { if (eligibleAgents.length === 0) { console.log(chalk.gray('⏭️ No exploitation agents eligible (no vulnerabilities found)')); } else { console.log(chalk.gray('⏭️ All eligible exploitation agents already completed')); } return { completed: eligibleAgents, failed: [] }; } console.log(chalk.cyan(`\n🎯 Starting ${activeAgents.length} exploitation specialists in parallel...`)); console.log(chalk.gray(' Specialists: ' + activeAgents.join(', '))); console.log(); const startTime = Date.now(); // Collect all results without logging individual completions const results = await Promise.allSettled( activeAgents.map(async (agentName, index) => { // Add 2-second stagger to prevent API overwhelm await new Promise(resolve => setTimeout(resolve, index * 2000)); let lastError; let attempts = 0; const maxAttempts = 3; while (attempts < maxAttempts) { attempts++; try { const result = await runSingleAgent(agentName, freshSession, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt, false, true); return { agentName, ...result, attempts }; } catch (error) { lastError = error; if (attempts < maxAttempts) { console.log(chalk.yellow(`⚠️ ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`)); await new Promise(resolve => setTimeout(resolve, 5000)); } } } throw { agentName, error: lastError, attempts }; }) ); const totalDuration = Date.now() - startTime; // Process and display results in a nice table console.log(chalk.cyan('\n🎯 Exploitation Results')); console.log(chalk.gray('─'.repeat(80))); // Table header console.log(chalk.bold('Agent Status Result Attempt Duration Cost')); console.log(chalk.gray('─'.repeat(80))); const completed = []; const failed = []; results.forEach((result, index) => { const agentName = activeAgents[index]; const agentDisplay = agentName.padEnd(22); if (result.status === 'fulfilled') { const data = result.value; completed.push(agentName); const exploitResult = 'Success'; // Could be enhanced to show actual exploitation result const duration = formatDuration(data.timing || 0); const cost = `$${(data.cost || 0).toFixed(4)}`; console.log( `${chalk.green(agentDisplay)} ${chalk.green('βœ“ Success')} ${exploitResult.padEnd(6)} ` + `${data.attempts}/3 ${duration.padEnd(11)} ${cost}` ); // Show log file path for detailed review if (data.logFile) { const relativePath = path.relative(process.cwd(), data.logFile); console.log(chalk.gray(` └─ Detailed log: ${relativePath}`)); } } else { const error = result.reason.error || result.reason; failed.push({ agent: agentName, error: error.message }); const attempts = result.reason.attempts || 3; // Default to 3 if not available console.log( `${chalk.red(agentDisplay)} ${chalk.red('βœ— Failed ')} - ` + `${attempts}/3 - -` ); console.log(chalk.gray(` └─ ${error.message.substring(0, 60)}...`)); } }); console.log(chalk.gray('─'.repeat(80))); console.log(chalk.cyan(`Summary: ${completed.length}/${activeAgents.length} succeeded in ${formatDuration(totalDuration)}`)); return { completed, failed }; }; // Run all agents in a phase export const runPhase = async (phaseName, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt) => { console.log(chalk.cyan(`\nπŸ“‹ Running phase: ${phaseName} (parallel execution)`)); // Use parallel execution for both vulnerability-analysis and exploitation phases if (phaseName === 'vulnerability-analysis') { console.log(chalk.cyan('πŸš€ Using parallel execution for 5x faster vulnerability analysis')); const results = await runParallelVuln(session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt); if (results.failed.length > 0) { console.log(chalk.yellow(`⚠️ ${results.failed.length} agents failed, but phase continues`)); results.failed.forEach(failure => { console.log(chalk.red(` - ${failure.agent}: ${failure.error}`)); }); } console.log(chalk.green(`βœ… Phase '${phaseName}' completed: ${results.completed.length} succeeded, ${results.failed.length} failed`)); return; } if (phaseName === 'exploitation') { console.log(chalk.cyan('🎯 Using parallel execution for 5x faster exploitation')); const results = await runParallelExploit(session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt); if (results.failed.length > 0) { console.log(chalk.yellow(`⚠️ ${results.failed.length} agents failed, but phase continues`)); results.failed.forEach(failure => { console.log(chalk.red(` - ${failure.agent}: ${failure.error}`)); }); } console.log(chalk.green(`βœ… Phase '${phaseName}' completed: ${results.completed.length} succeeded, ${results.failed.length} failed`)); return; } // For other phases (pre-reconnaissance, reconnaissance, reporting), run the single agent const agents = validatePhase(phaseName); if (agents.length === 1) { const agent = agents[0]; if (session.completedAgents.includes(agent.name)) { console.log(chalk.gray(`⏭️ Agent '${agent.name}' already completed, skipping`)); return; } await runSingleAgent(agent.name, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt); console.log(chalk.green(`βœ… Phase '${phaseName}' completed successfully`)); } else { throw new PentestError(`Phase '${phaseName}' has multiple agents but no parallel execution defined`, 'validation', false); } }; // Rollback to specific agent checkpoint export const rollbackTo = async (targetAgent, session) => { console.log(chalk.yellow(`πŸ”„ Rolling back to agent: ${targetAgent}`)); await validateTargetRepo(session.targetRepo); validateAgent(targetAgent); if (!session.checkpoints[targetAgent]) { throw new PentestError( `No checkpoint found for agent '${targetAgent}' in session history`, 'validation', false, { targetAgent, availableCheckpoints: Object.keys(session.checkpoints) } ); } const commitHash = session.checkpoints[targetAgent]; // Rollback git workspace await rollbackGitToCommit(session.targetRepo, commitHash); // Update session state (removes agents from completedAgents) await rollbackToAgent(session.id, targetAgent); // Mark rolled-back agents in audit system (for forensic trail) try { const { AuditSession } = await import('./audit/index.js'); const auditSession = new AuditSession(session); await auditSession.initialize(); // Find agents that were rolled back (agents after targetAgent) const targetOrder = AGENTS[targetAgent].order; const rolledBackAgents = Object.values(AGENTS) .filter(agent => agent.order > targetOrder) .map(agent => agent.name); // Mark them as rolled-back in audit system if (rolledBackAgents.length > 0) { await auditSession.markMultipleRolledBack(rolledBackAgents); console.log(chalk.gray(` Marked ${rolledBackAgents.length} agents as rolled-back in audit logs`)); } } catch (error) { // Non-critical: rollback succeeded even if audit update failed console.log(chalk.yellow(` ⚠️ Failed to update audit logs: ${error.message}`)); } console.log(chalk.green(`βœ… Successfully rolled back to agent '${targetAgent}'`)); }; // Rerun specific agent (rollback to previous + run current) export const rerunAgent = async (agentName, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt) => { console.log(chalk.cyan(`πŸ” Rerunning agent: ${agentName}`)); const agent = validateAgent(agentName); // Find previous agent checkpoint or initial state let rollbackTarget = null; if (agent.prerequisites.length > 0) { // Find the last completed prerequisite const completedPrereqs = agent.prerequisites.filter(prereq => session.completedAgents.includes(prereq) ); if (completedPrereqs.length > 0) { // Get the prerequisite with highest order rollbackTarget = completedPrereqs.reduce((latest, current) => AGENTS[current].order > AGENTS[latest].order ? current : latest ); } } if (rollbackTarget) { console.log(chalk.blue(`πŸ“ Rolling back to prerequisite: ${rollbackTarget}`)); await rollbackTo(rollbackTarget, session); } else if (agent.name === 'pre-recon') { // Special case: rollback to initial clone console.log(chalk.blue(`πŸ“ Rolling back to initial repository state`)); try { const initialCommit = await executeGitCommandWithRetry(['git', 'log', '--reverse', '--format=%H'], session.targetRepo, 'finding initial commit'); const firstCommit = initialCommit.stdout.trim().split('\n')[0]; await rollbackGitToCommit(session.targetRepo, firstCommit); } catch (error) { console.log(chalk.yellow(`⚠️ Could not find initial commit, using HEAD: ${error.message}`)); } } // Run the target agent (allow rerun since we've explicitly rolled back) await runSingleAgent(agentName, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt, true); console.log(chalk.green(`βœ… Agent '${agentName}' rerun completed successfully`)); }; // Run all remaining agents to completion export const runAll = async (session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt) => { // Get all agents in order const allAgentNames = Object.keys(AGENTS); console.log(chalk.cyan(`\nπŸš€ Running all remaining agents to completion`)); console.log(chalk.gray(`Current progress: ${session.completedAgents.length}/${allAgentNames.length} agents completed`)); // Find remaining agents (not yet completed) const remainingAgents = allAgentNames.filter(agentName => !session.completedAgents.includes(agentName) ); if (remainingAgents.length === 0) { console.log(chalk.green('βœ… All agents already completed!')); return; } console.log(chalk.blue(`πŸ“‹ Remaining agents: ${remainingAgents.join(', ')}`)); console.log(); // Run each remaining agent in sequence for (const agentName of remainingAgents) { await runSingleAgent(agentName, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt); } console.log(chalk.green(`\nπŸŽ‰ All agents completed successfully! Session marked as completed.`)); }; // Display session status export const displayStatus = async (session) => { const status = getSessionStatus(session); const timeAgo = getTimeAgo(session.lastActivity); console.log(chalk.cyan(`Session: ${new URL(session.webUrl).hostname} + ${path.basename(session.repoPath)}`)); console.log(chalk.gray(`Session ID: ${session.id}`)); console.log(chalk.gray(`Source Directory: ${session.targetRepo}`)); // Check if final deliverable exists and show its path if (session.targetRepo) { const finalReportPath = path.join(session.targetRepo, 'deliverables', 'comprehensive_security_assessment_report.md'); try { if (await fs.pathExists(finalReportPath)) { console.log(chalk.gray(`Final Deliverable Available: ${finalReportPath}`)); } } catch (error) { // Silently ignore if we can't check the file } } const statusColor = status.status === 'completed' ? chalk.green : status.status === 'failed' ? chalk.red : chalk.blue; console.log(statusColor(`Status: ${status.status} (${status.completedCount}/${status.totalAgents} agents completed)`)); console.log(chalk.gray(`Last Activity: ${timeAgo}`)); if (session.configFile) { console.log(chalk.gray(`Config: ${session.configFile}`)); } // Display cost and timing breakdown if available if (session.costBreakdown || session.timingBreakdown) { console.log(); // Empty line before metrics if (session.timingBreakdown) { console.log(chalk.blue('⏱️ Timing Breakdown:')); console.log(chalk.gray(` Total Execution: ${formatDuration(session.timingBreakdown.total || 0)}`)); if (session.timingBreakdown.phases) { Object.entries(session.timingBreakdown.phases).forEach(([phase, duration]) => { console.log(chalk.gray(` ${phase}: ${formatDuration(duration)}`)); }); } if (session.timingBreakdown.agents) { console.log(chalk.gray(' Per Agent:')); Object.entries(session.timingBreakdown.agents).forEach(([agent, duration]) => { console.log(chalk.gray(` ${agent}: ${formatDuration(duration)}`)); }); } } if (session.costBreakdown) { console.log(chalk.blue('πŸ’° Cost Breakdown:')); console.log(chalk.gray(` Total Cost: $${(session.costBreakdown.total || 0).toFixed(4)}`)); if (session.costBreakdown.agents) { console.log(chalk.gray(' Per Agent:')); Object.entries(session.costBreakdown.agents).forEach(([agent, cost]) => { console.log(chalk.gray(` ${agent}: $${cost.toFixed(4)}`)); }); } } } console.log(); // Empty line // Display agent status const agentList = Object.values(AGENTS).sort((a, b) => a.order - b.order); for (const agent of agentList) { let statusIcon, statusText, statusColor; if (session.completedAgents.includes(agent.name)) { statusIcon = 'βœ…'; statusText = `completed ${getTimeAgoForAgent(session, agent.name)}`; statusColor = chalk.green; } else if (session.failedAgents.includes(agent.name)) { statusIcon = '❌'; statusText = `failed ${getTimeAgoForAgent(session, agent.name)}`; statusColor = chalk.red; } else { statusIcon = '⏸️'; statusText = 'pending'; statusColor = chalk.gray; } const displayName = agent.name.replace(/-/g, ' '); console.log(`${statusIcon} ${statusColor(displayName.padEnd(20))} (${statusText})`); } // Show next action const nextAgent = getNextAgent(session); if (nextAgent) { console.log(chalk.cyan(`\nNext: Run --run-agent ${nextAgent.name}`)); } else if (status.failedCount > 0) { const failedAgent = session.failedAgents[0]; console.log(chalk.yellow(`\nNext: Fix ${failedAgent} failure or run --rerun ${failedAgent}`)); } else if (status.status === 'completed') { console.log(chalk.green('\nAll agents completed successfully! πŸŽ‰')); } }; // List all available agents export const listAgents = () => { console.log(chalk.cyan('Available Agents:')); const phaseNames = Object.keys(PHASES); phaseNames.forEach((phaseName, phaseIndex) => { const phaseAgents = PHASES[phaseName]; const phaseDisplayName = phaseName.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); console.log(chalk.yellow(`\nPhase ${phaseIndex + 1} - ${phaseDisplayName}:`)); phaseAgents.forEach(agentName => { const agent = AGENTS[agentName]; console.log(chalk.white(` ${agent.name.padEnd(18)} ${agent.displayName}`)); }); }); }; // Helper function to get prompt name from agent name const getPromptName = (agentName) => { const mappings = { 'pre-recon': 'pre-recon-code', 'recon': 'recon', 'injection-vuln': 'vuln-injection', 'xss-vuln': 'vuln-xss', 'auth-vuln': 'vuln-auth', 'ssrf-vuln': 'vuln-ssrf', 'authz-vuln': 'vuln-authz', 'injection-exploit': 'exploit-injection', 'xss-exploit': 'exploit-xss', 'auth-exploit': 'exploit-auth', 'ssrf-exploit': 'exploit-ssrf', 'authz-exploit': 'exploit-authz', 'report': 'report-executive' }; return mappings[agentName] || agentName; }; // Helper function to get time ago for specific agent const getTimeAgoForAgent = (session, agentName) => { // This would need to be implemented based on session checkpoint timestamps // For now, just return relative to last activity return getTimeAgo(session.lastActivity); }; // Helper function for time ago calculation const getTimeAgo = (timestamp) => { const now = new Date(); const past = new Date(timestamp); const diffMs = now - past; const diffMins = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMs / (1000 * 60 * 60)); const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); if (diffMins < 60) { return `${diffMins}m ago`; } else if (diffHours < 24) { return `${diffHours}h ago`; } else { return `${diffDays}d ago`; } };