runbooks 1.1.4__py3-none-any.whl → 1.1.5__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 +31 -2
- runbooks/__init___optimized.py +18 -4
- runbooks/_platform/__init__.py +1 -5
- runbooks/_platform/core/runbooks_wrapper.py +141 -138
- runbooks/aws2/accuracy_validator.py +812 -0
- runbooks/base.py +7 -0
- runbooks/cfat/assessment/compliance.py +1 -1
- runbooks/cfat/assessment/runner.py +1 -0
- runbooks/cfat/cloud_foundations_assessment.py +227 -239
- runbooks/cli/__init__.py +1 -1
- runbooks/cli/commands/cfat.py +64 -23
- runbooks/cli/commands/finops.py +1005 -54
- runbooks/cli/commands/inventory.py +138 -35
- runbooks/cli/commands/operate.py +9 -36
- runbooks/cli/commands/security.py +42 -18
- runbooks/cli/commands/validation.py +432 -18
- runbooks/cli/commands/vpc.py +81 -17
- runbooks/cli/registry.py +22 -10
- runbooks/cloudops/__init__.py +20 -27
- runbooks/cloudops/base.py +96 -107
- runbooks/cloudops/cost_optimizer.py +544 -542
- runbooks/cloudops/infrastructure_optimizer.py +5 -4
- runbooks/cloudops/interfaces.py +224 -225
- runbooks/cloudops/lifecycle_manager.py +5 -4
- runbooks/cloudops/mcp_cost_validation.py +252 -235
- runbooks/cloudops/models.py +78 -53
- runbooks/cloudops/monitoring_automation.py +5 -4
- runbooks/cloudops/notebook_framework.py +177 -213
- runbooks/cloudops/security_enforcer.py +125 -159
- runbooks/common/accuracy_validator.py +11 -0
- runbooks/common/aws_pricing.py +349 -326
- runbooks/common/aws_pricing_api.py +211 -212
- runbooks/common/aws_profile_manager.py +40 -36
- runbooks/common/aws_utils.py +74 -79
- runbooks/common/business_logic.py +126 -104
- runbooks/common/cli_decorators.py +36 -60
- runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
- runbooks/common/cross_account_manager.py +197 -204
- runbooks/common/date_utils.py +27 -39
- runbooks/common/decorators.py +29 -19
- runbooks/common/dry_run_examples.py +173 -208
- runbooks/common/dry_run_framework.py +157 -155
- runbooks/common/enhanced_exception_handler.py +15 -4
- runbooks/common/enhanced_logging_example.py +50 -64
- runbooks/common/enhanced_logging_integration_example.py +65 -37
- runbooks/common/env_utils.py +16 -16
- runbooks/common/error_handling.py +40 -38
- runbooks/common/lazy_loader.py +41 -23
- runbooks/common/logging_integration_helper.py +79 -86
- runbooks/common/mcp_cost_explorer_integration.py +476 -493
- runbooks/common/mcp_integration.py +63 -74
- runbooks/common/memory_optimization.py +140 -118
- runbooks/common/module_cli_base.py +37 -58
- runbooks/common/organizations_client.py +175 -193
- runbooks/common/patterns.py +23 -25
- runbooks/common/performance_monitoring.py +67 -71
- runbooks/common/performance_optimization_engine.py +283 -274
- runbooks/common/profile_utils.py +111 -37
- runbooks/common/rich_utils.py +201 -141
- runbooks/common/sre_performance_suite.py +177 -186
- runbooks/enterprise/__init__.py +1 -1
- runbooks/enterprise/logging.py +144 -106
- runbooks/enterprise/security.py +187 -204
- runbooks/enterprise/validation.py +43 -56
- runbooks/finops/__init__.py +26 -30
- runbooks/finops/account_resolver.py +1 -1
- runbooks/finops/advanced_optimization_engine.py +980 -0
- runbooks/finops/automation_core.py +268 -231
- runbooks/finops/business_case_config.py +184 -179
- runbooks/finops/cli.py +660 -139
- runbooks/finops/commvault_ec2_analysis.py +157 -164
- runbooks/finops/compute_cost_optimizer.py +336 -320
- runbooks/finops/config.py +20 -20
- runbooks/finops/cost_optimizer.py +484 -618
- runbooks/finops/cost_processor.py +332 -214
- runbooks/finops/dashboard_runner.py +1006 -172
- runbooks/finops/ebs_cost_optimizer.py +991 -657
- runbooks/finops/elastic_ip_optimizer.py +317 -257
- runbooks/finops/enhanced_mcp_integration.py +340 -0
- runbooks/finops/enhanced_progress.py +32 -29
- runbooks/finops/enhanced_trend_visualization.py +3 -2
- runbooks/finops/enterprise_wrappers.py +223 -285
- runbooks/finops/executive_export.py +203 -160
- runbooks/finops/helpers.py +130 -288
- runbooks/finops/iam_guidance.py +1 -1
- runbooks/finops/infrastructure/__init__.py +80 -0
- runbooks/finops/infrastructure/commands.py +506 -0
- runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
- runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
- runbooks/finops/markdown_exporter.py +337 -174
- runbooks/finops/mcp_validator.py +1952 -0
- runbooks/finops/nat_gateway_optimizer.py +1512 -481
- runbooks/finops/network_cost_optimizer.py +657 -587
- runbooks/finops/notebook_utils.py +226 -188
- runbooks/finops/optimization_engine.py +1136 -0
- runbooks/finops/optimizer.py +19 -23
- runbooks/finops/rds_snapshot_optimizer.py +367 -411
- runbooks/finops/reservation_optimizer.py +427 -363
- runbooks/finops/scenario_cli_integration.py +64 -65
- runbooks/finops/scenarios.py +1277 -438
- runbooks/finops/schemas.py +218 -182
- runbooks/finops/snapshot_manager.py +2289 -0
- runbooks/finops/types.py +3 -3
- runbooks/finops/validation_framework.py +259 -265
- runbooks/finops/vpc_cleanup_exporter.py +189 -144
- runbooks/finops/vpc_cleanup_optimizer.py +591 -573
- runbooks/finops/workspaces_analyzer.py +171 -182
- runbooks/integration/__init__.py +89 -0
- runbooks/integration/mcp_integration.py +1920 -0
- runbooks/inventory/CLAUDE.md +816 -0
- runbooks/inventory/__init__.py +2 -2
- runbooks/inventory/cloud_foundations_integration.py +144 -149
- runbooks/inventory/collectors/aws_comprehensive.py +1 -1
- runbooks/inventory/collectors/aws_networking.py +109 -99
- runbooks/inventory/collectors/base.py +4 -0
- runbooks/inventory/core/collector.py +495 -313
- runbooks/inventory/drift_detection_cli.py +69 -96
- runbooks/inventory/inventory_mcp_cli.py +48 -46
- runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
- runbooks/inventory/mcp_inventory_validator.py +549 -465
- runbooks/inventory/mcp_vpc_validator.py +359 -442
- runbooks/inventory/organizations_discovery.py +55 -51
- runbooks/inventory/rich_inventory_display.py +33 -32
- runbooks/inventory/unified_validation_engine.py +278 -251
- runbooks/inventory/vpc_analyzer.py +732 -695
- runbooks/inventory/vpc_architecture_validator.py +293 -348
- runbooks/inventory/vpc_dependency_analyzer.py +382 -378
- runbooks/inventory/vpc_flow_analyzer.py +1 -1
- runbooks/main.py +49 -34
- runbooks/main_final.py +91 -60
- runbooks/main_minimal.py +22 -10
- runbooks/main_optimized.py +131 -100
- runbooks/main_ultra_minimal.py +7 -2
- runbooks/mcp/__init__.py +36 -0
- runbooks/mcp/integration.py +679 -0
- runbooks/monitoring/performance_monitor.py +9 -4
- runbooks/operate/dynamodb_operations.py +3 -1
- runbooks/operate/ec2_operations.py +145 -137
- runbooks/operate/iam_operations.py +146 -152
- runbooks/operate/networking_cost_heatmap.py +29 -8
- runbooks/operate/rds_operations.py +223 -254
- runbooks/operate/s3_operations.py +107 -118
- runbooks/operate/vpc_operations.py +646 -616
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/commons.py +10 -7
- runbooks/remediation/commvault_ec2_analysis.py +70 -66
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
- runbooks/remediation/multi_account.py +24 -21
- runbooks/remediation/rds_snapshot_list.py +86 -60
- runbooks/remediation/remediation_cli.py +92 -146
- runbooks/remediation/universal_account_discovery.py +83 -79
- runbooks/remediation/workspaces_list.py +46 -41
- runbooks/security/__init__.py +19 -0
- runbooks/security/assessment_runner.py +1150 -0
- runbooks/security/baseline_checker.py +812 -0
- runbooks/security/cloudops_automation_security_validator.py +509 -535
- runbooks/security/compliance_automation_engine.py +17 -17
- runbooks/security/config/__init__.py +2 -2
- runbooks/security/config/compliance_config.py +50 -50
- runbooks/security/config_template_generator.py +63 -76
- runbooks/security/enterprise_security_framework.py +1 -1
- runbooks/security/executive_security_dashboard.py +519 -508
- runbooks/security/multi_account_security_controls.py +959 -1210
- runbooks/security/real_time_security_monitor.py +422 -444
- runbooks/security/security_baseline_tester.py +1 -1
- runbooks/security/security_cli.py +143 -112
- runbooks/security/test_2way_validation.py +439 -0
- runbooks/security/two_way_validation_framework.py +852 -0
- runbooks/sre/production_monitoring_framework.py +167 -177
- runbooks/tdd/__init__.py +15 -0
- runbooks/tdd/cli.py +1071 -0
- runbooks/utils/__init__.py +14 -17
- runbooks/utils/logger.py +7 -2
- runbooks/utils/version_validator.py +50 -47
- runbooks/validation/__init__.py +6 -6
- runbooks/validation/cli.py +9 -3
- runbooks/validation/comprehensive_2way_validator.py +745 -704
- runbooks/validation/mcp_validator.py +906 -228
- runbooks/validation/terraform_citations_validator.py +104 -115
- runbooks/validation/terraform_drift_detector.py +447 -451
- runbooks/vpc/README.md +617 -0
- runbooks/vpc/__init__.py +8 -1
- runbooks/vpc/analyzer.py +577 -0
- runbooks/vpc/cleanup_wrapper.py +476 -413
- runbooks/vpc/cli_cloudtrail_commands.py +339 -0
- runbooks/vpc/cli_mcp_validation_commands.py +480 -0
- runbooks/vpc/cloudtrail_audit_integration.py +717 -0
- runbooks/vpc/config.py +92 -97
- runbooks/vpc/cost_engine.py +411 -148
- runbooks/vpc/cost_explorer_integration.py +553 -0
- runbooks/vpc/cross_account_session.py +101 -106
- runbooks/vpc/enhanced_mcp_validation.py +917 -0
- runbooks/vpc/eni_gate_validator.py +961 -0
- runbooks/vpc/heatmap_engine.py +185 -160
- runbooks/vpc/mcp_no_eni_validator.py +680 -639
- runbooks/vpc/nat_gateway_optimizer.py +358 -0
- runbooks/vpc/networking_wrapper.py +15 -8
- runbooks/vpc/pdca_remediation_planner.py +528 -0
- runbooks/vpc/performance_optimized_analyzer.py +219 -231
- runbooks/vpc/runbooks_adapter.py +1167 -241
- runbooks/vpc/tdd_red_phase_stubs.py +601 -0
- runbooks/vpc/test_data_loader.py +358 -0
- runbooks/vpc/tests/conftest.py +314 -4
- runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
- runbooks/vpc/tests/test_cost_engine.py +0 -2
- runbooks/vpc/topology_generator.py +326 -0
- runbooks/vpc/unified_scenarios.py +1297 -1124
- runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
- runbooks-1.1.5.dist-info/METADATA +328 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/RECORD +214 -193
- runbooks/finops/README.md +0 -414
- runbooks/finops/accuracy_cross_validator.py +0 -647
- runbooks/finops/business_cases.py +0 -950
- runbooks/finops/dashboard_router.py +0 -922
- runbooks/finops/ebs_optimizer.py +0 -973
- runbooks/finops/embedded_mcp_validator.py +0 -1629
- runbooks/finops/enhanced_dashboard_runner.py +0 -527
- runbooks/finops/finops_dashboard.py +0 -584
- runbooks/finops/finops_scenarios.py +0 -1218
- runbooks/finops/legacy_migration.py +0 -730
- runbooks/finops/multi_dashboard.py +0 -1519
- runbooks/finops/single_dashboard.py +0 -1113
- runbooks/finops/unlimited_scenarios.py +0 -393
- runbooks-1.1.4.dist-info/METADATA +0 -800
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
- {runbooks-1.1.4.dist-info → runbooks-1.1.5.dist-info}/top_level.txt +0 -0
@@ -29,8 +29,15 @@ import boto3
|
|
29
29
|
from botocore.exceptions import ClientError, NoCredentialsError
|
30
30
|
|
31
31
|
from ..common.rich_utils import (
|
32
|
-
console,
|
33
|
-
|
32
|
+
console,
|
33
|
+
print_header,
|
34
|
+
print_success,
|
35
|
+
print_error,
|
36
|
+
print_warning,
|
37
|
+
print_info,
|
38
|
+
create_table,
|
39
|
+
create_progress_bar,
|
40
|
+
format_cost,
|
34
41
|
)
|
35
42
|
from ..common.profile_utils import get_profile_for_operation
|
36
43
|
|
@@ -58,8 +65,13 @@ class RDSSnapshotConfigAggregator:
|
|
58
65
|
|
59
66
|
# Config aggregator regions for comprehensive coverage
|
60
67
|
self.config_regions = [
|
61
|
-
|
62
|
-
|
68
|
+
"us-east-1",
|
69
|
+
"us-west-2",
|
70
|
+
"eu-west-1",
|
71
|
+
"ap-southeast-2",
|
72
|
+
"ap-northeast-1",
|
73
|
+
"ca-central-1",
|
74
|
+
"eu-central-1",
|
63
75
|
]
|
64
76
|
|
65
77
|
# Session management
|
@@ -69,12 +81,12 @@ class RDSSnapshotConfigAggregator:
|
|
69
81
|
|
70
82
|
# Performance metrics
|
71
83
|
self.metrics = {
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
84
|
+
"aggregators_found": 0,
|
85
|
+
"snapshots_discovered": 0,
|
86
|
+
"accounts_covered": 0,
|
87
|
+
"regions_scanned": 0,
|
88
|
+
"api_calls_made": 0,
|
89
|
+
"errors_encountered": 0,
|
78
90
|
}
|
79
91
|
|
80
92
|
def initialize_session(self) -> bool:
|
@@ -84,7 +96,7 @@ class RDSSnapshotConfigAggregator:
|
|
84
96
|
self.session = boto3.Session(profile_name=profile)
|
85
97
|
|
86
98
|
# Verify access with STS
|
87
|
-
sts_client = self.session.client(
|
99
|
+
sts_client = self.session.client("sts")
|
88
100
|
identity = sts_client.get_caller_identity()
|
89
101
|
|
90
102
|
print_success(f"✅ Initialized session with profile: {profile}")
|
@@ -108,15 +120,11 @@ class RDSSnapshotConfigAggregator:
|
|
108
120
|
aggregator_map = {}
|
109
121
|
|
110
122
|
with create_progress_bar() as progress:
|
111
|
-
task_id = progress.add_task(
|
112
|
-
"Scanning regions for Config aggregators...",
|
113
|
-
total=len(self.config_regions)
|
114
|
-
)
|
123
|
+
task_id = progress.add_task("Scanning regions for Config aggregators...", total=len(self.config_regions))
|
115
124
|
|
116
125
|
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
117
126
|
future_to_region = {
|
118
|
-
executor.submit(self._find_aggregators_in_region, region): region
|
119
|
-
for region in self.config_regions
|
127
|
+
executor.submit(self._find_aggregators_in_region, region): region for region in self.config_regions
|
120
128
|
}
|
121
129
|
|
122
130
|
for future in as_completed(future_to_region):
|
@@ -125,16 +133,16 @@ class RDSSnapshotConfigAggregator:
|
|
125
133
|
aggregators = future.result()
|
126
134
|
if aggregators:
|
127
135
|
aggregator_map[region] = aggregators
|
128
|
-
self.metrics[
|
136
|
+
self.metrics["aggregators_found"] += len(aggregators)
|
129
137
|
console.print(f"[green]✓[/green] {region}: Found {len(aggregators)} aggregator(s)")
|
130
138
|
else:
|
131
139
|
console.print(f"[dim]○[/dim] {region}: No aggregators found")
|
132
140
|
|
133
|
-
self.metrics[
|
141
|
+
self.metrics["regions_scanned"] += 1
|
134
142
|
|
135
143
|
except Exception as e:
|
136
144
|
print_warning(f"❌ {region}: {str(e)}")
|
137
|
-
self.metrics[
|
145
|
+
self.metrics["errors_encountered"] += 1
|
138
146
|
|
139
147
|
progress.advance(task_id)
|
140
148
|
|
@@ -152,28 +160,28 @@ class RDSSnapshotConfigAggregator:
|
|
152
160
|
def _find_aggregators_in_region(self, region: str) -> List[str]:
|
153
161
|
"""Find Config aggregators in a specific region"""
|
154
162
|
try:
|
155
|
-
config_client = self.session.client(
|
163
|
+
config_client = self.session.client("config", region_name=region)
|
156
164
|
|
157
165
|
# List configuration aggregators
|
158
166
|
response = config_client.describe_configuration_aggregators()
|
159
|
-
self.metrics[
|
167
|
+
self.metrics["api_calls_made"] += 1
|
160
168
|
|
161
169
|
aggregators = []
|
162
|
-
for aggregator in response.get(
|
163
|
-
aggregator_name = aggregator[
|
170
|
+
for aggregator in response.get("ConfigurationAggregators", []):
|
171
|
+
aggregator_name = aggregator["ConfigurationAggregatorName"]
|
164
172
|
aggregators.append(aggregator_name)
|
165
173
|
|
166
174
|
# Log aggregator details for debugging
|
167
175
|
logger.debug(f"Found aggregator {aggregator_name} in {region}")
|
168
|
-
if
|
169
|
-
org_source = aggregator[
|
176
|
+
if "OrganizationAggregationSource" in aggregator:
|
177
|
+
org_source = aggregator["OrganizationAggregationSource"]
|
170
178
|
logger.debug(f"Organization aggregator: AllAwsRegions={org_source.get('AllAwsRegions', False)}")
|
171
179
|
|
172
180
|
return aggregators
|
173
181
|
|
174
182
|
except ClientError as e:
|
175
|
-
error_code = e.response[
|
176
|
-
if error_code in [
|
183
|
+
error_code = e.response["Error"]["Code"]
|
184
|
+
if error_code in ["AccessDenied", "UnauthorizedOperation"]:
|
177
185
|
logger.debug(f"No Config access in {region}: {error_code}")
|
178
186
|
else:
|
179
187
|
logger.warning(f"Config API error in {region}: {e}")
|
@@ -207,23 +215,18 @@ class RDSSnapshotConfigAggregator:
|
|
207
215
|
# Process each region's aggregators
|
208
216
|
with create_progress_bar() as progress:
|
209
217
|
total_aggregators = sum(len(aggs) for aggs in self.discovered_aggregators.values())
|
210
|
-
task_id = progress.add_task(
|
211
|
-
"Discovering RDS snapshots via Config aggregators...",
|
212
|
-
total=total_aggregators
|
213
|
-
)
|
218
|
+
task_id = progress.add_task("Discovering RDS snapshots via Config aggregators...", total=total_aggregators)
|
214
219
|
|
215
220
|
for region, aggregators in self.discovered_aggregators.items():
|
216
221
|
console.print(f"\n[cyan]🔍 Processing Config aggregators in {region}[/cyan]")
|
217
222
|
|
218
223
|
for aggregator_name in aggregators:
|
219
224
|
try:
|
220
|
-
snapshots = self._query_snapshots_from_aggregator(
|
221
|
-
region, aggregator_name, target_account_ids
|
222
|
-
)
|
225
|
+
snapshots = self._query_snapshots_from_aggregator(region, aggregator_name, target_account_ids)
|
223
226
|
|
224
227
|
if snapshots:
|
225
228
|
all_snapshots.extend(snapshots)
|
226
|
-
unique_accounts = set(s.get(
|
229
|
+
unique_accounts = set(s.get("AccountId", "unknown") for s in snapshots)
|
227
230
|
console.print(
|
228
231
|
f"[green]✓[/green] {aggregator_name}: "
|
229
232
|
f"Found {len(snapshots)} snapshots across {len(unique_accounts)} accounts"
|
@@ -233,20 +236,19 @@ class RDSSnapshotConfigAggregator:
|
|
233
236
|
|
234
237
|
except Exception as e:
|
235
238
|
print_warning(f"❌ {aggregator_name}: {str(e)}")
|
236
|
-
self.metrics[
|
239
|
+
self.metrics["errors_encountered"] += 1
|
237
240
|
|
238
241
|
progress.advance(task_id)
|
239
242
|
|
240
243
|
# Update metrics
|
241
|
-
self.metrics[
|
242
|
-
unique_accounts = set(s.get(
|
243
|
-
self.metrics[
|
244
|
+
self.metrics["snapshots_discovered"] = len(all_snapshots)
|
245
|
+
unique_accounts = set(s.get("AccountId", "unknown") for s in all_snapshots)
|
246
|
+
self.metrics["accounts_covered"] = len(unique_accounts)
|
244
247
|
|
245
248
|
# Summary
|
246
249
|
if all_snapshots:
|
247
250
|
print_success(
|
248
|
-
f"✅ Discovery complete: {len(all_snapshots)} RDS snapshots "
|
249
|
-
f"across {len(unique_accounts)} accounts"
|
251
|
+
f"✅ Discovery complete: {len(all_snapshots)} RDS snapshots across {len(unique_accounts)} accounts"
|
250
252
|
)
|
251
253
|
else:
|
252
254
|
print_warning("⚠️ No RDS snapshots discovered via Config aggregators")
|
@@ -255,14 +257,11 @@ class RDSSnapshotConfigAggregator:
|
|
255
257
|
return all_snapshots
|
256
258
|
|
257
259
|
def _query_snapshots_from_aggregator(
|
258
|
-
self,
|
259
|
-
region: str,
|
260
|
-
aggregator_name: str,
|
261
|
-
target_account_ids: Optional[List[str]] = None
|
260
|
+
self, region: str, aggregator_name: str, target_account_ids: Optional[List[str]] = None
|
262
261
|
) -> List[Dict]:
|
263
262
|
"""Query RDS snapshots from a specific Config aggregator"""
|
264
263
|
try:
|
265
|
-
config_client = self.session.client(
|
264
|
+
config_client = self.session.client("config", region_name=region)
|
266
265
|
|
267
266
|
# Base query for RDS DB snapshots
|
268
267
|
query_expression = """
|
@@ -290,19 +289,19 @@ class RDSSnapshotConfigAggregator:
|
|
290
289
|
|
291
290
|
while True:
|
292
291
|
query_params = {
|
293
|
-
|
294
|
-
|
295
|
-
|
292
|
+
"ConfigurationAggregatorName": aggregator_name,
|
293
|
+
"Expression": query_expression,
|
294
|
+
"Limit": 100, # Maximum allowed by Config API
|
296
295
|
}
|
297
296
|
|
298
297
|
if next_token:
|
299
|
-
query_params[
|
298
|
+
query_params["NextToken"] = next_token
|
300
299
|
|
301
300
|
response = config_client.select_aggregate_resource_config(**query_params)
|
302
|
-
self.metrics[
|
301
|
+
self.metrics["api_calls_made"] += 1
|
303
302
|
|
304
303
|
# Process results
|
305
|
-
for result in response.get(
|
304
|
+
for result in response.get("Results", []):
|
306
305
|
try:
|
307
306
|
snapshot_data = json.loads(result)
|
308
307
|
processed_snapshot = self._process_config_snapshot_result(snapshot_data)
|
@@ -314,17 +313,17 @@ class RDSSnapshotConfigAggregator:
|
|
314
313
|
logger.warning(f"Failed to process snapshot data: {e}")
|
315
314
|
|
316
315
|
# Check for more results
|
317
|
-
next_token = response.get(
|
316
|
+
next_token = response.get("NextToken")
|
318
317
|
if not next_token:
|
319
318
|
break
|
320
319
|
|
321
320
|
return snapshots
|
322
321
|
|
323
322
|
except ClientError as e:
|
324
|
-
error_code = e.response[
|
325
|
-
if error_code ==
|
323
|
+
error_code = e.response["Error"]["Code"]
|
324
|
+
if error_code == "NoSuchConfigurationAggregatorException":
|
326
325
|
logger.warning(f"Aggregator {aggregator_name} not found in {region}")
|
327
|
-
elif error_code in [
|
326
|
+
elif error_code in ["AccessDenied", "UnauthorizedOperation"]:
|
328
327
|
logger.warning(f"Access denied to aggregator {aggregator_name} in {region}")
|
329
328
|
else:
|
330
329
|
logger.error(f"Config aggregator query failed: {e}")
|
@@ -338,16 +337,16 @@ class RDSSnapshotConfigAggregator:
|
|
338
337
|
try:
|
339
338
|
# Extract base metadata
|
340
339
|
snapshot_info = {
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
340
|
+
"DBSnapshotIdentifier": config_data.get("resourceId", "unknown"),
|
341
|
+
"AccountId": config_data.get("accountId", "unknown"),
|
342
|
+
"Region": config_data.get("awsRegion", "unknown"),
|
343
|
+
"DiscoveryMethod": "config_aggregator",
|
344
|
+
"ConfigCaptureTime": config_data.get("configurationItemCaptureTime"),
|
345
|
+
"ResourceCreationTime": config_data.get("resourceCreationTime"),
|
347
346
|
}
|
348
347
|
|
349
348
|
# Parse configuration details if available
|
350
|
-
configuration = config_data.get(
|
349
|
+
configuration = config_data.get("configuration", {})
|
351
350
|
if isinstance(configuration, str):
|
352
351
|
try:
|
353
352
|
configuration = json.loads(configuration)
|
@@ -356,55 +355,57 @@ class RDSSnapshotConfigAggregator:
|
|
356
355
|
|
357
356
|
# Extract RDS-specific details
|
358
357
|
if configuration:
|
359
|
-
snapshot_info.update(
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
358
|
+
snapshot_info.update(
|
359
|
+
{
|
360
|
+
"DBInstanceIdentifier": configuration.get("dBInstanceIdentifier", "unknown"),
|
361
|
+
"SnapshotType": configuration.get("snapshotType", "unknown"),
|
362
|
+
"Status": configuration.get("status", "unknown"),
|
363
|
+
"Engine": configuration.get("engine", "unknown"),
|
364
|
+
"EngineVersion": configuration.get("engineVersion", "unknown"),
|
365
|
+
"AllocatedStorage": configuration.get("allocatedStorage", 0),
|
366
|
+
"StorageType": configuration.get("storageType", "unknown"),
|
367
|
+
"Encrypted": configuration.get("encrypted", False),
|
368
|
+
"SnapshotCreateTime": configuration.get("snapshotCreateTime"),
|
369
|
+
"InstanceCreateTime": configuration.get("instanceCreateTime"),
|
370
|
+
"MasterUsername": configuration.get("masterUsername", "unknown"),
|
371
|
+
"Port": configuration.get("port", 0),
|
372
|
+
"VpcId": configuration.get("vpcId"),
|
373
|
+
"AvailabilityZone": configuration.get("availabilityZone"),
|
374
|
+
"LicenseModel": configuration.get("licenseModel", "unknown"),
|
375
|
+
"OptionGroupName": configuration.get("optionGroupName"),
|
376
|
+
"PercentProgress": configuration.get("percentProgress", 0),
|
377
|
+
"SourceRegion": configuration.get("sourceRegion"),
|
378
|
+
"SourceDBSnapshotIdentifier": configuration.get("sourceDBSnapshotIdentifier"),
|
379
|
+
"StorageEncrypted": configuration.get("storageEncrypted", False),
|
380
|
+
"KmsKeyId": configuration.get("kmsKeyId"),
|
381
|
+
"Timezone": configuration.get("timezone"),
|
382
|
+
"IAMDatabaseAuthenticationEnabled": configuration.get(
|
383
|
+
"iAMDatabaseAuthenticationEnabled", False
|
384
|
+
),
|
385
|
+
"ProcessorFeatures": configuration.get("processorFeatures", []),
|
386
|
+
"DbiResourceId": configuration.get("dbiResourceId"),
|
387
|
+
"TagList": configuration.get("tagList", []),
|
388
|
+
}
|
389
|
+
)
|
387
390
|
|
388
391
|
# Calculate age and estimated cost
|
389
|
-
if snapshot_info.get(
|
392
|
+
if snapshot_info.get("SnapshotCreateTime"):
|
390
393
|
try:
|
391
|
-
create_time = datetime.fromisoformat(
|
392
|
-
snapshot_info['SnapshotCreateTime'].replace('Z', '+00:00')
|
393
|
-
)
|
394
|
+
create_time = datetime.fromisoformat(snapshot_info["SnapshotCreateTime"].replace("Z", "+00:00"))
|
394
395
|
age_days = (datetime.now(timezone.utc) - create_time).days
|
395
|
-
snapshot_info[
|
396
|
+
snapshot_info["AgeDays"] = age_days
|
396
397
|
|
397
398
|
# Estimate storage cost (basic calculation)
|
398
|
-
allocated_storage = snapshot_info.get(
|
399
|
+
allocated_storage = snapshot_info.get("AllocatedStorage", 0)
|
399
400
|
if allocated_storage > 0:
|
400
401
|
# Basic cost estimation - $0.095 per GB-month for snapshot storage
|
401
402
|
monthly_cost = allocated_storage * 0.095
|
402
|
-
snapshot_info[
|
403
|
-
snapshot_info[
|
403
|
+
snapshot_info["EstimatedMonthlyCost"] = round(monthly_cost, 2)
|
404
|
+
snapshot_info["EstimatedAnnualCost"] = round(monthly_cost * 12, 2)
|
404
405
|
|
405
406
|
except Exception as e:
|
406
407
|
logger.debug(f"Failed to calculate snapshot age: {e}")
|
407
|
-
snapshot_info[
|
408
|
+
snapshot_info["AgeDays"] = 0
|
408
409
|
|
409
410
|
return snapshot_info
|
410
411
|
|
@@ -418,7 +419,7 @@ class RDSSnapshotConfigAggregator:
|
|
418
419
|
account_filter: Optional[List[str]] = None,
|
419
420
|
age_filter_days: Optional[int] = None,
|
420
421
|
snapshot_type_filter: Optional[str] = None,
|
421
|
-
engine_filter: Optional[str] = None
|
422
|
+
engine_filter: Optional[str] = None,
|
422
423
|
) -> List[Dict]:
|
423
424
|
"""
|
424
425
|
Apply filters to discovered snapshots
|
@@ -437,34 +438,24 @@ class RDSSnapshotConfigAggregator:
|
|
437
438
|
|
438
439
|
# Account filter
|
439
440
|
if account_filter:
|
440
|
-
filtered_snapshots = [
|
441
|
-
s for s in filtered_snapshots
|
442
|
-
if s.get('AccountId') in account_filter
|
443
|
-
]
|
441
|
+
filtered_snapshots = [s for s in filtered_snapshots if s.get("AccountId") in account_filter]
|
444
442
|
console.print(f"[dim]Account filter: {len(filtered_snapshots)} snapshots[/dim]")
|
445
443
|
|
446
444
|
# Age filter
|
447
445
|
if age_filter_days is not None:
|
448
|
-
filtered_snapshots = [
|
449
|
-
s for s in filtered_snapshots
|
450
|
-
if s.get('AgeDays', 0) >= age_filter_days
|
451
|
-
]
|
446
|
+
filtered_snapshots = [s for s in filtered_snapshots if s.get("AgeDays", 0) >= age_filter_days]
|
452
447
|
console.print(f"[dim]Age filter (>{age_filter_days}d): {len(filtered_snapshots)} snapshots[/dim]")
|
453
448
|
|
454
449
|
# Snapshot type filter
|
455
450
|
if snapshot_type_filter:
|
456
451
|
filtered_snapshots = [
|
457
|
-
s for s in filtered_snapshots
|
458
|
-
if s.get('SnapshotType', '').lower() == snapshot_type_filter.lower()
|
452
|
+
s for s in filtered_snapshots if s.get("SnapshotType", "").lower() == snapshot_type_filter.lower()
|
459
453
|
]
|
460
454
|
console.print(f"[dim]Type filter ({snapshot_type_filter}): {len(filtered_snapshots)} snapshots[/dim]")
|
461
455
|
|
462
456
|
# Engine filter
|
463
457
|
if engine_filter:
|
464
|
-
filtered_snapshots = [
|
465
|
-
s for s in filtered_snapshots
|
466
|
-
if engine_filter.lower() in s.get('Engine', '').lower()
|
467
|
-
]
|
458
|
+
filtered_snapshots = [s for s in filtered_snapshots if engine_filter.lower() in s.get("Engine", "").lower()]
|
468
459
|
console.print(f"[dim]Engine filter ({engine_filter}): {len(filtered_snapshots)} snapshots[/dim]")
|
469
460
|
|
470
461
|
return filtered_snapshots
|
@@ -472,67 +463,66 @@ class RDSSnapshotConfigAggregator:
|
|
472
463
|
def generate_summary_report(self, snapshots: List[Dict]) -> Dict:
|
473
464
|
"""Generate comprehensive summary report of discovered snapshots"""
|
474
465
|
if not snapshots:
|
475
|
-
return {
|
476
|
-
'total_snapshots': 0,
|
477
|
-
'summary': 'No snapshots discovered'
|
478
|
-
}
|
466
|
+
return {"total_snapshots": 0, "summary": "No snapshots discovered"}
|
479
467
|
|
480
468
|
# Basic statistics
|
481
469
|
total_snapshots = len(snapshots)
|
482
|
-
unique_accounts = set(s.get(
|
483
|
-
unique_regions = set(s.get(
|
484
|
-
unique_engines = set(s.get(
|
470
|
+
unique_accounts = set(s.get("AccountId", "unknown") for s in snapshots)
|
471
|
+
unique_regions = set(s.get("Region", "unknown") for s in snapshots)
|
472
|
+
unique_engines = set(s.get("Engine", "unknown") for s in snapshots)
|
485
473
|
|
486
474
|
# Snapshot type breakdown
|
487
|
-
manual_snapshots = [s for s in snapshots if s.get(
|
488
|
-
automated_snapshots = [s for s in snapshots if s.get(
|
475
|
+
manual_snapshots = [s for s in snapshots if s.get("SnapshotType", "").lower() == "manual"]
|
476
|
+
automated_snapshots = [s for s in snapshots if s.get("SnapshotType", "").lower() == "automated"]
|
489
477
|
|
490
478
|
# Age analysis
|
491
|
-
aged_snapshots = [s for s in snapshots if s.get(
|
492
|
-
very_old_snapshots = [s for s in snapshots if s.get(
|
479
|
+
aged_snapshots = [s for s in snapshots if s.get("AgeDays", 0) >= 90] # 3+ months
|
480
|
+
very_old_snapshots = [s for s in snapshots if s.get("AgeDays", 0) >= 180] # 6+ months
|
493
481
|
|
494
482
|
# Storage analysis
|
495
|
-
total_storage = sum(s.get(
|
496
|
-
total_estimated_cost = sum(s.get(
|
483
|
+
total_storage = sum(s.get("AllocatedStorage", 0) for s in snapshots)
|
484
|
+
total_estimated_cost = sum(s.get("EstimatedMonthlyCost", 0) for s in snapshots)
|
497
485
|
|
498
486
|
# Encryption analysis
|
499
|
-
encrypted_snapshots = [s for s in snapshots if s.get(
|
487
|
+
encrypted_snapshots = [s for s in snapshots if s.get("Encrypted", False)]
|
500
488
|
|
501
489
|
return {
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
490
|
+
"total_snapshots": total_snapshots,
|
491
|
+
"unique_accounts": len(unique_accounts),
|
492
|
+
"unique_regions": len(unique_regions),
|
493
|
+
"unique_engines": len(unique_engines),
|
494
|
+
"account_ids": sorted(list(unique_accounts)),
|
495
|
+
"regions": sorted(list(unique_regions)),
|
496
|
+
"engines": sorted(list(unique_engines)),
|
497
|
+
"snapshot_types": {
|
498
|
+
"manual": len(manual_snapshots),
|
499
|
+
"automated": len(automated_snapshots),
|
500
|
+
"unknown": total_snapshots - len(manual_snapshots) - len(automated_snapshots),
|
513
501
|
},
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
502
|
+
"age_analysis": {
|
503
|
+
"aged_snapshots_90d": len(aged_snapshots),
|
504
|
+
"very_old_snapshots_180d": len(very_old_snapshots),
|
505
|
+
"cleanup_candidates": len([s for s in manual_snapshots if s.get("AgeDays", 0) >= 90]),
|
518
506
|
},
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
507
|
+
"storage_analysis": {
|
508
|
+
"total_storage_gb": total_storage,
|
509
|
+
"estimated_monthly_cost": round(total_estimated_cost, 2),
|
510
|
+
"estimated_annual_cost": round(total_estimated_cost * 12, 2),
|
523
511
|
},
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
512
|
+
"security_analysis": {
|
513
|
+
"encrypted_snapshots": len(encrypted_snapshots),
|
514
|
+
"unencrypted_snapshots": total_snapshots - len(encrypted_snapshots),
|
515
|
+
"encryption_percentage": round((len(encrypted_snapshots) / total_snapshots) * 100, 1)
|
516
|
+
if total_snapshots > 0
|
517
|
+
else 0,
|
528
518
|
},
|
529
|
-
|
519
|
+
"discovery_metrics": self.metrics,
|
530
520
|
}
|
531
521
|
|
532
|
-
def export_results(self, snapshots: List[Dict], output_file: str, format: str =
|
522
|
+
def export_results(self, snapshots: List[Dict], output_file: str, format: str = "csv") -> bool:
|
533
523
|
"""Export snapshot results to file"""
|
534
524
|
try:
|
535
|
-
if format.lower() ==
|
525
|
+
if format.lower() == "csv":
|
536
526
|
import csv
|
537
527
|
|
538
528
|
if not snapshots:
|
@@ -546,22 +536,22 @@ class RDSSnapshotConfigAggregator:
|
|
546
536
|
|
547
537
|
fieldnames = sorted(list(all_keys))
|
548
538
|
|
549
|
-
with open(output_file,
|
539
|
+
with open(output_file, "w", newline="", encoding="utf-8") as csvfile:
|
550
540
|
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
551
541
|
writer.writeheader()
|
552
542
|
for snapshot in snapshots:
|
553
543
|
# Convert complex objects to strings for CSV
|
554
544
|
row = {}
|
555
545
|
for key in fieldnames:
|
556
|
-
value = snapshot.get(key,
|
546
|
+
value = snapshot.get(key, "")
|
557
547
|
if isinstance(value, (list, dict)):
|
558
548
|
row[key] = json.dumps(value)
|
559
549
|
else:
|
560
|
-
row[key] = str(value) if value is not None else
|
550
|
+
row[key] = str(value) if value is not None else ""
|
561
551
|
writer.writerow(row)
|
562
552
|
|
563
|
-
elif format.lower() ==
|
564
|
-
with open(output_file,
|
553
|
+
elif format.lower() == "json":
|
554
|
+
with open(output_file, "w", encoding="utf-8") as jsonfile:
|
565
555
|
json.dump(snapshots, jsonfile, indent=2, default=str)
|
566
556
|
|
567
557
|
else:
|
@@ -577,16 +567,16 @@ class RDSSnapshotConfigAggregator:
|
|
577
567
|
|
578
568
|
|
579
569
|
@click.command()
|
580
|
-
@click.option(
|
581
|
-
@click.option(
|
582
|
-
@click.option(
|
583
|
-
@click.option(
|
584
|
-
@click.option(
|
585
|
-
@click.option(
|
586
|
-
@click.option(
|
587
|
-
@click.option(
|
588
|
-
@click.option(
|
589
|
-
@click.option(
|
570
|
+
@click.option("--profile", help="AWS profile with Config aggregator access (overrides environment)")
|
571
|
+
@click.option("--target-accounts", multiple=True, help="Specific account IDs to analyze")
|
572
|
+
@click.option("--regions", multiple=True, help="Specific regions to check for aggregators")
|
573
|
+
@click.option("--age-filter", type=int, help="Filter snapshots older than X days")
|
574
|
+
@click.option("--snapshot-type", type=click.Choice(["manual", "automated"]), help="Filter by snapshot type")
|
575
|
+
@click.option("--engine-filter", help="Filter by database engine (partial match)")
|
576
|
+
@click.option("--output-file", default="./rds_snapshots_config_discovery.csv", help="Output file path")
|
577
|
+
@click.option("--format", type=click.Choice(["csv", "json"]), default="csv", help="Output format")
|
578
|
+
@click.option("--max-workers", type=int, default=10, help="Maximum concurrent workers")
|
579
|
+
@click.option("--summary-only", is_flag=True, help="Show only summary report")
|
590
580
|
def discover_rds_snapshots(
|
591
581
|
profile: str,
|
592
582
|
target_accounts: Tuple[str],
|
@@ -597,7 +587,7 @@ def discover_rds_snapshots(
|
|
597
587
|
output_file: str,
|
598
588
|
format: str,
|
599
589
|
max_workers: int,
|
600
|
-
summary_only: bool
|
590
|
+
summary_only: bool,
|
601
591
|
):
|
602
592
|
"""
|
603
593
|
Enhanced RDS Snapshot Discovery via AWS Config Organization Aggregator
|
@@ -622,10 +612,7 @@ def discover_rds_snapshots(
|
|
622
612
|
print_header("Enhanced RDS Snapshot Discovery via Config Aggregator", "v1.0")
|
623
613
|
|
624
614
|
# Initialize discovery engine
|
625
|
-
aggregator = RDSSnapshotConfigAggregator(
|
626
|
-
management_profile=profile,
|
627
|
-
max_workers=max_workers
|
628
|
-
)
|
615
|
+
aggregator = RDSSnapshotConfigAggregator(management_profile=profile, max_workers=max_workers)
|
629
616
|
|
630
617
|
# Override default regions if specified
|
631
618
|
if regions:
|
@@ -658,7 +645,7 @@ def discover_rds_snapshots(
|
|
658
645
|
account_filter=target_account_list,
|
659
646
|
age_filter_days=age_filter,
|
660
647
|
snapshot_type_filter=snapshot_type,
|
661
|
-
engine_filter=engine_filter
|
648
|
+
engine_filter=engine_filter,
|
662
649
|
)
|
663
650
|
|
664
651
|
# Generate summary report
|
@@ -669,49 +656,46 @@ def discover_rds_snapshots(
|
|
669
656
|
|
670
657
|
summary_table = create_table(
|
671
658
|
title="RDS Snapshot Discovery Results",
|
672
|
-
columns=[
|
673
|
-
{"header": "Metric", "style": "cyan"},
|
674
|
-
{"header": "Value", "style": "green bold"}
|
675
|
-
]
|
659
|
+
columns=[{"header": "Metric", "style": "cyan"}, {"header": "Value", "style": "green bold"}],
|
676
660
|
)
|
677
661
|
|
678
|
-
summary_table.add_row("Total Snapshots", str(summary[
|
679
|
-
summary_table.add_row("Unique Accounts", str(summary[
|
680
|
-
summary_table.add_row("Unique Regions", str(summary[
|
681
|
-
summary_table.add_row("Database Engines", str(summary[
|
682
|
-
summary_table.add_row("Manual Snapshots", str(summary[
|
683
|
-
summary_table.add_row("Automated Snapshots", str(summary[
|
684
|
-
summary_table.add_row("Old Snapshots (90d+)", str(summary[
|
685
|
-
summary_table.add_row("Cleanup Candidates", str(summary[
|
686
|
-
summary_table.add_row("Total Storage (GB)", str(summary[
|
687
|
-
summary_table.add_row("Est. Monthly Cost", format_cost(summary[
|
688
|
-
summary_table.add_row("Est. Annual Cost", format_cost(summary[
|
689
|
-
summary_table.add_row(
|
662
|
+
summary_table.add_row("Total Snapshots", str(summary["total_snapshots"]))
|
663
|
+
summary_table.add_row("Unique Accounts", str(summary["unique_accounts"]))
|
664
|
+
summary_table.add_row("Unique Regions", str(summary["unique_regions"]))
|
665
|
+
summary_table.add_row("Database Engines", str(summary["unique_engines"]))
|
666
|
+
summary_table.add_row("Manual Snapshots", str(summary["snapshot_types"]["manual"]))
|
667
|
+
summary_table.add_row("Automated Snapshots", str(summary["snapshot_types"]["automated"]))
|
668
|
+
summary_table.add_row("Old Snapshots (90d+)", str(summary["age_analysis"]["aged_snapshots_90d"]))
|
669
|
+
summary_table.add_row("Cleanup Candidates", str(summary["age_analysis"]["cleanup_candidates"]))
|
670
|
+
summary_table.add_row("Total Storage (GB)", str(summary["storage_analysis"]["total_storage_gb"]))
|
671
|
+
summary_table.add_row("Est. Monthly Cost", format_cost(summary["storage_analysis"]["estimated_monthly_cost"]))
|
672
|
+
summary_table.add_row("Est. Annual Cost", format_cost(summary["storage_analysis"]["estimated_annual_cost"]))
|
673
|
+
summary_table.add_row(
|
674
|
+
"Encrypted Snapshots",
|
675
|
+
f"{summary['security_analysis']['encrypted_snapshots']} ({summary['security_analysis']['encryption_percentage']}%)",
|
676
|
+
)
|
690
677
|
|
691
678
|
console.print(summary_table)
|
692
679
|
|
693
680
|
# Discovery metrics
|
694
681
|
metrics_table = create_table(
|
695
682
|
title="Discovery Performance Metrics",
|
696
|
-
columns=[
|
697
|
-
{"header": "Metric", "style": "blue"},
|
698
|
-
{"header": "Count", "style": "yellow"}
|
699
|
-
]
|
683
|
+
columns=[{"header": "Metric", "style": "blue"}, {"header": "Count", "style": "yellow"}],
|
700
684
|
)
|
701
685
|
|
702
|
-
metrics = summary[
|
703
|
-
metrics_table.add_row("Config Aggregators Found", str(metrics[
|
704
|
-
metrics_table.add_row("Regions Scanned", str(metrics[
|
705
|
-
metrics_table.add_row("API Calls Made", str(metrics[
|
706
|
-
metrics_table.add_row("Errors Encountered", str(metrics[
|
686
|
+
metrics = summary["discovery_metrics"]
|
687
|
+
metrics_table.add_row("Config Aggregators Found", str(metrics["aggregators_found"]))
|
688
|
+
metrics_table.add_row("Regions Scanned", str(metrics["regions_scanned"]))
|
689
|
+
metrics_table.add_row("API Calls Made", str(metrics["api_calls_made"]))
|
690
|
+
metrics_table.add_row("Errors Encountered", str(metrics["errors_encountered"]))
|
707
691
|
|
708
692
|
console.print(metrics_table)
|
709
693
|
|
710
694
|
# Account details
|
711
|
-
if summary[
|
695
|
+
if summary["account_ids"] and len(summary["account_ids"]) <= 20: # Don't flood output
|
712
696
|
console.print(f"\n[cyan]📋 Accounts with RDS snapshots:[/cyan]")
|
713
|
-
for account_id in summary[
|
714
|
-
account_snapshots = [s for s in snapshots if s.get(
|
697
|
+
for account_id in summary["account_ids"]:
|
698
|
+
account_snapshots = [s for s in snapshots if s.get("AccountId") == account_id]
|
715
699
|
console.print(f" [green]•[/green] {account_id}: {len(account_snapshots)} snapshots")
|
716
700
|
|
717
701
|
# Export results unless summary-only
|
@@ -724,13 +708,13 @@ def discover_rds_snapshots(
|
|
724
708
|
for region, aggregators in aggregator_map.items():
|
725
709
|
console.print(f" [blue]•[/blue] {region}: {', '.join(aggregators)}")
|
726
710
|
|
727
|
-
if summary[
|
711
|
+
if summary["age_analysis"]["cleanup_candidates"] > 0:
|
728
712
|
print_warning(
|
729
713
|
f"🎯 Found {summary['age_analysis']['cleanup_candidates']} manual snapshots "
|
730
714
|
f"older than 90 days - consider cleanup for cost optimization"
|
731
715
|
)
|
732
716
|
|
733
|
-
if summary[
|
717
|
+
if summary["security_analysis"]["unencrypted_snapshots"] > 0:
|
734
718
|
print_warning(
|
735
719
|
f"🔒 Found {summary['security_analysis']['unencrypted_snapshots']} unencrypted snapshots "
|
736
720
|
f"- review for security compliance"
|
@@ -742,4 +726,4 @@ def discover_rds_snapshots(
|
|
742
726
|
|
743
727
|
|
744
728
|
if __name__ == "__main__":
|
745
|
-
discover_rds_snapshots()
|
729
|
+
discover_rds_snapshots()
|