runbooks 1.1.3__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.
Files changed (247) hide show
  1. runbooks/__init__.py +31 -2
  2. runbooks/__init___optimized.py +18 -4
  3. runbooks/_platform/__init__.py +1 -5
  4. runbooks/_platform/core/runbooks_wrapper.py +141 -138
  5. runbooks/aws2/accuracy_validator.py +812 -0
  6. runbooks/base.py +7 -0
  7. runbooks/cfat/WEIGHT_CONFIG_README.md +1 -1
  8. runbooks/cfat/assessment/compliance.py +8 -8
  9. runbooks/cfat/assessment/runner.py +1 -0
  10. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  11. runbooks/cfat/models.py +6 -2
  12. runbooks/cfat/tests/__init__.py +6 -1
  13. runbooks/cli/__init__.py +13 -0
  14. runbooks/cli/commands/cfat.py +274 -0
  15. runbooks/cli/commands/finops.py +1164 -0
  16. runbooks/cli/commands/inventory.py +379 -0
  17. runbooks/cli/commands/operate.py +239 -0
  18. runbooks/cli/commands/security.py +248 -0
  19. runbooks/cli/commands/validation.py +825 -0
  20. runbooks/cli/commands/vpc.py +310 -0
  21. runbooks/cli/registry.py +107 -0
  22. runbooks/cloudops/__init__.py +23 -30
  23. runbooks/cloudops/base.py +96 -107
  24. runbooks/cloudops/cost_optimizer.py +549 -547
  25. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  26. runbooks/cloudops/interfaces.py +226 -227
  27. runbooks/cloudops/lifecycle_manager.py +5 -4
  28. runbooks/cloudops/mcp_cost_validation.py +252 -235
  29. runbooks/cloudops/models.py +78 -53
  30. runbooks/cloudops/monitoring_automation.py +5 -4
  31. runbooks/cloudops/notebook_framework.py +179 -215
  32. runbooks/cloudops/security_enforcer.py +125 -159
  33. runbooks/common/accuracy_validator.py +11 -0
  34. runbooks/common/aws_pricing.py +349 -326
  35. runbooks/common/aws_pricing_api.py +211 -212
  36. runbooks/common/aws_profile_manager.py +341 -0
  37. runbooks/common/aws_utils.py +75 -80
  38. runbooks/common/business_logic.py +127 -105
  39. runbooks/common/cli_decorators.py +36 -60
  40. runbooks/common/comprehensive_cost_explorer_integration.py +456 -464
  41. runbooks/common/cross_account_manager.py +198 -205
  42. runbooks/common/date_utils.py +27 -39
  43. runbooks/common/decorators.py +235 -0
  44. runbooks/common/dry_run_examples.py +173 -208
  45. runbooks/common/dry_run_framework.py +157 -155
  46. runbooks/common/enhanced_exception_handler.py +15 -4
  47. runbooks/common/enhanced_logging_example.py +50 -64
  48. runbooks/common/enhanced_logging_integration_example.py +65 -37
  49. runbooks/common/env_utils.py +16 -16
  50. runbooks/common/error_handling.py +40 -38
  51. runbooks/common/lazy_loader.py +41 -23
  52. runbooks/common/logging_integration_helper.py +79 -86
  53. runbooks/common/mcp_cost_explorer_integration.py +478 -495
  54. runbooks/common/mcp_integration.py +63 -74
  55. runbooks/common/memory_optimization.py +140 -118
  56. runbooks/common/module_cli_base.py +37 -58
  57. runbooks/common/organizations_client.py +176 -194
  58. runbooks/common/patterns.py +204 -0
  59. runbooks/common/performance_monitoring.py +67 -71
  60. runbooks/common/performance_optimization_engine.py +283 -274
  61. runbooks/common/profile_utils.py +248 -39
  62. runbooks/common/rich_utils.py +643 -92
  63. runbooks/common/sre_performance_suite.py +177 -186
  64. runbooks/enterprise/__init__.py +1 -1
  65. runbooks/enterprise/logging.py +144 -106
  66. runbooks/enterprise/security.py +187 -204
  67. runbooks/enterprise/validation.py +43 -56
  68. runbooks/finops/__init__.py +29 -33
  69. runbooks/finops/account_resolver.py +1 -1
  70. runbooks/finops/advanced_optimization_engine.py +980 -0
  71. runbooks/finops/automation_core.py +268 -231
  72. runbooks/finops/business_case_config.py +184 -179
  73. runbooks/finops/cli.py +660 -139
  74. runbooks/finops/commvault_ec2_analysis.py +157 -164
  75. runbooks/finops/compute_cost_optimizer.py +336 -320
  76. runbooks/finops/config.py +20 -20
  77. runbooks/finops/cost_optimizer.py +488 -622
  78. runbooks/finops/cost_processor.py +332 -214
  79. runbooks/finops/dashboard_runner.py +1006 -172
  80. runbooks/finops/ebs_cost_optimizer.py +991 -657
  81. runbooks/finops/elastic_ip_optimizer.py +317 -257
  82. runbooks/finops/enhanced_mcp_integration.py +340 -0
  83. runbooks/finops/enhanced_progress.py +40 -37
  84. runbooks/finops/enhanced_trend_visualization.py +3 -2
  85. runbooks/finops/enterprise_wrappers.py +230 -292
  86. runbooks/finops/executive_export.py +203 -160
  87. runbooks/finops/helpers.py +130 -288
  88. runbooks/finops/iam_guidance.py +1 -1
  89. runbooks/finops/infrastructure/__init__.py +80 -0
  90. runbooks/finops/infrastructure/commands.py +506 -0
  91. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  92. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  93. runbooks/finops/markdown_exporter.py +338 -175
  94. runbooks/finops/mcp_validator.py +1952 -0
  95. runbooks/finops/nat_gateway_optimizer.py +1513 -482
  96. runbooks/finops/network_cost_optimizer.py +657 -587
  97. runbooks/finops/notebook_utils.py +226 -188
  98. runbooks/finops/optimization_engine.py +1136 -0
  99. runbooks/finops/optimizer.py +25 -29
  100. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  101. runbooks/finops/reservation_optimizer.py +427 -363
  102. runbooks/finops/scenario_cli_integration.py +77 -78
  103. runbooks/finops/scenarios.py +1278 -439
  104. runbooks/finops/schemas.py +218 -182
  105. runbooks/finops/snapshot_manager.py +2289 -0
  106. runbooks/finops/tests/test_finops_dashboard.py +3 -3
  107. runbooks/finops/tests/test_reference_images_validation.py +2 -2
  108. runbooks/finops/tests/test_single_account_features.py +17 -17
  109. runbooks/finops/tests/validate_test_suite.py +1 -1
  110. runbooks/finops/types.py +3 -3
  111. runbooks/finops/validation_framework.py +263 -269
  112. runbooks/finops/vpc_cleanup_exporter.py +191 -146
  113. runbooks/finops/vpc_cleanup_optimizer.py +593 -575
  114. runbooks/finops/workspaces_analyzer.py +171 -182
  115. runbooks/hitl/enhanced_workflow_engine.py +1 -1
  116. runbooks/integration/__init__.py +89 -0
  117. runbooks/integration/mcp_integration.py +1920 -0
  118. runbooks/inventory/CLAUDE.md +816 -0
  119. runbooks/inventory/README.md +3 -3
  120. runbooks/inventory/Tests/common_test_data.py +30 -30
  121. runbooks/inventory/__init__.py +2 -2
  122. runbooks/inventory/cloud_foundations_integration.py +144 -149
  123. runbooks/inventory/collectors/aws_comprehensive.py +28 -11
  124. runbooks/inventory/collectors/aws_networking.py +111 -101
  125. runbooks/inventory/collectors/base.py +4 -0
  126. runbooks/inventory/core/collector.py +495 -313
  127. runbooks/inventory/discovery.md +2 -2
  128. runbooks/inventory/drift_detection_cli.py +69 -96
  129. runbooks/inventory/find_ec2_security_groups.py +1 -1
  130. runbooks/inventory/inventory_mcp_cli.py +48 -46
  131. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  132. runbooks/inventory/mcp_inventory_validator.py +549 -465
  133. runbooks/inventory/mcp_vpc_validator.py +359 -442
  134. runbooks/inventory/organizations_discovery.py +56 -52
  135. runbooks/inventory/rich_inventory_display.py +33 -32
  136. runbooks/inventory/unified_validation_engine.py +278 -251
  137. runbooks/inventory/vpc_analyzer.py +733 -696
  138. runbooks/inventory/vpc_architecture_validator.py +293 -348
  139. runbooks/inventory/vpc_dependency_analyzer.py +382 -378
  140. runbooks/inventory/vpc_flow_analyzer.py +3 -3
  141. runbooks/main.py +152 -9147
  142. runbooks/main_final.py +91 -60
  143. runbooks/main_minimal.py +22 -10
  144. runbooks/main_optimized.py +131 -100
  145. runbooks/main_ultra_minimal.py +7 -2
  146. runbooks/mcp/__init__.py +36 -0
  147. runbooks/mcp/integration.py +679 -0
  148. runbooks/metrics/dora_metrics_engine.py +2 -2
  149. runbooks/monitoring/performance_monitor.py +9 -4
  150. runbooks/operate/dynamodb_operations.py +3 -1
  151. runbooks/operate/ec2_operations.py +145 -137
  152. runbooks/operate/iam_operations.py +146 -152
  153. runbooks/operate/mcp_integration.py +1 -1
  154. runbooks/operate/networking_cost_heatmap.py +33 -10
  155. runbooks/operate/privatelink_operations.py +1 -1
  156. runbooks/operate/rds_operations.py +223 -254
  157. runbooks/operate/s3_operations.py +107 -118
  158. runbooks/operate/vpc_endpoints.py +1 -1
  159. runbooks/operate/vpc_operations.py +648 -618
  160. runbooks/remediation/base.py +1 -1
  161. runbooks/remediation/commons.py +10 -7
  162. runbooks/remediation/commvault_ec2_analysis.py +71 -67
  163. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  164. runbooks/remediation/multi_account.py +24 -21
  165. runbooks/remediation/rds_snapshot_list.py +91 -65
  166. runbooks/remediation/remediation_cli.py +92 -146
  167. runbooks/remediation/universal_account_discovery.py +83 -79
  168. runbooks/remediation/workspaces_list.py +49 -44
  169. runbooks/security/__init__.py +19 -0
  170. runbooks/security/assessment_runner.py +1150 -0
  171. runbooks/security/baseline_checker.py +812 -0
  172. runbooks/security/cloudops_automation_security_validator.py +509 -535
  173. runbooks/security/compliance_automation_engine.py +17 -17
  174. runbooks/security/config/__init__.py +2 -2
  175. runbooks/security/config/compliance_config.py +50 -50
  176. runbooks/security/config_template_generator.py +63 -76
  177. runbooks/security/enterprise_security_framework.py +1 -1
  178. runbooks/security/executive_security_dashboard.py +519 -508
  179. runbooks/security/integration_test_enterprise_security.py +5 -3
  180. runbooks/security/multi_account_security_controls.py +959 -1210
  181. runbooks/security/real_time_security_monitor.py +422 -444
  182. runbooks/security/run_script.py +1 -1
  183. runbooks/security/security_baseline_tester.py +1 -1
  184. runbooks/security/security_cli.py +143 -112
  185. runbooks/security/test_2way_validation.py +439 -0
  186. runbooks/security/two_way_validation_framework.py +852 -0
  187. runbooks/sre/mcp_reliability_engine.py +6 -6
  188. runbooks/sre/production_monitoring_framework.py +167 -177
  189. runbooks/tdd/__init__.py +15 -0
  190. runbooks/tdd/cli.py +1071 -0
  191. runbooks/utils/__init__.py +14 -17
  192. runbooks/utils/logger.py +7 -2
  193. runbooks/utils/version_validator.py +51 -48
  194. runbooks/validation/__init__.py +6 -6
  195. runbooks/validation/cli.py +9 -3
  196. runbooks/validation/comprehensive_2way_validator.py +754 -708
  197. runbooks/validation/mcp_validator.py +906 -228
  198. runbooks/validation/terraform_citations_validator.py +104 -115
  199. runbooks/validation/terraform_drift_detector.py +447 -451
  200. runbooks/vpc/README.md +617 -0
  201. runbooks/vpc/__init__.py +8 -1
  202. runbooks/vpc/analyzer.py +577 -0
  203. runbooks/vpc/cleanup_wrapper.py +476 -413
  204. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  205. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  206. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  207. runbooks/vpc/config.py +92 -97
  208. runbooks/vpc/cost_engine.py +411 -148
  209. runbooks/vpc/cost_explorer_integration.py +553 -0
  210. runbooks/vpc/cross_account_session.py +101 -106
  211. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  212. runbooks/vpc/eni_gate_validator.py +961 -0
  213. runbooks/vpc/heatmap_engine.py +190 -162
  214. runbooks/vpc/mcp_no_eni_validator.py +681 -640
  215. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  216. runbooks/vpc/networking_wrapper.py +15 -8
  217. runbooks/vpc/pdca_remediation_planner.py +528 -0
  218. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  219. runbooks/vpc/runbooks_adapter.py +1167 -241
  220. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  221. runbooks/vpc/test_data_loader.py +358 -0
  222. runbooks/vpc/tests/conftest.py +314 -4
  223. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  224. runbooks/vpc/tests/test_cost_engine.py +0 -2
  225. runbooks/vpc/topology_generator.py +326 -0
  226. runbooks/vpc/unified_scenarios.py +1302 -1129
  227. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  228. runbooks-1.1.5.dist-info/METADATA +328 -0
  229. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/RECORD +233 -200
  230. runbooks/finops/README.md +0 -414
  231. runbooks/finops/accuracy_cross_validator.py +0 -647
  232. runbooks/finops/business_cases.py +0 -950
  233. runbooks/finops/dashboard_router.py +0 -922
  234. runbooks/finops/ebs_optimizer.py +0 -956
  235. runbooks/finops/embedded_mcp_validator.py +0 -1629
  236. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  237. runbooks/finops/finops_dashboard.py +0 -584
  238. runbooks/finops/finops_scenarios.py +0 -1218
  239. runbooks/finops/legacy_migration.py +0 -730
  240. runbooks/finops/multi_dashboard.py +0 -1519
  241. runbooks/finops/single_dashboard.py +0 -1113
  242. runbooks/finops/unlimited_scenarios.py +0 -393
  243. runbooks-1.1.3.dist-info/METADATA +0 -799
  244. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/WHEEL +0 -0
  245. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/entry_points.txt +0 -0
  246. {runbooks-1.1.3.dist-info → runbooks-1.1.5.dist-info}/licenses/LICENSE +0 -0
  247. {runbooks-1.1.3.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, print_header, print_success, print_error, print_warning, print_info,
33
- create_table, create_progress_bar, format_cost
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
- 'us-east-1', 'us-west-2', 'eu-west-1', 'ap-southeast-2',
62
- 'ap-northeast-1', 'ca-central-1', 'eu-central-1'
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
- 'aggregators_found': 0,
73
- 'snapshots_discovered': 0,
74
- 'accounts_covered': 0,
75
- 'regions_scanned': 0,
76
- 'api_calls_made': 0,
77
- 'errors_encountered': 0
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('sts')
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['aggregators_found'] += len(aggregators)
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['regions_scanned'] += 1
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['errors_encountered'] += 1
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('config', region_name=region)
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['api_calls_made'] += 1
167
+ self.metrics["api_calls_made"] += 1
160
168
 
161
169
  aggregators = []
162
- for aggregator in response.get('ConfigurationAggregators', []):
163
- aggregator_name = aggregator['ConfigurationAggregatorName']
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 'OrganizationAggregationSource' in aggregator:
169
- org_source = aggregator['OrganizationAggregationSource']
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['Error']['Code']
176
- if error_code in ['AccessDenied', 'UnauthorizedOperation']:
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('AccountId', 'unknown') for s in snapshots)
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['errors_encountered'] += 1
239
+ self.metrics["errors_encountered"] += 1
237
240
 
