runbooks 0.9.9__py3-none-any.whl → 1.0.1__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 (111) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/WEIGHT_CONFIG_README.md +368 -0
  3. runbooks/cfat/app.ts +27 -19
  4. runbooks/cfat/assessment/runner.py +6 -5
  5. runbooks/cfat/cloud_foundations_assessment.py +626 -0
  6. runbooks/cfat/tests/test_weight_configuration.ts +449 -0
  7. runbooks/cfat/weight_config.ts +574 -0
  8. runbooks/cloudops/cost_optimizer.py +95 -33
  9. runbooks/common/__init__.py +26 -9
  10. runbooks/common/aws_pricing.py +1353 -0
  11. runbooks/common/aws_pricing_api.py +205 -0
  12. runbooks/common/aws_utils.py +2 -2
  13. runbooks/common/comprehensive_cost_explorer_integration.py +979 -0
  14. runbooks/common/cross_account_manager.py +606 -0
  15. runbooks/common/date_utils.py +115 -0
  16. runbooks/common/enhanced_exception_handler.py +14 -7
  17. runbooks/common/env_utils.py +96 -0
  18. runbooks/common/mcp_cost_explorer_integration.py +5 -4
  19. runbooks/common/mcp_integration.py +49 -2
  20. runbooks/common/organizations_client.py +579 -0
  21. runbooks/common/profile_utils.py +127 -72
  22. runbooks/common/rich_utils.py +3 -3
  23. runbooks/finops/cost_optimizer.py +2 -1
  24. runbooks/finops/dashboard_runner.py +47 -28
  25. runbooks/finops/ebs_optimizer.py +56 -9
  26. runbooks/finops/elastic_ip_optimizer.py +13 -9
  27. runbooks/finops/embedded_mcp_validator.py +31 -0
  28. runbooks/finops/enhanced_trend_visualization.py +10 -4
  29. runbooks/finops/finops_dashboard.py +6 -5
  30. runbooks/finops/iam_guidance.py +6 -1
  31. runbooks/finops/markdown_exporter.py +217 -2
  32. runbooks/finops/nat_gateway_optimizer.py +76 -20
  33. runbooks/finops/tests/test_integration.py +3 -1
  34. runbooks/finops/vpc_cleanup_exporter.py +28 -26
  35. runbooks/finops/vpc_cleanup_optimizer.py +363 -16
  36. runbooks/inventory/__init__.py +10 -1
  37. runbooks/inventory/cloud_foundations_integration.py +409 -0
  38. runbooks/inventory/core/collector.py +1177 -94
  39. runbooks/inventory/discovery.md +339 -0
  40. runbooks/inventory/drift_detection_cli.py +327 -0
  41. runbooks/inventory/inventory_mcp_cli.py +171 -0
  42. runbooks/inventory/inventory_modules.py +6 -9
  43. runbooks/inventory/list_ec2_instances.py +3 -3
  44. runbooks/inventory/mcp_inventory_validator.py +2149 -0
  45. runbooks/inventory/mcp_vpc_validator.py +23 -6
  46. runbooks/inventory/organizations_discovery.py +104 -9
  47. runbooks/inventory/rich_inventory_display.py +129 -1
  48. runbooks/inventory/unified_validation_engine.py +1279 -0
  49. runbooks/inventory/verify_ec2_security_groups.py +3 -1
  50. runbooks/inventory/vpc_analyzer.py +825 -7
  51. runbooks/inventory/vpc_flow_analyzer.py +36 -42
  52. runbooks/main.py +708 -47
  53. runbooks/monitoring/performance_monitor.py +11 -7
  54. runbooks/operate/base.py +9 -6
  55. runbooks/operate/deployment_framework.py +5 -4
  56. runbooks/operate/deployment_validator.py +6 -5
  57. runbooks/operate/dynamodb_operations.py +6 -5
  58. runbooks/operate/ec2_operations.py +3 -2
  59. runbooks/operate/mcp_integration.py +6 -5
  60. runbooks/operate/networking_cost_heatmap.py +21 -16
  61. runbooks/operate/s3_operations.py +13 -12
  62. runbooks/operate/vpc_operations.py +100 -12
  63. runbooks/remediation/base.py +4 -2
  64. runbooks/remediation/commons.py +5 -5
  65. runbooks/remediation/commvault_ec2_analysis.py +68 -15
  66. runbooks/remediation/config/accounts_example.json +31 -0
  67. runbooks/remediation/ec2_unattached_ebs_volumes.py +6 -3
  68. runbooks/remediation/multi_account.py +120 -7
  69. runbooks/remediation/rds_snapshot_list.py +5 -3
  70. runbooks/remediation/remediation_cli.py +710 -0
  71. runbooks/remediation/universal_account_discovery.py +377 -0
  72. runbooks/security/compliance_automation_engine.py +99 -20
  73. runbooks/security/config/__init__.py +24 -0
  74. runbooks/security/config/compliance_config.py +255 -0
  75. runbooks/security/config/compliance_weights_example.json +22 -0
  76. runbooks/security/config_template_generator.py +500 -0
  77. runbooks/security/security_cli.py +377 -0
  78. runbooks/validation/__init__.py +21 -1
  79. runbooks/validation/cli.py +8 -7
  80. runbooks/validation/comprehensive_2way_validator.py +2007 -0
  81. runbooks/validation/mcp_validator.py +965 -101
  82. runbooks/validation/terraform_citations_validator.py +363 -0
  83. runbooks/validation/terraform_drift_detector.py +1098 -0
  84. runbooks/vpc/cleanup_wrapper.py +231 -10
  85. runbooks/vpc/config.py +346 -73
  86. runbooks/vpc/cross_account_session.py +312 -0
  87. runbooks/vpc/heatmap_engine.py +115 -41
  88. runbooks/vpc/manager_interface.py +9 -9
  89. runbooks/vpc/mcp_no_eni_validator.py +1630 -0
  90. runbooks/vpc/networking_wrapper.py +14 -8
  91. runbooks/vpc/runbooks_adapter.py +33 -12
  92. runbooks/vpc/tests/conftest.py +4 -2
  93. runbooks/vpc/tests/test_cost_engine.py +4 -2
  94. runbooks/vpc/unified_scenarios.py +73 -3
  95. runbooks/vpc/vpc_cleanup_integration.py +512 -78
  96. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/METADATA +94 -52
  97. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/RECORD +101 -81
  98. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  99. runbooks/finops/runbooks.security.report_generator.log +0 -0
  100. runbooks/finops/runbooks.security.run_script.log +0 -0
  101. runbooks/finops/runbooks.security.security_export.log +0 -0
  102. runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
  103. runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
  104. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  105. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  106. runbooks/inventory/runbooks.security.run_script.log +0 -0
  107. runbooks/inventory/runbooks.security.security_export.log +0 -0
  108. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
  109. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.9.9.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
@@ -1,23 +1,31 @@
1
1
  """
2
- Enhanced Inventory collector for AWS resources with 4-Profile Architecture.
3
-
4
- This module provides the main inventory collection orchestration,
5
- leveraging existing inventory scripts and extending them with
6
- cloud foundations best practices.
7
-
8
- ENHANCED v0.8.0: 4-Profile AWS SSO Architecture & Performance Benchmarking
9
- - Proven FinOps success patterns: 61 accounts, $474,406 validated
10
- - Performance targets: <45s for inventory discovery operations
11
- - Comprehensive error handling with profile fallbacks
12
- - Enterprise-grade reliability and monitoring
13
- - Phase 4: MCP Integration Framework & Cross-Module Data Flow
2
+ Enhanced Inventory Collector - AWS Resource Discovery with Enterprise Profile Management.
3
+
4
+ Strategic Alignment:
5
+ - "Do one thing and do it well" - Focused inventory collection with proven patterns
6
+ - "Move Fast, But Not So Fast We Crash" - Performance with enterprise reliability
7
+
8
+ Core Capabilities:
9
+ - Single profile architecture: --profile override pattern for all operations
10
+ - Multi-account discovery leveraging existing enterprise infrastructure
11
+ - Performance targets: <45s inventory operations across 60+ accounts
12
+ - MCP integration for real-time AWS API validation and accuracy
13
+ - Rich CLI output following enterprise UX standards
14
+
15
+ Business Value:
16
+ - Enables systematic AWS resource governance across enterprise landing zones
17
+ - Provides foundation for cost optimization and security compliance initiatives
18
+ - Supports terraform IaC validation and configuration drift detection
14
19
  """
