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
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dynamic Date Utilities for CloudOps Runbooks Platform
4
+
5
+ Replaces hardcoded 2024 dates with dynamic date generation following manager's
6
+ "No hardcoded values" requirement. Supports current month/year calculations
7
+ for all test data and AWS API time period generation.
8
+
9
+ Strategic Alignment: "Do one thing and do it well" - Focused date utility
10
+ KISS Principle: Simple, reusable date functions for all modules
11
+ """
12
+
13
+ from datetime import datetime, timedelta
14
+ from typing import Dict, Tuple
15
+
16
+
17
+ def get_current_year() -> int:
18
+ """Get current year dynamically."""
19
+ return datetime.now().year
20
+
21
+
22
+ def get_current_month_period() -> Dict[str, str]:
23
+ """
24
+ Generate current month's start and end dates for AWS API calls.
25
+
26
+ Returns:
27
+ Dict with 'Start' and 'End' keys in YYYY-MM-DD format
28
+ """
29
+ now = datetime.now()
30
+ start_date = now.replace(day=1).strftime('%Y-%m-%d')
31
+
32
+ # Get last day of current month
33
+ if now.month == 12:
34
+ next_month = now.replace(year=now.year + 1, month=1, day=1)
35
+ else:
36
+ next_month = now.replace(month=now.month + 1, day=1)
37
+
38
+ end_date = (next_month - timedelta(days=1)).strftime('%Y-%m-%d')
39
+
40
+ return {
41
+ 'Start': start_date,
42
+ 'End': end_date
43
+ }
44
+
45
+
46
+ def get_previous_month_period() -> Dict[str, str]:
47
+ """
48
+ Generate previous month's start and end dates for AWS API calls.
49
+
50
+ Returns:
51
+ Dict with 'Start' and 'End' keys in YYYY-MM-DD format
52
+ """
53
+ now = datetime.now()
54
+
55
+ # Get first day of previous month
56
+ if now.month == 1:
57
+ prev_month = now.replace(year=now.year - 1, month=12, day=1)
58
+ else:
59
+ prev_month = now.replace(month=now.month - 1, day=1)
60
+
61
+ start_date = prev_month.strftime('%Y-%m-%d')
62
+
63
+ # Get last day of previous month
64
+ end_date = (now.replace(day=1) - timedelta(days=1)).strftime('%Y-%m-%d')
65
+
66
+ return {
67
+ 'Start': start_date,
68
+ 'End': end_date
69
+ }
70
+
71
+
72
+ def get_test_date_period(days_back: int = 30) -> Dict[str, str]:
73
+ """
74
+ Generate test date periods for consistent test data.
75
+
76
+ Args:
77
+ days_back: Number of days back from today for start date
78
+
79
+ Returns:
80
+ Dict with 'Start' and 'End' keys in YYYY-MM-DD format
81
+ """
82
+ end_date = datetime.now().strftime('%Y-%m-%d')
83
+ start_date = (datetime.now() - timedelta(days=days_back)).strftime('%Y-%m-%d')
84
+
85
+ return {
86
+ 'Start': start_date,
87
+ 'End': end_date
88
+ }
89
+
90
+
91
+ def get_aws_cli_example_period() -> Tuple[str, str]:
92
+ """
93
+ Generate example date period for AWS CLI documentation.
94
+ Uses yesterday and today to ensure valid time range.
95
+
96
+ Returns:
97
+ Tuple of (start_date, end_date) in YYYY-MM-DD format
98
+ """
99
+ today = datetime.now()
100
+ yesterday = today - timedelta(days=1)
101
+
102
+ return (
103
+ yesterday.strftime('%Y-%m-%d'),
104
+ today.strftime('%Y-%m-%d')
105
+ )
106
+
107
+
108
+ def get_collection_timestamp() -> str:
109
+ """
110
+ Generate collection timestamp for test data.
111
+
112
+ Returns:
113
+ ISO format timestamp string
114
+ """
115
+ return datetime.now().isoformat()
@@ -323,6 +323,10 @@ class EnterpriseExceptionHandler:
323
323
  Returns:
324
324
  Enhanced error if performance target significantly exceeded, None otherwise
325
325
  """
326
+ # Defensive check for None values
327
+ if execution_time is None or performance_target is None or performance_target == 0:
328
+ return None
329
+
326
330
  performance_ratio = execution_time / performance_target
327
331
 
328
332
  # Only create error if performance significantly exceeded (>150% of target)
@@ -968,13 +972,15 @@ class EnterpriseExceptionHandler:
968
972
  }
969
973
 
970
974
  def _initialize_profile_recommendations(self) -> Dict[str, List[str]]:
