aiptx 2.0.2__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.

Potentially problematic release.


This version of aiptx might be problematic. Click here for more details.

Files changed (165) hide show
  1. aipt_v2/__init__.py +110 -0
  2. aipt_v2/__main__.py +24 -0
  3. aipt_v2/agents/AIPTxAgent/__init__.py +10 -0
  4. aipt_v2/agents/AIPTxAgent/aiptx_agent.py +211 -0
  5. aipt_v2/agents/__init__.py +24 -0
  6. aipt_v2/agents/base.py +520 -0
  7. aipt_v2/agents/ptt.py +406 -0
  8. aipt_v2/agents/state.py +168 -0
  9. aipt_v2/app.py +960 -0
  10. aipt_v2/browser/__init__.py +31 -0
  11. aipt_v2/browser/automation.py +458 -0
  12. aipt_v2/browser/crawler.py +453 -0
  13. aipt_v2/cli.py +321 -0
  14. aipt_v2/compliance/__init__.py +71 -0
  15. aipt_v2/compliance/compliance_report.py +449 -0
  16. aipt_v2/compliance/framework_mapper.py +424 -0
  17. aipt_v2/compliance/nist_mapping.py +345 -0
  18. aipt_v2/compliance/owasp_mapping.py +330 -0
  19. aipt_v2/compliance/pci_mapping.py +297 -0
  20. aipt_v2/config.py +288 -0
  21. aipt_v2/core/__init__.py +43 -0
  22. aipt_v2/core/agent.py +630 -0
  23. aipt_v2/core/llm.py +395 -0
  24. aipt_v2/core/memory.py +305 -0
  25. aipt_v2/core/ptt.py +329 -0
  26. aipt_v2/database/__init__.py +14 -0
  27. aipt_v2/database/models.py +232 -0
  28. aipt_v2/database/repository.py +384 -0
  29. aipt_v2/docker/__init__.py +23 -0
  30. aipt_v2/docker/builder.py +260 -0
  31. aipt_v2/docker/manager.py +222 -0
  32. aipt_v2/docker/sandbox.py +371 -0
  33. aipt_v2/evasion/__init__.py +58 -0
  34. aipt_v2/evasion/request_obfuscator.py +272 -0
  35. aipt_v2/evasion/tls_fingerprint.py +285 -0
  36. aipt_v2/evasion/ua_rotator.py +301 -0
  37. aipt_v2/evasion/waf_bypass.py +439 -0
  38. aipt_v2/execution/__init__.py +23 -0
  39. aipt_v2/execution/executor.py +302 -0
  40. aipt_v2/execution/parser.py +544 -0
  41. aipt_v2/execution/terminal.py +337 -0
  42. aipt_v2/health.py +437 -0
  43. aipt_v2/intelligence/__init__.py +85 -0
  44. aipt_v2/intelligence/auth.py +520 -0
  45. aipt_v2/intelligence/chaining.py +775 -0
  46. aipt_v2/intelligence/cve_aipt.py +334 -0
  47. aipt_v2/intelligence/cve_info.py +1111 -0
  48. aipt_v2/intelligence/rag.py +239 -0
  49. aipt_v2/intelligence/scope.py +442 -0
  50. aipt_v2/intelligence/searchers/__init__.py +5 -0
  51. aipt_v2/intelligence/searchers/exploitdb_searcher.py +523 -0
  52. aipt_v2/intelligence/searchers/github_searcher.py +467 -0
  53. aipt_v2/intelligence/searchers/google_searcher.py +281 -0
  54. aipt_v2/intelligence/tools.json +443 -0
  55. aipt_v2/intelligence/triage.py +670 -0
  56. aipt_v2/interface/__init__.py +5 -0
  57. aipt_v2/interface/cli.py +230 -0
  58. aipt_v2/interface/main.py +501 -0
  59. aipt_v2/interface/tui.py +1276 -0
  60. aipt_v2/interface/utils.py +583 -0
  61. aipt_v2/llm/__init__.py +39 -0
  62. aipt_v2/llm/config.py +26 -0
  63. aipt_v2/llm/llm.py +514 -0
  64. aipt_v2/llm/memory.py +214 -0
  65. aipt_v2/llm/request_queue.py +89 -0
  66. aipt_v2/llm/utils.py +89 -0
  67. aipt_v2/models/__init__.py +15 -0
  68. aipt_v2/models/findings.py +295 -0
  69. aipt_v2/models/phase_result.py +224 -0
  70. aipt_v2/models/scan_config.py +207 -0
  71. aipt_v2/monitoring/grafana/dashboards/aipt-dashboard.json +355 -0
  72. aipt_v2/monitoring/grafana/dashboards/default.yml +17 -0
  73. aipt_v2/monitoring/grafana/datasources/prometheus.yml +17 -0
  74. aipt_v2/monitoring/prometheus.yml +60 -0
  75. aipt_v2/orchestration/__init__.py +52 -0
  76. aipt_v2/orchestration/pipeline.py +398 -0
  77. aipt_v2/orchestration/progress.py +300 -0
  78. aipt_v2/orchestration/scheduler.py +296 -0
  79. aipt_v2/orchestrator.py +2284 -0
  80. aipt_v2/payloads/__init__.py +27 -0
  81. aipt_v2/payloads/cmdi.py +150 -0
  82. aipt_v2/payloads/sqli.py +263 -0
  83. aipt_v2/payloads/ssrf.py +204 -0
  84. aipt_v2/payloads/templates.py +222 -0
  85. aipt_v2/payloads/traversal.py +166 -0
  86. aipt_v2/payloads/xss.py +204 -0
  87. aipt_v2/prompts/__init__.py +60 -0
  88. aipt_v2/proxy/__init__.py +29 -0
  89. aipt_v2/proxy/history.py +352 -0
  90. aipt_v2/proxy/interceptor.py +452 -0
  91. aipt_v2/recon/__init__.py +44 -0
  92. aipt_v2/recon/dns.py +241 -0
  93. aipt_v2/recon/osint.py +367 -0
  94. aipt_v2/recon/subdomain.py +372 -0
  95. aipt_v2/recon/tech_detect.py +311 -0
  96. aipt_v2/reports/__init__.py +17 -0
  97. aipt_v2/reports/generator.py +313 -0
  98. aipt_v2/reports/html_report.py +378 -0
  99. aipt_v2/runtime/__init__.py +44 -0
  100. aipt_v2/runtime/base.py +30 -0
  101. aipt_v2/runtime/docker.py +401 -0
  102. aipt_v2/runtime/local.py +346 -0
  103. aipt_v2/runtime/tool_server.py +205 -0
  104. aipt_v2/scanners/__init__.py +28 -0
  105. aipt_v2/scanners/base.py +273 -0
  106. aipt_v2/scanners/nikto.py +244 -0
  107. aipt_v2/scanners/nmap.py +402 -0
  108. aipt_v2/scanners/nuclei.py +273 -0
  109. aipt_v2/scanners/web.py +454 -0
  110. aipt_v2/scripts/security_audit.py +366 -0
  111. aipt_v2/telemetry/__init__.py +7 -0
  112. aipt_v2/telemetry/tracer.py +347 -0
  113. aipt_v2/terminal/__init__.py +28 -0
  114. aipt_v2/terminal/executor.py +400 -0
  115. aipt_v2/terminal/sandbox.py +350 -0
  116. aipt_v2/tools/__init__.py +44 -0
  117. aipt_v2/tools/active_directory/__init__.py +78 -0
  118. aipt_v2/tools/active_directory/ad_config.py +238 -0
  119. aipt_v2/tools/active_directory/bloodhound_wrapper.py +447 -0
  120. aipt_v2/tools/active_directory/kerberos_attacks.py +430 -0
  121. aipt_v2/tools/active_directory/ldap_enum.py +533 -0
  122. aipt_v2/tools/active_directory/smb_attacks.py +505 -0
  123. aipt_v2/tools/agents_graph/__init__.py +19 -0
  124. aipt_v2/tools/agents_graph/agents_graph_actions.py +69 -0
  125. aipt_v2/tools/api_security/__init__.py +76 -0
  126. aipt_v2/tools/api_security/api_discovery.py +608 -0
  127. aipt_v2/tools/api_security/graphql_scanner.py +622 -0
  128. aipt_v2/tools/api_security/jwt_analyzer.py +577 -0
  129. aipt_v2/tools/api_security/openapi_fuzzer.py +761 -0
  130. aipt_v2/tools/browser/__init__.py +5 -0
  131. aipt_v2/tools/browser/browser_actions.py +238 -0
  132. aipt_v2/tools/browser/browser_instance.py +535 -0
  133. aipt_v2/tools/browser/tab_manager.py +344 -0
  134. aipt_v2/tools/cloud/__init__.py +70 -0
  135. aipt_v2/tools/cloud/cloud_config.py +273 -0
  136. aipt_v2/tools/cloud/cloud_scanner.py +639 -0
  137. aipt_v2/tools/cloud/prowler_tool.py +571 -0
  138. aipt_v2/tools/cloud/scoutsuite_tool.py +359 -0
  139. aipt_v2/tools/executor.py +307 -0
  140. aipt_v2/tools/parser.py +408 -0
  141. aipt_v2/tools/proxy/__init__.py +5 -0
  142. aipt_v2/tools/proxy/proxy_actions.py +103 -0
  143. aipt_v2/tools/proxy/proxy_manager.py +789 -0
  144. aipt_v2/tools/registry.py +196 -0
  145. aipt_v2/tools/scanners/__init__.py +343 -0
  146. aipt_v2/tools/scanners/acunetix_tool.py +712 -0
  147. aipt_v2/tools/scanners/burp_tool.py +631 -0
  148. aipt_v2/tools/scanners/config.py +156 -0
  149. aipt_v2/tools/scanners/nessus_tool.py +588 -0
  150. aipt_v2/tools/scanners/zap_tool.py +612 -0
  151. aipt_v2/tools/terminal/__init__.py +5 -0
  152. aipt_v2/tools/terminal/terminal_actions.py +37 -0
  153. aipt_v2/tools/terminal/terminal_manager.py +153 -0
  154. aipt_v2/tools/terminal/terminal_session.py +449 -0
  155. aipt_v2/tools/tool_processing.py +108 -0
  156. aipt_v2/utils/__init__.py +17 -0
  157. aipt_v2/utils/logging.py +201 -0
  158. aipt_v2/utils/model_manager.py +187 -0
  159. aipt_v2/utils/searchers/__init__.py +269 -0
  160. aiptx-2.0.2.dist-info/METADATA +324 -0
  161. aiptx-2.0.2.dist-info/RECORD +165 -0
  162. aiptx-2.0.2.dist-info/WHEEL +5 -0
  163. aiptx-2.0.2.dist-info/entry_points.txt +7 -0
  164. aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
  165. aiptx-2.0.2.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