15
20
 
16
21
  import asyncio
22
+ import json
23
+ import os
17
24
  from concurrent.futures import ThreadPoolExecutor, as_completed
18
25
  from datetime import datetime, timezone
19
26
  from typing import Any, Dict, List, Optional, Set
20
27
 
28
+ import boto3
21
29
  from loguru import logger
22
30
 
23
31
  from runbooks.base import CloudFoundationsBase, ProgressTracker
@@ -34,12 +42,13 @@ try:
34
42
  ENHANCED_PROFILES_AVAILABLE = True
35
43
  except ImportError:
36
44
  ENHANCED_PROFILES_AVAILABLE = False
37
- # Fallback profile definitions
45
+ # Fallback profile definitions with universal environment support
46
+ import os
38
47
  ENTERPRISE_PROFILES = {
39
- "BILLING_PROFILE": "ams-admin-Billing-ReadOnlyAccess-909135376185",
40
- "MANAGEMENT_PROFILE": "ams-admin-ReadOnlyAccess-909135376185",
41
- "CENTRALISED_OPS_PROFILE": "ams-centralised-ops-ReadOnlyAccess-335083429030",
42
- "SINGLE_ACCOUNT_PROFILE": "ams-shared-services-non-prod-ReadOnlyAccess-499201730520",
48
+ "BILLING_PROFILE": os.getenv("BILLING_PROFILE", "default-billing-profile"),
49
+ "MANAGEMENT_PROFILE": os.getenv("MANAGEMENT_PROFILE", "default-management-profile"),
50
+ "CENTRALISED_OPS_PROFILE": os.getenv("CENTRALISED_OPS_PROFILE", "default-ops-profile"),
51
+ "SINGLE_ACCOUNT_PROFILE": os.getenv("SINGLE_AWS_PROFILE", "default-single-profile"),
43
52
  }
44
53
 
45
54
 
@@ -87,8 +96,8 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
87
96
  self.benchmarks = []
88
97
  self.current_benchmark = None
89
98
 
90
- # Enhanced profile management
91
- self.available_profiles = self._initialize_profile_architecture()
99
+ # Simplified profile management: single profile for all operations
100
+ self.active_profile = self._initialize_profile_architecture()
92
101
 
93
102
  # Resource collectors
94
103
  self._resource_collectors = self._initialize_collectors()
@@ -97,9 +106,20 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
97
106
  self.mcp_integrator = EnterpriseMCPIntegrator(profile)
98
107
  self.cross_module_integrator = EnterpriseCrossModuleIntegrator(profile)
99
108
  self.enable_mcp_validation = True
109
+
110
+ # Initialize inventory-specific MCP validator
111
+ self.inventory_mcp_validator = None
112
+ try:
113
+ from ..mcp_inventory_validator import create_inventory_mcp_validator
114
+ # Use profiles that would work for inventory operations
115
+ validator_profiles = [self.active_profile]
116
+ self.inventory_mcp_validator = create_inventory_mcp_validator(validator_profiles)
117
+ print_info("Inventory MCP validator initialized for real-time validation")
118
+ except Exception as e:
119
+ print_warning(f"Inventory MCP validator initialization failed: {str(e)[:50]}...")
100
120
 
101
121
  print_info("Enhanced inventory collector with MCP integration initialized")
102
- logger.info(f"Enhanced inventory collector initialized with {len(self.available_profiles)} profiles")
122
+ logger.info(f"Enhanced inventory collector initialized with active profile: {self.active_profile}")
103
123
 
104
124
  def run(self, **kwargs) -> Dict[str, Any]:
