diff --git a/.dockerignore b/.dockerignore index c03a091..deaa1cc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -18,7 +18,6 @@ xben-benchmark-results/ # Development files *.md !CLAUDE.md -.env* .DS_Store Thumbs.db diff --git a/.gitignore b/.gitignore index 23d0423..b91c4cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ .shannon-store.json +.env agent-logs/ /audit-logs/ dist/ diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..57c4f78 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,83 @@ +version: '3' + +dotenv: ['.env'] + +vars: + COMPOSE_FILE: docker-compose.yml + +tasks: + default: + silent: true + cmds: [task help] + + help: + desc: Show usage information + silent: true + cmds: + - | + echo "Shannon - AI Penetration Testing Framework" + echo "" + echo "Usage:" + echo " task start URL= REPO= Start a pentest workflow" + echo " task logs View real-time worker logs" + echo " task query ID= Query workflow progress" + echo " task stop Stop all containers" + echo " task help Show this help message" + echo "" + echo "Examples:" + echo " task start URL=https://example.com REPO=/path/to/repo" + echo " task start URL=https://example.com REPO=/path/to/repo -- --pipeline-testing" + echo " task start URL=https://example.com REPO=/path/to/repo -- --config ./config.yaml" + echo " task query ID=shannon-1234567890" + echo "" + echo "Monitor workflows at http://localhost:8233" + + start: + desc: Start a pentest workflow + silent: true + requires: + vars: [URL, REPO] + cmds: + - | + 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 + - TARGET_REPO={{.REPO}} docker compose -f {{.COMPOSE_FILE}} up -d --build + - | + for i in $(seq 1 30); do + docker compose -f {{.COMPOSE_FILE}} exec -T temporal \ + temporal operator cluster health --address localhost:7233 2>/dev/null | grep -q "SERVING" && break + [ $i -eq 30 ] && echo "Timeout waiting for Temporal" && exit 1 + sleep 2 + done + - | + docker compose -f {{.COMPOSE_FILE}} exec -T worker \ + node dist/temporal/client.js "{{.URL}}" "/target-repo" {{.CLI_ARGS}} + + logs: + desc: View real-time worker logs + silent: true + cmds: + - docker compose -f {{.COMPOSE_FILE}} logs -f worker {{.CLI_ARGS}} + + query: + desc: Query workflow progress + silent: true + requires: + vars: [ID] + cmds: + - | + docker compose -f {{.COMPOSE_FILE}} exec -T worker \ + node dist/temporal/query.js "{{.ID}}" + + stop: + desc: Stop all containers + silent: true + cmds: + - | + if [ "{{.CLI_ARGS}}" = "--clean" ]; then + docker compose -f {{.COMPOSE_FILE}} down -v + else + docker compose -f {{.COMPOSE_FILE}} down + fi diff --git a/src/temporal/client.ts b/src/temporal/client.ts index 627121b..8278e19 100644 --- a/src/temporal/client.ts +++ b/src/temporal/client.ts @@ -20,7 +20,7 @@ * --output Output directory for audit logs * --pipeline-testing Use minimal prompts for fast testing * --workflow-id Custom workflow ID (default: shannon-) - * --no-wait Start workflow and exit without waiting + * --wait Wait for workflow completion with progress polling * * Environment: * TEMPORAL_ADDRESS - Temporal server address (default: localhost:7233) @@ -29,6 +29,7 @@ import { Connection, Client } from '@temporalio/client'; import dotenv from 'dotenv'; import chalk from 'chalk'; +import { displaySplashScreen } from '../splash-screen.js'; // Import types only - these don't pull in workflow runtime code import type { PipelineInput, PipelineState, PipelineProgress } from './shared.js'; @@ -51,7 +52,7 @@ function showUsage(): void { console.log( ' --workflow-id Custom workflow ID (default: shannon-)' ); - console.log(' --no-wait Start workflow and exit without waiting\n'); + console.log(' --wait Wait for workflow completion with progress polling\n'); console.log(chalk.yellow('Examples:')); console.log(' node dist/temporal/client.js https://example.com /path/to/repo'); console.log( @@ -74,7 +75,7 @@ async function startPipeline(): Promise { let outputPath: string | undefined; let pipelineTestingMode = false; let customWorkflowId: string | undefined; - let waitForCompletion = true; + let waitForCompletion = false; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -98,8 +99,8 @@ async function startPipeline(): Promise { } } else if (arg === '--pipeline-testing') { pipelineTestingMode = true; - } else if (arg === '--no-wait') { - waitForCompletion = false; + } else if (arg === '--wait') { + waitForCompletion = true; } else if (arg && !arg.startsWith('-')) { if (!webUrl) { webUrl = arg; @@ -115,8 +116,11 @@ async function startPipeline(): Promise { process.exit(1); } + // Display splash screen + await displaySplashScreen(); + const address = process.env.TEMPORAL_ADDRESS || 'localhost:7233'; - console.log(chalk.cyan(`Connecting to Temporal at ${address}...`)); + console.log(chalk.gray(`Connecting to Temporal at ${address}...`)); const connection = await Connection.connect({ address }); const client = new Client({ connection }); @@ -132,14 +136,17 @@ async function startPipeline(): Promise { ...(pipelineTestingMode && { pipelineTestingMode }), }; - console.log(chalk.green(`\nStarting workflow: ${workflowId}`)); - console.log(chalk.gray(`Target: ${webUrl}`)); - console.log(chalk.gray(`Repository: ${repoPath}`)); - console.log( - chalk.blue( - `Web UI: http://localhost:8233/namespaces/default/workflows/${workflowId}\n` - ) - ); + console.log(chalk.green.bold(`✓ Workflow started: ${workflowId}`)); + console.log(); + console.log(chalk.white(' Target: ') + chalk.cyan(webUrl)); + console.log(chalk.white(' Repository: ') + chalk.cyan(repoPath)); + if (configPath) { + console.log(chalk.white(' Config: ') + chalk.cyan(configPath)); + } + if (pipelineTestingMode) { + console.log(chalk.white(' Mode: ') + chalk.yellow('Pipeline Testing')); + } + console.log(); // Start workflow by name (not by importing the function) const handle = await client.workflow.start<(input: PipelineInput) => Promise>( @@ -152,10 +159,11 @@ async function startPipeline(): Promise { ); if (!waitForCompletion) { - console.log( - chalk.yellow('Workflow started in background. Use query tool to check progress.') - ); - console.log(chalk.gray(` npm run temporal:query -- ${workflowId}`)); + console.log(chalk.bold('Monitor progress:')); + console.log(chalk.white(' Web UI: ') + chalk.blue(`http://localhost:8233/namespaces/default/workflows/${workflowId}`)); + console.log(chalk.white(' Logs: ') + chalk.gray('task logs')); + console.log(chalk.white(' Query: ') + chalk.gray(`task query ID=${workflowId}`)); + console.log(); return; }