#!/bin/bash # Shannon CLI - AI Penetration Testing Framework set -e COMPOSE_FILE="docker-compose.yml" # Load .env if present if [ -f .env ]; then set -a source .env set +a fi show_help() { cat << 'EOF' Shannon - AI Penetration Testing Framework Usage: ./shannon start URL= REPO= Start a pentest workflow ./shannon logs ID= Tail logs for a specific workflow ./shannon query ID= Query workflow progress ./shannon stop Stop all containers ./shannon help Show this help message Options for 'start': CONFIG= Configuration file (YAML) OUTPUT= Output directory for reports PIPELINE_TESTING=true Use minimal prompts for fast testing Options for 'stop': 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 logs ID=example.com_shannon-1234567890 ./shannon query ID=shannon-1234567890 ./shannon stop CLEAN=true Monitor workflows at http://localhost:8233 EOF } # Parse KEY=value arguments into variables parse_args() { for arg in "$@"; do case "$arg" in URL=*) URL="${arg#URL=}" ;; REPO=*) REPO="${arg#REPO=}" ;; CONFIG=*) CONFIG="${arg#CONFIG=}" ;; OUTPUT=*) OUTPUT="${arg#OUTPUT=}" ;; ID=*) ID="${arg#ID=}" ;; CLEAN=*) CLEAN="${arg#CLEAN=}" ;; PIPELINE_TESTING=*) PIPELINE_TESTING="${arg#PIPELINE_TESTING=}" ;; REBUILD=*) REBUILD="${arg#REBUILD=}" ;; esac done } # Check if Temporal is running and healthy is_temporal_ready() { docker compose -f "$COMPOSE_FILE" exec -T temporal \ temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING" } # Ensure containers are running ensure_containers() { # Quick check: if Temporal is already healthy, we're good if is_temporal_ready; then return 0 fi # Need to start containers echo "Starting Shannon containers..." if [ "$REBUILD" = "true" ]; then # Force rebuild without cache (use when code changes aren't being picked up) echo "Rebuilding with --no-cache..." docker compose -f "$COMPOSE_FILE" build --no-cache worker fi docker compose -f "$COMPOSE_FILE" up -d --build # Wait for Temporal to be ready echo "Waiting for Temporal to be ready..." for i in $(seq 1 30); do if is_temporal_ready; then echo "Temporal is ready!" return 0 fi if [ "$i" -eq 30 ]; then echo "Timeout waiting for Temporal" exit 1 fi sleep 2 done } cmd_start() { parse_args "$@" # Validate required vars if [ -z "$URL" ] || [ -z "$REPO" ]; then echo "ERROR: URL and REPO are required" echo "Usage: ./shannon start URL= REPO=" exit 1 fi # Check for API key if [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then echo "ERROR: Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env" exit 1 fi # Determine container path for REPO # - If REPO is already a container path (/benchmarks/*, /target-repo), use as-is # - Otherwise, it's a host path - mount to /target-repo and use that case "$REPO" in /benchmarks/*|/target-repo|/target-repo/*) CONTAINER_REPO="$REPO" ;; *) # Host path - export for docker-compose mount export TARGET_REPO="$REPO" CONTAINER_REPO="/target-repo" ;; esac # 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" [ "$PIPELINE_TESTING" = "true" ] && ARGS="$ARGS --pipeline-testing" # Run the client to submit workflow docker compose -f "$COMPOSE_FILE" exec -T worker \ node dist/temporal/client.js "$URL" "$CONTAINER_REPO" $ARGS } cmd_logs() { parse_args "$@" if [ -z "$ID" ]; then echo "ERROR: ID is required" echo "Usage: ./shannon logs ID=" exit 1 fi WORKFLOW_LOG="./audit-logs/${ID}/workflow.log" if [ -f "$WORKFLOW_LOG" ]; then echo "Tailing workflow log: $WORKFLOW_LOG" tail -f "$WORKFLOW_LOG" else echo "ERROR: Workflow log not found: $WORKFLOW_LOG" 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 fi } cmd_query() { parse_args "$@" if [ -z "$ID" ]; then echo "ERROR: ID is required" echo "Usage: ./shannon query ID=" exit 1 fi docker compose -f "$COMPOSE_FILE" exec -T worker \ node dist/temporal/query.js "$ID" } cmd_stop() { parse_args "$@" if [ "$CLEAN" = "true" ]; then docker compose -f "$COMPOSE_FILE" down -v else docker compose -f "$COMPOSE_FILE" down fi } # Main command dispatch case "${1:-help}" in start) shift cmd_start "$@" ;; logs) shift cmd_logs "$@" ;; query) shift cmd_query "$@" ;; stop) shift cmd_stop "$@" ;; help|--help|-h|*) show_help ;; esac