105
125
  """
@@ -116,17 +136,42 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
116
136
  resource_types=resource_types, account_ids=account_ids, include_costs=include_costs
117
137
  )
118
138
 
119
- def _initialize_profile_architecture(self) -> Dict[str, str]:
120
- """Initialize 4-profile AWS SSO architecture"""
121
- if self.use_enterprise_profiles and ENHANCED_PROFILES_AVAILABLE:
122
- profiles = ENTERPRISE_PROFILES.copy()
123
- logger.info("Using proven enterprise 4-profile AWS SSO architecture")
139
+ def _initialize_profile_architecture(self) -> str:
140
+ """
141
+ Initialize profile management following --profile or --all patterns.
142
+
143
+ Strategic Alignment: "Do one thing and do it well"
144
+ - Single profile override pattern: --profile takes precedence
145
+ - Universal AWS environment compatibility: works with ANY profile configuration
146
+ - Graceful fallback system for discovery across different AWS setups
147
+
148
+ Returns:
149
+ str: The active profile to use for all operations
150
+ """
151
+ # PRIMARY: User --profile parameter takes absolute precedence (Universal Compatibility)
152
+ if self.profile:
153
+ print_info(f"✅ Universal AWS Compatibility: Using user-specified profile '{self.profile}'")
154
+ logger.info("Profile override via --profile parameter - universal environment support")
155
+ return self.profile
156
+
157
+ # SECONDARY: Environment variable fallback with intelligent prioritization
158
+ # Priority order: Management > Billing > Operations > Default (Organizations discovery preference)
159
+ env_profile = (
160
+ os.getenv("MANAGEMENT_PROFILE") or
161
+ os.getenv("BILLING_PROFILE") or
162
+ os.getenv("CENTRALISED_OPS_PROFILE") or
163
+ os.getenv("SINGLE_AWS_PROFILE") or
164
+ "default"
165
+ )
166
+
167
+ if env_profile != "default":
168
+ print_info(f"✅ Universal AWS Compatibility: Using environment profile '{env_profile}'")
169
+ logger.info(f"Environment variable profile selected: {env_profile}")
124
170
  else:
125
- # Fallback to single profile or provided profile
126
- profiles = {"PRIMARY_PROFILE": self.profile or "default"}
127
- logger.info(f"Using single profile architecture: {profiles['PRIMARY_PROFILE']}")
128
-
129
- return profiles
171
+ print_info("✅ Universal AWS Compatibility: Using 'default' profile - works with any AWS CLI configuration")
172
+ logger.info("Using default profile - universal compatibility mode")
173
+
174
+ return env_profile
130
175
 
131
176
  def _initialize_collectors(self) -> Dict[str, str]:
132
177
  """Initialize available resource collectors."""
@@ -140,19 +185,68 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
140
185
  "vpc": "VPCCollector",
141
186
  "cloudformation": "CloudFormationCollector",
142
187
  "costs": "CostCollector",
188
+ "organizations": "ManagementResourceCollector",
143
189
  }
144
190
 
145
191
  logger.debug(f"Initialized {len(collectors)} resource collectors")
146
192
  return collectors
193
+
194
+ def _extract_resource_counts(self, resource_data: Dict[str, Any]) -> Dict[str, int]:
195
+ """
196
+ Extract resource counts from collected inventory data for MCP validation.
197
+
198
+ Args:
199
+ resource_data: Raw resource data from inventory collection
200
+
201
+ Returns:
202
+ Dictionary mapping resource types to counts
203
+ """
204
+ resource_counts = {}
205
+
206
+ try:
207
+ # Handle various data structures from inventory collection
208
+ if isinstance(resource_data, dict):
209
+ for resource_type, resources in resource_data.items():
210
+ if isinstance(resources, list):
211
+ resource_counts[resource_type] = len(resources)
212
+ elif isinstance(resources, dict):
213
+ # Handle nested structures (e.g., by region)
214
+ total_count = 0
215
+ for region_data in resources.values():
216
+ if isinstance(region_data, list):
217
+ total_count += len(region_data)
218
+ elif isinstance(region_data, dict) and 'resources' in region_data:
219
+ total_count += len(region_data['resources'])
220
+ resource_counts[resource_type] = total_count
221
+ elif isinstance(resources, int):
222
+ resource_counts[resource_type] = resources
223
+
224
+ logger.debug(f"Extracted resource counts for validation: {resource_counts}")
225
+ return resource_counts
226
+
227
+ except Exception as e:
228
+ logger.warning(f"Failed to extract resource counts for MCP validation: {e}")
229
+ return {}
147
230
 
148
231
  def get_all_resource_types(self) -> List[str]:
149
232
  """Get list of all available resource types."""
150
233
  return list(self._resource_collectors.keys())
151
234
 
152
235
  def get_organization_accounts(self) -> List[str]:
153
- """Get list of accounts in AWS Organization."""
236
+ """
237
+ Get list of accounts in AWS Organization with universal compatibility.
238
+
239
+ Strategic Alignment: "Do one thing and do it well"
240
+ - Universal AWS environment compatibility: works with ANY Organizations setup
241
+ - Intelligent fallback system: Organizations → standalone account detection
242
+ - Graceful handling of different permission scenarios
243
+ """
154
244
  try:
155
- organizations_client = self.get_client("organizations")
245
+ # Use active profile for Organizations operations (Universal Compatibility)
246
+ management_session = create_management_session(profile=self.active_profile)
247
+ organizations_client = management_session.client("organizations")
248
+
249
+ print_info(f"🔍 Universal Discovery: Attempting Organizations API with profile '{self.active_profile}'...")
156
250
  response = self._make_aws_call(organizations_client.list_accounts)
157
251
 
158
252
  accounts = []
@@ -160,12 +254,31 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
160
254
  if account["Status"] == "ACTIVE":
161
255
  accounts.append(account["Id"])
162
256
 
163
- logger.info(f"Found {len(accounts)} active accounts in organization")
164
- return accounts
257
+ if accounts:
258
+ print_success(f"✅ Organizations Discovery: Found {len(accounts)} active accounts in organization")
259
+ logger.info(f"Organizations discovery successful: {len(accounts)} accounts with profile {self.active_profile}")
260
+ return accounts
261
+ else:
262
+ print_warning("⚠️ Organizations Discovery: No active accounts found in organization")
263
+ return [self.get_account_id()]
165
264
 
166
265
  except Exception as e:
167
- logger.warning(f"Could not list organization accounts: {e}")
168
- # Fallback to current account
266
+ # Enhanced error messages for different AWS environment scenarios
267
+ error_message = str(e).lower()
268
+
269
+ if "accessdenied" in error_message or "unauthorized" in error_message:
270
+ print_warning(f"⚠️ Universal Compatibility: Profile '{self.active_profile}' lacks Organizations permissions")
271
+ print_info("💡 Single Account Mode: Continuing with current account (universal compatibility)")
272
+ elif "organizationsnotinuse" in error_message:
273
+ print_info(f"ℹ️ Standalone Account: Profile '{self.active_profile}' not in an AWS Organization")
274
+ print_info("💡 Single Account Mode: Continuing with current account")
275
+ else:
276
+ print_warning(f"⚠️ Organizations Discovery Failed: {e}")
277
+ print_info("💡 Fallback Mode: Continuing with current account for universal compatibility")
278
+
279
+ logger.warning(f"Organization discovery failed, graceful fallback: {e}")
280
+
281
+ # Universal fallback: always return current account for single-account operations
169
282
  return [self.get_account_id()]
170
283
 
171
284
  def get_current_account_id(self) -> str:
@@ -210,13 +323,13 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
210
323
  "collector_profile": self.profile,
211
324
  "collector_region": self.region,
212
325
  "enterprise_profiles_used": self.use_enterprise_profiles,
213
- "available_profiles": len(self.available_profiles),
326
+ "active_profile": self.active_profile,
214
327
  "performance_target": self.performance_target_seconds,
215
328
  },
216
329
  "resources": {},
217
330
  "summary": {},
218
331
  "errors": [],
219
- "profile_info": self.available_profiles,
332
+ "profile_info": {"active_profile": self.active_profile},
220
333
  }
221
334
 
222
335
  try:
@@ -228,22 +341,55 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
228
341
  results["resources"] = resource_data
229
342
  results["summary"] = self._generate_summary(resource_data)
230
343
 
231
- # Phase 4: MCP Validation Integration
232
- if self.enable_mcp_validation:
344
+ # Phase 4: Enhanced Inventory MCP Validation Integration
345
+ if self.enable_mcp_validation and self.inventory_mcp_validator:
233
346
  try:
234
- print_info("Validating inventory results with MCP integration")
235
- validation_result = asyncio.run(self.mcp_integrator.validate_inventory_operations(results))
236
-
237
- results["mcp_validation"] = validation_result.to_dict()
238
-
239
- if validation_result.success:
240
- print_success(f"MCP validation passed: {validation_result.accuracy_score}% accuracy")
347
+ print_info("Validating inventory results with specialized inventory MCP validator")
348
+
349
+ # Extract resource counts for validation
350
+ # Build validation data structure that matches what the validator expects
351
+ resource_counts = self._extract_resource_counts(resource_data)
352
+
353
+ # Add resource counts to results for the validator to find
354
+ results["resource_counts"] = resource_counts
355
+
356
+ validation_data = {
357
+ "resource_counts": resource_counts,
358
+ "regions": results["metadata"].get("regions_scanned", []),
359
+ self.active_profile: {
360
+ "resource_counts": resource_counts,
361
+ "regions": results["metadata"].get("regions_scanned", [])
362
+ }
363
+ }
364
+
365
+ # Run inventory-specific MCP validation
366
+ inventory_validation = self.inventory_mcp_validator.validate_inventory_data(validation_data)
367
+
368
+ results["inventory_mcp_validation"] = inventory_validation
369
+
370
+ overall_accuracy = inventory_validation.get("total_accuracy", 0)
371
+ if inventory_validation.get("passed_validation", False):
372
+ print_success(f"✅ Inventory MCP validation PASSED: {overall_accuracy:.1f}% accuracy achieved")
241
373
  else:
242
- print_warning("MCP validation encountered issues - results may need review")
374
+ print_warning(f"⚠️ Inventory MCP validation: {overall_accuracy:.1f}% accuracy (≥99.5% required)")
375
+
376
+ # Also try the generic MCP integrator as backup
377
+ try:
378
+ validation_result = asyncio.run(self.mcp_integrator.validate_inventory_operations(results))
379
+ results["mcp_validation"] = validation_result.to_dict()
380
+ except Exception:
381
+ pass # Skip generic validation if it fails
243
382
 
244
383
  except Exception as e:
245
- print_warning(f"MCP validation failed: {str(e)[:50]}... - continuing without validation")
246
- results["mcp_validation"] = {"error": str(e), "validation_skipped": True}
384
+ print_warning(f"Inventory MCP validation failed: {str(e)[:50]}... - continuing without validation")
385
+ results["inventory_mcp_validation"] = {"error": str(e), "validation_skipped": True}
386
+
387
+ # Fallback to generic MCP integration
388
+ try:
389
+ validation_result = asyncio.run(self.mcp_integrator.validate_inventory_operations(results))
390
+ results["mcp_validation"] = validation_result.to_dict()
391
+ except Exception as fallback_e:
392
+ results["mcp_validation"] = {"error": str(fallback_e), "validation_skipped": True}
247
393
 
248
394
  # Complete performance benchmark
249
395
  end_time = datetime.now()
@@ -292,6 +438,946 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
292
438
  results["errors"].append(error_msg)
293
439
  return results
294
440
 
441
+ def _collect_parallel(
442
+ self, resource_types: List[str], account_ids: List[str], include_costs: bool
443
+ ) -> Dict[str, Any]:
444
+ """
445
+ Collect inventory in parallel with enhanced performance monitoring.
446
+
447
+ Follows the same pattern as legacy implementation but with enterprise
448
+ performance monitoring and error handling.
449
+ """
450
+ results = {}
451
+ total_tasks = len(resource_types) * len(account_ids)
452
+ progress = ProgressTracker(total_tasks, "Collecting inventory")
453
+
454
+ with ThreadPoolExecutor(max_workers=10) as executor:
455
+ # Submit collection tasks
456
+ future_to_params = {}
457
+
458
+ for resource_type in resource_types:
459
+ for account_id in account_ids:
460
+ future = executor.submit(
461
+ self._collect_resource_for_account, resource_type, account_id, include_costs
462
+ )
463
+ future_to_params[future] = (resource_type, account_id)
464
+
465
+ # Collect results
466
+ for future in as_completed(future_to_params):
467
+ resource_type, account_id = future_to_params[future]
468
+ try:
469
+ resource_data = future.result()
470
+
471
+ if resource_type not in results:
472
+ results[resource_type] = {}
473
+
474
+ results[resource_type][account_id] = resource_data
475
+ progress.update(status=f"Completed {resource_type} for {account_id}")
476
+
477
+ except Exception as e:
478
+ logger.error(f"Failed to collect {resource_type} for account {account_id}: {e}")
479
+ progress.update(status=f"Failed {resource_type} for {account_id}")
480
+
481
+ progress.complete()
482
+ return results
483
+
484
+ def _collect_sequential(
485
+ self, resource_types: List[str], account_ids: List[str], include_costs: bool
486
+ ) -> Dict[str, Any]:
487
+ """
488
+ Collect inventory sequentially with enhanced error handling.
489
+
490
+ Follows the same pattern as legacy implementation but with enhanced
491
+ error handling and progress tracking.
492
+ """
493
+ results = {}
494
+ total_tasks = len(resource_types) * len(account_ids)
495
+ progress = ProgressTracker(total_tasks, "Collecting inventory")
496
+
497
+ for resource_type in resource_types:
498
+ results[resource_type] = {}
499
+
500
+ for account_id in account_ids:
501
+ try:
502
+ resource_data = self._collect_resource_for_account(resource_type, account_id, include_costs)
503
+ results[resource_type][account_id] = resource_data
504
+ progress.update(status=f"Completed {resource_type} for {account_id}")
505
+
506
+ except Exception as e:
507
+ logger.error(f"Failed to collect {resource_type} for account {account_id}: {e}")
508
+ results[resource_type][account_id] = {"error": str(e)}
509
+ progress.update(status=f"Failed {resource_type} for {account_id}")
510
+
511
+ progress.complete()
512
+ return results
513
+
514
+ def _collect_resource_for_account(self, resource_type: str, account_id: str, include_costs: bool) -> Dict[str, Any]:
515
+ """
516
+ Collect specific resource type for an account using REAL AWS API calls.
517
+
518
+ This method makes actual AWS API calls to discover resources, following
519
+ the proven patterns from the existing inventory modules.
520
+ """
521
+ try:
522
+ # Use active profile for AWS API calls
523
+ session = boto3.Session(profile_name=self.active_profile)
524
+
525
+ print_info(f"Collecting {resource_type} resources from account {account_id} using profile {self.active_profile}")
526
+
527
+ if resource_type == "ec2":
528
+ return self._collect_ec2_instances(session, account_id)
529
+ elif resource_type == "rds":
530
+ return self._collect_rds_instances(session, account_id)
531
+ elif resource_type == "s3":
532
+ return self._collect_s3_buckets(session, account_id)
533
+ elif resource_type == "lambda":
534
+ return self._collect_lambda_functions(session, account_id)
535
+ elif resource_type == "iam":
536
+ return self._collect_iam_resources(session, account_id)
537
+ elif resource_type == "vpc":
538
+ return self._collect_vpc_resources(session, account_id)
539
+ elif resource_type == "cloudformation":
540
+ return self._collect_cloudformation_stacks(session, account_id)
541
+ elif resource_type == "organizations":
542
+ return self._collect_organizations_data(session, account_id)
543
+ elif resource_type == "costs" and include_costs:
544
+ return self._collect_cost_data(session, account_id)
545
+ else:
546
+ print_warning(f"Resource type '{resource_type}' not supported yet")
547
+ return {
548
+ "resources": [],
549
+ "count": 0,
550
+ "resource_type": resource_type,
551
+ "account_id": account_id,
552
+ "collection_timestamp": datetime.now().isoformat(),
553
+ "warning": f"Resource type {resource_type} not implemented yet"
554
+ }
555
+
556
+ except Exception as e:
557
+ error_msg = f"Failed to collect {resource_type} for account {account_id}: {e}"
558
+ logger.error(error_msg)
559
+ print_error(error_msg)
560
+ return {
561
+ "error": str(e),
562
+ "resource_type": resource_type,
563
+ "account_id": account_id,
564
+ "collection_timestamp": datetime.now().isoformat(),
565
+ }
566
+
567
+ def _collect_ec2_instances(self, session: boto3.Session, account_id: str) -> Dict[str, Any]:
568
+ """Collect EC2 instances using real AWS API calls."""
569
+ try:
570
+ region = self.region or session.region_name or "us-east-1"
571
+ ec2_client = session.client("ec2", region_name=region)
572
+
573
+ print_info(f"Calling EC2 describe_instances API for account {account_id} in region {region}")
574
+
575
+ # Make real AWS API call with pagination support
576
+ instances = []
577
+ paginator = ec2_client.get_paginator('describe_instances')
578
+
579
+ for page in paginator.paginate():
580
+ for reservation in page.get("Reservations", []):
581
+ for instance in reservation.get("Instances", []):
582
+ # Extract instance data
583
+ instance_data = {
584
+ "instance_id": instance["InstanceId"],
585
+ "instance_type": instance["InstanceType"],
586
+ "state": instance["State"]["Name"],
587
+ "region": region,
588
+ "account_id": account_id,
589
+ "launch_time": instance.get("LaunchTime", "").isoformat() if instance.get("LaunchTime") else "",
590
+ "availability_zone": instance.get("Placement", {}).get("AvailabilityZone", ""),
591
+ "vpc_id": instance.get("VpcId", ""),
592
+ "subnet_id": instance.get("SubnetId", ""),
593
+ "private_ip_address": instance.get("PrivateIpAddress", ""),
594
+ "public_ip_address": instance.get("PublicIpAddress", ""),
595
+ "public_dns_name": instance.get("PublicDnsName", ""),
596
+ }
597
+
598
+ # Extract tags
599
+ tags = {}
600
+ name = "No Name Tag"
601
+ for tag in instance.get("Tags", []):
602
+ tags[tag["Key"]] = tag["Value"]
603
+ if tag["Key"] == "Name":
604
+ name = tag["Value"]
605
+
606
+ instance_data["tags"] = tags
607
+ instance_data["name"] = name
608
+
609
+ # Extract security groups
610
+ instance_data["security_groups"] = [
611
+ {"group_id": sg["GroupId"], "group_name": sg["GroupName"]}
612
+ for sg in instance.get("SecurityGroups", [])
613
+ ]
614
+
615
+ instances.append(instance_data)
616
+
617
+ print_success(f"Found {len(instances)} EC2 instances in account {account_id}")
618
+
619
+ return {
620
+ "instances": instances,
621
+ "count": len(instances),
622
+ "collection_timestamp": datetime.now().isoformat(),
623
+ "region": region,
624
+ "account_id": account_id,
625
+ }
626
+
627
+ except Exception as e:
628
+ print_error(f"Failed to collect EC2 instances: {e}")
629
+ raise
630
+
631
+ def _collect_rds_instances(self, session: boto3.Session, account_id: str) -> Dict[str, Any]:
632
+ """Collect RDS instances using real AWS API calls."""
633
+ try:
634
+ region = self.region or session.region_name or "us-east-1"
635
+ rds_client = session.client("rds", region_name=region)
636
+
637
+ print_info(f"Calling RDS describe_db_instances API for account {account_id} in region {region}")
638
+
639
+ # Make real AWS API call with pagination support
640
+ instances = []
641
+ paginator = rds_client.get_paginator('describe_db_instances')
642
+
643
+ for page in paginator.paginate():
644
+ for db_instance in page.get("DBInstances", []):
645
+ instance_data = {
646
+ "db_instance_identifier": db_instance["DBInstanceIdentifier"],
647
+ "engine": db_instance["Engine"],
648
+ "engine_version": db_instance["EngineVersion"],
649
+ "instance_class": db_instance["DBInstanceClass"],
650
+ "status": db_instance["DBInstanceStatus"],
651
+ "account_id": account_id,
652
+ "region": region,
653
+ "multi_az": db_instance.get("MultiAZ", False),
654
+ "storage_type": db_instance.get("StorageType", ""),
655
+ "allocated_storage": db_instance.get("AllocatedStorage", 0),
656
+ "endpoint": db_instance.get("Endpoint", {}).get("Address", "") if db_instance.get("Endpoint") else "",
657
+ "port": db_instance.get("Endpoint", {}).get("Port", 0) if db_instance.get("Endpoint") else 0,
658
+ "vpc_id": db_instance.get("DBSubnetGroup", {}).get("VpcId", "") if db_instance.get("DBSubnetGroup") else "",
659
+ }
660
+
661
+ instances.append(instance_data)
662
+
663
+ print_success(f"Found {len(instances)} RDS instances in account {account_id}")
664
+
665
+ return {
666
+ "instances": instances,
667
+ "count": len(instances),
668
+ "collection_timestamp": datetime.now().isoformat(),
669
+ "region": region,
670
+ "account_id": account_id,
671
+ }
672
+
673
+ except Exception as e:
674
+ print_error(f"Failed to collect RDS instances: {e}")
675
+ raise
676
+
677
+ def _collect_s3_buckets(self, session: boto3.Session, account_id: str) -> Dict[str, Any]:
678
+ """Collect S3 buckets using real AWS API calls."""
679
+ try:
680
+ s3_client = session.client("s3")
681
+
682
+ print_info(f"Calling S3 list_buckets API for account {account_id}")
683
+
684
+ # Make real AWS API call - S3 buckets are global
685
+ response = s3_client.list_buckets()
686
+ buckets = []
687
+
688
+ for bucket in response.get("Buckets", []):
689
+ bucket_data = {
690
+ "name": bucket["Name"],
691
+ "creation_date": bucket["CreationDate"].isoformat(),
692
+ "account_id": account_id,
693
+ }
694
+
695
+ # Try to get bucket location (region)
696
+ try:
697
+ location_response = s3_client.get_bucket_location(Bucket=bucket["Name"])
698
+ bucket_region = location_response.get("LocationConstraint")
699
+ if bucket_region is None:
700
+ bucket_region = "us-east-1" # Default for US Standard
701
+ bucket_data["region"] = bucket_region
702
+ except Exception as e:
703
+ logger.warning(f"Could not get location for bucket {bucket['Name']}: {e}")
704
+ bucket_data["region"] = "unknown"
705
+
706
+ # Try to get bucket versioning
707
+ try:
708
+ versioning_response = s3_client.get_bucket_versioning(Bucket=bucket["Name"])
709
+ bucket_data["versioning"] = versioning_response.get("Status", "Suspended")
710
+ except Exception as e:
711
+ logger.warning(f"Could not get versioning for bucket {bucket['Name']}: {e}")
712
+ bucket_data["versioning"] = "unknown"
713
+
714
+ buckets.append(bucket_data)
715
+
716
+ print_success(f"Found {len(buckets)} S3 buckets in account {account_id}")
717
+
718
+ return {
719
+ "buckets": buckets,
720
+ "count": len(buckets),
721
+ "collection_timestamp": datetime.now().isoformat(),
722
+ "account_id": account_id,
723
+ }
724
+
725
+ except Exception as e:
726
+ print_error(f"Failed to collect S3 buckets: {e}")
727
+ raise
728
+
729
+ def _collect_lambda_functions(self, session: boto3.Session, account_id: str) -> Dict[str, Any]:
730
+ """Collect Lambda functions using real AWS API calls."""
731
+ try:
732
+ region = self.region or session.region_name or "us-east-1"
733
+ lambda_client = session.client("lambda", region_name=region)
734
+
735
+ print_info(f"Calling Lambda list_functions API for account {account_id} in region {region}")
736
+
737
+ # Make real AWS API call with pagination support
738
+ functions = []
739
+ paginator = lambda_client.get_paginator('list_functions')
740
+
741
+ for page in paginator.paginate():
742
+ for function in page.get("Functions", []):
743
+ function_data = {
744
+ "function_name": function["FunctionName"],
745
+ "runtime": function.get("Runtime", ""),
746
+ "handler": function.get("Handler", ""),
747
+ "code_size": function.get("CodeSize", 0),
748
+ "description": function.get("Description", ""),
749
+ "timeout": function.get("Timeout", 0),
750
+ "memory_size": function.get("MemorySize", 0),
751
+ "last_modified": function.get("LastModified", ""),
752
+ "role": function.get("Role", ""),
753
+ "account_id": account_id,
754
+ "region": region,
755
+ }
756
+
757
+ functions.append(function_data)
758
+
759
+ print_success(f"Found {len(functions)} Lambda functions in account {account_id}")
760
+
761
+ return {
762
+ "functions": functions,
763
+ "count": len(functions),
764
+ "collection_timestamp": datetime.now().isoformat(),
765
+ "region": region,
766
+ "account_id": account_id,
767
+ }
768
+
769
+ except Exception as e:
770
+ print_error(f"Failed to collect Lambda functions: {e}")
771
+ raise
772
+
773
+ def _collect_iam_resources(self, session: boto3.Session, account_id: str) -> Dict[str, Any]:
774
+ """Collect IAM resources using real AWS API calls."""
775
+ try:
776
+ iam_client = session.client("iam")
777
+
778
+ print_info(f"Calling IAM APIs for account {account_id}")
779
+
780
+ resources = {"users": [], "roles": [], "policies": [], "groups": []}
781
+
782
+ # Collect users
783
+ paginator = iam_client.get_paginator('list_users')
784
+ for page in paginator.paginate():
785
+ for user in page.get("Users", []):
786
+ user_data = {
787
+ "user_name": user["UserName"],
788
+ "user_id": user["UserId"],
789
+ "arn": user["Arn"],
790
+ "create_date": user["CreateDate"].isoformat(),
791
+ "path": user["Path"],
792
+ "account_id": account_id,
793
+ }
794
+ resources["users"].append(user_data)
795
+
796
+ # Collect roles
797
+ paginator = iam_client.get_paginator('list_roles')
798
+ for page in paginator.paginate():
799
+ for role in page.get("Roles", []):
800
+ role_data = {
801
+ "role_name": role["RoleName"],
802
+ "role_id": role["RoleId"],
803
+ "arn": role["Arn"],
804
+ "create_date": role["CreateDate"].isoformat(),
805
+ "path": role["Path"],
806
+ "account_id": account_id,
807
+ }
808
+ resources["roles"].append(role_data)
809
+
810
+ total_count = len(resources["users"]) + len(resources["roles"])
811
+ print_success(f"Found {total_count} IAM resources in account {account_id}")
812
+
813
+ return {
814
+ "resources": resources,
815
+ "count": total_count,
816
+ "collection_timestamp": datetime.now().isoformat(),
817
+ "account_id": account_id,
818
+ }
819
+
820
+ except Exception as e:
821
+ print_error(f"Failed to collect IAM resources: {e}")
822
+ raise
823
+
824
+ def _collect_vpc_resources(self, session: boto3.Session, account_id: str) -> Dict[str, Any]:
825
+ """Collect VPC resources using real AWS API calls."""
826
+ try:
827
+ region = self.region or session.region_name or "us-east-1"
828
+ ec2_client = session.client("ec2", region_name=region)
829
+
830
+ print_info(f"Calling EC2 VPC APIs for account {account_id} in region {region}")
831
+
832
+ vpcs = []
833
+ paginator = ec2_client.get_paginator('describe_vpcs')
834
+
835
+ for page in paginator.paginate():
836
+ for vpc in page.get("Vpcs", []):
837
+ vpc_data = {
838
+ "vpc_id": vpc["VpcId"],
839
+ "cidr_block": vpc["CidrBlock"],
840
+ "state": vpc["State"],
841
+ "is_default": vpc.get("IsDefault", False),
842
+ "instance_tenancy": vpc.get("InstanceTenancy", ""),
843
+ "account_id": account_id,
844
+ "region": region,
845
+ }
846
+
847
+ # Extract tags
848
+ tags = {}
849
+ name = "No Name Tag"
850
+ for tag in vpc.get("Tags", []):
851
+ tags[tag["Key"]] = tag["Value"]
852
+ if tag["Key"] == "Name":
853
+ name = tag["Value"]
854
+
855
+ vpc_data["tags"] = tags
856
+ vpc_data["name"] = name
857
+
858
+ vpcs.append(vpc_data)
859
+
860
+ print_success(f"Found {len(vpcs)} VPCs in account {account_id}")
861
+
862
+ return {
863
+ "vpcs": vpcs,
864
+ "count": len(vpcs),
865
+ "collection_timestamp": datetime.now().isoformat(),
866
+ "region": region,
867
+ "account_id": account_id,
868
+ }
869
+
870
+ except Exception as e:
871
+ print_error(f"Failed to collect VPC resources: {e}")
872
+ raise
873
+
874
+ def _collect_cloudformation_stacks(self, session: boto3.Session, account_id: str) -> Dict[str, Any]:
875
+ """Collect CloudFormation stacks using real AWS API calls."""
876
+ try:
877
+ region = self.region or session.region_name or "us-east-1"
878
+ cf_client = session.client("cloudformation", region_name=region)
879
+
880
+ print_info(f"Calling CloudFormation describe_stacks API for account {account_id} in region {region}")
881
+
882
+ stacks = []
883
+ paginator = cf_client.get_paginator('describe_stacks')
884
+
885
+ for page in paginator.paginate():
886
+ for stack in page.get("Stacks", []):
887
+ stack_data = {
888
+ "stack_name": stack["StackName"],
889
+ "stack_id": stack["StackId"],
890
+ "stack_status": stack["StackStatus"],
891
+ "creation_time": stack["CreationTime"].isoformat(),
892
+ "description": stack.get("Description", ""),
893
+ "account_id": account_id,
894
+ "region": region,
895
+ }
896
+
897
+ if "LastUpdatedTime" in stack:
898
+ stack_data["last_updated_time"] = stack["LastUpdatedTime"].isoformat()
899
+
900
+ stacks.append(stack_data)
901
+
902
+ print_success(f"Found {len(stacks)} CloudFormation stacks in account {account_id}")
903
+
904
+ return {
905
+ "stacks": stacks,
906
+ "count": len(stacks),
907
+ "collection_timestamp": datetime.now().isoformat(),
908
+ "region": region,
909
+ "account_id": account_id,
910
+ }
911
+
912
+ except Exception as e:
913
+ print_error(f"Failed to collect CloudFormation stacks: {e}")
914
+ raise
915
+
916
+ def _collect_cost_data(self, session: boto3.Session, account_id: str) -> Dict[str, Any]:
917
+ """Collect cost data using real AWS Cost Explorer API calls."""
918
+ try:
919
+ # Note: Cost Explorer requires specific billing permissions
920
+ print_warning("Cost data collection requires AWS Cost Explorer permissions")
921
+ print_info(f"Attempting to collect cost data for account {account_id}")
922
+
923
+ # For now, return placeholder - would need billing profile for actual cost data
924
+ return {
925
+ "monthly_costs": {
926
+ "note": "Cost data collection requires proper billing permissions and profile",
927
+ "suggestion": "Use BILLING_PROFILE environment variable or --profile with billing access"
928
+ },
929
+ "account_id": account_id,
930
+ "collection_timestamp": datetime.now().isoformat(),
931
+ }
932
+
933
+ except Exception as e:
934
+ print_error(f"Failed to collect cost data: {e}")
935
+ raise
936
+
937
+ def _collect_organizations_data(self, session: boto3.Session, account_id: str) -> Dict[str, Any]:
938
+ """Collect AWS Organizations data using existing organizations discovery module."""
939
+ try:
940
+ print_info(f"Collecting Organizations data for account {account_id}")
941
+
942
+ # Use the session's profile name for organizations discovery
943
+ profile_name = session.profile_name or self.active_profile
944
+
945
+ org_client = session.client('organizations', region_name='us-east-1') # Organizations is always us-east-1
946
+
947
+ # Collect organization structure and accounts
948
+ organizations_data = {
949
+ "organization_info": {},
950
+ "accounts": [],
951
+ "organizational_units": [],
952
+ "resource_type": "organizations",
953
+ "account_id": account_id,
954
+ "collection_timestamp": datetime.now().isoformat()
955
+ }
956
+
957
+ try:
958
+ # Get organization details
959
+ org_response = org_client.describe_organization()
960
+ organizations_data["organization_info"] = org_response.get("Organization", {})
961
+
962
+ # Get all accounts in the organization
963
+ paginator = org_client.get_paginator('list_accounts')
964
+ accounts = []
965
+ for page in paginator.paginate():
966
+ accounts.extend(page.get('Accounts', []))
967
+
968
+ organizations_data["accounts"] = accounts
969
+ organizations_data["count"] = len(accounts)
970
+
971
+ # Get organizational units
972
+ try:
973
+ roots_response = org_client.list_roots()
974
+ for root in roots_response.get('Roots', []):
975
+ ou_paginator = org_client.get_paginator('list_organizational_units_for_parent')
976
+ for ou_page in ou_paginator.paginate(ParentId=root['Id']):
977
+ organizations_data["organizational_units"].extend(ou_page.get('OrganizationalUnits', []))
978
+ except Exception as ou_e:
979
+ print_warning(f"Could not collect organizational units: {ou_e}")
980
+ organizations_data["organizational_units"] = []
981
+
982
+ print_success(f"Successfully collected {len(accounts)} accounts from organization")
983
+
984
+ except Exception as org_e:
985
+ print_warning(f"Organization data collection limited: {org_e}")
986
+ # Try to collect at least basic account info if not in an organization
987
+ try:
988
+ sts_client = session.client('sts')
989
+ caller_identity = sts_client.get_caller_identity()
990
+ organizations_data["accounts"] = [{
991
+ "Id": caller_identity.get("Account"),
992
+ "Name": f"Account-{caller_identity.get('Account')}",
993
+ "Status": "ACTIVE",
994
+ "JoinedMethod": "STANDALONE"
995
+ }]
996
+ organizations_data["count"] = 1
997
+ print_info("Collected standalone account information")
998
+ except Exception as sts_e:
999
+ print_error(f"Could not collect account information: {sts_e}")
1000
+ organizations_data["count"] = 0
1001
+
1002
+ return organizations_data
1003
+
1004
+ except Exception as e:
1005
+ print_error(f"Failed to collect organizations data: {e}")
1006
+ raise
1007
+
1008
+ def _generate_summary(self, resource_data: Dict[str, Any]) -> Dict[str, Any]:
1009
+ """
1010
+ Generate comprehensive summary statistics from collected data.
1011
+
1012
+ Enhanced implementation with better error handling and metrics.
1013
+ """
1014
+ summary = {
1015
+ "total_resources": 0,
1016
+ "resources_by_type": {},
1017
+ "resources_by_account": {},
1018
+ "collection_status": "completed",
1019
+ "errors": [],
1020
+ "collection_summary": {
1021
+ "successful_collections": 0,
1022
+ "failed_collections": 0,
1023
+ "accounts_processed": set(),
1024
+ "resource_types_processed": set(),
1025
+ }
1026
+ }
1027
+
1028
+ for resource_type, accounts_data in resource_data.items():
1029
+ type_count = 0
1030
+ summary["collection_summary"]["resource_types_processed"].add(resource_type)
1031
+
1032
+ for account_id, account_data in accounts_data.items():
1033
+ summary["collection_summary"]["accounts_processed"].add(account_id)
1034
+
1035
+ if "error" in account_data:
1036
+ summary["errors"].append(f"{resource_type}/{account_id}: {account_data['error']}")
1037
+ summary["collection_summary"]["failed_collections"] += 1
1038
+ continue
1039
+
1040
+ summary["collection_summary"]["successful_collections"] += 1
1041
+
1042
+ # Count resources based on type
1043
+ account_count = account_data.get("count", 0)
1044
+ if account_count == 0:
1045
+ # Try to calculate from actual resource lists
1046
+ if resource_type == "ec2":
1047
+ account_count = len(account_data.get("instances", []))
1048
+ elif resource_type == "rds":
1049
+ account_count = len(account_data.get("instances", []))
1050
+ elif resource_type == "s3":
1051
+ account_count = len(account_data.get("buckets", []))
1052
+ elif resource_type == "lambda":
1053
+ account_count = len(account_data.get("functions", []))
1054
+ else:
1055
+ account_count = len(account_data.get("resources", []))
1056
+
1057
+ type_count += account_count
1058
+
1059
+ if account_id not in summary["resources_by_account"]:
1060
+ summary["resources_by_account"][account_id] = 0
1061
+ summary["resources_by_account"][account_id] += account_count
1062
+
1063
+ summary["resources_by_type"][resource_type] = type_count
1064
+ summary["total_resources"] += type_count
1065
+
1066
+ # Convert sets to lists for JSON serialization
1067
+ summary["collection_summary"]["accounts_processed"] = list(summary["collection_summary"]["accounts_processed"])
1068
+ summary["collection_summary"]["resource_types_processed"] = list(summary["collection_summary"]["resource_types_processed"])
1069
+
1070
+ # Update collection status based on errors
1071
+ if summary["errors"]:
1072
+ if summary["collection_summary"]["successful_collections"] == 0:
1073
+ summary["collection_status"] = "failed"
1074
+ else:
1075
+ summary["collection_status"] = "completed_with_errors"
1076
+
1077
+ return summary
1078
+
1079
+ def export_inventory_results(
1080
+ self,
1081
+ results: Dict[str, Any],
1082
+ export_format: str = "json",
1083
+ output_file: Optional[str] = None
1084
+ ) -> str:
1085
+ """
1086
+ Export inventory results to multiple formats following proven finops patterns.
1087
+
1088
+ Args:
1089
+ results: Inventory results dictionary
1090
+ export_format: Export format (json, csv, markdown, pdf, yaml)
1091
+ output_file: Optional output file path
1092
+
1093
+ Returns:
1094
+ Export file path or formatted string content
1095
+ """
1096
+ import json
1097
+ import csv
1098
+ from datetime import datetime
1099
+ from pathlib import Path
1100
+
1101
+ # Determine output file path
1102
+ if not output_file:
1103
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1104
+ output_file = f"/Volumes/Working/1xOps/CloudOps-Runbooks/tmp/inventory_export_{timestamp}.{export_format}"
1105
+
1106
+ # Ensure tmp directory exists
1107
+ Path(output_file).parent.mkdir(parents=True, exist_ok=True)
1108
+
1109
+ try:
1110
+ if export_format.lower() == "json":
1111
+ return self._export_json(results, output_file)
1112
+ elif export_format.lower() == "csv":
1113
+ return self._export_csv(results, output_file)
1114
+ elif export_format.lower() == "markdown":
1115
+ return self._export_markdown(results, output_file)
1116
+ elif export_format.lower() == "yaml":
1117
+ return self._export_yaml(results, output_file)
1118
+ elif export_format.lower() == "pdf":
1119
+ return self._export_pdf(results, output_file)
1120
+ else:
1121
+ raise ValueError(f"Unsupported export format: {export_format}")
1122
+
1123
+ except Exception as e:
1124
+ error_msg = f"Export failed for format {export_format}: {e}"
1125
+ print_error(error_msg)
1126
+ logger.error(error_msg)
1127
+ raise
1128
+
1129
+ def _export_json(self, results: Dict[str, Any], output_file: str) -> str:
1130
+ """Export results to JSON format."""
1131
+ with open(output_file, 'w') as f:
1132
+ json.dump(results, f, indent=2, default=str)
1133
+
1134
+ print_success(f"Inventory exported to JSON: {output_file}")
1135
+ return output_file
1136
+
1137
+ def _export_csv(self, results: Dict[str, Any], output_file: str) -> str:
1138
+ """Export results to CSV format with real AWS data structure."""
1139
+ import csv
1140
+
1141
+ with open(output_file, 'w', newline='') as f:
1142
+ writer = csv.writer(f)
1143
+
1144
+ # Write header
1145
+ writer.writerow(["Account", "Region", "Resource Type", "Resource ID", "Name", "Status", "Additional Info"])
1146
+
1147
+ # Write data rows from real AWS resource structure
1148
+ resource_data = results.get("resources", {})
1149
+
1150
+ for resource_type, accounts_data in resource_data.items():
1151
+ for account_id, account_data in accounts_data.items():
1152
+ if "error" in account_data:
1153
+ # Handle error cases
1154
+ writer.writerow([
1155
+ account_id,
1156
+ account_data.get("region", "unknown"),
1157
+ resource_type,
1158
+ "",
1159
+ "",
1160
+ "ERROR",
1161
+ account_data.get("error", "")
1162
+ ])
1163
+ continue
1164
+
1165
+ account_region = account_data.get("region", "unknown")
1166
+
1167
+ # Handle different resource types with their specific data structures
1168
+ if resource_type == "ec2" and "instances" in account_data:
1169
+ for instance in account_data["instances"]:
1170
+ writer.writerow([
1171
+ account_id,
1172
+ instance.get("region", account_region),
1173
+ "ec2-instance",
1174
+ instance.get("instance_id", ""),
1175
+ instance.get("name", "No Name Tag"),
1176
+ instance.get("state", ""),
1177
+ f"Type: {instance.get('instance_type', '')}, AZ: {instance.get('availability_zone', '')}"
1178
+ ])
1179
+
1180
+ elif resource_type == "rds" and "instances" in account_data:
1181
+ for instance in account_data["instances"]:
1182
+ writer.writerow([
1183
+ account_id,
1184
+ instance.get("region", account_region),
1185
+ "rds-instance",
1186
+ instance.get("db_instance_identifier", ""),
1187
+ instance.get("db_instance_identifier", ""),
1188
+ instance.get("status", ""),
1189
+ f"Engine: {instance.get('engine', '')}, Class: {instance.get('instance_class', '')}"
1190
+ ])
1191
+
1192
+ elif resource_type == "s3" and "buckets" in account_data:
1193
+ for bucket in account_data["buckets"]:
1194
+ writer.writerow([
1195
+ account_id,
1196
+ bucket.get("region", account_region),
1197
+ "s3-bucket",
1198
+ bucket.get("name", ""),
1199
+ bucket.get("name", ""),
1200
+ "",
1201
+ f"Created: {bucket.get('creation_date', '')}"
1202
+ ])
1203
+
1204
+ elif resource_type == "lambda" and "functions" in account_data:
1205
+ for function in account_data["functions"]:
1206
+ writer.writerow([
1207
+ account_id,
1208
+ function.get("region", account_region),
1209
+ "lambda-function",
1210
+ function.get("function_name", ""),
1211
+ function.get("function_name", ""),
1212
+ "",
1213
+ f"Runtime: {function.get('runtime', '')}, Memory: {function.get('memory_size', '')}MB"
1214
+ ])
1215
+
1216
+ elif resource_type == "iam" and "resources" in account_data:
1217
+ iam_resources = account_data["resources"]
1218
+ for user in iam_resources.get("users", []):
1219
+ writer.writerow([
1220
+ account_id,
1221
+ "global",
1222
+ "iam-user",
1223
+ user.get("user_name", ""),
1224
+ user.get("user_name", ""),
1225
+ "",
1226
+ f"ARN: {user.get('arn', '')}"
1227
+ ])
1228
+ for role in iam_resources.get("roles", []):
1229
+ writer.writerow([
1230
+ account_id,
1231
+ "global",
1232
+ "iam-role",
1233
+ role.get("role_name", ""),
1234
+ role.get("role_name", ""),
1235
+ "",
1236
+ f"ARN: {role.get('arn', '')}"
1237
+ ])
1238
+
1239
+ elif resource_type == "vpc" and "vpcs" in account_data:
1240
+ for vpc in account_data["vpcs"]:
1241
+ writer.writerow([
1242
+ account_id,
1243
+ vpc.get("region", account_region),
1244
+ "vpc",
1245
+ vpc.get("vpc_id", ""),
1246
+ vpc.get("name", "No Name Tag"),
1247
+ vpc.get("state", ""),
1248
+ f"CIDR: {vpc.get('cidr_block', '')}, Default: {vpc.get('is_default', False)}"
1249
+ ])
1250
+
1251
+ elif resource_type == "cloudformation" and "stacks" in account_data:
1252
+ for stack in account_data["stacks"]:
1253
+ writer.writerow([
1254
+ account_id,
1255
+ stack.get("region", account_region),
1256
+ "cloudformation-stack",
1257
+ stack.get("stack_name", ""),
1258
+ stack.get("stack_name", ""),
1259
+ stack.get("stack_status", ""),
1260
+ f"Created: {stack.get('creation_time', '')}"
1261
+ ])
1262
+
1263
+ # Handle cases where no specific resources were found but collection was successful
1264
+ elif account_data.get("count", 0) == 0:
1265
+ writer.writerow([
1266
+ account_id,
1267
+ account_region,
1268
+ resource_type,
1269
+ "",
1270
+ "",
1271
+ "NO_RESOURCES",
1272
+ f"No {resource_type} resources found"
1273
+ ])
1274
+
1275
+ print_success(f"Inventory exported to CSV: {output_file}")
1276
+ return output_file
1277
+
1278
+ def _export_markdown(self, results: Dict[str, Any], output_file: str) -> str:
1279
+ """Export results to Markdown format with tables."""
1280
+ content = []
1281
+ content.append("# AWS Inventory Report")
1282
+ content.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
1283
+ content.append("")
1284
+
1285
+ # Summary section
1286
+ total_resources = sum(
1287
+ len(resources)
1288
+ for account_data in results.get("accounts", {}).values()
1289
+ for region_data in account_data.get("regions", {}).values()
1290
+ for resources in region_data.get("resources", {}).values()
1291
+ )
1292
+
1293
+ content.append("## Summary")
1294
+ content.append(f"- Total Accounts: {len(results.get('accounts', {}))}")
1295
+ content.append(f"- Total Resources: {total_resources}")
1296
+ content.append("")
1297
+
1298
+ # Detailed inventory
1299
+ content.append("## Detailed Inventory")
1300
+ content.append("")
1301
+ content.append("| Account | Region | Resource Type | Resource ID | Name | Status |")
1302
+ content.append("|---------|--------|---------------|-------------|------|--------|")
1303
+
1304
+ for account_id, account_data in results.get("accounts", {}).items():
1305
+ for region, region_data in account_data.get("regions", {}).items():
1306
+ for resource_type, resources in region_data.get("resources", {}).items():
1307
+ for resource in resources:
1308
+ content.append(f"| {account_id} | {region} | {resource_type} | {resource.get('id', '')} | {resource.get('name', '')} | {resource.get('state', '')} |")
1309
+
1310
+ with open(output_file, 'w') as f:
1311
+ f.write('\n'.join(content))
1312
+
1313
+ print_success(f"Inventory exported to Markdown: {output_file}")
1314
+ return output_file
1315
+
1316
+ def _export_yaml(self, results: Dict[str, Any], output_file: str) -> str:
1317
+ """Export results to YAML format."""
1318
+ try:
1319
+ import yaml
1320
+ except ImportError:
1321
+ print_error("PyYAML not available. Install with: pip install pyyaml")
1322
+ raise
1323
+
1324
+ with open(output_file, 'w') as f:
1325
+ yaml.dump(results, f, default_flow_style=False, sort_keys=False)
1326
+
1327
+ print_success(f"Inventory exported to YAML: {output_file}")
1328
+ return output_file
1329
+
1330
+ def _export_pdf(self, results: Dict[str, Any], output_file: str) -> str:
1331
+ """Export results to executive PDF report."""
1332
+ try:
1333
+ from reportlab.lib.pagesizes import letter, A4
1334
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
1335
+ from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
1336
+ from reportlab.lib.units import inch
1337
+ from reportlab.lib import colors
1338
+ except ImportError:
1339
+ # Graceful fallback to markdown if reportlab not available
1340
+ print_warning("ReportLab not available, exporting to markdown instead")
1341
+ return self._export_markdown(results, output_file.replace('.pdf', '.md'))
1342
+
1343
+ doc = SimpleDocTemplate(output_file, pagesize=A4)
1344
+ styles = getSampleStyleSheet()
1345
+ story = []
1346
+
1347
+ # Title
1348
+ title_style = ParagraphStyle(
1349
+ 'CustomTitle',
1350
+ parent=styles['Heading1'],
1351
+ fontSize=24,
1352
+ spaceAfter=30,
1353
+ textColor=colors.darkblue
1354
+ )
1355
+ story.append(Paragraph("AWS Inventory Report", title_style))
1356
+ story.append(Spacer(1, 20))
1357
+
1358
+ # Executive Summary
1359
+ story.append(Paragraph("Executive Summary", styles['Heading2']))
1360
+
1361
+ total_resources = sum(
1362
+ len(resources)
1363
+ for account_data in results.get("accounts", {}).values()
1364
+ for region_data in account_data.get("regions", {}).values()
1365
+ for resources in region_data.get("resources", {}).values()
1366
+ )
1367
+
1368
+ summary_text = f"""
1369
+ This report provides a comprehensive inventory of AWS resources across {len(results.get('accounts', {}))} accounts.
1370
+ A total of {total_resources} resources were discovered and catalogued.
1371
+ """
1372
+ story.append(Paragraph(summary_text, styles['Normal']))
1373
+ story.append(Spacer(1, 20))
1374
+
1375
+ # Build the PDF
1376
+ doc.build(story)
1377
+
1378
+ print_success(f"Inventory exported to PDF: {output_file}")
1379
+ return output_file
1380
+
295
1381
 
296
1382
  # Legacy compatibility class - maintain backward compatibility
297
1383
  class InventoryCollector(EnhancedInventoryCollector):
@@ -390,54 +1476,51 @@ class InventoryCollector(EnhancedInventoryCollector):
390
1476
  this would delegate to specific resource collectors.
391
1477
  """
