runbooks 1.0.0__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 (77) 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/tests/test_weight_configuration.ts +449 -0
  6. runbooks/cfat/weight_config.ts +574 -0
  7. runbooks/common/__init__.py +26 -9
  8. runbooks/common/aws_pricing.py +1070 -105
  9. runbooks/common/date_utils.py +115 -0
  10. runbooks/common/enhanced_exception_handler.py +10 -7
  11. runbooks/common/mcp_cost_explorer_integration.py +5 -4
  12. runbooks/common/profile_utils.py +76 -115
  13. runbooks/common/rich_utils.py +3 -3
  14. runbooks/finops/dashboard_runner.py +47 -28
  15. runbooks/finops/ebs_optimizer.py +56 -9
  16. runbooks/finops/enhanced_trend_visualization.py +7 -2
  17. runbooks/finops/finops_dashboard.py +6 -5
  18. runbooks/finops/iam_guidance.py +6 -1
  19. runbooks/finops/nat_gateway_optimizer.py +46 -27
  20. runbooks/finops/tests/test_integration.py +3 -1
  21. runbooks/finops/vpc_cleanup_optimizer.py +22 -29
  22. runbooks/inventory/core/collector.py +51 -28
  23. runbooks/inventory/discovery.md +197 -247
  24. runbooks/inventory/inventory_modules.py +2 -2
  25. runbooks/inventory/list_ec2_instances.py +3 -3
  26. runbooks/inventory/organizations_discovery.py +13 -8
  27. runbooks/inventory/unified_validation_engine.py +2 -15
  28. runbooks/main.py +74 -32
  29. runbooks/operate/base.py +9 -6
  30. runbooks/operate/deployment_framework.py +5 -4
  31. runbooks/operate/deployment_validator.py +6 -5
  32. runbooks/operate/mcp_integration.py +6 -5
  33. runbooks/operate/networking_cost_heatmap.py +17 -13
  34. runbooks/operate/vpc_operations.py +52 -12
  35. runbooks/remediation/base.py +3 -1
  36. runbooks/remediation/commons.py +5 -5
  37. runbooks/remediation/commvault_ec2_analysis.py +66 -18
  38. runbooks/remediation/config/accounts_example.json +31 -0
  39. runbooks/remediation/multi_account.py +120 -7
  40. runbooks/remediation/remediation_cli.py +710 -0
  41. runbooks/remediation/universal_account_discovery.py +377 -0
  42. runbooks/security/compliance_automation_engine.py +99 -20
  43. runbooks/security/config/__init__.py +24 -0
  44. runbooks/security/config/compliance_config.py +255 -0
  45. runbooks/security/config/compliance_weights_example.json +22 -0
  46. runbooks/security/config_template_generator.py +500 -0
  47. runbooks/security/security_cli.py +377 -0
  48. runbooks/validation/cli.py +8 -7
  49. runbooks/validation/comprehensive_2way_validator.py +26 -15
  50. runbooks/validation/mcp_validator.py +62 -8
  51. runbooks/vpc/config.py +32 -7
  52. runbooks/vpc/cross_account_session.py +5 -1
  53. runbooks/vpc/heatmap_engine.py +21 -14
  54. runbooks/vpc/mcp_no_eni_validator.py +115 -36
  55. runbooks/vpc/runbooks_adapter.py +33 -12
  56. runbooks/vpc/tests/conftest.py +4 -2
  57. runbooks/vpc/tests/test_cost_engine.py +3 -1
  58. {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/METADATA +1 -1
  59. {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/RECORD +63 -65
  60. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  61. runbooks/finops/runbooks.security.report_generator.log +0 -0
  62. runbooks/finops/runbooks.security.run_script.log +0 -0
  63. runbooks/finops/runbooks.security.security_export.log +0 -0
  64. runbooks/finops/tests/results_test_finops_dashboard.xml +0 -1
  65. runbooks/inventory/artifacts/scale-optimize-status.txt +0 -12
  66. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  67. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  68. runbooks/inventory/runbooks.security.run_script.log +0 -0
  69. runbooks/inventory/runbooks.security.security_export.log +0 -0
  70. runbooks/vpc/runbooks.inventory.organizations_discovery.log +0 -0
  71. runbooks/vpc/runbooks.security.report_generator.log +0 -0
  72. runbooks/vpc/runbooks.security.run_script.log +0 -0
  73. runbooks/vpc/runbooks.security.security_export.log +0 -0
  74. {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/WHEEL +0 -0
  75. {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/entry_points.txt +0 -0
  76. {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/licenses/LICENSE +0 -0
  77. {runbooks-1.0.0.dist-info → runbooks-1.0.1.dist-info}/top_level.txt +0 -0
@@ -166,15 +166,8 @@ class EBSOptimizer:
166
166
  profile_name=get_profile_for_operation("operational", profile_name)
167
167
  )
168
168
 
169
- # EBS pricing (per GB per month, as of 2024)
170
- self.ebs_pricing = {
171
- 'gp2': 0.10, # $0.10/GB/month
172
- 'gp3': 0.08, # $0.08/GB/month (20% cheaper than GP2)
173
- 'io1': 0.125, # $0.125/GB/month
174
- 'io2': 0.125, # $0.125/GB/month
175
- 'st1': 0.045, # $0.045/GB/month
176
- 'sc1': 0.025, # $0.025/GB/month
177
- }
169
+ # EBS pricing using dynamic AWS pricing engine for universal compatibility
170
+ self.ebs_pricing = self._initialize_dynamic_ebs_pricing()
178
171
 
179
172
  # GP3 conversion savings percentage
180
173
  self.gp3_savings_percentage = 0.20 # 20% savings GP2→GP3
@@ -184,6 +177,60 @@ class EBSOptimizer:
184
177
  self.low_usage_threshold_bytes = 1_000_000 # 1MB per day
185
178
  self.analysis_period_days = 7
186
179
 
180
+ def _initialize_dynamic_ebs_pricing(self) -> Dict[str, float]:
181
+ """Initialize dynamic EBS pricing using AWS pricing engine for universal compatibility."""
182
+ try:
183
+ from ..common.aws_pricing import get_service_monthly_cost
184
+
185
+ # Get dynamic pricing for common EBS volume types in us-east-1 (base region)
186
+ base_region = "us-east-1"
187
+
188
+ return {
189
+ 'gp2': get_service_monthly_cost("ebs_gp2", base_region, self.profile_name),
190
+ 'gp3': get_service_monthly_cost("ebs_gp3", base_region, self.profile_name),
191
+ 'io1': get_service_monthly_cost("ebs_io1", base_region, self.profile_name),
192
+ 'io2': get_service_monthly_cost("ebs_io2", base_region, self.profile_name),
193
+ 'st1': get_service_monthly_cost("ebs_st1", base_region, self.profile_name),
194
+ 'sc1': get_service_monthly_cost("ebs_sc1", base_region, self.profile_name),
195
+ }
196
+ except Exception as e:
197
+ print_warning(f"Dynamic EBS pricing initialization failed: {e}")
198
+ print_warning("Attempting AWS Pricing API fallback with universal profile support")
199
+
200
+ try:
201
+ from ..common.aws_pricing import get_aws_pricing_engine
202
+
203
+ # Use AWS Pricing API with profile support for universal compatibility
204
+ pricing_engine = get_aws_pricing_engine(profile=self.profile_name, enable_fallback=True)
205
+
206
+ # Get actual AWS pricing instead of hardcoded values
207
+ gp2_pricing = pricing_engine.get_ebs_pricing("gp2", "us-east-1")
208
+ gp3_pricing = pricing_engine.get_ebs_pricing("gp3", "us-east-1")
209
+ io1_pricing = pricing_engine.get_ebs_pricing("io1", "us-east-1")
210
+ io2_pricing = pricing_engine.get_ebs_pricing("io2", "us-east-1")
211
+ st1_pricing = pricing_engine.get_ebs_pricing("st1", "us-east-1")
212
+ sc1_pricing = pricing_engine.get_ebs_pricing("sc1", "us-east-1")
213
+
214
+ return {
215
+ 'gp2': gp2_pricing.monthly_cost_per_gb,
216
+ 'gp3': gp3_pricing.monthly_cost_per_gb,
217
+ 'io1': io1_pricing.monthly_cost_per_gb,
218
+ 'io2': io2_pricing.monthly_cost_per_gb,
219
+ 'st1': st1_pricing.monthly_cost_per_gb,
220
+ 'sc1': sc1_pricing.monthly_cost_per_gb,
221
+ }
222
+
223
+ except Exception as pricing_error:
224
+ print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine EBS pricing without AWS API access: {pricing_error}")
225
+ print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
226
+
227
+ # Return error state instead of hardcoded values to maintain enterprise compliance
228
+ raise RuntimeError(
229
+ "Universal compatibility mode requires dynamic AWS pricing API access. "
230
+ "Please ensure your AWS profile has pricing:GetProducts permissions or configure "
231
+ "appropriate billing/management profile access."
232
+ )
233
+
187
234
  async def analyze_ebs_volumes(self, dry_run: bool = True) -> EBSOptimizerResults:
188
235
  """
189
236
  Comprehensive EBS volume cost optimization analysis.
@@ -409,10 +409,15 @@ if __name__ == "__main__":
409
409
  console.print("[bold bright_cyan]🚀 CloudOps Runbooks - Enhanced Trend Analysis[/]")
410
410
  console.print("[dim]QA Testing Specialist Implementation - Reference Image Compliance[/]")
411
411
 
412
+ import os
413
+ # Use environment-driven values for universal compatibility
414
+ account_id = os.getenv("AWS_ACCOUNT_ID", "123456789012")
415
+ profile = os.getenv("SINGLE_AWS_PROFILE", "default-single-profile")
416
+
412
417
  visualizer.create_enhanced_trend_display(
413
418
  monthly_costs=trend_data,
414
- account_id="499201730520",
415
- profile="ams-shared-services-non-prod-ReadOnlyAccess-499201730520",
419
+ account_id=account_id,
420
+ profile=profile,
416
421
  )
417
422
 
418
423
  # Export to JSON (contract compliance)
@@ -22,7 +22,8 @@ AWS_AVAILABLE = True
22
22
 
23
23
  def get_aws_profiles() -> List[str]:
24
24
  """Stub implementation - use dashboard_runner.py instead."""
25
- return ["default", "ams-admin-Billing-ReadOnlyAccess-909135376185"]
25
+ import os
26
+ return ["default", os.getenv("BILLING_PROFILE", "default-billing-profile")]
26
27
 
27
28
 
28
29
  def get_account_id(profile: str = "default") -> str:
@@ -46,10 +47,10 @@ class FinOpsConfig:
46
47
  include_budget_data: bool = True
47
48
  include_resource_analysis: bool = True
48
49
 
49
- # Legacy compatibility properties with environment variable support
50
- billing_profile: str = "ams-admin-Billing-ReadOnlyAccess-909135376185"
51
- management_profile: str = "ams-admin-ReadOnlyAccess-909135376185"
52
- operational_profile: str = "ams-centralised-ops-ReadOnlyAccess-335083429030"
50
+ # Legacy compatibility properties with universal environment support
51
+ billing_profile: str = field(default_factory=lambda: os.getenv("BILLING_PROFILE", "default-billing-profile"))
52
+ management_profile: str = field(default_factory=lambda: os.getenv("MANAGEMENT_PROFILE", "default-management-profile"))
53
+ operational_profile: str = field(default_factory=lambda: os.getenv("CENTRALISED_OPS_PROFILE", "default-ops-profile"))
53
54
 
54
55
  # Additional expected attributes from tests
55
56
  time_range_days: int = 30
@@ -11,6 +11,8 @@ from rich.console import Console
11
11
  from rich.panel import Panel
12
12
  from rich.table import Table
13
13
 
14
+ from runbooks.common import get_aws_cli_example_period
15
+
14
16
  console = Console()
15
17
 
16
18
 
@@ -311,6 +313,9 @@ def handle_cost_explorer_error(error: Exception, profile_name: Optional[str] = N
311
313
 
312
314
  def _display_single_account_cost_explorer_guidance(error: Exception, profile_name: Optional[str] = None):
313
315
  """Display context-aware guidance for single account Cost Explorer limitations."""
316
+
317
+ # Get dynamic date period for CLI examples
318
+ start_date, end_date = get_aws_cli_example_period()
314
319
 
315
320
  # Main explanation panel
316
321
  explanation_panel = Panel(
@@ -354,7 +359,7 @@ def _display_single_account_cost_explorer_guidance(error: Exception, profile_nam
354
359
  f"[green]✅ Recommended Solutions:[/green]\n\n"
355
360
  f"{solution_commands}\n\n"
356
361
  f"[bold]🎯 Quick Test Commands:[/bold]\n"
357
- f"• Test billing access: `aws ce get-cost-and-usage --time-period Start=2024-01-01,End=2024-01-02 --granularity MONTHLY --metrics UnblendedCost --profile your-billing-profile`\n"
362
+ f"• Test billing access: `aws ce get-cost-and-usage --time-period Start={start_date},End={end_date} --granularity MONTHLY --metrics UnblendedCost --profile your-billing-profile`\n"
358
363
  f"• List available profiles: `aws configure list-profiles`\n"
359
364
  f"• Check current identity: `aws sts get-caller-identity --profile {profile_name or 'your-profile'}`\n\n"
360
365
  f"[bold]💡 Alternative Approach:[/bold]\n"
@@ -143,7 +143,7 @@ class NATGatewayOptimizer:
143
143
  # NAT Gateway pricing - using dynamic pricing engine
144
144
  # Base monthly cost calculation (will be applied per region)
145
145
  self._base_monthly_cost_us_east_1 = get_service_monthly_cost("nat_gateway", "us-east-1")
146
- self.nat_gateway_data_processing_cost = 0.045 # $0.045/GB (data transfer pricing)
146
+ self.nat_gateway_data_processing_cost = get_service_monthly_cost("data_transfer", "us-east-1") # Dynamic data transfer pricing
147
147
 
148
148
  # Enterprise thresholds for optimization recommendations
149
149
  self.low_usage_threshold_connections = 10 # Active connections per day
@@ -155,9 +155,9 @@ class NATGatewayOptimizer:
155
155
  try:
156
156
  return get_service_monthly_cost("nat_gateway", region)
157
157
  except Exception:
158
- # Fallback to regional cost calculation
158
+ # Fallback to regional cost calculation using dynamic pricing
159
159
  from ..common.aws_pricing import calculate_regional_cost
160
- return calculate_regional_cost(self._base_monthly_cost_us_east_1, region)
160
+ return calculate_regional_cost(self._base_monthly_cost_us_east_1, region, "nat_gateway", self.profile_name)
161
161
 
162
162
  async def analyze_nat_gateways(self, dry_run: bool = True) -> NATGatewayOptimizerResults:
163
163
  """
@@ -778,39 +778,58 @@ class EnhancedVPCCostOptimizer:
778
778
  self.cost_model = self._initialize_dynamic_cost_model()
779
779
 
780
780
  def _initialize_dynamic_cost_model(self) -> Dict[str, float]:
781
- """Initialize dynamic cost model using AWS pricing engine."""
781
+ """Initialize dynamic cost model using AWS pricing engine with universal compatibility."""
782
782
  try:
783
783
  # Get base pricing for us-east-1, then apply regional multipliers as needed
784
784
  base_region = "us-east-1"
785
785
 
786
786
  return {
787
- "nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region),
788
- "nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region),
789
- "transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region),
790
- "vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region),
791
- "data_transfer_regional": get_service_monthly_cost("data_transfer", base_region),
792
- "data_transfer_internet": get_service_monthly_cost("data_transfer", base_region) * 4.5, # Internet is ~4.5x higher
787
+ "nat_gateway_monthly": get_service_monthly_cost("nat_gateway", base_region, self.profile),
788
+ "nat_gateway_data_processing": get_service_monthly_cost("data_transfer", base_region, self.profile),
789
+ "transit_gateway_monthly": get_service_monthly_cost("transit_gateway", base_region, self.profile),
790
+ "vpc_endpoint_monthly": get_service_monthly_cost("vpc_endpoint", base_region, self.profile),
791
+ "vpc_endpoint_interface_hourly": get_service_monthly_cost("vpc_endpoint_interface", base_region, self.profile) / (24 * 30),
792
+ "transit_gateway_attachment_hourly": get_service_monthly_cost("transit_gateway_attachment", base_region, self.profile) / (24 * 30),
793
+ "data_transfer_regional": get_service_monthly_cost("data_transfer", base_region, self.profile),
794
+ "data_transfer_internet": get_service_monthly_cost("data_transfer", base_region, self.profile) * 4.5, # Internet is ~4.5x higher
793
795
  }
794
796
  except Exception as e:
795
797
  print_warning(f"Dynamic pricing initialization failed: {e}")
796
- # Fallback to regional cost calculation
797
- from ..common.aws_pricing import calculate_regional_cost
798
- base_costs = {
799
- "nat_gateway_hourly": 0.045,
800
- "nat_gateway_data_processing": 0.045, # per GB
801
- "transit_gateway_monthly": 36.50,
802
- "transit_gateway_attachment_hourly": 0.05,
803
- "vpc_endpoint_interface_hourly": 0.01,
804
- "data_transfer_regional": 0.01, # per GB within region
805
- "data_transfer_cross_region": 0.02, # per GB cross-region
806
- "data_transfer_internet": 0.09 # per GB to internet
807
- }
798
+ print_warning("Attempting AWS Pricing API fallback with universal profile support")
799
+ # Enhanced fallback with AWS Pricing API integration
800
+ from ..common.aws_pricing import get_aws_pricing_engine, AWSOfficialPricingEngine
808
801
 
809
- # Apply regional multipliers to fallback costs
810
- return {
811
- key: calculate_regional_cost(value, "us-east-1")
812
- for key, value in base_costs.items()
813
- }
802
+ try:
803
+ # Use AWS Pricing API with profile support for universal compatibility
804
+ pricing_engine = get_aws_pricing_engine(profile=self.profile, enable_fallback=True)
805
+
806
+ # Get actual AWS pricing instead of hardcoded values
807
+ nat_gateway_pricing = pricing_engine.get_nat_gateway_pricing("us-east-1")
808
+ transit_gateway_pricing = pricing_engine.get_transit_gateway_pricing("us-east-1")
809
+ vpc_endpoint_pricing = pricing_engine.get_vpc_endpoint_pricing("us-east-1")
810
+ data_transfer_pricing = pricing_engine.get_data_transfer_pricing("us-east-1", "internet")
811
+
812
+ return {
813
+ "nat_gateway_monthly": nat_gateway_pricing.monthly_cost,
814
+ "nat_gateway_data_processing": data_transfer_pricing.cost_per_gb,
815
+ "transit_gateway_monthly": transit_gateway_pricing.monthly_cost,
816
+ "transit_gateway_attachment_hourly": transit_gateway_pricing.attachment_hourly_cost,
817
+ "vpc_endpoint_interface_hourly": vpc_endpoint_pricing.interface_hourly_cost,
818
+ "data_transfer_regional": data_transfer_pricing.cost_per_gb * 0.1, # Regional is ~10% of internet cost
819
+ "data_transfer_cross_region": data_transfer_pricing.cost_per_gb * 0.2, # Cross-region is ~20% of internet cost
820
+ "data_transfer_internet": data_transfer_pricing.cost_per_gb
821
+ }
822
+
823
+ except Exception as pricing_error:
824
+ print_error(f"ENTERPRISE COMPLIANCE VIOLATION: Cannot determine pricing without AWS API access: {pricing_error}")
825
+ print_warning("Universal compatibility requires dynamic pricing - hardcoded values not permitted")
826
+
827
+ # Return error state instead of hardcoded values to maintain enterprise compliance
828
+ raise RuntimeError(
829
+ "Universal compatibility mode requires dynamic AWS pricing API access. "
830
+ "Please ensure your AWS profile has pricing:GetProducts permissions or configure "
831
+ "appropriate billing/management profile access."
832
+ )
814
833
 
815
834
  async def analyze_comprehensive_vpc_costs(self, profile: Optional[str] = None,
816
835
  regions: Optional[List[str]] = None) -> Dict[str, Any]:
@@ -34,6 +34,8 @@ try:
34
34
  except ImportError:
35
35
  # Define mock_costexplorer as a no-op decorator for compatibility
36
36
  def mock_costexplorer(func):
37
+ # Dynamic test period for consistent test data
38
+ test_period = get_test_date_period(30)
37
39
  def wrapper(*args, **kwargs):
38
40
  return func(*args, **kwargs)
39
41
 
@@ -128,7 +130,7 @@ class TestAWSIntegrationWithMoto:
128
130
  mock_cost_data = {
129
131
  "ResultsByTime": [
130
132
  {
131
- "TimePeriod": {"Start": "2024-01-01", "End": "2024-01-31"},
133
+ "TimePeriod": {"Start": test_period["Start"], "End": test_period["End"]},
132
134
  "Total": {"UnblendedCost": {"Amount": "50000.00", "Unit": "USD"}},
133
135
  "Groups": [
134
136
  {"Keys": ["EC2-Instance"], "Metrics": {"UnblendedCost": {"Amount": "20000.00", "Unit": "USD"}}},
@@ -610,18 +610,9 @@ class VPCCleanupOptimizer:
610
610
 
611
611
  print_header("🌐 Real-Time NO-ENI VPC Discovery", "MCP-Validated VPC Cleanup Analysis")
612
612
 
613
- # Configure enterprise profiles for MCP validation - Universal compatibility
614
- from runbooks.common.profile_utils import get_enterprise_profile_mapping
615
- enterprise_profiles = get_enterprise_profile_mapping()
616
-
617
- # Override with current profile if available
618
- current_profile_type = self._determine_profile_type(self.profile)
619
- if current_profile_type:
620
- enterprise_profiles[current_profile_type] = self.profile
621
-
622
- # Initialize MCP validator for dynamic discovery
613
+ # Initialize MCP validator with universal profile support
623
614
  print_info("🔧 Initializing dynamic MCP validator...")
624
- mcp_validator = NOENIVPCMCPValidator(enterprise_profiles)
615
+ mcp_validator = NOENIVPCMCPValidator(user_profile=self.profile)
625
616
 
626
617
  # Perform dynamic discovery across all accounts
627
618
  print_info("🚀 Starting real-time discovery across all AWS accounts...")
@@ -641,7 +632,7 @@ class VPCCleanupOptimizer:
641
632
  # Get detailed VPC information for each NO-ENI VPC
642
633
  try:
643
634
  # Use appropriate session for this account
644
- session = self._get_session_for_account(target.account_id, enterprise_profiles)
635
+ session = self._get_session_for_account(target.account_id)
645
636
  ec2_client = session.client('ec2', region_name=target.region)
646
637
 
647
638
  # Get VPC details
@@ -708,26 +699,28 @@ class VPCCleanupOptimizer:
708
699
  return 'CENTRALISED_OPS'
709
700
  return None
710
701
 
711
- def _get_session_for_account(self, account_id: str, enterprise_profiles: Dict[str, str]) -> boto3.Session:
712
- """Get appropriate session for accessing a specific account."""
702
+ def _get_session_for_account(self, account_id: str) -> boto3.Session:
703
+ """Get appropriate session for accessing a specific account using universal profile management."""
704
+ from runbooks.common.profile_utils import get_profile_for_operation
705
+
713
706
  # In enterprise setup, would assume role here
714
- # For now, return session with best available profile
707
+ # For now, return session with best available profile using three-tier priority system
715
708
 
716
- # Priority order for account access
717
- profile_priority = ['MANAGEMENT', 'CENTRALISED_OPS', 'BILLING']
709
+ # Try different operation types in priority order
710
+ profile_types = ['management', 'operational', 'billing']
718
711
 
719
- for profile_type in profile_priority:
720
- if profile_type in enterprise_profiles:
721
- try:
722
- session = boto3.Session(profile_name=enterprise_profiles[profile_type])
723
- # Verify access
724
- sts_client = session.client('sts')
725
- identity = sts_client.get_caller_identity()
726
-
727
- if identity['Account'] == account_id:
728
- return session
729
- except Exception:
730
- continue
712
+ for profile_type in profile_types:
713
+ try:
714
+ profile_name = get_profile_for_operation(profile_type, self.profile)
715
+ session = boto3.Session(profile_name=profile_name)
716
+ # Verify access
717
+ sts_client = session.client('sts')
718
+ identity = sts_client.get_caller_identity()
719
+
720
+ if identity['Account'] == account_id:
721
+ return session
722
+ except Exception:
723
+ continue
731
724
 
732
725
  # Fallback to current session
733
726
  return self.session
@@ -42,12 +42,13 @@ try:
42
42
  ENHANCED_PROFILES_AVAILABLE = True
43
43
  except ImportError:
44
44
  ENHANCED_PROFILES_AVAILABLE = False
45
- # Fallback profile definitions
45
+ # Fallback profile definitions with universal environment support
46
+ import os
46
47
  ENTERPRISE_PROFILES = {
47
- "BILLING_PROFILE": "ams-admin-Billing-ReadOnlyAccess-909135376185",
48
- "MANAGEMENT_PROFILE": "ams-admin-ReadOnlyAccess-909135376185",
49
- "CENTRALISED_OPS_PROFILE": "ams-centralised-ops-ReadOnlyAccess-335083429030",
50
- "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"),
51
52
  }
52
53
 
53
54
 
@@ -141,28 +142,35 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
141
142
 
142
143
  Strategic Alignment: "Do one thing and do it well"
143
144
  - Single profile override pattern: --profile takes precedence
144
- - Simple fallback to environment variables when no --profile specified
145
- - No hardcoded profile mappings - dynamic based on user input
145
+ - Universal AWS environment compatibility: works with ANY profile configuration
146
+ - Graceful fallback system for discovery across different AWS setups
146
147
 
147
148
  Returns:
148
149
  str: The active profile to use for all operations
149
150
  """
150
- # Primary profile determination: user --profile parameter takes absolute precedence
151
+ # PRIMARY: User --profile parameter takes absolute precedence (Universal Compatibility)
151
152
  if self.profile:
152
- print_info(f"Using user-specified profile: {self.profile}")
153
- logger.info("Profile override via --profile parameter - enterprise priority system")
153
+ print_info(f"✅ Universal AWS Compatibility: Using user-specified profile '{self.profile}'")
154
+ logger.info("Profile override via --profile parameter - universal environment support")
154
155
  return self.profile
155
156
 
156
- # Fallback to environment variables when no --profile specified
157
+ # SECONDARY: Environment variable fallback with intelligent prioritization
158
+ # Priority order: Management > Billing > Operations > Default (Organizations discovery preference)
157
159
  env_profile = (
158
160
  os.getenv("MANAGEMENT_PROFILE") or
159
161
  os.getenv("BILLING_PROFILE") or
160
162
  os.getenv("CENTRALISED_OPS_PROFILE") or
163
+ os.getenv("SINGLE_AWS_PROFILE") or
161
164
  "default"
162
165
  )
163
166
 
164
- print_info(f"No --profile specified, using environment fallback: {env_profile}")
165
- logger.info("Using environment variable fallback profile")
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}")
170
+ else:
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
+
166
174
  return env_profile
167
175
 
168
176
  def _initialize_collectors(self) -> Dict[str, str]:
@@ -226,21 +234,19 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
226
234
 
227
235
  def get_organization_accounts(self) -> List[str]:
228
236
  """
229
- Get list of accounts in AWS Organization.
237
+ Get list of accounts in AWS Organization with universal compatibility.
230
238
 
231
239
  Strategic Alignment: "Do one thing and do it well"
232
- - Single responsibility: discover organization structure
233
- - Uses active profile for Organizations API access
234
- - Enables multi-account inventory operations
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
235
243
  """
236
244
  try:
237
- # Use management profile for Organizations operations
238
- # Use single active profile for all operations following --profile pattern
239
- management_profile = self.active_profile
240
- management_session = create_management_session(profile=management_profile)
245
+ # Use active profile for Organizations operations (Universal Compatibility)
246
+ management_session = create_management_session(profile=self.active_profile)
241
247
  organizations_client = management_session.client("organizations")
242
248
 
243
- print_info("Discovering organization accounts with management profile...")
249
+ print_info(f"🔍 Universal Discovery: Attempting Organizations API with profile '{self.active_profile}'...")
244
250
  response = self._make_aws_call(organizations_client.list_accounts)
245
251
 
246
252
  accounts = []
@@ -248,14 +254,31 @@ class EnhancedInventoryCollector(CloudFoundationsBase):
248
254
  if account["Status"] == "ACTIVE":
249
255
  accounts.append(account["Id"])
250
256
 
251
- print_success(f"Found {len(accounts)} active accounts in organization")
252
- logger.info(f"Found {len(accounts)} active accounts using management profile: {management_profile}")
253
- 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()]
254
264
 
255
265
  except Exception as e:
256
- print_warning(f"Could not list organization accounts: {e}")
257
- logger.warning(f"Organization discovery failed, falling back to current account: {e}")
258
- # 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
259
282
  return [self.get_account_id()]
260
283
 
261
284
  def get_current_account_id(self) -> str: