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.
- 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 +24 -0
- aipt_v2/agents/base.py +520 -0
- aipt_v2/agents/ptt.py +406 -0
- aipt_v2/agents/state.py +168 -0
- aipt_v2/app.py +960 -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 +321 -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 +288 -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 +85 -0
- aipt_v2/intelligence/auth.py +520 -0
- aipt_v2/intelligence/chaining.py +775 -0
- aipt_v2/intelligence/cve_aipt.py +334 -0
- aipt_v2/intelligence/cve_info.py +1111 -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/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/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 +2284 -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 +44 -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/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/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 +201 -0
- aipt_v2/utils/model_manager.py +187 -0
- aipt_v2/utils/searchers/__init__.py +269 -0
- aiptx-2.0.2.dist-info/METADATA +324 -0
- aiptx-2.0.2.dist-info/RECORD +165 -0
- aiptx-2.0.2.dist-info/WHEEL +5 -0
- aiptx-2.0.2.dist-info/entry_points.txt +7 -0
- aiptx-2.0.2.dist-info/licenses/LICENSE +21 -0
- aiptx-2.0.2.dist-info/top_level.txt +1 -0
aipt_v2/agents/ptt.py
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIPT Penetration Testing Tree (PTT) - Hierarchical task tracking
|
|
3
|
+
Tracks progress through pentest phases with visual feedback.
|
|
4
|
+
|
|
5
|
+
Inspired by: PentestGPT's PTT structure
|
|
6
|
+
Format:
|
|
7
|
+
1. Reconnaissance - [completed]
|
|
8
|
+
1.1 Passive Information Gathering - (completed)
|
|
9
|
+
1.2 Active Scanning - (in-progress)
|
|
10
|
+
2. Enumeration - [to-do]
|
|
11
|
+
2.1 Service Enumeration - (to-do)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import Optional
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from datetime import datetime
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskStatus(str, Enum):
|
|
23
|
+
"""Status of a task"""
|
|
24
|
+
TODO = "to-do"
|
|
25
|
+
IN_PROGRESS = "in-progress"
|
|
26
|
+
COMPLETED = "completed"
|
|
27
|
+
BLOCKED = "blocked"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PhaseType(str, Enum):
|
|
31
|
+
"""Penetration testing phases"""
|
|
32
|
+
RECON = "recon"
|
|
33
|
+
SCANNING = "enum"
|
|
34
|
+
EXPLOITATION = "exploit"
|
|
35
|
+
POST_EXPLOITATION = "post"
|
|
36
|
+
REPORTING = "report"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Task:
|
|
41
|
+
"""A single task in the PTT"""
|
|
42
|
+
id: str
|
|
43
|
+
description: str
|
|
44
|
+
status: TaskStatus = TaskStatus.TODO
|
|
45
|
+
findings: list[dict] = field(default_factory=list)
|
|
46
|
+
started_at: Optional[str] = None
|
|
47
|
+
completed_at: Optional[str] = None
|
|
48
|
+
notes: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class Phase:
|
|
53
|
+
"""A phase containing multiple tasks"""
|
|
54
|
+
name: str
|
|
55
|
+
description: str
|
|
56
|
+
status: TaskStatus = TaskStatus.TODO
|
|
57
|
+
tasks: list[Task] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class PTT:
|
|
61
|
+
"""
|
|
62
|
+
Penetration Testing Tree - Hierarchical task tracking.
|
|
63
|
+
|
|
64
|
+
Provides:
|
|
65
|
+
- Visual progress tracking
|
|
66
|
+
- Phase-based organization
|
|
67
|
+
- Finding association
|
|
68
|
+
- Session persistence
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
# Standard pentest phases
|
|
72
|
+
PHASES = [
|
|
73
|
+
("recon", "Reconnaissance and information gathering"),
|
|
74
|
+
("enum", "Enumeration of services and vulnerabilities"),
|
|
75
|
+
("exploit", "Exploitation of discovered vulnerabilities"),
|
|
76
|
+
("post", "Post-exploitation and privilege escalation"),
|
|
77
|
+
("report", "Documentation and report generation"),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
def __init__(self, session_dir: Optional[str] = None):
|
|
81
|
+
self.session_dir = Path(session_dir or "~/.aipt/sessions").expanduser()
|
|
82
|
+
self.session_dir.mkdir(parents=True, exist_ok=True)
|
|
83
|
+
|
|
84
|
+
self.target: str = ""
|
|
85
|
+
self.phases: dict[str, Phase] = {}
|
|
86
|
+
self.current_phase: str = "recon"
|
|
87
|
+
self.session_id: Optional[str] = None
|
|
88
|
+
|
|
89
|
+
def initialize(self, target: str) -> dict:
|
|
90
|
+
"""
|
|
91
|
+
Initialize PTT for a new target.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
target: Target being tested
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Initial PTT state as dict
|
|
98
|
+
"""
|
|
99
|
+
self.target = target
|
|
100
|
+
self.session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
101
|
+
|
|
102
|
+
# Initialize phases
|
|
103
|
+
self.phases = {}
|
|
104
|
+
for phase_name, phase_desc in self.PHASES:
|
|
105
|
+
self.phases[phase_name] = Phase(
|
|
106
|
+
name=phase_name,
|
|
107
|
+
description=phase_desc,
|
|
108
|
+
status=TaskStatus.TODO,
|
|
109
|
+
tasks=[],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Set first phase to in-progress
|
|
113
|
+
self.phases["recon"].status = TaskStatus.IN_PROGRESS
|
|
114
|
+
self.current_phase = "recon"
|
|
115
|
+
|
|
116
|
+
return self.to_dict()
|
|
117
|
+
|
|
118
|
+
def add_task(
|
|
119
|
+
self,
|
|
120
|
+
phase: str,
|
|
121
|
+
description: str,
|
|
122
|
+
status: TaskStatus = TaskStatus.TODO,
|
|
123
|
+
) -> str:
|
|
124
|
+
"""
|
|
125
|
+
Add a task to a phase.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
phase: Phase name (recon, enum, exploit, post, report)
|
|
129
|
+
description: Task description
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Task ID
|
|
133
|
+
"""
|
|
134
|
+
if phase not in self.phases:
|
|
135
|
+
raise ValueError(f"Unknown phase: {phase}")
|
|
136
|
+
|
|
137
|
+
phase_obj = self.phases[phase]
|
|
138
|
+
task_num = len(phase_obj.tasks) + 1
|
|
139
|
+
|
|
140
|
+
# Generate task ID (e.g., "R1", "E2")
|
|
141
|
+
phase_prefix = phase[0].upper()
|
|
142
|
+
task_id = f"{phase_prefix}{task_num}"
|
|
143
|
+
|
|
144
|
+
task = Task(
|
|
145
|
+
id=task_id,
|
|
146
|
+
description=description,
|
|
147
|
+
status=status,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
if status == TaskStatus.IN_PROGRESS:
|
|
151
|
+
task.started_at = datetime.now().isoformat()
|
|
152
|
+
|
|
153
|
+
phase_obj.tasks.append(task)
|
|
154
|
+
|
|
155
|
+
return task_id
|
|
156
|
+
|
|
157
|
+
def update_task(
|
|
158
|
+
self,
|
|
159
|
+
task_id: str,
|
|
160
|
+
status: Optional[TaskStatus] = None,
|
|
161
|
+
findings: Optional[list[dict]] = None,
|
|
162
|
+
notes: Optional[str] = None,
|
|
163
|
+
) -> bool:
|
|
164
|
+
"""
|
|
165
|
+
Update a task's status or findings.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
task_id: Task identifier
|
|
169
|
+
status: New status
|
|
170
|
+
findings: Findings to add
|
|
171
|
+
notes: Additional notes
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if task was found and updated
|
|
175
|
+
"""
|
|
176
|
+
task = self._find_task(task_id)
|
|
177
|
+
if not task:
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
if status:
|
|
181
|
+
task.status = status
|
|
182
|
+
if status == TaskStatus.IN_PROGRESS:
|
|
183
|
+
task.started_at = datetime.now().isoformat()
|
|
184
|
+
elif status == TaskStatus.COMPLETED:
|
|
185
|
+
task.completed_at = datetime.now().isoformat()
|
|
186
|
+
|
|
187
|
+
if findings:
|
|
188
|
+
task.findings.extend(findings)
|
|
189
|
+
|
|
190
|
+
if notes:
|
|
191
|
+
task.notes = notes
|
|
192
|
+
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
def complete_task(self, task_id: str, findings: Optional[list[dict]] = None) -> bool:
|
|
196
|
+
"""Mark a task as completed"""
|
|
197
|
+
return self.update_task(task_id, TaskStatus.COMPLETED, findings)
|
|
198
|
+
|
|
199
|
+
def add_findings(self, phase: str, findings: list[dict]) -> None:
|
|
200
|
+
"""
|
|
201
|
+
Add findings to the current active task in a phase.
|
|
202
|
+
If no active task, create one.
|
|
203
|
+
"""
|
|
204
|
+
if phase not in self.phases:
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
phase_obj = self.phases[phase]
|
|
208
|
+
|
|
209
|
+
# Find active task
|
|
210
|
+
active_task = None
|
|
211
|
+
for task in phase_obj.tasks:
|
|
212
|
+
if task.status == TaskStatus.IN_PROGRESS:
|
|
213
|
+
active_task = task
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
# Create task if none active
|
|
217
|
+
if not active_task:
|
|
218
|
+
task_id = self.add_task(phase, "Auto-generated task", TaskStatus.IN_PROGRESS)
|
|
219
|
+
active_task = self._find_task(task_id)
|
|
220
|
+
|
|
221
|
+
if active_task:
|
|
222
|
+
active_task.findings.extend(findings)
|
|
223
|
+
|
|
224
|
+
def advance_phase(self) -> str:
|
|
225
|
+
"""
|
|
226
|
+
Advance to the next phase.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
New phase name
|
|
230
|
+
"""
|
|
231
|
+
phase_names = [p[0] for p in self.PHASES]
|
|
232
|
+
current_idx = phase_names.index(self.current_phase)
|
|
233
|
+
|
|
234
|
+
if current_idx < len(phase_names) - 1:
|
|
235
|
+
# Complete current phase
|
|
236
|
+
self.phases[self.current_phase].status = TaskStatus.COMPLETED
|
|
237
|
+
|
|
238
|
+
# Move to next phase
|
|
239
|
+
self.current_phase = phase_names[current_idx + 1]
|
|
240
|
+
self.phases[self.current_phase].status = TaskStatus.IN_PROGRESS
|
|
241
|
+
|
|
242
|
+
return self.current_phase
|
|
243
|
+
|
|
244
|
+
def set_phase(self, phase: str) -> None:
|
|
245
|
+
"""Set current phase directly"""
|
|
246
|
+
if phase in self.phases:
|
|
247
|
+
self.current_phase = phase
|
|
248
|
+
self.phases[phase].status = TaskStatus.IN_PROGRESS
|
|
249
|
+
|
|
250
|
+
def _find_task(self, task_id: str) -> Optional[Task]:
|
|
251
|
+
"""Find a task by ID"""
|
|
252
|
+
for phase in self.phases.values():
|
|
253
|
+
for task in phase.tasks:
|
|
254
|
+
if task.id == task_id:
|
|
255
|
+
return task
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
def to_prompt(self) -> str:
|
|
259
|
+
"""
|
|
260
|
+
Generate PTT string for LLM prompt.
|
|
261
|
+
|
|
262
|
+
Format:
|
|
263
|
+
1. Reconnaissance - [completed]
|
|
264
|
+
R1. Port scanning - (completed) - 5 findings
|
|
265
|
+
R2. Service detection - (in-progress)
|
|
266
|
+
2. Enumeration - [in-progress]
|
|
267
|
+
E1. Directory brute-force - (to-do)
|
|
268
|
+
"""
|
|
269
|
+
lines = [f"Target: {self.target}", ""]
|
|
270
|
+
|
|
271
|
+
for i, (phase_name, _) in enumerate(self.PHASES, 1):
|
|
272
|
+
phase = self.phases.get(phase_name)
|
|
273
|
+
if not phase:
|
|
274
|
+
continue
|
|
275
|
+
|
|
276
|
+
# Phase line
|
|
277
|
+
status_str = f"[{phase.status.value}]"
|
|
278
|
+
phase_title = phase_name.title()
|
|
279
|
+
lines.append(f"{i}. {phase_title} - {status_str}")
|
|
280
|
+
|
|
281
|
+
# Task lines
|
|
282
|
+
for task in phase.tasks:
|
|
283
|
+
finding_count = len(task.findings)
|
|
284
|
+
finding_str = f" - {finding_count} findings" if finding_count else ""
|
|
285
|
+
status_str = f"({task.status.value})"
|
|
286
|
+
lines.append(f" {task.id}. {task.description} - {status_str}{finding_str}")
|
|
287
|
+
|
|
288
|
+
if not phase.tasks:
|
|
289
|
+
lines.append(" (no tasks yet)")
|
|
290
|
+
|
|
291
|
+
return "\n".join(lines)
|
|
292
|
+
|
|
293
|
+
def to_dict(self) -> dict:
|
|
294
|
+
"""Export PTT as dictionary"""
|
|
295
|
+
return {
|
|
296
|
+
"target": self.target,
|
|
297
|
+
"session_id": self.session_id,
|
|
298
|
+
"current_phase": self.current_phase,
|
|
299
|
+
"phases": {
|
|
300
|
+
name: {
|
|
301
|
+
"name": phase.name,
|
|
302
|
+
"description": phase.description,
|
|
303
|
+
"status": phase.status.value,
|
|
304
|
+
"tasks": [
|
|
305
|
+
{
|
|
306
|
+
"id": task.id,
|
|
307
|
+
"description": task.description,
|
|
308
|
+
"status": task.status.value,
|
|
309
|
+
"findings": task.findings,
|
|
310
|
+
"started_at": task.started_at,
|
|
311
|
+
"completed_at": task.completed_at,
|
|
312
|
+
"notes": task.notes,
|
|
313
|
+
}
|
|
314
|
+
for task in phase.tasks
|
|
315
|
+
],
|
|
316
|
+
}
|
|
317
|
+
for name, phase in self.phases.items()
|
|
318
|
+
},
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
def save(self, filename: Optional[str] = None) -> str:
|
|
322
|
+
"""
|
|
323
|
+
Save PTT to file.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Path to saved file
|
|
327
|
+
"""
|
|
328
|
+
if not filename:
|
|
329
|
+
filename = f"ptt_{self.session_id}_{self.target.replace('/', '_')}.json"
|
|
330
|
+
|
|
331
|
+
filepath = self.session_dir / filename
|
|
332
|
+
|
|
333
|
+
with open(filepath, "w") as f:
|
|
334
|
+
json.dump(self.to_dict(), f, indent=2)
|
|
335
|
+
|
|
336
|
+
return str(filepath)
|
|
337
|
+
|
|
338
|
+
def load(self, filepath: str) -> None:
|
|
339
|
+
"""Load PTT from file"""
|
|
340
|
+
with open(filepath, "r") as f:
|
|
341
|
+
data = json.load(f)
|
|
342
|
+
|
|
343
|
+
self.target = data.get("target", "")
|
|
344
|
+
self.session_id = data.get("session_id")
|
|
345
|
+
self.current_phase = data.get("current_phase", "recon")
|
|
346
|
+
|
|
347
|
+
self.phases = {}
|
|
348
|
+
for name, phase_data in data.get("phases", {}).items():
|
|
349
|
+
tasks = [
|
|
350
|
+
Task(
|
|
351
|
+
id=t["id"],
|
|
352
|
+
description=t["description"],
|
|
353
|
+
status=TaskStatus(t["status"]),
|
|
354
|
+
findings=t.get("findings", []),
|
|
355
|
+
started_at=t.get("started_at"),
|
|
356
|
+
completed_at=t.get("completed_at"),
|
|
357
|
+
notes=t.get("notes", ""),
|
|
358
|
+
)
|
|
359
|
+
for t in phase_data.get("tasks", [])
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
self.phases[name] = Phase(
|
|
363
|
+
name=phase_data["name"],
|
|
364
|
+
description=phase_data["description"],
|
|
365
|
+
status=TaskStatus(phase_data["status"]),
|
|
366
|
+
tasks=tasks,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def get_summary(self) -> dict:
|
|
370
|
+
"""Get summary statistics"""
|
|
371
|
+
total_tasks = 0
|
|
372
|
+
completed_tasks = 0
|
|
373
|
+
total_findings = 0
|
|
374
|
+
|
|
375
|
+
for phase in self.phases.values():
|
|
376
|
+
for task in phase.tasks:
|
|
377
|
+
total_tasks += 1
|
|
378
|
+
if task.status == TaskStatus.COMPLETED:
|
|
379
|
+
completed_tasks += 1
|
|
380
|
+
total_findings += len(task.findings)
|
|
381
|
+
|
|
382
|
+
completed_phases = sum(
|
|
383
|
+
1 for p in self.phases.values()
|
|
384
|
+
if p.status == TaskStatus.COMPLETED
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
return {
|
|
388
|
+
"target": self.target,
|
|
389
|
+
"current_phase": self.current_phase,
|
|
390
|
+
"phases_completed": completed_phases,
|
|
391
|
+
"phases_total": len(self.phases),
|
|
392
|
+
"tasks_completed": completed_tasks,
|
|
393
|
+
"tasks_total": total_tasks,
|
|
394
|
+
"total_findings": total_findings,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
def get_tasks_by_phase(self, phase: PhaseType) -> list[Task]:
|
|
398
|
+
"""Get all tasks for a specific phase"""
|
|
399
|
+
phase_name = phase.value if isinstance(phase, PhaseType) else phase
|
|
400
|
+
if phase_name in self.phases:
|
|
401
|
+
return self.phases[phase_name].tasks
|
|
402
|
+
return []
|
|
403
|
+
|
|
404
|
+
def update_task_status(self, task_id: str, status: TaskStatus) -> bool:
|
|
405
|
+
"""Update a task's status by ID"""
|
|
406
|
+
return self.update_task(task_id, status=status)
|
aipt_v2/agents/state.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import uuid
|
|
3
|
+
from datetime import datetime, timezone
|
|
4
|
+
from typing import Any, Optional, List, Dict
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _generate_agent_id() -> str:
|
|
10
|
+
return f"agent_{uuid.uuid4().hex[:8]}"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _now_iso() -> str:
|
|
14
|
+
return datetime.now(timezone.utc).isoformat()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AgentState(BaseModel):
|
|
18
|
+
agent_id: str = Field(default_factory=_generate_agent_id)
|
|
19
|
+
agent_name: str = "AIPT Agent"
|
|
20
|
+
parent_id: Optional[str] = None
|
|
21
|
+
sandbox_id: Optional[str] = None
|
|
22
|
+
sandbox_token: Optional[str] = None
|
|
23
|
+
sandbox_info: Optional[Dict[str, Any]] = None
|
|
24
|
+
|
|
25
|
+
task: str = ""
|
|
26
|
+
iteration: int = 0
|
|
27
|
+
max_iterations: int = 300
|
|
28
|
+
completed: bool = False
|
|
29
|
+
stop_requested: bool = False
|
|
30
|
+
waiting_for_input: bool = False
|
|
31
|
+
llm_failed: bool = False
|
|
32
|
+
waiting_start_time: Optional[datetime] = None
|
|
33
|
+
final_result: Optional[Dict[str, Any]] = None
|
|
34
|
+
max_iterations_warning_sent: bool = False
|
|
35
|
+
|
|
36
|
+
messages: List[Dict[str, Any]] = Field(default_factory=list)
|
|
37
|
+
context: Dict[str, Any] = Field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
start_time: str = Field(default_factory=_now_iso)
|
|
40
|
+
last_updated: str = Field(default_factory=_now_iso)
|
|
41
|
+
|
|
42
|
+
actions_taken: List[Dict[str, Any]] = Field(default_factory=list)
|
|
43
|
+
observations: List[Dict[str, Any]] = Field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
errors: List[str] = Field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
def increment_iteration(self) -> None:
|
|
48
|
+
self.iteration += 1
|
|
49
|
+
self.last_updated = _now_iso()
|
|
50
|
+
|
|
51
|
+
def add_message(self, role: str, content: Any) -> None:
|
|
52
|
+
self.messages.append({"role": role, "content": content})
|
|
53
|
+
self.last_updated = _now_iso()
|
|
54
|
+
|
|
55
|
+
def add_action(self, action: Dict[str, Any]) -> None:
|
|
56
|
+
self.actions_taken.append(
|
|
57
|
+
{
|
|
58
|
+
"iteration": self.iteration,
|
|
59
|
+
"timestamp": _now_iso(),
|
|
60
|
+
"action": action,
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def add_observation(self, observation: Dict[str, Any]) -> None:
|
|
65
|
+
self.observations.append(
|
|
66
|
+
{
|
|
67
|
+
"iteration": self.iteration,
|
|
68
|
+
"timestamp": _now_iso(),
|
|
69
|
+
"observation": observation,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def add_error(self, error: str) -> None:
|
|
74
|
+
self.errors.append(f"Iteration {self.iteration}: {error}")
|
|
75
|
+
self.last_updated = _now_iso()
|
|
76
|
+
|
|
77
|
+
def update_context(self, key: str, value: Any) -> None:
|
|
78
|
+
self.context[key] = value
|
|
79
|
+
self.last_updated = _now_iso()
|
|
80
|
+
|
|
81
|
+
def set_completed(self, final_result: Optional[Dict[str, Any]] = None) -> None:
|
|
82
|
+
self.completed = True
|
|
83
|
+
self.final_result = final_result
|
|
84
|
+
self.last_updated = _now_iso()
|
|
85
|
+
|
|
86
|
+
def request_stop(self) -> None:
|
|
87
|
+
self.stop_requested = True
|
|
88
|
+
self.last_updated = _now_iso()
|
|
89
|
+
|
|
90
|
+
def should_stop(self) -> bool:
|
|
91
|
+
return self.stop_requested or self.completed or self.has_reached_max_iterations()
|
|
92
|
+
|
|
93
|
+
def is_waiting_for_input(self) -> bool:
|
|
94
|
+
return self.waiting_for_input
|
|
95
|
+
|
|
96
|
+
def enter_waiting_state(self, llm_failed: bool = False) -> None:
|
|
97
|
+
self.waiting_for_input = True
|
|
98
|
+
self.waiting_start_time = datetime.now(timezone.utc)
|
|
99
|
+
self.llm_failed = llm_failed
|
|
100
|
+
self.last_updated = _now_iso()
|
|
101
|
+
|
|
102
|
+
def resume_from_waiting(self, new_task: Optional[str] = None) -> None:
|
|
103
|
+
self.waiting_for_input = False
|
|
104
|
+
self.waiting_start_time = None
|
|
105
|
+
self.stop_requested = False
|
|
106
|
+
self.completed = False
|
|
107
|
+
self.llm_failed = False
|
|
108
|
+
if new_task:
|
|
109
|
+
self.task = new_task
|
|
110
|
+
self.last_updated = _now_iso()
|
|
111
|
+
|
|
112
|
+
def has_reached_max_iterations(self) -> bool:
|
|
113
|
+
return self.iteration >= self.max_iterations
|
|
114
|
+
|
|
115
|
+
def is_approaching_max_iterations(self, threshold: float = 0.85) -> bool:
|
|
116
|
+
return self.iteration >= int(self.max_iterations * threshold)
|
|
117
|
+
|
|
118
|
+
def has_waiting_timeout(self) -> bool:
|
|
119
|
+
if not self.waiting_for_input or not self.waiting_start_time:
|
|
120
|
+
return False
|
|
121
|
+
|
|
122
|
+
if (
|
|
123
|
+
self.stop_requested
|
|
124
|
+
or self.llm_failed
|
|
125
|
+
or self.completed
|
|
126
|
+
or self.has_reached_max_iterations()
|
|
127
|
+
):
|
|
128
|
+
return False
|
|
129
|
+
|
|
130
|
+
elapsed = (datetime.now(timezone.utc) - self.waiting_start_time).total_seconds()
|
|
131
|
+
return elapsed > 600
|
|
132
|
+
|
|
133
|
+
def has_empty_last_messages(self, count: int = 3) -> bool:
|
|
134
|
+
if len(self.messages) < count:
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
last_messages = self.messages[-count:]
|
|
138
|
+
|
|
139
|
+
for message in last_messages:
|
|
140
|
+
content = message.get("content", "")
|
|
141
|
+
if isinstance(content, str) and content.strip():
|
|
142
|
+
return False
|
|
143
|
+
|
|
144
|
+
return True
|
|
145
|
+
|
|
146
|
+
def get_conversation_history(self) -> list[dict[str, Any]]:
|
|
147
|
+
return self.messages
|
|
148
|
+
|
|
149
|
+
def get_execution_summary(self) -> dict[str, Any]:
|
|
150
|
+
return {
|
|
151
|
+
"agent_id": self.agent_id,
|
|
152
|
+
"agent_name": self.agent_name,
|
|
153
|
+
"parent_id": self.parent_id,
|
|
154
|
+
"sandbox_id": self.sandbox_id,
|
|
155
|
+
"sandbox_info": self.sandbox_info,
|
|
156
|
+
"task": self.task,
|
|
157
|
+
"iteration": self.iteration,
|
|
158
|
+
"max_iterations": self.max_iterations,
|
|
159
|
+
"completed": self.completed,
|
|
160
|
+
"final_result": self.final_result,
|
|
161
|
+
"start_time": self.start_time,
|
|
162
|
+
"last_updated": self.last_updated,
|
|
163
|
+
"total_actions": len(self.actions_taken),
|
|
164
|
+
"total_observations": len(self.observations),
|
|
165
|
+
"total_errors": len(self.errors),
|
|
166
|
+
"has_errors": len(self.errors) > 0,
|
|
167
|
+
"max_iterations_reached": self.has_reached_max_iterations() and not self.completed,
|
|
168
|
+
}
|