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
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ScoutSuite Integration - Multi-Cloud Security Auditing
|
|
3
|
+
|
|
4
|
+
ScoutSuite is an open-source multi-cloud security auditing tool
|
|
5
|
+
that assesses the security posture of cloud environments.
|
|
6
|
+
|
|
7
|
+
Supports:
|
|
8
|
+
- AWS (Amazon Web Services)
|
|
9
|
+
- Azure (Microsoft Azure)
|
|
10
|
+
- GCP (Google Cloud Platform)
|
|
11
|
+
- Alibaba Cloud
|
|
12
|
+
- Oracle Cloud Infrastructure
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
from aipt_v2.tools.cloud import run_scoutsuite
|
|
16
|
+
|
|
17
|
+
findings = await run_scoutsuite("aws", profile="production")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from datetime import datetime, timezone
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import List, Dict, Any, Optional
|
|
27
|
+
|
|
28
|
+
from aipt_v2.tools.cloud.cloud_config import CloudConfig, get_cloud_config
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ScoutSuiteConfig:
|
|
33
|
+
"""ScoutSuite configuration."""
|
|
34
|
+
provider: str = "aws" # aws, azure, gcp, aliyun, oci
|
|
35
|
+
|
|
36
|
+
# AWS options
|
|
37
|
+
aws_profile: str = "default"
|
|
38
|
+
aws_regions: List[str] = field(default_factory=list) # Empty = all
|
|
39
|
+
|
|
40
|
+
# Azure options
|
|
41
|
+
azure_subscription_id: str = ""
|
|
42
|
+
azure_tenant_id: str = ""
|
|
43
|
+
|
|
44
|
+
# GCP options
|
|
45
|
+
gcp_project_id: str = ""
|
|
46
|
+
gcp_service_account: str = ""
|
|
47
|
+
|
|
48
|
+
# Scanning options
|
|
49
|
+
services: List[str] = field(default_factory=list) # Empty = all
|
|
50
|
+
skip_services: List[str] = field(default_factory=list)
|
|
51
|
+
max_workers: int = 25
|
|
52
|
+
no_browser: bool = True
|
|
53
|
+
|
|
54
|
+
# Output options
|
|
55
|
+
output_dir: str = "./scoutsuite_results"
|
|
56
|
+
report_name: str = ""
|
|
57
|
+
timestamp: bool = True
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class ScoutSuiteResult:
|
|
62
|
+
"""Result of a ScoutSuite scan."""
|
|
63
|
+
provider: str
|
|
64
|
+
status: str
|
|
65
|
+
started_at: str
|
|
66
|
+
finished_at: str
|
|
67
|
+
duration: float
|
|
68
|
+
findings_count: int
|
|
69
|
+
flagged_items: int
|
|
70
|
+
report_path: str
|
|
71
|
+
summary: Dict[str, int]
|
|
72
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ScoutSuiteTool:
|
|
76
|
+
"""
|
|
77
|
+
ScoutSuite wrapper for multi-cloud security auditing.
|
|
78
|
+
|
|
79
|
+
Provides a Python interface to the ScoutSuite CLI tool
|
|
80
|
+
for automated cloud security assessments.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
def __init__(self, config: Optional[ScoutSuiteConfig] = None):
|
|
84
|
+
"""
|
|
85
|
+
Initialize ScoutSuite tool.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
config: ScoutSuite configuration
|
|
89
|
+
"""
|
|
90
|
+
self.config = config or ScoutSuiteConfig()
|
|
91
|
+
self.output_dir = Path(self.config.output_dir)
|
|
92
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
self._installed = None
|
|
94
|
+
|
|
95
|
+
async def check_installed(self) -> bool:
|
|
96
|
+
"""Check if ScoutSuite is installed."""
|
|
97
|
+
if self._installed is not None:
|
|
98
|
+
return self._installed
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
process = await asyncio.create_subprocess_shell(
|
|
102
|
+
"scout --version",
|
|
103
|
+
stdout=asyncio.subprocess.PIPE,
|
|
104
|
+
stderr=asyncio.subprocess.PIPE
|
|
105
|
+
)
|
|
106
|
+
stdout, _ = await process.communicate()
|
|
107
|
+
self._installed = process.returncode == 0
|
|
108
|
+
except Exception:
|
|
109
|
+
self._installed = False
|
|
110
|
+
|
|
111
|
+
return self._installed
|
|
112
|
+
|
|
113
|
+
def _build_command(self) -> str:
|
|
114
|
+
"""Build ScoutSuite command from configuration."""
|
|
115
|
+
cmd_parts = ["scout", self.config.provider]
|
|
116
|
+
|
|
117
|
+
# Add provider-specific options
|
|
118
|
+
if self.config.provider == "aws":
|
|
119
|
+
if self.config.aws_profile:
|
|
120
|
+
cmd_parts.extend(["--profile", self.config.aws_profile])
|
|
121
|
+
if self.config.aws_regions:
|
|
122
|
+
cmd_parts.extend(["--regions", ",".join(self.config.aws_regions)])
|
|
123
|
+
|
|
124
|
+
elif self.config.provider == "azure":
|
|
125
|
+
if self.config.azure_subscription_id:
|
|
126
|
+
cmd_parts.extend(["--subscription-id", self.config.azure_subscription_id])
|
|
127
|
+
if self.config.azure_tenant_id:
|
|
128
|
+
cmd_parts.extend(["--tenant-id", self.config.azure_tenant_id])
|
|
129
|
+
|
|
130
|
+
elif self.config.provider == "gcp":
|
|
131
|
+
if self.config.gcp_project_id:
|
|
132
|
+
cmd_parts.extend(["--project-id", self.config.gcp_project_id])
|
|
133
|
+
if self.config.gcp_service_account:
|
|
134
|
+
cmd_parts.extend(["--service-account", self.config.gcp_service_account])
|
|
135
|
+
|
|
136
|
+
# Add service filtering
|
|
137
|
+
if self.config.services:
|
|
138
|
+
cmd_parts.extend(["--services", ",".join(self.config.services)])
|
|
139
|
+
if self.config.skip_services:
|
|
140
|
+
cmd_parts.extend(["--skip", ",".join(self.config.skip_services)])
|
|
141
|
+
|
|
142
|
+
# Add general options
|
|
143
|
+
cmd_parts.extend(["--max-workers", str(self.config.max_workers)])
|
|
144
|
+
cmd_parts.extend(["--report-dir", str(self.output_dir)])
|
|
145
|
+
|
|
146
|
+
if self.config.report_name:
|
|
147
|
+
cmd_parts.extend(["--report-name", self.config.report_name])
|
|
148
|
+
|
|
149
|
+
if self.config.no_browser:
|
|
150
|
+
cmd_parts.append("--no-browser")
|
|
151
|
+
|
|
152
|
+
if self.config.timestamp:
|
|
153
|
+
cmd_parts.append("--timestamp")
|
|
154
|
+
|
|
155
|
+
return " ".join(cmd_parts)
|
|
156
|
+
|
|
157
|
+
async def scan(self, timeout: int = 3600) -> ScoutSuiteResult:
|
|
158
|
+
"""
|
|
159
|
+
Run ScoutSuite scan.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
timeout: Scan timeout in seconds
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
ScoutSuiteResult with findings summary
|
|
166
|
+
"""
|
|
167
|
+
if not await self.check_installed():
|
|
168
|
+
raise RuntimeError("ScoutSuite is not installed. Install with: pip install scoutsuite")
|
|
169
|
+
|
|
170
|
+
started_at = datetime.now(timezone.utc).isoformat()
|
|
171
|
+
start_time = asyncio.get_event_loop().time()
|
|
172
|
+
|
|
173
|
+
cmd = self._build_command()
|
|
174
|
+
print(f"[*] Running: {cmd}")
|
|
175
|
+
|
|
176
|
+
# Set up environment
|
|
177
|
+
env = os.environ.copy()
|
|
178
|
+
|
|
179
|
+
process = await asyncio.create_subprocess_shell(
|
|
180
|
+
cmd,
|
|
181
|
+
stdout=asyncio.subprocess.PIPE,
|
|
182
|
+
stderr=asyncio.subprocess.PIPE,
|
|
183
|
+
env=env
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
stdout, stderr = await asyncio.wait_for(
|
|
188
|
+
process.communicate(),
|
|
189
|
+
timeout=timeout
|
|
190
|
+
)
|
|
191
|
+
except asyncio.TimeoutError:
|
|
192
|
+
process.kill()
|
|
193
|
+
raise TimeoutError(f"ScoutSuite scan timed out after {timeout}s")
|
|
194
|
+
|
|
195
|
+
finished_at = datetime.now(timezone.utc).isoformat()
|
|
196
|
+
duration = asyncio.get_event_loop().time() - start_time
|
|
197
|
+
|
|
198
|
+
# Parse results
|
|
199
|
+
report_path = self._find_latest_report()
|
|
200
|
+
findings_count = 0
|
|
201
|
+
flagged_items = 0
|
|
202
|
+
summary = {"danger": 0, "warning": 0, "info": 0}
|
|
203
|
+
|
|
204
|
+
if report_path and report_path.exists():
|
|
205
|
+
results = self._parse_results(report_path)
|
|
206
|
+
findings_count = results.get("findings_count", 0)
|
|
207
|
+
flagged_items = results.get("flagged_items", 0)
|
|
208
|
+
summary = results.get("summary", summary)
|
|
209
|
+
|
|
210
|
+
status = "completed" if process.returncode == 0 else "failed"
|
|
211
|
+
|
|
212
|
+
return ScoutSuiteResult(
|
|
213
|
+
provider=self.config.provider,
|
|
214
|
+
status=status,
|
|
215
|
+
started_at=started_at,
|
|
216
|
+
finished_at=finished_at,
|
|
217
|
+
duration=duration,
|
|
218
|
+
findings_count=findings_count,
|
|
219
|
+
flagged_items=flagged_items,
|
|
220
|
+
report_path=str(report_path) if report_path else "",
|
|
221
|
+
summary=summary,
|
|
222
|
+
metadata={
|
|
223
|
+
"command": cmd,
|
|
224
|
+
"return_code": process.returncode,
|
|
225
|
+
"stderr": stderr.decode() if process.returncode != 0 else ""
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _find_latest_report(self) -> Optional[Path]:
|
|
230
|
+
"""Find the latest ScoutSuite results file."""
|
|
231
|
+
results_dir = self.output_dir / "scoutsuite-results"
|
|
232
|
+
if results_dir.exists():
|
|
233
|
+
js_files = list(results_dir.glob("scoutsuite_results*.js"))
|
|
234
|
+
if js_files:
|
|
235
|
+
return max(js_files, key=lambda f: f.stat().st_mtime)
|
|
236
|
+
|
|
237
|
+
# Try alternative location
|
|
238
|
+
js_files = list(self.output_dir.glob("**/scoutsuite_results*.js"))
|
|
239
|
+
if js_files:
|
|
240
|
+
return max(js_files, key=lambda f: f.stat().st_mtime)
|
|
241
|
+
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def _parse_results(self, results_file: Path) -> Dict[str, Any]:
|
|
245
|
+
"""Parse ScoutSuite results JavaScript file."""
|
|
246
|
+
try:
|
|
247
|
+
content = results_file.read_text()
|
|
248
|
+
|
|
249
|
+
# Remove JS variable assignment
|
|
250
|
+
if "scoutsuite_results =" in content:
|
|
251
|
+
content = content.split("scoutsuite_results =", 1)[1].strip()
|
|
252
|
+
if content.endswith(";"):
|
|
253
|
+
content = content[:-1]
|
|
254
|
+
|
|
255
|
+
data = json.loads(content)
|
|
256
|
+
|
|
257
|
+
# Count findings
|
|
258
|
+
findings_count = 0
|
|
259
|
+
flagged_items = 0
|
|
260
|
+
summary = {"danger": 0, "warning": 0, "info": 0}
|
|
261
|
+
|
|
262
|
+
services = data.get("services", {})
|
|
263
|
+
for service_data in services.values():
|
|
264
|
+
service_findings = service_data.get("findings", {})
|
|
265
|
+
for finding in service_findings.values():
|
|
266
|
+
findings_count += 1
|
|
267
|
+
flagged = finding.get("flagged_items", 0)
|
|
268
|
+
flagged_items += flagged
|
|
269
|
+
|
|
270
|
+
if flagged > 0:
|
|
271
|
+
level = finding.get("level", "warning")
|
|
272
|
+
if level in summary:
|
|
273
|
+
summary[level] += flagged
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
"findings_count": findings_count,
|
|
277
|
+
"flagged_items": flagged_items,
|
|
278
|
+
"summary": summary,
|
|
279
|
+
"account_id": data.get("account_id", ""),
|
|
280
|
+
"last_run": data.get("last_run", {})
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
except Exception as e:
|
|
284
|
+
print(f"[!] Error parsing ScoutSuite results: {e}")
|
|
285
|
+
return {"findings_count": 0, "flagged_items": 0, "summary": {}}
|
|
286
|
+
|
|
287
|
+
def get_findings(self) -> List[Dict[str, Any]]:
|
|
288
|
+
"""Get parsed findings from the latest scan."""
|
|
289
|
+
report_path = self._find_latest_report()
|
|
290
|
+
if not report_path:
|
|
291
|
+
return []
|
|
292
|
+
|
|
293
|
+
try:
|
|
294
|
+
content = report_path.read_text()
|
|
295
|
+
if "scoutsuite_results =" in content:
|
|
296
|
+
content = content.split("scoutsuite_results =", 1)[1].strip()
|
|
297
|
+
if content.endswith(";"):
|
|
298
|
+
content = content[:-1]
|
|
299
|
+
|
|
300
|
+
data = json.loads(content)
|
|
301
|
+
findings = []
|
|
302
|
+
|
|
303
|
+
services = data.get("services", {})
|
|
304
|
+
for service_name, service_data in services.items():
|
|
305
|
+
for finding_id, finding_data in service_data.get("findings", {}).items():
|
|
306
|
+
if finding_data.get("flagged_items", 0) > 0:
|
|
307
|
+
findings.append({
|
|
308
|
+
"provider": self.config.provider,
|
|
309
|
+
"service": service_name,
|
|
310
|
+
"id": finding_id,
|
|
311
|
+
"level": finding_data.get("level", "warning"),
|
|
312
|
+
"description": finding_data.get("description", ""),
|
|
313
|
+
"rationale": finding_data.get("rationale", ""),
|
|
314
|
+
"remediation": finding_data.get("remediation", ""),
|
|
315
|
+
"flagged_items": finding_data.get("flagged_items", 0),
|
|
316
|
+
"items": finding_data.get("items", []),
|
|
317
|
+
"compliance": finding_data.get("compliance", [])
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
return findings
|
|
321
|
+
|
|
322
|
+
except Exception as e:
|
|
323
|
+
print(f"[!] Error getting findings: {e}")
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
# Convenience function
|
|
328
|
+
async def run_scoutsuite(
|
|
329
|
+
provider: str = "aws",
|
|
330
|
+
profile: Optional[str] = None,
|
|
331
|
+
regions: Optional[List[str]] = None,
|
|
332
|
+
services: Optional[List[str]] = None,
|
|
333
|
+
output_dir: str = "./scoutsuite_results",
|
|
334
|
+
timeout: int = 3600
|
|
335
|
+
) -> ScoutSuiteResult:
|
|
336
|
+
"""
|
|
337
|
+
Run ScoutSuite scan.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
provider: Cloud provider (aws, azure, gcp)
|
|
341
|
+
profile: AWS profile name (for AWS)
|
|
342
|
+
regions: Specific regions to scan
|
|
343
|
+
services: Specific services to scan
|
|
344
|
+
output_dir: Output directory for reports
|
|
345
|
+
timeout: Scan timeout in seconds
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
ScoutSuiteResult
|
|
349
|
+
"""
|
|
350
|
+
config = ScoutSuiteConfig(
|
|
351
|
+
provider=provider,
|
|
352
|
+
aws_profile=profile or "default",
|
|
353
|
+
aws_regions=regions or [],
|
|
354
|
+
services=services or [],
|
|
355
|
+
output_dir=output_dir
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
tool = ScoutSuiteTool(config)
|
|
359
|
+
return await tool.scan(timeout=timeout)
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import os
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
if os.getenv("AIPT_SANDBOX_MODE", "false").lower() == "false":
|
|
11
|
+
from aipt_v2.runtime import get_runtime
|
|
12
|
+
|
|
13
|
+
from .argument_parser import convert_arguments
|
|
14
|
+
from .registry import (
|
|
15
|
+
get_tool_by_name,
|
|
16
|
+
get_tool_names,
|
|
17
|
+
needs_agent_state,
|
|
18
|
+
should_execute_in_sandbox,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def execute_tool(tool_name: str, agent_state: Any | None = None, **kwargs: Any) -> Any:
|
|
23
|
+
execute_in_sandbox = should_execute_in_sandbox(tool_name)
|
|
24
|
+
sandbox_mode = os.getenv("AIPT_SANDBOX_MODE", "false").lower() == "true"
|
|
25
|
+
|
|
26
|
+
if execute_in_sandbox and not sandbox_mode:
|
|
27
|
+
return await _execute_tool_in_sandbox(tool_name, agent_state, **kwargs)
|
|
28
|
+
|
|
29
|
+
return await _execute_tool_locally(tool_name, agent_state, **kwargs)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def _execute_tool_in_sandbox(tool_name: str, agent_state: Any, **kwargs: Any) -> Any:
|
|
33
|
+
if not hasattr(agent_state, "sandbox_id") or not agent_state.sandbox_id:
|
|
34
|
+
raise ValueError("Agent state with a valid sandbox_id is required for sandbox execution.")
|
|
35
|
+
|
|
36
|
+
if not hasattr(agent_state, "sandbox_token") or not agent_state.sandbox_token:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
"Agent state with a valid sandbox_token is required for sandbox execution."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if (
|
|
42
|
+
not hasattr(agent_state, "sandbox_info")
|
|
43
|
+
or "tool_server_port" not in agent_state.sandbox_info
|
|
44
|
+
):
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"Agent state with a valid sandbox_info containing tool_server_port is required."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
runtime = get_runtime()
|
|
50
|
+
tool_server_port = agent_state.sandbox_info["tool_server_port"]
|
|
51
|
+
server_url = await runtime.get_sandbox_url(agent_state.sandbox_id, tool_server_port)
|
|
52
|
+
request_url = f"{server_url}/execute"
|
|
53
|
+
|
|
54
|
+
agent_id = getattr(agent_state, "agent_id", "unknown")
|
|
55
|
+
|
|
56
|
+
request_data = {
|
|
57
|
+
"agent_id": agent_id,
|
|
58
|
+
"tool_name": tool_name,
|
|
59
|
+
"kwargs": kwargs,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
headers = {
|
|
63
|
+
"Authorization": f"Bearer {agent_state.sandbox_token}",
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async with httpx.AsyncClient(trust_env=False) as client:
|
|
68
|
+
try:
|
|
69
|
+
response = await client.post(
|
|
70
|
+
request_url, json=request_data, headers=headers, timeout=None
|
|
71
|
+
)
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
response_data = response.json()
|
|
74
|
+
if response_data.get("error"):
|
|
75
|
+
raise RuntimeError(f"Sandbox execution error: {response_data['error']}")
|
|
76
|
+
return response_data.get("result")
|
|
77
|
+
except httpx.HTTPStatusError as e:
|
|
78
|
+
if e.response.status_code == 401:
|
|
79
|
+
raise RuntimeError("Authentication failed: Invalid or missing sandbox token") from e
|
|
80
|
+
raise RuntimeError(f"HTTP error calling tool server: {e.response.status_code}") from e
|
|
81
|
+
except httpx.RequestError as e:
|
|
82
|
+
raise RuntimeError(f"Request error calling tool server: {e}") from e
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
async def _execute_tool_locally(tool_name: str, agent_state: Any | None, **kwargs: Any) -> Any:
|
|
86
|
+
tool_func = get_tool_by_name(tool_name)
|
|
87
|
+
if not tool_func:
|
|
88
|
+
raise ValueError(f"Tool '{tool_name}' not found")
|
|
89
|
+
|
|
90
|
+
converted_kwargs = convert_arguments(tool_func, kwargs)
|
|
91
|
+
|
|
92
|
+
if needs_agent_state(tool_name):
|
|
93
|
+
if agent_state is None:
|
|
94
|
+
raise ValueError(f"Tool '{tool_name}' requires agent_state but none was provided.")
|
|
95
|
+
result = tool_func(agent_state=agent_state, **converted_kwargs)
|
|
96
|
+
else:
|
|
97
|
+
result = tool_func(**converted_kwargs)
|
|
98
|
+
|
|
99
|
+
return await result if inspect.isawaitable(result) else result
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def validate_tool_availability(tool_name: str | None) -> tuple[bool, str]:
|
|
103
|
+
if tool_name is None:
|
|
104
|
+
return False, "Tool name is missing"
|
|
105
|
+
|
|
106
|
+
if tool_name not in get_tool_names():
|
|
107
|
+
return False, f"Tool '{tool_name}' is not available"
|
|
108
|
+
|
|
109
|
+
return True, ""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
async def execute_tool_with_validation(
|
|
113
|
+
tool_name: str | None, agent_state: Any | None = None, **kwargs: Any
|
|
114
|
+
) -> Any:
|
|
115
|
+
is_valid, error_msg = validate_tool_availability(tool_name)
|
|
116
|
+
if not is_valid:
|
|
117
|
+
return f"Error: {error_msg}"
|
|
118
|
+
|
|
119
|
+
assert tool_name is not None
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
result = await execute_tool(tool_name, agent_state, **kwargs)
|
|
123
|
+
except Exception as e: # noqa: BLE001
|
|
124
|
+
error_str = str(e)
|
|
125
|
+
if len(error_str) > 500:
|
|
126
|
+
error_str = error_str[:500] + "... [truncated]"
|
|
127
|
+
return f"Error executing {tool_name}: {error_str}"
|
|
128
|
+
else:
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
async def execute_tool_invocation(tool_inv: dict[str, Any], agent_state: Any | None = None) -> Any:
|
|
133
|
+
tool_name = tool_inv.get("toolName")
|
|
134
|
+
tool_args = tool_inv.get("args", {})
|
|
135
|
+
|
|
136
|
+
return await execute_tool_with_validation(tool_name, agent_state, **tool_args)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _check_error_result(result: Any) -> tuple[bool, Any]:
|
|
140
|
+
is_error = False
|
|
141
|
+
error_payload: Any = None
|
|
142
|
+
|
|
143
|
+
if (isinstance(result, dict) and "error" in result) or (
|
|
144
|
+
isinstance(result, str) and result.strip().lower().startswith("error:")
|
|
145
|
+
):
|
|
146
|
+
is_error = True
|
|
147
|
+
error_payload = result
|
|
148
|
+
|
|
149
|
+
return is_error, error_payload
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _update_tracer_with_result(
|
|
153
|
+
tracer: Any, execution_id: Any, is_error: bool, result: Any, error_payload: Any
|
|
154
|
+
) -> None:
|
|
155
|
+
if not tracer or not execution_id:
|
|
156
|
+
return
|
|
157
|
+
|
|
158
|
+
try:
|
|
159
|
+
if is_error:
|
|
160
|
+
tracer.update_tool_execution(execution_id, "error", error_payload)
|
|
161
|
+
else:
|
|
162
|
+
tracer.update_tool_execution(execution_id, "completed", result)
|
|
163
|
+
except (ConnectionError, RuntimeError) as e:
|
|
164
|
+
error_msg = str(e)
|
|
165
|
+
if tracer and execution_id:
|
|
166
|
+
tracer.update_tool_execution(execution_id, "error", error_msg)
|
|
167
|
+
raise
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _format_tool_result(tool_name: str, result: Any) -> tuple[str, list[dict[str, Any]]]:
|
|
171
|
+
images: list[dict[str, Any]] = []
|
|
172
|
+
|
|
173
|
+
screenshot_data = extract_screenshot_from_result(result)
|
|
174
|
+
if screenshot_data:
|
|
175
|
+
images.append(
|
|
176
|
+
{
|
|
177
|
+
"type": "image_url",
|
|
178
|
+
"image_url": {"url": f"data:image/png;base64,{screenshot_data}"},
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
result_str = remove_screenshot_from_result(result)
|
|
182
|
+
else:
|
|
183
|
+
result_str = result
|
|
184
|
+
|
|
185
|
+
if result_str is None:
|
|
186
|
+
final_result_str = f"Tool {tool_name} executed successfully"
|
|
187
|
+
else:
|
|
188
|
+
final_result_str = str(result_str)
|
|
189
|
+
if len(final_result_str) > 10000:
|
|
190
|
+
start_part = final_result_str[:4000]
|
|
191
|
+
end_part = final_result_str[-4000:]
|
|
192
|
+
final_result_str = start_part + "\n\n... [middle content truncated] ...\n\n" + end_part
|
|
193
|
+
|
|
194
|
+
observation_xml = (
|
|
195
|
+
f"<tool_result>\n<tool_name>{tool_name}</tool_name>\n"
|
|
196
|
+
f"<result>{final_result_str}</result>\n</tool_result>"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return observation_xml, images
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
async def _execute_single_tool(
|
|
203
|
+
tool_inv: dict[str, Any],
|
|
204
|
+
agent_state: Any | None,
|
|
205
|
+
tracer: Any | None,
|
|
206
|
+
agent_id: str,
|
|
207
|
+
) -> tuple[str, list[dict[str, Any]], bool]:
|
|
208
|
+
tool_name = tool_inv.get("toolName", "unknown")
|
|
209
|
+
args = tool_inv.get("args", {})
|
|
210
|
+
execution_id = None
|
|
211
|
+
should_agent_finish = False
|
|
212
|
+
|
|
213
|
+
if tracer:
|
|
214
|
+
execution_id = tracer.log_tool_execution_start(agent_id, tool_name, args)
|
|
215
|
+
|
|
216
|
+
try:
|
|
217
|
+
result = await execute_tool_invocation(tool_inv, agent_state)
|
|
218
|
+
|
|
219
|
+
is_error, error_payload = _check_error_result(result)
|
|
220
|
+
|
|
221
|
+
if (
|
|
222
|
+
tool_name in ("finish_scan", "agent_finish")
|
|
223
|
+
and not is_error
|
|
224
|
+
and isinstance(result, dict)
|
|
225
|
+
):
|
|
226
|
+
if tool_name == "finish_scan":
|
|
227
|
+
should_agent_finish = result.get("scan_completed", False)
|
|
228
|
+
elif tool_name == "agent_finish":
|
|
229
|
+
should_agent_finish = result.get("agent_completed", False)
|
|
230
|
+
|
|
231
|
+
_update_tracer_with_result(tracer, execution_id, is_error, result, error_payload)
|
|
232
|
+
|
|
233
|
+
except (ConnectionError, RuntimeError, ValueError, TypeError, OSError) as e:
|
|
234
|
+
error_msg = str(e)
|
|
235
|
+
if tracer and execution_id:
|
|
236
|
+
tracer.update_tool_execution(execution_id, "error", error_msg)
|
|
237
|
+
raise
|
|
238
|
+
|
|
239
|
+
observation_xml, images = _format_tool_result(tool_name, result)
|
|
240
|
+
return observation_xml, images, should_agent_finish
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _get_tracer_and_agent_id(agent_state: Any | None) -> tuple[Any | None, str]:
|
|
244
|
+
try:
|
|
245
|
+
from aipt_v2.telemetry.tracer import get_global_tracer
|
|
246
|
+
|
|
247
|
+
tracer = get_global_tracer()
|
|
248
|
+
agent_id = agent_state.agent_id if agent_state else "unknown_agent"
|
|
249
|
+
except (ImportError, AttributeError):
|
|
250
|
+
tracer = None
|
|
251
|
+
agent_id = "unknown_agent"
|
|
252
|
+
|
|
253
|
+
return tracer, agent_id
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
async def process_tool_invocations(
|
|
257
|
+
tool_invocations: list[dict[str, Any]],
|
|
258
|
+
conversation_history: list[dict[str, Any]],
|
|
259
|
+
agent_state: Any | None = None,
|
|
260
|
+
) -> bool:
|
|
261
|
+
observation_parts: list[str] = []
|
|
262
|
+
all_images: list[dict[str, Any]] = []
|
|
263
|
+
should_agent_finish = False
|
|
264
|
+
|
|
265
|
+
tracer, agent_id = _get_tracer_and_agent_id(agent_state)
|
|
266
|
+
|
|
267
|
+
for tool_inv in tool_invocations:
|
|
268
|
+
observation_xml, images, tool_should_finish = await _execute_single_tool(
|
|
269
|
+
tool_inv, agent_state, tracer, agent_id
|
|
270
|
+
)
|
|
271
|
+
observation_parts.append(observation_xml)
|
|
272
|
+
all_images.extend(images)
|
|
273
|
+
|
|
274
|
+
if tool_should_finish:
|
|
275
|
+
should_agent_finish = True
|
|
276
|
+
|
|
277
|
+
if all_images:
|
|
278
|
+
content = [{"type": "text", "text": "Tool Results:\n\n" + "\n\n".join(observation_parts)}]
|
|
279
|
+
content.extend(all_images)
|
|
280
|
+
conversation_history.append({"role": "user", "content": content})
|
|
281
|
+
else:
|
|
282
|
+
observation_content = "Tool Results:\n\n" + "\n\n".join(observation_parts)
|
|
283
|
+
conversation_history.append({"role": "user", "content": observation_content})
|
|
284
|
+
|
|
285
|
+
return should_agent_finish
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def extract_screenshot_from_result(result: Any) -> str | None:
|
|
289
|
+
if not isinstance(result, dict):
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
screenshot = result.get("screenshot")
|
|
293
|
+
if isinstance(screenshot, str) and screenshot:
|
|
294
|
+
return screenshot
|
|
295
|
+
|
|
296
|
+
return None
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def remove_screenshot_from_result(result: Any) -> Any:
|
|
300
|
+
if not isinstance(result, dict):
|
|
301
|
+
return result
|
|
302
|
+
|
|
303
|
+
result_copy = result.copy()
|
|
304
|
+
if "screenshot" in result_copy:
|
|
305
|
+
result_copy["screenshot"] = "[Image data extracted - see attached image]"
|
|
306
|
+
|
|
307
|
+
return result_copy
|