diff --git a/CLAUDE.md b/CLAUDE.md index f5a7321..75ad253 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -15,13 +15,14 @@ npm install ### Running the Penetration Testing Agent ```bash -shannon --config +shannon [--config ] [--output ] ``` Example: ```bash shannon "https://example.com" "/path/to/local/repo" shannon "https://juice-shop.herokuapp.com" "/home/user/juice-shop" --config juice-shop-config.yaml +shannon "https://example.com" "/path/to/repo" --output /path/to/reports ``` ### Alternative Execution @@ -194,10 +195,11 @@ The agent implements a sophisticated checkpoint system using git: The agent implements a crash-safe, self-healing audit system (v3.0) with the following guarantees: **Architecture:** -- **audit-logs/**: Centralized metrics and forensic logs (source of truth) +- **audit-logs/** (or custom `--output` path): Centralized metrics and forensic logs (source of truth) - `{hostname}_{sessionId}/session.json` - Comprehensive metrics with attempt-level detail - `{hostname}_{sessionId}/prompts/` - Exact prompts used for reproducibility - `{hostname}_{sessionId}/agents/` - Turn-by-turn execution logs + - `{hostname}_{sessionId}/deliverables/` - Security reports and findings - **.shannon-store.json**: Minimal orchestration state (completedAgents, checkpoints) **Crash Safety:** @@ -287,13 +289,15 @@ dist/ # Compiled JavaScript output └── ... # Other compiled files package.json # Node.js dependencies .shannon-store.json # Orchestration state (minimal) -audit-logs/ # Centralized audit data (v3.0) +audit-logs/ # Centralized audit data (default, or use --output) └── {hostname}_{sessionId}/ ├── session.json # Comprehensive metrics ├── prompts/ # Prompt snapshots │ └── {agent}.md - └── agents/ # Agent execution logs - └── {timestamp}_{agent}_attempt-{N}.log + ├── agents/ # Agent execution logs + │ └── {timestamp}_{agent}_attempt-{N}.log + └── deliverables/ # Security reports and findings + └── ... configs/ # Configuration files ├── config-schema.json # JSON Schema validation ├── example-config.yaml # Template configuration diff --git a/README.md b/README.md index 8fbf5e0..dfcaf93 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,15 @@ docker run --rm -it \ -e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \ -v "$(pwd)/repos:/app/repos" \ -v "$(pwd)/configs:/app/configs" \ + # Comment below line if using custom output directory + -v "$(pwd)/audit-logs:/app/audit-logs" \ shannon:latest \ "https://your-app.com/" \ "/app/repos/your-app" \ --config /app/configs/example-config.yaml + # Optional: uncomment below for custom output directory + # -v "$(pwd)/reports:/app/reports" \ + # --output /app/reports ``` **With Anthropic API Key:** @@ -186,10 +191,15 @@ docker run --rm -it \ -e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \ -v "$(pwd)/repos:/app/repos" \ -v "$(pwd)/configs:/app/configs" \ + # Comment below line if using custom output directory + -v "$(pwd)/audit-logs:/app/audit-logs" \ shannon:latest \ "https://your-app.com/" \ "/app/repos/your-app" \ --config /app/configs/example-config.yaml + # Optional: uncomment below for custom output directory + # -v "$(pwd)/reports:/app/reports" \ + # --output /app/reports ``` #### Platform-Specific Instructions @@ -217,10 +227,15 @@ docker run --rm -it \ -e CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000 \ -v "$(pwd)/repos:/app/repos" \ -v "$(pwd)/configs:/app/configs" \ + # Comment below line if using custom output directory + -v "$(pwd)/audit-logs:/app/audit-logs" \ shannon:latest \ "http://host.docker.internal:3000" \ "/app/repos/your-app" \ --config /app/configs/example-config.yaml + # Optional: uncomment below for custom output directory + # -v "$(pwd)/reports:/app/reports" \ + # --output /app/reports ``` ### Configuration (Optional) @@ -281,7 +296,7 @@ docker run --rm shannon:latest --status ### Output and Results -All analysis results are saved to the `deliverables/` directory: +All results are saved to `./audit-logs/` by default. Use `--output ` to specify a custom directory. If using `--output`, ensure that path is mounted to an accessible host directory (e.g., `-v "$(pwd)/custom-directory:/app/reports"`). - **Pre-reconnaissance reports** - External scan results - **Vulnerability assessments** - Potential vulnerabilities from thorough code analysis and network mapping diff --git a/audit-logs/.gitkeep b/audit-logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/audit/utils.ts b/src/audit/utils.ts index ffc2c78..b05d0d6 100644 --- a/src/audit/utils.ts +++ b/src/audit/utils.ts @@ -26,6 +26,7 @@ export interface SessionMetadata { id: string; webUrl: string; repoPath?: string; + outputPath?: string; [key: string]: unknown; } @@ -40,10 +41,12 @@ export function generateSessionIdentifier(sessionMetadata: SessionMetadata): str /** * Generate path to audit log directory for a session + * Uses custom outputPath if provided, otherwise defaults to AUDIT_LOGS_DIR */ export function generateAuditPath(sessionMetadata: SessionMetadata): string { const sessionIdentifier = generateSessionIdentifier(sessionMetadata); - return path.join(AUDIT_LOGS_DIR, sessionIdentifier); + const baseDir = sessionMetadata.outputPath || AUDIT_LOGS_DIR; + return path.join(baseDir, sessionIdentifier); } /** diff --git a/src/checkpoint-manager.ts b/src/checkpoint-manager.ts index d0f13f6..5a8e03f 100644 --- a/src/checkpoint-manager.ts +++ b/src/checkpoint-manager.ts @@ -293,7 +293,7 @@ const runSingleAgent = async ( AGENTS[agentName]!.displayName, agentName, getAgentColor(agentName), - { id: currentSession.id, webUrl: currentSession.webUrl, repoPath: currentSession.repoPath } + { id: currentSession.id, webUrl: currentSession.webUrl, repoPath: currentSession.repoPath, ...(currentSession.outputPath && { outputPath: currentSession.outputPath }) } ); if (!result.success) { @@ -727,7 +727,8 @@ export const rollbackTo = async (targetAgent: string, session: Session): Promise const sessionMetadata: SessionMetadata = { id: session.id, webUrl: session.webUrl, - repoPath: session.repoPath + repoPath: session.repoPath, + ...(session.outputPath && { outputPath: session.outputPath }) }; const auditSession = new AuditSession(sessionMetadata); await auditSession.initialize(); diff --git a/src/cli/ui.ts b/src/cli/ui.ts index 413ab54..07f49ba 100644 --- a/src/cli/ui.ts +++ b/src/cli/ui.ts @@ -33,6 +33,9 @@ export function showHelp(): void { console.log( ' --config YAML configuration file for authentication and testing parameters' ); + console.log( + ' --output Custom output directory for session folder (default: ./audit-logs/)' + ); console.log( ' --pipeline-testing Use minimal prompts for fast pipeline testing (creates minimal deliverables)' ); @@ -55,6 +58,7 @@ export function showHelp(): void { console.log(' # Normal mode - create new session'); console.log(' shannon "https://example.com" "/path/to/local/repo"'); console.log(' shannon "https://example.com" "/path/to/local/repo" --config auth.yaml'); + console.log(' shannon "https://example.com" "/path/to/local/repo" --output /path/to/reports'); console.log( ' shannon "https://example.com" "/path/to/local/repo" --setup-only # Setup only\n' ); diff --git a/src/phases/pre-recon.ts b/src/phases/pre-recon.ts index edbd395..f3ba1e2 100644 --- a/src/phases/pre-recon.ts +++ b/src/phases/pre-recon.ts @@ -137,7 +137,8 @@ async function runPreReconWave1( variables: PromptVariables, config: DistributedConfig | null, pipelineTestingMode: boolean = false, - sessionId: string | null = null + sessionId: string | null = null, + outputPath: string | null = null ): Promise { console.log(chalk.blue(' → Launching Wave 1 operations in parallel...')); @@ -155,7 +156,7 @@ async function runPreReconWave1( AGENTS['pre-recon'].displayName, 'pre-recon', // Agent name for snapshot creation chalk.cyan, - { id: sessionId!, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field) + { id: sessionId!, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field) ) ); const [codeAnalysis] = await Promise.all(operations); @@ -178,7 +179,7 @@ async function runPreReconWave1( AGENTS['pre-recon'].displayName, 'pre-recon', // Agent name for snapshot creation chalk.cyan, - { id: sessionId!, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field) + { id: sessionId!, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field) ) ); } @@ -357,13 +358,14 @@ export async function executePreReconPhase( config: DistributedConfig | null, toolAvailability: ToolAvailability, pipelineTestingMode: boolean, - sessionId: string | null = null + sessionId: string | null = null, + outputPath: string | null = null ): Promise { console.log(chalk.yellow.bold('\n🔍 PHASE 1: PRE-RECONNAISSANCE')); const timer = new Timer('phase-1-pre-recon'); console.log(chalk.yellow('Wave 1: Initial footprinting...')); - const wave1Results = await runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTestingMode, sessionId); + const wave1Results = await runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTestingMode, sessionId, outputPath); console.log(chalk.green(' ✅ Wave 1 operations completed')); console.log(chalk.yellow('Wave 2: Additional scanning...')); diff --git a/src/session-manager.ts b/src/session-manager.ts index 8433778..00b3257 100644 --- a/src/session-manager.ts +++ b/src/session-manager.ts @@ -41,6 +41,7 @@ export interface Session { repoPath: string; configFile: string | null; targetRepo: string; + outputPath: string | null; status: 'in-progress' | 'completed' | 'failed'; completedAgents: AgentName[]; failedAgents: AgentName[]; @@ -265,7 +266,8 @@ export const createSession = async ( webUrl: string, repoPath: string, configFile: string | null = null, - targetRepo: string | null = null + targetRepo: string | null = null, + outputPath: string | null = null ): Promise => { // Use targetRepo if provided, otherwise use repoPath const resolvedTargetRepo = targetRepo || repoPath; @@ -279,9 +281,12 @@ export const createSession = async ( console.log(chalk.blue(`📝 Reusing existing session: ${existingSession.id.substring(0, 8)}...`)); console.log(chalk.gray(` Progress: ${existingSession.completedAgents.length}/${Object.keys(AGENTS).length} agents completed`)); - // Update last activity timestamp - await updateSession(existingSession.id, { lastActivity: new Date().toISOString() }); - return existingSession; + // Update last activity timestamp and outputPath if provided + await updateSession(existingSession.id, { + lastActivity: new Date().toISOString(), + ...(outputPath && { outputPath }) + }); + return { ...existingSession, ...(outputPath && { outputPath }) }; } // If completed, create a new session (allows re-running after completion) @@ -298,6 +303,7 @@ export const createSession = async ( repoPath, configFile, targetRepo: resolvedTargetRepo, + outputPath, status: 'in-progress', completedAgents: [], failedAgents: [], diff --git a/src/shannon.ts b/src/shannon.ts index b597012..54974c6 100644 --- a/src/shannon.ts +++ b/src/shannon.ts @@ -80,6 +80,27 @@ interface MainResult { // Configure zx to disable timeouts (let tools run as long as needed) $.timeout = 0; +/** + * Consolidate deliverables from target repo into the session folder + * Copies deliverables directory from source repo to session audit path + */ +async function consolidateOutputs(sourceDir: string, sessionPath: string): Promise { + const srcDeliverables = path.join(sourceDir, 'deliverables'); + const destDeliverables = path.join(sessionPath, 'deliverables'); + + try { + if (await fs.pathExists(srcDeliverables)) { + await fs.copy(srcDeliverables, destDeliverables, { overwrite: true }); + console.log(chalk.gray(`📄 Deliverables copied to session folder`)); + } else { + console.log(chalk.yellow(`⚠️ No deliverables directory found at ${srcDeliverables}`)); + } + } catch (error) { + const err = error as Error; + console.log(chalk.yellow(`⚠️ Failed to consolidate deliverables: ${err.message}`)); + } +} + // Setup graceful cleanup on process signals process.on('SIGINT', async () => { console.log(chalk.yellow('\n⚠️ Received SIGINT, cleaning up...')); @@ -99,7 +120,8 @@ async function main( repoPath: string, configPath: string | null = null, pipelineTestingMode: boolean = false, - disableLoader: boolean = false + disableLoader: boolean = false, + outputPath: string | null = null ): Promise { // Set global flag for loader control global.SHANNON_DISABLE_LOADER = disableLoader; @@ -116,6 +138,9 @@ async function main( if (configPath) { console.log(chalk.cyan(`⚙️ Config: ${configPath}`)); } + if (outputPath) { + console.log(chalk.cyan(`📂 Output: ${outputPath}`)); + } console.log(chalk.gray('─'.repeat(60))); // Parse configuration if provided @@ -166,7 +191,7 @@ async function main( const variables: PromptVariables = { webUrl, repoPath, sourceDir }; // Create session for tracking (in normal mode) - const session: Session = await createSession(webUrl, repoPath, configPath, sourceDir); + const session: Session = await createSession(webUrl, repoPath, configPath, sourceDir, outputPath); console.log(chalk.blue(`📝 Session created: ${session.id.substring(0, 8)}...`)); // If setup-only mode, exit after session creation @@ -243,7 +268,8 @@ async function main( distributedConfig, toolAvailability, pipelineTestingMode, - session.id // Pass session ID for logging + session.id, // Pass session ID for logging + outputPath // Pass output path for audit logging ); timingResults.phases['pre-recon'] = preReconDuration; await updateSessionProgress('pre-recon'); @@ -262,7 +288,7 @@ async function main( AGENTS['recon'].displayName, 'recon', // Agent name for snapshot creation chalk.cyan, - { id: session.id, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field) + { id: session.id, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field) ); const reconDuration = reconTimer.stop(); timingResults.phases['recon'] = reconDuration; @@ -345,7 +371,7 @@ async function main( 'Executive Summary and Report Cleanup', 'report', // Agent name for snapshot creation chalk.cyan, - { id: session.id, webUrl, repoPath: sourceDir } // Session metadata for audit logging (STANDARD: use 'id' field) + { id: session.id, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field) ); const reportDuration = reportTimer.stop(); @@ -380,7 +406,11 @@ async function main( console.log(chalk.gray('─'.repeat(60))); // Calculate audit logs path - const auditLogsPath = generateAuditPath({ id: session.id, webUrl: session.webUrl, repoPath: session.repoPath }); + const auditLogsPath = generateAuditPath({ id: session.id, webUrl: session.webUrl, repoPath: session.repoPath, ...(outputPath && { outputPath }) }); + + // Consolidate deliverables into the session folder + await consolidateOutputs(sourceDir, auditLogsPath); + console.log(chalk.green(`\n📂 All outputs consolidated: ${auditLogsPath}`)); // Return final report path and audit logs path for clickable output return { @@ -398,6 +428,7 @@ if (args[0] && args[0].includes('shannon')) { // Parse flags and arguments let configPath: string | null = null; +let outputPath: string | null = null; let pipelineTestingMode = false; let disableLoader = false; const nonFlagArgs: string[] = []; @@ -413,6 +444,14 @@ for (let i = 0; i < args.length; i++) { console.log(chalk.red('❌ --config flag requires a file path')); process.exit(1); } + } else if (args[i] === '--output') { + if (i + 1 < args.length) { + outputPath = path.resolve(args[i + 1]!); + i++; // Skip the next argument + } else { + console.log(chalk.red('❌ --output flag requires a directory path')); + process.exit(1); + } } else if (args[i] === '--pipeline-testing') { pipelineTestingMode = true; } else if (args[i] === '--disable-loader') { @@ -494,6 +533,9 @@ 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 (outputPath) { + console.log(chalk.gray(` Output Path: ${outputPath}\n`)); +} if (pipelineTestingMode) { console.log(chalk.yellow('⚡ PIPELINE TESTING MODE ENABLED - Using minimal test prompts for fast pipeline validation\n')); } @@ -502,7 +544,7 @@ if (disableLoader) { } try { - const result = await main(webUrl!, repoPathValidation.path!, configPath, pipelineTestingMode, disableLoader); + const result = await main(webUrl!, repoPathValidation.path!, configPath, pipelineTestingMode, disableLoader, outputPath); console.log(chalk.green.bold('\n📄 FINAL REPORT AVAILABLE:')); console.log(chalk.cyan(result.reportPath)); console.log(chalk.green.bold('\n📂 AUDIT LOGS AVAILABLE:'));