392
1478
  # Mock implementation - replace with actual collectors
393
- import random
394
1479
  import time
395
1480
 
396
- # Simulate collection time
397
- time.sleep(random.uniform(0.1, 0.5))
1481
+ # Deterministic collection timing
1482
+ time.sleep(0.2) # Fixed 200ms delay for testing
398
1483
 
399
- # Generate mock data based on resource type
400
- if resource_type == "ec2":
401
- return {
402
- "instances": [
403
- {
404
- "instance_id": f"i-{random.randint(100000000000, 999999999999):012x}",
405
- "instance_type": random.choice(["t3.micro", "t3.small", "m5.large"]),
406
- "state": random.choice(["running", "stopped"]),
407
- "region": self.region or "us-east-1",
408
- "account_id": account_id,
409
- "tags": {"Environment": random.choice(["dev", "staging", "prod"])},
410
- }
411
- for _ in range(random.randint(0, 5))
412
- ],
413
- "count": random.randint(0, 5),
414
- }
415
- elif resource_type == "rds":
416
- return {
417
- "instances": [
418
- {
419
- "db_instance_identifier": f"db-{random.randint(1000, 9999)}",
420
- "engine": random.choice(["mysql", "postgres", "aurora"]),
421
- "instance_class": random.choice(["db.t3.micro", "db.t3.small"]),
422
- "status": "available",
423
- "account_id": account_id,
424
- }
425
- for _ in range(random.randint(0, 3))
426
- ],
427
- "count": random.randint(0, 3),
428
- }
429
- elif resource_type == "s3":
1484
+ # REMOVED: Mock data generation violates enterprise standards
1485
+ # Use real AWS API calls with proper authentication and error handling
1486
+ try:
1487
+ if resource_type == "ec2":
1488
+ # TODO: Implement real EC2 API call
1489
+ # ec2_client = self.session.client('ec2', region_name=self.region)
1490
+ # response = ec2_client.describe_instances()
1491
+ return {
1492
+ "instances": [], # Replace with real EC2 API response processing
1493
+ "count": 0,
1494
+ "account_id": account_id,
1495
+ "region": self.region or "us-east-1"
1496
+ }
1497
+ elif resource_type == "rds":
1498
+ # TODO: Implement real RDS API call
1499
+ # rds_client = self.session.client('rds', region_name=self.region)
1500
+ # response = rds_client.describe_db_instances()
1501
+ return {
1502
+ "instances": [], # Replace with real RDS API response processing
1503
+ "count": 0,
1504
+ "account_id": account_id,
1505
+ "region": self.region or "us-east-1"
1506
+ }
1507
+ elif resource_type == "s3":
1508
+ # TODO: Implement real S3 API call
1509
+ # s3_client = self.session.client('s3')
1510
+ # response = s3_client.list_buckets()
1511
+ return {
1512
+ "buckets": [], # Replace with real S3 API response processing
1513
+ "count": 0,
1514
+ "account_id": account_id,
1515
+ "region": self.region or "us-east-1"
1516
+ }
1517
+ except Exception as e:
1518
+ # Proper error handling for AWS API failures
430
1519
  return {
431
- "buckets": [
432
- {
433
- "name": f"bucket-{account_id}-{random.randint(1000, 9999)}",
434
- "creation_date": datetime.now().isoformat(),
435
- "region": self.region or "us-east-1",
436
- "account_id": account_id,
437
- }
438
- for _ in range(random.randint(1, 10))
439
- ],
440
- "count": random.randint(1, 10),
1520
+ "error": str(e),
1521
+ "resource_type": resource_type,
1522
+ "account_id": account_id,
1523
+ "count": 0
441
1524
  }
442
1525
  else:
443
1526
  return {"resources": [], "count": 0, "resource_type": resource_type, "account_id": account_id}