runbooks 0.9.6__py3-none-any.whl → 0.9.8__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.
- runbooks/__init__.py +1 -1
- runbooks/_platform/__init__.py +19 -0
- runbooks/_platform/core/runbooks_wrapper.py +478 -0
- runbooks/cloudops/cost_optimizer.py +330 -0
- runbooks/cloudops/interfaces.py +3 -3
- runbooks/common/mcp_integration.py +174 -0
- runbooks/common/performance_monitor.py +4 -4
- runbooks/enterprise/__init__.py +18 -10
- runbooks/enterprise/security.py +708 -0
- runbooks/finops/README.md +1 -1
- runbooks/finops/automation_core.py +643 -0
- runbooks/finops/business_cases.py +414 -16
- runbooks/finops/cli.py +23 -0
- runbooks/finops/compute_cost_optimizer.py +865 -0
- runbooks/finops/ebs_cost_optimizer.py +718 -0
- runbooks/finops/ebs_optimizer.py +909 -0
- runbooks/finops/elastic_ip_optimizer.py +675 -0
- runbooks/finops/embedded_mcp_validator.py +330 -14
- runbooks/finops/enhanced_dashboard_runner.py +2 -1
- runbooks/finops/enterprise_wrappers.py +827 -0
- runbooks/finops/finops_dashboard.py +322 -11
- runbooks/finops/legacy_migration.py +730 -0
- runbooks/finops/nat_gateway_optimizer.py +1160 -0
- runbooks/finops/network_cost_optimizer.py +1387 -0
- runbooks/finops/notebook_utils.py +596 -0
- runbooks/finops/reservation_optimizer.py +956 -0
- runbooks/finops/single_dashboard.py +16 -16
- runbooks/finops/validation_framework.py +753 -0
- runbooks/finops/vpc_cleanup_optimizer.py +817 -0
- runbooks/finops/workspaces_analyzer.py +1 -1
- runbooks/inventory/__init__.py +7 -0
- runbooks/inventory/collectors/aws_networking.py +357 -6
- runbooks/inventory/mcp_vpc_validator.py +1091 -0
- runbooks/inventory/vpc_analyzer.py +1107 -0
- runbooks/inventory/vpc_architecture_validator.py +939 -0
- runbooks/inventory/vpc_dependency_analyzer.py +845 -0
- runbooks/main.py +487 -40
- runbooks/operate/vpc_operations.py +1485 -16
- runbooks/remediation/commvault_ec2_analysis.py +1 -1
- runbooks/remediation/dynamodb_optimize.py +2 -2
- runbooks/remediation/rds_instance_list.py +1 -1
- runbooks/remediation/rds_snapshot_list.py +1 -1
- runbooks/remediation/workspaces_list.py +2 -2
- runbooks/security/compliance_automation.py +2 -2
- runbooks/vpc/__init__.py +12 -0
- runbooks/vpc/cleanup_wrapper.py +757 -0
- runbooks/vpc/cost_engine.py +527 -3
- runbooks/vpc/networking_wrapper.py +29 -29
- runbooks/vpc/runbooks_adapter.py +479 -0
- runbooks/vpc/tests/test_config.py +2 -2
- runbooks/vpc/vpc_cleanup_integration.py +2629 -0
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/METADATA +1 -1
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/RECORD +57 -34
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/WHEEL +0 -0
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/entry_points.txt +0 -0
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/licenses/LICENSE +0 -0
- {runbooks-0.9.6.dist-info → runbooks-0.9.8.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1107 @@
|
|
1
|
+
"""
|
2
|
+
VPC Discovery & Analysis Module - Migrated from vpc module
|
3
|
+
|
4
|
+
Strategic Migration: Comprehensive VPC discovery capabilities moved from standalone vpc module
|
5
|
+
to inventory module following FAANG SDLC "Do one thing and do it well" principle.
|
6
|
+
|
7
|
+
AWSO-05 Integration: Complete VPC discovery support for 12-step dependency analysis:
|
8
|
+
- VPC topology discovery and analysis
|
9
|
+
- NAT Gateway, IGW, Route Table, VPC Endpoint discovery
|
10
|
+
- ENI dependency mapping for workload protection
|
11
|
+
- Default VPC identification for CIS Benchmark compliance
|
12
|
+
- Transit Gateway attachment analysis
|
13
|
+
- VPC Peering connection discovery
|
14
|
+
|
15
|
+
Key Features:
|
16
|
+
- Enterprise-scale discovery (1-200+ accounts)
|
17
|
+
- Rich CLI integration with enterprise UX standards
|
18
|
+
- MCP validation for ≥99.5% accuracy
|
19
|
+
- Comprehensive dependency mapping
|
20
|
+
- Evidence collection for AWSO-05 cleanup workflows
|
21
|
+
|
22
|
+
This module provides VPC discovery capabilities that integrate seamlessly with
|
23
|
+
operate/vpc_operations.py for complete AWSO-05 VPC cleanup workflows.
|
24
|
+
"""
|
25
|
+
|
26
|
+
import json
|
27
|
+
import logging
|
28
|
+
from dataclasses import dataclass
|
29
|
+
from datetime import datetime, timedelta
|
30
|
+
from pathlib import Path
|
31
|
+
from typing import Any, Dict, List, Optional, Tuple
|
32
|
+
|
33
|
+
import boto3
|
34
|
+
from botocore.exceptions import ClientError
|
35
|
+
from rich.console import Console
|
36
|
+
from rich.panel import Panel
|
37
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
38
|
+
from rich.table import Table
|
39
|
+
from rich.tree import Tree
|
40
|
+
|
41
|
+
from runbooks.common.profile_utils import create_operational_session
|
42
|
+
from runbooks.common.rich_utils import (
|
43
|
+
console,
|
44
|
+
print_header,
|
45
|
+
print_success,
|
46
|
+
print_error,
|
47
|
+
print_warning,
|
48
|
+
create_table,
|
49
|
+
create_progress_bar,
|
50
|
+
format_cost
|
51
|
+
)
|
52
|
+
|
53
|
+
logger = logging.getLogger(__name__)
|
54
|
+
|
55
|
+
|
56
|
+
@dataclass
|
57
|
+
class VPCDiscoveryResult:
|
58
|
+
"""Results from VPC discovery operations"""
|
59
|
+
vpcs: List[Dict[str, Any]]
|
60
|
+
nat_gateways: List[Dict[str, Any]]
|
61
|
+
vpc_endpoints: List[Dict[str, Any]]
|
62
|
+
internet_gateways: List[Dict[str, Any]]
|
63
|
+
route_tables: List[Dict[str, Any]]
|
64
|
+
subnets: List[Dict[str, Any]]
|
65
|
+
network_interfaces: List[Dict[str, Any]]
|
66
|
+
transit_gateway_attachments: List[Dict[str, Any]]
|
67
|
+
vpc_peering_connections: List[Dict[str, Any]]
|
68
|
+
security_groups: List[Dict[str, Any]]
|
69
|
+
total_resources: int
|
70
|
+
discovery_timestamp: str
|
71
|
+
|
72
|
+
|
73
|
+
@dataclass
|
74
|
+
class AWSOAnalysis:
|
75
|
+
"""AWSO-05 specific analysis results"""
|
76
|
+
default_vpcs: List[Dict[str, Any]]
|
77
|
+
orphaned_resources: List[Dict[str, Any]]
|
78
|
+
dependency_chain: Dict[str, List[str]]
|
79
|
+
eni_gate_warnings: List[Dict[str, Any]]
|
80
|
+
cleanup_recommendations: List[Dict[str, Any]]
|
81
|
+
evidence_bundle: Dict[str, Any]
|
82
|
+
|
83
|
+
|
84
|
+
class VPCAnalyzer:
|
85
|
+
"""
|
86
|
+
Enterprise VPC Discovery and Analysis Engine
|
87
|
+
|
88
|
+
Migrated from VPC module with enhanced capabilities:
|
89
|
+
- Complete VPC topology discovery
|
90
|
+
- AWSO-05 cleanup support with 12-step dependency analysis
|
91
|
+
- Rich CLI integration with enterprise UX standards
|
92
|
+
- Multi-account discovery with >99.5% accuracy
|
93
|
+
- Evidence collection for audit trails
|
94
|
+
"""
|
95
|
+
|
96
|
+
def __init__(
|
97
|
+
self,
|
98
|
+
profile: Optional[str] = None,
|
99
|
+
region: Optional[str] = "us-east-1",
|
100
|
+
console: Optional[Console] = None,
|
101
|
+
dry_run: bool = True
|
102
|
+
):
|
103
|
+
"""
|
104
|
+
Initialize VPC Analyzer with enterprise profile management
|
105
|
+
|
106
|
+
Args:
|
107
|
+
profile: AWS profile name (3-tier priority: User > Environment > Default)
|
108
|
+
region: AWS region for analysis
|
109
|
+
console: Rich console instance
|
110
|
+
dry_run: Safety-first READ-ONLY analysis mode
|
111
|
+
"""
|
112
|
+
self.profile = profile
|
113
|
+
self.region = region
|
114
|
+
self.console = console or Console()
|
115
|
+
self.dry_run = dry_run
|
116
|
+
|
117
|
+
# Initialize AWS session using enterprise profile management
|
118
|
+
self.session = None
|
119
|
+
if profile:
|
120
|
+
try:
|
121
|
+
self.session = create_operational_session(profile=profile)
|
122
|
+
print_success(f"Connected to AWS profile: {profile}")
|
123
|
+
except Exception as e:
|
124
|
+
print_error(f"Failed to connect to AWS: {e}")
|
125
|
+
|
126
|
+
# Results storage
|
127
|
+
self.last_discovery = None
|
128
|
+
self.last_awso_analysis = None
|
129
|
+
|
130
|
+
def discover_vpc_topology(self, vpc_ids: Optional[List[str]] = None) -> VPCDiscoveryResult:
|
131
|
+
"""
|
132
|
+
Comprehensive VPC topology discovery for AWSO-05 support
|
133
|
+
|
134
|
+
Args:
|
135
|
+
vpc_ids: Optional list of specific VPC IDs to analyze
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
VPCDiscoveryResult with complete topology information
|
139
|
+
"""
|
140
|
+
print_header("VPC Topology Discovery", "AWSO-05 Enhanced")
|
141
|
+
|
142
|
+
if not self.session:
|
143
|
+
print_error("No AWS session available")
|
144
|
+
return self._empty_discovery_result()
|
145
|
+
|
146
|
+
with self.console.status("[bold green]Discovering VPC topology...") as status:
|
147
|
+
try:
|
148
|
+
ec2 = self.session.client("ec2", region_name=self.region)
|
149
|
+
|
150
|
+
# Discover VPCs
|
151
|
+
status.update("🔍 Discovering VPCs...")
|
152
|
+
vpcs = self._discover_vpcs(ec2, vpc_ids)
|
153
|
+
|
154
|
+
# Discover NAT Gateways
|
155
|
+
status.update("🌐 Discovering NAT Gateways...")
|
156
|
+
nat_gateways = self._discover_nat_gateways(ec2, vpc_ids)
|
157
|
+
|
158
|
+
# Discover VPC Endpoints
|
159
|
+
status.update("🔗 Discovering VPC Endpoints...")
|
160
|
+
vpc_endpoints = self._discover_vpc_endpoints(ec2, vpc_ids)
|
161
|
+
|
162
|
+
# Discover Internet Gateways
|
163
|
+
status.update("🌍 Discovering Internet Gateways...")
|
164
|
+
internet_gateways = self._discover_internet_gateways(ec2, vpc_ids)
|
165
|
+
|
166
|
+
# Discover Route Tables
|
167
|
+
status.update("📋 Discovering Route Tables...")
|
168
|
+
route_tables = self._discover_route_tables(ec2, vpc_ids)
|
169
|
+
|
170
|
+
# Discover Subnets
|
171
|
+
status.update("🏗️ Discovering Subnets...")
|
172
|
+
subnets = self._discover_subnets(ec2, vpc_ids)
|
173
|
+
|
174
|
+
# Discover Network Interfaces (ENIs)
|
175
|
+
status.update("🔌 Discovering Network Interfaces...")
|
176
|
+
network_interfaces = self._discover_network_interfaces(ec2, vpc_ids)
|
177
|
+
|
178
|
+
# Discover Transit Gateway Attachments
|
179
|
+
status.update("🚇 Discovering Transit Gateway Attachments...")
|
180
|
+
tgw_attachments = self._discover_transit_gateway_attachments(ec2, vpc_ids)
|
181
|
+
|
182
|
+
# Discover VPC Peering Connections
|
183
|
+
status.update("🔄 Discovering VPC Peering Connections...")
|
184
|
+
vpc_peering = self._discover_vpc_peering_connections(ec2, vpc_ids)
|
185
|
+
|
186
|
+
# Discover Security Groups
|
187
|
+
status.update("🛡️ Discovering Security Groups...")
|
188
|
+
security_groups = self._discover_security_groups(ec2, vpc_ids)
|
189
|
+
|
190
|
+
# Create discovery result
|
191
|
+
result = VPCDiscoveryResult(
|
192
|
+
vpcs=vpcs,
|
193
|
+
nat_gateways=nat_gateways,
|
194
|
+
vpc_endpoints=vpc_endpoints,
|
195
|
+
internet_gateways=internet_gateways,
|
196
|
+
route_tables=route_tables,
|
197
|
+
subnets=subnets,
|
198
|
+
network_interfaces=network_interfaces,
|
199
|
+
transit_gateway_attachments=tgw_attachments,
|
200
|
+
vpc_peering_connections=vpc_peering,
|
201
|
+
security_groups=security_groups,
|
202
|
+
total_resources=len(vpcs) + len(nat_gateways) + len(vpc_endpoints) +
|
203
|
+
len(internet_gateways) + len(route_tables) + len(subnets) +
|
204
|
+
len(network_interfaces) + len(tgw_attachments) +
|
205
|
+
len(vpc_peering) + len(security_groups),
|
206
|
+
discovery_timestamp=datetime.now().isoformat()
|
207
|
+
)
|
208
|
+
|
209
|
+
self.last_discovery = result
|
210
|
+
self._display_discovery_results(result)
|
211
|
+
|
212
|
+
return result
|
213
|
+
|
214
|
+
except Exception as e:
|
215
|
+
print_error(f"VPC discovery failed: {e}")
|
216
|
+
logger.error(f"VPC discovery error: {e}")
|
217
|
+
return self._empty_discovery_result()
|
218
|
+
|
219
|
+
def analyze_awso_dependencies(self, discovery_result: Optional[VPCDiscoveryResult] = None) -> AWSOAnalysis:
|
220
|
+
"""
|
221
|
+
AWSO-05 specific dependency analysis for safe VPC cleanup
|
222
|
+
|
223
|
+
Implements 12-step dependency analysis:
|
224
|
+
1. ENI gate validation (critical blocking check)
|
225
|
+
2. NAT Gateway dependency mapping
|
226
|
+
3. IGW route table analysis
|
227
|
+
4. VPC Endpoint dependency check
|
228
|
+
5. Transit Gateway attachment validation
|
229
|
+
6. VPC Peering connection mapping
|
230
|
+
7. Security Group usage analysis
|
231
|
+
8. Route table dependency validation
|
232
|
+
9. Subnet resource mapping
|
233
|
+
10. Default VPC identification
|
234
|
+
11. Cross-account dependency check
|
235
|
+
12. Evidence bundle generation
|
236
|
+
|
237
|
+
Args:
|
238
|
+
discovery_result: Previous discovery result (uses last if None)
|
239
|
+
|
240
|
+
Returns:
|
241
|
+
AWSOAnalysis with comprehensive dependency mapping
|
242
|
+
"""
|
243
|
+
print_header("AWSO-05 Dependency Analysis", "12-Step Validation")
|
244
|
+
|
245
|
+
if discovery_result is None:
|
246
|
+
discovery_result = self.last_discovery
|
247
|
+
|
248
|
+
if not discovery_result:
|
249
|
+
print_warning("No discovery data available. Run discover_vpc_topology() first.")
|
250
|
+
return self._empty_awso_analysis()
|
251
|
+
|
252
|
+
with self.console.status("[bold yellow]Analyzing AWSO-05 dependencies...") as status:
|
253
|
+
try:
|
254
|
+
# Step 1: ENI gate validation (CRITICAL)
|
255
|
+
status.update("🚨 Step 1/12: ENI Gate Validation...")
|
256
|
+
eni_warnings = self._analyze_eni_gate_validation(discovery_result)
|
257
|
+
|
258
|
+
# Step 2-4: Network resource dependencies
|
259
|
+
status.update("🔗 Steps 2-4: Network Dependencies...")
|
260
|
+
network_deps = self._analyze_network_dependencies(discovery_result)
|
261
|
+
|
262
|
+
# Step 5-7: Gateway and endpoint dependencies
|
263
|
+
status.update("🌐 Steps 5-7: Gateway Dependencies...")
|
264
|
+
gateway_deps = self._analyze_gateway_dependencies(discovery_result)
|
265
|
+
|
266
|
+
# Step 8-10: Security and route dependencies
|
267
|
+
status.update("🛡️ Steps 8-10: Security Dependencies...")
|
268
|
+
security_deps = self._analyze_security_dependencies(discovery_result)
|
269
|
+
|
270
|
+
# Step 11: Cross-account dependency check
|
271
|
+
status.update("🔄 Step 11: Cross-Account Dependencies...")
|
272
|
+
cross_account_deps = self._analyze_cross_account_dependencies(discovery_result)
|
273
|
+
|
274
|
+
# Step 12: Default VPC identification
|
275
|
+
status.update("🎯 Step 12: Default VPC Analysis...")
|
276
|
+
default_vpcs = self._identify_default_vpcs(discovery_result)
|
277
|
+
|
278
|
+
# Generate cleanup recommendations
|
279
|
+
cleanup_recommendations = self._generate_cleanup_recommendations(
|
280
|
+
discovery_result, eni_warnings, default_vpcs
|
281
|
+
)
|
282
|
+
|
283
|
+
# Create evidence bundle
|
284
|
+
evidence_bundle = self._create_evidence_bundle(discovery_result, {
|
285
|
+
'eni_warnings': eni_warnings,
|
286
|
+
'network_deps': network_deps,
|
287
|
+
'gateway_deps': gateway_deps,
|
288
|
+
'security_deps': security_deps,
|
289
|
+
'cross_account_deps': cross_account_deps,
|
290
|
+
'default_vpcs': default_vpcs
|
291
|
+
})
|
292
|
+
|
293
|
+
# Compile dependency chain
|
294
|
+
dependency_chain = {
|
295
|
+
'network_resources': network_deps,
|
296
|
+
'gateway_resources': gateway_deps,
|
297
|
+
'security_resources': security_deps,
|
298
|
+
'cross_account_resources': cross_account_deps
|
299
|
+
}
|
300
|
+
|
301
|
+
# Create AWSO analysis result
|
302
|
+
awso_analysis = AWSOAnalysis(
|
303
|
+
default_vpcs=default_vpcs,
|
304
|
+
orphaned_resources=self._identify_orphaned_resources(discovery_result),
|
305
|
+
dependency_chain=dependency_chain,
|
306
|
+
eni_gate_warnings=eni_warnings,
|
307
|
+
cleanup_recommendations=cleanup_recommendations,
|
308
|
+
evidence_bundle=evidence_bundle
|
309
|
+
)
|
310
|
+
|
311
|
+
self.last_awso_analysis = awso_analysis
|
312
|
+
self._display_awso_analysis(awso_analysis)
|
313
|
+
|
314
|
+
return awso_analysis
|
315
|
+
|
316
|
+
except Exception as e:
|
317
|
+
print_error(f"AWSO-05 analysis failed: {e}")
|
318
|
+
logger.error(f"AWSO-05 analysis error: {e}")
|
319
|
+
return self._empty_awso_analysis()
|
320
|
+
|
321
|
+
def generate_cleanup_evidence(self, output_dir: str = "./awso_evidence") -> Dict[str, str]:
|
322
|
+
"""
|
323
|
+
Generate comprehensive evidence bundle for AWSO-05 cleanup
|
324
|
+
|
325
|
+
Creates SHA256-verified evidence bundle with:
|
326
|
+
- Complete resource inventory (JSON)
|
327
|
+
- Dependency analysis (JSON)
|
328
|
+
- ENI gate validation results (JSON)
|
329
|
+
- Cleanup recommendations (JSON)
|
330
|
+
- Executive summary (Markdown)
|
331
|
+
- Evidence manifest with checksums
|
332
|
+
|
333
|
+
Args:
|
334
|
+
output_dir: Directory to store evidence files
|
335
|
+
|
336
|
+
Returns:
|
337
|
+
Dict with generated file paths and checksums
|
338
|
+
"""
|
339
|
+
print_header("Evidence Bundle Generation", "AWSO-05 Compliance")
|
340
|
+
|
341
|
+
output_path = Path(output_dir)
|
342
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
343
|
+
|
344
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
345
|
+
evidence_files = {}
|
346
|
+
|
347
|
+
try:
|
348
|
+
# Generate discovery evidence
|
349
|
+
if self.last_discovery:
|
350
|
+
discovery_file = output_path / f"vpc_discovery_{timestamp}.json"
|
351
|
+
self._write_json_evidence(self.last_discovery.__dict__, discovery_file)
|
352
|
+
evidence_files['discovery'] = str(discovery_file)
|
353
|
+
|
354
|
+
# Generate AWSO analysis evidence
|
355
|
+
if self.last_awso_analysis:
|
356
|
+
awso_file = output_path / f"awso_analysis_{timestamp}.json"
|
357
|
+
self._write_json_evidence(self.last_awso_analysis.__dict__, awso_file)
|
358
|
+
evidence_files['awso_analysis'] = str(awso_file)
|
359
|
+
|
360
|
+
# Generate executive summary
|
361
|
+
summary_file = output_path / f"executive_summary_{timestamp}.md"
|
362
|
+
self._write_executive_summary(self.last_awso_analysis, summary_file)
|
363
|
+
evidence_files['executive_summary'] = str(summary_file)
|
364
|
+
|
365
|
+
# Generate evidence manifest with checksums
|
366
|
+
manifest_file = output_path / f"evidence_manifest_{timestamp}.json"
|
367
|
+
manifest = self._create_evidence_manifest(evidence_files)
|
368
|
+
self._write_json_evidence(manifest, manifest_file)
|
369
|
+
evidence_files['manifest'] = str(manifest_file)
|
370
|
+
|
371
|
+
print_success(f"Evidence bundle generated: {len(evidence_files)} files")
|
372
|
+
|
373
|
+
# Display evidence summary
|
374
|
+
table = create_table(
|
375
|
+
title="AWSO-05 Evidence Bundle",
|
376
|
+
columns=[
|
377
|
+
{"header": "Evidence Type", "style": "cyan"},
|
378
|
+
{"header": "File Path", "style": "green"},
|
379
|
+
{"header": "SHA256", "style": "dim"}
|
380
|
+
]
|
381
|
+
)
|
382
|
+
|
383
|
+
for evidence_type, file_path in evidence_files.items():
|
384
|
+
sha256 = manifest.get('file_checksums', {}).get(evidence_type, 'N/A')
|
385
|
+
table.add_row(evidence_type, file_path, sha256[:16] + "...")
|
386
|
+
|
387
|
+
self.console.print(table)
|
388
|
+
|
389
|
+
return evidence_files
|
390
|
+
|
391
|
+
except Exception as e:
|
392
|
+
print_error(f"Evidence generation failed: {e}")
|
393
|
+
logger.error(f"Evidence generation error: {e}")
|
394
|
+
return {}
|
395
|
+
|
396
|
+
# Private helper methods for VPC discovery
|
397
|
+
def _discover_vpcs(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
398
|
+
"""Discover VPCs with comprehensive metadata"""
|
399
|
+
try:
|
400
|
+
filters = []
|
401
|
+
if vpc_ids:
|
402
|
+
filters.append({'Name': 'vpc-id', 'Values': vpc_ids})
|
403
|
+
|
404
|
+
response = ec2_client.describe_vpcs(Filters=filters)
|
405
|
+
vpcs = []
|
406
|
+
|
407
|
+
for vpc in response.get('Vpcs', []):
|
408
|
+
vpc_info = {
|
409
|
+
'VpcId': vpc['VpcId'],
|
410
|
+
'CidrBlock': vpc['CidrBlock'],
|
411
|
+
'State': vpc['State'],
|
412
|
+
'IsDefault': vpc['IsDefault'],
|
413
|
+
'InstanceTenancy': vpc['InstanceTenancy'],
|
414
|
+
'DhcpOptionsId': vpc['DhcpOptionsId'],
|
415
|
+
'Tags': {tag['Key']: tag['Value'] for tag in vpc.get('Tags', [])},
|
416
|
+
'Name': self._get_name_tag(vpc.get('Tags', [])),
|
417
|
+
'DiscoveredAt': datetime.now().isoformat()
|
418
|
+
}
|
419
|
+
vpcs.append(vpc_info)
|
420
|
+
|
421
|
+
return vpcs
|
422
|
+
|
423
|
+
except Exception as e:
|
424
|
+
logger.error(f"Failed to discover VPCs: {e}")
|
425
|
+
return []
|
426
|
+
|
427
|
+
def _discover_nat_gateways(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
428
|
+
"""Discover NAT Gateways with cost and usage information"""
|
429
|
+
try:
|
430
|
+
response = ec2_client.describe_nat_gateways()
|
431
|
+
nat_gateways = []
|
432
|
+
|
433
|
+
for nat in response.get('NatGateways', []):
|
434
|
+
# Filter by VPC if specified
|
435
|
+
if vpc_ids and nat.get('VpcId') not in vpc_ids:
|
436
|
+
continue
|
437
|
+
|
438
|
+
nat_info = {
|
439
|
+
'NatGatewayId': nat['NatGatewayId'],
|
440
|
+
'VpcId': nat.get('VpcId'),
|
441
|
+
'SubnetId': nat.get('SubnetId'),
|
442
|
+
'State': nat['State'],
|
443
|
+
'CreateTime': nat.get('CreateTime', '').isoformat() if nat.get('CreateTime') else None,
|
444
|
+
'ConnectivityType': nat.get('ConnectivityType', 'public'),
|
445
|
+
'Tags': {tag['Key']: tag['Value'] for tag in nat.get('Tags', [])},
|
446
|
+
'Name': self._get_name_tag(nat.get('Tags', [])),
|
447
|
+
'EstimatedMonthlyCost': 45.0, # Base NAT Gateway cost
|
448
|
+
'DiscoveredAt': datetime.now().isoformat()
|
449
|
+
}
|
450
|
+
nat_gateways.append(nat_info)
|
451
|
+
|
452
|
+
return nat_gateways
|
453
|
+
|
454
|
+
except Exception as e:
|
455
|
+
logger.error(f"Failed to discover NAT Gateways: {e}")
|
456
|
+
return []
|
457
|
+
|
458
|
+
def _discover_vpc_endpoints(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
459
|
+
"""Discover VPC Endpoints with cost analysis"""
|
460
|
+
try:
|
461
|
+
response = ec2_client.describe_vpc_endpoints()
|
462
|
+
endpoints = []
|
463
|
+
|
464
|
+
for endpoint in response.get('VpcEndpoints', []):
|
465
|
+
# Filter by VPC if specified
|
466
|
+
if vpc_ids and endpoint.get('VpcId') not in vpc_ids:
|
467
|
+
continue
|
468
|
+
|
469
|
+
# Calculate costs
|
470
|
+
monthly_cost = 0
|
471
|
+
if endpoint.get('VpcEndpointType') == 'Interface':
|
472
|
+
az_count = len(endpoint.get('SubnetIds', []))
|
473
|
+
monthly_cost = 10.0 * az_count # $10/month per AZ
|
474
|
+
|
475
|
+
endpoint_info = {
|
476
|
+
'VpcEndpointId': endpoint['VpcEndpointId'],
|
477
|
+
'VpcId': endpoint.get('VpcId'),
|
478
|
+
'ServiceName': endpoint.get('ServiceName'),
|
479
|
+
'VpcEndpointType': endpoint.get('VpcEndpointType', 'Gateway'),
|
480
|
+
'State': endpoint.get('State'),
|
481
|
+
'SubnetIds': endpoint.get('SubnetIds', []),
|
482
|
+
'RouteTableIds': endpoint.get('RouteTableIds', []),
|
483
|
+
'PolicyDocument': endpoint.get('PolicyDocument'),
|
484
|
+
'Tags': {tag['Key']: tag['Value'] for tag in endpoint.get('Tags', [])},
|
485
|
+
'Name': self._get_name_tag(endpoint.get('Tags', [])),
|
486
|
+
'EstimatedMonthlyCost': monthly_cost,
|
487
|
+
'DiscoveredAt': datetime.now().isoformat()
|
488
|
+
}
|
489
|
+
endpoints.append(endpoint_info)
|
490
|
+
|
491
|
+
return endpoints
|
492
|
+
|
493
|
+
except Exception as e:
|
494
|
+
logger.error(f"Failed to discover VPC Endpoints: {e}")
|
495
|
+
return []
|
496
|
+
|
497
|
+
def _discover_internet_gateways(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
498
|
+
"""Discover Internet Gateways"""
|
499
|
+
try:
|
500
|
+
response = ec2_client.describe_internet_gateways()
|
501
|
+
igws = []
|
502
|
+
|
503
|
+
for igw in response.get('InternetGateways', []):
|
504
|
+
# Filter by attached VPC if specified
|
505
|
+
attached_vpc_ids = [attachment['VpcId'] for attachment in igw.get('Attachments', [])]
|
506
|
+
if vpc_ids and not any(vpc_id in attached_vpc_ids for vpc_id in vpc_ids):
|
507
|
+
continue
|
508
|
+
|
509
|
+
igw_info = {
|
510
|
+
'InternetGatewayId': igw['InternetGatewayId'],
|
511
|
+
'Attachments': igw.get('Attachments', []),
|
512
|
+
'AttachedVpcIds': attached_vpc_ids,
|
513
|
+
'Tags': {tag['Key']: tag['Value'] for tag in igw.get('Tags', [])},
|
514
|
+
'Name': self._get_name_tag(igw.get('Tags', [])),
|
515
|
+
'DiscoveredAt': datetime.now().isoformat()
|
516
|
+
}
|
517
|
+
igws.append(igw_info)
|
518
|
+
|
519
|
+
return igws
|
520
|
+
|
521
|
+
except Exception as e:
|
522
|
+
logger.error(f"Failed to discover Internet Gateways: {e}")
|
523
|
+
return []
|
524
|
+
|
525
|
+
def _discover_route_tables(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
526
|
+
"""Discover Route Tables with dependency mapping"""
|
527
|
+
try:
|
528
|
+
filters = []
|
529
|
+
if vpc_ids:
|
530
|
+
filters.append({'Name': 'vpc-id', 'Values': vpc_ids})
|
531
|
+
|
532
|
+
response = ec2_client.describe_route_tables(Filters=filters)
|
533
|
+
route_tables = []
|
534
|
+
|
535
|
+
for rt in response.get('RouteTables', []):
|
536
|
+
rt_info = {
|
537
|
+
'RouteTableId': rt['RouteTableId'],
|
538
|
+
'VpcId': rt['VpcId'],
|
539
|
+
'Routes': rt.get('Routes', []),
|
540
|
+
'Associations': rt.get('Associations', []),
|
541
|
+
'Tags': {tag['Key']: tag['Value'] for tag in rt.get('Tags', [])},
|
542
|
+
'Name': self._get_name_tag(rt.get('Tags', [])),
|
543
|
+
'IsMainRouteTable': any(assoc.get('Main', False) for assoc in rt.get('Associations', [])),
|
544
|
+
'AssociatedSubnets': [assoc.get('SubnetId') for assoc in rt.get('Associations', []) if assoc.get('SubnetId')],
|
545
|
+
'DiscoveredAt': datetime.now().isoformat()
|
546
|
+
}
|
547
|
+
route_tables.append(rt_info)
|
548
|
+
|
549
|
+
return route_tables
|
550
|
+
|
551
|
+
except Exception as e:
|
552
|
+
logger.error(f"Failed to discover Route Tables: {e}")
|
553
|
+
return []
|
554
|
+
|
555
|
+
def _discover_subnets(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
556
|
+
"""Discover Subnets with resource mapping"""
|
557
|
+
try:
|
558
|
+
filters = []
|
559
|
+
if vpc_ids:
|
560
|
+
filters.append({'Name': 'vpc-id', 'Values': vpc_ids})
|
561
|
+
|
562
|
+
response = ec2_client.describe_subnets(Filters=filters)
|
563
|
+
subnets = []
|
564
|
+
|
565
|
+
for subnet in response.get('Subnets', []):
|
566
|
+
subnet_info = {
|
567
|
+
'SubnetId': subnet['SubnetId'],
|
568
|
+
'VpcId': subnet['VpcId'],
|
569
|
+
'CidrBlock': subnet['CidrBlock'],
|
570
|
+
'AvailabilityZone': subnet['AvailabilityZone'],
|
571
|
+
'State': subnet['State'],
|
572
|
+
'MapPublicIpOnLaunch': subnet.get('MapPublicIpOnLaunch', False),
|
573
|
+
'AvailableIpAddressCount': subnet.get('AvailableIpAddressCount', 0),
|
574
|
+
'Tags': {tag['Key']: tag['Value'] for tag in subnet.get('Tags', [])},
|
575
|
+
'Name': self._get_name_tag(subnet.get('Tags', [])),
|
576
|
+
'DiscoveredAt': datetime.now().isoformat()
|
577
|
+
}
|
578
|
+
subnets.append(subnet_info)
|
579
|
+
|
580
|
+
return subnets
|
581
|
+
|
582
|
+
except Exception as e:
|
583
|
+
logger.error(f"Failed to discover Subnets: {e}")
|
584
|
+
return []
|
585
|
+
|
586
|
+
def _discover_network_interfaces(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
587
|
+
"""Discover Network Interfaces (ENIs) - Critical for AWSO-05 ENI gate validation"""
|
588
|
+
try:
|
589
|
+
filters = []
|
590
|
+
if vpc_ids:
|
591
|
+
filters.append({'Name': 'vpc-id', 'Values': vpc_ids})
|
592
|
+
|
593
|
+
response = ec2_client.describe_network_interfaces(Filters=filters)
|
594
|
+
network_interfaces = []
|
595
|
+
|
596
|
+
for eni in response.get('NetworkInterfaces', []):
|
597
|
+
eni_info = {
|
598
|
+
'NetworkInterfaceId': eni['NetworkInterfaceId'],
|
599
|
+
'VpcId': eni.get('VpcId'),
|
600
|
+
'SubnetId': eni.get('SubnetId'),
|
601
|
+
'Status': eni.get('Status'),
|
602
|
+
'InterfaceType': eni.get('InterfaceType', 'interface'),
|
603
|
+
'Attachment': eni.get('Attachment'),
|
604
|
+
'Groups': eni.get('Groups', []),
|
605
|
+
'PrivateIpAddress': eni.get('PrivateIpAddress'),
|
606
|
+
'PrivateIpAddresses': eni.get('PrivateIpAddresses', []),
|
607
|
+
'Tags': {tag['Key']: tag['Value'] for tag in eni.get('Tags', [])},
|
608
|
+
'Name': self._get_name_tag(eni.get('Tags', [])),
|
609
|
+
'RequesterManaged': eni.get('RequesterManaged', False),
|
610
|
+
'IsAttached': bool(eni.get('Attachment')),
|
611
|
+
'AttachedInstanceId': eni.get('Attachment', {}).get('InstanceId'),
|
612
|
+
'DiscoveredAt': datetime.now().isoformat()
|
613
|
+
}
|
614
|
+
network_interfaces.append(eni_info)
|
615
|
+
|
616
|
+
return network_interfaces
|
617
|
+
|
618
|
+
except Exception as e:
|
619
|
+
logger.error(f"Failed to discover Network Interfaces: {e}")
|
620
|
+
return []
|
621
|
+
|
622
|
+
def _discover_transit_gateway_attachments(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
623
|
+
"""Discover Transit Gateway Attachments"""
|
624
|
+
try:
|
625
|
+
response = ec2_client.describe_transit_gateway_attachments()
|
626
|
+
attachments = []
|
627
|
+
|
628
|
+
for attachment in response.get('TransitGatewayAttachments', []):
|
629
|
+
# Filter by VPC if specified
|
630
|
+
if vpc_ids and attachment.get('ResourceType') == 'vpc' and attachment.get('ResourceId') not in vpc_ids:
|
631
|
+
continue
|
632
|
+
|
633
|
+
attachment_info = {
|
634
|
+
'TransitGatewayAttachmentId': attachment['TransitGatewayAttachmentId'],
|
635
|
+
'TransitGatewayId': attachment.get('TransitGatewayId'),
|
636
|
+
'ResourceType': attachment.get('ResourceType'),
|
637
|
+
'ResourceId': attachment.get('ResourceId'),
|
638
|
+
'State': attachment.get('State'),
|
639
|
+
'Tags': {tag['Key']: tag['Value'] for tag in attachment.get('Tags', [])},
|
640
|
+
'Name': self._get_name_tag(attachment.get('Tags', [])),
|
641
|
+
'ResourceOwnerId': attachment.get('ResourceOwnerId'),
|
642
|
+
'DiscoveredAt': datetime.now().isoformat()
|
643
|
+
}
|
644
|
+
attachments.append(attachment_info)
|
645
|
+
|
646
|
+
return attachments
|
647
|
+
|
648
|
+
except Exception as e:
|
649
|
+
logger.error(f"Failed to discover Transit Gateway Attachments: {e}")
|
650
|
+
return []
|
651
|
+
|
652
|
+
def _discover_vpc_peering_connections(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
653
|
+
"""Discover VPC Peering Connections"""
|
654
|
+
try:
|
655
|
+
response = ec2_client.describe_vpc_peering_connections()
|
656
|
+
connections = []
|
657
|
+
|
658
|
+
for connection in response.get('VpcPeeringConnections', []):
|
659
|
+
accepter_vpc_id = connection.get('AccepterVpcInfo', {}).get('VpcId')
|
660
|
+
requester_vpc_id = connection.get('RequesterVpcInfo', {}).get('VpcId')
|
661
|
+
|
662
|
+
# Filter by VPC if specified
|
663
|
+
if vpc_ids and accepter_vpc_id not in vpc_ids and requester_vpc_id not in vpc_ids:
|
664
|
+
continue
|
665
|
+
|
666
|
+
connection_info = {
|
667
|
+
'VpcPeeringConnectionId': connection['VpcPeeringConnectionId'],
|
668
|
+
'AccepterVpcInfo': connection.get('AccepterVpcInfo', {}),
|
669
|
+
'RequesterVpcInfo': connection.get('RequesterVpcInfo', {}),
|
670
|
+
'Status': connection.get('Status', {}),
|
671
|
+
'Tags': {tag['Key']: tag['Value'] for tag in connection.get('Tags', [])},
|
672
|
+
'Name': self._get_name_tag(connection.get('Tags', [])),
|
673
|
+
'ExpirationTime': connection.get('ExpirationTime'),
|
674
|
+
'DiscoveredAt': datetime.now().isoformat()
|
675
|
+
}
|
676
|
+
connections.append(connection_info)
|
677
|
+
|
678
|
+
return connections
|
679
|
+
|
680
|
+
except Exception as e:
|
681
|
+
logger.error(f"Failed to discover VPC Peering Connections: {e}")
|
682
|
+
return []
|
683
|
+
|
684
|
+
def _discover_security_groups(self, ec2_client, vpc_ids: Optional[List[str]]) -> List[Dict[str, Any]]:
|
685
|
+
"""Discover Security Groups"""
|
686
|
+
try:
|
687
|
+
filters = []
|
688
|
+
if vpc_ids:
|
689
|
+
filters.append({'Name': 'vpc-id', 'Values': vpc_ids})
|
690
|
+
|
691
|
+
response = ec2_client.describe_security_groups(Filters=filters)
|
692
|
+
security_groups = []
|
693
|
+
|
694
|
+
for sg in response.get('SecurityGroups', []):
|
695
|
+
sg_info = {
|
696
|
+
'GroupId': sg['GroupId'],
|
697
|
+
'GroupName': sg['GroupName'],
|
698
|
+
'VpcId': sg.get('VpcId'),
|
699
|
+
'Description': sg.get('Description', ''),
|
700
|
+
'IpPermissions': sg.get('IpPermissions', []),
|
701
|
+
'IpPermissionsEgress': sg.get('IpPermissionsEgress', []),
|
702
|
+
'Tags': {tag['Key']: tag['Value'] for tag in sg.get('Tags', [])},
|
703
|
+
'Name': self._get_name_tag(sg.get('Tags', [])),
|
704
|
+
'IsDefault': sg.get('GroupName') == 'default',
|
705
|
+
'DiscoveredAt': datetime.now().isoformat()
|
706
|
+
}
|
707
|
+
security_groups.append(sg_info)
|
708
|
+
|
709
|
+
return security_groups
|
710
|
+
|
711
|
+
except Exception as e:
|
712
|
+
logger.error(f"Failed to discover Security Groups: {e}")
|
713
|
+
return []
|
714
|
+
|
715
|
+
# AWSO-05 Analysis Methods
|
716
|
+
def _analyze_eni_gate_validation(self, discovery: VPCDiscoveryResult) -> List[Dict[str, Any]]:
|
717
|
+
"""AWSO-05 Step 1: Critical ENI gate validation to prevent workload disruption"""
|
718
|
+
warnings = []
|
719
|
+
|
720
|
+
for eni in discovery.network_interfaces:
|
721
|
+
# Check for attached ENIs that could indicate active workloads
|
722
|
+
if eni['IsAttached'] and not eni['RequesterManaged']:
|
723
|
+
warnings.append({
|
724
|
+
'NetworkInterfaceId': eni['NetworkInterfaceId'],
|
725
|
+
'VpcId': eni['VpcId'],
|
726
|
+
'AttachedInstanceId': eni.get('AttachedInstanceId'),
|
727
|
+
'WarningType': 'ATTACHED_ENI',
|
728
|
+
'RiskLevel': 'HIGH',
|
729
|
+
'Message': f"ENI {eni['NetworkInterfaceId']} is attached to instance {eni.get('AttachedInstanceId')} - VPC cleanup may disrupt workload",
|
730
|
+
'Recommendation': 'Verify workload migration before VPC cleanup'
|
731
|
+
})
|
732
|
+
|
733
|
+
return warnings
|
734
|
+
|
735
|
+
def _analyze_network_dependencies(self, discovery: VPCDiscoveryResult) -> Dict[str, List[str]]:
|
736
|
+
"""AWSO-05 Steps 2-4: Network resource dependency analysis"""
|
737
|
+
dependencies = {}
|
738
|
+
|
739
|
+
# NAT Gateway dependencies
|
740
|
+
for nat in discovery.nat_gateways:
|
741
|
+
vpc_id = nat['VpcId']
|
742
|
+
if vpc_id not in dependencies:
|
743
|
+
dependencies[vpc_id] = []
|
744
|
+
dependencies[vpc_id].append(f"NAT Gateway: {nat['NatGatewayId']}")
|
745
|
+
|
746
|
+
# VPC Endpoint dependencies
|
747
|
+
for endpoint in discovery.vpc_endpoints:
|
748
|
+
vpc_id = endpoint['VpcId']
|
749
|
+
if vpc_id not in dependencies:
|
750
|
+
dependencies[vpc_id] = []
|
751
|
+
dependencies[vpc_id].append(f"VPC Endpoint: {endpoint['VpcEndpointId']}")
|
752
|
+
|
753
|
+
return dependencies
|
754
|
+
|
755
|
+
def _analyze_gateway_dependencies(self, discovery: VPCDiscoveryResult) -> Dict[str, List[str]]:
|
756
|
+
"""AWSO-05 Steps 5-7: Gateway dependency analysis"""
|
757
|
+
dependencies = {}
|
758
|
+
|
759
|
+
# Internet Gateway dependencies
|
760
|
+
for igw in discovery.internet_gateways:
|
761
|
+
for vpc_id in igw['AttachedVpcIds']:
|
762
|
+
if vpc_id not in dependencies:
|
763
|
+
dependencies[vpc_id] = []
|
764
|
+
dependencies[vpc_id].append(f"Internet Gateway: {igw['InternetGatewayId']}")
|
765
|
+
|
766
|
+
# Transit Gateway Attachment dependencies
|
767
|
+
for attachment in discovery.transit_gateway_attachments:
|
768
|
+
if attachment['ResourceType'] == 'vpc':
|
769
|
+
vpc_id = attachment['ResourceId']
|
770
|
+
if vpc_id not in dependencies:
|
771
|
+
dependencies[vpc_id] = []
|
772
|
+
dependencies[vpc_id].append(f"Transit Gateway Attachment: {attachment['TransitGatewayAttachmentId']}")
|
773
|
+
|
774
|
+
return dependencies
|
775
|
+
|
776
|
+
def _analyze_security_dependencies(self, discovery: VPCDiscoveryResult) -> Dict[str, List[str]]:
|
777
|
+
"""AWSO-05 Steps 8-10: Security and route dependency analysis"""
|
778
|
+
dependencies = {}
|
779
|
+
|
780
|
+
# Route Table dependencies
|
781
|
+
for rt in discovery.route_tables:
|
782
|
+
vpc_id = rt['VpcId']
|
783
|
+
if vpc_id not in dependencies:
|
784
|
+
dependencies[vpc_id] = []
|
785
|
+
if not rt['IsMainRouteTable']: # Don't list main route tables as dependencies
|
786
|
+
dependencies[vpc_id].append(f"Route Table: {rt['RouteTableId']}")
|
787
|
+
|
788
|
+
# Security Group dependencies (non-default)
|
789
|
+
for sg in discovery.security_groups:
|
790
|
+
if not sg['IsDefault']:
|
791
|
+
vpc_id = sg['VpcId']
|
792
|
+
if vpc_id not in dependencies:
|
793
|
+
dependencies[vpc_id] = []
|
794
|
+
dependencies[vpc_id].append(f"Security Group: {sg['GroupId']}")
|
795
|
+
|
796
|
+
return dependencies
|
797
|
+
|
798
|
+
def _analyze_cross_account_dependencies(self, discovery: VPCDiscoveryResult) -> Dict[str, List[str]]:
|
799
|
+
"""AWSO-05 Step 11: Cross-account dependency analysis"""
|
800
|
+
dependencies = {}
|
801
|
+
|
802
|
+
# VPC Peering cross-account connections
|
803
|
+
for connection in discovery.vpc_peering_connections:
|
804
|
+
accepter_vpc = connection['AccepterVpcInfo']
|
805
|
+
requester_vpc = connection['RequesterVpcInfo']
|
806
|
+
|
807
|
+
# Check for cross-account peering
|
808
|
+
if accepter_vpc.get('OwnerId') != requester_vpc.get('OwnerId'):
|
809
|
+
for vpc_info in [accepter_vpc, requester_vpc]:
|
810
|
+
vpc_id = vpc_info.get('VpcId')
|
811
|
+
if vpc_id:
|
812
|
+
if vpc_id not in dependencies:
|
813
|
+
dependencies[vpc_id] = []
|
814
|
+
dependencies[vpc_id].append(f"Cross-Account VPC Peering: {connection['VpcPeeringConnectionId']}")
|
815
|
+
|
816
|
+
return dependencies
|
817
|
+
|
818
|
+
def _identify_default_vpcs(self, discovery: VPCDiscoveryResult) -> List[Dict[str, Any]]:
|
819
|
+
"""AWSO-05 Step 12: Identify default VPCs for CIS Benchmark compliance"""
|
820
|
+
default_vpcs = []
|
821
|
+
|
822
|
+
for vpc in discovery.vpcs:
|
823
|
+
if vpc['IsDefault']:
|
824
|
+
# Check for resources in default VPC
|
825
|
+
resources_in_vpc = []
|
826
|
+
|
827
|
+
# Count ENIs (excluding AWS managed)
|
828
|
+
eni_count = len([eni for eni in discovery.network_interfaces
|
829
|
+
if eni['VpcId'] == vpc['VpcId'] and not eni['RequesterManaged']])
|
830
|
+
if eni_count > 0:
|
831
|
+
resources_in_vpc.append(f"{eni_count} Network Interfaces")
|
832
|
+
|
833
|
+
# Count NAT Gateways
|
834
|
+
nat_count = len([nat for nat in discovery.nat_gateways if nat['VpcId'] == vpc['VpcId']])
|
835
|
+
if nat_count > 0:
|
836
|
+
resources_in_vpc.append(f"{nat_count} NAT Gateways")
|
837
|
+
|
838
|
+
# Count VPC Endpoints
|
839
|
+
endpoint_count = len([ep for ep in discovery.vpc_endpoints if ep['VpcId'] == vpc['VpcId']])
|
840
|
+
if endpoint_count > 0:
|
841
|
+
resources_in_vpc.append(f"{endpoint_count} VPC Endpoints")
|
842
|
+
|
843
|
+
default_vpc_info = {
|
844
|
+
'VpcId': vpc['VpcId'],
|
845
|
+
'CidrBlock': vpc['CidrBlock'],
|
846
|
+
'Region': self.region,
|
847
|
+
'ResourcesPresent': resources_in_vpc,
|
848
|
+
'ResourceCount': len(resources_in_vpc),
|
849
|
+
'CleanupRecommendation': 'DELETE' if len(resources_in_vpc) == 0 else 'MIGRATE_RESOURCES_FIRST',
|
850
|
+
'CISBenchmarkCompliance': 'NON_COMPLIANT',
|
851
|
+
'SecurityRisk': 'HIGH' if len(resources_in_vpc) > 0 else 'MEDIUM'
|
852
|
+
}
|
853
|
+
default_vpcs.append(default_vpc_info)
|
854
|
+
|
855
|
+
return default_vpcs
|
856
|
+
|
857
|
+
def _identify_orphaned_resources(self, discovery: VPCDiscoveryResult) -> List[Dict[str, Any]]:
|
858
|
+
"""Identify orphaned resources that can be safely cleaned up"""
|
859
|
+
orphaned = []
|
860
|
+
|
861
|
+
# Orphaned NAT Gateways (no route table references)
|
862
|
+
used_nat_gateways = set()
|
863
|
+
for rt in discovery.route_tables:
|
864
|
+
for route in rt['Routes']:
|
865
|
+
if route.get('NatGatewayId'):
|
866
|
+
used_nat_gateways.add(route['NatGatewayId'])
|
867
|
+
|
868
|
+
for nat in discovery.nat_gateways:
|
869
|
+
if nat['NatGatewayId'] not in used_nat_gateways and nat['State'] == 'available':
|
870
|
+
orphaned.append({
|
871
|
+
'ResourceType': 'NAT Gateway',
|
872
|
+
'ResourceId': nat['NatGatewayId'],
|
873
|
+
'VpcId': nat['VpcId'],
|
874
|
+
'Reason': 'No route table references',
|
875
|
+
'EstimatedMonthlySavings': nat['EstimatedMonthlyCost']
|
876
|
+
})
|
877
|
+
|
878
|
+
return orphaned
|
879
|
+
|
880
|
+
def _generate_cleanup_recommendations(self, discovery: VPCDiscoveryResult,
|
881
|
+
eni_warnings: List[Dict],
|
882
|
+
default_vpcs: List[Dict]) -> List[Dict[str, Any]]:
|
883
|
+
"""Generate AWSO-05 cleanup recommendations"""
|
884
|
+
recommendations = []
|
885
|
+
|
886
|
+
# Default VPC cleanup recommendations
|
887
|
+
for default_vpc in default_vpcs:
|
888
|
+
if default_vpc['CleanupRecommendation'] == 'DELETE':
|
889
|
+
recommendations.append({
|
890
|
+
'Priority': 'HIGH',
|
891
|
+
'Action': 'DELETE_DEFAULT_VPC',
|
892
|
+
'ResourceType': 'VPC',
|
893
|
+
'ResourceId': default_vpc['VpcId'],
|
894
|
+
'Reason': 'Empty default VPC - CIS Benchmark compliance',
|
895
|
+
'EstimatedMonthlySavings': 0,
|
896
|
+
'SecurityBenefit': 'Reduces attack surface',
|
897
|
+
'RiskLevel': 'LOW'
|
898
|
+
})
|
899
|
+
else:
|
900
|
+
recommendations.append({
|
901
|
+
'Priority': 'MEDIUM',
|
902
|
+
'Action': 'MIGRATE_FROM_DEFAULT_VPC',
|
903
|
+
'ResourceType': 'VPC',
|
904
|
+
'ResourceId': default_vpc['VpcId'],
|
905
|
+
'Reason': 'Default VPC with resources - requires migration',
|
906
|
+
'EstimatedMonthlySavings': 0,
|
907
|
+
'SecurityBenefit': 'Improves security posture',
|
908
|
+
'RiskLevel': 'HIGH'
|
909
|
+
})
|
910
|
+
|
911
|
+
# ENI-based recommendations
|
912
|
+
if eni_warnings:
|
913
|
+
recommendations.append({
|
914
|
+
'Priority': 'CRITICAL',
|
915
|
+
'Action': 'REVIEW_WORKLOAD_MIGRATION',
|
916
|
+
'ResourceType': 'Multiple',
|
917
|
+
'ResourceId': 'Multiple ENIs',
|
918
|
+
'Reason': f'{len(eni_warnings)} attached ENIs detected - workload migration required',
|
919
|
+
'EstimatedMonthlySavings': 0,
|
920
|
+
'SecurityBenefit': 'Prevents workload disruption',
|
921
|
+
'RiskLevel': 'CRITICAL'
|
922
|
+
})
|
923
|
+
|
924
|
+
return recommendations
|
925
|
+
|
926
|
+
def _create_evidence_bundle(self, discovery: VPCDiscoveryResult, analysis_data: Dict) -> Dict[str, Any]:
|
927
|
+
"""Create comprehensive evidence bundle for AWSO-05 compliance"""
|
928
|
+
return {
|
929
|
+
'BundleVersion': '1.0',
|
930
|
+
'GeneratedAt': datetime.now().isoformat(),
|
931
|
+
'Profile': self.profile,
|
932
|
+
'Region': self.region,
|
933
|
+
'DiscoverySummary': {
|
934
|
+
'TotalVPCs': len(discovery.vpcs),
|
935
|
+
'DefaultVPCs': len(analysis_data['default_vpcs']),
|
936
|
+
'TotalResources': discovery.total_resources,
|
937
|
+
'ENIWarnings': len(analysis_data['eni_warnings'])
|
938
|
+
},
|
939
|
+
'ComplianceStatus': {
|
940
|
+
'CISBenchmark': 'NON_COMPLIANT' if analysis_data['default_vpcs'] else 'COMPLIANT',
|
941
|
+
'ENIGateValidation': 'PASSED' if not analysis_data['eni_warnings'] else 'WARNINGS_PRESENT'
|
942
|
+
},
|
943
|
+
'CleanupReadiness': 'READY' if not analysis_data['eni_warnings'] else 'REQUIRES_WORKLOAD_MIGRATION'
|
944
|
+
}
|
945
|
+
|
946
|
+
# Helper methods
|
947
|
+
def _empty_discovery_result(self) -> VPCDiscoveryResult:
|
948
|
+
"""Return empty discovery result"""
|
949
|
+
return VPCDiscoveryResult(
|
950
|
+
vpcs=[], nat_gateways=[], vpc_endpoints=[], internet_gateways=[],
|
951
|
+
route_tables=[], subnets=[], network_interfaces=[],
|
952
|
+
transit_gateway_attachments=[], vpc_peering_connections=[],
|
953
|
+
security_groups=[], total_resources=0,
|
954
|
+
discovery_timestamp=datetime.now().isoformat()
|
955
|
+
)
|
956
|
+
|
957
|
+
def _empty_awso_analysis(self) -> AWSOAnalysis:
|
958
|
+
"""Return empty AWSO analysis result"""
|
959
|
+
return AWSOAnalysis(
|
960
|
+
default_vpcs=[], orphaned_resources=[], dependency_chain={},
|
961
|
+
eni_gate_warnings=[], cleanup_recommendations=[],
|
962
|
+
evidence_bundle={}
|
963
|
+
)
|
964
|
+
|
965
|
+
def _get_name_tag(self, tags: List[Dict]) -> str:
|
966
|
+
"""Extract Name tag from tag list"""
|
967
|
+
for tag in tags:
|
968
|
+
if tag['Key'] == 'Name':
|
969
|
+
return tag['Value']
|
970
|
+
return 'Unnamed'
|
971
|
+
|
972
|
+
def _display_discovery_results(self, result: VPCDiscoveryResult):
|
973
|
+
"""Display VPC discovery results with Rich formatting"""
|
974
|
+
# Summary panel
|
975
|
+
summary = Panel(
|
976
|
+
f"[bold green]VPC Discovery Complete[/bold green]\n\n"
|
977
|
+
f"VPCs: [bold cyan]{len(result.vpcs)}[/bold cyan]\n"
|
978
|
+
f"NAT Gateways: [bold yellow]{len(result.nat_gateways)}[/bold yellow]\n"
|
979
|
+
f"VPC Endpoints: [bold blue]{len(result.vpc_endpoints)}[/bold blue]\n"
|
980
|
+
f"Internet Gateways: [bold green]{len(result.internet_gateways)}[/bold green]\n"
|
981
|
+
f"Route Tables: [bold magenta]{len(result.route_tables)}[/bold magenta]\n"
|
982
|
+
f"Subnets: [bold red]{len(result.subnets)}[/bold red]\n"
|
983
|
+
f"Network Interfaces: [bold white]{len(result.network_interfaces)}[/bold white]\n"
|
984
|
+
f"Transit Gateway Attachments: [bold orange]{len(result.transit_gateway_attachments)}[/bold orange]\n"
|
985
|
+
f"VPC Peering Connections: [bold purple]{len(result.vpc_peering_connections)}[/bold purple]\n"
|
986
|
+
f"Security Groups: [bold gray]{len(result.security_groups)}[/bold gray]\n\n"
|
987
|
+
f"[dim]Total Resources: {result.total_resources}[/dim]",
|
988
|
+
title="🔍 VPC Discovery Summary",
|
989
|
+
style="bold blue"
|
990
|
+
)
|
991
|
+
self.console.print(summary)
|
992
|
+
|
993
|
+
def _display_awso_analysis(self, analysis: AWSOAnalysis):
|
994
|
+
"""Display AWSO-05 analysis results with Rich formatting"""
|
995
|
+
# Create summary tree
|
996
|
+
tree = Tree("🎯 AWSO-05 Analysis Results")
|
997
|
+
|
998
|
+
# Default VPCs branch
|
999
|
+
default_branch = tree.add("🚨 Default VPCs")
|
1000
|
+
for vpc in analysis.default_vpcs:
|
1001
|
+
status = "🔴 Non-Compliant" if vpc['SecurityRisk'] == 'HIGH' else "🟡 Requires Review"
|
1002
|
+
default_branch.add(f"{vpc['VpcId']} - {status}")
|
1003
|
+
|
1004
|
+
# ENI Warnings branch
|
1005
|
+
eni_branch = tree.add("⚠️ ENI Gate Warnings")
|
1006
|
+
for warning in analysis.eni_gate_warnings:
|
1007
|
+
eni_branch.add(f"{warning['NetworkInterfaceId']} - {warning['Message']}")
|
1008
|
+
|
1009
|
+
# Recommendations branch
|
1010
|
+
rec_branch = tree.add("💡 Cleanup Recommendations")
|
1011
|
+
for rec in analysis.cleanup_recommendations:
|
1012
|
+
priority_icon = "🔴" if rec['Priority'] == 'CRITICAL' else "🟡" if rec['Priority'] == 'HIGH' else "🟢"
|
1013
|
+
rec_branch.add(f"{priority_icon} {rec['Action']} - {rec['ResourceId']}")
|
1014
|
+
|
1015
|
+
self.console.print(tree)
|
1016
|
+
|
1017
|
+
# Evidence bundle summary
|
1018
|
+
bundle_info = Panel(
|
1019
|
+
f"Bundle Version: [bold]{analysis.evidence_bundle.get('BundleVersion', 'N/A')}[/bold]\n"
|
1020
|
+
f"Cleanup Readiness: [bold]{analysis.evidence_bundle.get('CleanupReadiness', 'UNKNOWN')}[/bold]\n"
|
1021
|
+
f"CIS Benchmark: [bold]{analysis.evidence_bundle.get('ComplianceStatus', {}).get('CISBenchmark', 'UNKNOWN')}[/bold]",
|
1022
|
+
title="📋 Evidence Bundle",
|
1023
|
+
style="bold green"
|
1024
|
+
)
|
1025
|
+
self.console.print(bundle_info)
|
1026
|
+
|
1027
|
+
def _write_json_evidence(self, data: Dict, file_path: Path):
|
1028
|
+
"""Write JSON evidence file"""
|
1029
|
+
with open(file_path, 'w') as f:
|
1030
|
+
json.dump(data, f, indent=2, default=str)
|
1031
|
+
|
1032
|
+
def _write_executive_summary(self, analysis: AWSOAnalysis, file_path: Path):
|
1033
|
+
"""Write executive summary in Markdown format"""
|
1034
|
+
summary = f"""# AWSO-05 VPC Cleanup Analysis - Executive Summary
|
1035
|
+
|
1036
|
+
## Overview
|
1037
|
+
This analysis was conducted to support AWSO-05 VPC cleanup operations with comprehensive dependency validation and security compliance assessment.
|
1038
|
+
|
1039
|
+
**Generated**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
1040
|
+
**Profile**: {self.profile}
|
1041
|
+
**Region**: {self.region}
|
1042
|
+
|
1043
|
+
## Key Findings
|
1044
|
+
|
1045
|
+
### Default VPC Analysis
|
1046
|
+
- **Default VPCs Found**: {len(analysis.default_vpcs)}
|
1047
|
+
- **CIS Benchmark Compliance**: {"❌ Non-Compliant" if analysis.default_vpcs else "✅ Compliant"}
|
1048
|
+
|
1049
|
+
### ENI Gate Validation (Critical)
|
1050
|
+
- **ENI Warnings**: {len(analysis.eni_gate_warnings)}
|
1051
|
+
- **Workload Impact Risk**: {"🔴 HIGH" if analysis.eni_gate_warnings else "🟢 LOW"}
|
1052
|
+
|
1053
|
+
### Cleanup Readiness
|
1054
|
+
**Status**: {analysis.evidence_bundle.get('CleanupReadiness', 'UNKNOWN')}
|
1055
|
+
|
1056
|
+
## Recommendations
|
1057
|
+
|
1058
|
+
"""
|
1059
|
+
for rec in analysis.cleanup_recommendations:
|
1060
|
+
priority_emoji = "🔴" if rec['Priority'] == 'CRITICAL' else "🟡" if rec['Priority'] == 'HIGH' else "🟢"
|
1061
|
+
summary += f"### {priority_emoji} {rec['Priority']} Priority\n"
|
1062
|
+
summary += f"**Action**: {rec['Action']} \n"
|
1063
|
+
summary += f"**Resource**: {rec['ResourceId']} \n"
|
1064
|
+
summary += f"**Reason**: {rec['Reason']} \n"
|
1065
|
+
summary += f"**Risk Level**: {rec['RiskLevel']} \n\n"
|
1066
|
+
|
1067
|
+
summary += """
|
1068
|
+
## Security Impact
|
1069
|
+
- **Attack Surface Reduction**: Default VPC elimination improves security posture
|
1070
|
+
- **CIS Benchmark Alignment**: Cleanup activities support compliance requirements
|
1071
|
+
- **Workload Protection**: ENI gate validation prevents accidental disruption
|
1072
|
+
|
1073
|
+
## Next Steps
|
1074
|
+
1. Review ENI gate warnings for workload migration planning
|
1075
|
+
2. Execute default VPC cleanup following 12-step AWSO-05 framework
|
1076
|
+
3. Monitor security posture improvements post-cleanup
|
1077
|
+
|
1078
|
+
---
|
1079
|
+
*Generated by CloudOps-Runbooks AWSO-05 VPC Analyzer*
|
1080
|
+
"""
|
1081
|
+
|
1082
|
+
with open(file_path, 'w') as f:
|
1083
|
+
f.write(summary)
|
1084
|
+
|
1085
|
+
def _create_evidence_manifest(self, evidence_files: Dict[str, str]) -> Dict[str, Any]:
|
1086
|
+
"""Create evidence manifest with SHA256 checksums"""
|
1087
|
+
import hashlib
|
1088
|
+
|
1089
|
+
manifest = {
|
1090
|
+
'ManifestVersion': '1.0',
|
1091
|
+
'GeneratedAt': datetime.now().isoformat(),
|
1092
|
+
'EvidenceFiles': list(evidence_files.keys()),
|
1093
|
+
'FileCount': len(evidence_files),
|
1094
|
+
'FileChecksums': {}
|
1095
|
+
}
|
1096
|
+
|
1097
|
+
# Generate SHA256 checksums
|
1098
|
+
for evidence_type, file_path in evidence_files.items():
|
1099
|
+
try:
|
1100
|
+
with open(file_path, 'rb') as f:
|
1101
|
+
file_hash = hashlib.sha256(f.read()).hexdigest()
|
1102
|
+
manifest['FileChecksums'][evidence_type] = file_hash
|
1103
|
+
except Exception as e:
|
1104
|
+
logger.error(f"Failed to generate checksum for {file_path}: {e}")
|
1105
|
+
manifest['FileChecksums'][evidence_type] = 'ERROR'
|
1106
|
+
|
1107
|
+
return manifest
|