项目文件夹

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

492 行
17 KiB
Python

"""Main TUI application for PentestGPT using Textual framework."""
import asyncio
import threading
from pathlib import Path
from typing import Any, ClassVar
from rich.text import Text
from textual import events
from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Grid, Vertical, VerticalScroll
from textual.reactive import reactive
from textual.screen import ModalScreen
from textual.widgets import Button, Input, Label, Static
from pentestgpt.core.controller import AgentController
from pentestgpt.core.events import Event, EventBus, EventType
from pentestgpt.interface.components.activity_feed import ActivityFeed
from pentestgpt.interface.components.splash import SplashScreen
class HelpScreen(ModalScreen[None]):
"""Modal screen showing keyboard shortcuts and help."""
def compose(self) -> ComposeResult:
"""Create the help dialog."""
yield Grid(
Label("🚩 PentestGPT CTF Solver - Help", id="help_title"),
Label(
"F1 Help\n"
"Ctrl+P Pause/Resume agent\n"
"Ctrl+Q Quit\n"
"Ctrl+C Quit\n"
"↑/↓ Scroll feed\n"
"PgUp/PgDn Fast scroll\n"
"Enter Send instruction (when paused)",
id="help_content",
),
id="help_dialog",
)
def on_key(self, _event: events.Key) -> None:
"""Close help on any key press."""
self.app.pop_screen()
class QuitScreen(ModalScreen[None]):
"""Modal screen for quit confirmation."""
def compose(self) -> ComposeResult:
"""Create the quit confirmation dialog."""
yield Grid(
Label("🚩 Quit PentestGPT CTF Solver?", id="quit_title"),
Grid(
Button("Yes", variant="error", id="btn_quit_confirm"),
Button("No", variant="default", id="btn_quit_cancel"),
id="quit_buttons",
),
id="quit_dialog",
)
def on_mount(self) -> None:
"""Focus cancel button by default."""
cancel_button = self.query_one("#btn_quit_cancel", Button)
cancel_button.focus()
def on_button_pressed(self, event: Button.Pressed) -> None:
"""Handle button clicks."""
if event.button.id == "btn_quit_confirm":
self.app.exit()
else:
self.app.pop_screen()
class PentestGPTApp(App[None]):
"""Main TUI application for PentestGPT CTF Solver."""
CSS_PATH = "styles.tcss"
TITLE = "PentestGPT - CTF Challenge Solver"
show_splash: reactive[bool] = reactive(default=True)
agent_state: reactive[str] = reactive(default="idle")
BINDINGS: ClassVar[list[Binding | tuple[str, str] | tuple[str, str, str]]] = [
Binding("f1", "toggle_help", "Help", priority=True),
Binding("ctrl+p", "toggle_pause", "Pause/Resume", priority=True),
Binding("ctrl+q", "request_quit", "Quit", priority=True),
Binding("ctrl+c", "request_quit", "Quit", priority=True),
]
def __init__(
self,
target: str,
custom_instruction: str | None = None,
model: str | None = None,
debug: bool = False,
resume_session: str | None = None,
) -> None:
"""Initialize the TUI app."""
super().__init__()
self.target = target
self.custom_instruction = custom_instruction
self.model = model
self._debug = debug
self.resume_session = resume_session
self._agent_thread: threading.Thread | None = None
self._agent_stop_event = threading.Event()
self._activity_feed: ActivityFeed | None = None
self._controller: AgentController | None = None
self._events: EventBus | None = None # Initialize in on_mount
def _setup_event_handlers(self) -> None:
"""Subscribe to agent events for UI updates."""
if self._events is None:
return
self._events.subscribe(EventType.STATE_CHANGED, self._on_state_change)
self._events.subscribe(EventType.MESSAGE, self._on_agent_message)
self._events.subscribe(EventType.FLAG_FOUND, self._on_flag)
self._events.subscribe(EventType.TOOL, self._on_tool)
def _on_state_change(self, event: Event) -> None:
"""Handle agent state changes."""
state = event.data.get("state", "unknown")
details = event.data.get("details", "")
# Update reactive state (triggers watch_agent_state)
self.call_from_thread(setattr, self, "agent_state", state)
# Log state change
if self._activity_feed and details:
self.call_from_thread(
self._activity_feed.add_message,
f"Agent: {details}",
"info",
)
def _on_agent_message(self, event: Event) -> None:
"""Handle agent messages."""
if not self._activity_feed:
return
text = event.data.get("text", "")
msg_type = event.data.get("type", "info")
if text:
self.call_from_thread(
self._activity_feed.add_message,
text,
msg_type,
)
def _on_flag(self, event: Event) -> None:
"""Handle flag found events."""
if not self._activity_feed:
return
flag = event.data.get("flag", "")
if flag:
self.call_from_thread(
self._activity_feed.add_message,
f"🚩 FLAG FOUND: {flag}",
"success",
)
def _on_tool(self, event: Event) -> None:
"""Handle tool events."""
if not self._activity_feed:
return
status = event.data.get("status", "")
name = event.data.get("name", "")
args = event.data.get("args", {})
result = event.data.get("result")
if status == "start":
self.call_from_thread(
self._activity_feed.add_tool_execution,
name,
args,
"running",
None,
)
elif status == "complete":
self.call_from_thread(
self._activity_feed.add_tool_execution,
name,
args,
"completed",
result,
)
def watch_agent_state(self, state: str) -> None:
"""React to agent state changes - show/hide input field."""
try:
user_input = self.query_one("#user_input", Input)
# Show input when paused, hide otherwise
user_input.display = state == "paused"
if state == "paused":
user_input.focus()
except Exception:
pass
# Update status bar
self._update_status_bar()
def compose(self) -> ComposeResult:
"""Create the initial UI layout."""
if self.show_splash:
yield SplashScreen(id="splash_screen")
def watch_show_splash(self, show_splash: bool) -> None:
"""React to splash screen visibility changes."""
if not show_splash and hasattr(self, "_driver") and self._driver is not None:
# Remove splash screen
try:
splash = self.query_one("#splash_screen")
splash.remove()
except Exception:
pass
# Build main interface
self._build_main_interface()
def _build_main_interface(self) -> None:
"""Build the main application interface."""
# Main container
main_container = Vertical(id="main_container")
self.mount(main_container)
# Header
header = self._create_header()
main_container.mount(header)
# Activity feed
feed = ActivityFeed(id="activity_feed")
self._activity_feed = feed
content_area = VerticalScroll(feed, id="content_area")
main_container.mount(content_area)
# User input field (hidden by default, shown when paused)
user_input = Input(
placeholder="Type instruction and press Enter...",
id="user_input",
)
user_input.display = False
main_container.mount(user_input)
# Status bar
status_bar = self._create_status_bar()
main_container.mount(status_bar)
def _create_header(self) -> Static:
"""Create the header bar."""
header_text = Text()
header_text.append("🚩 ", style="bold #6366f1")
header_text.append("PentestGPT", style="bold #6366f1")
header_text.append(" CTF Solver", style="bold #6366f1")
header_text.append(" v1.0", style="dim")
header_text.append(" │ ", style="dim")
header_text.append("Target: ", style="dim")
header_text.append(self.target, style="bold")
if self.model:
header_text.append(" │ ", style="dim")
header_text.append(f"Model: {self.model}", style="dim")
header = Static(header_text, id="header")
return header
def _create_status_bar(self) -> Static:
"""Create the status bar."""
status_text = self._build_status_text()
status_bar = Static(status_text, id="status_bar")
return status_bar
def _build_status_text(self) -> Text:
"""Build status bar text based on current state."""
status_text = Text()
# Agent state
status_text.append("Agent: ", style="dim")
state_styles = {
"idle": ("Idle", "dim"),
"running": ("Running", "bold #6366f1"),
"paused": ("PAUSED", "bold #f59e0b"),
"completed": ("Completed", "bold #10b981"),
"error": ("Error", "bold #ef4444"),
}
state_label, state_style = state_styles.get(self.agent_state, ("Unknown", "dim"))
status_text.append(state_label, style=state_style)
if self._debug:
from pentestgpt.core.agent import DEBUG_LOG
status_text.append(" │ ", style="dim")
status_text.append("Debug: ", style="dim")
status_text.append(str(DEBUG_LOG), style="bold #f59e0b")
status_text.append(" │ ", style="dim")
status_text.append("Ctrl+P", style="bold #8b5cf6")
status_text.append(" Pause ", style="dim")
status_text.append("F1", style="bold #8b5cf6")
status_text.append(" Help ", style="dim")
status_text.append("Ctrl+Q", style="bold #8b5cf6")
status_text.append(" Quit", style="dim")
return status_text
def _update_status_bar(self) -> None:
"""Update status bar with current state."""
try:
status_bar = self.query_one("#status_bar", Static)
status_bar.update(self._build_status_text())
except Exception:
pass
def on_mount(self) -> None:
"""Handle app mount - show splash then start agent."""
# Initialize event bus after app is mounted
self._events = EventBus.get()
self._setup_event_handlers()
# Show splash for 2.5 seconds
self.set_timer(2.5, self._hide_splash)
def _hide_splash(self) -> None:
"""Hide splash screen and start main app."""
self.show_splash = False
# Start agent after splash
self.call_later(self._start_agent)
def _start_agent(self) -> None:
"""Start the agent in a background thread using AgentController."""
from pentestgpt.core.config import load_config
if self._activity_feed:
self._activity_feed.add_message(
"🚩 CTF Solver initializing...",
message_type="info",
)
def run_agent() -> None:
"""Run the agent with controller."""
try:
# Build config
config_kwargs: dict[str, Any] = {
"target": self.target,
"working_directory": Path("/workspace"),
}
if self.custom_instruction:
config_kwargs["custom_instruction"] = self.custom_instruction
if self.model:
config_kwargs["llm_model"] = self.model
config = load_config(**config_kwargs)
# Create controller
self._controller = AgentController(config)
# Build task
task = f"Solve this CTF challenge and capture the flag(s): {self.target}"
if self.custom_instruction:
task += f"\n\nChallenge context: {self.custom_instruction}"
# Run agent with controller
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result = loop.run_until_complete(
self._controller.run(task, resume_session_id=self.resume_session)
)
loop.close()
# Show final result summary
if self._activity_feed:
if result.get("success"):
flags = result.get("flags_found", [])
cost = result.get("cost_usd", 0)
session_id = result.get("session_id", "")
if flags:
self.call_from_thread(
self._activity_feed.add_message,
f"🚩 Challenge complete! {len(flags)} flag(s) | Cost: ${cost:.4f} | Session: {session_id}",
"success",
)
else:
self.call_from_thread(
self._activity_feed.add_message,
f"⚠ Challenge ended without flags | Cost: ${cost:.4f} | Session: {session_id}",
"warning",
)
else:
error = result.get("error", "Unknown error")
self.call_from_thread(
self._activity_feed.add_message,
f"✗ Agent error: {error}",
"error",
)
except Exception as e:
if self._activity_feed:
self.call_from_thread(
self._activity_feed.add_message,
f"✗ Agent error: {e!s}",
"error",
)
self._agent_thread = threading.Thread(target=run_agent, daemon=True)
self._agent_thread.start()
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle user input submission."""
if event.input.id == "user_input":
text = event.value.strip()
if text and self._events:
# Emit user input event
self._events.emit_input(text)
if self._activity_feed:
self._activity_feed.add_message(
f"📝 Instruction queued: {text[:50]}...",
message_type="info",
)
event.input.value = ""
def action_toggle_pause(self) -> None:
"""Toggle pause/resume state (Ctrl+P)."""
if self.show_splash or not self._controller:
return
if self.agent_state == "paused":
self._controller.resume()
elif self.agent_state == "running":
self._controller.pause()
def action_toggle_help(self) -> None:
"""Show/hide help screen."""
if self.show_splash:
return
if isinstance(self.screen, HelpScreen):
self.pop_screen()
else:
self.push_screen(HelpScreen())
def action_request_quit(self) -> None:
"""Show quit confirmation."""
if self.show_splash:
self.exit()
return
self.push_screen(QuitScreen())
def on_unmount(self) -> None:
"""Cleanup when app closes."""
# Signal agent to stop
self._agent_stop_event.set()
# Wait for agent thread
if self._agent_thread and self._agent_thread.is_alive():
self._agent_thread.join(timeout=2.0)
async def run_tui(
target: str,
custom_instruction: str | None = None,
model: str | None = None,
debug: bool = False,
resume_session: str | None = None,
) -> None:
"""Run the TUI application.
Args:
target: Target URL/IP/domain
custom_instruction: Optional challenge context
model: Optional model override
debug: Enable debug mode
resume_session: Optional session ID to resume
"""
app = PentestGPTApp(
target=target,
custom_instruction=custom_instruction,
model=model,
debug=debug,
resume_session=resume_session,
)
await app.run_async()
# For testing
if __name__ == "__main__":
import asyncio
asyncio.run(run_tui("example.com"))