aiptx 2.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aipt_v2/__init__.py +110 -0
- aipt_v2/__main__.py +24 -0
- aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
- aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
- aipt_v2/agents/__init__.py +46 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/exploit_agent.py +688 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +957 -0
- aipt_v2/browser/__init__.py +31 -0
- aipt_v2/browser/automation.py +458 -0
- aipt_v2/browser/crawler.py +453 -0
- aipt_v2/cli.py +2933 -0
- aipt_v2/compliance/__init__.py +71 -0
- aipt_v2/compliance/compliance_report.py +449 -0
- aipt_v2/compliance/framework_mapper.py +424 -0
- aipt_v2/compliance/nist_mapping.py +345 -0
- aipt_v2/compliance/owasp_mapping.py +330 -0
- aipt_v2/compliance/pci_mapping.py +297 -0
- aipt_v2/config.py +341 -0
- aipt_v2/core/__init__.py +43 -0
- aipt_v2/core/agent.py +630 -0
- aipt_v2/core/llm.py +395 -0
- aipt_v2/core/memory.py +305 -0
- aipt_v2/core/ptt.py +329 -0
- aipt_v2/database/__init__.py +14 -0
- aipt_v2/database/models.py +232 -0
- aipt_v2/database/repository.py +384 -0
- aipt_v2/docker/__init__.py +23 -0
- aipt_v2/docker/builder.py +260 -0
- aipt_v2/docker/manager.py +222 -0
- aipt_v2/docker/sandbox.py +371 -0
- aipt_v2/evasion/__init__.py +58 -0
- aipt_v2/evasion/request_obfuscator.py +272 -0
- aipt_v2/evasion/tls_fingerprint.py +285 -0
- aipt_v2/evasion/ua_rotator.py +301 -0
- aipt_v2/evasion/waf_bypass.py +439 -0
- aipt_v2/execution/__init__.py +23 -0
- aipt_v2/execution/executor.py +302 -0
- aipt_v2/execution/parser.py +544 -0
- aipt_v2/execution/terminal.py +337 -0
- aipt_v2/health.py +437 -0
- aipt_v2/intelligence/__init__.py +194 -0
- aipt_v2/intelligence/adaptation.py +474 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/correlation.py +536 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -0
- aipt_v2/intelligence/knowledge_graph.py +590 -0
- aipt_v2/intelligence/learning.py +626 -0
- aipt_v2/intelligence/llm_analyzer.py +502 -0
- aipt_v2/intelligence/llm_tool_selector.py +518 -0
- aipt_v2/intelligence/payload_generator.py +562 -0
- aipt_v2/intelligence/rag.py +239 -0
- aipt_v2/intelligence/scope.py +442 -0
- aipt_v2/intelligence/searchers/__init__.py +5 -0
- aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
- aipt_v2/intelligence/searchers/github_searcher.py +467 -0
- aipt_v2/intelligence/searchers/google_searcher.py +281 -0
- aipt_v2/intelligence/tools.json +443 -0
- aipt_v2/intelligence/triage.py +670 -0
- aipt_v2/interactive_shell.py +559 -0
- aipt_v2/interface/__init__.py +5 -0
- aipt_v2/interface/cli.py +230 -0
- aipt_v2/interface/main.py +501 -0
- aipt_v2/interface/tui.py +1276 -0
- aipt_v2/interface/utils.py +583 -0
- aipt_v2/llm/__init__.py +39 -0
- aipt_v2/llm/config.py +26 -0
- aipt_v2/llm/llm.py +514 -0
- aipt_v2/llm/memory.py +214 -0
- aipt_v2/llm/request_queue.py +89 -0
- aipt_v2/llm/utils.py +89 -0
- aipt_v2/local_tool_installer.py +1467 -0
- aipt_v2/models/__init__.py +15 -0
- aipt_v2/models/findings.py +295 -0
- aipt_v2/models/phase_result.py +224 -0
- aipt_v2/models/scan_config.py +207 -0
- aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
- aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
- aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
- aipt_v2/monitoring/prometheus.yml +60 -0
- aipt_v2/orchestration/__init__.py +52 -0
- aipt_v2/orchestration/pipeline.py +398 -0
- aipt_v2/orchestration/progress.py +300 -0
- aipt_v2/orchestration/scheduler.py +296 -0
- aipt_v2/orchestrator.py +2427 -0
- aipt_v2/payloads/__init__.py +27 -0
- aipt_v2/payloads/cmdi.py +150 -0
- aipt_v2/payloads/sqli.py +263 -0
- aipt_v2/payloads/ssrf.py +204 -0
- aipt_v2/payloads/templates.py +222 -0
- aipt_v2/payloads/traversal.py +166 -0
- aipt_v2/payloads/xss.py +204 -0
- aipt_v2/prompts/__init__.py +60 -0
- aipt_v2/proxy/__init__.py +29 -0
- aipt_v2/proxy/history.py +352 -0
- aipt_v2/proxy/interceptor.py +452 -0
- aipt_v2/recon/__init__.py +44 -0
- aipt_v2/recon/dns.py +241 -0
- aipt_v2/recon/osint.py +367 -0
- aipt_v2/recon/subdomain.py +372 -0
- aipt_v2/recon/tech_detect.py +311 -0
- aipt_v2/reports/__init__.py +17 -0
- aipt_v2/reports/generator.py +313 -0
- aipt_v2/reports/html_report.py +378 -0
- aipt_v2/runtime/__init__.py +53 -0
- aipt_v2/runtime/base.py +30 -0
- aipt_v2/runtime/docker.py +401 -0
- aipt_v2/runtime/local.py +346 -0
- aipt_v2/runtime/tool_server.py +205 -0
- aipt_v2/runtime/vps.py +830 -0
- aipt_v2/scanners/__init__.py +28 -0
- aipt_v2/scanners/base.py +273 -0
- aipt_v2/scanners/nikto.py +244 -0
- aipt_v2/scanners/nmap.py +402 -0
- aipt_v2/scanners/nuclei.py +273 -0
- aipt_v2/scanners/web.py +454 -0
- aipt_v2/scripts/security_audit.py +366 -0
- aipt_v2/setup_wizard.py +941 -0
- aipt_v2/skills/__init__.py +80 -0
- aipt_v2/skills/agents/__init__.py +14 -0
- aipt_v2/skills/agents/api_tester.py +706 -0
- aipt_v2/skills/agents/base.py +477 -0
- aipt_v2/skills/agents/code_review.py +459 -0
- aipt_v2/skills/agents/security_agent.py +336 -0
- aipt_v2/skills/agents/web_pentest.py +818 -0
- aipt_v2/skills/prompts/__init__.py +647 -0
- aipt_v2/system_detector.py +539 -0
- aipt_v2/telemetry/__init__.py +7 -0
- aipt_v2/telemetry/tracer.py +347 -0
- aipt_v2/terminal/__init__.py +28 -0
- aipt_v2/terminal/executor.py +400 -0
- aipt_v2/terminal/sandbox.py +350 -0
- aipt_v2/tools/__init__.py +44 -0
- aipt_v2/tools/active_directory/__init__.py +78 -0
- aipt_v2/tools/active_directory/ad_config.py +238 -0
- aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
- aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
- aipt_v2/tools/active_directory/ldap_enum.py +533 -0
- aipt_v2/tools/active_directory/smb_attacks.py +505 -0
- aipt_v2/tools/agents_graph/__init__.py +19 -0
- aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
- aipt_v2/tools/api_security/__init__.py +76 -0
- aipt_v2/tools/api_security/api_discovery.py +608 -0
- aipt_v2/tools/api_security/graphql_scanner.py +622 -0
- aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
- aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
- aipt_v2/tools/browser/__init__.py +5 -0
- aipt_v2/tools/browser/browser_actions.py +238 -0
- aipt_v2/tools/browser/browser_instance.py +535 -0
- aipt_v2/tools/browser/tab_manager.py +344 -0
- aipt_v2/tools/cloud/__init__.py +70 -0
- aipt_v2/tools/cloud/cloud_config.py +273 -0
- aipt_v2/tools/cloud/cloud_scanner.py +639 -0
- aipt_v2/tools/cloud/prowler_tool.py +571 -0
- aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
- aipt_v2/tools/executor.py +307 -0
- aipt_v2/tools/parser.py +408 -0
- aipt_v2/tools/proxy/__init__.py +5 -0
- aipt_v2/tools/proxy/proxy_actions.py +103 -0
- aipt_v2/tools/proxy/proxy_manager.py +789 -0
- aipt_v2/tools/registry.py +196 -0
- aipt_v2/tools/scanners/__init__.py +343 -0
- aipt_v2/tools/scanners/acunetix_tool.py +712 -0
- aipt_v2/tools/scanners/burp_tool.py +631 -0
- aipt_v2/tools/scanners/config.py +156 -0
- aipt_v2/tools/scanners/nessus_tool.py +588 -0
- aipt_v2/tools/scanners/zap_tool.py +612 -0
- aipt_v2/tools/terminal/__init__.py +5 -0
- aipt_v2/tools/terminal/terminal_actions.py +37 -0
- aipt_v2/tools/terminal/terminal_manager.py +153 -0
- aipt_v2/tools/terminal/terminal_session.py +449 -0
- aipt_v2/tools/tool_processing.py +108 -0
- aipt_v2/utils/__init__.py +17 -0
- aipt_v2/utils/logging.py +202 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aipt_v2/verify_install.py +793 -0
- aiptx-2.0.7.dist-info/METADATA +345 -0
- aiptx-2.0.7.dist-info/RECORD +187 -0
- aiptx-2.0.7.dist-info/WHEEL +5 -0
- aiptx-2.0.7.dist-info/entry_points.txt +7 -0
- aiptx-2.0.7.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.7.dist-info/top_level.txt +1 -0
aipt_v2/core/agent.py
ADDED
|
@@ -0,0 +1,630 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT Agent - LangGraph-based autonomous pentesting agent
|
|
3
|
+
|
|
4
|
+
The heart of AIPT: Think → Select → Execute → Learn loop
|
|
5
|
+
|
|
6
|
+
Inspired by:
|
|
7
|
+
- Strix: LangGraph state machine, 300 iterations
|
|
8
|
+
- PentestGPT: PTT task tracking
|
|
9
|
+
- Pentagi: Message chain isolation
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from typing import Optional, Any, Callable
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from enum import Enum
|
|
16
|
+
import json
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from pydantic import BaseModel, Field
|
|
20
|
+
PYDANTIC_AVAILABLE = True
|
|
21
|
+
except ImportError:
|
|
22
|
+
PYDANTIC_AVAILABLE = False
|
|
23
|
+
BaseModel = object
|
|
24
|
+
def Field(**kwargs): return None
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
from langgraph.graph import StateGraph, END
|
|
28
|
+
LANGGRAPH_AVAILABLE = True
|
|
29
|
+
except ImportError:
|
|
30
|
+
LANGGRAPH_AVAILABLE = False
|
|
31
|
+
StateGraph = None
|
|
32
|
+
END = "END"
|
|
33
|
+
|
|
34
|
+
from .memory import MemoryManager
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Phase(str, Enum):
|
|
38
|
+
"""Penetration testing phases"""
|
|
39
|
+
RECON = "recon"
|
|
40
|
+
ENUM = "enum"
|
|
41
|
+
EXPLOIT = "exploit"
|
|
42
|
+
POST = "post"
|
|
43
|
+
REPORT = "report"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if PYDANTIC_AVAILABLE:
|
|
47
|
+
class PentestState(BaseModel):
|
|
48
|
+
"""
|
|
49
|
+
State object passed through the LangGraph.
|
|
50
|
+
Contains all information about the current pentest session.
|
|
51
|
+
"""
|
|
52
|
+
# Target information
|
|
53
|
+
target: str = Field(default="", description="Primary target (IP, domain, or range)")
|
|
54
|
+
scope: list[str] = Field(default_factory=list, description="In-scope targets")
|
|
55
|
+
|
|
56
|
+
# Current phase and progress
|
|
57
|
+
phase: Phase = Field(default=Phase.RECON, description="Current pentest phase")
|
|
58
|
+
iteration: int = Field(default=0, description="Current iteration count")
|
|
59
|
+
max_iterations: int = Field(default=100, description="Maximum iterations")
|
|
60
|
+
|
|
61
|
+
# Current action
|
|
62
|
+
current_objective: str = Field(default="", description="What we're trying to do")
|
|
63
|
+
selected_tool: Optional[dict] = Field(default=None, description="Selected tool")
|
|
64
|
+
tool_command: str = Field(default="", description="Command to execute")
|
|
65
|
+
|
|
66
|
+
# Results
|
|
67
|
+
last_output: str = Field(default="", description="Last tool output")
|
|
68
|
+
findings: list[dict] = Field(default_factory=list, description="All findings")
|
|
69
|
+
|
|
70
|
+
# PTT (Penetration Testing Tree)
|
|
71
|
+
ptt: dict = Field(default_factory=dict, description="Task hierarchy")
|
|
72
|
+
|
|
73
|
+
# Control flags
|
|
74
|
+
needs_human_confirm: bool = Field(default=False, description="Needs human confirmation")
|
|
75
|
+
stop_reason: Optional[str] = Field(default=None, description="Why agent stopped")
|
|
76
|
+
error: Optional[str] = Field(default=None, description="Last error if any")
|
|
77
|
+
|
|
78
|
+
class Config:
|
|
79
|
+
arbitrary_types_allowed = True
|
|
80
|
+
else:
|
|
81
|
+
@dataclass
|
|
82
|
+
class PentestState:
|
|
83
|
+
"""Fallback state without Pydantic"""
|
|
84
|
+
target: str = ""
|
|
85
|
+
scope: list = field(default_factory=list)
|
|
86
|
+
phase: Phase = Phase.RECON
|
|
87
|
+
iteration: int = 0
|
|
88
|
+
max_iterations: int = 100
|
|
89
|
+
current_objective: str = ""
|
|
90
|
+
selected_tool: Optional[dict] = None
|
|
91
|
+
tool_command: str = ""
|
|
92
|
+
last_output: str = ""
|
|
93
|
+
findings: list = field(default_factory=list)
|
|
94
|
+
ptt: dict = field(default_factory=dict)
|
|
95
|
+
needs_human_confirm: bool = False
|
|
96
|
+
stop_reason: Optional[str] = None
|
|
97
|
+
error: Optional[str] = None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AIPTAgent:
|
|
101
|
+
"""
|
|
102
|
+
Autonomous AI Penetration Testing Agent.
|
|
103
|
+
|
|
104
|
+
Uses a 4-node LangGraph state machine:
|
|
105
|
+
1. THINK: Analyze current state, decide next action
|
|
106
|
+
2. SELECT: Use RAG to choose the best tool
|
|
107
|
+
3. EXECUTE: Run the tool, capture output
|
|
108
|
+
4. LEARN: Extract findings, update PTT, decide next phase
|
|
109
|
+
|
|
110
|
+
Example:
|
|
111
|
+
from aipt_v2.core import AIPTAgent, get_llm
|
|
112
|
+
|
|
113
|
+
llm = get_llm("openai")
|
|
114
|
+
agent = AIPTAgent(llm, tools_rag, terminal, ptt)
|
|
115
|
+
result = agent.run("192.168.1.0/24")
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
# System prompt for the pentesting agent
|
|
119
|
+
SYSTEM_PROMPT = """You are AIPT, an expert AI penetration testing assistant.
|
|
120
|
+
|
|
121
|
+
Your goal is to systematically test the target for security vulnerabilities.
|
|
122
|
+
|
|
123
|
+
## Current Phase: {phase}
|
|
124
|
+
## Target: {target}
|
|
125
|
+
## Iteration: {iteration}/{max_iterations}
|
|
126
|
+
|
|
127
|
+
## Phases:
|
|
128
|
+
1. RECON: Discover hosts, ports, services (nmap, masscan, subfinder)
|
|
129
|
+
2. ENUM: Enumerate services, find vulnerabilities (gobuster, nikto, nuclei)
|
|
130
|
+
3. EXPLOIT: Exploit vulnerabilities (sqlmap, metasploit, hydra)
|
|
131
|
+
4. POST: Post-exploitation, privilege escalation (linpeas, mimikatz)
|
|
132
|
+
5. REPORT: Generate final report
|
|
133
|
+
|
|
134
|
+
## Current PTT (Penetration Testing Tree):
|
|
135
|
+
{ptt}
|
|
136
|
+
|
|
137
|
+
## Recent Findings:
|
|
138
|
+
{findings}
|
|
139
|
+
|
|
140
|
+
## Rules:
|
|
141
|
+
1. Be methodical - complete reconnaissance before exploitation
|
|
142
|
+
2. Document everything - update PTT with each finding
|
|
143
|
+
3. Stay in scope - only test authorized targets
|
|
144
|
+
4. Be efficient - don't repeat the same scans
|
|
145
|
+
5. Escalate phases when current phase objectives are met
|
|
146
|
+
|
|
147
|
+
Respond with your analysis and recommended next action.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
def __init__(
|
|
151
|
+
self,
|
|
152
|
+
llm: Any,
|
|
153
|
+
tools_rag: Any = None,
|
|
154
|
+
terminal: Any = None,
|
|
155
|
+
ptt_tracker: Any = None,
|
|
156
|
+
human_confirm_exploits: bool = True,
|
|
157
|
+
on_think: Optional[Callable] = None,
|
|
158
|
+
on_select: Optional[Callable] = None,
|
|
159
|
+
on_execute: Optional[Callable] = None,
|
|
160
|
+
on_learn: Optional[Callable] = None,
|
|
161
|
+
):
|
|
162
|
+
"""
|
|
163
|
+
Initialize AIPT Agent.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
llm: LLM provider instance
|
|
167
|
+
tools_rag: Tool RAG for tool selection
|
|
168
|
+
terminal: Terminal executor for running commands
|
|
169
|
+
ptt_tracker: PTT tracker for task management
|
|
170
|
+
human_confirm_exploits: Require human confirmation for exploits
|
|
171
|
+
on_think: Callback for think phase
|
|
172
|
+
on_select: Callback for select phase
|
|
173
|
+
on_execute: Callback for execute phase
|
|
174
|
+
on_learn: Callback for learn phase
|
|
175
|
+
"""
|
|
176
|
+
self.llm = llm
|
|
177
|
+
self.tools_rag = tools_rag
|
|
178
|
+
self.terminal = terminal
|
|
179
|
+
self.ptt = ptt_tracker
|
|
180
|
+
self.human_confirm_exploits = human_confirm_exploits
|
|
181
|
+
self.memory = MemoryManager(llm)
|
|
182
|
+
|
|
183
|
+
# Callbacks
|
|
184
|
+
self.on_think = on_think
|
|
185
|
+
self.on_select = on_select
|
|
186
|
+
self.on_execute = on_execute
|
|
187
|
+
self.on_learn = on_learn
|
|
188
|
+
|
|
189
|
+
# Build the graph if LangGraph is available
|
|
190
|
+
self.graph = self._build_graph() if LANGGRAPH_AVAILABLE else None
|
|
191
|
+
|
|
192
|
+
def _build_graph(self):
|
|
193
|
+
"""Build the LangGraph state machine"""
|
|
194
|
+
if not LANGGRAPH_AVAILABLE:
|
|
195
|
+
return None
|
|
196
|
+
|
|
197
|
+
graph = StateGraph(PentestState)
|
|
198
|
+
|
|
199
|
+
# Add nodes
|
|
200
|
+
graph.add_node("think", self._think)
|
|
201
|
+
graph.add_node("select", self._select)
|
|
202
|
+
graph.add_node("execute", self._execute)
|
|
203
|
+
graph.add_node("learn", self._learn)
|
|
204
|
+
|
|
205
|
+
# Add edges
|
|
206
|
+
graph.add_edge("think", "select")
|
|
207
|
+
graph.add_conditional_edges(
|
|
208
|
+
"select",
|
|
209
|
+
self._should_execute,
|
|
210
|
+
{
|
|
211
|
+
"execute": "execute",
|
|
212
|
+
"think": "think",
|
|
213
|
+
"end": END,
|
|
214
|
+
}
|
|
215
|
+
)
|
|
216
|
+
graph.add_edge("execute", "learn")
|
|
217
|
+
graph.add_conditional_edges(
|
|
218
|
+
"learn",
|
|
219
|
+
self._should_continue,
|
|
220
|
+
{
|
|
221
|
+
"continue": "think",
|
|
222
|
+
"end": END,
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Set entry point
|
|
227
|
+
graph.set_entry_point("think")
|
|
228
|
+
|
|
229
|
+
return graph.compile()
|
|
230
|
+
|
|
231
|
+
def _think(self, state: PentestState) -> dict:
|
|
232
|
+
"""THINK node: Analyze current state and decide next action."""
|
|
233
|
+
if self.on_think:
|
|
234
|
+
self.on_think(state)
|
|
235
|
+
|
|
236
|
+
# Build context
|
|
237
|
+
ptt_str = self._format_ptt(state)
|
|
238
|
+
findings_str = self._format_recent_findings(state.findings[-5:])
|
|
239
|
+
|
|
240
|
+
system_prompt = self.SYSTEM_PROMPT.format(
|
|
241
|
+
phase=state.phase.value.upper(),
|
|
242
|
+
target=state.target,
|
|
243
|
+
iteration=state.iteration,
|
|
244
|
+
max_iterations=state.max_iterations,
|
|
245
|
+
ptt=ptt_str,
|
|
246
|
+
findings=findings_str,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.memory.add_system(system_prompt)
|
|
250
|
+
|
|
251
|
+
# Build user prompt
|
|
252
|
+
user_prompt = f"""Based on the current state, what should we do next?
|
|
253
|
+
|
|
254
|
+
Target: {state.target}
|
|
255
|
+
Phase: {state.phase.value}
|
|
256
|
+
Last Output: {state.last_output[:1000] if state.last_output else 'None'}
|
|
257
|
+
|
|
258
|
+
Provide:
|
|
259
|
+
1. ANALYSIS: What do we know so far?
|
|
260
|
+
2. OBJECTIVE: What should we do next?
|
|
261
|
+
3. TOOL_HINT: What type of tool would help? (e.g., "port scanner", "directory brute-forcer")
|
|
262
|
+
|
|
263
|
+
Be specific and actionable."""
|
|
264
|
+
|
|
265
|
+
self.memory.add_user(user_prompt)
|
|
266
|
+
|
|
267
|
+
# Get LLM response
|
|
268
|
+
messages = self.memory.get_messages()
|
|
269
|
+
response = self.llm.invoke(messages)
|
|
270
|
+
|
|
271
|
+
self.memory.add_assistant(response.content)
|
|
272
|
+
|
|
273
|
+
# Extract objective from response
|
|
274
|
+
objective = self._extract_objective(response.content)
|
|
275
|
+
|
|
276
|
+
return {"current_objective": objective}
|
|
277
|
+
|
|
278
|
+
def _select(self, state: PentestState) -> dict:
|
|
279
|
+
"""SELECT node: Use RAG to choose the best tool for the objective."""
|
|
280
|
+
if self.on_select:
|
|
281
|
+
self.on_select(state)
|
|
282
|
+
|
|
283
|
+
if not state.current_objective:
|
|
284
|
+
return {"selected_tool": None, "error": "No objective set"}
|
|
285
|
+
|
|
286
|
+
if not self.tools_rag:
|
|
287
|
+
# No RAG available, use LLM to suggest tool
|
|
288
|
+
return self._select_tool_via_llm(state)
|
|
289
|
+
|
|
290
|
+
try:
|
|
291
|
+
tools = self.tools_rag.search(
|
|
292
|
+
query=state.current_objective,
|
|
293
|
+
phase=state.phase.value,
|
|
294
|
+
top_k=3,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if not tools:
|
|
298
|
+
return {"selected_tool": None, "error": "No suitable tools found"}
|
|
299
|
+
|
|
300
|
+
selected = tools[0]
|
|
301
|
+
command = self._build_command(selected, state.target)
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
"selected_tool": selected,
|
|
305
|
+
"tool_command": command,
|
|
306
|
+
"error": None,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
return {"selected_tool": None, "error": str(e)}
|
|
311
|
+
|
|
312
|
+
def _select_tool_via_llm(self, state: PentestState) -> dict:
|
|
313
|
+
"""Select tool using LLM when RAG is not available"""
|
|
314
|
+
prompt = f"""Given the objective: {state.current_objective}
|
|
315
|
+
Target: {state.target}
|
|
316
|
+
Phase: {state.phase.value}
|
|
317
|
+
|
|
318
|
+
Suggest a security tool and command to achieve this objective.
|
|
319
|
+
Return JSON: {{"tool": "tool_name", "command": "full command"}}"""
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
response = self.llm.invoke([
|
|
323
|
+
{"role": "system", "content": "You are a security tool expert. Return only valid JSON."},
|
|
324
|
+
{"role": "user", "content": prompt}
|
|
325
|
+
])
|
|
326
|
+
|
|
327
|
+
content = response.content.strip()
|
|
328
|
+
if content.startswith("```"):
|
|
329
|
+
content = content.split("```")[1]
|
|
330
|
+
if content.startswith("json"):
|
|
331
|
+
content = content[4:]
|
|
332
|
+
|
|
333
|
+
tool_info = json.loads(content)
|
|
334
|
+
return {
|
|
335
|
+
"selected_tool": {"name": tool_info.get("tool", "unknown")},
|
|
336
|
+
"tool_command": tool_info.get("command", ""),
|
|
337
|
+
"error": None,
|
|
338
|
+
}
|
|
339
|
+
except Exception as e:
|
|
340
|
+
return {"selected_tool": None, "error": f"Tool selection failed: {e}"}
|
|
341
|
+
|
|
342
|
+
def _should_execute(self, state: PentestState) -> str:
|
|
343
|
+
"""Decide whether to execute, rethink, or end"""
|
|
344
|
+
if state.error:
|
|
345
|
+
return "think"
|
|
346
|
+
|
|
347
|
+
if not state.selected_tool:
|
|
348
|
+
return "think"
|
|
349
|
+
|
|
350
|
+
if state.phase == Phase.EXPLOIT and self.human_confirm_exploits:
|
|
351
|
+
state.needs_human_confirm = True
|
|
352
|
+
|
|
353
|
+
return "execute"
|
|
354
|
+
|
|
355
|
+
def _execute(self, state: PentestState) -> dict:
|
|
356
|
+
"""EXECUTE node: Run the selected tool."""
|
|
357
|
+
if self.on_execute:
|
|
358
|
+
self.on_execute(state)
|
|
359
|
+
|
|
360
|
+
if not state.tool_command:
|
|
361
|
+
return {"last_output": "", "error": "No command to execute"}
|
|
362
|
+
|
|
363
|
+
if not self.terminal:
|
|
364
|
+
return {"last_output": "[No terminal configured]", "error": "Terminal not available"}
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
result = self.terminal.execute(
|
|
368
|
+
command=state.tool_command,
|
|
369
|
+
timeout=300,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
"last_output": result.output if hasattr(result, 'output') else str(result),
|
|
374
|
+
"error": result.error if hasattr(result, 'error') and result.error else None,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
except Exception as e:
|
|
378
|
+
return {"last_output": "", "error": str(e)}
|
|
379
|
+
|
|
380
|
+
def _learn(self, state: PentestState) -> dict:
|
|
381
|
+
"""LEARN node: Extract findings, update PTT, decide next steps."""
|
|
382
|
+
if self.on_learn:
|
|
383
|
+
self.on_learn(state)
|
|
384
|
+
|
|
385
|
+
updates = {}
|
|
386
|
+
|
|
387
|
+
# Parse output for findings
|
|
388
|
+
if state.last_output:
|
|
389
|
+
findings = self._extract_findings(
|
|
390
|
+
state.last_output,
|
|
391
|
+
state.selected_tool,
|
|
392
|
+
state.phase,
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if findings:
|
|
396
|
+
current_findings = list(state.findings)
|
|
397
|
+
current_findings.extend(findings)
|
|
398
|
+
updates["findings"] = current_findings
|
|
399
|
+
|
|
400
|
+
# Update PTT
|
|
401
|
+
self._update_ptt(state, findings)
|
|
402
|
+
|
|
403
|
+
# Increment iteration
|
|
404
|
+
updates["iteration"] = state.iteration + 1
|
|
405
|
+
|
|
406
|
+
# Check for phase transition
|
|
407
|
+
new_phase = self._check_phase_transition(state)
|
|
408
|
+
if new_phase != state.phase:
|
|
409
|
+
updates["phase"] = new_phase
|
|
410
|
+
|
|
411
|
+
return updates
|
|
412
|
+
|
|
413
|
+
def _should_continue(self, state: PentestState) -> str:
|
|
414
|
+
"""Decide whether to continue or end"""
|
|
415
|
+
if state.iteration >= state.max_iterations:
|
|
416
|
+
return "end"
|
|
417
|
+
|
|
418
|
+
if state.phase == Phase.REPORT:
|
|
419
|
+
return "end"
|
|
420
|
+
|
|
421
|
+
if self._has_shell_access(state.findings):
|
|
422
|
+
return "end"
|
|
423
|
+
|
|
424
|
+
if state.error and "CRITICAL" in str(state.error):
|
|
425
|
+
return "end"
|
|
426
|
+
|
|
427
|
+
return "continue"
|
|
428
|
+
|
|
429
|
+
def run(
|
|
430
|
+
self,
|
|
431
|
+
target: str,
|
|
432
|
+
scope: Optional[list[str]] = None,
|
|
433
|
+
max_iterations: int = 100,
|
|
434
|
+
start_phase: Phase = Phase.RECON,
|
|
435
|
+
) -> PentestState:
|
|
436
|
+
"""
|
|
437
|
+
Run the autonomous pentest agent.
|
|
438
|
+
|
|
439
|
+
Args:
|
|
440
|
+
target: Primary target (IP, domain, range)
|
|
441
|
+
scope: List of in-scope targets (defaults to target only)
|
|
442
|
+
max_iterations: Maximum iterations
|
|
443
|
+
start_phase: Starting phase
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
Final PentestState with all findings
|
|
447
|
+
"""
|
|
448
|
+
initial_state = PentestState(
|
|
449
|
+
target=target,
|
|
450
|
+
scope=scope or [target],
|
|
451
|
+
phase=start_phase,
|
|
452
|
+
max_iterations=max_iterations,
|
|
453
|
+
ptt=self._initialize_ptt(target),
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
if self.graph:
|
|
457
|
+
# Use LangGraph
|
|
458
|
+
final_state = self.graph.invoke(initial_state)
|
|
459
|
+
else:
|
|
460
|
+
# Fallback: manual loop
|
|
461
|
+
final_state = self._run_manual_loop(initial_state)
|
|
462
|
+
|
|
463
|
+
return final_state
|
|
464
|
+
|
|
465
|
+
def _run_manual_loop(self, state: PentestState) -> PentestState:
|
|
466
|
+
"""Manual execution loop when LangGraph is not available"""
|
|
467
|
+
while state.iteration < state.max_iterations:
|
|
468
|
+
# Think
|
|
469
|
+
updates = self._think(state)
|
|
470
|
+
state.current_objective = updates.get("current_objective", "")
|
|
471
|
+
|
|
472
|
+
# Select
|
|
473
|
+
updates = self._select(state)
|
|
474
|
+
state.selected_tool = updates.get("selected_tool")
|
|
475
|
+
state.tool_command = updates.get("tool_command", "")
|
|
476
|
+
state.error = updates.get("error")
|
|
477
|
+
|
|
478
|
+
# Check if should execute
|
|
479
|
+
if self._should_execute(state) != "execute":
|
|
480
|
+
continue
|
|
481
|
+
|
|
482
|
+
# Execute
|
|
483
|
+
updates = self._execute(state)
|
|
484
|
+
state.last_output = updates.get("last_output", "")
|
|
485
|
+
state.error = updates.get("error")
|
|
486
|
+
|
|
487
|
+
# Learn
|
|
488
|
+
updates = self._learn(state)
|
|
489
|
+
state.iteration = updates.get("iteration", state.iteration)
|
|
490
|
+
state.phase = updates.get("phase", state.phase)
|
|
491
|
+
if "findings" in updates:
|
|
492
|
+
state.findings = updates["findings"]
|
|
493
|
+
|
|
494
|
+
# Check if should continue
|
|
495
|
+
if self._should_continue(state) == "end":
|
|
496
|
+
break
|
|
497
|
+
|
|
498
|
+
return state
|
|
499
|
+
|
|
500
|
+
# ============== Helper Methods ==============
|
|
501
|
+
|
|
502
|
+
def _format_ptt(self, state: PentestState) -> str:
|
|
503
|
+
"""Format PTT for prompt"""
|
|
504
|
+
if self.ptt and hasattr(self.ptt, 'to_prompt'):
|
|
505
|
+
return self.ptt.to_prompt()
|
|
506
|
+
return json.dumps(state.ptt, indent=2) if state.ptt else "No tasks yet."
|
|
507
|
+
|
|
508
|
+
def _initialize_ptt(self, target: str) -> dict:
|
|
509
|
+
"""Initialize PTT structure"""
|
|
510
|
+
if self.ptt and hasattr(self.ptt, 'initialize'):
|
|
511
|
+
return self.ptt.initialize(target)
|
|
512
|
+
return {
|
|
513
|
+
"target": target,
|
|
514
|
+
"phases": {
|
|
515
|
+
"recon": {"status": "pending", "tasks": []},
|
|
516
|
+
"enum": {"status": "pending", "tasks": []},
|
|
517
|
+
"exploit": {"status": "pending", "tasks": []},
|
|
518
|
+
"post": {"status": "pending", "tasks": []},
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
def _extract_objective(self, llm_response: str) -> str:
|
|
523
|
+
"""Extract objective from LLM response"""
|
|
524
|
+
lines = llm_response.split("\n")
|
|
525
|
+
for line in lines:
|
|
526
|
+
if "OBJECTIVE:" in line.upper():
|
|
527
|
+
return line.split(":", 1)[1].strip()
|
|
528
|
+
|
|
529
|
+
for line in reversed(lines):
|
|
530
|
+
if line.strip() and len(line.strip()) > 10:
|
|
531
|
+
return line.strip()
|
|
532
|
+
|
|
533
|
+
return "Continue reconnaissance"
|
|
534
|
+
|
|
535
|
+
def _build_command(self, tool: dict, target: str) -> str:
|
|
536
|
+
"""Build command string from tool definition"""
|
|
537
|
+
cmd_template = tool.get("cmd", tool.get("command", ""))
|
|
538
|
+
return cmd_template.replace("{target}", target).replace("{url}", target)
|
|
539
|
+
|
|
540
|
+
def _format_recent_findings(self, findings: list[dict]) -> str:
|
|
541
|
+
"""Format findings for prompt"""
|
|
542
|
+
if not findings:
|
|
543
|
+
return "No findings yet."
|
|
544
|
+
|
|
545
|
+
lines = []
|
|
546
|
+
for f in findings:
|
|
547
|
+
lines.append(f"- [{f.get('type', 'info')}] {f.get('description', 'Unknown')}")
|
|
548
|
+
return "\n".join(lines)
|
|
549
|
+
|
|
550
|
+
def _extract_findings(
|
|
551
|
+
self,
|
|
552
|
+
output: str,
|
|
553
|
+
tool: Optional[dict],
|
|
554
|
+
phase: Phase,
|
|
555
|
+
) -> list[dict]:
|
|
556
|
+
"""Extract structured findings from tool output using LLM."""
|
|
557
|
+
if not output or len(output) < 10:
|
|
558
|
+
return []
|
|
559
|
+
|
|
560
|
+
extract_prompt = f"""Analyze this security tool output and extract findings.
|
|
561
|
+
|
|
562
|
+
Tool: {tool.get('name', 'unknown') if tool else 'unknown'}
|
|
563
|
+
Phase: {phase.value}
|
|
564
|
+
|
|
565
|
+
Output:
|
|
566
|
+
{output[:5000]}
|
|
567
|
+
|
|
568
|
+
Extract findings in this JSON format:
|
|
569
|
+
[
|
|
570
|
+
{{"type": "port|service|vuln|credential|host", "description": "brief description", "severity": "info|low|medium|high|critical", "data": {{}}}}
|
|
571
|
+
]
|
|
572
|
+
|
|
573
|
+
Only return valid JSON array. If no findings, return []."""
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
response = self.llm.invoke([
|
|
577
|
+
{"role": "system", "content": "You are a security findings parser. Return only valid JSON."},
|
|
578
|
+
{"role": "user", "content": extract_prompt},
|
|
579
|
+
], max_tokens=1000)
|
|
580
|
+
|
|
581
|
+
content = response.content.strip()
|
|
582
|
+
if content.startswith("```"):
|
|
583
|
+
content = content.split("```")[1]
|
|
584
|
+
if content.startswith("json"):
|
|
585
|
+
content = content[4:]
|
|
586
|
+
|
|
587
|
+
return json.loads(content)
|
|
588
|
+
except (json.JSONDecodeError, Exception):
|
|
589
|
+
return [{
|
|
590
|
+
"type": "info",
|
|
591
|
+
"description": f"Tool output captured ({len(output)} chars)",
|
|
592
|
+
"severity": "info",
|
|
593
|
+
"data": {"raw_length": len(output)},
|
|
594
|
+
}]
|
|
595
|
+
|
|
596
|
+
def _update_ptt(self, state: PentestState, findings: list[dict]) -> None:
|
|
597
|
+
"""Update PTT with new findings"""
|
|
598
|
+
if self.ptt and hasattr(self.ptt, 'add_findings'):
|
|
599
|
+
self.ptt.add_findings(state.phase.value, findings)
|
|
600
|
+
|
|
601
|
+
def _check_phase_transition(self, state: PentestState) -> Phase:
|
|
602
|
+
"""Check if we should move to next phase"""
|
|
603
|
+
finding_types = [f.get("type") for f in state.findings]
|
|
604
|
+
|
|
605
|
+
if state.phase == Phase.RECON:
|
|
606
|
+
if "port" in finding_types or "service" in finding_types:
|
|
607
|
+
if len([f for f in finding_types if f in ["port", "service"]]) >= 3:
|
|
608
|
+
return Phase.ENUM
|
|
609
|
+
|
|
610
|
+
elif state.phase == Phase.ENUM:
|
|
611
|
+
if "vuln" in finding_types:
|
|
612
|
+
return Phase.EXPLOIT
|
|
613
|
+
|
|
614
|
+
elif state.phase == Phase.EXPLOIT:
|
|
615
|
+
if "credential" in finding_types or self._has_shell_access(state.findings):
|
|
616
|
+
return Phase.POST
|
|
617
|
+
|
|
618
|
+
elif state.phase == Phase.POST:
|
|
619
|
+
if state.iteration > 10:
|
|
620
|
+
return Phase.REPORT
|
|
621
|
+
|
|
622
|
+
return state.phase
|
|
623
|
+
|
|
624
|
+
def _has_shell_access(self, findings: list[dict]) -> bool:
|
|
625
|
+
"""Check if we've obtained shell access"""
|
|
626
|
+
for f in findings:
|
|
627
|
+
desc = f.get("description", "").lower()
|
|
628
|
+
if any(kw in desc for kw in ["shell", "rce", "command execution", "reverse shell"]):
|
|
629
|
+
return True
|
|
630
|
+
return False
|