971
- """Initialize profile recommendations for different operations."""
975
+ """Initialize profile recommendations for different operations with universal support."""
976
+ import os
977
+ # Environment variable-driven profile recommendations
972
978
  return {
973
- "inventory": ["ams-admin-ReadOnlyAccess-909135376185", "ams-centralised-ops-ReadOnlyAccess-335083429030"],
974
- "operate": ["ams-centralised-ops-ReadOnlyAccess-335083429030", "ams-admin-ReadOnlyAccess-909135376185"],
975
- "finops": ["ams-admin-Billing-ReadOnlyAccess-909135376185", "ams-admin-ReadOnlyAccess-909135376185"],
976
- "security": ["ams-admin-ReadOnlyAccess-909135376185"],
977
- "cfat": ["ams-admin-ReadOnlyAccess-909135376185"],
979
+ "inventory": [os.getenv("MANAGEMENT_PROFILE", "management-profile"), os.getenv("CENTRALISED_OPS_PROFILE", "ops-profile")],
980
+ "operate": [os.getenv("CENTRALISED_OPS_PROFILE", "ops-profile"), os.getenv("MANAGEMENT_PROFILE", "management-profile")],
981
+ "finops": [os.getenv("BILLING_PROFILE", "billing-profile"), os.getenv("MANAGEMENT_PROFILE", "management-profile")],
982
+ "security": [os.getenv("MANAGEMENT_PROFILE", "management-profile")],
983
+ "cfat": [os.getenv("MANAGEMENT_PROFILE", "management-profile")],
978
984
  }
979
985
 
980
986
  def _analyze_aws_error(self, error: ClientError, error_code: str, service: str, operation: str) -> Dict[str, Any]:
@@ -987,7 +993,8 @@ class EnterpriseExceptionHandler:
987
993
  # Service-specific adjustments
988
994
  if service == "ce" and error_code == "AccessDenied":
989
995
  # Cost Explorer requires special billing permissions
990
- base_analysis["recommended_profiles"] = ["ams-admin-Billing-ReadOnlyAccess-909135376185"]
996
+ import os
997
+ base_analysis["recommended_profiles"] = [os.getenv("BILLING_PROFILE", "billing-profile")]
991
998
 
992
999
  return base_analysis
993
1000
 
@@ -0,0 +1,96 @@
1
+ """
2
+ Environment Variable Utilities - Enterprise Hardcoded Value Elimination
3
+
4
+ This module provides enterprise-compliant environment variable utilities
5
+ that enforce zero hardcoded defaults policy across the entire codebase.
6
+ """
7
+
8
+ import os
9
+ from typing import Union
10
+
11
+
12
+ def get_required_env(var_name: str) -> str:
13
+ """
14
+ Get required string environment variable - NO hardcoded defaults.
15
+
16
+ Args:
17
+ var_name: Environment variable name
18
+
19
+ Returns:
20
+ Environment variable value
21
+
22
+ Raises:
23
+ ValueError: If environment variable is not set
24
+ """
25
+ value = os.getenv(var_name)
26
+ if value is None:
27
+ raise ValueError(f"Environment variable {var_name} required - no hardcoded defaults allowed")
28
+ return value
29
+
30
+
31
+ def get_required_env_int(var_name: str) -> int:
32
+ """
33
+ Get required integer environment variable - NO hardcoded defaults.
34
+
35
+ Args:
36
+ var_name: Environment variable name
37
+
38
+ Returns:
39
+ Environment variable value as integer
40
+
41
+ Raises:
42
+ ValueError: If environment variable is not set or not a valid integer
43
+ """
44
+ value = get_required_env(var_name)
45
+ try:
46
+ return int(value)
47
+ except ValueError as e:
48
+ raise ValueError(f"Environment variable {var_name} must be a valid integer, got: {value}") from e
49
+
50
+
51
+ def get_required_env_float(var_name: str) -> float:
52
+ """
53
+ Get required float environment variable - NO hardcoded defaults.
54
+
55
+ Args:
56
+ var_name: Environment variable name
57
+
58
+ Returns:
59
+ Environment variable value as float
60
+
61
+ Raises:
62
+ ValueError: If environment variable is not set or not a valid float
63
+ """
64
+ value = get_required_env(var_name)
65
+ try:
66
+ return float(value)
67
+ except ValueError as e:
68
+ raise ValueError(f"Environment variable {var_name} must be a valid float, got: {value}") from e
69
+
70
+
71
+ def get_required_env_bool(var_name: str) -> bool:
72
+ """
73
+ Get required boolean environment variable - NO hardcoded defaults.
74
+
75
+ Args:
76
+ var_name: Environment variable name
77
+
78
+ Returns:
79
+ Environment variable value as boolean
80
+
81
+ Raises:
82
+ ValueError: If environment variable is not set
83
+ """
84
+ value = get_required_env(var_name).lower()
85
+ if value in ('true', '1', 'yes', 'on'):
86
+ return True
87
+ elif value in ('false', '0', 'no', 'off'):
88
+ return False
89
+ else:
90
+ raise ValueError(f"Environment variable {var_name} must be a valid boolean (true/false), got: {value}")
91
+
92
+
93
+ # Legacy compatibility function names for existing code
94
+ _get_required_env_float = get_required_env_float
95
+ _get_required_env_int = get_required_env_int
96
+ _get_required_env = get_required_env
@@ -79,10 +79,11 @@ class MCPCostExplorerIntegration:
79
79
  performance_target_seconds: Performance target for operations
