项目文件夹

文件
2025-12-13 01:23:24 +08:00

471 行
14 KiB
Python

#!/usr/bin/env python3
"""Main CLI entry point for PentestGPT."""
import argparse
import asyncio
import sys
from rich.console import Console
from rich.text import Text
def parse_arguments() -> argparse.Namespace:
"""Parse command line arguments."""
parser = argparse.ArgumentParser(
prog="pentestgpt",
description="PentestGPT - AI-Powered CTF Challenge Solver",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Interactive TUI mode (default) - HTB machine
pentestgpt --target 10.10.11.234
# Web challenge
pentestgpt --target https://ctf.example.com/challenge1
# With challenge context/hints
pentestgpt --target 10.10.11.100 --instruction "Wordpress site, focus on plugin vulnerabilities"
# Non-interactive mode
pentestgpt --target challenge.htb --non-interactive
# Custom model
pentestgpt --target 10.10.11.50 --model claude-opus-4-20250514
For more information: https://github.com/yourusername/pentestgpt
""",
)
parser.add_argument(
"-t",
"--target",
type=str,
required=True,
help="Target CTF challenge or machine (URL, IP address, domain, or file path)",
)
parser.add_argument(
"-i",
"--instruction",
type=str,
help="Custom challenge context, hints, or instructions",
)
parser.add_argument(
"-m",
"--model",
type=str,
help="Claude model to use (default: claude-sonnet-4-5-20250929)",
)
parser.add_argument(
"-n",
"--non-interactive",
action="store_true",
help="Run in non-interactive mode (no TUI, exits on completion)",
)
parser.add_argument(
"-v",
"--verbose",
action="store_true",
help="Enable verbose output",
)
parser.add_argument(
"-d",
"--debug",
action="store_true",
help="Enable debug mode with detailed logging",
)
parser.add_argument(
"--raw",
action="store_true",
help="Raw output mode for debugging (no TUI, no spinner, direct streaming)",
)
parser.add_argument(
"--version",
action="version",
version="%(prog)s 1.0.0",
)
# Session management
parser.add_argument(
"-r",
"--resume",
action="store_true",
help="Resume the most recent session for the target",
)
parser.add_argument(
"--session-id",
type=str,
help="Resume a specific session by ID",
)
parser.add_argument(
"--list-sessions",
action="store_true",
help="List available sessions and exit",
)
# Telemetry
parser.add_argument(
"--no-telemetry",
action="store_true",
help="Disable anonymous telemetry data collection",
)
return parser.parse_args()
def validate_environment() -> None:
"""
Validate environment (optional checks).
Note: API key is no longer required here as Claude Code manages
its own configuration.
"""
# No required validations - Claude Code handles API configuration
pass
def print_banner() -> None:
"""Print welcome banner."""
console = Console()
banner = Text()
banner.append("🚩 ", style="bold #6366f1")
banner.append("PentestGPT", style="bold #6366f1")
banner.append(" v1.0.0", style="dim")
banner.append("\n")
banner.append("AI-Powered CTF Challenge Solver", style="dim italic")
console.print()
console.print(banner)
console.print()
async def run_cli_mode(args: argparse.Namespace) -> None:
"""Run in non-interactive CLI mode."""
from pentestgpt.core.agent import run_pentest
from pentestgpt.core.session import SessionStore
console = Console()
# Determine session to resume
resume_session = args.session_id
if args.resume and not resume_session:
sessions = SessionStore()
latest = sessions.get_latest(args.target)
if latest:
resume_session = latest.session_id
console.print(f"[dim]Resuming session: {resume_session}[/]")
console.print(f"[bold]Target:[/] {args.target}")
if args.instruction:
console.print(f"[bold]Challenge Context:[/] {args.instruction}")
if args.debug:
console.print("[dim]Debug mode: enabled[/]")
console.print()
# Allow up to 3 attempts
max_attempts = 3
attempt = 1
custom_instruction = args.instruction
total_cost = 0.0
while attempt <= max_attempts:
if attempt > 1:
console.print(f"\n[bold cyan]Attempt {attempt}/{max_attempts}[/]")
console.print("[dim]Adding persistence context...[/]\n")
with console.status("[bold cyan]Solving CTF challenge...", spinner="dots"):
result = await run_pentest(
target=args.target,
custom_instruction=custom_instruction,
model=args.model,
debug=args.debug,
resume_session=resume_session if attempt == 1 else None,
)
total_cost += result.get("cost_usd", 0)
console.print()
if result["success"]:
# Display flags found
flags_found = result.get("flags_found", [])
if flags_found:
console.print(
f"[bold green]🚩 Challenge solved! {len(flags_found)} flag(s) found:[/]"
)
for flag_data in flags_found:
console.print(f" • [bold cyan]{flag_data['flag']}[/]")
console.print(f"\n{result['output']}")
# Display session info
session_id = result.get("session_id", "")
if session_id:
console.print(f"\n[dim]Session: {session_id}[/]")
if total_cost > 0:
console.print(f"[dim]Total cost: ${total_cost:.4f}[/]")
# Success! Exit cleanly
return
else:
# No flags found - this is a problem
console.print("[bold red]⚠ CHALLENGE INCOMPLETE - NO FLAGS CAPTURED[/]")
console.print("[yellow]The agent stopped without capturing flags.[/]")
console.print(f"\n{result['output']}")
if attempt < max_attempts:
console.print(f"\n[bold yellow]Attempts remaining: {max_attempts - attempt}[/]")
console.print(
"[dim]The agent will retry with stronger persistence instructions.[/]\n"
)
# Add feedback for next attempt
if custom_instruction:
custom_instruction += "\n\nPREVIOUS ATTEMPT FEEDBACK: You stopped without capturing flags. This is unacceptable. You MUST capture at least one flag. Try different approaches, enumerate harder, and do not stop until you have flags."
else:
custom_instruction = "IMPORTANT: The previous attempt failed to capture any flags. You MUST capture at least one flag. Do not stop until flags are found. Try all available techniques systematically."
attempt += 1
continue
else:
console.print(f"\n[bold red]✗ Maximum attempts reached ({max_attempts})[/]")
console.print(
"[yellow]Consider providing more specific hints via --instruction[/]"
)
# Display session info for resumption
session_id = result.get("session_id", "")
if session_id:
console.print(
f"\n[dim]Session: {session_id} (resume with --session-id {session_id})[/]"
)
if total_cost > 0:
console.print(f"[dim]Total cost: ${total_cost:.4f}[/]")
sys.exit(1)
else:
console.print(
f"[bold red]✗ Challenge failed:[/] {result.get('error', 'Unknown error')}"
)
sys.exit(1)
async def run_raw_mode(args: argparse.Namespace) -> None:
"""Run in raw CLI mode with streaming output for debugging."""
from pentestgpt.core.config import load_config
from pentestgpt.core.controller import AgentController
from pentestgpt.core.events import Event, EventBus, EventType
from pentestgpt.core.session import SessionStore
# Print startup info
print(f"[INFO] Target: {args.target}")
if args.instruction:
print(f"[INFO] Instruction: {args.instruction}")
if args.model:
print(f"[INFO] Model: {args.model}")
if args.debug:
print("[INFO] Debug mode: enabled")
print("[INFO] Starting agent...", flush=True)
# Set up event handlers that print directly to stdout
events = EventBus.get()
def on_message(event: Event) -> None:
text = event.data.get("text", "")
msg_type = event.data.get("type", "info")
if text:
print(f"[{msg_type.upper()}] {text}", flush=True)
def on_tool(event: Event) -> None:
status = event.data.get("status")
name = event.data.get("name", "unknown")
if status == "start":
args_data = event.data.get("args", {})
print(f"[TOOL] {name}: {args_data}", flush=True)
else:
print(f"[TOOL] {name} done", flush=True)
def on_flag(event: Event) -> None:
flag = event.data.get("flag", "")
if flag:
print(f"[FLAG] {flag}", flush=True)
def on_state(event: Event) -> None:
state = event.data.get("state", "")
details = event.data.get("details", "")
if details:
print(f"[STATE] {state}: {details}", flush=True)
else:
print(f"[STATE] {state}", flush=True)
events.subscribe(EventType.MESSAGE, on_message)
events.subscribe(EventType.TOOL, on_tool)
events.subscribe(EventType.FLAG_FOUND, on_flag)
events.subscribe(EventType.STATE_CHANGED, on_state)
# Determine session to resume
resume_session = args.session_id
if args.resume and not resume_session:
sessions = SessionStore()
latest = sessions.get_latest(args.target)
if latest:
resume_session = latest.session_id
print(f"[INFO] Resuming session: {resume_session}", flush=True)
# Build config
config_kwargs = {"target": args.target}
if args.instruction:
config_kwargs["custom_instruction"] = args.instruction
if args.model:
config_kwargs["llm_model"] = args.model
config = load_config(**config_kwargs)
# Build task
task = f"Solve this CTF challenge and capture the flag(s): {args.target}"
if args.instruction:
task += f"\n\nChallenge context: {args.instruction}"
# Run agent with controller
controller = AgentController(config)
try:
result = await controller.run(task, resume_session_id=resume_session)
# Print final result
if result.get("success"):
flags = result.get("flags_found", [])
cost = result.get("cost_usd", 0)
session_id = result.get("session_id", "")
print(
f"[DONE] Flags: {len(flags)}, Cost: ${cost:.4f}, Session: {session_id}",
flush=True,
)
if not flags:
print("[WARN] No flags captured", flush=True)
sys.exit(1)
else:
error = result.get("error", "Unknown error")
print(f"[ERROR] {error}", flush=True)
sys.exit(1)
except Exception as e:
print(f"[ERROR] {e!s}", flush=True)
import traceback
print(f"[TRACE] {traceback.format_exc()}", flush=True)
sys.exit(1)
async def run_tui_mode(args: argparse.Namespace) -> None:
"""Run in interactive TUI mode."""
from pentestgpt.core.session import SessionStore
from pentestgpt.interface.tui import run_tui
# Determine session to resume
resume_session = args.session_id
if args.resume and not resume_session:
sessions = SessionStore()
latest = sessions.get_latest(args.target)
if latest:
resume_session = latest.session_id
await run_tui(
target=args.target,
custom_instruction=args.instruction,
model=args.model,
debug=args.debug,
resume_session=resume_session,
)
def list_sessions(target: str | None = None) -> None:
"""List available sessions."""
from pentestgpt.core.session import SessionStore
console = Console()
sessions = SessionStore()
session_list = sessions.list_sessions(target)
if not session_list:
console.print("[dim]No sessions found.[/]")
return
console.print(f"[bold]Sessions{f' for {target}' if target else ''}:[/]\n")
console.print(f"{'ID':<10} {'Date':<18} {'Target':<25} {'Status':<12} {'Flags':<6}")
console.print("-" * 75)
for s in session_list:
date_str = s.created_at.strftime("%Y-%m-%d %H:%M")
target_str = s.target[:23] + ".." if len(s.target) > 25 else s.target
flags_count = len(s.flags_found)
console.print(
f"{s.session_id:<10} {date_str:<18} {target_str:<25} {s.status.value:<12} {flags_count:<6}"
)
def main() -> None:
"""Main entry point."""
# Parse arguments
args = parse_arguments()
# Handle --list-sessions
if args.list_sessions:
list_sessions(args.target if hasattr(args, "target") else None)
return
# Validate environment
validate_environment()
# Initialize telemetry (enabled by default, use --no-telemetry to disable)
from pentestgpt.core.langfuse import init_langfuse, shutdown_langfuse
init_langfuse(disabled=args.no_telemetry)
# Print banner in CLI mode
if args.non_interactive:
print_banner()
# Run appropriate mode
try:
if args.raw:
asyncio.run(run_raw_mode(args))
elif args.non_interactive:
asyncio.run(run_cli_mode(args))
else:
asyncio.run(run_tui_mode(args))
except KeyboardInterrupt:
console = Console()
console.print("\n\n[yellow]Operation cancelled by user[/]")
sys.exit(130)
except Exception as e:
console = Console()
console.print(f"\n[bold red]Error:[/] {e!s}", style="red")
# Always show traceback for debugging
import traceback
console.print("\n[dim]Traceback:[/]")
console.print(traceback.format_exc())
sys.exit(1)
finally:
shutdown_langfuse()
if __name__ == "__main__":
main()