runbooks 1.1.4__py3-none-any.whl → 1.1.6__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 (273) 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/assessment/compliance.py +1 -1
  8. runbooks/cfat/assessment/runner.py +1 -0
  9. runbooks/cfat/cloud_foundations_assessment.py +227 -239
  10. runbooks/cli/__init__.py +1 -1
  11. runbooks/cli/commands/cfat.py +64 -23
  12. runbooks/cli/commands/finops.py +1005 -54
  13. runbooks/cli/commands/inventory.py +135 -91
  14. runbooks/cli/commands/operate.py +9 -36
  15. runbooks/cli/commands/security.py +42 -18
  16. runbooks/cli/commands/validation.py +432 -18
  17. runbooks/cli/commands/vpc.py +81 -17
  18. runbooks/cli/registry.py +22 -10
  19. runbooks/cloudops/__init__.py +20 -27
  20. runbooks/cloudops/base.py +96 -107
  21. runbooks/cloudops/cost_optimizer.py +544 -542
  22. runbooks/cloudops/infrastructure_optimizer.py +5 -4
  23. runbooks/cloudops/interfaces.py +224 -225
  24. runbooks/cloudops/lifecycle_manager.py +5 -4
  25. runbooks/cloudops/mcp_cost_validation.py +252 -235
  26. runbooks/cloudops/models.py +78 -53
  27. runbooks/cloudops/monitoring_automation.py +5 -4
  28. runbooks/cloudops/notebook_framework.py +177 -213
  29. runbooks/cloudops/security_enforcer.py +125 -159
  30. runbooks/common/accuracy_validator.py +17 -12
  31. runbooks/common/aws_pricing.py +349 -326
  32. runbooks/common/aws_pricing_api.py +211 -212
  33. runbooks/common/aws_profile_manager.py +40 -36
  34. runbooks/common/aws_utils.py +74 -79
  35. runbooks/common/business_logic.py +126 -104
  36. runbooks/common/cli_decorators.py +36 -60
  37. runbooks/common/comprehensive_cost_explorer_integration.py +455 -463
  38. runbooks/common/cross_account_manager.py +197 -204
  39. runbooks/common/date_utils.py +27 -39
  40. runbooks/common/decorators.py +29 -19
  41. runbooks/common/dry_run_examples.py +173 -208
  42. runbooks/common/dry_run_framework.py +157 -155
  43. runbooks/common/enhanced_exception_handler.py +15 -4
  44. runbooks/common/enhanced_logging_example.py +50 -64
  45. runbooks/common/enhanced_logging_integration_example.py +65 -37
  46. runbooks/common/env_utils.py +16 -16
  47. runbooks/common/error_handling.py +40 -38
  48. runbooks/common/lazy_loader.py +41 -23
  49. runbooks/common/logging_integration_helper.py +79 -86
  50. runbooks/common/mcp_cost_explorer_integration.py +476 -493
  51. runbooks/common/mcp_integration.py +99 -79
  52. runbooks/common/memory_optimization.py +140 -118
  53. runbooks/common/module_cli_base.py +37 -58
  54. runbooks/common/organizations_client.py +175 -193
  55. runbooks/common/patterns.py +23 -25
  56. runbooks/common/performance_monitoring.py +67 -71
  57. runbooks/common/performance_optimization_engine.py +283 -274
  58. runbooks/common/profile_utils.py +111 -37
  59. runbooks/common/rich_utils.py +315 -141
  60. runbooks/common/sre_performance_suite.py +177 -186
  61. runbooks/enterprise/__init__.py +1 -1
  62. runbooks/enterprise/logging.py +144 -106
  63. runbooks/enterprise/security.py +187 -204
  64. runbooks/enterprise/validation.py +43 -56
  65. runbooks/finops/__init__.py +26 -30
  66. runbooks/finops/account_resolver.py +1 -1
  67. runbooks/finops/advanced_optimization_engine.py +980 -0
  68. runbooks/finops/automation_core.py +268 -231
  69. runbooks/finops/business_case_config.py +184 -179
  70. runbooks/finops/cli.py +660 -139
  71. runbooks/finops/commvault_ec2_analysis.py +157 -164
  72. runbooks/finops/compute_cost_optimizer.py +336 -320
  73. runbooks/finops/config.py +20 -20
  74. runbooks/finops/cost_optimizer.py +484 -618
  75. runbooks/finops/cost_processor.py +332 -214
  76. runbooks/finops/dashboard_runner.py +1006 -172
  77. runbooks/finops/ebs_cost_optimizer.py +991 -657
  78. runbooks/finops/elastic_ip_optimizer.py +317 -257
  79. runbooks/finops/enhanced_mcp_integration.py +340 -0
  80. runbooks/finops/enhanced_progress.py +32 -29
  81. runbooks/finops/enhanced_trend_visualization.py +3 -2
  82. runbooks/finops/enterprise_wrappers.py +223 -285
  83. runbooks/finops/executive_export.py +203 -160
  84. runbooks/finops/helpers.py +130 -288
  85. runbooks/finops/iam_guidance.py +1 -1
  86. runbooks/finops/infrastructure/__init__.py +80 -0
  87. runbooks/finops/infrastructure/commands.py +506 -0
  88. runbooks/finops/infrastructure/load_balancer_optimizer.py +866 -0
  89. runbooks/finops/infrastructure/vpc_endpoint_optimizer.py +832 -0
  90. runbooks/finops/markdown_exporter.py +337 -174
  91. runbooks/finops/mcp_validator.py +1952 -0
  92. runbooks/finops/nat_gateway_optimizer.py +1512 -481
  93. runbooks/finops/network_cost_optimizer.py +657 -587
  94. runbooks/finops/notebook_utils.py +226 -188
  95. runbooks/finops/optimization_engine.py +1136 -0
  96. runbooks/finops/optimizer.py +19 -23
  97. runbooks/finops/rds_snapshot_optimizer.py +367 -411
  98. runbooks/finops/reservation_optimizer.py +427 -363
  99. runbooks/finops/scenario_cli_integration.py +64 -65
  100. runbooks/finops/scenarios.py +1277 -438
  101. runbooks/finops/schemas.py +218 -182
  102. runbooks/finops/snapshot_manager.py +2289 -0
  103. runbooks/finops/types.py +3 -3
  104. runbooks/finops/validation_framework.py +259 -265
  105. runbooks/finops/vpc_cleanup_exporter.py +189 -144
  106. runbooks/finops/vpc_cleanup_optimizer.py +591 -573
  107. runbooks/finops/workspaces_analyzer.py +171 -182
  108. runbooks/integration/__init__.py +89 -0
  109. runbooks/integration/mcp_integration.py +1920 -0
  110. runbooks/inventory/CLAUDE.md +816 -0
  111. runbooks/inventory/__init__.py +2 -2
  112. runbooks/inventory/aws_decorators.py +2 -3
  113. runbooks/inventory/check_cloudtrail_compliance.py +2 -4
  114. runbooks/inventory/check_controltower_readiness.py +152 -151
  115. runbooks/inventory/check_landingzone_readiness.py +85 -84
  116. runbooks/inventory/cloud_foundations_integration.py +144 -149
  117. runbooks/inventory/collectors/aws_comprehensive.py +1 -1
  118. runbooks/inventory/collectors/aws_networking.py +109 -99
  119. runbooks/inventory/collectors/base.py +4 -0
  120. runbooks/inventory/core/collector.py +495 -313
  121. runbooks/inventory/core/formatter.py +11 -0
  122. runbooks/inventory/draw_org_structure.py +8 -9
  123. runbooks/inventory/drift_detection_cli.py +69 -96
  124. runbooks/inventory/ec2_vpc_utils.py +2 -2
  125. runbooks/inventory/find_cfn_drift_detection.py +5 -7
  126. runbooks/inventory/find_cfn_orphaned_stacks.py +7 -9
  127. runbooks/inventory/find_cfn_stackset_drift.py +5 -6
  128. runbooks/inventory/find_ec2_security_groups.py +48 -42
  129. runbooks/inventory/find_landingzone_versions.py +4 -6
  130. runbooks/inventory/find_vpc_flow_logs.py +7 -9
  131. runbooks/inventory/inventory_mcp_cli.py +48 -46
  132. runbooks/inventory/inventory_modules.py +103 -91
  133. runbooks/inventory/list_cfn_stacks.py +9 -10
  134. runbooks/inventory/list_cfn_stackset_operation_results.py +1 -3
  135. runbooks/inventory/list_cfn_stackset_operations.py +79 -57
  136. runbooks/inventory/list_cfn_stacksets.py +8 -10
  137. runbooks/inventory/list_config_recorders_delivery_channels.py +49 -39
  138. runbooks/inventory/list_ds_directories.py +65 -53
  139. runbooks/inventory/list_ec2_availability_zones.py +2 -4
  140. runbooks/inventory/list_ec2_ebs_volumes.py +32 -35
  141. runbooks/inventory/list_ec2_instances.py +23 -28
  142. runbooks/inventory/list_ecs_clusters_and_tasks.py +26 -34
  143. runbooks/inventory/list_elbs_load_balancers.py +22 -20
  144. runbooks/inventory/list_enis_network_interfaces.py +26 -33
  145. runbooks/inventory/list_guardduty_detectors.py +2 -4
  146. runbooks/inventory/list_iam_policies.py +2 -4
  147. runbooks/inventory/list_iam_roles.py +5 -7
  148. runbooks/inventory/list_iam_saml_providers.py +4 -6
  149. runbooks/inventory/list_lambda_functions.py +38 -38
  150. runbooks/inventory/list_org_accounts.py +6 -8
  151. runbooks/inventory/list_org_accounts_users.py +55 -44
  152. runbooks/inventory/list_rds_db_instances.py +31 -33
  153. runbooks/inventory/list_rds_snapshots_aggregator.py +192 -208
  154. runbooks/inventory/list_route53_hosted_zones.py +3 -5
  155. runbooks/inventory/list_servicecatalog_provisioned_products.py +37 -41
  156. runbooks/inventory/list_sns_topics.py +2 -4
  157. runbooks/inventory/list_ssm_parameters.py +4 -7
  158. runbooks/inventory/list_vpc_subnets.py +2 -4
  159. runbooks/inventory/list_vpcs.py +7 -10
  160. runbooks/inventory/mcp_inventory_validator.py +554 -468
  161. runbooks/inventory/mcp_vpc_validator.py +359 -442
  162. runbooks/inventory/organizations_discovery.py +63 -55
  163. runbooks/inventory/recover_cfn_stack_ids.py +7 -8
  164. runbooks/inventory/requirements.txt +0 -1
  165. runbooks/inventory/rich_inventory_display.py +35 -34
  166. runbooks/inventory/run_on_multi_accounts.py +3 -5
  167. runbooks/inventory/unified_validation_engine.py +281 -253
  168. runbooks/inventory/verify_ec2_security_groups.py +1 -1
  169. runbooks/inventory/vpc_analyzer.py +735 -697
  170. runbooks/inventory/vpc_architecture_validator.py +293 -348
  171. runbooks/inventory/vpc_dependency_analyzer.py +384 -380
  172. runbooks/inventory/vpc_flow_analyzer.py +1 -1
  173. runbooks/main.py +49 -34
  174. runbooks/main_final.py +91 -60
  175. runbooks/main_minimal.py +22 -10
  176. runbooks/main_optimized.py +131 -100
  177. runbooks/main_ultra_minimal.py +7 -2
  178. runbooks/mcp/__init__.py +36 -0
  179. runbooks/mcp/integration.py +679 -0
  180. runbooks/monitoring/performance_monitor.py +9 -4
  181. runbooks/operate/dynamodb_operations.py +3 -1
  182. runbooks/operate/ec2_operations.py +145 -137
  183. runbooks/operate/iam_operations.py +146 -152
  184. runbooks/operate/networking_cost_heatmap.py +29 -8
  185. runbooks/operate/rds_operations.py +223 -254
  186. runbooks/operate/s3_operations.py +107 -118
  187. runbooks/operate/vpc_operations.py +646 -616
  188. runbooks/remediation/base.py +1 -1
  189. runbooks/remediation/commons.py +10 -7
  190. runbooks/remediation/commvault_ec2_analysis.py +70 -66
  191. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -0
  192. runbooks/remediation/multi_account.py +24 -21
  193. runbooks/remediation/rds_snapshot_list.py +86 -60
  194. runbooks/remediation/remediation_cli.py +92 -146
  195. runbooks/remediation/universal_account_discovery.py +83 -79
  196. runbooks/remediation/workspaces_list.py +46 -41
  197. runbooks/security/__init__.py +19 -0
  198. runbooks/security/assessment_runner.py +1150 -0
  199. runbooks/security/baseline_checker.py +812 -0
  200. runbooks/security/cloudops_automation_security_validator.py +509 -535
  201. runbooks/security/compliance_automation_engine.py +17 -17
  202. runbooks/security/config/__init__.py +2 -2
  203. runbooks/security/config/compliance_config.py +50 -50
  204. runbooks/security/config_template_generator.py +63 -76
  205. runbooks/security/enterprise_security_framework.py +1 -1
  206. runbooks/security/executive_security_dashboard.py +519 -508
  207. runbooks/security/multi_account_security_controls.py +959 -1210
  208. runbooks/security/real_time_security_monitor.py +422 -444
  209. runbooks/security/security_baseline_tester.py +1 -1
  210. runbooks/security/security_cli.py +143 -112
  211. runbooks/security/test_2way_validation.py +439 -0
  212. runbooks/security/two_way_validation_framework.py +852 -0
  213. runbooks/sre/production_monitoring_framework.py +167 -177
  214. runbooks/tdd/__init__.py +15 -0
  215. runbooks/tdd/cli.py +1071 -0
  216. runbooks/utils/__init__.py +14 -17
  217. runbooks/utils/logger.py +7 -2
  218. runbooks/utils/version_validator.py +50 -47
  219. runbooks/validation/__init__.py +6 -6
  220. runbooks/validation/cli.py +9 -3
  221. runbooks/validation/comprehensive_2way_validator.py +745 -704
  222. runbooks/validation/mcp_validator.py +906 -228
  223. runbooks/validation/terraform_citations_validator.py +104 -115
  224. runbooks/validation/terraform_drift_detector.py +461 -454
  225. runbooks/vpc/README.md +617 -0
  226. runbooks/vpc/__init__.py +8 -1
  227. runbooks/vpc/analyzer.py +577 -0
  228. runbooks/vpc/cleanup_wrapper.py +476 -413
  229. runbooks/vpc/cli_cloudtrail_commands.py +339 -0
  230. runbooks/vpc/cli_mcp_validation_commands.py +480 -0
  231. runbooks/vpc/cloudtrail_audit_integration.py +717 -0
  232. runbooks/vpc/config.py +92 -97
  233. runbooks/vpc/cost_engine.py +411 -148
  234. runbooks/vpc/cost_explorer_integration.py +553 -0
  235. runbooks/vpc/cross_account_session.py +101 -106
  236. runbooks/vpc/enhanced_mcp_validation.py +917 -0
  237. runbooks/vpc/eni_gate_validator.py +961 -0
  238. runbooks/vpc/heatmap_engine.py +185 -160
  239. runbooks/vpc/mcp_no_eni_validator.py +680 -639
  240. runbooks/vpc/nat_gateway_optimizer.py +358 -0
  241. runbooks/vpc/networking_wrapper.py +15 -8
  242. runbooks/vpc/pdca_remediation_planner.py +528 -0
  243. runbooks/vpc/performance_optimized_analyzer.py +219 -231
  244. runbooks/vpc/runbooks_adapter.py +1167 -241
  245. runbooks/vpc/tdd_red_phase_stubs.py +601 -0
  246. runbooks/vpc/test_data_loader.py +358 -0
  247. runbooks/vpc/tests/conftest.py +314 -4
  248. runbooks/vpc/tests/test_cleanup_framework.py +1022 -0
  249. runbooks/vpc/tests/test_cost_engine.py +0 -2
  250. runbooks/vpc/topology_generator.py +326 -0
  251. runbooks/vpc/unified_scenarios.py +1297 -1124
  252. runbooks/vpc/vpc_cleanup_integration.py +1943 -1115
  253. runbooks-1.1.6.dist-info/METADATA +327 -0
  254. runbooks-1.1.6.dist-info/RECORD +489 -0
  255. runbooks/finops/README.md +0 -414
  256. runbooks/finops/accuracy_cross_validator.py +0 -647
  257. runbooks/finops/business_cases.py +0 -950
  258. runbooks/finops/dashboard_router.py +0 -922
  259. runbooks/finops/ebs_optimizer.py +0 -973
  260. runbooks/finops/embedded_mcp_validator.py +0 -1629
  261. runbooks/finops/enhanced_dashboard_runner.py +0 -527
  262. runbooks/finops/finops_dashboard.py +0 -584
  263. runbooks/finops/finops_scenarios.py +0 -1218
  264. runbooks/finops/legacy_migration.py +0 -730
  265. runbooks/finops/multi_dashboard.py +0 -1519
  266. runbooks/finops/single_dashboard.py +0 -1113
  267. runbooks/finops/unlimited_scenarios.py +0 -393
  268. runbooks-1.1.4.dist-info/METADATA +0 -800
  269. runbooks-1.1.4.dist-info/RECORD +0 -468
  270. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/WHEEL +0 -0
  271. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/entry_points.txt +0 -0
  272. {runbooks-1.1.4.dist-info → runbooks-1.1.6.dist-info}/licenses/LICENSE +0 -0
  273. {runbooks-1.1.4.dist-info → runbooks-1.1.6.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()