ae4bd45a30
Support ANTHROPIC_BASE_URL + ANTHROPIC_AUTH_TOKEN in .env to route SDK requests through proxies or gateways. Preflight now validates the custom endpoint is reachable instead of skipping credential checks.
379 lines
14 KiB
Bash
Executable File
379 lines
14 KiB
Bash
Executable File
#!/bin/bash
|
|
# Shannon CLI - AI Penetration Testing Framework
|
|
|
|
set -e
|
|
|
|
# Prevent MSYS from converting Unix paths (e.g. /repos/my-repo) to Windows paths
|
|
case "$OSTYPE" in
|
|
msys*) export MSYS_NO_PATHCONV=1 ;;
|
|
esac
|
|
|
|
# Detect Podman vs Docker and set compose files accordingly
|
|
# Podman doesn't support host-gateway, so we only include the Docker override for actual Docker
|
|
COMPOSE_BASE="docker-compose.yml"
|
|
if command -v podman &>/dev/null; then
|
|
# Podman detected (either native or via Docker Desktop shim) - use base config only
|
|
COMPOSE_OVERRIDE=""
|
|
else
|
|
# Docker detected - include extra_hosts override for Linux localhost access
|
|
COMPOSE_OVERRIDE="-f docker-compose.docker.yml"
|
|
fi
|
|
COMPOSE_FILE="$COMPOSE_BASE"
|
|
|
|
# Load .env if present
|
|
if [ -f .env ]; then
|
|
set -a
|
|
source .env
|
|
set +a
|
|
fi
|
|
|
|
show_help() {
|
|
cat << 'EOF'
|
|
|
|
███████╗██╗ ██╗ █████╗ ███╗ ██╗███╗ ██╗ ██████╗ ███╗ ██╗
|
|
██╔════╝██║ ██║██╔══██╗████╗ ██║████╗ ██║██╔═══██╗████╗ ██║
|
|
███████╗███████║███████║██╔██╗ ██║██╔██╗ ██║██║ ██║██╔██╗ ██║
|
|
╚════██║██╔══██║██╔══██║██║╚██╗██║██║╚██╗██║██║ ██║██║╚██╗██║
|
|
███████║██║ ██║██║ ██║██║ ╚████║██║ ╚████║╚██████╔╝██║ ╚████║
|
|
╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═══╝ ╚═════╝ ╚═╝ ╚═══╝
|
|
|
|
AI Penetration Testing Framework
|
|
|
|
Usage:
|
|
./shannon start URL=<url> REPO=<name> Start a pentest workflow
|
|
./shannon workspaces List all workspaces
|
|
./shannon logs ID=<workflow-id> Tail logs for a specific workflow
|
|
./shannon stop Stop all containers
|
|
./shannon help Show this help message
|
|
|
|
Options for 'start':
|
|
REPO=<name> Folder name under ./repos/ (e.g. REPO=repo-name)
|
|
CONFIG=<path> Configuration file (YAML)
|
|
OUTPUT=<path> Output directory for reports (default: ./audit-logs/)
|
|
WORKSPACE=<name> Named workspace (auto-resumes if exists, creates if new)
|
|
PIPELINE_TESTING=true Use minimal prompts for fast testing
|
|
ROUTER=true Route requests through claude-code-router (multi-model support)
|
|
|
|
Options for 'stop':
|
|
CLEAN=true Remove all data including volumes
|
|
|
|
Examples:
|
|
./shannon start URL=https://example.com REPO=repo-name
|
|
./shannon start URL=https://example.com REPO=repo-name WORKSPACE=q1-audit
|
|
./shannon start URL=https://example.com REPO=repo-name CONFIG=./config.yaml
|
|
./shannon start URL=https://example.com REPO=repo-name OUTPUT=./my-reports
|
|
./shannon workspaces
|
|
./shannon logs ID=example.com_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=}" ;;
|
|
ROUTER=*) ROUTER="${arg#ROUTER=}" ;;
|
|
WORKSPACE=*) WORKSPACE="${arg#WORKSPACE=}" ;;
|
|
esac
|
|
done
|
|
}
|
|
|
|
# Check if Temporal is running and healthy
|
|
is_temporal_ready() {
|
|
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE exec -T temporal \
|
|
temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING"
|
|
}
|
|
|
|
# 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" $COMPOSE_OVERRIDE 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
|
|
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" $COMPOSE_OVERRIDE build --no-cache worker
|
|
fi
|
|
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE 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=<url> REPO=<name>"
|
|
exit 1
|
|
fi
|
|
|
|
# Check for API key (Bedrock, Vertex, router, and custom base URL modes can bypass this)
|
|
if [ -z "$ANTHROPIC_API_KEY" ] && [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
|
|
if [ -n "$ANTHROPIC_BASE_URL" ] && [ -n "$ANTHROPIC_AUTH_TOKEN" ]; then
|
|
# Custom base URL mode — use auth token as API key for SDK initialization
|
|
echo "Using custom base URL: $ANTHROPIC_BASE_URL"
|
|
elif [ "$CLAUDE_CODE_USE_BEDROCK" = "1" ]; then
|
|
# Bedrock mode — validate required AWS credentials
|
|
MISSING=""
|
|
[ -z "$AWS_REGION" ] && MISSING="$MISSING AWS_REGION"
|
|
[ -z "$AWS_BEARER_TOKEN_BEDROCK" ] && MISSING="$MISSING AWS_BEARER_TOKEN_BEDROCK"
|
|
[ -z "$ANTHROPIC_SMALL_MODEL" ] && MISSING="$MISSING ANTHROPIC_SMALL_MODEL"
|
|
[ -z "$ANTHROPIC_MEDIUM_MODEL" ] && MISSING="$MISSING ANTHROPIC_MEDIUM_MODEL"
|
|
[ -z "$ANTHROPIC_LARGE_MODEL" ] && MISSING="$MISSING ANTHROPIC_LARGE_MODEL"
|
|
if [ -n "$MISSING" ]; then
|
|
echo "ERROR: Bedrock mode requires the following env vars in .env:$MISSING"
|
|
exit 1
|
|
fi
|
|
elif [ "$CLAUDE_CODE_USE_VERTEX" = "1" ]; then
|
|
# Vertex AI mode — validate required GCP credentials
|
|
MISSING=""
|
|
[ -z "$CLOUD_ML_REGION" ] && MISSING="$MISSING CLOUD_ML_REGION"
|
|
[ -z "$ANTHROPIC_VERTEX_PROJECT_ID" ] && MISSING="$MISSING ANTHROPIC_VERTEX_PROJECT_ID"
|
|
[ -z "$ANTHROPIC_SMALL_MODEL" ] && MISSING="$MISSING ANTHROPIC_SMALL_MODEL"
|
|
[ -z "$ANTHROPIC_MEDIUM_MODEL" ] && MISSING="$MISSING ANTHROPIC_MEDIUM_MODEL"
|
|
[ -z "$ANTHROPIC_LARGE_MODEL" ] && MISSING="$MISSING ANTHROPIC_LARGE_MODEL"
|
|
if [ -n "$MISSING" ]; then
|
|
echo "ERROR: Vertex AI mode requires the following env vars in .env:$MISSING"
|
|
exit 1
|
|
fi
|
|
# Validate service account key file (must be inside ./credentials/ for Docker mount)
|
|
if [ -z "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
|
|
echo "ERROR: Vertex AI mode requires GOOGLE_APPLICATION_CREDENTIALS in .env"
|
|
echo " Place your service account key in ./credentials/ and set:"
|
|
echo " GOOGLE_APPLICATION_CREDENTIALS=./credentials/gcp-sa-key.json"
|
|
exit 1
|
|
fi
|
|
if [ ! -f "$GOOGLE_APPLICATION_CREDENTIALS" ]; then
|
|
echo "ERROR: Service account key file not found: $GOOGLE_APPLICATION_CREDENTIALS"
|
|
echo " Download a key from the GCP Console (IAM > Service Accounts > Keys)"
|
|
exit 1
|
|
fi
|
|
elif [ "$ROUTER" = "true" ] && { [ -n "$OPENAI_API_KEY" ] || [ -n "$OPENROUTER_API_KEY" ]; }; then
|
|
# Router mode with alternative provider - set a placeholder for SDK init
|
|
export ANTHROPIC_API_KEY="router-mode"
|
|
else
|
|
echo "ERROR: Set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN in .env"
|
|
echo " (or use CLAUDE_CODE_USE_BEDROCK=1 for AWS Bedrock,"
|
|
echo " CLAUDE_CODE_USE_VERTEX=1 for Google Vertex AI,"
|
|
echo " or ROUTER=true with OPENAI_API_KEY or OPENROUTER_API_KEY)"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
# Determine container path for REPO
|
|
# - If REPO is already a container path (/benchmarks/*, /repos/*), use as-is
|
|
# - Otherwise, treat as a folder name under ./repos/ (mounted at /repos in container)
|
|
case "$REPO" in
|
|
/benchmarks/*|/repos/*)
|
|
CONTAINER_REPO="$REPO"
|
|
;;
|
|
*)
|
|
if [ ! -d "./repos/$REPO" ]; then
|
|
echo "ERROR: Repository not found at ./repos/$REPO"
|
|
echo ""
|
|
echo "Place your target repository under the ./repos/ directory"
|
|
exit 1
|
|
fi
|
|
CONTAINER_REPO="/repos/$REPO"
|
|
;;
|
|
esac
|
|
|
|
# Handle custom OUTPUT directory
|
|
# Export OUTPUT_DIR for docker-compose volume mount BEFORE starting containers
|
|
if [ -n "$OUTPUT" ]; then
|
|
# Create output directory with write permissions for container user (UID 1001)
|
|
mkdir -p "$OUTPUT"
|
|
chmod 777 "$OUTPUT"
|
|
export OUTPUT_DIR="$OUTPUT"
|
|
fi
|
|
|
|
# Handle ROUTER flag - start claude-code-router for multi-model support
|
|
if [ "$ROUTER" = "true" ]; then
|
|
# Check if router is already running
|
|
if docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE --profile router ps router 2>/dev/null | grep -q "running"; then
|
|
echo "Router already running, skipping startup..."
|
|
else
|
|
echo "Starting claude-code-router..."
|
|
|
|
# Check for provider API keys
|
|
if [ -z "$OPENAI_API_KEY" ] && [ -z "$OPENROUTER_API_KEY" ]; then
|
|
echo "WARNING: No provider API key set (OPENAI_API_KEY or OPENROUTER_API_KEY). Router may not work."
|
|
fi
|
|
|
|
# Start router with profile
|
|
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE --profile router up -d router
|
|
|
|
# Give router a few seconds to start (health check disabled for now - TODO: debug later)
|
|
echo "Waiting for router to start..."
|
|
sleep 5
|
|
fi
|
|
|
|
# Set ANTHROPIC_BASE_URL to route through router
|
|
export ANTHROPIC_BASE_URL="http://router:3456"
|
|
# Set auth token to match router's APIKEY
|
|
export ANTHROPIC_AUTH_TOKEN="shannon-router-key"
|
|
fi
|
|
|
|
# Ensure audit-logs directory exists with write permissions for container user (UID 1001)
|
|
mkdir -p ./audit-logs ./credentials
|
|
chmod 777 ./audit-logs
|
|
|
|
# Ensure repo deliverables directory is writable by container user (UID 1001)
|
|
if [ -d "./repos/$REPO" ]; then
|
|
mkdir -p "./repos/$REPO/deliverables"
|
|
chmod 777 "./repos/$REPO/deliverables"
|
|
fi
|
|
|
|
# Ensure containers are running (starts them if needed)
|
|
ensure_containers
|
|
|
|
# Build optional args
|
|
ARGS=""
|
|
[ -n "$CONFIG" ] && ARGS="$ARGS --config $CONFIG"
|
|
|
|
# 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"
|
|
[ -n "$WORKSPACE" ] && ARGS="$ARGS --workspace $WORKSPACE"
|
|
|
|
# Run the client to submit workflow
|
|
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE 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=<workflow-id>"
|
|
exit 1
|
|
fi
|
|
|
|
# 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 "./audit-logs/${ID}/workflow.log" ]; then
|
|
WORKFLOW_LOG="./audit-logs/${ID}/workflow.log"
|
|
else
|
|
# For resume workflow IDs (e.g. workspace_resume_123), check the original workspace
|
|
WORKSPACE_ID="${ID%%_resume_*}"
|
|
if [ "$WORKSPACE_ID" != "$ID" ] && [ -f "./audit-logs/${WORKSPACE_ID}/workflow.log" ]; then
|
|
WORKFLOW_LOG="./audit-logs/${WORKSPACE_ID}/workflow.log"
|
|
fi
|
|
|
|
# For named workspace IDs (e.g. workspace_shannon-123), check the workspace name
|
|
if [ -z "$WORKFLOW_LOG" ]; then
|
|
WORKSPACE_ID="${ID%%_shannon-*}"
|
|
if [ "$WORKSPACE_ID" != "$ID" ] && [ -f "./audit-logs/${WORKSPACE_ID}/workflow.log" ]; then
|
|
WORKFLOW_LOG="./audit-logs/${WORKSPACE_ID}/workflow.log"
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$WORKFLOW_LOG" ]; then
|
|
# 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
|
|
fi
|
|
|
|
if [ -n "$WORKFLOW_LOG" ]; then
|
|
echo "Tailing workflow log: $WORKFLOW_LOG"
|
|
tail -f "$WORKFLOW_LOG"
|
|
else
|
|
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 ""
|
|
echo "Check the Temporal Web UI at http://localhost:8233 for workflow details"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
cmd_workspaces() {
|
|
# Ensure containers are running (need worker to execute node)
|
|
ensure_containers
|
|
|
|
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE exec -T worker \
|
|
node dist/temporal/workspaces.js
|
|
}
|
|
|
|
cmd_stop() {
|
|
parse_args "$@"
|
|
|
|
if [ "$CLEAN" = "true" ]; then
|
|
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE --profile router down -v
|
|
else
|
|
docker compose -f "$COMPOSE_FILE" $COMPOSE_OVERRIDE --profile router down
|
|
fi
|
|
}
|
|
|
|
# Main command dispatch
|
|
case "${1:-help}" in
|
|
start)
|
|
shift
|
|
cmd_start "$@"
|
|
;;
|
|
logs)
|
|
shift
|
|
cmd_logs "$@"
|
|
;;
|
|
workspaces)
|
|
shift
|
|
cmd_workspaces
|
|
;;
|
|
stop)
|
|
shift
|
|
cmd_stop "$@"
|
|
;;
|
|
help|--help|-h|*)
|
|
show_help
|
|
;;
|
|
esac
|