238
241
  progress.advance(task_id)
239
242
 
240
243
  # Update metrics
241
- self.metrics['snapshots_discovered'] = len(all_snapshots)
242
- unique_accounts = set(s.get('AccountId', 'unknown') for s in all_snapshots)
243
- self.metrics['accounts_covered'] = len(unique_accounts)
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('config', region_name=region)
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
- 'ConfigurationAggregatorName': aggregator_name,
294
- 'Expression': query_expression,
295
- 'Limit': 100 # Maximum allowed by Config API
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['NextToken'] = next_token
298
+ query_params["NextToken"] = next_token
300
299
 
301
300
  response = config_client.select_aggregate_resource_config(**query_params)
302
- self.metrics['api_calls_made'] += 1
301
+ self.metrics["api_calls_made"] += 1
303
302
 
304
303
  # Process results
305
- for result in response.get('Results', []):
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('NextToken')
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['Error']['Code']
325
- if error_code == 'NoSuchConfigurationAggregatorException':
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 ['AccessDenied', 'UnauthorizedOperation']:
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
- 'DBSnapshotIdentifier': config_data.get('resourceId', 'unknown'),
342
- 'AccountId': config_data.get('accountId', 'unknown'),
343
- 'Region': config_data.get('awsRegion', 'unknown'),
344
- 'DiscoveryMethod': 'config_aggregator',
345
- 'ConfigCaptureTime': config_data.get('configurationItemCaptureTime'),
346
- 'ResourceCreationTime': config_data.get('resourceCreationTime')
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('configuration', {})
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
- '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('iAMDatabaseAuthenticationEnabled', False),
383
- 'ProcessorFeatures': configuration.get('processorFeatures', []),
384
- 'DbiResourceId': configuration.get('dbiResourceId'),
385
- 'TagList': configuration.get('tagList', [])
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('SnapshotCreateTime'):
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['AgeDays'] = age_days
396
+ snapshot_info["AgeDays"] = age_days
396
397
 
