mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-02-12 17:22:50 +00:00
refactor: remove orchestration layer (#45)
* refactor: remove orchestration layer and simplify CLI Remove the complex orchestration layer including checkpoint management, rollback/recovery commands, and session management commands. This consolidates the execution logic directly in shannon.ts for a simpler fire-and-forget execution model. Changes: - Remove checkpoint-manager.ts and rollback functionality - Remove command-handler.ts and cli/prompts.ts - Simplify session-manager.ts to just agent definitions - Consolidate orchestration logic in shannon.ts - Update CLAUDE.md documentation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: move session lock logic to shannon.ts, simplify session-manager - Reduce session-manager.ts to only AGENTS, AGENT_ORDER, getParallelGroups() - Move Session interface and lock file functions to shannon.ts - Simplify Session to only: id, webUrl, repoPath, status, startedAt - Remove unused types/session.ts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: use crypto.randomUUID() for session ID generation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
148
CLAUDE.md
148
CLAUDE.md
@@ -30,6 +30,15 @@ shannon "https://example.com" "/path/to/repo" --output /path/to/reports
|
||||
npm start <WEB_URL> <REPO_PATH> --config <CONFIG_FILE>
|
||||
```
|
||||
|
||||
### Options
|
||||
```bash
|
||||
--config <file> YAML configuration file for authentication and testing parameters
|
||||
--output <path> Custom output directory for session folder (default: ./audit-logs/)
|
||||
--pipeline-testing Use minimal prompts for fast pipeline testing (creates minimal deliverables)
|
||||
--disable-loader Disable the animated progress loader (useful when logs interfere with spinner)
|
||||
--help Show help message
|
||||
```
|
||||
|
||||
### Configuration Validation
|
||||
```bash
|
||||
# Configuration validation is built into the main script
|
||||
@@ -43,58 +52,7 @@ TOTP generation is now handled automatically via the `generate_totp` MCP tool du
|
||||
```bash
|
||||
# No linting or testing commands available in this project
|
||||
# Development is done by running the agent in pipeline-testing mode
|
||||
shannon <commands> --pipeline-testing
|
||||
```
|
||||
|
||||
### Session Management Commands
|
||||
```bash
|
||||
# Setup session without running
|
||||
shannon --setup-only <WEB_URL> <REPO_PATH> --config <CONFIG_FILE>
|
||||
|
||||
# Check session status (shows progress, timing, costs)
|
||||
shannon --status
|
||||
|
||||
# List all available agents by phase
|
||||
shannon --list-agents
|
||||
|
||||
# Show help
|
||||
shannon --help
|
||||
```
|
||||
|
||||
### Execution Commands
|
||||
```bash
|
||||
# Run all remaining agents to completion
|
||||
shannon --run-all [--pipeline-testing]
|
||||
|
||||
# Run a specific agent
|
||||
shannon --run-agent <agent-name> [--pipeline-testing]
|
||||
|
||||
# Run a range of agents
|
||||
shannon --run-agents <start-agent>:<end-agent> [--pipeline-testing]
|
||||
|
||||
# Run a specific phase
|
||||
shannon --run-phase <phase-name> [--pipeline-testing]
|
||||
|
||||
# Pipeline testing mode (minimal prompts for fast testing)
|
||||
shannon <command> --pipeline-testing
|
||||
```
|
||||
|
||||
### Rollback & Recovery Commands
|
||||
```bash
|
||||
# Rollback to specific checkpoint
|
||||
shannon --rollback-to <agent-name>
|
||||
|
||||
# Rollback and re-execute specific agent
|
||||
shannon --rerun <agent-name> [--pipeline-testing]
|
||||
```
|
||||
|
||||
### Session Cleanup Commands
|
||||
```bash
|
||||
# Delete all sessions (with confirmation)
|
||||
shannon --cleanup
|
||||
|
||||
# Delete specific session by ID
|
||||
shannon --cleanup <session-id>
|
||||
shannon <WEB_URL> <REPO_PATH> --pipeline-testing
|
||||
```
|
||||
|
||||
## Architecture & Components
|
||||
@@ -106,22 +64,20 @@ shannon --cleanup <session-id>
|
||||
- `src/config-parser.ts` - Handles YAML configuration parsing, validation, and distribution to agents
|
||||
- `src/error-handling.ts` - Comprehensive error handling with retry logic and categorized error types
|
||||
- `src/tool-checker.ts` - Validates availability of external security tools before execution
|
||||
- `src/session-manager.ts` - Manages persistent session state and agent lifecycle
|
||||
- `src/checkpoint-manager.ts` - Git-based checkpointing system for rollback capabilities
|
||||
- Pipeline orchestration is built into the main `src/shannon.ts` script
|
||||
- `src/session-manager.ts` - Agent definitions, execution order, and parallel groups
|
||||
- `src/queue-validation.ts` - Validates deliverables and agent prerequisites
|
||||
|
||||
### Five-Phase Testing Workflow
|
||||
|
||||
1. **Pre-Reconnaissance** (`pre-recon`) - External tool scans (nmap, subfinder, whatweb) + source code analysis
|
||||
2. **Reconnaissance** (`recon`) - Analysis of initial findings and attack surface mapping
|
||||
3. **Vulnerability Analysis** (5 agents)
|
||||
3. **Vulnerability Analysis** (5 agents run in parallel)
|
||||
- `injection-vuln` - SQL injection, command injection
|
||||
- `xss-vuln` - Cross-site scripting
|
||||
- `auth-vuln` - Authentication bypasses
|
||||
- `authz-vuln` - Authorization flaws
|
||||
- `ssrf-vuln` - Server-side request forgery
|
||||
4. **Exploitation** (5 agents)
|
||||
4. **Exploitation** (5 agents run in parallel, only if vulnerabilities found)
|
||||
- `injection-exploit` - Exploit injection vulnerabilities
|
||||
- `xss-exploit` - Exploit XSS vulnerabilities
|
||||
- `auth-exploit` - Exploit authentication issues
|
||||
@@ -182,45 +138,25 @@ The agent integrates with external security tools:
|
||||
|
||||
Tools are validated for availability before execution using the tool-checker module.
|
||||
|
||||
### Git-Based Checkpointing System
|
||||
The agent implements a sophisticated checkpoint system using git:
|
||||
- Every agent creates a git checkpoint before execution
|
||||
- Rollback to any previous agent state using `--rollback-to` or `--rerun`
|
||||
- Failed agents don't affect completed work
|
||||
- Rolled-back agents marked in audit system with status: "rolled-back"
|
||||
- Reconciliation automatically syncs Shannon store with audit logs after rollback
|
||||
- Fail-fast safety prevents accidental re-execution of completed agents
|
||||
|
||||
### Unified Audit & Metrics System
|
||||
The agent implements a crash-safe, self-healing audit system (v3.0) with the following guarantees:
|
||||
### Audit & Metrics System
|
||||
The agent implements a crash-safe audit system with the following features:
|
||||
|
||||
**Architecture:**
|
||||
- **audit-logs/** (or custom `--output` path): Centralized metrics and forensic logs (source of truth)
|
||||
- **audit-logs/** (or custom `--output` path): Centralized metrics and forensic logs
|
||||
- `{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)
|
||||
- **.shannon-store.json**: Minimal session lock file (prevents concurrent runs)
|
||||
|
||||
**Crash Safety:**
|
||||
- Append-only logging with immediate flush (survives kill -9)
|
||||
- Atomic writes for session.json (no partial writes)
|
||||
- Event-based logging (tool_start, tool_end, llm_response) closes data loss windows
|
||||
|
||||
**Self-Healing:**
|
||||
- Automatic reconciliation before every CLI command
|
||||
- Recovers from crashes during rollback
|
||||
- Audit logs are source of truth; Shannon store follows
|
||||
|
||||
**Forensic Completeness:**
|
||||
- All retry attempts logged with errors, costs, durations
|
||||
- Rolled-back agents preserved with status: "rolled-back"
|
||||
- Partial cost capture for failed attempts
|
||||
- Complete event trail for debugging
|
||||
- Event-based logging (tool_start, tool_end, llm_response)
|
||||
|
||||
**Concurrency Safety:**
|
||||
- SessionMutex prevents race conditions during parallel agent execution
|
||||
- Safe parallel execution of vulnerability and exploitation phases
|
||||
- 5x faster execution with parallel vulnerability and exploitation phases
|
||||
|
||||
**Metrics & Reporting:**
|
||||
- Export metrics to CSV with `./scripts/export-metrics.js`
|
||||
@@ -238,16 +174,20 @@ For detailed design, see `docs/unified-audit-system-design.md`.
|
||||
- **SDK-First Approach**: Heavy reliance on Claude Agent SDK for autonomous AI operations
|
||||
- **Progressive Analysis**: Each phase builds on previous phase results
|
||||
- **Local Repository Setup**: Target applications are accessed directly from user-provided local directories
|
||||
- **Fire-and-Forget Execution**: Single entry point, runs all phases to completion
|
||||
|
||||
### Error Handling Strategy
|
||||
The application uses a comprehensive error handling system with:
|
||||
- Categorized error types (PentestError, ConfigError, NetworkError, etc.)
|
||||
- Automatic retry logic for transient failures
|
||||
- Automatic retry logic for transient failures (3 attempts per agent)
|
||||
- Graceful degradation when external tools are unavailable
|
||||
- Detailed error logging and user-friendly error messages
|
||||
|
||||
### Testing Mode
|
||||
The agent includes a testing mode that skips external tool execution for faster development cycles.
|
||||
The agent includes a testing mode that skips external tool execution for faster development cycles:
|
||||
```bash
|
||||
shannon <WEB_URL> <REPO_PATH> --pipeline-testing
|
||||
```
|
||||
|
||||
### Security Focus
|
||||
This is explicitly designed as a **defensive security tool** for:
|
||||
@@ -263,32 +203,48 @@ The tool should only be used on systems you own or have explicit permission to t
|
||||
```
|
||||
src/ # TypeScript source files
|
||||
├── shannon.ts # Main orchestration script (entry point)
|
||||
├── constants.ts # Shared constants
|
||||
├── config-parser.ts # Configuration handling
|
||||
├── error-handling.ts # Error management
|
||||
├── tool-checker.ts # Tool validation
|
||||
├── session-manager.ts # Agent definitions, order, and parallel groups
|
||||
├── queue-validation.ts # Deliverable validation
|
||||
├── splash-screen.ts # ASCII art splash screen
|
||||
├── progress-indicator.ts # Progress display utilities
|
||||
├── types/ # TypeScript type definitions
|
||||
│ ├── index.ts # Barrel exports
|
||||
│ ├── agents.ts # Agent type definitions
|
||||
│ ├── config.ts # Configuration interfaces
|
||||
│ ├── errors.ts # Error type definitions
|
||||
│ └── session.ts # Session type definitions
|
||||
├── audit/ # Unified audit system (v3.0)
|
||||
├── audit/ # Audit system
|
||||
│ ├── index.ts # Public API
|
||||
│ ├── audit-session.ts # Main facade (logger + metrics + mutex)
|
||||
│ ├── logger.ts # Append-only crash-safe logging
|
||||
│ ├── metrics-tracker.ts # Timing, cost, attempt tracking
|
||||
│ └── utils.ts # Path generation, atomic writes
|
||||
├── config-parser.ts # Configuration handling
|
||||
├── error-handling.ts # Error management
|
||||
├── tool-checker.ts # Tool validation
|
||||
├── session-manager.ts # Session state + reconciliation
|
||||
├── checkpoint-manager.ts # Git-based checkpointing + rollback
|
||||
├── queue-validation.ts # Deliverable validation
|
||||
├── ai/
|
||||
│ └── claude-executor.ts # Claude Agent SDK integration
|
||||
├── phases/
|
||||
│ ├── pre-recon.ts # Pre-reconnaissance phase
|
||||
│ └── reporting.ts # Final report assembly
|
||||
├── prompts/
|
||||
│ └── prompt-manager.ts # Prompt loading and variable substitution
|
||||
├── setup/
|
||||
│ └── environment.ts # Local repository setup
|
||||
├── cli/
|
||||
│ ├── ui.ts # Help text display
|
||||
│ └── input-validator.ts # URL and path validation
|
||||
└── utils/
|
||||
├── git-manager.ts # Git operations
|
||||
├── metrics.ts # Timing utilities
|
||||
├── output-formatter.ts # Output formatting utilities
|
||||
└── concurrency.ts # SessionMutex for parallel execution
|
||||
dist/ # Compiled JavaScript output
|
||||
├── shannon.js # Compiled entry point
|
||||
└── ... # Other compiled files
|
||||
package.json # Node.js dependencies
|
||||
.shannon-store.json # Orchestration state (minimal)
|
||||
.shannon-store.json # Session lock file
|
||||
audit-logs/ # Centralized audit data (default, or use --output)
|
||||
└── {hostname}_{sessionId}/
|
||||
├── session.json # Comprehensive metrics
|
||||
@@ -329,11 +285,9 @@ docs/ # Documentation
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
- **"Agent already completed"**: Use `--rerun <agent>` for explicit re-execution
|
||||
- **"Missing prerequisites"**: Check `--status` and run prerequisite agents first
|
||||
- **"No sessions found"**: Create a session with `--setup-only` first
|
||||
- **"A session is already running"**: Wait for the current session to complete, or delete `.shannon-store.json`
|
||||
- **"Repository not found"**: Ensure target local directory exists and is accessible
|
||||
- **"Too many test sessions"**: Use `--cleanup` to remove old sessions and free disk space
|
||||
- **Concurrent runs blocked**: Only one session can run at a time per target
|
||||
|
||||
### External Tool Dependencies
|
||||
Missing tools can be skipped using `--pipeline-testing` mode during development:
|
||||
|
||||
@@ -286,14 +286,6 @@ rules:
|
||||
|
||||
If your application uses two-factor authentication, simply add the TOTP secret to your config file. The AI will automatically generate the required codes during testing.
|
||||
|
||||
### Check Status
|
||||
|
||||
View progress of previous runs:
|
||||
|
||||
```bash
|
||||
docker run --rm shannon:latest --status
|
||||
```
|
||||
|
||||
### Output and Results
|
||||
|
||||
All results are saved to `./audit-logs/` by default. Use `--output <path>` 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"`).
|
||||
|
||||
@@ -155,21 +155,6 @@ export class AuditSession {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark multiple agents as rolled back
|
||||
*/
|
||||
async markMultipleRolledBack(agentNames: string[]): Promise<void> {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const unlock = await sessionMutex.lock(this.sessionId);
|
||||
try {
|
||||
await this.metricsTracker.reload();
|
||||
await this.metricsTracker.markMultipleRolledBack(agentNames);
|
||||
} finally {
|
||||
unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session status
|
||||
*/
|
||||
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
calculatePercentage,
|
||||
type SessionMetadata,
|
||||
} from './utils.js';
|
||||
import type { AgentName, PhaseName } from '../types/index.js';
|
||||
|
||||
interface AttemptData {
|
||||
attempt_number: number;
|
||||
@@ -32,12 +31,11 @@ interface AttemptData {
|
||||
}
|
||||
|
||||
interface AgentMetrics {
|
||||
status: 'in-progress' | 'success' | 'failed' | 'rolled-back';
|
||||
status: 'in-progress' | 'success' | 'failed';
|
||||
attempts: AttemptData[];
|
||||
final_duration_ms: number;
|
||||
total_cost_usd: number;
|
||||
checkpoint?: string;
|
||||
rolled_back_at?: string;
|
||||
}
|
||||
|
||||
interface PhaseMetrics {
|
||||
@@ -208,42 +206,6 @@ export class MetricsTracker {
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark agent as rolled back
|
||||
*/
|
||||
async markRolledBack(agentName: string): Promise<void> {
|
||||
if (!this.data || !this.data.metrics.agents[agentName]) {
|
||||
return; // Agent not tracked
|
||||
}
|
||||
|
||||
const agent = this.data.metrics.agents[agentName]!;
|
||||
agent.status = 'rolled-back';
|
||||
agent.rolled_back_at = formatTimestamp();
|
||||
|
||||
// Recalculate aggregations (exclude rolled-back agents)
|
||||
this.recalculateAggregations();
|
||||
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark multiple agents as rolled back
|
||||
*/
|
||||
async markMultipleRolledBack(agentNames: string[]): Promise<void> {
|
||||
if (!this.data) return;
|
||||
|
||||
for (const agentName of agentNames) {
|
||||
if (this.data.metrics.agents[agentName]) {
|
||||
const agent = this.data.metrics.agents[agentName]!;
|
||||
agent.status = 'rolled-back';
|
||||
agent.rolled_back_at = formatTimestamp();
|
||||
}
|
||||
}
|
||||
|
||||
this.recalculateAggregations();
|
||||
await this.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update session status
|
||||
*/
|
||||
@@ -267,7 +229,7 @@ export class MetricsTracker {
|
||||
|
||||
const agents = this.data.metrics.agents;
|
||||
|
||||
// Only count successful agents (not rolled-back or failed)
|
||||
// Only count successful agents
|
||||
const successfulAgents = Object.entries(agents).filter(
|
||||
([, data]) => data.status === 'success'
|
||||
);
|
||||
|
||||
@@ -1,936 +0,0 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import { fs, path } from 'zx';
|
||||
import chalk, { type ChalkInstance } from 'chalk';
|
||||
import { PentestError } from './error-handling.js';
|
||||
import { parseConfig, distributeConfig } from './config-parser.js';
|
||||
import { executeGitCommandWithRetry } from './utils/git-manager.js';
|
||||
import { formatDuration } from './audit/utils.js';
|
||||
import {
|
||||
AGENTS,
|
||||
PHASES,
|
||||
validateAgent,
|
||||
validateAgentRange,
|
||||
validatePhase,
|
||||
checkPrerequisites,
|
||||
getNextAgent,
|
||||
markAgentCompleted,
|
||||
markAgentFailed,
|
||||
getSessionStatus,
|
||||
rollbackToAgent,
|
||||
getSession
|
||||
} from './session-manager.js';
|
||||
import type { Session, AgentDefinition } from './session-manager.js';
|
||||
import type { AgentName, PhaseName, PromptName } from './types/index.js';
|
||||
import type { DistributedConfig } from './types/config.js';
|
||||
import type { SessionMetadata } from './audit/utils.js';
|
||||
|
||||
// Types for callback functions
|
||||
type RunClaudePromptWithRetry = (
|
||||
prompt: string,
|
||||
sourceDir: string,
|
||||
allowedTools: string,
|
||||
context: string,
|
||||
description: string,
|
||||
agentName: string | null,
|
||||
colorFn: ChalkInstance,
|
||||
sessionMetadata: SessionMetadata | null
|
||||
) => Promise<AgentResult>;
|
||||
|
||||
type LoadPrompt = (
|
||||
promptName: string,
|
||||
variables: { webUrl: string; repoPath: string; sourceDir?: string },
|
||||
config: DistributedConfig | null,
|
||||
pipelineTestingMode: boolean
|
||||
) => Promise<string>;
|
||||
|
||||
export interface AgentResult {
|
||||
success: boolean;
|
||||
duration: number;
|
||||
cost?: number;
|
||||
partialCost?: number;
|
||||
error?: string;
|
||||
retryable?: boolean;
|
||||
logFile?: string;
|
||||
}
|
||||
|
||||
interface ValidationData {
|
||||
shouldExploit: boolean;
|
||||
vulnerabilityCount: number;
|
||||
}
|
||||
|
||||
interface SingleAgentResult {
|
||||
success: boolean;
|
||||
agentName: string;
|
||||
result?: AgentResult;
|
||||
validation?: ValidationData | null;
|
||||
timing?: number | null;
|
||||
cost?: number | null;
|
||||
checkpoint?: string;
|
||||
completedAt?: string;
|
||||
attempts?: number;
|
||||
logFile?: string;
|
||||
error?: {
|
||||
message: string;
|
||||
type: string;
|
||||
retryable: boolean;
|
||||
originalError?: Error;
|
||||
};
|
||||
failedAt?: string;
|
||||
context?: {
|
||||
targetRepo: string;
|
||||
promptName: PromptName;
|
||||
sessionId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ParallelResult {
|
||||
completed: AgentName[];
|
||||
failed: Array<{ agent: AgentName; error: string }>;
|
||||
}
|
||||
|
||||
// Check if target repository exists and is accessible
|
||||
const validateTargetRepo = async (targetRepo: string): Promise<boolean> => {
|
||||
if (!targetRepo || !await fs.pathExists(targetRepo)) {
|
||||
throw new PentestError(
|
||||
`Target repository '${targetRepo}' not found or not accessible`,
|
||||
'filesystem',
|
||||
false,
|
||||
{ targetRepo }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if it's a git repository
|
||||
const gitDir = path.join(targetRepo, '.git');
|
||||
if (!await fs.pathExists(gitDir)) {
|
||||
throw new PentestError(
|
||||
`Target repository '${targetRepo}' is not a git repository`,
|
||||
'validation',
|
||||
false,
|
||||
{ targetRepo }
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Get git commit hash for checkpoint
|
||||
export const getGitCommitHash = async (targetRepo: string): Promise<string> => {
|
||||
try {
|
||||
const result = await executeGitCommandWithRetry(['git', 'rev-parse', 'HEAD'], targetRepo, 'getting commit hash');
|
||||
return result.stdout.trim();
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
throw new PentestError(
|
||||
`Failed to get git commit hash: ${errMsg}`,
|
||||
'validation',
|
||||
false,
|
||||
{ targetRepo, originalError: errMsg }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Rollback git workspace to specific commit
|
||||
const rollbackGitToCommit = async (targetRepo: string, commitHash: string): Promise<void> => {
|
||||
try {
|
||||
await executeGitCommandWithRetry(['git', 'reset', '--hard', commitHash], targetRepo, 'rollback to commit');
|
||||
await executeGitCommandWithRetry(['git', 'clean', '-fd'], targetRepo, 'cleaning after rollback');
|
||||
console.log(chalk.green(`✅ Git workspace rolled back to commit ${commitHash.substring(0, 8)}`));
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
throw new PentestError(
|
||||
`Failed to rollback git workspace: ${errMsg}`,
|
||||
'validation',
|
||||
false,
|
||||
{ targetRepo, commitHash, originalError: errMsg }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to get prompt name from agent name
|
||||
const getPromptName = (agentName: AgentName): PromptName => {
|
||||
const mappings: Record<AgentName, PromptName> = {
|
||||
'pre-recon': 'pre-recon-code',
|
||||
'recon': 'recon',
|
||||
'injection-vuln': 'vuln-injection',
|
||||
'xss-vuln': 'vuln-xss',
|
||||
'auth-vuln': 'vuln-auth',
|
||||
'ssrf-vuln': 'vuln-ssrf',
|
||||
'authz-vuln': 'vuln-authz',
|
||||
'injection-exploit': 'exploit-injection',
|
||||
'xss-exploit': 'exploit-xss',
|
||||
'auth-exploit': 'exploit-auth',
|
||||
'ssrf-exploit': 'exploit-ssrf',
|
||||
'authz-exploit': 'exploit-authz',
|
||||
'report': 'report-executive'
|
||||
};
|
||||
|
||||
return mappings[agentName] || agentName as PromptName;
|
||||
};
|
||||
|
||||
// Get color function for agent
|
||||
const getAgentColor = (agentName: AgentName): ChalkInstance => {
|
||||
const colorMap: Partial<Record<AgentName, ChalkInstance>> = {
|
||||
'injection-vuln': chalk.red,
|
||||
'injection-exploit': chalk.red,
|
||||
'xss-vuln': chalk.yellow,
|
||||
'xss-exploit': chalk.yellow,
|
||||
'auth-vuln': chalk.blue,
|
||||
'auth-exploit': chalk.blue,
|
||||
'ssrf-vuln': chalk.magenta,
|
||||
'ssrf-exploit': chalk.magenta,
|
||||
'authz-vuln': chalk.green,
|
||||
'authz-exploit': chalk.green
|
||||
};
|
||||
return colorMap[agentName] || chalk.cyan;
|
||||
};
|
||||
|
||||
// Run a single agent with retry logic and checkpointing
|
||||
const runSingleAgent = async (
|
||||
agentName: AgentName,
|
||||
session: Session,
|
||||
pipelineTestingMode: boolean,
|
||||
runClaudePromptWithRetry: RunClaudePromptWithRetry,
|
||||
loadPrompt: LoadPrompt,
|
||||
allowRerun: boolean = false,
|
||||
skipWorkspaceClean: boolean = false
|
||||
): Promise<SingleAgentResult> => {
|
||||
// Validate agent first
|
||||
const agent = validateAgent(agentName);
|
||||
|
||||
console.log(chalk.cyan(`\n🤖 Running agent: ${agent.displayName}`));
|
||||
|
||||
// Reload session to get latest state (important for agent ranges)
|
||||
const freshSession = await getSession(session.id);
|
||||
if (!freshSession) {
|
||||
throw new PentestError(`Session ${session.id} not found`, 'validation', false);
|
||||
}
|
||||
|
||||
// Use fresh session for all subsequent checks
|
||||
const currentSession = freshSession;
|
||||
|
||||
// Warn if session is completed
|
||||
if (currentSession.status === 'completed') {
|
||||
console.log(chalk.yellow('⚠️ This session is already completed. Re-running will modify completed results.'));
|
||||
}
|
||||
|
||||
// Block re-running completed agents unless explicitly allowed
|
||||
if (!allowRerun && currentSession.completedAgents.includes(agentName)) {
|
||||
throw new PentestError(
|
||||
`Agent '${agentName}' has already been completed. Use --rerun ${agentName} for explicit rollback and re-execution.`,
|
||||
'validation',
|
||||
false,
|
||||
{
|
||||
agentName,
|
||||
suggestion: `--rerun ${agentName}`,
|
||||
completedAgents: currentSession.completedAgents
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const targetRepo = currentSession.targetRepo;
|
||||
await validateTargetRepo(targetRepo);
|
||||
|
||||
// Check prerequisites
|
||||
checkPrerequisites(currentSession, agentName);
|
||||
|
||||
// Clean workspace if needed
|
||||
if (!currentSession.completedAgents.includes(agentName) && !allowRerun && !skipWorkspaceClean) {
|
||||
try {
|
||||
const status = await executeGitCommandWithRetry(['git', 'status', '--porcelain'], targetRepo, 'checking workspace status');
|
||||
const hasUncommittedChanges = status.stdout.trim().length > 0;
|
||||
|
||||
if (hasUncommittedChanges) {
|
||||
console.log(chalk.yellow(` ⚠️ Detected uncommitted changes before running ${agentName}`));
|
||||
console.log(chalk.yellow(` 🧹 Cleaning workspace to ensure clean agent execution`));
|
||||
await executeGitCommandWithRetry(['git', 'reset', '--hard', 'HEAD'], targetRepo, 'cleaning workspace');
|
||||
await executeGitCommandWithRetry(['git', 'clean', '-fd'], targetRepo, 'removing untracked files');
|
||||
console.log(chalk.green(` ✅ Workspace cleaned successfully`));
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
console.log(chalk.yellow(` ⚠️ Could not check/clean workspace: ${errMsg}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Create variables for prompt
|
||||
const variables = {
|
||||
webUrl: currentSession.webUrl,
|
||||
repoPath: currentSession.repoPath,
|
||||
sourceDir: targetRepo
|
||||
};
|
||||
|
||||
// Handle relative config paths
|
||||
let configPath: string | null = null;
|
||||
if (currentSession.configFile) {
|
||||
configPath = path.isAbsolute(currentSession.configFile) || currentSession.configFile.startsWith('configs/')
|
||||
? currentSession.configFile
|
||||
: path.join('configs', currentSession.configFile);
|
||||
}
|
||||
|
||||
const config = configPath ? await parseConfig(configPath) : null;
|
||||
const distributedConfig = config ? distributeConfig(config) : null;
|
||||
|
||||
// Initialize variables for result
|
||||
let validationData: ValidationData | null = null;
|
||||
let timingData: number | null = null;
|
||||
let costData: number | null = null;
|
||||
|
||||
try {
|
||||
// Load and run the appropriate prompt
|
||||
const promptName = getPromptName(agentName);
|
||||
const prompt = await loadPrompt(promptName, variables, distributedConfig, pipelineTestingMode);
|
||||
|
||||
const result = await runClaudePromptWithRetry(
|
||||
prompt,
|
||||
targetRepo,
|
||||
'*',
|
||||
'',
|
||||
AGENTS[agentName]!.displayName,
|
||||
agentName,
|
||||
getAgentColor(agentName),
|
||||
{ id: currentSession.id, webUrl: currentSession.webUrl, repoPath: currentSession.repoPath, ...(currentSession.outputPath && { outputPath: currentSession.outputPath }) }
|
||||
);
|
||||
|
||||
if (!result.success) {
|
||||
throw new PentestError(
|
||||
`Agent execution failed: ${result.error}`,
|
||||
'validation',
|
||||
result.retryable || false,
|
||||
{ agentName, result }
|
||||
);
|
||||
}
|
||||
|
||||
// Get commit hash for checkpoint
|
||||
const commitHash = await getGitCommitHash(targetRepo);
|
||||
|
||||
// Extract timing and cost data
|
||||
timingData = result.duration;
|
||||
costData = result.cost || 0;
|
||||
|
||||
if (agentName.includes('-vuln')) {
|
||||
// Validate vulnerability analysis results
|
||||
const vulnType = agentName.replace('-vuln', '');
|
||||
try {
|
||||
const { safeValidateQueueAndDeliverable } = await import('./queue-validation.js');
|
||||
const validation = await safeValidateQueueAndDeliverable(vulnType as 'injection' | 'xss' | 'auth' | 'ssrf' | 'authz', targetRepo);
|
||||
|
||||
if (validation.success && validation.data) {
|
||||
console.log(chalk.blue(`📋 Validation: ${validation.data.shouldExploit ? `Ready for exploitation (${validation.data.vulnerabilityCount} vulnerabilities)` : 'No vulnerabilities found'}`));
|
||||
validationData = {
|
||||
shouldExploit: validation.data.shouldExploit,
|
||||
vulnerabilityCount: validation.data.vulnerabilityCount
|
||||
};
|
||||
} else if (validation.error) {
|
||||
console.log(chalk.yellow(`⚠️ Validation failed: ${validation.error.message}`));
|
||||
}
|
||||
} catch (validationError) {
|
||||
const errMsg = validationError instanceof Error ? validationError.message : String(validationError);
|
||||
console.log(chalk.yellow(`⚠️ Could not validate ${vulnType}: ${errMsg}`));
|
||||
}
|
||||
}
|
||||
|
||||
// Mark agent as completed
|
||||
await markAgentCompleted(currentSession.id, agentName, commitHash);
|
||||
|
||||
// Only show completion message for sequential execution
|
||||
if (!skipWorkspaceClean) {
|
||||
console.log(chalk.green(`✅ Agent '${agentName}' completed successfully`));
|
||||
}
|
||||
|
||||
// Return immutable result object
|
||||
return Object.freeze({
|
||||
success: true,
|
||||
agentName,
|
||||
result,
|
||||
validation: validationData,
|
||||
timing: timingData,
|
||||
cost: costData,
|
||||
checkpoint: commitHash,
|
||||
completedAt: new Date().toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
// Mark agent as failed
|
||||
await markAgentFailed(currentSession.id, agentName);
|
||||
|
||||
const err = error as Error & { retryable?: boolean };
|
||||
|
||||
// Only show failure message for sequential execution
|
||||
if (!skipWorkspaceClean) {
|
||||
console.log(chalk.red(`❌ Agent '${agentName}' failed: ${err.message}`));
|
||||
}
|
||||
|
||||
// Return immutable error object
|
||||
const errorResult: SingleAgentResult = Object.freeze({
|
||||
success: false,
|
||||
agentName,
|
||||
error: {
|
||||
message: err.message,
|
||||
type: err.constructor.name,
|
||||
retryable: err.retryable || false,
|
||||
originalError: err
|
||||
},
|
||||
validation: validationData,
|
||||
timing: timingData,
|
||||
failedAt: new Date().toISOString(),
|
||||
context: {
|
||||
targetRepo,
|
||||
promptName: getPromptName(agentName),
|
||||
sessionId: currentSession.id
|
||||
}
|
||||
});
|
||||
|
||||
// Throw enhanced error
|
||||
const enhancedError = new PentestError(
|
||||
`Agent '${agentName}' execution failed: ${err.message}`,
|
||||
'validation',
|
||||
err.retryable || false,
|
||||
{
|
||||
agentName,
|
||||
sessionId: currentSession.id,
|
||||
originalError: err.message,
|
||||
errorResult
|
||||
}
|
||||
);
|
||||
|
||||
throw enhancedError;
|
||||
}
|
||||
};
|
||||
|
||||
// Run vulnerability agents in parallel
|
||||
const runParallelVuln = async (
|
||||
session: Session,
|
||||
pipelineTestingMode: boolean,
|
||||
runClaudePromptWithRetry: RunClaudePromptWithRetry,
|
||||
loadPrompt: LoadPrompt
|
||||
): Promise<ParallelResult> => {
|
||||
const vulnAgents: AgentName[] = ['injection-vuln', 'xss-vuln', 'auth-vuln', 'ssrf-vuln', 'authz-vuln'];
|
||||
const activeAgents = vulnAgents.filter(agent => !session.completedAgents.includes(agent));
|
||||
|
||||
if (activeAgents.length === 0) {
|
||||
console.log(chalk.gray('⏭️ All vulnerability agents already completed'));
|
||||
return { completed: vulnAgents, failed: [] };
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\n🚀 Starting ${activeAgents.length} vulnerability analysis specialists in parallel...`));
|
||||
console.log(chalk.gray(' Specialists: ' + activeAgents.join(', ')));
|
||||
console.log();
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Collect all results without logging individual completions
|
||||
const results = await Promise.allSettled(
|
||||
activeAgents.map(async (agentName, index) => {
|
||||
// Add 2-second stagger to prevent API overwhelm
|
||||
await new Promise(resolve => setTimeout(resolve, index * 2000));
|
||||
|
||||
let lastError: Error | undefined;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
try {
|
||||
const result = await runSingleAgent(agentName, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt, false, true);
|
||||
return { ...result, attempts };
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempts < maxAttempts) {
|
||||
console.log(chalk.yellow(`⚠️ ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`));
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw { agentName, error: lastError, attempts };
|
||||
})
|
||||
);
|
||||
|
||||
const totalDuration = Date.now() - startTime;
|
||||
|
||||
// Process and display results
|
||||
console.log(chalk.cyan('\n📊 Vulnerability Analysis Results'));
|
||||
console.log(chalk.gray('─'.repeat(80)));
|
||||
console.log(chalk.bold('Agent Status Vulns Attempt Duration Cost'));
|
||||
console.log(chalk.gray('─'.repeat(80)));
|
||||
|
||||
const completed: AgentName[] = [];
|
||||
const failed: Array<{ agent: AgentName; error: string }> = [];
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const agentName = activeAgents[index]!;
|
||||
const agentDisplay = agentName.padEnd(22);
|
||||
|
||||
if (result.status === 'fulfilled') {
|
||||
const data = result.value;
|
||||
completed.push(agentName);
|
||||
|
||||
const vulnCount = data.validation?.vulnerabilityCount || 0;
|
||||
const duration = formatDuration(data.timing || 0);
|
||||
const cost = `$${(data.cost || 0).toFixed(4)}`;
|
||||
|
||||
console.log(
|
||||
`${chalk.green(agentDisplay)} ${chalk.green('✓ Success')} ${vulnCount.toString().padStart(5)} ` +
|
||||
`${data.attempts}/3 ${duration.padEnd(11)} ${cost}`
|
||||
);
|
||||
|
||||
if (data.logFile) {
|
||||
const relativePath = path.relative(process.cwd(), data.logFile);
|
||||
console.log(chalk.gray(` └─ Detailed log: ${relativePath}`));
|
||||
}
|
||||
} else {
|
||||
const reason = result.reason as { error?: Error; attempts?: number };
|
||||
const error = reason.error || result.reason;
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
failed.push({ agent: agentName, error: errMsg });
|
||||
|
||||
const attempts = reason.attempts || 3;
|
||||
|
||||
console.log(
|
||||
`${chalk.red(agentDisplay)} ${chalk.red('✗ Failed ')} - ` +
|
||||
`${attempts}/3 - -`
|
||||
);
|
||||
console.log(chalk.gray(` └─ ${errMsg.substring(0, 60)}...`));
|
||||
}
|
||||
});
|
||||
|
||||
console.log(chalk.gray('─'.repeat(80)));
|
||||
console.log(chalk.cyan(`Summary: ${completed.length}/${activeAgents.length} succeeded in ${formatDuration(totalDuration)}`));
|
||||
|
||||
return { completed, failed };
|
||||
};
|
||||
|
||||
// Run exploitation agents in parallel
|
||||
const runParallelExploit = async (
|
||||
session: Session,
|
||||
pipelineTestingMode: boolean,
|
||||
runClaudePromptWithRetry: RunClaudePromptWithRetry,
|
||||
loadPrompt: LoadPrompt
|
||||
): Promise<ParallelResult> => {
|
||||
const exploitAgents: AgentName[] = ['injection-exploit', 'xss-exploit', 'auth-exploit', 'ssrf-exploit', 'authz-exploit'];
|
||||
|
||||
// Get fresh session data
|
||||
const freshSession = await getSession(session.id);
|
||||
if (!freshSession) {
|
||||
throw new PentestError(`Session ${session.id} not found`, 'validation', false);
|
||||
}
|
||||
|
||||
// Load validation module
|
||||
const { safeValidateQueueAndDeliverable } = await import('./queue-validation.js');
|
||||
|
||||
// Check eligibility
|
||||
const eligibilityChecks = await Promise.all(
|
||||
exploitAgents.map(async (agentName) => {
|
||||
const vulnAgentName = agentName.replace('-exploit', '-vuln') as AgentName;
|
||||
|
||||
if (!freshSession.completedAgents.includes(vulnAgentName)) {
|
||||
return { agentName, eligible: false };
|
||||
}
|
||||
|
||||
const vulnType = vulnAgentName.replace('-vuln', '') as 'injection' | 'xss' | 'auth' | 'ssrf' | 'authz';
|
||||
const validation = await safeValidateQueueAndDeliverable(vulnType, freshSession.targetRepo);
|
||||
|
||||
if (!validation.success || !validation.data?.shouldExploit) {
|
||||
console.log(chalk.gray(`⏭️ Skipping ${agentName} (no vulnerabilities found in ${vulnAgentName})`));
|
||||
return { agentName, eligible: false };
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`✓ ${agentName} eligible (${validation.data.vulnerabilityCount} vulnerabilities from ${vulnAgentName})`));
|
||||
return { agentName, eligible: true };
|
||||
})
|
||||
);
|
||||
|
||||
const eligibleAgents = eligibilityChecks
|
||||
.filter(check => check.eligible)
|
||||
.map(check => check.agentName);
|
||||
|
||||
const activeAgents = eligibleAgents.filter(agent => !freshSession.completedAgents.includes(agent));
|
||||
|
||||
if (activeAgents.length === 0) {
|
||||
if (eligibleAgents.length === 0) {
|
||||
console.log(chalk.gray('⏭️ No exploitation agents eligible (no vulnerabilities found)'));
|
||||
} else {
|
||||
console.log(chalk.gray('⏭️ All eligible exploitation agents already completed'));
|
||||
}
|
||||
return { completed: eligibleAgents, failed: [] };
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\n🎯 Starting ${activeAgents.length} exploitation specialists in parallel...`));
|
||||
console.log(chalk.gray(' Specialists: ' + activeAgents.join(', ')));
|
||||
console.log();
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
activeAgents.map(async (agentName, index) => {
|
||||
await new Promise(resolve => setTimeout(resolve, index * 2000));
|
||||
|
||||
let lastError: Error | undefined;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
try {
|
||||
const result = await runSingleAgent(agentName, freshSession, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt, false, true);
|
||||
return { ...result, attempts };
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempts < maxAttempts) {
|
||||
console.log(chalk.yellow(`⚠️ ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`));
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
throw { agentName, error: lastError, attempts };
|
||||
})
|
||||
);
|
||||
|
||||
const totalDuration = Date.now() - startTime;
|
||||
|
||||
console.log(chalk.cyan('\n🎯 Exploitation Results'));
|
||||
console.log(chalk.gray('─'.repeat(80)));
|
||||
console.log(chalk.bold('Agent Status Result Attempt Duration Cost'));
|
||||
console.log(chalk.gray('─'.repeat(80)));
|
||||
|
||||
const completed: AgentName[] = [];
|
||||
const failed: Array<{ agent: AgentName; error: string }> = [];
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const agentName = activeAgents[index]!;
|
||||
const agentDisplay = agentName.padEnd(22);
|
||||
|
||||
if (result.status === 'fulfilled') {
|
||||
const data = result.value;
|
||||
completed.push(agentName);
|
||||
|
||||
const exploitResult = 'Success';
|
||||
const duration = formatDuration(data.timing || 0);
|
||||
const cost = `$${(data.cost || 0).toFixed(4)}`;
|
||||
|
||||
console.log(
|
||||
`${chalk.green(agentDisplay)} ${chalk.green('✓ Success')} ${exploitResult.padEnd(6)} ` +
|
||||
`${data.attempts}/3 ${duration.padEnd(11)} ${cost}`
|
||||
);
|
||||
|
||||
if (data.logFile) {
|
||||
const relativePath = path.relative(process.cwd(), data.logFile);
|
||||
console.log(chalk.gray(` └─ Detailed log: ${relativePath}`));
|
||||
}
|
||||
} else {
|
||||
const reason = result.reason as { error?: Error; attempts?: number };
|
||||
const error = reason.error || result.reason;
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
failed.push({ agent: agentName, error: errMsg });
|
||||
|
||||
const attempts = reason.attempts || 3;
|
||||
|
||||
console.log(
|
||||
`${chalk.red(agentDisplay)} ${chalk.red('✗ Failed ')} - ` +
|
||||
`${attempts}/3 - -`
|
||||
);
|
||||
console.log(chalk.gray(` └─ ${errMsg.substring(0, 60)}...`));
|
||||
}
|
||||
});
|
||||
|
||||
console.log(chalk.gray('─'.repeat(80)));
|
||||
console.log(chalk.cyan(`Summary: ${completed.length}/${activeAgents.length} succeeded in ${formatDuration(totalDuration)}`));
|
||||
|
||||
return { completed, failed };
|
||||
};
|
||||
|
||||
// Run all agents in a phase
|
||||
export const runPhase = async (
|
||||
phaseName: string,
|
||||
session: Session,
|
||||
pipelineTestingMode: boolean,
|
||||
runClaudePromptWithRetry: RunClaudePromptWithRetry,
|
||||
loadPrompt: LoadPrompt
|
||||
): Promise<void> => {
|
||||
console.log(chalk.cyan(`\n📋 Running phase: ${phaseName} (parallel execution)`));
|
||||
|
||||
if (phaseName === 'vulnerability-analysis') {
|
||||
console.log(chalk.cyan('🚀 Using parallel execution for 5x faster vulnerability analysis'));
|
||||
const results = await runParallelVuln(session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
|
||||
if (results.failed.length > 0) {
|
||||
console.log(chalk.yellow(`⚠️ ${results.failed.length} agents failed, but phase continues`));
|
||||
results.failed.forEach(failure => {
|
||||
console.log(chalk.red(` - ${failure.agent}: ${failure.error}`));
|
||||
});
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ Phase '${phaseName}' completed: ${results.completed.length} succeeded, ${results.failed.length} failed`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (phaseName === 'exploitation') {
|
||||
console.log(chalk.cyan('🎯 Using parallel execution for 5x faster exploitation'));
|
||||
const results = await runParallelExploit(session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
|
||||
if (results.failed.length > 0) {
|
||||
console.log(chalk.yellow(`⚠️ ${results.failed.length} agents failed, but phase continues`));
|
||||
results.failed.forEach(failure => {
|
||||
console.log(chalk.red(` - ${failure.agent}: ${failure.error}`));
|
||||
});
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ Phase '${phaseName}' completed: ${results.completed.length} succeeded, ${results.failed.length} failed`));
|
||||
return;
|
||||
}
|
||||
|
||||
// For other phases, run single agent
|
||||
const agents = validatePhase(phaseName);
|
||||
if (agents.length === 1) {
|
||||
const agent = agents[0]!;
|
||||
if (session.completedAgents.includes(agent.name)) {
|
||||
console.log(chalk.gray(`⏭️ Agent '${agent.name}' already completed, skipping`));
|
||||
return;
|
||||
}
|
||||
|
||||
await runSingleAgent(agent.name, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
console.log(chalk.green(`✅ Phase '${phaseName}' completed successfully`));
|
||||
} else {
|
||||
throw new PentestError(`Phase '${phaseName}' has multiple agents but no parallel execution defined`, 'validation', false);
|
||||
}
|
||||
};
|
||||
|
||||
// Rollback to specific agent checkpoint
|
||||
export const rollbackTo = async (targetAgent: string, session: Session): Promise<void> => {
|
||||
console.log(chalk.yellow(`🔄 Rolling back to agent: ${targetAgent}`));
|
||||
|
||||
await validateTargetRepo(session.targetRepo);
|
||||
validateAgent(targetAgent);
|
||||
|
||||
const agentName = targetAgent as AgentName;
|
||||
if (!session.checkpoints[agentName]) {
|
||||
throw new PentestError(
|
||||
`No checkpoint found for agent '${targetAgent}' in session history`,
|
||||
'validation',
|
||||
false,
|
||||
{ targetAgent, availableCheckpoints: Object.keys(session.checkpoints) }
|
||||
);
|
||||
}
|
||||
|
||||
const commitHash = session.checkpoints[agentName]!;
|
||||
|
||||
await rollbackGitToCommit(session.targetRepo, commitHash);
|
||||
await rollbackToAgent(session.id, targetAgent);
|
||||
|
||||
// Mark rolled-back agents in audit system
|
||||
try {
|
||||
const { AuditSession } = await import('./audit/index.js');
|
||||
const sessionMetadata: SessionMetadata = {
|
||||
id: session.id,
|
||||
webUrl: session.webUrl,
|
||||
repoPath: session.repoPath,
|
||||
...(session.outputPath && { outputPath: session.outputPath })
|
||||
};
|
||||
const auditSession = new AuditSession(sessionMetadata);
|
||||
await auditSession.initialize();
|
||||
|
||||
const targetOrder = AGENTS[agentName]!.order;
|
||||
const rolledBackAgents = Object.values(AGENTS)
|
||||
.filter(agent => agent.order > targetOrder)
|
||||
.map(agent => agent.name);
|
||||
|
||||
if (rolledBackAgents.length > 0) {
|
||||
await auditSession.markMultipleRolledBack(rolledBackAgents);
|
||||
console.log(chalk.gray(` Marked ${rolledBackAgents.length} agents as rolled-back in audit logs`));
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
console.log(chalk.yellow(` ⚠️ Failed to update audit logs: ${errMsg}`));
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✅ Successfully rolled back to agent '${targetAgent}'`));
|
||||
};
|
||||
|
||||
// Rerun specific agent
|
||||
export const rerunAgent = async (
|
||||
agentName: string,
|
||||
session: Session,
|
||||
pipelineTestingMode: boolean,
|
||||
runClaudePromptWithRetry: RunClaudePromptWithRetry,
|
||||
loadPrompt: LoadPrompt
|
||||
): Promise<void> => {
|
||||
console.log(chalk.cyan(`🔁 Rerunning agent: ${agentName}`));
|
||||
|
||||
const agent = validateAgent(agentName);
|
||||
|
||||
// Find previous agent checkpoint
|
||||
let rollbackTarget: AgentName | null = null;
|
||||
if (agent.prerequisites.length > 0) {
|
||||
const completedPrereqs = agent.prerequisites.filter(prereq =>
|
||||
session.completedAgents.includes(prereq)
|
||||
);
|
||||
if (completedPrereqs.length > 0) {
|
||||
rollbackTarget = completedPrereqs.reduce((latest, current) =>
|
||||
AGENTS[current]!.order > AGENTS[latest]!.order ? current : latest
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (rollbackTarget) {
|
||||
console.log(chalk.blue(`📍 Rolling back to prerequisite: ${rollbackTarget}`));
|
||||
await rollbackTo(rollbackTarget, session);
|
||||
} else if (agent.name === 'pre-recon') {
|
||||
console.log(chalk.blue(`📍 Rolling back to initial repository state`));
|
||||
try {
|
||||
const initialCommit = await executeGitCommandWithRetry(['git', 'log', '--reverse', '--format=%H'], session.targetRepo, 'finding initial commit');
|
||||
const firstCommit = initialCommit.stdout.trim().split('\n')[0];
|
||||
if (firstCommit) {
|
||||
await rollbackGitToCommit(session.targetRepo, firstCommit);
|
||||
}
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
console.log(chalk.yellow(`⚠️ Could not find initial commit, using HEAD: ${errMsg}`));
|
||||
}
|
||||
}
|
||||
|
||||
await runSingleAgent(agent.name, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt, true);
|
||||
|
||||
console.log(chalk.green(`✅ Agent '${agentName}' rerun completed successfully`));
|
||||
};
|
||||
|
||||
// Run all remaining agents
|
||||
export const runAll = async (
|
||||
session: Session,
|
||||
pipelineTestingMode: boolean,
|
||||
runClaudePromptWithRetry: RunClaudePromptWithRetry,
|
||||
loadPrompt: LoadPrompt
|
||||
): Promise<void> => {
|
||||
const allAgentNames = Object.keys(AGENTS) as AgentName[];
|
||||
|
||||
console.log(chalk.cyan(`\n🚀 Running all remaining agents to completion`));
|
||||
console.log(chalk.gray(`Current progress: ${session.completedAgents.length}/${allAgentNames.length} agents completed`));
|
||||
|
||||
const remainingAgents = allAgentNames.filter(agentName =>
|
||||
!session.completedAgents.includes(agentName)
|
||||
);
|
||||
|
||||
if (remainingAgents.length === 0) {
|
||||
console.log(chalk.green('✅ All agents already completed!'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`📋 Remaining agents: ${remainingAgents.join(', ')}`));
|
||||
console.log();
|
||||
|
||||
for (const agentName of remainingAgents) {
|
||||
await runSingleAgent(agentName, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
}
|
||||
|
||||
console.log(chalk.green(`\n🎉 All agents completed successfully! Session marked as completed.`));
|
||||
};
|
||||
|
||||
// Helper for time ago calculation
|
||||
const getTimeAgo = (timestamp: string): string => {
|
||||
const now = new Date();
|
||||
const past = new Date(timestamp);
|
||||
const diffMs = now.getTime() - past.getTime();
|
||||
|
||||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins}m ago`;
|
||||
} else if (diffHours < 24) {
|
||||
return `${diffHours}h ago`;
|
||||
} else {
|
||||
return `${diffDays}d ago`;
|
||||
}
|
||||
};
|
||||
|
||||
// Display session status
|
||||
export const displayStatus = async (session: Session): Promise<void> => {
|
||||
const status = getSessionStatus(session);
|
||||
const timeAgo = getTimeAgo(session.lastActivity);
|
||||
|
||||
console.log(chalk.cyan(`Session: ${new URL(session.webUrl).hostname} + ${path.basename(session.repoPath)}`));
|
||||
console.log(chalk.gray(`Session ID: ${session.id}`));
|
||||
console.log(chalk.gray(`Source Directory: ${session.targetRepo}`));
|
||||
|
||||
// Check if final deliverable exists
|
||||
if (session.targetRepo) {
|
||||
const finalReportPath = path.join(session.targetRepo, 'deliverables', 'comprehensive_security_assessment_report.md');
|
||||
try {
|
||||
if (await fs.pathExists(finalReportPath)) {
|
||||
console.log(chalk.gray(`Final Deliverable Available: ${finalReportPath}`));
|
||||
}
|
||||
} catch {
|
||||
// Silently ignore
|
||||
}
|
||||
}
|
||||
|
||||
const statusColor = status.status === 'completed' ? chalk.green : status.status === 'failed' ? chalk.red : chalk.blue;
|
||||
console.log(statusColor(`Status: ${status.status} (${status.completedCount}/${status.totalAgents} agents completed)`));
|
||||
console.log(chalk.gray(`Last Activity: ${timeAgo}`));
|
||||
|
||||
if (session.configFile) {
|
||||
console.log(chalk.gray(`Config: ${session.configFile}`));
|
||||
}
|
||||
|
||||
console.log();
|
||||
|
||||
// Display agent status
|
||||
const agentList = Object.values(AGENTS).sort((a, b) => a.order - b.order);
|
||||
|
||||
for (const agent of agentList) {
|
||||
let statusIcon: string, statusText: string, statusColorFn: ChalkInstance;
|
||||
|
||||
if (session.completedAgents.includes(agent.name)) {
|
||||
statusIcon = '✅';
|
||||
statusText = `completed ${getTimeAgo(session.lastActivity)}`;
|
||||
statusColorFn = chalk.green;
|
||||
} else if (session.failedAgents.includes(agent.name)) {
|
||||
statusIcon = '❌';
|
||||
statusText = `failed ${getTimeAgo(session.lastActivity)}`;
|
||||
statusColorFn = chalk.red;
|
||||
} else {
|
||||
statusIcon = '⏸️';
|
||||
statusText = 'pending';
|
||||
statusColorFn = chalk.gray;
|
||||
}
|
||||
|
||||
const displayName = agent.name.replace(/-/g, ' ');
|
||||
console.log(`${statusIcon} ${statusColorFn(displayName.padEnd(20))} (${statusText})`);
|
||||
}
|
||||
|
||||
// Show next action
|
||||
const nextAgent = getNextAgent(session);
|
||||
if (nextAgent) {
|
||||
console.log(chalk.cyan(`\nNext: Run --run-agent ${nextAgent.name}`));
|
||||
} else if (status.failedCount > 0) {
|
||||
const failedAgent = session.failedAgents[0];
|
||||
console.log(chalk.yellow(`\nNext: Fix ${failedAgent} failure or run --rerun ${failedAgent}`));
|
||||
} else if (status.status === 'completed') {
|
||||
console.log(chalk.green('\nAll agents completed successfully! 🎉'));
|
||||
}
|
||||
};
|
||||
|
||||
// List all available agents
|
||||
export const listAgents = (): void => {
|
||||
console.log(chalk.cyan('Available Agents:'));
|
||||
|
||||
const phaseNames = Object.keys(PHASES) as PhaseName[];
|
||||
|
||||
phaseNames.forEach((phaseName, phaseIndex) => {
|
||||
const phaseAgents = PHASES[phaseName];
|
||||
const phaseDisplayName = phaseName.split('-').map(word =>
|
||||
word.charAt(0).toUpperCase() + word.slice(1)
|
||||
).join(' ');
|
||||
|
||||
console.log(chalk.yellow(`\nPhase ${phaseIndex + 1} - ${phaseDisplayName}:`));
|
||||
|
||||
phaseAgents.forEach(agentName => {
|
||||
const agent = AGENTS[agentName]!;
|
||||
console.log(chalk.white(` ${agent.name.padEnd(18)} ${agent.displayName}`));
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -1,180 +0,0 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
selectSession, deleteSession, deleteAllSessions,
|
||||
validateAgent, validatePhase, reconcileSession, getSession
|
||||
} from '../session-manager.js';
|
||||
import type { Session } from '../session-manager.js';
|
||||
import {
|
||||
runPhase, runAll, rollbackTo, rerunAgent, displayStatus, listAgents
|
||||
} from '../checkpoint-manager.js';
|
||||
import type { AgentResult } from '../checkpoint-manager.js';
|
||||
import { logError, PentestError } from '../error-handling.js';
|
||||
import { promptConfirmation } from './prompts.js';
|
||||
import type { ChalkInstance } from 'chalk';
|
||||
import type { SessionMetadata } from '../audit/utils.js';
|
||||
import type { DistributedConfig } from '../types/config.js';
|
||||
|
||||
// Types for callback functions
|
||||
type RunClaudePromptWithRetry = (
|
||||
prompt: string,
|
||||
sourceDir: string,
|
||||
allowedTools: string,
|
||||
context: string,
|
||||
description: string,
|
||||
agentName: string | null,
|
||||
colorFn: ChalkInstance,
|
||||
sessionMetadata: SessionMetadata | null
|
||||
) => Promise<AgentResult>;
|
||||
|
||||
type LoadPrompt = (
|
||||
promptName: string,
|
||||
variables: { webUrl: string; repoPath: string },
|
||||
config: DistributedConfig | null,
|
||||
pipelineTestingMode: boolean
|
||||
) => Promise<string>;
|
||||
|
||||
// Developer command handlers
|
||||
export async function handleDeveloperCommand(
|
||||
command: string,
|
||||
args: string[],
|
||||
pipelineTestingMode: boolean,
|
||||
runClaudePromptWithRetry: RunClaudePromptWithRetry,
|
||||
loadPrompt: LoadPrompt
|
||||
): Promise<void> {
|
||||
try {
|
||||
let session: Session | null;
|
||||
|
||||
// Commands that don't require session selection
|
||||
if (command === '--list-agents') {
|
||||
listAgents();
|
||||
return;
|
||||
}
|
||||
|
||||
if (command === '--cleanup') {
|
||||
// Handle cleanup without needing session selection first
|
||||
if (args[0]) {
|
||||
// Cleanup specific session by ID
|
||||
const sessionId = args[0];
|
||||
const deletedSession = await deleteSession(sessionId);
|
||||
console.log(chalk.green(`✅ Deleted session ${sessionId} (${new URL(deletedSession.webUrl).hostname})`));
|
||||
} else {
|
||||
// Cleanup all sessions - require confirmation
|
||||
const confirmed = await promptConfirmation(chalk.yellow('⚠️ This will delete all pentest sessions. Are you sure? (y/N):'));
|
||||
if (confirmed) {
|
||||
const deleted = await deleteAllSessions();
|
||||
if (deleted) {
|
||||
console.log(chalk.green('✅ All sessions deleted'));
|
||||
} else {
|
||||
console.log(chalk.yellow('⚠️ No sessions found to delete'));
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray('Cleanup cancelled'));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Early validation for commands with agent names (before session selection)
|
||||
|
||||
if (command === '--run-phase') {
|
||||
if (!args[0]) {
|
||||
console.log(chalk.red('❌ --run-phase requires a phase name'));
|
||||
console.log(chalk.gray('Usage: shannon --run-phase <phase-name>'));
|
||||
process.exit(1);
|
||||
}
|
||||
validatePhase(args[0]); // This will throw PentestError if invalid
|
||||
}
|
||||
|
||||
if (command === '--rollback-to' || command === '--rerun') {
|
||||
if (!args[0]) {
|
||||
console.log(chalk.red(`❌ ${command} requires an agent name`));
|
||||
console.log(chalk.gray(`Usage: shannon ${command} <agent-name>`));
|
||||
process.exit(1);
|
||||
}
|
||||
validateAgent(args[0]); // This will throw PentestError if invalid
|
||||
}
|
||||
|
||||
// Get session for other commands
|
||||
try {
|
||||
session = await selectSession();
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
console.log(chalk.red(`❌ ${errMsg}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Self-healing: Reconcile session with audit logs before executing command
|
||||
// This ensures Shannon store is consistent with audit data, even after crash recovery
|
||||
try {
|
||||
const reconcileReport = await reconcileSession(session.id);
|
||||
|
||||
if (reconcileReport.promotions.length > 0) {
|
||||
console.log(chalk.blue(`🔄 Reconciled: Added ${reconcileReport.promotions.length} completed agents from audit logs`));
|
||||
}
|
||||
if (reconcileReport.demotions.length > 0) {
|
||||
console.log(chalk.yellow(`🔄 Reconciled: Removed ${reconcileReport.demotions.length} rolled-back agents`));
|
||||
}
|
||||
if (reconcileReport.failures.length > 0) {
|
||||
console.log(chalk.yellow(`🔄 Reconciled: Marked ${reconcileReport.failures.length} failed agents`));
|
||||
}
|
||||
|
||||
// Reload session after reconciliation to get fresh state
|
||||
session = await getSession(session.id);
|
||||
} catch (error) {
|
||||
// Reconciliation failure is non-critical, but log warning
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
console.log(chalk.yellow(`⚠️ Failed to reconcile session with audit logs: ${errMsg}`));
|
||||
}
|
||||
|
||||
if (!session) {
|
||||
console.log(chalk.red('❌ Session not found after reconciliation'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
switch (command) {
|
||||
|
||||
case '--run-phase':
|
||||
await runPhase(args[0]!, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
break;
|
||||
|
||||
case '--run-all':
|
||||
await runAll(session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
break;
|
||||
|
||||
case '--rollback-to':
|
||||
await rollbackTo(args[0]!, session);
|
||||
break;
|
||||
|
||||
case '--rerun':
|
||||
await rerunAgent(args[0]!, session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
break;
|
||||
|
||||
case '--status':
|
||||
await displayStatus(session);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log(chalk.red(`❌ Unknown developer command: ${command}`));
|
||||
console.log(chalk.gray('Use --help to see available commands'));
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof PentestError) {
|
||||
await logError(error, `Developer command ${command}`);
|
||||
console.log(chalk.red.bold(`\n🚨 Command failed: ${error.message}`));
|
||||
} else {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
console.log(chalk.red.bold(`\n🚨 Unexpected error: ${errMsg}`));
|
||||
if (process.env.DEBUG && error instanceof Error) {
|
||||
console.log(chalk.gray(error.stack));
|
||||
}
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import { createInterface } from 'readline';
|
||||
import { PentestError } from '../error-handling.js';
|
||||
|
||||
/**
|
||||
* Prompt user for yes/no confirmation
|
||||
*/
|
||||
export async function promptConfirmation(message: string): Promise<boolean> {
|
||||
const readline = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve) => {
|
||||
readline.question(message + ' ', (answer) => {
|
||||
readline.close();
|
||||
const confirmed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
||||
resolve(confirmed);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Prompt user to select from numbered list
|
||||
*/
|
||||
export async function promptSelection<T>(message: string, items: T[]): Promise<T> {
|
||||
if (!items || items.length === 0) {
|
||||
throw new PentestError('No items available for selection', 'validation', false);
|
||||
}
|
||||
|
||||
const readline = createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
readline.question(message + ' ', (answer) => {
|
||||
readline.close();
|
||||
|
||||
const choice = parseInt(answer);
|
||||
if (isNaN(choice) || choice < 1 || choice > items.length) {
|
||||
reject(
|
||||
new PentestError(
|
||||
`Invalid selection. Please enter a number between 1 and ${items.length}`,
|
||||
'validation',
|
||||
false,
|
||||
{ choice: answer }
|
||||
)
|
||||
);
|
||||
} else {
|
||||
resolve(items[choice - 1]!);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -12,22 +12,8 @@ export function showHelp(): void {
|
||||
console.log(chalk.cyan.bold('AI Penetration Testing Agent'));
|
||||
console.log(chalk.gray('Automated security assessment tool\n'));
|
||||
|
||||
console.log(chalk.yellow.bold('NORMAL MODE (Creates Sessions):'));
|
||||
console.log(
|
||||
' shannon <WEB_URL> <REPO_PATH> [--config config.yaml] [--pipeline-testing]'
|
||||
);
|
||||
console.log(
|
||||
' shannon <WEB_URL> <REPO_PATH> --setup-only # Setup local repo and create session only\n'
|
||||
);
|
||||
|
||||
console.log(chalk.yellow.bold('DEVELOPER MODE (Operates on Existing Sessions):'));
|
||||
console.log(' shannon --run-phase <phase-name> [--pipeline-testing]');
|
||||
console.log(' shannon --run-all [--pipeline-testing]');
|
||||
console.log(' shannon --rollback-to <agent-name>');
|
||||
console.log(' shannon --rerun <agent-name> [--pipeline-testing]');
|
||||
console.log(' shannon --status');
|
||||
console.log(' shannon --list-agents');
|
||||
console.log(' shannon --cleanup [session-id] # Delete sessions\n');
|
||||
console.log(chalk.yellow.bold('USAGE:'));
|
||||
console.log(' shannon <WEB_URL> <REPO_PATH> [--config config.yaml] [--output /path/to/reports]\n');
|
||||
|
||||
console.log(chalk.yellow.bold('OPTIONS:'));
|
||||
console.log(
|
||||
@@ -40,42 +26,20 @@ export function showHelp(): void {
|
||||
' --pipeline-testing Use minimal prompts for fast pipeline testing (creates minimal deliverables)'
|
||||
);
|
||||
console.log(
|
||||
' --disable-loader Disable the animated progress loader (useful when logs interfere with spinner)\n'
|
||||
' --disable-loader Disable the animated progress loader (useful when logs interfere with spinner)'
|
||||
);
|
||||
|
||||
console.log(chalk.yellow.bold('DEVELOPER COMMANDS:'));
|
||||
console.log(
|
||||
' --run-phase Run all agents in a phase (parallel execution for 5x speedup)'
|
||||
);
|
||||
console.log(' --run-all Run all remaining agents to completion (parallel execution)');
|
||||
console.log(' --rollback-to Rollback git workspace to agent checkpoint');
|
||||
console.log(' --rerun Rollback and rerun specific agent');
|
||||
console.log(' --status Show current session status and progress');
|
||||
console.log(' --list-agents List all available agents and phases');
|
||||
console.log(' --cleanup Delete all sessions or specific session by ID\n');
|
||||
console.log(' --help Show this help message\n');
|
||||
|
||||
console.log(chalk.yellow.bold('EXAMPLES:'));
|
||||
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'
|
||||
);
|
||||
|
||||
console.log(' # Developer mode - operate on existing session');
|
||||
console.log(' shannon --status # Show session status');
|
||||
console.log(' shannon --run-phase exploitation # Run entire phase');
|
||||
console.log(' shannon --run-all # Run all remaining agents');
|
||||
console.log(' shannon --rerun xss-vuln # Fix and rerun failed agent');
|
||||
console.log(' shannon --cleanup # Delete all sessions');
|
||||
console.log(' shannon --cleanup <session-id> # Delete specific session\n');
|
||||
console.log(' shannon "https://example.com" "/path/to/local/repo" --pipeline-testing\n');
|
||||
|
||||
console.log(chalk.yellow.bold('REQUIREMENTS:'));
|
||||
console.log(' • WEB_URL must start with http:// or https://');
|
||||
console.log(' • REPO_PATH must be an accessible local directory');
|
||||
console.log(' • Only test systems you own or have permission to test');
|
||||
console.log(' • Developer mode requires existing pentest session\n');
|
||||
console.log(' • Only test systems you own or have permission to test\n');
|
||||
|
||||
console.log(chalk.yellow.bold('ENVIRONMENT VARIABLES:'));
|
||||
console.log(' PENTEST_MAX_RETRIES Number of retries for AI agents (default: 3)');
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { $, fs, path } from 'zx';
|
||||
import chalk from 'chalk';
|
||||
import { Timer, timingResults } from '../utils/metrics.js';
|
||||
import { Timer } from '../utils/metrics.js';
|
||||
import { formatDuration } from '../audit/utils.js';
|
||||
import { handleToolError, PentestError } from '../error-handling.js';
|
||||
import { AGENTS } from '../session-manager.js';
|
||||
@@ -14,7 +14,14 @@ import { runClaudePromptWithRetry } from '../ai/claude-executor.js';
|
||||
import { loadPrompt } from '../prompts/prompt-manager.js';
|
||||
import type { ToolAvailability } from '../tool-checker.js';
|
||||
import type { DistributedConfig } from '../types/config.js';
|
||||
import type { AgentResult } from '../checkpoint-manager.js';
|
||||
|
||||
interface AgentResult {
|
||||
success: boolean;
|
||||
duration: number;
|
||||
cost?: number;
|
||||
error?: string;
|
||||
retryable?: boolean;
|
||||
}
|
||||
|
||||
type ToolName = 'nmap' | 'subfinder' | 'whatweb' | 'schemathesis';
|
||||
type ToolStatus = 'success' | 'skipped' | 'error';
|
||||
@@ -61,7 +68,6 @@ async function runTerminalScan(tool: ToolName, target: string, sourceDir: string
|
||||
const nmapHostname = new URL(target).hostname;
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`nmap -sV -sC ${nmapHostname}`;
|
||||
const duration = timer.stop();
|
||||
timingResults.commands[tool] = duration;
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(duration)}`));
|
||||
return { tool: 'nmap', output: result.stdout, status: 'success', duration };
|
||||
}
|
||||
@@ -70,7 +76,6 @@ async function runTerminalScan(tool: ToolName, target: string, sourceDir: string
|
||||
const hostname = new URL(target).hostname;
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`subfinder -d ${hostname}`;
|
||||
const subfinderDuration = timer.stop();
|
||||
timingResults.commands[tool] = subfinderDuration;
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(subfinderDuration)}`));
|
||||
return { tool: 'subfinder', output: result.stdout, status: 'success', duration: subfinderDuration };
|
||||
}
|
||||
@@ -80,7 +85,6 @@ async function runTerminalScan(tool: ToolName, target: string, sourceDir: string
|
||||
console.log(chalk.gray(` Command: ${command}`));
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`whatweb --open-timeout 30 --read-timeout 60 ${target}`;
|
||||
const whatwebDuration = timer.stop();
|
||||
timingResults.commands[tool] = whatwebDuration;
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(whatwebDuration)}`));
|
||||
return { tool: 'whatweb', output: result.stdout, status: 'success', duration: whatwebDuration };
|
||||
}
|
||||
@@ -107,7 +111,6 @@ async function runTerminalScan(tool: ToolName, target: string, sourceDir: string
|
||||
}
|
||||
|
||||
const schemaDuration = timer.stop();
|
||||
timingResults.commands[tool] = schemaDuration;
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(schemaDuration)}`));
|
||||
return { tool: 'schemathesis', output: allResults.join('\n\n'), status: 'success', duration: schemaDuration };
|
||||
} else {
|
||||
@@ -124,7 +127,6 @@ async function runTerminalScan(tool: ToolName, target: string, sourceDir: string
|
||||
}
|
||||
} catch (error) {
|
||||
const duration = timer.stop();
|
||||
timingResults.commands[tool] = duration;
|
||||
console.log(chalk.red(` ❌ ${tool} failed in ${formatDuration(duration)}`));
|
||||
return handleToolError(tool, error as Error & { code?: string }) as TerminalScanResult;
|
||||
}
|
||||
|
||||
@@ -4,809 +4,112 @@
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import { fs, path } from 'zx';
|
||||
import chalk from 'chalk';
|
||||
import crypto from 'crypto';
|
||||
import { PentestError } from './error-handling.js';
|
||||
import { SessionMutex } from './utils/concurrency.js';
|
||||
import { promptSelection } from './cli/prompts.js';
|
||||
import type { AgentName, PhaseName } from './types/index.js';
|
||||
import type { SessionMetadata } from './audit/utils.js';
|
||||
|
||||
// Audit data types for reconciliation
|
||||
interface AuditAgentData {
|
||||
status: 'in-progress' | 'success' | 'failed' | 'rolled-back';
|
||||
checkpoint?: string;
|
||||
}
|
||||
|
||||
interface AuditMetricsData {
|
||||
metrics: {
|
||||
agents: Record<string, AuditAgentData>;
|
||||
};
|
||||
}
|
||||
import { path } from 'zx';
|
||||
import type { AgentName } from './types/index.js';
|
||||
|
||||
// Agent definition interface
|
||||
export interface AgentDefinition {
|
||||
name: AgentName;
|
||||
displayName: string;
|
||||
phase: PhaseName;
|
||||
order: number;
|
||||
prerequisites: AgentName[];
|
||||
}
|
||||
|
||||
// Session interface
|
||||
export interface Session {
|
||||
id: string;
|
||||
webUrl: string;
|
||||
repoPath: string;
|
||||
configFile: string | null;
|
||||
targetRepo: string;
|
||||
outputPath: string | null;
|
||||
status: 'in-progress' | 'completed' | 'failed';
|
||||
completedAgents: AgentName[];
|
||||
failedAgents: AgentName[];
|
||||
checkpoints: Record<AgentName, string>;
|
||||
createdAt: string;
|
||||
lastActivity: string;
|
||||
}
|
||||
// Agent definitions according to PRD
|
||||
export const AGENTS: Readonly<Record<AgentName, AgentDefinition>> = Object.freeze({
|
||||
'pre-recon': {
|
||||
name: 'pre-recon',
|
||||
displayName: 'Pre-recon agent',
|
||||
prerequisites: []
|
||||
},
|
||||
'recon': {
|
||||
name: 'recon',
|
||||
displayName: 'Recon agent',
|
||||
prerequisites: ['pre-recon']
|
||||
},
|
||||
'injection-vuln': {
|
||||
name: 'injection-vuln',
|
||||
displayName: 'Injection vuln agent',
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
'xss-vuln': {
|
||||
name: 'xss-vuln',
|
||||
displayName: 'XSS vuln agent',
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
'auth-vuln': {
|
||||
name: 'auth-vuln',
|
||||
displayName: 'Auth vuln agent',
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
'ssrf-vuln': {
|
||||
name: 'ssrf-vuln',
|
||||
displayName: 'SSRF vuln agent',
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
'authz-vuln': {
|
||||
name: 'authz-vuln',
|
||||
displayName: 'Authz vuln agent',
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
'injection-exploit': {
|
||||
name: 'injection-exploit',
|
||||
displayName: 'Injection exploit agent',
|
||||
prerequisites: ['injection-vuln']
|
||||
},
|
||||
'xss-exploit': {
|
||||
name: 'xss-exploit',
|
||||
displayName: 'XSS exploit agent',
|
||||
prerequisites: ['xss-vuln']
|
||||
},
|
||||
'auth-exploit': {
|
||||
name: 'auth-exploit',
|
||||
displayName: 'Auth exploit agent',
|
||||
prerequisites: ['auth-vuln']
|
||||
},
|
||||
'ssrf-exploit': {
|
||||
name: 'ssrf-exploit',
|
||||
displayName: 'SSRF exploit agent',
|
||||
prerequisites: ['ssrf-vuln']
|
||||
},
|
||||
'authz-exploit': {
|
||||
name: 'authz-exploit',
|
||||
displayName: 'Authz exploit agent',
|
||||
prerequisites: ['authz-vuln']
|
||||
},
|
||||
'report': {
|
||||
name: 'report',
|
||||
displayName: 'Report agent',
|
||||
prerequisites: ['injection-exploit', 'xss-exploit', 'auth-exploit', 'ssrf-exploit', 'authz-exploit']
|
||||
}
|
||||
});
|
||||
|
||||
// Session store interface
|
||||
interface SessionStore {
|
||||
sessions: Record<string, Session>;
|
||||
}
|
||||
// Agent execution order
|
||||
export const AGENT_ORDER: readonly AgentName[] = Object.freeze([
|
||||
'pre-recon',
|
||||
'recon',
|
||||
'injection-vuln',
|
||||
'xss-vuln',
|
||||
'auth-vuln',
|
||||
'ssrf-vuln',
|
||||
'authz-vuln',
|
||||
'injection-exploit',
|
||||
'xss-exploit',
|
||||
'auth-exploit',
|
||||
'ssrf-exploit',
|
||||
'authz-exploit',
|
||||
'report'
|
||||
] as const);
|
||||
|
||||
// Session status result
|
||||
export interface SessionStatusResult {
|
||||
status: 'in-progress' | 'completed' | 'failed';
|
||||
completedCount: number;
|
||||
totalAgents: number;
|
||||
failedCount: number;
|
||||
completionPercentage: number;
|
||||
}
|
||||
// Parallel execution groups
|
||||
export const getParallelGroups = (): Readonly<{ vuln: AgentName[]; exploit: AgentName[] }> => Object.freeze({
|
||||
vuln: ['injection-vuln', 'xss-vuln', 'auth-vuln', 'ssrf-vuln', 'authz-vuln'],
|
||||
exploit: ['injection-exploit', 'xss-exploit', 'auth-exploit', 'ssrf-exploit', 'authz-exploit']
|
||||
});
|
||||
|
||||
// Reconciliation report
|
||||
interface ReconciliationReport {
|
||||
promotions: string[];
|
||||
demotions: string[];
|
||||
failures: string[];
|
||||
}
|
||||
|
||||
// Generate a session-based log folder path
|
||||
// NEW FORMAT: {hostname}_{sessionId} (no hash, full UUID for consistency with audit system)
|
||||
// Generate a session-based log folder path (used by claude-executor.ts)
|
||||
export const generateSessionLogPath = (webUrl: string, sessionId: string): string => {
|
||||
const hostname = new URL(webUrl).hostname.replace(/[^a-zA-Z0-9-]/g, '-');
|
||||
const sessionFolderName = `${hostname}_${sessionId}`;
|
||||
return path.join(process.cwd(), 'agent-logs', sessionFolderName);
|
||||
};
|
||||
|
||||
const sessionMutex = new SessionMutex();
|
||||
|
||||
// Agent definitions according to PRD
|
||||
export const AGENTS: Readonly<Record<AgentName, AgentDefinition>> = Object.freeze({
|
||||
// Phase 1 - Pre-reconnaissance
|
||||
'pre-recon': {
|
||||
name: 'pre-recon',
|
||||
displayName: 'Pre-recon agent',
|
||||
phase: 'pre-reconnaissance',
|
||||
order: 1,
|
||||
prerequisites: []
|
||||
},
|
||||
|
||||
// Phase 2 - Reconnaissance
|
||||
'recon': {
|
||||
name: 'recon',
|
||||
displayName: 'Recon agent',
|
||||
phase: 'reconnaissance',
|
||||
order: 2,
|
||||
prerequisites: ['pre-recon']
|
||||
},
|
||||
|
||||
// Phase 3 - Vulnerability Analysis
|
||||
'injection-vuln': {
|
||||
name: 'injection-vuln',
|
||||
displayName: 'Injection vuln agent',
|
||||
phase: 'vulnerability-analysis',
|
||||
order: 3,
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
'xss-vuln': {
|
||||
name: 'xss-vuln',
|
||||
displayName: 'XSS vuln agent',
|
||||
phase: 'vulnerability-analysis',
|
||||
order: 4,
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
'auth-vuln': {
|
||||
name: 'auth-vuln',
|
||||
displayName: 'Auth vuln agent',
|
||||
phase: 'vulnerability-analysis',
|
||||
order: 5,
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
'ssrf-vuln': {
|
||||
name: 'ssrf-vuln',
|
||||
displayName: 'SSRF vuln agent',
|
||||
phase: 'vulnerability-analysis',
|
||||
order: 6,
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
'authz-vuln': {
|
||||
name: 'authz-vuln',
|
||||
displayName: 'Authz vuln agent',
|
||||
phase: 'vulnerability-analysis',
|
||||
order: 7,
|
||||
prerequisites: ['recon']
|
||||
},
|
||||
|
||||
// Phase 4 - Exploitation
|
||||
'injection-exploit': {
|
||||
name: 'injection-exploit',
|
||||
displayName: 'Injection exploit agent',
|
||||
phase: 'exploitation',
|
||||
order: 8,
|
||||
prerequisites: ['injection-vuln']
|
||||
},
|
||||
'xss-exploit': {
|
||||
name: 'xss-exploit',
|
||||
displayName: 'XSS exploit agent',
|
||||
phase: 'exploitation',
|
||||
order: 9,
|
||||
prerequisites: ['xss-vuln']
|
||||
},
|
||||
'auth-exploit': {
|
||||
name: 'auth-exploit',
|
||||
displayName: 'Auth exploit agent',
|
||||
phase: 'exploitation',
|
||||
order: 10,
|
||||
prerequisites: ['auth-vuln']
|
||||
},
|
||||
'ssrf-exploit': {
|
||||
name: 'ssrf-exploit',
|
||||
displayName: 'SSRF exploit agent',
|
||||
phase: 'exploitation',
|
||||
order: 11,
|
||||
prerequisites: ['ssrf-vuln']
|
||||
},
|
||||
'authz-exploit': {
|
||||
name: 'authz-exploit',
|
||||
displayName: 'Authz exploit agent',
|
||||
phase: 'exploitation',
|
||||
order: 12,
|
||||
prerequisites: ['authz-vuln']
|
||||
},
|
||||
|
||||
// Phase 5 - Reporting
|
||||
'report': {
|
||||
name: 'report',
|
||||
displayName: 'Report agent',
|
||||
phase: 'reporting',
|
||||
order: 13,
|
||||
prerequisites: ['authz-exploit']
|
||||
}
|
||||
});
|
||||
|
||||
// Phase definitions
|
||||
export const PHASES: Readonly<Record<PhaseName, readonly AgentName[]>> = Object.freeze({
|
||||
'pre-reconnaissance': ['pre-recon'],
|
||||
'reconnaissance': ['recon'],
|
||||
'vulnerability-analysis': ['injection-vuln', 'xss-vuln', 'auth-vuln', 'ssrf-vuln', 'authz-vuln'],
|
||||
'exploitation': ['injection-exploit', 'xss-exploit', 'auth-exploit', 'ssrf-exploit', 'authz-exploit'],
|
||||
'reporting': ['report']
|
||||
});
|
||||
|
||||
// Session store file path
|
||||
const STORE_FILE = path.join(process.cwd(), '.shannon-store.json');
|
||||
|
||||
// Load sessions from store file
|
||||
const loadSessions = async (): Promise<SessionStore> => {
|
||||
try {
|
||||
if (!await fs.pathExists(STORE_FILE)) {
|
||||
return { sessions: {} };
|
||||
}
|
||||
|
||||
const content = await fs.readFile(STORE_FILE, 'utf8');
|
||||
const store = JSON.parse(content) as unknown;
|
||||
|
||||
// Validate store structure
|
||||
if (!store || typeof store !== 'object' || !('sessions' in store)) {
|
||||
console.log(chalk.yellow('⚠️ Invalid session store format, creating new store'));
|
||||
return { sessions: {} };
|
||||
}
|
||||
|
||||
return store as SessionStore;
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
console.log(chalk.yellow(`⚠️ Failed to load session store: ${errMsg}, creating new store`));
|
||||
return { sessions: {} };
|
||||
}
|
||||
};
|
||||
|
||||
// Save sessions to store file atomically
|
||||
const saveSessions = async (store: SessionStore): Promise<void> => {
|
||||
try {
|
||||
const tempFile = `${STORE_FILE}.tmp`;
|
||||
await fs.writeJSON(tempFile, store, { spaces: 2 });
|
||||
await fs.move(tempFile, STORE_FILE, { overwrite: true });
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
throw new PentestError(
|
||||
`Failed to save session store: ${errMsg}`,
|
||||
'filesystem',
|
||||
false,
|
||||
{ storeFile: STORE_FILE, originalError: errMsg }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// Find existing session for the same web URL and repository path
|
||||
const findExistingSession = async (webUrl: string, targetRepo: string): Promise<Session | undefined> => {
|
||||
const store = await loadSessions();
|
||||
const sessions = Object.values(store.sessions);
|
||||
|
||||
// Normalize paths for comparison
|
||||
const normalizedTargetRepo = path.resolve(targetRepo);
|
||||
|
||||
// Look for existing session with same webUrl and targetRepo
|
||||
const existingSession = sessions.find(session => {
|
||||
const normalizedSessionRepo = path.resolve(session.targetRepo || session.repoPath);
|
||||
return session.webUrl === webUrl && normalizedSessionRepo === normalizedTargetRepo;
|
||||
});
|
||||
|
||||
return existingSession;
|
||||
};
|
||||
|
||||
// Generate session ID as unique UUID
|
||||
const generateSessionId = (): string => {
|
||||
// Always generate a unique UUID for each session
|
||||
return crypto.randomUUID();
|
||||
};
|
||||
|
||||
// Create new session or return existing one
|
||||
export const createSession = async (
|
||||
webUrl: string,
|
||||
repoPath: string,
|
||||
configFile: string | null = null,
|
||||
targetRepo: string | null = null,
|
||||
outputPath: string | null = null
|
||||
): Promise<Session> => {
|
||||
// Use targetRepo if provided, otherwise use repoPath
|
||||
const resolvedTargetRepo = targetRepo || repoPath;
|
||||
|
||||
// Check for existing session first
|
||||
const existingSession = await findExistingSession(webUrl, resolvedTargetRepo);
|
||||
|
||||
if (existingSession) {
|
||||
// If session is not completed, reuse it
|
||||
if (existingSession.status !== 'completed') {
|
||||
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 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)
|
||||
console.log(chalk.gray(`Previous session was completed, creating new session...`));
|
||||
}
|
||||
|
||||
const sessionId = generateSessionId();
|
||||
|
||||
// STANDARD: All sessions use 'id' field (NOT 'sessionId')
|
||||
// This is the canonical session structure used throughout the codebase
|
||||
const session: Session = {
|
||||
id: sessionId,
|
||||
webUrl,
|
||||
repoPath,
|
||||
configFile,
|
||||
targetRepo: resolvedTargetRepo,
|
||||
outputPath,
|
||||
status: 'in-progress',
|
||||
completedAgents: [],
|
||||
failedAgents: [],
|
||||
checkpoints: {} as Record<AgentName, string>,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastActivity: new Date().toISOString()
|
||||
};
|
||||
|
||||
const store = await loadSessions();
|
||||
store.sessions[sessionId] = session;
|
||||
await saveSessions(store);
|
||||
|
||||
return session;
|
||||
};
|
||||
|
||||
// Get session by ID
|
||||
export const getSession = async (sessionId: string): Promise<Session | null> => {
|
||||
const store = await loadSessions();
|
||||
return store.sessions[sessionId] || null;
|
||||
};
|
||||
|
||||
// Update session
|
||||
export const updateSession = async (
|
||||
sessionId: string,
|
||||
updates: Partial<Session>
|
||||
): Promise<Session> => {
|
||||
const store = await loadSessions();
|
||||
|
||||
if (!store.sessions[sessionId]) {
|
||||
throw new PentestError(
|
||||
`Session ${sessionId} not found`,
|
||||
'validation',
|
||||
false,
|
||||
{ sessionId }
|
||||
);
|
||||
}
|
||||
|
||||
store.sessions[sessionId] = {
|
||||
...store.sessions[sessionId]!,
|
||||
...updates,
|
||||
lastActivity: new Date().toISOString()
|
||||
};
|
||||
|
||||
await saveSessions(store);
|
||||
return store.sessions[sessionId]!;
|
||||
};
|
||||
|
||||
// List all sessions
|
||||
const listSessions = async (): Promise<Session[]> => {
|
||||
const store = await loadSessions();
|
||||
return Object.values(store.sessions);
|
||||
};
|
||||
|
||||
// Interactive session selection
|
||||
export const selectSession = async (): Promise<Session> => {
|
||||
const sessions = await listSessions();
|
||||
|
||||
if (sessions.length === 0) {
|
||||
throw new PentestError(
|
||||
'No pentest sessions found. Run a normal pentest first to create a session.',
|
||||
'validation',
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if (sessions.length === 1) {
|
||||
return sessions[0]!;
|
||||
}
|
||||
|
||||
// Display session options
|
||||
console.log(chalk.cyan('\nMultiple pentest sessions found:\n'));
|
||||
|
||||
sessions.forEach((session, index) => {
|
||||
const completedCount = session.completedAgents.length;
|
||||
const totalAgents = Object.keys(AGENTS).length;
|
||||
const timeAgo = getTimeAgo(session.lastActivity);
|
||||
|
||||
// Use dynamic status calculation instead of stored status
|
||||
const { status } = getSessionStatus(session);
|
||||
const statusColor = status === 'completed' ? chalk.green : chalk.blue;
|
||||
|
||||
console.log(statusColor(`${index + 1}) ${new URL(session.webUrl).hostname} + ${path.basename(session.repoPath)} [${status}]`));
|
||||
console.log(chalk.gray(` Last activity: ${timeAgo}, Completed: ${completedCount}/${totalAgents} agents`));
|
||||
console.log(chalk.gray(` Session ID: ${session.id}`));
|
||||
|
||||
if (session.configFile) {
|
||||
console.log(chalk.gray(` Config: ${session.configFile}`));
|
||||
}
|
||||
|
||||
console.log(); // Empty line between sessions
|
||||
});
|
||||
|
||||
// Get user selection
|
||||
return await promptSelection(
|
||||
chalk.cyan(`Select session (1-${sessions.length}):`),
|
||||
sessions
|
||||
);
|
||||
};
|
||||
|
||||
// Validate agent name
|
||||
export const validateAgent = (agentName: string): AgentDefinition => {
|
||||
const agent = AGENTS[agentName as AgentName];
|
||||
if (!agent) {
|
||||
throw new PentestError(
|
||||
`Agent '${agentName}' not recognized. Use --list-agents to see valid names.`,
|
||||
'validation',
|
||||
false,
|
||||
{ agentName, validAgents: Object.keys(AGENTS) }
|
||||
);
|
||||
}
|
||||
return agent;
|
||||
};
|
||||
|
||||
// Validate agent range
|
||||
export const validateAgentRange = (startAgent: string, endAgent: string): AgentDefinition[] => {
|
||||
const start = validateAgent(startAgent);
|
||||
const end = validateAgent(endAgent);
|
||||
|
||||
if (start.order >= end.order) {
|
||||
throw new PentestError(
|
||||
`End agent '${endAgent}' must come after start agent '${startAgent}' in sequence.`,
|
||||
'validation',
|
||||
false,
|
||||
{ startAgent, endAgent, startOrder: start.order, endOrder: end.order }
|
||||
);
|
||||
}
|
||||
|
||||
// Get all agents in range
|
||||
const agentList = Object.values(AGENTS)
|
||||
.filter(agent => agent.order >= start.order && agent.order <= end.order)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
|
||||
return agentList;
|
||||
};
|
||||
|
||||
// Validate phase name
|
||||
export const validatePhase = (phaseName: string): AgentDefinition[] => {
|
||||
const phase = PHASES[phaseName as PhaseName];
|
||||
if (!phase) {
|
||||
throw new PentestError(
|
||||
`Phase '${phaseName}' not recognized. Valid phases: ${Object.keys(PHASES).join(', ')}`,
|
||||
'validation',
|
||||
false,
|
||||
{ phaseName, validPhases: Object.keys(PHASES) }
|
||||
);
|
||||
}
|
||||
return phase.map(agentName => AGENTS[agentName]!);
|
||||
};
|
||||
|
||||
// Check prerequisites for an agent
|
||||
export const checkPrerequisites = (session: Session, agentName: string): boolean => {
|
||||
const agent = validateAgent(agentName);
|
||||
|
||||
const missingPrereqs = agent.prerequisites.filter(prereq =>
|
||||
!session.completedAgents.includes(prereq)
|
||||
);
|
||||
|
||||
if (missingPrereqs.length > 0) {
|
||||
throw new PentestError(
|
||||
`Cannot run '${agentName}': prerequisite agent(s) not completed: ${missingPrereqs.join(', ')}`,
|
||||
'validation',
|
||||
false,
|
||||
{ agentName, missingPrerequisites: missingPrereqs, completedAgents: session.completedAgents }
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
// Get next suggested agent
|
||||
export const getNextAgent = (session: Session): AgentDefinition | undefined => {
|
||||
const completed = new Set(session.completedAgents);
|
||||
|
||||
// Find the next agent that hasn't been completed and has all prerequisites
|
||||
const nextAgent = Object.values(AGENTS)
|
||||
.sort((a, b) => a.order - b.order)
|
||||
.find(agent => {
|
||||
if (completed.has(agent.name)) return false; // Already completed
|
||||
|
||||
// Check if all prerequisites are completed
|
||||
const prereqsMet = agent.prerequisites.every(prereq => completed.has(prereq));
|
||||
return prereqsMet;
|
||||
});
|
||||
|
||||
return nextAgent;
|
||||
};
|
||||
|
||||
// Mark agent as completed with checkpoint
|
||||
// NOTE: Timing, cost, and validation data now managed by AuditSession (audit-logs/session.json)
|
||||
// Shannon store contains ONLY orchestration state (completedAgents, checkpoints)
|
||||
export const markAgentCompleted = async (
|
||||
sessionId: string,
|
||||
agentName: string,
|
||||
checkpointCommit: string
|
||||
): Promise<Session> => {
|
||||
// Use mutex to prevent race conditions during parallel agent execution
|
||||
const unlock = await sessionMutex.lock(sessionId);
|
||||
|
||||
try {
|
||||
// Get fresh session data under lock
|
||||
const session = await getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new PentestError(`Session ${sessionId} not found`, 'validation', false);
|
||||
}
|
||||
|
||||
validateAgent(agentName);
|
||||
|
||||
const updates: Partial<Session> = {
|
||||
completedAgents: [...new Set([...session.completedAgents, agentName as AgentName])],
|
||||
failedAgents: session.failedAgents.filter(agent => agent !== agentName),
|
||||
checkpoints: {
|
||||
...session.checkpoints,
|
||||
[agentName]: checkpointCommit
|
||||
} as Record<AgentName, string>
|
||||
};
|
||||
|
||||
// Check if all agents are now completed and update session status
|
||||
const totalAgents = Object.keys(AGENTS).length;
|
||||
if (updates.completedAgents!.length === totalAgents) {
|
||||
updates.status = 'completed';
|
||||
}
|
||||
|
||||
return await updateSession(sessionId, updates);
|
||||
} finally {
|
||||
// Always release the lock, even if an error occurs
|
||||
unlock();
|
||||
}
|
||||
};
|
||||
|
||||
// Mark agent as failed
|
||||
export const markAgentFailed = async (sessionId: string, agentName: string): Promise<Session> => {
|
||||
const session = await getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new PentestError(`Session ${sessionId} not found`, 'validation', false);
|
||||
}
|
||||
|
||||
validateAgent(agentName);
|
||||
|
||||
const updates: Partial<Session> = {
|
||||
failedAgents: [...new Set([...session.failedAgents, agentName as AgentName])],
|
||||
completedAgents: session.completedAgents.filter(agent => agent !== agentName)
|
||||
};
|
||||
|
||||
return await updateSession(sessionId, updates);
|
||||
};
|
||||
|
||||
// Get time ago helper
|
||||
const getTimeAgo = (timestamp: string): string => {
|
||||
const now = new Date();
|
||||
const past = new Date(timestamp);
|
||||
const diffMs = now.getTime() - past.getTime();
|
||||
|
||||
const diffMins = Math.floor(diffMs / (1000 * 60));
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins}m ago`;
|
||||
} else if (diffHours < 24) {
|
||||
return `${diffHours}h ago`;
|
||||
} else {
|
||||
return `${diffDays}d ago`;
|
||||
}
|
||||
};
|
||||
|
||||
// Get session status summary
|
||||
export const getSessionStatus = (session: Session): SessionStatusResult => {
|
||||
const totalAgents = Object.keys(AGENTS).length;
|
||||
const completedCount = session.completedAgents.length;
|
||||
const failedCount = session.failedAgents.length;
|
||||
|
||||
let status: 'in-progress' | 'completed' | 'failed';
|
||||
if (completedCount === totalAgents) {
|
||||
status = 'completed';
|
||||
} else if (failedCount > 0) {
|
||||
status = 'failed';
|
||||
} else {
|
||||
status = 'in-progress';
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
completedCount,
|
||||
totalAgents,
|
||||
failedCount,
|
||||
completionPercentage: Math.round((completedCount / totalAgents) * 100)
|
||||
};
|
||||
};
|
||||
|
||||
// Calculate comprehensive summary statistics for vulnerability analysis
|
||||
export const calculateVulnerabilityAnalysisSummary = (session: Session): Readonly<{
|
||||
totalAnalyses: number;
|
||||
totalVulnerabilities: number;
|
||||
exploitationCandidates: number;
|
||||
completedAgents: AgentName[];
|
||||
}> => {
|
||||
const vulnAgents = PHASES['vulnerability-analysis'];
|
||||
const completedVulnAgents = session.completedAgents.filter(agent =>
|
||||
vulnAgents.includes(agent)
|
||||
);
|
||||
|
||||
// NOTE: Actual vulnerability counts require reading queue files
|
||||
// This summary only shows completion counts
|
||||
return Object.freeze({
|
||||
totalAnalyses: completedVulnAgents.length,
|
||||
totalVulnerabilities: 0, // Requires reading queue files
|
||||
exploitationCandidates: 0, // Requires reading queue files
|
||||
completedAgents: completedVulnAgents
|
||||
});
|
||||
};
|
||||
|
||||
// Calculate exploitation summary statistics
|
||||
export const calculateExploitationSummary = (session: Session): Readonly<{
|
||||
totalAttempts: number;
|
||||
eligibleExploits: number;
|
||||
skippedExploits: number;
|
||||
completedAgents: AgentName[];
|
||||
}> => {
|
||||
const exploitAgents = PHASES['exploitation'];
|
||||
const completedExploitAgents = session.completedAgents.filter(agent =>
|
||||
exploitAgents.includes(agent)
|
||||
);
|
||||
|
||||
// NOTE: Eligibility requires reading queue files
|
||||
// This summary only shows completion counts
|
||||
return Object.freeze({
|
||||
totalAttempts: completedExploitAgents.length,
|
||||
eligibleExploits: 0, // Requires reading queue files
|
||||
skippedExploits: 0, // Requires reading queue files
|
||||
completedAgents: completedExploitAgents
|
||||
});
|
||||
};
|
||||
|
||||
// Rollback session to specific agent checkpoint
|
||||
export const rollbackToAgent = async (
|
||||
sessionId: string,
|
||||
targetAgent: string
|
||||
): Promise<Session> => {
|
||||
const session = await getSession(sessionId);
|
||||
if (!session) {
|
||||
throw new PentestError(`Session ${sessionId} not found`, 'validation', false);
|
||||
}
|
||||
|
||||
validateAgent(targetAgent);
|
||||
|
||||
if (!session.checkpoints[targetAgent as AgentName]) {
|
||||
throw new PentestError(
|
||||
`No checkpoint found for agent '${targetAgent}' in session history`,
|
||||
'validation',
|
||||
false,
|
||||
{ targetAgent, availableCheckpoints: Object.keys(session.checkpoints) }
|
||||
);
|
||||
}
|
||||
|
||||
// Find agents that need to be removed (those after the target agent)
|
||||
const targetOrder = AGENTS[targetAgent as AgentName]!.order;
|
||||
const agentsToRemove = Object.values(AGENTS)
|
||||
.filter(agent => agent.order > targetOrder)
|
||||
.map(agent => agent.name);
|
||||
|
||||
const updates: Partial<Session> = {
|
||||
completedAgents: session.completedAgents.filter(agent => !agentsToRemove.includes(agent)),
|
||||
failedAgents: session.failedAgents.filter(agent => !agentsToRemove.includes(agent)),
|
||||
checkpoints: Object.fromEntries(
|
||||
Object.entries(session.checkpoints).filter(([agent]) => !agentsToRemove.includes(agent as AgentName))
|
||||
) as Record<AgentName, string>
|
||||
};
|
||||
|
||||
// NOTE: Timing and cost data now managed in audit-logs/session.json
|
||||
// Rollback will be reflected via reconcileSession() which marks agents as "rolled-back"
|
||||
|
||||
return await updateSession(sessionId, updates);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reconcile Shannon store with audit logs (self-healing)
|
||||
*
|
||||
* This function ensures the Shannon store (.shannon-store.json) is consistent with
|
||||
* the audit logs (audit-logs/session.json) by syncing agent completion status.
|
||||
*
|
||||
* Three-part reconciliation:
|
||||
* 1. PROMOTIONS: Agents completed/failed in audit → added to Shannon store
|
||||
* 2. DEMOTIONS: Agents rolled-back in audit → removed from Shannon store
|
||||
* 3. VERIFICATION: Ensure audit state fully reflected in orchestration
|
||||
*
|
||||
* Critical for crash recovery, especially crash during rollback operations.
|
||||
*/
|
||||
export const reconcileSession = async (sessionId: string): Promise<ReconciliationReport> => {
|
||||
const { AuditSession } = await import('./audit/index.js');
|
||||
|
||||
// Get Shannon store session
|
||||
const shannonSession = await getSession(sessionId);
|
||||
if (!shannonSession) {
|
||||
throw new PentestError(`Session ${sessionId} not found in Shannon store`, 'validation', false);
|
||||
}
|
||||
|
||||
// Get audit session data - cast session to SessionMetadata for compatibility
|
||||
const sessionMetadata: SessionMetadata = {
|
||||
id: shannonSession.id,
|
||||
webUrl: shannonSession.webUrl,
|
||||
repoPath: shannonSession.repoPath,
|
||||
};
|
||||
const auditSession = new AuditSession(sessionMetadata);
|
||||
await auditSession.initialize();
|
||||
const auditData = await auditSession.getMetrics() as AuditMetricsData;
|
||||
|
||||
const report: ReconciliationReport = {
|
||||
promotions: [],
|
||||
demotions: [],
|
||||
failures: []
|
||||
};
|
||||
|
||||
// PART 1: PROMOTIONS (Additive)
|
||||
// Find agents completed in audit but not in Shannon store
|
||||
const auditCompleted = Object.entries(auditData.metrics.agents)
|
||||
.filter(([, agentData]) => agentData.status === 'success')
|
||||
.map(([agentName]) => agentName);
|
||||
|
||||
const missing = auditCompleted.filter(agent => !shannonSession.completedAgents.includes(agent as AgentName));
|
||||
|
||||
for (const agentName of missing) {
|
||||
const agentData = auditData.metrics.agents[agentName];
|
||||
const checkpoint = agentData?.checkpoint || '';
|
||||
await markAgentCompleted(sessionId, agentName, checkpoint);
|
||||
report.promotions.push(agentName);
|
||||
}
|
||||
|
||||
// PART 2: DEMOTIONS (Subtractive) - CRITICAL FOR ROLLBACK RECOVERY
|
||||
// Find agents rolled-back in audit but still in Shannon store
|
||||
const auditRolledBack = Object.entries(auditData.metrics.agents)
|
||||
.filter(([, agentData]) => agentData.status === 'rolled-back')
|
||||
.map(([agentName]) => agentName);
|
||||
|
||||
const toRemove = shannonSession.completedAgents.filter(agent => auditRolledBack.includes(agent));
|
||||
|
||||
if (toRemove.length > 0) {
|
||||
// Reload session to get fresh state
|
||||
const freshSession = await getSession(sessionId);
|
||||
|
||||
if (freshSession) {
|
||||
const updates: Partial<Session> = {
|
||||
completedAgents: freshSession.completedAgents.filter(agent => !toRemove.includes(agent)),
|
||||
checkpoints: Object.fromEntries(
|
||||
Object.entries(freshSession.checkpoints).filter(([agent]) => !toRemove.includes(agent as AgentName))
|
||||
) as Record<AgentName, string>
|
||||
};
|
||||
|
||||
await updateSession(sessionId, updates);
|
||||
report.demotions.push(...toRemove);
|
||||
}
|
||||
}
|
||||
|
||||
// PART 3: FAILURES
|
||||
// Find agents failed in audit but not marked failed in Shannon store
|
||||
const auditFailed = Object.entries(auditData.metrics.agents)
|
||||
.filter(([, agentData]) => agentData.status === 'failed')
|
||||
.map(([agentName]) => agentName);
|
||||
|
||||
const failedToAdd = auditFailed.filter(agent => !shannonSession.failedAgents.includes(agent as AgentName));
|
||||
|
||||
for (const agentName of failedToAdd) {
|
||||
await markAgentFailed(sessionId, agentName);
|
||||
report.failures.push(agentName);
|
||||
}
|
||||
|
||||
return report;
|
||||
};
|
||||
|
||||
// Delete a specific session by ID
|
||||
export const deleteSession = async (sessionId: string): Promise<Session> => {
|
||||
const store = await loadSessions();
|
||||
|
||||
if (!store.sessions[sessionId]) {
|
||||
throw new PentestError(
|
||||
`Session ${sessionId} not found`,
|
||||
'validation',
|
||||
false,
|
||||
{ sessionId }
|
||||
);
|
||||
}
|
||||
|
||||
const deletedSession = store.sessions[sessionId]!;
|
||||
delete store.sessions[sessionId];
|
||||
await saveSessions(store);
|
||||
|
||||
return deletedSession;
|
||||
};
|
||||
|
||||
// Delete all sessions (remove entire storage)
|
||||
export const deleteAllSessions = async (): Promise<boolean> => {
|
||||
try {
|
||||
if (await fs.pathExists(STORE_FILE)) {
|
||||
await fs.remove(STORE_FILE);
|
||||
return true;
|
||||
}
|
||||
return false; // File didn't exist
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
throw new PentestError(
|
||||
`Failed to delete session storage: ${errMsg}`,
|
||||
'filesystem',
|
||||
false,
|
||||
{ storeFile: STORE_FILE, originalError: errMsg }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
710
src/shannon.ts
710
src/shannon.ts
@@ -6,7 +6,7 @@
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import { path, fs, $ } from 'zx';
|
||||
import chalk from 'chalk';
|
||||
import chalk, { type ChalkInstance } from 'chalk';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config();
|
||||
@@ -15,11 +15,9 @@ dotenv.config();
|
||||
import { parseConfig, distributeConfig } from './config-parser.js';
|
||||
import { checkToolAvailability, handleMissingTools } from './tool-checker.js';
|
||||
|
||||
// Session and Checkpoints
|
||||
import { createSession, updateSession, getSession, AGENTS } from './session-manager.js';
|
||||
import type { Session } from './session-manager.js';
|
||||
import type { AgentName } from './types/index.js';
|
||||
import { runPhase, getGitCommitHash } from './checkpoint-manager.js';
|
||||
// Session
|
||||
import { AGENTS, getParallelGroups } from './session-manager.js';
|
||||
import type { AgentName, PromptName } from './types/index.js';
|
||||
|
||||
// Setup and Deliverables
|
||||
import { setupLocalRepo } from './setup/environment.js';
|
||||
@@ -33,56 +31,173 @@ import { executePreReconPhase } from './phases/pre-recon.js';
|
||||
import { assembleFinalReport } from './phases/reporting.js';
|
||||
|
||||
// Utils
|
||||
import { timingResults, costResults, displayTimingSummary, Timer } from './utils/metrics.js';
|
||||
import { timingResults, displayTimingSummary, Timer } from './utils/metrics.js';
|
||||
import { formatDuration, generateAuditPath } from './audit/utils.js';
|
||||
import type { SessionMetadata } from './audit/utils.js';
|
||||
import { AuditSession } from './audit/audit-session.js';
|
||||
|
||||
// CLI
|
||||
import { handleDeveloperCommand } from './cli/command-handler.js';
|
||||
import { showHelp, displaySplashScreen } from './cli/ui.js';
|
||||
import { validateWebUrl, validateRepoPath } from './cli/input-validator.js';
|
||||
|
||||
// Error Handling
|
||||
import { PentestError, logError } from './error-handling.js';
|
||||
|
||||
// Session Manager Functions
|
||||
import {
|
||||
calculateVulnerabilityAnalysisSummary,
|
||||
calculateExploitationSummary,
|
||||
getNextAgent
|
||||
} from './session-manager.js';
|
||||
|
||||
import type { DistributedConfig } from './types/config.js';
|
||||
import type { ToolAvailability } from './tool-checker.js';
|
||||
import { safeValidateQueueAndDeliverable } from './queue-validation.js';
|
||||
|
||||
// Extend global namespace for SHANNON_DISABLE_LOADER
|
||||
declare global {
|
||||
var SHANNON_DISABLE_LOADER: boolean | undefined;
|
||||
}
|
||||
|
||||
// Session Lock File Management
|
||||
const STORE_PATH = path.join(process.cwd(), '.shannon-store.json');
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
webUrl: string;
|
||||
repoPath: string;
|
||||
status: 'in-progress' | 'completed' | 'failed';
|
||||
startedAt: string;
|
||||
}
|
||||
|
||||
interface SessionStore {
|
||||
sessions: Session[];
|
||||
}
|
||||
|
||||
function generateSessionId(): string {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
async function loadSessions(): Promise<SessionStore> {
|
||||
try {
|
||||
if (await fs.pathExists(STORE_PATH)) {
|
||||
return await fs.readJson(STORE_PATH) as SessionStore;
|
||||
}
|
||||
} catch {
|
||||
// Corrupted file, start fresh
|
||||
}
|
||||
return { sessions: [] };
|
||||
}
|
||||
|
||||
async function saveSessions(store: SessionStore): Promise<void> {
|
||||
await fs.writeJson(STORE_PATH, store, { spaces: 2 });
|
||||
}
|
||||
|
||||
async function createSession(webUrl: string, repoPath: string): Promise<Session> {
|
||||
const store = await loadSessions();
|
||||
|
||||
// Check for existing in-progress session
|
||||
const existing = store.sessions.find(
|
||||
s => s.repoPath === repoPath && s.status === 'in-progress'
|
||||
);
|
||||
if (existing) {
|
||||
throw new PentestError(
|
||||
`Session already in progress for ${repoPath}`,
|
||||
'validation',
|
||||
false,
|
||||
{ sessionId: existing.id }
|
||||
);
|
||||
}
|
||||
|
||||
const session: Session = {
|
||||
id: generateSessionId(),
|
||||
webUrl,
|
||||
repoPath,
|
||||
status: 'in-progress',
|
||||
startedAt: new Date().toISOString()
|
||||
};
|
||||
|
||||
store.sessions.push(session);
|
||||
await saveSessions(store);
|
||||
return session;
|
||||
}
|
||||
|
||||
async function updateSessionStatus(
|
||||
sessionId: string,
|
||||
status: 'in-progress' | 'completed' | 'failed'
|
||||
): Promise<void> {
|
||||
const store = await loadSessions();
|
||||
const session = store.sessions.find(s => s.id === sessionId);
|
||||
if (session) {
|
||||
session.status = status;
|
||||
await saveSessions(store);
|
||||
}
|
||||
}
|
||||
|
||||
interface PromptVariables {
|
||||
webUrl: string;
|
||||
repoPath: string;
|
||||
sourceDir: string;
|
||||
}
|
||||
|
||||
interface SessionUpdates {
|
||||
completedAgents?: AgentName[];
|
||||
failedAgents?: AgentName[];
|
||||
status?: 'in-progress' | 'completed' | 'failed';
|
||||
checkpoints?: Record<AgentName, string>;
|
||||
}
|
||||
|
||||
interface MainResult {
|
||||
reportPath: string;
|
||||
auditLogsPath: string;
|
||||
}
|
||||
|
||||
interface AgentResult {
|
||||
success: boolean;
|
||||
duration: number;
|
||||
cost?: number;
|
||||
error?: string;
|
||||
retryable?: boolean;
|
||||
}
|
||||
|
||||
interface ParallelAgentResult {
|
||||
agentName: AgentName;
|
||||
success: boolean;
|
||||
timing?: number | undefined;
|
||||
cost?: number | undefined;
|
||||
attempts: number;
|
||||
error?: string | undefined;
|
||||
}
|
||||
|
||||
// Configure zx to disable timeouts (let tools run as long as needed)
|
||||
$.timeout = 0;
|
||||
|
||||
// Helper function to get prompt name from agent name
|
||||
const getPromptName = (agentName: AgentName): PromptName => {
|
||||
const mappings: Record<AgentName, PromptName> = {
|
||||
'pre-recon': 'pre-recon-code',
|
||||
'recon': 'recon',
|
||||
'injection-vuln': 'vuln-injection',
|
||||
'xss-vuln': 'vuln-xss',
|
||||
'auth-vuln': 'vuln-auth',
|
||||
'ssrf-vuln': 'vuln-ssrf',
|
||||
'authz-vuln': 'vuln-authz',
|
||||
'injection-exploit': 'exploit-injection',
|
||||
'xss-exploit': 'exploit-xss',
|
||||
'auth-exploit': 'exploit-auth',
|
||||
'ssrf-exploit': 'exploit-ssrf',
|
||||
'authz-exploit': 'exploit-authz',
|
||||
'report': 'report-executive'
|
||||
};
|
||||
|
||||
return mappings[agentName] || agentName as PromptName;
|
||||
};
|
||||
|
||||
// Get color function for agent
|
||||
const getAgentColor = (agentName: AgentName): ChalkInstance => {
|
||||
const colorMap: Partial<Record<AgentName, ChalkInstance>> = {
|
||||
'injection-vuln': chalk.red,
|
||||
'injection-exploit': chalk.red,
|
||||
'xss-vuln': chalk.yellow,
|
||||
'xss-exploit': chalk.yellow,
|
||||
'auth-vuln': chalk.blue,
|
||||
'auth-exploit': chalk.blue,
|
||||
'ssrf-vuln': chalk.magenta,
|
||||
'ssrf-exploit': chalk.magenta,
|
||||
'authz-vuln': chalk.green,
|
||||
'authz-exploit': chalk.green
|
||||
};
|
||||
return colorMap[agentName] || chalk.cyan;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<void> {
|
||||
const srcDeliverables = path.join(sourceDir, 'deliverables');
|
||||
@@ -101,6 +216,316 @@ async function consolidateOutputs(sourceDir: string, sessionPath: string): Promi
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single agent
|
||||
*/
|
||||
async function runAgent(
|
||||
agentName: AgentName,
|
||||
sourceDir: string,
|
||||
variables: PromptVariables,
|
||||
distributedConfig: DistributedConfig | null,
|
||||
pipelineTestingMode: boolean,
|
||||
sessionMetadata: SessionMetadata
|
||||
): Promise<AgentResult> {
|
||||
const agent = AGENTS[agentName];
|
||||
const promptName = getPromptName(agentName);
|
||||
const prompt = await loadPrompt(promptName, variables, distributedConfig, pipelineTestingMode);
|
||||
|
||||
return await runClaudePromptWithRetry(
|
||||
prompt,
|
||||
sourceDir,
|
||||
'*',
|
||||
'',
|
||||
agent.displayName,
|
||||
agentName,
|
||||
getAgentColor(agentName),
|
||||
sessionMetadata
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run vulnerability agents in parallel
|
||||
*/
|
||||
async function runParallelVuln(
|
||||
sourceDir: string,
|
||||
variables: PromptVariables,
|
||||
distributedConfig: DistributedConfig | null,
|
||||
pipelineTestingMode: boolean,
|
||||
sessionMetadata: SessionMetadata
|
||||
): Promise<ParallelAgentResult[]> {
|
||||
const { vuln: vulnAgents } = getParallelGroups();
|
||||
|
||||
console.log(chalk.cyan(`\nStarting ${vulnAgents.length} vulnerability analysis specialists in parallel...`));
|
||||
console.log(chalk.gray(' Specialists: ' + vulnAgents.join(', ')));
|
||||
console.log();
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
vulnAgents.map(async (agentName, index) => {
|
||||
// Add 2-second stagger to prevent API overwhelm
|
||||
await new Promise(resolve => setTimeout(resolve, index * 2000));
|
||||
|
||||
let lastError: Error | undefined;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
try {
|
||||
const result = await runAgent(
|
||||
agentName,
|
||||
sourceDir,
|
||||
variables,
|
||||
distributedConfig,
|
||||
pipelineTestingMode,
|
||||
sessionMetadata
|
||||
);
|
||||
|
||||
// Validate vulnerability analysis results
|
||||
const vulnType = agentName.replace('-vuln', '');
|
||||
try {
|
||||
const validation = await safeValidateQueueAndDeliverable(vulnType as 'injection' | 'xss' | 'auth' | 'ssrf' | 'authz', sourceDir);
|
||||
|
||||
if (validation.success && validation.data) {
|
||||
console.log(chalk.blue(`${agentName}: ${validation.data.shouldExploit ? `Ready for exploitation (${validation.data.vulnerabilityCount} vulnerabilities)` : 'No vulnerabilities found'}`));
|
||||
}
|
||||
} catch {
|
||||
// Validation failure is non-critical
|
||||
}
|
||||
|
||||
return {
|
||||
agentName,
|
||||
success: result.success,
|
||||
timing: result.duration,
|
||||
cost: result.cost,
|
||||
attempts
|
||||
};
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempts < maxAttempts) {
|
||||
console.log(chalk.yellow(`Warning: ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`));
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
agentName,
|
||||
success: false,
|
||||
attempts,
|
||||
error: lastError?.message || 'Unknown error'
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const totalDuration = Date.now() - startTime;
|
||||
|
||||
// Process and display results
|
||||
console.log(chalk.cyan('\nVulnerability Analysis Results'));
|
||||
console.log(chalk.gray('-'.repeat(80)));
|
||||
console.log(chalk.bold('Agent Status Attempt Duration Cost'));
|
||||
console.log(chalk.gray('-'.repeat(80)));
|
||||
|
||||
const processedResults: ParallelAgentResult[] = [];
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const agentName = vulnAgents[index]!;
|
||||
const agentDisplay = agentName.padEnd(22);
|
||||
|
||||
if (result.status === 'fulfilled') {
|
||||
const data = result.value;
|
||||
processedResults.push(data);
|
||||
|
||||
if (data.success) {
|
||||
const duration = formatDuration(data.timing || 0);
|
||||
const cost = `$${(data.cost || 0).toFixed(4)}`;
|
||||
|
||||
console.log(
|
||||
`${chalk.green(agentDisplay)} ${chalk.green('Success')} ` +
|
||||
`${data.attempts}/3 ${duration.padEnd(11)} ${cost}`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`${chalk.red(agentDisplay)} ${chalk.red('Failed ')} ` +
|
||||
`${data.attempts}/3 - -`
|
||||
);
|
||||
if (data.error) {
|
||||
console.log(chalk.gray(` Error: ${data.error.substring(0, 60)}...`));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
processedResults.push({
|
||||
agentName,
|
||||
success: false,
|
||||
attempts: 3,
|
||||
error: String(result.reason)
|
||||
});
|
||||
|
||||
console.log(
|
||||
`${chalk.red(agentDisplay)} ${chalk.red('Failed ')} ` +
|
||||
`3/3 - -`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(chalk.gray('-'.repeat(80)));
|
||||
const successCount = processedResults.filter(r => r.success).length;
|
||||
console.log(chalk.cyan(`Summary: ${successCount}/${vulnAgents.length} succeeded in ${formatDuration(totalDuration)}`));
|
||||
|
||||
return processedResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run exploitation agents in parallel
|
||||
*/
|
||||
async function runParallelExploit(
|
||||
sourceDir: string,
|
||||
variables: PromptVariables,
|
||||
distributedConfig: DistributedConfig | null,
|
||||
pipelineTestingMode: boolean,
|
||||
sessionMetadata: SessionMetadata
|
||||
): Promise<ParallelAgentResult[]> {
|
||||
const { exploit: exploitAgents, vuln: vulnAgents } = getParallelGroups();
|
||||
|
||||
// Load validation module
|
||||
const { safeValidateQueueAndDeliverable } = await import('./queue-validation.js');
|
||||
|
||||
// Check eligibility
|
||||
const eligibilityChecks = await Promise.all(
|
||||
exploitAgents.map(async (agentName) => {
|
||||
const vulnAgentName = agentName.replace('-exploit', '-vuln') as AgentName;
|
||||
const vulnType = vulnAgentName.replace('-vuln', '') as 'injection' | 'xss' | 'auth' | 'ssrf' | 'authz';
|
||||
|
||||
const validation = await safeValidateQueueAndDeliverable(vulnType, sourceDir);
|
||||
|
||||
if (!validation.success || !validation.data?.shouldExploit) {
|
||||
console.log(chalk.gray(`Skipping ${agentName} (no vulnerabilities found in ${vulnAgentName})`));
|
||||
return { agentName, eligible: false };
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`${agentName} eligible (${validation.data.vulnerabilityCount} vulnerabilities from ${vulnAgentName})`));
|
||||
return { agentName, eligible: true };
|
||||
})
|
||||
);
|
||||
|
||||
const eligibleAgents = eligibilityChecks
|
||||
.filter(check => check.eligible)
|
||||
.map(check => check.agentName);
|
||||
|
||||
if (eligibleAgents.length === 0) {
|
||||
console.log(chalk.gray('No exploitation agents eligible (no vulnerabilities found)'));
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log(chalk.cyan(`\nStarting ${eligibleAgents.length} exploitation specialists in parallel...`));
|
||||
console.log(chalk.gray(' Specialists: ' + eligibleAgents.join(', ')));
|
||||
console.log();
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
eligibleAgents.map(async (agentName, index) => {
|
||||
await new Promise(resolve => setTimeout(resolve, index * 2000));
|
||||
|
||||
let lastError: Error | undefined;
|
||||
let attempts = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
attempts++;
|
||||
try {
|
||||
const result = await runAgent(
|
||||
agentName,
|
||||
sourceDir,
|
||||
variables,
|
||||
distributedConfig,
|
||||
pipelineTestingMode,
|
||||
sessionMetadata
|
||||
);
|
||||
|
||||
return {
|
||||
agentName,
|
||||
success: result.success,
|
||||
timing: result.duration,
|
||||
cost: result.cost,
|
||||
attempts
|
||||
};
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
if (attempts < maxAttempts) {
|
||||
console.log(chalk.yellow(`Warning: ${agentName} failed attempt ${attempts}/${maxAttempts}, retrying...`));
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
agentName,
|
||||
success: false,
|
||||
attempts,
|
||||
error: lastError?.message || 'Unknown error'
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const totalDuration = Date.now() - startTime;
|
||||
|
||||
// Process and display results
|
||||
console.log(chalk.cyan('\nExploitation Results'));
|
||||
console.log(chalk.gray('-'.repeat(80)));
|
||||
console.log(chalk.bold('Agent Status Attempt Duration Cost'));
|
||||
console.log(chalk.gray('-'.repeat(80)));
|
||||
|
||||
const processedResults: ParallelAgentResult[] = [];
|
||||
|
||||
results.forEach((result, index) => {
|
||||
const agentName = eligibleAgents[index]!;
|
||||
const agentDisplay = agentName.padEnd(22);
|
||||
|
||||
if (result.status === 'fulfilled') {
|
||||
const data = result.value;
|
||||
processedResults.push(data);
|
||||
|
||||
if (data.success) {
|
||||
const duration = formatDuration(data.timing || 0);
|
||||
const cost = `$${(data.cost || 0).toFixed(4)}`;
|
||||
|
||||
console.log(
|
||||
`${chalk.green(agentDisplay)} ${chalk.green('Success')} ` +
|
||||
`${data.attempts}/3 ${duration.padEnd(11)} ${cost}`
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`${chalk.red(agentDisplay)} ${chalk.red('Failed ')} ` +
|
||||
`${data.attempts}/3 - -`
|
||||
);
|
||||
if (data.error) {
|
||||
console.log(chalk.gray(` Error: ${data.error.substring(0, 60)}...`));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
processedResults.push({
|
||||
agentName,
|
||||
success: false,
|
||||
attempts: 3,
|
||||
error: String(result.reason)
|
||||
});
|
||||
|
||||
console.log(
|
||||
`${chalk.red(agentDisplay)} ${chalk.red('Failed ')} ` +
|
||||
`3/3 - -`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
console.log(chalk.gray('-'.repeat(80)));
|
||||
const successCount = processedResults.filter(r => r.success).length;
|
||||
console.log(chalk.cyan(`Summary: ${successCount}/${eligibleAgents.length} succeeded in ${formatDuration(totalDuration)}`));
|
||||
|
||||
return processedResults;
|
||||
}
|
||||
|
||||
// Setup graceful cleanup on process signals
|
||||
process.on('SIGINT', async () => {
|
||||
console.log(chalk.yellow('\n⚠️ Received SIGINT, cleaning up...'));
|
||||
@@ -190,40 +615,16 @@ 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, outputPath);
|
||||
console.log(chalk.blue(`📝 Session created: ${session.id.substring(0, 8)}...`));
|
||||
// Create session (acts as lock file)
|
||||
const session: Session = await createSession(webUrl, repoPath);
|
||||
console.log(chalk.blue(`Session created: ${session.id.substring(0, 8)}...`));
|
||||
|
||||
// If setup-only mode, exit after session creation
|
||||
if (process.argv.includes('--setup-only')) {
|
||||
console.log(chalk.green('✅ Setup complete! Local repository setup and session created.'));
|
||||
console.log(chalk.gray('Use developer commands to run individual agents:'));
|
||||
console.log(chalk.gray(' shannon --run-agent pre-recon'));
|
||||
console.log(chalk.gray(' shannon --status'));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Helper function to update session progress
|
||||
const updateSessionProgress = async (agentName: AgentName, commitHash: string | null = null): Promise<void> => {
|
||||
try {
|
||||
const updates: SessionUpdates = {
|
||||
completedAgents: [...new Set([...session.completedAgents, agentName])] as AgentName[],
|
||||
failedAgents: session.failedAgents.filter(name => name !== agentName),
|
||||
status: 'in-progress'
|
||||
};
|
||||
|
||||
if (commitHash) {
|
||||
updates.checkpoints = { ...session.checkpoints, [agentName]: commitHash };
|
||||
}
|
||||
|
||||
await updateSession(session.id, updates);
|
||||
// Update local session object for subsequent updates
|
||||
Object.assign(session, updates);
|
||||
console.log(chalk.gray(` 📝 Session updated: ${agentName} completed`));
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.log(chalk.yellow(` ⚠️ Failed to update session: ${err.message}`));
|
||||
}
|
||||
// Session metadata for audit logging
|
||||
const sessionMetadata: SessionMetadata = {
|
||||
id: session.id,
|
||||
webUrl,
|
||||
repoPath: sourceDir,
|
||||
...(outputPath && { outputPath })
|
||||
};
|
||||
|
||||
// Create outputs directory in source directory
|
||||
@@ -242,25 +643,8 @@ async function main(
|
||||
);
|
||||
}
|
||||
|
||||
// Check if we should continue from where session left off
|
||||
const nextAgent = getNextAgent(session);
|
||||
if (!nextAgent) {
|
||||
console.log(chalk.green(`✅ All agents completed! Session is finished.`));
|
||||
displayTimingSummary();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`🔄 Continuing from ${nextAgent.displayName} (${session.completedAgents.length}/${Object.keys(AGENTS).length} agents completed)`));
|
||||
|
||||
// Determine which phase to start from based on next agent
|
||||
const startPhase = nextAgent.name === 'pre-recon' ? 1
|
||||
: nextAgent.name === 'recon' ? 2
|
||||
: ['injection-vuln', 'xss-vuln', 'auth-vuln', 'ssrf-vuln', 'authz-vuln'].includes(nextAgent.name) ? 3
|
||||
: ['injection-exploit', 'xss-exploit', 'auth-exploit', 'ssrf-exploit', 'authz-exploit'].includes(nextAgent.name) ? 4
|
||||
: nextAgent.name === 'report' ? 5 : 1;
|
||||
|
||||
try {
|
||||
// PHASE 1: PRE-RECONNAISSANCE
|
||||
if (startPhase <= 1) {
|
||||
const { duration: preReconDuration } = await executePreReconPhase(
|
||||
webUrl,
|
||||
sourceDir,
|
||||
@@ -268,92 +652,64 @@ async function main(
|
||||
distributedConfig,
|
||||
toolAvailability,
|
||||
pipelineTestingMode,
|
||||
session.id, // Pass session ID for logging
|
||||
outputPath // Pass output path for audit logging
|
||||
session.id,
|
||||
outputPath
|
||||
);
|
||||
timingResults.phases['pre-recon'] = preReconDuration;
|
||||
await updateSessionProgress('pre-recon');
|
||||
}
|
||||
console.log(chalk.green(`Pre-reconnaissance complete in ${formatDuration(preReconDuration)}`));
|
||||
|
||||
// PHASE 2: RECONNAISSANCE
|
||||
if (startPhase <= 2) {
|
||||
console.log(chalk.magenta.bold('\n🔎 PHASE 2: RECONNAISSANCE'));
|
||||
console.log(chalk.magenta('Analyzing initial findings...'));
|
||||
const reconTimer = new Timer('phase-2-recon');
|
||||
await runClaudePromptWithRetry(
|
||||
await loadPrompt('recon', variables, distributedConfig, pipelineTestingMode),
|
||||
|
||||
await runAgent(
|
||||
'recon',
|
||||
sourceDir,
|
||||
'*',
|
||||
'',
|
||||
AGENTS['recon'].displayName,
|
||||
'recon', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: session.id, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
variables,
|
||||
distributedConfig,
|
||||
pipelineTestingMode,
|
||||
sessionMetadata
|
||||
);
|
||||
const reconDuration = reconTimer.stop();
|
||||
timingResults.phases['recon'] = reconDuration;
|
||||
|
||||
console.log(chalk.green(`✅ Reconnaissance complete in ${formatDuration(reconDuration)}`));
|
||||
await updateSessionProgress('recon');
|
||||
}
|
||||
|
||||
// PHASE 3: VULNERABILITY ANALYSIS
|
||||
if (startPhase <= 3) {
|
||||
const vulnTimer = new Timer('phase-3-vulnerability-analysis');
|
||||
console.log(chalk.red.bold('\n🚨 PHASE 3: VULNERABILITY ANALYSIS'));
|
||||
|
||||
await runPhase('vulnerability-analysis', session, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
|
||||
// Display vulnerability analysis summary
|
||||
const currentSession = await getSession(session.id);
|
||||
if (currentSession) {
|
||||
const vulnSummary = calculateVulnerabilityAnalysisSummary(currentSession);
|
||||
console.log(chalk.blue(`\n📊 Vulnerability Analysis Summary: ${vulnSummary.totalAnalyses} analyses, ${vulnSummary.totalVulnerabilities} vulnerabilities found, ${vulnSummary.exploitationCandidates} ready for exploitation`));
|
||||
}
|
||||
const vulnResults = await runParallelVuln(
|
||||
sourceDir,
|
||||
variables,
|
||||
distributedConfig,
|
||||
pipelineTestingMode,
|
||||
sessionMetadata
|
||||
);
|
||||
|
||||
const vulnDuration = vulnTimer.stop();
|
||||
timingResults.phases['vulnerability-analysis'] = vulnDuration;
|
||||
|
||||
console.log(chalk.green(`✅ Vulnerability analysis phase complete in ${formatDuration(vulnDuration)}`));
|
||||
}
|
||||
|
||||
// PHASE 4: EXPLOITATION
|
||||
if (startPhase <= 4) {
|
||||
const exploitTimer = new Timer('phase-4-exploitation');
|
||||
console.log(chalk.red.bold('\n💥 PHASE 4: EXPLOITATION'));
|
||||
|
||||
// Get fresh session data to ensure we have latest vulnerability analysis results
|
||||
const freshSession = await getSession(session.id);
|
||||
if (freshSession) {
|
||||
await runPhase('exploitation', freshSession, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
}
|
||||
|
||||
// Display exploitation summary
|
||||
const finalSession = await getSession(session.id);
|
||||
if (finalSession) {
|
||||
const exploitSummary = calculateExploitationSummary(finalSession);
|
||||
if (exploitSummary.eligibleExploits > 0) {
|
||||
console.log(chalk.blue(`\n🎯 Exploitation Summary: ${exploitSummary.totalAttempts}/${exploitSummary.eligibleExploits} attempted, ${exploitSummary.skippedExploits} skipped (no vulnerabilities)`));
|
||||
} else {
|
||||
console.log(chalk.gray(`\n🎯 Exploitation Summary: No exploitation attempts (no vulnerabilities found)`));
|
||||
}
|
||||
}
|
||||
const exploitResults = await runParallelExploit(
|
||||
sourceDir,
|
||||
variables,
|
||||
distributedConfig,
|
||||
pipelineTestingMode,
|
||||
sessionMetadata
|
||||
);
|
||||
|
||||
const exploitDuration = exploitTimer.stop();
|
||||
timingResults.phases['exploitation'] = exploitDuration;
|
||||
|
||||
console.log(chalk.green(`✅ Exploitation phase complete in ${formatDuration(exploitDuration)}`));
|
||||
}
|
||||
|
||||
// PHASE 5: REPORTING
|
||||
if (startPhase <= 5) {
|
||||
console.log(chalk.greenBright.bold('\n📊 PHASE 5: REPORTING'));
|
||||
console.log(chalk.greenBright('Generating executive summary and assembling final report...'));
|
||||
const reportTimer = new Timer('phase-5-reporting');
|
||||
|
||||
// First, assemble all deliverables into a single concatenated report
|
||||
// Assemble all deliverables into a single concatenated report
|
||||
console.log(chalk.blue('📝 Assembling deliverables from specialist agents...'));
|
||||
|
||||
try {
|
||||
await assembleFinalReport(sourceDir);
|
||||
} catch (error) {
|
||||
@@ -361,62 +717,58 @@ async function main(
|
||||
console.log(chalk.red(`❌ Error assembling final report: ${err.message}`));
|
||||
}
|
||||
|
||||
// Then run reporter agent to create executive summary and clean up hallucinations
|
||||
console.log(chalk.blue('📋 Generating executive summary and cleaning up report...'));
|
||||
await runClaudePromptWithRetry(
|
||||
await loadPrompt('report-executive', variables, distributedConfig, pipelineTestingMode),
|
||||
// Run reporter agent to create executive summary
|
||||
console.log(chalk.blue('Generating executive summary and cleaning up report...'));
|
||||
await runAgent(
|
||||
'report',
|
||||
sourceDir,
|
||||
'*',
|
||||
'',
|
||||
'Executive Summary and Report Cleanup',
|
||||
'report', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: session.id, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
variables,
|
||||
distributedConfig,
|
||||
pipelineTestingMode,
|
||||
sessionMetadata
|
||||
);
|
||||
|
||||
const reportDuration = reportTimer.stop();
|
||||
timingResults.phases['reporting'] = reportDuration;
|
||||
|
||||
console.log(chalk.green(`✅ Final report generated in ${formatDuration(reportDuration)}`));
|
||||
|
||||
// Get the commit hash after successful report generation for checkpoint
|
||||
try {
|
||||
const reportCommitHash = await getGitCommitHash(sourceDir);
|
||||
await updateSessionProgress('report', reportCommitHash);
|
||||
console.log(chalk.gray(` 📍 Report checkpoint saved: ${reportCommitHash.substring(0, 8)}`));
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.log(chalk.yellow(` ⚠️ Failed to save report checkpoint: ${err.message}`));
|
||||
await updateSessionProgress('report'); // Fallback without checkpoint
|
||||
}
|
||||
}
|
||||
// Calculate final timing
|
||||
timingResults.total.stop();
|
||||
|
||||
// Calculate final timing and cost data
|
||||
timingResults.total.stop();
|
||||
// Mark session as completed in both stores
|
||||
await updateSessionStatus(session.id, 'completed');
|
||||
|
||||
// Mark session as completed
|
||||
await updateSession(session.id, {
|
||||
status: 'completed'
|
||||
});
|
||||
// Update audit system's session.json status
|
||||
const auditSession = new AuditSession(sessionMetadata);
|
||||
await auditSession.updateSessionStatus('completed');
|
||||
|
||||
// Display comprehensive timing summary
|
||||
displayTimingSummary();
|
||||
// Display comprehensive timing summary
|
||||
displayTimingSummary();
|
||||
|
||||
console.log(chalk.cyan.bold('\n🎉 PENETRATION TESTING COMPLETE!'));
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
|
||||
// Calculate audit logs path
|
||||
const auditLogsPath = generateAuditPath({ id: session.id, webUrl: session.webUrl, repoPath: session.repoPath, ...(outputPath && { outputPath }) });
|
||||
const auditLogsPath = generateAuditPath(sessionMetadata);
|
||||
|
||||
// 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 {
|
||||
reportPath: path.join(sourceDir, 'deliverables', 'comprehensive_security_assessment_report.md'),
|
||||
auditLogsPath
|
||||
};
|
||||
return {
|
||||
reportPath: path.join(sourceDir, 'deliverables', 'comprehensive_security_assessment_report.md'),
|
||||
auditLogsPath
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
// Mark session as failed in both stores
|
||||
await updateSessionStatus(session.id, 'failed');
|
||||
|
||||
// Update audit system's session.json status
|
||||
const auditSession = new AuditSession(sessionMetadata);
|
||||
await auditSession.updateSessionStatus('failed');
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Entry point - handle both direct node execution and shebang execution
|
||||
@@ -432,8 +784,6 @@ let outputPath: string | null = null;
|
||||
let pipelineTestingMode = false;
|
||||
let disableLoader = false;
|
||||
const nonFlagArgs: string[] = [];
|
||||
let developerCommand: string | null = null;
|
||||
const developerCommands = ['--run-phase', '--run-all', '--rollback-to', '--rerun', '--status', '--list-agents', '--cleanup'];
|
||||
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
if (args[i] === '--config') {
|
||||
@@ -456,24 +806,6 @@ for (let i = 0; i < args.length; i++) {
|
||||
pipelineTestingMode = true;
|
||||
} else if (args[i] === '--disable-loader') {
|
||||
disableLoader = true;
|
||||
} else if (developerCommands.includes(args[i]!)) {
|
||||
developerCommand = args[i]!;
|
||||
// Collect remaining args for the developer command
|
||||
const remainingArgs = args.slice(i + 1).filter(arg => !arg.startsWith('--') || arg === '--pipeline-testing' || arg === '--disable-loader');
|
||||
|
||||
// Check for --pipeline-testing in remaining args
|
||||
if (remainingArgs.includes('--pipeline-testing')) {
|
||||
pipelineTestingMode = true;
|
||||
}
|
||||
|
||||
// Check for --disable-loader in remaining args
|
||||
if (remainingArgs.includes('--disable-loader')) {
|
||||
disableLoader = true;
|
||||
}
|
||||
|
||||
// Add non-flag args (excluding --pipeline-testing and --disable-loader)
|
||||
nonFlagArgs.push(...remainingArgs.filter(arg => arg !== '--pipeline-testing' && arg !== '--disable-loader'));
|
||||
break; // Stop parsing after developer command
|
||||
} else if (!args[i]!.startsWith('-')) {
|
||||
nonFlagArgs.push(args[i]!);
|
||||
}
|
||||
@@ -485,16 +817,6 @@ if (args.includes('--help') || args.includes('-h') || args.includes('help')) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Handle developer commands
|
||||
if (developerCommand) {
|
||||
// Set global flag for loader control in developer mode too
|
||||
global.SHANNON_DISABLE_LOADER = disableLoader;
|
||||
|
||||
await handleDeveloperCommand(developerCommand, nonFlagArgs, pipelineTestingMode, runClaudePromptWithRetry, loadPrompt);
|
||||
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Handle no arguments - show help
|
||||
if (nonFlagArgs.length === 0) {
|
||||
console.log(chalk.red.bold('❌ Error: No arguments provided\n'));
|
||||
|
||||
@@ -51,23 +51,15 @@ export type AgentValidatorMap = Record<AgentName, AgentValidator>;
|
||||
|
||||
export type McpAgentMapping = Record<PromptName, PlaywrightAgent>;
|
||||
|
||||
export type AgentPhase =
|
||||
| 'pre-recon'
|
||||
| 'recon'
|
||||
| 'vuln'
|
||||
| 'exploit'
|
||||
| 'report';
|
||||
|
||||
export interface AgentDefinition {
|
||||
name: AgentName;
|
||||
promptName: PromptName;
|
||||
phase: AgentPhase;
|
||||
dependencies?: AgentName[];
|
||||
}
|
||||
|
||||
export type AgentStatus =
|
||||
| 'pending'
|
||||
| 'in_progress'
|
||||
| 'completed'
|
||||
| 'failed'
|
||||
| 'rolled-back';
|
||||
|
||||
export interface AgentDefinition {
|
||||
name: AgentName;
|
||||
displayName: string;
|
||||
prerequisites: AgentName[];
|
||||
}
|
||||
|
||||
@@ -10,5 +10,4 @@
|
||||
|
||||
export * from './errors.js';
|
||||
export * from './config.js';
|
||||
export * from './session.js';
|
||||
export * from './agents.js';
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* Session type definitions
|
||||
*/
|
||||
|
||||
import type { AgentName, AgentStatus } from './agents.js';
|
||||
|
||||
export type PhaseName =
|
||||
| 'pre-reconnaissance'
|
||||
| 'reconnaissance'
|
||||
| 'vulnerability-analysis'
|
||||
| 'exploitation'
|
||||
| 'reporting';
|
||||
|
||||
export interface AgentInfo {
|
||||
name: AgentName;
|
||||
displayName: string;
|
||||
phase: PhaseName;
|
||||
order: number;
|
||||
prerequisites: AgentName[];
|
||||
}
|
||||
|
||||
export type AgentDefinitions = Record<AgentName, AgentInfo>;
|
||||
|
||||
export type PhaseDefinitions = Record<PhaseName, AgentName[]>;
|
||||
|
||||
export interface AgentState {
|
||||
status: AgentStatus;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
error?: string;
|
||||
attempts?: number;
|
||||
}
|
||||
|
||||
export interface Session {
|
||||
id: string;
|
||||
targetUrl: string;
|
||||
repoPath: string;
|
||||
configPath?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
completedAgents: AgentName[];
|
||||
agentStates: Record<AgentName, AgentState>;
|
||||
checkpoints: Record<AgentName, string>;
|
||||
}
|
||||
|
||||
export interface SessionStore {
|
||||
sessions: Record<string, Session>;
|
||||
}
|
||||
|
||||
export interface SessionSummary {
|
||||
id: string;
|
||||
targetUrl: string;
|
||||
repoPath: string;
|
||||
createdAt: string;
|
||||
completedAgents: number;
|
||||
totalAgents: number;
|
||||
}
|
||||
@@ -30,22 +30,12 @@ export class Timer {
|
||||
}
|
||||
}
|
||||
|
||||
interface TimingResultsPhases {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
interface TimingResultsCommands {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
interface TimingResultsAgents {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
interface TimingResults {
|
||||
total: Timer | null;
|
||||
phases: TimingResultsPhases;
|
||||
commands: TimingResultsCommands;
|
||||
agents: TimingResultsAgents;
|
||||
}
|
||||
|
||||
@@ -61,8 +51,6 @@ interface CostResults {
|
||||
// Global timing and cost tracker
|
||||
export const timingResults: TimingResults = {
|
||||
total: null,
|
||||
phases: {},
|
||||
commands: {},
|
||||
agents: {},
|
||||
};
|
||||
|
||||
@@ -87,44 +75,6 @@ export const displayTimingSummary = (): void => {
|
||||
console.log(chalk.cyan(`📊 Total Execution Time: ${formatDuration(totalDuration)}`));
|
||||
console.log();
|
||||
|
||||
// Phase breakdown
|
||||
if (Object.keys(timingResults.phases).length > 0) {
|
||||
console.log(chalk.yellow.bold('🔍 Phase Breakdown:'));
|
||||
let phaseTotal = 0;
|
||||
for (const [phase, duration] of Object.entries(timingResults.phases)) {
|
||||
const percentage = ((duration / totalDuration) * 100).toFixed(1);
|
||||
console.log(
|
||||
chalk.yellow(` ${phase.padEnd(20)} ${formatDuration(duration).padStart(8)} (${percentage}%)`)
|
||||
);
|
||||
phaseTotal += duration;
|
||||
}
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` ${'Phases Total'.padEnd(20)} ${formatDuration(phaseTotal).padStart(8)} (${((phaseTotal / totalDuration) * 100).toFixed(1)}%)`
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Command breakdown
|
||||
if (Object.keys(timingResults.commands).length > 0) {
|
||||
console.log(chalk.blue.bold('🖥️ Command Breakdown:'));
|
||||
let commandTotal = 0;
|
||||
for (const [command, duration] of Object.entries(timingResults.commands)) {
|
||||
const percentage = ((duration / totalDuration) * 100).toFixed(1);
|
||||
console.log(
|
||||
chalk.blue(` ${command.padEnd(20)} ${formatDuration(duration).padStart(8)} (${percentage}%)`)
|
||||
);
|
||||
commandTotal += duration;
|
||||
}
|
||||
console.log(
|
||||
chalk.gray(
|
||||
` ${'Commands Total'.padEnd(20)} ${formatDuration(commandTotal).padStart(8)} (${((commandTotal / totalDuration) * 100).toFixed(1)}%)`
|
||||
)
|
||||
);
|
||||
console.log();
|
||||
}
|
||||
|
||||
// Agent breakdown
|
||||
if (Object.keys(timingResults.agents).length > 0) {
|
||||
console.log(chalk.magenta.bold('🤖 Agent Breakdown:'));
|
||||
|
||||
Reference in New Issue
Block a user