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,639 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified Cloud Security Scanner
|
|
3
|
+
|
|
4
|
+
Provides a unified interface for scanning AWS, Azure, and GCP
|
|
5
|
+
cloud infrastructure for security misconfigurations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import subprocess
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import List, Dict, Any, Optional, Callable
|
|
18
|
+
|
|
19
|
+
from aipt_v2.tools.cloud.cloud_config import CloudConfig, get_cloud_config
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CloudSeverity(Enum):
|
|
25
|
+
"""Cloud finding severity levels."""
|
|
26
|
+
CRITICAL = "critical"
|
|
27
|
+
HIGH = "high"
|
|
28
|
+
MEDIUM = "medium"
|
|
29
|
+
LOW = "low"
|
|
30
|
+
INFO = "info"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CloudProvider(Enum):
|
|
34
|
+
"""Supported cloud providers."""
|
|
35
|
+
AWS = "aws"
|
|
36
|
+
AZURE = "azure"
|
|
37
|
+
GCP = "gcp"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CloudFinding:
|
|
42
|
+
"""A security finding from cloud scanning."""
|
|
43
|
+
provider: str
|
|
44
|
+
service: str
|
|
45
|
+
resource_id: str
|
|
46
|
+
resource_name: str
|
|
47
|
+
title: str
|
|
48
|
+
description: str
|
|
49
|
+
severity: str
|
|
50
|
+
recommendation: str
|
|
51
|
+
region: str = ""
|
|
52
|
+
account_id: str = ""
|
|
53
|
+
compliance: List[str] = field(default_factory=list) # e.g., ["CIS 1.1", "PCI 2.1"]
|
|
54
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
55
|
+
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
56
|
+
|
|
57
|
+
def to_aipt_finding(self) -> Dict[str, Any]:
|
|
58
|
+
"""Convert to AIPT Finding format."""
|
|
59
|
+
return {
|
|
60
|
+
"type": f"cloud_{self.service}",
|
|
61
|
+
"value": self.resource_id,
|
|
62
|
+
"description": f"[{self.provider.upper()}] {self.title}: {self.description}",
|
|
63
|
+
"severity": self.severity,
|
|
64
|
+
"phase": "scan",
|
|
65
|
+
"tool": f"cloud_scanner_{self.provider}",
|
|
66
|
+
"target": self.resource_name,
|
|
67
|
+
"evidence": json.dumps(self.metadata),
|
|
68
|
+
"remediation": self.recommendation,
|
|
69
|
+
"metadata": {
|
|
70
|
+
"provider": self.provider,
|
|
71
|
+
"service": self.service,
|
|
72
|
+
"region": self.region,
|
|
73
|
+
"account_id": self.account_id,
|
|
74
|
+
"compliance": self.compliance,
|
|
75
|
+
"resource_id": self.resource_id
|
|
76
|
+
},
|
|
77
|
+
"timestamp": self.timestamp
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass
|
|
82
|
+
class CloudScanResult:
|
|
83
|
+
"""Result of a cloud security scan."""
|
|
84
|
+
provider: str
|
|
85
|
+
status: str # completed, failed, partial
|
|
86
|
+
started_at: str
|
|
87
|
+
finished_at: str
|
|
88
|
+
duration: float
|
|
89
|
+
findings: List[CloudFinding]
|
|
90
|
+
summary: Dict[str, int] # Severity counts
|
|
91
|
+
errors: List[str] = field(default_factory=list)
|
|
92
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class CloudScanner:
|
|
96
|
+
"""
|
|
97
|
+
Unified cloud security scanner.
|
|
98
|
+
|
|
99
|
+
Orchestrates scanning across multiple cloud providers using
|
|
100
|
+
ScoutSuite, Prowler, and custom checks.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(self, config: Optional[CloudConfig] = None):
|
|
104
|
+
"""
|
|
105
|
+
Initialize cloud scanner.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
config: Cloud configuration (auto-detected if not provided)
|
|
109
|
+
"""
|
|
110
|
+
self.config = config or get_cloud_config()
|
|
111
|
+
self.output_dir = Path(self.config.output_dir)
|
|
112
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
self.findings: List[CloudFinding] = []
|
|
114
|
+
self.on_finding: Optional[Callable[[CloudFinding], None]] = None
|
|
115
|
+
|
|
116
|
+
def _log(self, message: str, level: str = "info"):
|
|
117
|
+
"""Log with timestamp."""
|
|
118
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
119
|
+
prefix = {"info": "[*]", "success": "[+]", "error": "[-]", "warning": "[!]"}
|
|
120
|
+
print(f"{timestamp} {prefix.get(level, '[*]')} {message}")
|
|
121
|
+
if level == "error":
|
|
122
|
+
logger.error(message)
|
|
123
|
+
else:
|
|
124
|
+
logger.info(message)
|
|
125
|
+
|
|
126
|
+
async def scan(self, providers: Optional[List[str]] = None) -> List[CloudScanResult]:
|
|
127
|
+
"""
|
|
128
|
+
Run cloud security scan across specified providers.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
providers: List of providers to scan (uses config if not specified)
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
List of CloudScanResult for each provider
|
|
135
|
+
"""
|
|
136
|
+
providers = providers or self.config.get_configured_providers()
|
|
137
|
+
|
|
138
|
+
if not providers:
|
|
139
|
+
self._log("No cloud providers configured. Set credentials first.", "error")
|
|
140
|
+
return []
|
|
141
|
+
|
|
142
|
+
self._log(f"Starting cloud security scan for: {', '.join(providers)}")
|
|
143
|
+
results = []
|
|
144
|
+
|
|
145
|
+
for provider in providers:
|
|
146
|
+
try:
|
|
147
|
+
result = await self._scan_provider(provider)
|
|
148
|
+
results.append(result)
|
|
149
|
+
self.findings.extend(result.findings)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
self._log(f"Error scanning {provider}: {str(e)}", "error")
|
|
152
|
+
results.append(CloudScanResult(
|
|
153
|
+
provider=provider,
|
|
154
|
+
status="failed",
|
|
155
|
+
started_at=datetime.now(timezone.utc).isoformat(),
|
|
156
|
+
finished_at=datetime.now(timezone.utc).isoformat(),
|
|
157
|
+
duration=0,
|
|
158
|
+
findings=[],
|
|
159
|
+
summary={},
|
|
160
|
+
errors=[str(e)]
|
|
161
|
+
))
|
|
162
|
+
|
|
163
|
+
return results
|
|
164
|
+
|
|
165
|
+
async def _scan_provider(self, provider: str) -> CloudScanResult:
|
|
166
|
+
"""Scan a specific cloud provider."""
|
|
167
|
+
started_at = datetime.now(timezone.utc).isoformat()
|
|
168
|
+
start_time = asyncio.get_event_loop().time()
|
|
169
|
+
|
|
170
|
+
self._log(f"Scanning {provider.upper()}...")
|
|
171
|
+
|
|
172
|
+
findings = []
|
|
173
|
+
errors = []
|
|
174
|
+
|
|
175
|
+
if provider == "aws":
|
|
176
|
+
findings, errors = await self._scan_aws()
|
|
177
|
+
elif provider == "azure":
|
|
178
|
+
findings, errors = await self._scan_azure()
|
|
179
|
+
elif provider == "gcp":
|
|
180
|
+
findings, errors = await self._scan_gcp()
|
|
181
|
+
else:
|
|
182
|
+
errors.append(f"Unknown provider: {provider}")
|
|
183
|
+
|
|
184
|
+
finished_at = datetime.now(timezone.utc).isoformat()
|
|
185
|
+
duration = asyncio.get_event_loop().time() - start_time
|
|
186
|
+
|
|
187
|
+
# Calculate summary
|
|
188
|
+
summary = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
|
|
189
|
+
for finding in findings:
|
|
190
|
+
sev = finding.severity.lower()
|
|
191
|
+
if sev in summary:
|
|
192
|
+
summary[sev] += 1
|
|
193
|
+
|
|
194
|
+
self._log(f"{provider.upper()} scan complete: {len(findings)} findings", "success")
|
|
195
|
+
|
|
196
|
+
return CloudScanResult(
|
|
197
|
+
provider=provider,
|
|
198
|
+
status="completed" if not errors else "partial",
|
|
199
|
+
started_at=started_at,
|
|
200
|
+
finished_at=finished_at,
|
|
201
|
+
duration=duration,
|
|
202
|
+
findings=findings,
|
|
203
|
+
summary=summary,
|
|
204
|
+
errors=errors
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
async def _scan_aws(self) -> tuple:
|
|
208
|
+
"""Scan AWS infrastructure."""
|
|
209
|
+
findings = []
|
|
210
|
+
errors = []
|
|
211
|
+
|
|
212
|
+
aws_config = self.config.aws
|
|
213
|
+
env = {**os.environ, **aws_config.to_env_dict()}
|
|
214
|
+
|
|
215
|
+
# Run ScoutSuite for AWS
|
|
216
|
+
try:
|
|
217
|
+
scoutsuite_findings = await self._run_scoutsuite("aws", env)
|
|
218
|
+
findings.extend(scoutsuite_findings)
|
|
219
|
+
except Exception as e:
|
|
220
|
+
errors.append(f"ScoutSuite AWS: {str(e)}")
|
|
221
|
+
self._log(f"ScoutSuite failed: {e}", "warning")
|
|
222
|
+
|
|
223
|
+
# Run Prowler for AWS
|
|
224
|
+
try:
|
|
225
|
+
prowler_findings = await self._run_prowler(env)
|
|
226
|
+
findings.extend(prowler_findings)
|
|
227
|
+
except Exception as e:
|
|
228
|
+
errors.append(f"Prowler: {str(e)}")
|
|
229
|
+
self._log(f"Prowler failed: {e}", "warning")
|
|
230
|
+
|
|
231
|
+
# Run custom AWS checks
|
|
232
|
+
try:
|
|
233
|
+
custom_findings = await self._run_aws_custom_checks(env)
|
|
234
|
+
findings.extend(custom_findings)
|
|
235
|
+
except Exception as e:
|
|
236
|
+
errors.append(f"Custom AWS checks: {str(e)}")
|
|
237
|
+
|
|
238
|
+
return findings, errors
|
|
239
|
+
|
|
240
|
+
async def _scan_azure(self) -> tuple:
|
|
241
|
+
"""Scan Azure infrastructure."""
|
|
242
|
+
findings = []
|
|
243
|
+
errors = []
|
|
244
|
+
|
|
245
|
+
azure_config = self.config.azure
|
|
246
|
+
env = {**os.environ, **azure_config.to_env_dict()}
|
|
247
|
+
|
|
248
|
+
# Run ScoutSuite for Azure
|
|
249
|
+
try:
|
|
250
|
+
scoutsuite_findings = await self._run_scoutsuite("azure", env)
|
|
251
|
+
findings.extend(scoutsuite_findings)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
errors.append(f"ScoutSuite Azure: {str(e)}")
|
|
254
|
+
|
|
255
|
+
return findings, errors
|
|
256
|
+
|
|
257
|
+
async def _scan_gcp(self) -> tuple:
|
|
258
|
+
"""Scan GCP infrastructure."""
|
|
259
|
+
findings = []
|
|
260
|
+
errors = []
|
|
261
|
+
|
|
262
|
+
gcp_config = self.config.gcp
|
|
263
|
+
env = {**os.environ, **gcp_config.to_env_dict()}
|
|
264
|
+
|
|
265
|
+
# Run ScoutSuite for GCP
|
|
266
|
+
try:
|
|
267
|
+
scoutsuite_findings = await self._run_scoutsuite("gcp", env)
|
|
268
|
+
findings.extend(scoutsuite_findings)
|
|
269
|
+
except Exception as e:
|
|
270
|
+
errors.append(f"ScoutSuite GCP: {str(e)}")
|
|
271
|
+
|
|
272
|
+
return findings, errors
|
|
273
|
+
|
|
274
|
+
async def _run_scoutsuite(self, provider: str, env: Dict[str, str]) -> List[CloudFinding]:
|
|
275
|
+
"""Run ScoutSuite for a provider."""
|
|
276
|
+
findings = []
|
|
277
|
+
output_dir = self.output_dir / f"scoutsuite_{provider}"
|
|
278
|
+
output_dir.mkdir(exist_ok=True)
|
|
279
|
+
|
|
280
|
+
cmd = f"scout {provider} --report-dir {output_dir} --no-browser"
|
|
281
|
+
|
|
282
|
+
self._log(f"Running ScoutSuite for {provider}...")
|
|
283
|
+
|
|
284
|
+
process = await asyncio.create_subprocess_shell(
|
|
285
|
+
cmd,
|
|
286
|
+
stdout=asyncio.subprocess.PIPE,
|
|
287
|
+
stderr=asyncio.subprocess.PIPE,
|
|
288
|
+
env=env
|
|
289
|
+
)
|
|
290
|
+
stdout, stderr = await asyncio.wait_for(
|
|
291
|
+
process.communicate(),
|
|
292
|
+
timeout=self.config.timeout
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if process.returncode == 0:
|
|
296
|
+
# Parse ScoutSuite results
|
|
297
|
+
results_file = output_dir / "scoutsuite-results" / "scoutsuite_results.js"
|
|
298
|
+
if results_file.exists():
|
|
299
|
+
findings = self._parse_scoutsuite_results(results_file, provider)
|
|
300
|
+
else:
|
|
301
|
+
raise Exception(f"ScoutSuite failed: {stderr.decode()}")
|
|
302
|
+
|
|
303
|
+
return findings
|
|
304
|
+
|
|
305
|
+
async def _run_prowler(self, env: Dict[str, str]) -> List[CloudFinding]:
|
|
306
|
+
"""Run Prowler for AWS."""
|
|
307
|
+
findings = []
|
|
308
|
+
output_file = self.output_dir / "prowler_results.json"
|
|
309
|
+
|
|
310
|
+
cmd = f"prowler aws --output-formats json --output-filename {output_file}"
|
|
311
|
+
|
|
312
|
+
self._log("Running Prowler for AWS...")
|
|
313
|
+
|
|
314
|
+
process = await asyncio.create_subprocess_shell(
|
|
315
|
+
cmd,
|
|
316
|
+
stdout=asyncio.subprocess.PIPE,
|
|
317
|
+
stderr=asyncio.subprocess.PIPE,
|
|
318
|
+
env=env
|
|
319
|
+
)
|
|
320
|
+
stdout, stderr = await asyncio.wait_for(
|
|
321
|
+
process.communicate(),
|
|
322
|
+
timeout=self.config.timeout
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
if process.returncode == 0 and output_file.exists():
|
|
326
|
+
findings = self._parse_prowler_results(output_file)
|
|
327
|
+
else:
|
|
328
|
+
# Prowler might not be installed
|
|
329
|
+
self._log("Prowler not available or failed", "warning")
|
|
330
|
+
|
|
331
|
+
return findings
|
|
332
|
+
|
|
333
|
+
async def _run_aws_custom_checks(self, env: Dict[str, str]) -> List[CloudFinding]:
|
|
334
|
+
"""Run custom AWS security checks using boto3."""
|
|
335
|
+
findings = []
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
import boto3
|
|
339
|
+
from botocore.exceptions import ClientError
|
|
340
|
+
|
|
341
|
+
# Create session
|
|
342
|
+
session = boto3.Session(
|
|
343
|
+
profile_name=self.config.aws.profile,
|
|
344
|
+
region_name=self.config.aws.region
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
# Check S3 public buckets
|
|
348
|
+
s3_findings = await self._check_s3_public_access(session)
|
|
349
|
+
findings.extend(s3_findings)
|
|
350
|
+
|
|
351
|
+
# Check security groups
|
|
352
|
+
sg_findings = await self._check_security_groups(session)
|
|
353
|
+
findings.extend(sg_findings)
|
|
354
|
+
|
|
355
|
+
# Check IAM
|
|
356
|
+
iam_findings = await self._check_iam_issues(session)
|
|
357
|
+
findings.extend(iam_findings)
|
|
358
|
+
|
|
359
|
+
except ImportError:
|
|
360
|
+
self._log("boto3 not installed, skipping custom AWS checks", "warning")
|
|
361
|
+
except Exception as e:
|
|
362
|
+
self._log(f"Custom AWS check error: {e}", "warning")
|
|
363
|
+
|
|
364
|
+
return findings
|
|
365
|
+
|
|
366
|
+
async def _check_s3_public_access(self, session) -> List[CloudFinding]:
|
|
367
|
+
"""Check for public S3 buckets."""
|
|
368
|
+
findings = []
|
|
369
|
+
s3 = session.client('s3')
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
buckets = s3.list_buckets().get('Buckets', [])
|
|
373
|
+
for bucket in buckets:
|
|
374
|
+
bucket_name = bucket['Name']
|
|
375
|
+
try:
|
|
376
|
+
# Check bucket ACL
|
|
377
|
+
acl = s3.get_bucket_acl(Bucket=bucket_name)
|
|
378
|
+
for grant in acl.get('Grants', []):
|
|
379
|
+
grantee = grant.get('Grantee', {})
|
|
380
|
+
if grantee.get('URI', '').endswith('AllUsers'):
|
|
381
|
+
findings.append(CloudFinding(
|
|
382
|
+
provider="aws",
|
|
383
|
+
service="s3",
|
|
384
|
+
resource_id=bucket_name,
|
|
385
|
+
resource_name=bucket_name,
|
|
386
|
+
title="Public S3 Bucket",
|
|
387
|
+
description=f"S3 bucket {bucket_name} is publicly accessible",
|
|
388
|
+
severity="critical",
|
|
389
|
+
recommendation="Remove public access from the bucket ACL",
|
|
390
|
+
compliance=["CIS 2.1.1", "PCI 2.1"],
|
|
391
|
+
metadata={"acl_grant": str(grant)}
|
|
392
|
+
))
|
|
393
|
+
except Exception:
|
|
394
|
+
continue
|
|
395
|
+
except Exception as e:
|
|
396
|
+
self._log(f"S3 check error: {e}", "warning")
|
|
397
|
+
|
|
398
|
+
return findings
|
|
399
|
+
|
|
400
|
+
async def _check_security_groups(self, session) -> List[CloudFinding]:
|
|
401
|
+
"""Check for overly permissive security groups."""
|
|
402
|
+
findings = []
|
|
403
|
+
ec2 = session.client('ec2')
|
|
404
|
+
|
|
405
|
+
try:
|
|
406
|
+
sgs = ec2.describe_security_groups().get('SecurityGroups', [])
|
|
407
|
+
for sg in sgs:
|
|
408
|
+
sg_id = sg['GroupId']
|
|
409
|
+
sg_name = sg['GroupName']
|
|
410
|
+
|
|
411
|
+
for rule in sg.get('IpPermissions', []):
|
|
412
|
+
for ip_range in rule.get('IpRanges', []):
|
|
413
|
+
cidr = ip_range.get('CidrIp', '')
|
|
414
|
+
if cidr == '0.0.0.0/0':
|
|
415
|
+
port = rule.get('FromPort', 'All')
|
|
416
|
+
if port in [22, 3389, 'All']:
|
|
417
|
+
findings.append(CloudFinding(
|
|
418
|
+
provider="aws",
|
|
419
|
+
service="ec2",
|
|
420
|
+
resource_id=sg_id,
|
|
421
|
+
resource_name=sg_name,
|
|
422
|
+
title="Security Group Open to Internet",
|
|
423
|
+
description=f"Security group {sg_name} allows inbound traffic from 0.0.0.0/0 on port {port}",
|
|
424
|
+
severity="high" if port in [22, 3389] else "medium",
|
|
425
|
+
recommendation="Restrict inbound access to specific IP ranges",
|
|
426
|
+
compliance=["CIS 4.1", "CIS 4.2"],
|
|
427
|
+
metadata={"port": port, "cidr": cidr}
|
|
428
|
+
))
|
|
429
|
+
except Exception as e:
|
|
430
|
+
self._log(f"Security groups check error: {e}", "warning")
|
|
431
|
+
|
|
432
|
+
return findings
|
|
433
|
+
|
|
434
|
+
async def _check_iam_issues(self, session) -> List[CloudFinding]:
|
|
435
|
+
"""Check for IAM security issues."""
|
|
436
|
+
findings = []
|
|
437
|
+
iam = session.client('iam')
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
# Check for root account access keys
|
|
441
|
+
try:
|
|
442
|
+
summary = iam.get_account_summary()
|
|
443
|
+
if summary.get('SummaryMap', {}).get('AccountAccessKeysPresent', 0) > 0:
|
|
444
|
+
findings.append(CloudFinding(
|
|
445
|
+
provider="aws",
|
|
446
|
+
service="iam",
|
|
447
|
+
resource_id="root",
|
|
448
|
+
resource_name="Root Account",
|
|
449
|
+
title="Root Account Has Access Keys",
|
|
450
|
+
description="The root account has active access keys which is a security risk",
|
|
451
|
+
severity="critical",
|
|
452
|
+
recommendation="Delete root account access keys and use IAM users instead",
|
|
453
|
+
compliance=["CIS 1.4"]
|
|
454
|
+
))
|
|
455
|
+
except Exception:
|
|
456
|
+
pass
|
|
457
|
+
|
|
458
|
+
# Check for users without MFA
|
|
459
|
+
users = iam.list_users().get('Users', [])
|
|
460
|
+
for user in users:
|
|
461
|
+
username = user['UserName']
|
|
462
|
+
try:
|
|
463
|
+
mfa = iam.list_mfa_devices(UserName=username)
|
|
464
|
+
if not mfa.get('MFADevices', []):
|
|
465
|
+
findings.append(CloudFinding(
|
|
466
|
+
provider="aws",
|
|
467
|
+
service="iam",
|
|
468
|
+
resource_id=username,
|
|
469
|
+
resource_name=username,
|
|
470
|
+
title="IAM User Without MFA",
|
|
471
|
+
description=f"IAM user {username} does not have MFA enabled",
|
|
472
|
+
severity="medium",
|
|
473
|
+
recommendation="Enable MFA for all IAM users",
|
|
474
|
+
compliance=["CIS 1.2"]
|
|
475
|
+
))
|
|
476
|
+
except Exception:
|
|
477
|
+
continue
|
|
478
|
+
|
|
479
|
+
except Exception as e:
|
|
480
|
+
self._log(f"IAM check error: {e}", "warning")
|
|
481
|
+
|
|
482
|
+
return findings
|
|
483
|
+
|
|
484
|
+
def _parse_scoutsuite_results(self, results_file: Path, provider: str) -> List[CloudFinding]:
|
|
485
|
+
"""Parse ScoutSuite results file."""
|
|
486
|
+
findings = []
|
|
487
|
+
|
|
488
|
+
try:
|
|
489
|
+
content = results_file.read_text()
|
|
490
|
+
# Remove JS wrapper
|
|
491
|
+
if content.startswith("scoutsuite_results ="):
|
|
492
|
+
content = content.replace("scoutsuite_results =", "").strip()
|
|
493
|
+
|
|
494
|
+
data = json.loads(content)
|
|
495
|
+
services = data.get('services', {})
|
|
496
|
+
|
|
497
|
+
for service_name, service_data in services.items():
|
|
498
|
+
service_findings = service_data.get('findings', {})
|
|
499
|
+
for finding_id, finding_data in service_findings.items():
|
|
500
|
+
if finding_data.get('flagged_items', 0) > 0:
|
|
501
|
+
severity = self._map_scoutsuite_severity(finding_data.get('level', 'warning'))
|
|
502
|
+
for item in finding_data.get('items', []):
|
|
503
|
+
findings.append(CloudFinding(
|
|
504
|
+
provider=provider,
|
|
505
|
+
service=service_name,
|
|
506
|
+
resource_id=item,
|
|
507
|
+
resource_name=item,
|
|
508
|
+
title=finding_data.get('description', finding_id),
|
|
509
|
+
description=finding_data.get('rationale', ''),
|
|
510
|
+
severity=severity,
|
|
511
|
+
recommendation=finding_data.get('remediation', ''),
|
|
512
|
+
compliance=finding_data.get('compliance', [])
|
|
513
|
+
))
|
|
514
|
+
except Exception as e:
|
|
515
|
+
self._log(f"Error parsing ScoutSuite results: {e}", "warning")
|
|
516
|
+
|
|
517
|
+
return findings
|
|
518
|
+
|
|
519
|
+
def _parse_prowler_results(self, results_file: Path) -> List[CloudFinding]:
|
|
520
|
+
"""Parse Prowler JSON results."""
|
|
521
|
+
findings = []
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
with open(results_file) as f:
|
|
525
|
+
for line in f:
|
|
526
|
+
if line.strip():
|
|
527
|
+
check = json.loads(line)
|
|
528
|
+
if check.get('StatusExtended', '').upper() == 'FAIL':
|
|
529
|
+
findings.append(CloudFinding(
|
|
530
|
+
provider="aws",
|
|
531
|
+
service=check.get('ServiceName', 'unknown'),
|
|
532
|
+
resource_id=check.get('ResourceId', ''),
|
|
533
|
+
resource_name=check.get('ResourceName', ''),
|
|
534
|
+
title=check.get('CheckTitle', ''),
|
|
535
|
+
description=check.get('StatusExtended', ''),
|
|
536
|
+
severity=check.get('Severity', 'medium').lower(),
|
|
537
|
+
recommendation=check.get('Remediation', {}).get('Recommendation', {}).get('Text', ''),
|
|
538
|
+
region=check.get('Region', ''),
|
|
539
|
+
account_id=check.get('AccountId', ''),
|
|
540
|
+
compliance=check.get('Compliance', [])
|
|
541
|
+
))
|
|
542
|
+
except Exception as e:
|
|
543
|
+
self._log(f"Error parsing Prowler results: {e}", "warning")
|
|
544
|
+
|
|
545
|
+
return findings
|
|
546
|
+
|
|
547
|
+
def _map_scoutsuite_severity(self, level: str) -> str:
|
|
548
|
+
"""Map ScoutSuite severity to standard levels."""
|
|
549
|
+
mapping = {
|
|
550
|
+
"danger": "critical",
|
|
551
|
+
"warning": "high",
|
|
552
|
+
"info": "medium"
|
|
553
|
+
}
|
|
554
|
+
return mapping.get(level.lower(), "medium")
|
|
555
|
+
|
|
556
|
+
def get_findings_by_severity(self, severity: str) -> List[CloudFinding]:
|
|
557
|
+
"""Get findings filtered by severity."""
|
|
558
|
+
return [f for f in self.findings if f.severity.lower() == severity.lower()]
|
|
559
|
+
|
|
560
|
+
def get_findings_by_provider(self, provider: str) -> List[CloudFinding]:
|
|
561
|
+
"""Get findings filtered by provider."""
|
|
562
|
+
return [f for f in self.findings if f.provider.lower() == provider.lower()]
|
|
563
|
+
|
|
564
|
+
def get_summary(self) -> Dict[str, Any]:
|
|
565
|
+
"""Get scan summary."""
|
|
566
|
+
summary = {
|
|
567
|
+
"total_findings": len(self.findings),
|
|
568
|
+
"by_severity": {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0},
|
|
569
|
+
"by_provider": {},
|
|
570
|
+
"by_service": {}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
for finding in self.findings:
|
|
574
|
+
# Count by severity
|
|
575
|
+
sev = finding.severity.lower()
|
|
576
|
+
if sev in summary["by_severity"]:
|
|
577
|
+
summary["by_severity"][sev] += 1
|
|
578
|
+
|
|
579
|
+
# Count by provider
|
|
580
|
+
provider = finding.provider
|
|
581
|
+
if provider not in summary["by_provider"]:
|
|
582
|
+
summary["by_provider"][provider] = 0
|
|
583
|
+
summary["by_provider"][provider] += 1
|
|
584
|
+
|
|
585
|
+
# Count by service
|
|
586
|
+
service = f"{finding.provider}:{finding.service}"
|
|
587
|
+
if service not in summary["by_service"]:
|
|
588
|
+
summary["by_service"][service] = 0
|
|
589
|
+
summary["by_service"][service] += 1
|
|
590
|
+
|
|
591
|
+
return summary
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# Convenience functions
|
|
595
|
+
def get_cloud_scanner(
|
|
596
|
+
providers: Optional[List[str]] = None,
|
|
597
|
+
aws_profile: Optional[str] = None,
|
|
598
|
+
azure_subscription: Optional[str] = None,
|
|
599
|
+
gcp_project: Optional[str] = None,
|
|
600
|
+
**kwargs
|
|
601
|
+
) -> CloudScanner:
|
|
602
|
+
"""
|
|
603
|
+
Get a configured cloud scanner instance.
|
|
604
|
+
|
|
605
|
+
Args:
|
|
606
|
+
providers: Cloud providers to scan
|
|
607
|
+
aws_profile: AWS CLI profile name
|
|
608
|
+
azure_subscription: Azure subscription ID
|
|
609
|
+
gcp_project: GCP project ID
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
CloudScanner instance
|
|
613
|
+
"""
|
|
614
|
+
config = get_cloud_config(
|
|
615
|
+
providers=providers,
|
|
616
|
+
aws_profile=aws_profile,
|
|
617
|
+
azure_subscription=azure_subscription,
|
|
618
|
+
gcp_project=gcp_project,
|
|
619
|
+
**kwargs
|
|
620
|
+
)
|
|
621
|
+
return CloudScanner(config)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
async def scan_cloud(
|
|
625
|
+
providers: Optional[List[str]] = None,
|
|
626
|
+
**kwargs
|
|
627
|
+
) -> List[CloudScanResult]:
|
|
628
|
+
"""
|
|
629
|
+
Run cloud security scan.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
providers: Cloud providers to scan
|
|
633
|
+
**kwargs: Additional configuration
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
List of CloudScanResult
|
|
637
|
+
"""
|
|
638
|
+
scanner = get_cloud_scanner(providers=providers, **kwargs)
|
|
639
|
+
return await scanner.scan()
|