397
398
  # Estimate storage cost (basic calculation)
398
- allocated_storage = snapshot_info.get('AllocatedStorage', 0)
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['EstimatedMonthlyCost'] = round(monthly_cost, 2)
403
- snapshot_info['EstimatedAnnualCost'] = round(monthly_cost * 12, 2)
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['AgeDays'] = 0
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('AccountId', 'unknown') for s in snapshots)
483
- unique_regions = set(s.get('Region', 'unknown') for s in snapshots)
484
- unique_engines = set(s.get('Engine', 'unknown') for s in snapshots)
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('SnapshotType', '').lower() == 'manual']
488
- automated_snapshots = [s for s in snapshots if s.get('SnapshotType', '').lower() == 'automated']
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('AgeDays', 0) >= 90] # 3+ months
492
- very_old_snapshots = [s for s in snapshots if s.get('AgeDays', 0) >= 180] # 6+ months
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('AllocatedStorage', 0) for s in snapshots)
496
- total_estimated_cost = sum(s.get('EstimatedMonthlyCost', 0) for s in snapshots)
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('Encrypted', False)]
487
+ encrypted_snapshots = [s for s in snapshots if s.get("Encrypted", False)]
500
488
 
501
489
  return {
502
- 'total_snapshots': total_snapshots,
503
- 'unique_accounts': len(unique_accounts),
504
- 'unique_regions': len(unique_regions),
505
- 'unique_engines': len(unique_engines),
506
- 'account_ids': sorted(list(unique_accounts)),
507
- 'regions': sorted(list(unique_regions)),
508
- 'engines': sorted(list(unique_engines)),
509
- 'snapshot_types': {
510
- 'manual': len(manual_snapshots),
511
- 'automated': len(automated_snapshots),
512
- 'unknown': total_snapshots - len(manual_snapshots) - len(automated_snapshots)
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
- 'age_analysis': {
515
- 'aged_snapshots_90d': len(aged_snapshots),
516
- 'very_old_snapshots_180d': len(very_old_snapshots),
517
- 'cleanup_candidates': len([s for s in manual_snapshots if s.get('AgeDays', 0) >= 90])
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
- 'storage_analysis': {
520
- 'total_storage_gb': total_storage,
521
- 'estimated_monthly_cost': round(total_estimated_cost, 2),
522
- 'estimated_annual_cost': round(total_estimated_cost * 12, 2)
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
- 'security_analysis': {
525
- 'encrypted_snapshots': len(encrypted_snapshots),
526
- 'unencrypted_snapshots': total_snapshots - len(encrypted_snapshots),
527
- 'encryption_percentage': round((len(encrypted_snapshots) / total_snapshots) * 100, 1) if total_snapshots > 0 else 0
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
- 'discovery_metrics': self.metrics
519
+ "discovery_metrics": self.metrics,
530
520
  }
531
521
 
532
- def export_results(self, snapshots: List[Dict], output_file: str, format: str = 'csv') -> bool:
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() == 'csv':
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, 'w', newline='', encoding='utf-8') as csvfile:
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() == 'json':
564
- with open(output_file, 'w', encoding='utf-8') as jsonfile:
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('--profile', help='AWS profile with Config aggregator access (overrides environment)')
581
- @click.option('--target-accounts', multiple=True, help='Specific account IDs to analyze')
582
- @click.option('--regions', multiple=True, help='Specific regions to check for aggregators')
583
- @click.option('--age-filter', type=int, help='Filter snapshots older than X days')
584
- @click.option('--snapshot-type', type=click.Choice(['manual', 'automated']), help='Filter by snapshot type')
585
- @click.option('--engine-filter', help='Filter by database engine (partial match)')
586
- @click.option('--output-file', default='./rds_snapshots_config_discovery.csv', help='Output file path')
587
- @click.option('--format', type=click.Choice(['csv', 'json']), default='csv', help='Output format')
588
- @click.option('--max-workers', type=int, default=10, help='Maximum concurrent workers')
589
- @click.option('--summary-only', is_flag=True, help='Show only summary report')
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['total_snapshots']))
679
- summary_table.add_row("Unique Accounts", str(summary['unique_accounts']))
680
- summary_table.add_row("Unique Regions", str(summary['unique_regions']))
681
- summary_table.add_row("Database Engines", str(summary['unique_engines']))
682
- summary_table.add_row("Manual Snapshots", str(summary['snapshot_types']['manual']))
683
- summary_table.add_row("Automated Snapshots", str(summary['snapshot_types']['automated']))
684
- summary_table.add_row("Old Snapshots (90d+)", str(summary['age_analysis']['aged_snapshots_90d']))
685
- summary_table.add_row("Cleanup Candidates", str(summary['age_analysis']['cleanup_candidates']))
686
- summary_table.add_row("Total Storage (GB)", str(summary['storage_analysis']['total_storage_gb']))
687
- summary_table.add_row("Est. Monthly Cost", format_cost(summary['storage_analysis']['estimated_monthly_cost']))
688
- summary_table.add_row("Est. Annual Cost", format_cost(summary['storage_analysis']['estimated_annual_cost']))
689
- summary_table.add_row("Encrypted Snapshots", f"{summary['security_analysis']['encrypted_snapshots']} ({summary['security_analysis']['encryption_percentage']}%)")
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['discovery_metrics']
703
- metrics_table.add_row("Config Aggregators Found", str(metrics['aggregators_found']))
704
- metrics_table.add_row("Regions Scanned", str(metrics['regions_scanned']))
705
- metrics_table.add_row("API Calls Made", str(metrics['api_calls_made']))
706
- metrics_table.add_row("Errors Encountered", str(metrics['errors_encountered']))
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['account_ids'] and len(summary['account_ids']) <= 20: # Don't flood output
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['account_ids']:
714
- account_snapshots = [s for s in snapshots if s.get('AccountId') == account_id]
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['age_analysis']['cleanup_candidates'] > 0:
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['security_analysis']['unencrypted_snapshots'] > 0:
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()