diff --git a/docker-compose.yml b/docker-compose.yml index 852ac11..211e2bf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,7 @@ services: volumes: - ./prompts:/app/prompts - ./audit-logs:/app/audit-logs + - ${OUTPUT_DIR:-./audit-logs}:/app/output - ${TARGET_REPO:-.}:/target-repo - ${BENCHMARKS_BASE:-.}:/benchmarks shm_size: 2gb diff --git a/shannon b/shannon index 9aaf223..61f8a4e 100755 --- a/shannon +++ b/shannon @@ -14,7 +14,15 @@ fi show_help() { cat << 'EOF' -Shannon - AI Penetration Testing Framework + + ███████╗██╗ ██╗ █████╗ ███╗ ██╗███╗ ██╗ ██████╗ ███╗ ██╗ + ██╔════╝██║ ██║██╔══██╗████╗ ██║████╗ ██║██╔═══██╗████╗ ██║ + ███████╗███████║███████║██╔██╗ ██║██╔██╗ ██║██║ ██║██╔██╗ ██║ + ╚════██║██╔══██║██╔══██║██║╚██╗██║██║╚██╗██║██║ ██║██║╚██╗██║ + ███████║██║ ██║██║ ██║██║ ╚████║██║ ╚████║╚██████╔╝██║ ╚████║ + ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═══╝ + + AI Penetration Testing Framework Usage: ./shannon start URL= REPO= Start a pentest workflow @@ -25,15 +33,16 @@ Usage: Options for 'start': CONFIG= Configuration file (YAML) - OUTPUT= Output directory for reports + OUTPUT= Output directory for reports (default: ./audit-logs/) PIPELINE_TESTING=true Use minimal prompts for fast testing Options for 'stop': - CLEAN=true Remove all data including volumes + CLEAN=true Remove all data including volumes Examples: ./shannon start URL=https://example.com REPO=/path/to/repo ./shannon start URL=https://example.com REPO=/path/to/repo CONFIG=./config.yaml + ./shannon start URL=https://example.com REPO=/path/to/repo OUTPUT=./my-reports ./shannon logs ID=example.com_shannon-1234567890 ./shannon query ID=shannon-1234567890 ./shannon stop CLEAN=true @@ -64,8 +73,15 @@ is_temporal_ready() { temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING" } -# Ensure containers are running +# Ensure containers are running with correct mounts ensure_containers() { + # If custom OUTPUT_DIR is set, always refresh worker to ensure correct volume mount + # Docker compose will only recreate if the mount actually changed + if [ -n "$OUTPUT_DIR" ]; then + echo "Ensuring worker has correct output mount..." + docker compose -f "$COMPOSE_FILE" up -d worker 2>/dev/null || true + fi + # Quick check: if Temporal is already healthy, we're good if is_temporal_ready; then return 0 @@ -125,13 +141,27 @@ cmd_start() { ;; esac + # Handle custom OUTPUT directory + # Export OUTPUT_DIR for docker-compose volume mount BEFORE starting containers + if [ -n "$OUTPUT" ]; then + # Create output directory if it doesn't exist + mkdir -p "$OUTPUT" + export OUTPUT_DIR="$OUTPUT" + fi + # Ensure containers are running (starts them if needed) ensure_containers # Build optional args ARGS="" [ -n "$CONFIG" ] && ARGS="$ARGS --config $CONFIG" - [ -n "$OUTPUT" ] && ARGS="$ARGS --output $OUTPUT" + + # Pass container path for output (where OUTPUT_DIR is mounted) + # Also pass display path so client can show the host path to user + if [ -n "$OUTPUT" ]; then + ARGS="$ARGS --output /app/output --display-output $OUTPUT" + fi + [ "$PIPELINE_TESTING" = "true" ] && ARGS="$ARGS --pipeline-testing" # Run the client to submit workflow @@ -148,18 +178,31 @@ cmd_logs() { exit 1 fi - WORKFLOW_LOG="./audit-logs/${ID}/workflow.log" + # Auto-discover the workflow log file + # 1. Check default location first + # 2. Search common output directories + # 3. Fall back to find command + WORKFLOW_LOG="" - if [ -f "$WORKFLOW_LOG" ]; then + if [ -f "./audit-logs/${ID}/workflow.log" ]; then + WORKFLOW_LOG="./audit-logs/${ID}/workflow.log" + else + # Search for the workflow directory (handles custom OUTPUT paths) + FOUND=$(find . -maxdepth 3 -path "*/${ID}/workflow.log" -type f 2>/dev/null | head -1) + if [ -n "$FOUND" ]; then + WORKFLOW_LOG="$FOUND" + fi + fi + + if [ -n "$WORKFLOW_LOG" ]; then echo "Tailing workflow log: $WORKFLOW_LOG" tail -f "$WORKFLOW_LOG" else - echo "ERROR: Workflow log not found: $WORKFLOW_LOG" + echo "ERROR: Workflow log not found for ID: $ID" echo "" echo "Possible causes:" echo " - Workflow hasn't started yet" echo " - Workflow ID is incorrect" - echo " - Workflow is using a custom OUTPUT path" echo "" echo "Check: ./shannon query ID=$ID for workflow details" exit 1 diff --git a/src/temporal/client.ts b/src/temporal/client.ts index f3e345c..945af42 100644 --- a/src/temporal/client.ts +++ b/src/temporal/client.ts @@ -74,6 +74,7 @@ async function startPipeline(): Promise { let repoPath: string | undefined; let configPath: string | undefined; let outputPath: string | undefined; + let displayOutputPath: string | undefined; // Host path for display purposes let pipelineTestingMode = false; let customWorkflowId: string | undefined; let waitForCompletion = false; @@ -92,6 +93,12 @@ async function startPipeline(): Promise { outputPath = nextArg; i++; } + } else if (arg === '--display-output') { + const nextArg = args[i + 1]; + if (nextArg && !nextArg.startsWith('-')) { + displayOutputPath = nextArg; + i++; + } } else if (arg === '--workflow-id') { const nextArg = args[i + 1]; if (nextArg && !nextArg.startsWith('-')) { @@ -138,6 +145,11 @@ async function startPipeline(): Promise { ...(pipelineTestingMode && { pipelineTestingMode }), }; + // Determine output directory for display + // Use displayOutputPath (host path) if provided, otherwise fall back to outputPath or default + const effectiveDisplayPath = displayOutputPath || outputPath || './audit-logs'; + const outputDir = `${effectiveDisplayPath}/${workflowId}`; + console.log(chalk.green.bold(`✓ Workflow started: ${workflowId}`)); console.log(); console.log(chalk.white(' Target: ') + chalk.cyan(webUrl)); @@ -145,6 +157,9 @@ async function startPipeline(): Promise { if (configPath) { console.log(chalk.white(' Config: ') + chalk.cyan(configPath)); } + if (displayOutputPath) { + console.log(chalk.white(' Output: ') + chalk.cyan(displayOutputPath)); + } if (pipelineTestingMode) { console.log(chalk.white(' Mode: ') + chalk.yellow('Pipeline Testing')); } @@ -166,6 +181,9 @@ async function startPipeline(): Promise { console.log(chalk.white(' Logs: ') + chalk.gray(`./shannon logs ID=${workflowId}`)); console.log(chalk.white(' Query: ') + chalk.gray(`./shannon query ID=${workflowId}`)); console.log(); + console.log(chalk.bold('Output:')); + console.log(chalk.white(' Reports: ') + chalk.cyan(outputDir)); + console.log(); return; }