项目文件夹

文件
Gelei Deng e238d701f2 feat: 🎸 version 1.0 agentic workflow (#325)
* feat: 🎸 version 1.0 agentic workflow

Major rewrite of PentestGPT to use an agentic pipeline architecture:
Core Changes: - New event-driven architecture with EventBus for
TUI-agent decoupling - Implemented AgentController with 5-state
lifecycle (IDLE->RUNNING->PAUSED->COMPLETED->ERROR) - Added AgentBackend
interface with ClaudeCodeBackend implementation - Session management
with file-based persistence for resumable pentests - Langfuse
integration for observability and tracing Interface: - New Textual-based
TUI with real-time activity feed - Keyboard shortcuts: F1 help, Ctrl+P
pause, Ctrl+Q quit - Enhanced CLI with --target, --instruction,
--non-interactive, --debug flags Project Structure: - Moved legacy
multi-LLM version (v0.15) to legacy/ directory - New pentestgpt/core/
for agent, controller, events, session modules - New
pentestgpt/interface/ for TUI and CLI components - New
pentestgpt/benchmark/ for xbow benchmark integration - Comprehensive
test suite in tests/ with unit and integration tests DevOps: - Docker
support with Ubuntu 24.04 container - GitHub Actions CI/CD pipeline -
Makefile with dev commands (test, lint, format, typecheck) - Added
xbow-validation-benchmarks as submodule

* style: format code with Black

This commit fixes the style issues introduced in abe3be0 according to the output
from Black.

Details: https://github.com/GreyDGL/PentestGPT/pull/325

* fix: 🐛 fix test pipeline

* feat: 🎸 update format

* feat: 🎸 update

---------

Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
2025-12-13 01:57:24 +08:00

356 行
14 KiB
Python

"""Enhanced Claude Code agent with tracer integration for PentestGPT."""
import logging
import re
from pathlib import Path
from typing import Any, ClassVar
from claude_agent_sdk import (
AssistantMessage,
ClaudeAgentOptions,
ClaudeSDKClient,
CLIConnectionError,
CLINotFoundError,
ResultMessage,
TextBlock,
ToolUseBlock,
)
from pentestgpt.core.config import PentestGPTConfig
from pentestgpt.core.tracer import Tracer, get_global_tracer
from pentestgpt.prompts.pentesting import get_ctf_prompt
# Set up debug logging to file
# Try to write to workspace first, fallback to /tmp if permission denied
DEBUG_LOG_WORKSPACE = Path("/workspace/pentestgpt-debug.log")
DEBUG_LOG_FALLBACK = Path("/tmp/pentestgpt-debug.log")
handlers: list[logging.Handler] = [logging.StreamHandler()] # Always log to stderr
try:
# Try to create log file in workspace
handlers.append(logging.FileHandler(DEBUG_LOG_WORKSPACE, mode="w"))
DEBUG_LOG = DEBUG_LOG_WORKSPACE
except (PermissionError, OSError):
# Fallback to /tmp if workspace is not writable
handlers.append(logging.FileHandler(DEBUG_LOG_FALLBACK, mode="w"))
DEBUG_LOG = DEBUG_LOG_FALLBACK
print(
f"Warning: Could not write to {DEBUG_LOG_WORKSPACE}, using {DEBUG_LOG_FALLBACK} instead",
flush=True,
)
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
handlers=handlers,
)
logger = logging.getLogger(__name__)
class PentestAgent:
"""Enhanced Claude Code agent with activity tracking and streaming updates."""
# Flag detection patterns
FLAG_PATTERNS: ClassVar[list[str]] = [
r"flag\{[^\}]+\}", # flag{...}
r"FLAG\{[^\}]+\}", # FLAG{...}
r"HTB\{[^\}]+\}", # HTB{...}
r"CTF\{[^\}]+\}", # CTF{...}
r"[A-Za-z0-9_]+\{[^\}]+\}", # Generic CTF format
r"\b[a-f0-9]{32}\b", # 32-char hex (HTB user/root flags)
]
def __init__(
self,
config: PentestGPTConfig,
tracer: Tracer | None = None,
) -> None:
"""
Initialize the pentest agent.
Args:
config: Configuration for the agent
tracer: Optional tracer instance (uses global if not provided)
"""
self.config = config
self.tracer = tracer or get_global_tracer()
self.client: ClaudeSDKClient | None = None
self.walkthrough_steps: list[str] = []
self.flags_found: list[dict[str, str]] = []
async def execute(self, task: str) -> dict[str, Any]:
"""
Execute a penetration testing task.
Args:
task: The task/instructions for the agent
Returns:
Dictionary with execution results
"""
logger.debug("=" * 80)
logger.debug(f"EXECUTE START - Task: {task[:100]}...")
self.tracer.track_agent_status("starting", "Initializing agent")
try:
# Setup Claude Code client
logger.debug("Creating ClaudeAgentOptions...")
options = ClaudeAgentOptions(
cwd=str(self.config.working_directory),
permission_mode=self.config.permission_mode, # type: ignore[arg-type]
system_prompt=self._build_system_prompt(),
model=self.config.llm_model,
)
logger.debug(f"Options created: cwd={options.cwd}, model={options.model}")
logger.debug("Creating ClaudeSDKClient...")
self.client = ClaudeSDKClient(options=options)
logger.debug("Attempting to connect to Claude Code CLI...")
await self.client.connect()
logger.debug("✓ Successfully connected to Claude Code CLI")
self.tracer.track_agent_status("connected", "Connected to Claude Code")
# Send the task
logger.debug(f"Sending query to Claude Code: {task[:100]}...")
await self.client.query(task)
logger.debug("✓ Query sent, waiting for responses...")
self.tracer.track_message(f"Task submitted: {task[:100]}...", message_type="info")
# Process responses
logger.debug("Starting message processing loop...")
messages = []
output_parts = []
current_tool_id: int | None = None
message_count = 0
async for message in self.client.receive_response():
message_count += 1
logger.debug(f"← Received message #{message_count}: {type(message).__name__}")
messages.append(message)
if isinstance(message, AssistantMessage):
logger.debug(f" AssistantMessage with {len(message.content)} blocks")
for block in message.content:
if isinstance(block, TextBlock):
logger.debug(f" TextBlock: {block.text[:50]}...")
output_parts.append(block.text)
# Detect flags in the text
detected_flags = self._detect_flags(block.text)
if detected_flags:
for flag in detected_flags:
flag_msg = f"🚩 FLAG DETECTED: {flag}"
logger.info(flag_msg)
self.tracer.track_message(flag_msg, message_type="success")
self.flags_found.append(
{"flag": flag, "context": block.text[:200]}
)
self.tracer.track_message(block.text, message_type="info")
# Track as walkthrough step if it's substantial
if len(block.text.strip()) > 20:
self._add_walkthrough_step(block.text)
elif isinstance(block, ToolUseBlock):
logger.debug(f" ToolUseBlock: {block.name}")
# Track tool execution start
current_tool_id = self.tracer.track_tool_start(
tool_name=block.name,
args=block.input,
)
elif isinstance(message, ResultMessage):
logger.debug(" ResultMessage - Task completed")
# Mark last tool as completed if there was one
if current_tool_id is not None:
self.tracer.track_tool_complete(current_tool_id, status="completed")
# Track completion
cost = getattr(message, "total_cost_usd", 0)
logger.debug(f" Total cost: ${cost:.4f}")
self.tracer.track_agent_status(
"completed", f"Task completed | Cost: ${cost:.4f}"
)
logger.debug(f"Message loop ended. Processed {message_count} messages total.")
result = {
"success": True,
"messages": messages,
"output": "\n".join(output_parts),
"cost_usd": getattr(messages[-1], "total_cost_usd", 0) if messages else 0,
"walkthrough": self.walkthrough_steps,
"flags_found": self.flags_found,
}
output_str = str(result["output"])
logger.debug(
f"✓ EXECUTE COMPLETE - Success: {result['success']}, Output length: {len(output_str)}"
)
logger.debug(f" Flags found: {len(self.flags_found)}")
logger.debug(f" Walkthrough steps: {len(self.walkthrough_steps)}")
return result
except CLINotFoundError as e:
logger.error(f"✗ CLINotFoundError: {e}")
error_msg = (
"Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code"
)
self.tracer.track_message(error_msg, message_type="error")
return {"success": False, "error": error_msg}
except CLIConnectionError as e:
logger.error(f"✗ CLIConnectionError: {e}")
error_msg = f"Failed to connect to Claude Code: {e!s}"
self.tracer.track_message(error_msg, message_type="error")
return {"success": False, "error": error_msg}
except Exception as e:
logger.error(f"✗ Unexpected Exception: {type(e).__name__}: {e}", exc_info=True)
error_msg = f"Unexpected error: {e!s}"
self.tracer.track_message(error_msg, message_type="error")
return {"success": False, "error": error_msg}
finally:
logger.debug("Cleanup: disconnecting client...")
if self.client:
await self.client.disconnect()
logger.debug("✓ Client disconnected")
self.tracer.track_agent_status("disconnected", "Agent stopped")
logger.debug("=" * 80)
def _build_system_prompt(self) -> str:
"""Build the CTF system prompt for the agent."""
return get_ctf_prompt(self.config.custom_instruction)
def _detect_flags(self, text: str) -> list[str]:
"""
Detect potential flags in text using regex patterns.
Args:
text: Text to search for flags
Returns:
List of detected flag strings
"""
flags = []
for pattern in self.FLAG_PATTERNS:
matches = re.finditer(pattern, text, re.IGNORECASE)
for match in matches:
flag = match.group(0)
# Avoid duplicates
if flag not in flags:
flags.append(flag)
return flags
def _add_walkthrough_step(self, step: str) -> None:
"""
Add a step to the walkthrough.
Args:
step: Description of the step taken
"""
# Clean up the step text
step = step.strip()
if step and step not in self.walkthrough_steps:
self.walkthrough_steps.append(step)
logger.debug(f"Walkthrough step added: {step[:100]}...")
async def run_pentest(
target: str,
custom_instruction: str | None = None,
model: str | None = None,
working_dir: str | None = None,
debug: bool = False,
resume_session: str | None = None,
) -> dict[str, Any]:
"""
Convenience function to run a CTF challenge or penetration test.
Uses the new AgentController for lifecycle management and session persistence.
Args:
target: Target challenge/machine to solve
custom_instruction: Optional custom challenge context or instructions
model: Optional model override
working_dir: Optional working directory override
debug: Enable debug mode with verbose console output
resume_session: Optional session ID to resume
Returns:
Dictionary with challenge results including walkthrough and flags found
"""
from pentestgpt.core.config import load_config
from pentestgpt.core.controller import AgentController
# Enable verbose console logging in debug mode
if debug:
logger.info("=" * 80)
logger.info("DEBUG MODE ENABLED - CTF CHALLENGE SOLVER")
logger.info(f"Debug log file: {DEBUG_LOG}")
logger.info(f"Target: {target}")
if custom_instruction:
logger.info(f"Challenge context: {custom_instruction}")
if model:
logger.info(f"Model: {model}")
if resume_session:
logger.info(f"Resuming session: {resume_session}")
logger.info("=" * 80)
# Build config
config_kwargs: dict[str, Any] = {"target": target}
if custom_instruction:
config_kwargs["custom_instruction"] = custom_instruction
if model:
config_kwargs["llm_model"] = model
if working_dir:
config_kwargs["working_directory"] = Path(working_dir)
config = load_config(**config_kwargs)
# Build task
task = f"Solve this CTF challenge and capture the flag(s): {target}"
if custom_instruction:
task += f"\n\nChallenge context: {custom_instruction}"
# Use AgentController for lifecycle management
controller = AgentController(config)
result = await controller.run(task, resume_session_id=resume_session)
# Map result to legacy format for backward compatibility
if result.get("success"):
# Convert flags_found from list of strings to list of dicts if needed
flags = result.get("flags_found", [])
if flags and isinstance(flags[0], str):
flags = [{"flag": f, "context": ""} for f in flags]
result = {
"success": True,
"output": result.get("output", ""),
"cost_usd": result.get("cost_usd", 0),
"flags_found": flags,
"walkthrough": [], # Walkthrough tracking moved to session
"session_id": result.get("session_id", ""),
}
if debug:
logger.info("=" * 80)
logger.info(f"CHALLENGE COMPLETE - Success: {result.get('success')}")
if result.get("success"):
logger.info(f"Flags found: {len(result.get('flags_found', []))}")
logger.info(f"Cost: ${result.get('cost_usd', 0):.4f}")
logger.info(f"Session: {result.get('session_id', 'N/A')}")
for flag_data in result.get("flags_found", []):
flag = (
flag_data.get("flag", flag_data) if isinstance(flag_data, dict) else flag_data
)
logger.info(f" 🚩 {flag}")
else:
logger.error(f"Error: {result.get('error', 'Unknown')}")
logger.info("=" * 80)
return result