mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-05-27 19:02:38 +02:00
refactor: remove ~750 lines of dead code across 12 files
- Delete 4 dead files: pre-recon.ts, tool-checker.ts, input-validator.ts, environment.ts - Remove runClaudePromptWithRetry() and its now-unused imports from claude-executor.ts - De-export unused symbols: AGENT_ORDER, getParallelGroups, logError, isRouterMode, showHelp, displayTimingSummary - De-export unused types: ProcessingState, ProcessingResult, SdkMessage, MessageDispatchResult, MessageDispatchContext - Remove dead import (path from zx) in session-manager.ts and deprecated comment in config.ts
This commit is contained in:
+1
-152
@@ -10,10 +10,9 @@ import { fs, path } from 'zx';
|
||||
import chalk, { type ChalkInstance } from 'chalk';
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
import { isRetryableError, getRetryDelay, PentestError } from '../error-handling.js';
|
||||
import { isRetryableError, PentestError } from '../error-handling.js';
|
||||
import { timingResults, Timer } from '../utils/metrics.js';
|
||||
import { formatTimestamp } from '../utils/formatting.js';
|
||||
import { createGitCheckpoint, commitGitSuccess, rollbackGitWorkspace, getGitCommitHash } from '../utils/git-manager.js';
|
||||
import { AGENT_VALIDATORS, MCP_AGENT_MAPPING } from '../constants.js';
|
||||
import { AuditSession } from '../audit/index.js';
|
||||
import { createShannonHelperServer } from '../../mcp-server/dist/index.js';
|
||||
@@ -403,153 +402,3 @@ async function processMessageStream(
|
||||
|
||||
return { turnCount, result, apiErrorDetected, cost, model };
|
||||
}
|
||||
|
||||
// Main entry point for agent execution. Handles retries, git checkpoints, and validation.
|
||||
export async function runClaudePromptWithRetry(
|
||||
prompt: string,
|
||||
sourceDir: string,
|
||||
_allowedTools: string = 'Read',
|
||||
context: string = '',
|
||||
description: string = 'Claude analysis',
|
||||
agentName: string | null = null,
|
||||
colorFn: ChalkInstance = chalk.cyan,
|
||||
sessionMetadata: SessionMetadata | null = null
|
||||
): Promise<ClaudePromptResult> {
|
||||
const maxRetries = 3;
|
||||
let lastError: Error | undefined;
|
||||
let retryContext = context;
|
||||
|
||||
console.log(chalk.cyan(`Starting ${description} with ${maxRetries} max attempts`));
|
||||
|
||||
let auditSession: AuditSession | null = null;
|
||||
if (sessionMetadata && agentName) {
|
||||
auditSession = new AuditSession(sessionMetadata);
|
||||
await auditSession.initialize();
|
||||
}
|
||||
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
await createGitCheckpoint(sourceDir, description, attempt);
|
||||
|
||||
if (auditSession && agentName) {
|
||||
const fullPrompt = retryContext ? `${retryContext}\n\n${prompt}` : prompt;
|
||||
await auditSession.startAgent(agentName, fullPrompt, attempt);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await runClaudePrompt(
|
||||
prompt, sourceDir, retryContext,
|
||||
description, agentName, colorFn, sessionMetadata, auditSession, attempt
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
const validationPassed = await validateAgentOutput(result, agentName, sourceDir);
|
||||
|
||||
if (validationPassed) {
|
||||
if (result.apiErrorDetected) {
|
||||
console.log(chalk.yellow(`Validation: Ready for exploitation despite API error warnings`));
|
||||
}
|
||||
|
||||
if (auditSession && agentName) {
|
||||
const commitHash = await getGitCommitHash(sourceDir);
|
||||
const endResult: {
|
||||
attemptNumber: number;
|
||||
duration_ms: number;
|
||||
cost_usd: number;
|
||||
success: true;
|
||||
checkpoint?: string;
|
||||
} = {
|
||||
attemptNumber: attempt,
|
||||
duration_ms: result.duration,
|
||||
cost_usd: result.cost || 0,
|
||||
success: true,
|
||||
};
|
||||
if (commitHash) {
|
||||
endResult.checkpoint = commitHash;
|
||||
}
|
||||
await auditSession.endAgent(agentName, endResult);
|
||||
}
|
||||
|
||||
await commitGitSuccess(sourceDir, description);
|
||||
console.log(chalk.green.bold(`${description} completed successfully on attempt ${attempt}/${maxRetries}`));
|
||||
return result;
|
||||
// Validation failure is retryable - agent might succeed on retry with cleaner workspace
|
||||
} else {
|
||||
console.log(chalk.yellow(`${description} completed but output validation failed`));
|
||||
|
||||
if (auditSession && agentName) {
|
||||
await auditSession.endAgent(agentName, {
|
||||
attemptNumber: attempt,
|
||||
duration_ms: result.duration,
|
||||
cost_usd: result.partialCost || result.cost || 0,
|
||||
success: false,
|
||||
error: 'Output validation failed',
|
||||
isFinalAttempt: attempt === maxRetries
|
||||
});
|
||||
}
|
||||
|
||||
if (result.apiErrorDetected) {
|
||||
console.log(chalk.yellow(`API Error detected with validation failure - treating as retryable`));
|
||||
lastError = new Error('API Error: terminated with validation failure');
|
||||
} else {
|
||||
lastError = new Error('Output validation failed');
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await rollbackGitWorkspace(sourceDir, 'validation failure');
|
||||
continue;
|
||||
} else {
|
||||
throw new PentestError(
|
||||
`Agent ${description} failed output validation after ${maxRetries} attempts. Required deliverable files were not created.`,
|
||||
'validation',
|
||||
false,
|
||||
{ description, sourceDir, attemptsExhausted: maxRetries }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error & { duration?: number; cost?: number; partialResults?: unknown };
|
||||
lastError = err;
|
||||
|
||||
if (auditSession && agentName) {
|
||||
await auditSession.endAgent(agentName, {
|
||||
attemptNumber: attempt,
|
||||
duration_ms: err.duration || 0,
|
||||
cost_usd: err.cost || 0,
|
||||
success: false,
|
||||
error: err.message,
|
||||
isFinalAttempt: attempt === maxRetries
|
||||
});
|
||||
}
|
||||
|
||||
if (!isRetryableError(err)) {
|
||||
console.log(chalk.red(`${description} failed with non-retryable error: ${err.message}`));
|
||||
await rollbackGitWorkspace(sourceDir, 'non-retryable error cleanup');
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (attempt < maxRetries) {
|
||||
await rollbackGitWorkspace(sourceDir, 'retryable error cleanup');
|
||||
|
||||
const delay = getRetryDelay(err, attempt);
|
||||
const delaySeconds = (delay / 1000).toFixed(1);
|
||||
console.log(chalk.yellow(`${description} failed (attempt ${attempt}/${maxRetries})`));
|
||||
console.log(chalk.gray(` Error: ${err.message}`));
|
||||
console.log(chalk.gray(` Workspace rolled back, retrying in ${delaySeconds}s...`));
|
||||
|
||||
if (err.partialResults) {
|
||||
retryContext = `${context}\n\nPrevious partial results: ${JSON.stringify(err.partialResults)}`;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
} else {
|
||||
await rollbackGitWorkspace(sourceDir, 'final failure cleanup');
|
||||
console.log(chalk.red(`${description} failed after ${maxRetries} attempts`));
|
||||
console.log(chalk.red(` Final error: ${err.message}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
@@ -26,9 +26,3 @@ export function getActualModelName(sdkReportedModel?: string): string | undefine
|
||||
return sdkReportedModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if router mode is active.
|
||||
*/
|
||||
export function isRouterMode(): boolean {
|
||||
return !!process.env.ANTHROPIC_BASE_URL && !!process.env.ROUTER_DEFAULT;
|
||||
}
|
||||
|
||||
+5
-5
@@ -13,7 +13,7 @@ export interface ExecutionContext {
|
||||
agentKey: string;
|
||||
}
|
||||
|
||||
export interface ProcessingState {
|
||||
interface ProcessingState {
|
||||
turnCount: number;
|
||||
result: string | null;
|
||||
apiErrorDetected: boolean;
|
||||
@@ -22,7 +22,7 @@ export interface ProcessingState {
|
||||
lastHeartbeat: number;
|
||||
}
|
||||
|
||||
export interface ProcessingResult {
|
||||
interface ProcessingResult {
|
||||
result: string | null;
|
||||
turnCount: number;
|
||||
apiErrorDetected: boolean;
|
||||
@@ -111,7 +111,7 @@ export interface ApiErrorDetection {
|
||||
}
|
||||
|
||||
// Message types from SDK stream
|
||||
export type SdkMessage =
|
||||
type SdkMessage =
|
||||
| AssistantMessage
|
||||
| ResultMessage
|
||||
| ToolUseMessage
|
||||
@@ -132,12 +132,12 @@ export interface UserMessage {
|
||||
}
|
||||
|
||||
// Dispatch result types for message processing
|
||||
export type MessageDispatchResult =
|
||||
type MessageDispatchResult =
|
||||
| { action: 'continue' }
|
||||
| { action: 'break'; result: string | null; cost: number }
|
||||
| { action: 'throw'; error: Error };
|
||||
|
||||
export interface MessageDispatchContext {
|
||||
interface MessageDispatchContext {
|
||||
turnCount: number;
|
||||
execContext: ExecutionContext;
|
||||
description: string;
|
||||
|
||||
@@ -1,59 +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';
|
||||
|
||||
interface ValidationResult {
|
||||
valid: boolean;
|
||||
error?: string;
|
||||
path?: string;
|
||||
}
|
||||
|
||||
// Helper function: Validate web URL
|
||||
export function validateWebUrl(url: string): ValidationResult {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
return { valid: false, error: 'Web URL must use HTTP or HTTPS protocol' };
|
||||
}
|
||||
if (!parsed.hostname) {
|
||||
return { valid: false, error: 'Web URL must have a valid hostname' };
|
||||
}
|
||||
return { valid: true };
|
||||
} catch {
|
||||
return { valid: false, error: 'Invalid web URL format' };
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function: Validate local repository path
|
||||
export async function validateRepoPath(repoPath: string): Promise<ValidationResult> {
|
||||
try {
|
||||
// Check if path exists
|
||||
if (!(await fs.pathExists(repoPath))) {
|
||||
return { valid: false, error: 'Repository path does not exist' };
|
||||
}
|
||||
|
||||
// Check if it's a directory
|
||||
const stats = await fs.stat(repoPath);
|
||||
if (!stats.isDirectory()) {
|
||||
return { valid: false, error: 'Repository path must be a directory' };
|
||||
}
|
||||
|
||||
// Check if it's readable
|
||||
try {
|
||||
await fs.access(repoPath, fs.constants.R_OK);
|
||||
} catch {
|
||||
return { valid: false, error: 'Repository path is not readable' };
|
||||
}
|
||||
|
||||
// Convert to absolute path
|
||||
const absolutePath = path.resolve(repoPath);
|
||||
return { valid: true, path: absolutePath };
|
||||
} catch (error) {
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
return { valid: false, error: `Invalid repository path: ${errMsg}` };
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -8,7 +8,7 @@ import chalk from 'chalk';
|
||||
import { displaySplashScreen } from '../splash-screen.js';
|
||||
|
||||
// Helper function: Display help information
|
||||
export function showHelp(): void {
|
||||
function showHelp(): void {
|
||||
console.log(chalk.cyan.bold('AI Penetration Testing Agent'));
|
||||
console.log(chalk.gray('Automated security assessment tool\n'));
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ export class PentestError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
// Centralized error logging function
|
||||
export async function logError(
|
||||
async function logError(
|
||||
error: Error & { type?: PentestErrorType; retryable?: boolean; context?: PentestErrorContext },
|
||||
contextMsg: string,
|
||||
sourceDir: string | null = null
|
||||
|
||||
@@ -1,381 +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 from 'chalk';
|
||||
import { Timer } from '../utils/metrics.js';
|
||||
import { formatDuration } from '../utils/formatting.js';
|
||||
import { handleToolError, PentestError } from '../error-handling.js';
|
||||
import { AGENTS } from '../session-manager.js';
|
||||
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';
|
||||
|
||||
interface AgentResult {
|
||||
success: boolean;
|
||||
duration: number;
|
||||
cost?: number | undefined;
|
||||
error?: string | undefined;
|
||||
retryable?: boolean | undefined;
|
||||
}
|
||||
|
||||
type ToolName = 'nmap' | 'subfinder' | 'whatweb' | 'schemathesis';
|
||||
type ToolStatus = 'success' | 'skipped' | 'error';
|
||||
|
||||
interface TerminalScanResult {
|
||||
tool: ToolName;
|
||||
output: string;
|
||||
status: ToolStatus;
|
||||
duration: number;
|
||||
success?: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
interface PromptVariables {
|
||||
webUrl: string;
|
||||
repoPath: string;
|
||||
}
|
||||
|
||||
// Discriminated union for Wave1 tool results - clearer than loose union types
|
||||
type Wave1ToolResult =
|
||||
| { kind: 'scan'; result: TerminalScanResult }
|
||||
| { kind: 'skipped'; message: string }
|
||||
| { kind: 'agent'; result: AgentResult };
|
||||
|
||||
interface Wave1Results {
|
||||
nmap: Wave1ToolResult;
|
||||
subfinder: Wave1ToolResult;
|
||||
whatweb: Wave1ToolResult;
|
||||
naabu?: Wave1ToolResult;
|
||||
codeAnalysis: AgentResult;
|
||||
}
|
||||
|
||||
interface Wave2Results {
|
||||
schemathesis: TerminalScanResult;
|
||||
}
|
||||
|
||||
interface PreReconResult {
|
||||
duration: number;
|
||||
report: string;
|
||||
}
|
||||
|
||||
// Runs external security tools (nmap, whatweb, etc). Schemathesis requires schemas from code analysis.
|
||||
async function runTerminalScan(tool: ToolName, target: string, sourceDir: string | null = null): Promise<TerminalScanResult> {
|
||||
const timer = new Timer(`command-${tool}`);
|
||||
try {
|
||||
let result;
|
||||
switch (tool) {
|
||||
case 'nmap': {
|
||||
console.log(chalk.blue(` 🔍 Running ${tool} scan...`));
|
||||
const nmapHostname = new URL(target).hostname;
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`nmap -sV -sC ${nmapHostname}`;
|
||||
const duration = timer.stop();
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(duration)}`));
|
||||
return { tool: 'nmap', output: result.stdout, status: 'success', duration };
|
||||
}
|
||||
case 'subfinder': {
|
||||
console.log(chalk.blue(` 🔍 Running ${tool} scan...`));
|
||||
const hostname = new URL(target).hostname;
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`subfinder -d ${hostname}`;
|
||||
const subfinderDuration = timer.stop();
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(subfinderDuration)}`));
|
||||
return { tool: 'subfinder', output: result.stdout, status: 'success', duration: subfinderDuration };
|
||||
}
|
||||
case 'whatweb': {
|
||||
console.log(chalk.blue(` 🔍 Running ${tool} scan...`));
|
||||
const command = `whatweb --open-timeout 30 --read-timeout 60 ${target}`;
|
||||
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();
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(whatwebDuration)}`));
|
||||
return { tool: 'whatweb', output: result.stdout, status: 'success', duration: whatwebDuration };
|
||||
}
|
||||
case 'schemathesis': {
|
||||
// Schemathesis depends on code analysis output - skip if no schemas found
|
||||
const schemasDir = path.join(sourceDir || '.', 'outputs', 'schemas');
|
||||
if (await fs.pathExists(schemasDir)) {
|
||||
const schemaFiles = await fs.readdir(schemasDir) as string[];
|
||||
const apiSchemas = schemaFiles.filter((f: string) => f.endsWith('.json') || f.endsWith('.yml') || f.endsWith('.yaml'));
|
||||
if (apiSchemas.length > 0) {
|
||||
console.log(chalk.blue(` 🔍 Running ${tool} scan...`));
|
||||
const allResults: string[] = [];
|
||||
|
||||
// Run schemathesis on each schema file
|
||||
for (const schemaFile of apiSchemas) {
|
||||
const schemaPath = path.join(schemasDir, schemaFile);
|
||||
try {
|
||||
result = await $({ silent: true, stdio: ['ignore', 'pipe', 'ignore'] })`schemathesis run ${schemaPath} -u ${target} --max-failures=5`;
|
||||
allResults.push(`Schema: ${schemaFile}\n${result.stdout}`);
|
||||
} catch (schemaError) {
|
||||
const err = schemaError as { stdout?: string; message?: string };
|
||||
allResults.push(`Schema: ${schemaFile}\nError: ${err.stdout || err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const schemaDuration = timer.stop();
|
||||
console.log(chalk.green(` ✅ ${tool} completed in ${formatDuration(schemaDuration)}`));
|
||||
return { tool: 'schemathesis', output: allResults.join('\n\n'), status: 'success', duration: schemaDuration };
|
||||
} else {
|
||||
console.log(chalk.gray(` ⏭️ ${tool} - no API schemas found`));
|
||||
return { tool: 'schemathesis', output: 'No API schemas found', status: 'skipped', duration: timer.stop() };
|
||||
}
|
||||
} else {
|
||||
console.log(chalk.gray(` ⏭️ ${tool} - schemas directory not found`));
|
||||
return { tool: 'schemathesis', output: 'Schemas directory not found', status: 'skipped', duration: timer.stop() };
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${tool}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const duration = timer.stop();
|
||||
console.log(chalk.red(` ❌ ${tool} failed in ${formatDuration(duration)}`));
|
||||
return handleToolError(tool, error as Error & { code?: string }) as TerminalScanResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Wave 1: Initial footprinting + authentication
|
||||
async function runPreReconWave1(
|
||||
webUrl: string,
|
||||
sourceDir: string,
|
||||
variables: PromptVariables,
|
||||
config: DistributedConfig | null,
|
||||
pipelineTestingMode: boolean = false,
|
||||
sessionId: string | null = null,
|
||||
outputPath: string | null = null
|
||||
): Promise<Wave1Results> {
|
||||
console.log(chalk.blue(' → Launching Wave 1 operations in parallel...'));
|
||||
|
||||
const operations: Promise<TerminalScanResult | AgentResult>[] = [];
|
||||
|
||||
const skippedResult = (message: string): Wave1ToolResult => ({ kind: 'skipped', message });
|
||||
|
||||
// Skip external commands in pipeline testing mode
|
||||
if (pipelineTestingMode) {
|
||||
console.log(chalk.gray(' ⏭️ Skipping external tools (pipeline testing mode)'));
|
||||
operations.push(
|
||||
runClaudePromptWithRetry(
|
||||
await loadPrompt('pre-recon-code', variables, null, pipelineTestingMode),
|
||||
sourceDir,
|
||||
'*',
|
||||
'',
|
||||
AGENTS['pre-recon'].displayName,
|
||||
'pre-recon', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: sessionId!, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
)
|
||||
);
|
||||
const [codeAnalysis] = await Promise.all(operations);
|
||||
return {
|
||||
nmap: skippedResult('Skipped (pipeline testing mode)'),
|
||||
subfinder: skippedResult('Skipped (pipeline testing mode)'),
|
||||
whatweb: skippedResult('Skipped (pipeline testing mode)'),
|
||||
codeAnalysis: codeAnalysis as AgentResult
|
||||
};
|
||||
} else {
|
||||
operations.push(
|
||||
runTerminalScan('nmap', webUrl),
|
||||
runTerminalScan('subfinder', webUrl),
|
||||
runTerminalScan('whatweb', webUrl),
|
||||
runClaudePromptWithRetry(
|
||||
await loadPrompt('pre-recon-code', variables, null, pipelineTestingMode),
|
||||
sourceDir,
|
||||
'*',
|
||||
'',
|
||||
AGENTS['pre-recon'].displayName,
|
||||
'pre-recon', // Agent name for snapshot creation
|
||||
chalk.cyan,
|
||||
{ id: sessionId!, webUrl, repoPath: sourceDir, ...(outputPath && { outputPath }) } // Session metadata for audit logging (STANDARD: use 'id' field)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if authentication config is provided for login instructions injection
|
||||
console.log(chalk.gray(` → Config check: ${config ? 'present' : 'missing'}, Auth: ${config?.authentication ? 'present' : 'missing'}`));
|
||||
|
||||
const [nmap, subfinder, whatweb, codeAnalysis] = await Promise.all(operations);
|
||||
|
||||
return {
|
||||
nmap: { kind: 'scan', result: nmap as TerminalScanResult },
|
||||
subfinder: { kind: 'scan', result: subfinder as TerminalScanResult },
|
||||
whatweb: { kind: 'scan', result: whatweb as TerminalScanResult },
|
||||
codeAnalysis: codeAnalysis as AgentResult
|
||||
};
|
||||
}
|
||||
|
||||
// Wave 2: Additional scanning
|
||||
async function runPreReconWave2(
|
||||
webUrl: string,
|
||||
sourceDir: string,
|
||||
toolAvailability: ToolAvailability,
|
||||
pipelineTestingMode: boolean = false
|
||||
): Promise<Wave2Results> {
|
||||
console.log(chalk.blue(' → Running Wave 2 additional scans in parallel...'));
|
||||
|
||||
// Skip external commands in pipeline testing mode
|
||||
if (pipelineTestingMode) {
|
||||
console.log(chalk.gray(' ⏭️ Skipping external tools (pipeline testing mode)'));
|
||||
return {
|
||||
schemathesis: { tool: 'schemathesis', output: 'Skipped (pipeline testing mode)', status: 'skipped', duration: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
const operations: Promise<TerminalScanResult>[] = [];
|
||||
|
||||
// Parallel additional scans (only run if tools are available)
|
||||
|
||||
if (toolAvailability.schemathesis) {
|
||||
operations.push(runTerminalScan('schemathesis', webUrl, sourceDir));
|
||||
}
|
||||
|
||||
// If no tools are available, return early
|
||||
if (operations.length === 0) {
|
||||
console.log(chalk.gray(' ⏭️ No Wave 2 tools available'));
|
||||
return {
|
||||
schemathesis: { tool: 'schemathesis', output: 'Tool not available', status: 'skipped', duration: 0 }
|
||||
};
|
||||
}
|
||||
|
||||
// Run all operations in parallel
|
||||
const results = await Promise.all(operations);
|
||||
|
||||
// Map results back to named properties
|
||||
const response: Wave2Results = {
|
||||
schemathesis: { tool: 'schemathesis', output: 'Tool not available', status: 'skipped', duration: 0 }
|
||||
};
|
||||
let resultIndex = 0;
|
||||
|
||||
if (toolAvailability.schemathesis) {
|
||||
response.schemathesis = results[resultIndex++]!;
|
||||
} else {
|
||||
console.log(chalk.gray(' ⏭️ schemathesis - tool not available'));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Extracts status and output from a Wave1 tool result
|
||||
function extractResult(r: Wave1ToolResult | undefined): { status: string; output: string } {
|
||||
if (!r) return { status: 'Skipped', output: 'No output' };
|
||||
switch (r.kind) {
|
||||
case 'scan':
|
||||
return { status: r.result.status || 'Skipped', output: r.result.output || 'No output' };
|
||||
case 'skipped':
|
||||
return { status: 'Skipped', output: r.message };
|
||||
case 'agent':
|
||||
return { status: r.result.success ? 'success' : 'error', output: 'See agent output' };
|
||||
}
|
||||
}
|
||||
|
||||
// Combines tool outputs into single deliverable. Falls back to reference if file missing.
|
||||
async function stitchPreReconOutputs(wave1: Wave1Results, additionalScans: TerminalScanResult[], sourceDir: string): Promise<string> {
|
||||
// Try to read the code analysis deliverable file
|
||||
let codeAnalysisContent = 'No analysis available';
|
||||
try {
|
||||
const codeAnalysisPath = path.join(sourceDir, 'deliverables', 'code_analysis_deliverable.md');
|
||||
codeAnalysisContent = await fs.readFile(codeAnalysisPath, 'utf8');
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
console.log(chalk.yellow(`⚠️ Could not read code analysis deliverable: ${err.message}`));
|
||||
codeAnalysisContent = 'Analysis located in deliverables/code_analysis_deliverable.md';
|
||||
}
|
||||
|
||||
// Build additional scans section
|
||||
let additionalSection = '';
|
||||
if (additionalScans.length > 0) {
|
||||
additionalSection = '\n## Authenticated Scans\n';
|
||||
for (const scan of additionalScans) {
|
||||
additionalSection += `
|
||||
### ${scan.tool.toUpperCase()}
|
||||
Status: ${scan.status}
|
||||
${scan.output}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const nmap = extractResult(wave1.nmap);
|
||||
const subfinder = extractResult(wave1.subfinder);
|
||||
const whatweb = extractResult(wave1.whatweb);
|
||||
const naabu = extractResult(wave1.naabu);
|
||||
|
||||
const report = `
|
||||
# Pre-Reconnaissance Report
|
||||
|
||||
## Port Discovery (naabu)
|
||||
Status: ${naabu.status}
|
||||
${naabu.output}
|
||||
|
||||
## Network Scanning (nmap)
|
||||
Status: ${nmap.status}
|
||||
${nmap.output}
|
||||
|
||||
## Subdomain Discovery (subfinder)
|
||||
Status: ${subfinder.status}
|
||||
${subfinder.output}
|
||||
|
||||
## Technology Detection (whatweb)
|
||||
Status: ${whatweb.status}
|
||||
${whatweb.output}
|
||||
## Code Analysis
|
||||
${codeAnalysisContent}
|
||||
${additionalSection}
|
||||
---
|
||||
Report generated at: ${new Date().toISOString()}
|
||||
`.trim();
|
||||
|
||||
// Ensure deliverables directory exists in the cloned repo
|
||||
try {
|
||||
const deliverablePath = path.join(sourceDir, 'deliverables', 'pre_recon_deliverable.md');
|
||||
await fs.ensureDir(path.join(sourceDir, 'deliverables'));
|
||||
|
||||
// Write to file in the cloned repository
|
||||
await fs.writeFile(deliverablePath, report);
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
throw new PentestError(
|
||||
`Failed to write pre-recon report: ${err.message}`,
|
||||
'filesystem',
|
||||
false,
|
||||
{ sourceDir, originalError: err.message }
|
||||
);
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
// Main pre-recon phase execution function
|
||||
export async function executePreReconPhase(
|
||||
webUrl: string,
|
||||
sourceDir: string,
|
||||
variables: PromptVariables,
|
||||
config: DistributedConfig | null,
|
||||
toolAvailability: ToolAvailability,
|
||||
pipelineTestingMode: boolean,
|
||||
sessionId: string | null = null,
|
||||
outputPath: string | null = null
|
||||
): Promise<PreReconResult> {
|
||||
console.log(chalk.yellow.bold('\n🔍 PHASE 1: PRE-RECONNAISSANCE'));
|
||||
const timer = new Timer('phase-1-pre-recon');
|
||||
|
||||
console.log(chalk.yellow('Wave 1: Initial footprinting...'));
|
||||
const wave1Results = await runPreReconWave1(webUrl, sourceDir, variables, config, pipelineTestingMode, sessionId, outputPath);
|
||||
console.log(chalk.green(' ✅ Wave 1 operations completed'));
|
||||
|
||||
console.log(chalk.yellow('Wave 2: Additional scanning...'));
|
||||
const wave2Results = await runPreReconWave2(webUrl, sourceDir, toolAvailability, pipelineTestingMode);
|
||||
console.log(chalk.green(' ✅ Wave 2 operations completed'));
|
||||
|
||||
console.log(chalk.blue('📝 Stitching pre-recon outputs...'));
|
||||
const additionalScans = wave2Results.schemathesis ? [wave2Results.schemathesis] : [];
|
||||
const preReconReport = await stitchPreReconOutputs(wave1Results, additionalScans, sourceDir);
|
||||
const duration = timer.stop();
|
||||
|
||||
console.log(chalk.green(`✅ Pre-reconnaissance complete in ${formatDuration(duration)}`));
|
||||
console.log(chalk.green(`💾 Saved to ${sourceDir}/deliverables/pre_recon_deliverable.md`));
|
||||
|
||||
return { duration, report: preReconReport };
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
import { path } from 'zx';
|
||||
import type { AgentName } from './types/index.js';
|
||||
|
||||
// Agent definition interface
|
||||
@@ -83,29 +82,6 @@ export const AGENTS: Readonly<Record<AgentName, AgentDefinition>> = Object.freez
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
// 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']
|
||||
});
|
||||
|
||||
// Phase names for metrics aggregation
|
||||
export type PhaseName = 'pre-recon' | 'recon' | 'vulnerability-analysis' | 'exploitation' | 'reporting';
|
||||
|
||||
|
||||
@@ -1,56 +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 from 'chalk';
|
||||
import { PentestError } from '../error-handling.js';
|
||||
|
||||
// Pure function: Setup local repository for testing
|
||||
export async function setupLocalRepo(repoPath: string): Promise<string> {
|
||||
try {
|
||||
const sourceDir = path.resolve(repoPath);
|
||||
|
||||
// MCP servers are now configured via mcpServers option in claude-executor.js
|
||||
// No need for pre-setup with claude CLI
|
||||
|
||||
// Initialize git repository if not already initialized and create checkpoint
|
||||
try {
|
||||
// Check if it's already a git repository
|
||||
const isGitRepo = await fs.pathExists(path.join(sourceDir, '.git'));
|
||||
|
||||
if (!isGitRepo) {
|
||||
await $`cd ${sourceDir} && git init`;
|
||||
console.log(chalk.blue('✅ Git repository initialized'));
|
||||
}
|
||||
|
||||
// Configure git for pentest agent
|
||||
await $`cd ${sourceDir} && git config user.name "Pentest Agent"`;
|
||||
await $`cd ${sourceDir} && git config user.email "agent@localhost"`;
|
||||
|
||||
// Create initial checkpoint
|
||||
await $`cd ${sourceDir} && git add -A && git commit -m "Initial checkpoint: Local repository setup" --allow-empty`;
|
||||
console.log(chalk.green('✅ Initial checkpoint created'));
|
||||
} catch (gitError) {
|
||||
const errMsg = gitError instanceof Error ? gitError.message : String(gitError);
|
||||
console.log(chalk.yellow(`⚠️ Git setup warning: ${errMsg}`));
|
||||
// Non-fatal - continue without Git setup
|
||||
}
|
||||
|
||||
// MCP tools (save_deliverable, generate_totp) are now available natively via shannon-helper MCP server
|
||||
// No need to copy bash scripts to target repository
|
||||
|
||||
return sourceDir;
|
||||
} catch (error) {
|
||||
if (error instanceof PentestError) {
|
||||
throw error;
|
||||
}
|
||||
const errMsg = error instanceof Error ? error.message : String(error);
|
||||
throw new PentestError(`Local repository setup failed: ${errMsg}`, 'filesystem', false, {
|
||||
repoPath,
|
||||
originalError: errMsg,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,66 +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 { $ } from 'zx';
|
||||
import chalk from 'chalk';
|
||||
|
||||
type ToolName = 'nmap' | 'subfinder' | 'whatweb' | 'schemathesis';
|
||||
|
||||
export type ToolAvailability = Record<ToolName, boolean>;
|
||||
|
||||
// Check availability of required tools
|
||||
export const checkToolAvailability = async (): Promise<ToolAvailability> => {
|
||||
const tools: ToolName[] = ['nmap', 'subfinder', 'whatweb', 'schemathesis'];
|
||||
const availability: ToolAvailability = {
|
||||
nmap: false,
|
||||
subfinder: false,
|
||||
whatweb: false,
|
||||
schemathesis: false
|
||||
};
|
||||
|
||||
console.log(chalk.blue('🔧 Checking tool availability...'));
|
||||
|
||||
for (const tool of tools) {
|
||||
try {
|
||||
await $`command -v ${tool}`;
|
||||
availability[tool] = true;
|
||||
console.log(chalk.green(` ✅ ${tool} - available`));
|
||||
} catch {
|
||||
availability[tool] = false;
|
||||
console.log(chalk.yellow(` ⚠️ ${tool} - not found`));
|
||||
}
|
||||
}
|
||||
|
||||
return availability;
|
||||
};
|
||||
|
||||
// Handle missing tools with user-friendly messages
|
||||
export const handleMissingTools = (toolAvailability: ToolAvailability): ToolName[] => {
|
||||
const missing = (Object.entries(toolAvailability) as Array<[ToolName, boolean]>)
|
||||
.filter(([, available]) => !available)
|
||||
.map(([tool]) => tool);
|
||||
|
||||
if (missing.length > 0) {
|
||||
console.log(chalk.yellow(`\n⚠️ Missing tools: ${missing.join(', ')}`));
|
||||
console.log(chalk.gray('Some functionality will be limited. Install missing tools for full capability.'));
|
||||
|
||||
// Provide installation hints
|
||||
const installHints: Record<ToolName, string> = {
|
||||
'nmap': 'brew install nmap (macOS) or apt install nmap (Ubuntu)',
|
||||
'subfinder': 'go install -v github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest',
|
||||
'whatweb': 'gem install whatweb',
|
||||
'schemathesis': 'pip install schemathesis'
|
||||
};
|
||||
|
||||
console.log(chalk.gray('\nInstallation hints:'));
|
||||
missing.forEach(tool => {
|
||||
console.log(chalk.gray(` ${tool}: ${installHints[tool]}`));
|
||||
});
|
||||
console.log('');
|
||||
}
|
||||
|
||||
return missing;
|
||||
};
|
||||
+1
-1
@@ -53,7 +53,7 @@ export interface Authentication {
|
||||
export interface Config {
|
||||
rules?: Rules;
|
||||
authentication?: Authentication;
|
||||
login?: unknown; // Deprecated
|
||||
login?: unknown;
|
||||
}
|
||||
|
||||
export interface DistributedConfig {
|
||||
|
||||
@@ -60,7 +60,7 @@ export const costResults: CostResults = {
|
||||
};
|
||||
|
||||
// Function to display comprehensive timing summary
|
||||
export const displayTimingSummary = (): void => {
|
||||
const displayTimingSummary = (): void => {
|
||||
if (!timingResults.total) {
|
||||
console.log(chalk.yellow('No timing data available'));
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user