80
80
  """
81
81
 
82
- # Profile configuration with intelligent defaults
83
- self.billing_profile = billing_profile or "ams-admin-Billing-ReadOnlyAccess-909135376185"
84
- self.management_profile = management_profile or "ams-admin-ReadOnlyAccess-909135376185"
85
- self.single_account_profile = single_account_profile or "ams-shared-services-non-prod-ReadOnlyAccess-499201730520"
82
+ # Profile configuration with universal environment support
83
+ from runbooks.common.profile_utils import get_profile_for_operation
84
+ self.billing_profile = billing_profile or get_profile_for_operation("billing", None)
85
+ self.management_profile = management_profile or get_profile_for_operation("management", None)
86
+ self.single_account_profile = single_account_profile or get_profile_for_operation("single_account", None)
86
87
 
87
88
  # Validation configuration
88
89
  self.tolerance_percent = tolerance_percent
@@ -150,6 +150,11 @@ class EnterpriseMCPIntegrator:
150
150
  self.validation_threshold = 99.5 # Enterprise accuracy requirement
151
151
  self.tolerance_percent = 5.0 # ±5% tolerance for validation
152
152
 
153
+ # Organizations API caching to prevent duplicate calls
154
+ self._organizations_cache = {}
155
+ self._organizations_cache_timestamp = None
156
+ self._cache_ttl_minutes = 30 # 30-minute TTL for Organizations data
157
+
153
158
  # Initialize enterprise profile architecture
154
159
  self._initialize_enterprise_profiles()
155
160
 
@@ -176,6 +181,27 @@ class EnterpriseMCPIntegrator:
176
181
  except Exception as e:
177
182
  print_error(f"Failed to initialize {profile_type} profile: {str(e)}")
178
183
 
184
+ def _is_organizations_cache_valid(self) -> bool:
185
+ """Check if Organizations cache is still valid."""
186
+ if not self._organizations_cache_timestamp:
187
+ return False
188
+
189
+ cache_age_minutes = (datetime.now() - self._organizations_cache_timestamp).total_seconds() / 60
190
+ return cache_age_minutes < self._cache_ttl_minutes
191
+
192
+ def get_cached_organization_accounts(self) -> Optional[List[Dict[str, Any]]]:
193
+ """Get cached Organizations data if valid."""
194
+ if self._is_organizations_cache_valid() and 'accounts' in self._organizations_cache:
195
+ print_info("Using cached Organizations data (performance optimization)")
196
+ return self._organizations_cache['accounts']
197
+ return None
198
+
199
+ def cache_organization_accounts(self, accounts: List[Dict[str, Any]]) -> None:
200
+ """Cache Organizations data for performance optimization."""
201
+ self._organizations_cache = {'accounts': accounts}
202
+ self._organizations_cache_timestamp = datetime.now()
203
+ print_success(f"Cached Organizations data: {len(accounts)} accounts (TTL: {self._cache_ttl_minutes}min)")
204
+
179
205
  async def validate_inventory_operations(self, inventory_data: Dict[str, Any]) -> MCPValidationResult:
180
206
  """
181
207
  Validate inventory operations using MCP integration.
@@ -496,11 +522,32 @@ class EnterpriseMCPIntegrator:
496
522
  async def _validate_resource_counts(self, inventory_data: Dict, progress, task) -> None:
497
523
  """Validate resource counts across services."""
498
524
  try:
499
- resources = inventory_data.get("resources", [])
525
+ # Enhanced: Handle both dict and string inputs for robust data structure handling
526
+ if isinstance(inventory_data, str):
527
+ # Handle case where inventory_data is a JSON string
528
+ try:
529
+ inventory_data = json.loads(inventory_data)
530
+ except json.JSONDecodeError:
531
+ print_warning(f"Invalid JSON string in inventory data")
532
+ return
533
+
534
+ resources = inventory_data.get("resources", []) if isinstance(inventory_data, dict) else []
535
+
536
+ # Enhanced: Ensure resources is always a list
537
+ if not isinstance(resources, list):
538
+ resources = []
539
+
500
540
  service_counts = {}
501
541
 
502
542
  for resource in resources:
503
- service = resource.get("service", "unknown")
543
+ # Enhanced: Handle both dict and string resource entries
544
+ if isinstance(resource, dict):
545
+ service = resource.get("service", "unknown")
546
+ elif isinstance(resource, str):
547
+ service = "unknown"
548
+ else:
549
+ service = "unknown"
550
+
504
551
  service_counts[service] = service_counts.get(service, 0) + 1
505
552
 
506
553
  progress.update(task, advance=40, description=f"Validated {len(resources)} resources...")