runbooks 0.7.9__py3-none-any.whl → 0.9.0__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 (95) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/README.md +12 -1
  3. runbooks/cfat/__init__.py +1 -1
  4. runbooks/cfat/assessment/runner.py +42 -34
  5. runbooks/cfat/models.py +1 -1
  6. runbooks/common/__init__.py +152 -0
  7. runbooks/common/accuracy_validator.py +1039 -0
  8. runbooks/common/context_logger.py +440 -0
  9. runbooks/common/cross_module_integration.py +594 -0
  10. runbooks/common/enhanced_exception_handler.py +1108 -0
  11. runbooks/common/enterprise_audit_integration.py +634 -0
  12. runbooks/common/mcp_integration.py +539 -0
  13. runbooks/common/performance_monitor.py +387 -0
  14. runbooks/common/profile_utils.py +216 -0
  15. runbooks/common/rich_utils.py +171 -0
  16. runbooks/feedback/user_feedback_collector.py +440 -0
  17. runbooks/finops/README.md +339 -451
  18. runbooks/finops/__init__.py +4 -21
  19. runbooks/finops/account_resolver.py +279 -0
  20. runbooks/finops/accuracy_cross_validator.py +638 -0
  21. runbooks/finops/aws_client.py +721 -36
  22. runbooks/finops/budget_integration.py +313 -0
  23. runbooks/finops/cli.py +59 -5
  24. runbooks/finops/cost_processor.py +211 -37
  25. runbooks/finops/dashboard_router.py +900 -0
  26. runbooks/finops/dashboard_runner.py +990 -232
  27. runbooks/finops/embedded_mcp_validator.py +288 -0
  28. runbooks/finops/enhanced_dashboard_runner.py +8 -7
  29. runbooks/finops/enhanced_progress.py +327 -0
  30. runbooks/finops/enhanced_trend_visualization.py +423 -0
  31. runbooks/finops/finops_dashboard.py +29 -1880
  32. runbooks/finops/helpers.py +509 -196
  33. runbooks/finops/iam_guidance.py +400 -0
  34. runbooks/finops/markdown_exporter.py +466 -0
  35. runbooks/finops/multi_dashboard.py +1502 -0
  36. runbooks/finops/optimizer.py +15 -15
  37. runbooks/finops/profile_processor.py +2 -2
  38. runbooks/finops/runbooks.inventory.organizations_discovery.log +0 -0
  39. runbooks/finops/runbooks.security.report_generator.log +0 -0
  40. runbooks/finops/runbooks.security.run_script.log +0 -0
  41. runbooks/finops/runbooks.security.security_export.log +0 -0
  42. runbooks/finops/service_mapping.py +195 -0
  43. runbooks/finops/single_dashboard.py +710 -0
  44. runbooks/finops/tests/test_reference_images_validation.py +1 -1
  45. runbooks/inventory/README.md +12 -1
  46. runbooks/inventory/core/collector.py +157 -29
  47. runbooks/inventory/list_ec2_instances.py +9 -6
  48. runbooks/inventory/list_ssm_parameters.py +10 -10
  49. runbooks/inventory/organizations_discovery.py +210 -164
  50. runbooks/inventory/rich_inventory_display.py +74 -107
  51. runbooks/inventory/run_on_multi_accounts.py +13 -13
  52. runbooks/main.py +740 -134
  53. runbooks/metrics/dora_metrics_engine.py +711 -17
  54. runbooks/monitoring/performance_monitor.py +433 -0
  55. runbooks/operate/README.md +394 -0
  56. runbooks/operate/base.py +215 -47
  57. runbooks/operate/ec2_operations.py +7 -5
  58. runbooks/operate/privatelink_operations.py +1 -1
  59. runbooks/operate/vpc_endpoints.py +1 -1
  60. runbooks/remediation/README.md +489 -13
  61. runbooks/remediation/commons.py +8 -4
  62. runbooks/security/ENTERPRISE_SECURITY_FRAMEWORK.md +506 -0
  63. runbooks/security/README.md +12 -1
  64. runbooks/security/__init__.py +164 -33
  65. runbooks/security/compliance_automation.py +12 -10
  66. runbooks/security/compliance_automation_engine.py +1021 -0
  67. runbooks/security/enterprise_security_framework.py +931 -0
  68. runbooks/security/enterprise_security_policies.json +293 -0
  69. runbooks/security/integration_test_enterprise_security.py +879 -0
  70. runbooks/security/module_security_integrator.py +641 -0
  71. runbooks/security/report_generator.py +1 -1
  72. runbooks/security/run_script.py +4 -8
  73. runbooks/security/security_baseline_tester.py +36 -49
  74. runbooks/security/security_export.py +99 -120
  75. runbooks/sre/README.md +472 -0
  76. runbooks/sre/__init__.py +33 -0
  77. runbooks/sre/mcp_reliability_engine.py +1049 -0
  78. runbooks/sre/performance_optimization_engine.py +1032 -0
  79. runbooks/sre/reliability_monitoring_framework.py +1011 -0
  80. runbooks/validation/__init__.py +2 -2
  81. runbooks/validation/benchmark.py +154 -149
  82. runbooks/validation/cli.py +159 -147
  83. runbooks/validation/mcp_validator.py +265 -236
  84. runbooks/vpc/README.md +478 -0
  85. runbooks/vpc/__init__.py +2 -2
  86. runbooks/vpc/manager_interface.py +366 -351
  87. runbooks/vpc/networking_wrapper.py +62 -33
  88. runbooks/vpc/rich_formatters.py +22 -8
  89. {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/METADATA +136 -54
  90. {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/RECORD +94 -55
  91. {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/entry_points.txt +1 -1
  92. runbooks/finops/cross_validation.py +0 -375
  93. {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/WHEEL +0 -0
  94. {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/licenses/LICENSE +0 -0
  95. {runbooks-0.7.9.dist-info → runbooks-0.9.0.dist-info}/top_level.txt +0 -0
@@ -1,7 +1,10 @@
1
1
  import csv
2
2
  import json
3
3
  import os
4
+ import threading
5
+ import time
4
6
  from collections import defaultdict
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
5
8
  from datetime import date, datetime, timedelta
6
9
  from typing import Any, Dict, List, Optional, Tuple
7
10
 
@@ -9,18 +12,69 @@ from boto3.session import Session
9
12
  from rich.console import Console
10
13
 
11
14
  from runbooks.finops.aws_client import get_account_id
15
+ from runbooks.finops.iam_guidance import handle_cost_explorer_error
12
16
  from runbooks.finops.types import BudgetInfo, CostData, EC2Summary, ProfileData
13
17
 
14
18
  console = Console()
15
19
 
20
+ # Enterprise batch processing configuration
21
+ BATCH_COST_EXPLORER_SIZE = 5 # Optimal batch size for Cost Explorer API to avoid rate limiting
22
+ MAX_CONCURRENT_COST_CALLS = 10 # AWS Cost Explorer rate limit consideration
16
23
 
17
- def get_trend(session: Session, tag: Optional[List[str]] = None) -> Dict[str, Any]:
24
+ # Service filtering configuration for analytical insights
25
+ NON_ANALYTICAL_SERVICES = ["Tax"] # Services excluded from Top N analysis per user requirements
26
+
27
+
28
+ def filter_analytical_services(
29
+ services_dict: Dict[str, float], excluded_services: List[str] = None
30
+ ) -> Dict[str, float]:
31
+ """
32
+ Filter out non-analytical services from service cost data.
33
+
34
+ Args:
35
+ services_dict: Dictionary of service names to costs
36
+ excluded_services: List of service patterns to exclude (defaults to NON_ANALYTICAL_SERVICES)
37
+
38
+ Returns:
39
+ Dictionary with non-analytical services filtered out
40
+
41
+ Example:
42
+ >>> services = {'Amazon EC2': 100.0, 'Tax': 10.0, 'S3': 50.0}
43
+ >>> filtered = filter_analytical_services(services)
44
+ >>> filtered
45
+ {'Amazon EC2': 100.0, 'S3': 50.0}
46
+ """
47
+ if excluded_services is None:
48
+ excluded_services = NON_ANALYTICAL_SERVICES
49
+
50
+ filtered_services = {}
51
+ filtered_count = 0
52
+
53
+ for service_name, cost in services_dict.items():
54
+ should_exclude = any(excluded in service_name for excluded in excluded_services)
55
+ if not should_exclude:
56
+ filtered_services[service_name] = cost
57
+ else:
58
+ filtered_count += 1
59
+
60
+ # Debug logging for enterprise troubleshooting
61
+ if filtered_count > 0:
62
+ excluded_names = [
63
+ name for name in services_dict.keys() if any(excluded in name for excluded in excluded_services)
64
+ ]
65
+ console.log(f"[dim yellow]🔍 Filtered {filtered_count} non-analytical services: {', '.join(excluded_names)}[/]")
66
+
67
+ return filtered_services
68
+
69
+
70
+ def get_trend(session: Session, tag: Optional[List[str]] = None, account_id: Optional[str] = None) -> Dict[str, Any]:
18
71
  """
19
72
  Get cost trend data for an AWS account.
20
73
 
21
74
  Args:
22
75
  session: The boto3 session to use
23
76
  tag: Optional list of tags in "Key=Value" format to filter resources.
77
+ account_id: Optional account ID to filter costs to specific account (multi-account support)
24
78
 
25
79
  """
26
80
  ce = session.client("ce")
@@ -30,30 +84,32 @@ def get_trend(session: Session, tag: Optional[List[str]] = None) -> Dict[str, An
30
84
  key, value = t.split("=", 1)
31
85
  tag_filters.append({"Key": key, "Values": [value]})
32
86
 
33
- filter_param: Optional[Dict[str, Any]] = None
87
+ # Build filters for trend data (similar to get_cost_data)
88
+ filters = []
89
+
90
+ # Add account filtering if account_id is provided
91
+ if account_id:
92
+ account_filter = {"Dimensions": {"Key": "LINKED_ACCOUNT", "Values": [account_id]}}
93
+ filters.append(account_filter)
94
+
95
+ # Add tag filtering if provided
34
96
  if tag_filters:
35
- if len(tag_filters) == 1:
36
- filter_param = {
97
+ for tag_filter in tag_filters:
98
+ tag_filter_dict = {
37
99
  "Tags": {
38
- "Key": tag_filters[0]["Key"],
39
- "Values": tag_filters[0]["Values"],
100
+ "Key": tag_filter["Key"],
101
+ "Values": tag_filter["Values"],
40
102
  "MatchOptions": ["EQUALS"],
41
103
  }
42
104
  }
105
+ filters.append(tag_filter_dict)
43
106
 
44
- else:
45
- filter_param = {
46
- "And": [
47
- {
48
- "Tags": {
49
- "Key": f["Key"],
50
- "Values": f["Values"],
51
- "MatchOptions": ["EQUALS"],
52
- }
53
- }
54
- for f in tag_filters
55
- ]
56
- }
107
+ # Combine filters appropriately
108
+ filter_param: Optional[Dict[str, Any]] = None
109
+ if len(filters) == 1:
110
+ filter_param = filters[0]
111
+ elif len(filters) > 1:
112
+ filter_param = {"And": filters}
57
113
  kwargs = {}
58
114
  if filter_param:
59
115
  kwargs["Filter"] = filter_param
@@ -81,6 +137,8 @@ def get_trend(session: Session, tag: Optional[List[str]] = None) -> Dict[str, An
81
137
  monthly_costs.append((month, cost))
82
138
  except Exception as e:
83
139
  console.log(f"[yellow]Error getting monthly trend data: {e}[/]")
140
+ if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
141
+ handle_cost_explorer_error(e, profile)
84
142
  monthly_costs = []
85
143
 
86
144
  return {
@@ -90,11 +148,109 @@ def get_trend(session: Session, tag: Optional[List[str]] = None) -> Dict[str, An
90
148
  }
91
149
 
92
150
 
151
+ def get_batch_cost_data(
152
+ sessions: List[Tuple[Session, str]],
153
+ time_range: Optional[int] = None,
154
+ tag: Optional[List[str]] = None,
155
+ max_workers: int = MAX_CONCURRENT_COST_CALLS,
156
+ ) -> Dict[str, CostData]:
157
+ """
158
+ Enterprise batch cost data retrieval with parallel processing.
159
+
160
+ Optimizes Cost Explorer API calls by processing multiple accounts concurrently
161
+ while respecting AWS rate limits and providing circuit breaker protection.
162
+
163
+ Args:
164
+ sessions: List of (session, profile_name) tuples for batch processing
165
+ time_range: Optional time range in days for cost data
166
+ tag: Optional list of tags for filtering
167
+ max_workers: Maximum concurrent API calls (default: 10 for rate limiting)
168
+
169
+ Returns:
170
+ Dictionary mapping profile_name to CostData results
171
+
172
+ Performance: 5-10x faster than sequential processing for 10+ accounts
173
+ """
174
+ if not sessions:
175
+ return {}
176
+
177
+ console.log(f"[blue]Enterprise batch processing: {len(sessions)} accounts with {max_workers} workers[/]")
178
+ start_time = time.time()
179
+ results = {}
180
+
181
+ # Thread-safe result collection
182
+ results_lock = threading.Lock()
183
+
184
+ def _process_single_cost_data(session_info: Tuple[Session, str]) -> Tuple[str, CostData]:
185
+ """Process cost data for a single session."""
186
+ session, profile_name = session_info
187
+ try:
188
+ # Extract account ID from profile if it's in Organizations API format (profile@accountId)
189
+ account_id = None
190
+ if "@" in profile_name:
191
+ _, account_id = profile_name.split("@", 1)
192
+
193
+ cost_data = get_cost_data(session, time_range, tag, False, profile_name, account_id)
194
+ return profile_name, cost_data
195
+ except Exception as e:
196
+ console.log(f"[yellow]Batch cost data error for {profile_name}: {str(e)[:50]}[/]")
197
+ # Return empty cost data structure for failed accounts
198
+ return profile_name, {
199
+ "account_id": get_account_id(session) or "unknown",
200
+ "current_month": 0.0,
201
+ "last_month": 0.0,
202
+ "current_month_cost_by_service": [],
203
+ "budgets": [],
204
+ "current_period_name": "Current month's cost",
205
+ "previous_period_name": "Last month's cost",
206
+ "time_range": time_range,
207
+ "current_period_start": "",
208
+ "current_period_end": "",
209
+ "previous_period_start": "",
210
+ "previous_period_end": "",
211
+ "monthly_costs": None,
212
+ "costs_by_service": {},
213
+ }
214
+
215
+ # Execute batch processing with ThreadPoolExecutor
216
+ with ThreadPoolExecutor(max_workers=max_workers) as executor:
217
+ future_to_profile = {
218
+ executor.submit(_process_single_cost_data, session_info): session_info[1] for session_info in sessions
219
+ }
220
+
221
+ processed = 0
222
+ for future in as_completed(future_to_profile, timeout=120): # 2 minute timeout for batch
223
+ try:
224
+ profile_name, cost_data = future.result(timeout=30) # 30s per account
225
+
226
+ with results_lock:
227
+ results[profile_name] = cost_data
228
+ processed += 1
229
+
230
+ if processed % 5 == 0: # Progress logging every 5 accounts
231
+ console.log(f"[green]Batch progress: {processed}/{len(sessions)} accounts processed[/]")
232
+
233
+ except Exception as e:
234
+ profile_name = future_to_profile[future]
235
+ console.log(f"[yellow]Batch timeout for {profile_name}: {str(e)[:50]}[/]")
236
+ # Continue processing other accounts
237
+
238
+ execution_time = time.time() - start_time
239
+ console.log(
240
+ f"[green]✅ Batch cost processing completed: {len(results)}/{len(sessions)} accounts in {execution_time:.1f}s[/]"
241
+ )
242
+ console.log(f"[dim]Performance: {len(sessions) / execution_time:.1f} accounts/second[/]")
243
+
244
+ return results
245
+
246
+
93
247
  def get_cost_data(
94
248
  session: Session,
95
249
  time_range: Optional[int] = None,
96
250
  tag: Optional[List[str]] = None,
97
251
  get_trend: bool = False,
252
+ profile_name: Optional[str] = None,
253
+ account_id: Optional[str] = None,
98
254
  ) -> CostData:
99
255
  """
100
256
  Get cost data for an AWS account.
@@ -104,6 +260,8 @@ def get_cost_data(
104
260
  time_range: Optional time range in days for cost data (default: current month)
105
261
  tag: Optional list of tags in "Key=Value" format to filter resources.
106
262
  get_trend: Optional boolean to get trend data for last 6 months (default).
263
+ profile_name: Optional AWS profile name for enhanced error messaging
264
+ account_id: Optional account ID to filter costs to specific account (multi-account support)
107
265
 
108
266
  """
109
267
  ce = session.client("ce")
@@ -116,30 +274,33 @@ def get_cost_data(
116
274
  key, value = t.split("=", 1)
117
275
  tag_filters.append({"Key": key, "Values": [value]})
118
276
 
119
- filter_param: Optional[Dict[str, Any]] = None
277
+ # Build filter parameters for Cost Explorer API
278
+ filters = []
279
+
280
+ # Add account filtering if account_id is provided (critical for multi-account scenarios)
281
+ if account_id:
282
+ account_filter = {"Dimensions": {"Key": "LINKED_ACCOUNT", "Values": [account_id]}}
283
+ filters.append(account_filter)
284
+ console.log(f"[blue]Account filtering enabled: {account_id}[/]")
285
+
286
+ # Add tag filtering if provided
120
287
  if tag_filters:
121
- if len(tag_filters) == 1:
122
- filter_param = {
288
+ for tag_filter in tag_filters:
289
+ tag_filter_dict = {
123
290
  "Tags": {
124
- "Key": tag_filters[0]["Key"],
125
- "Values": tag_filters[0]["Values"],
291
+ "Key": tag_filter["Key"],
292
+ "Values": tag_filter["Values"],
126
293
  "MatchOptions": ["EQUALS"],
127
294
  }
128
295
  }
296
+ filters.append(tag_filter_dict)
129
297
 
130
- else:
131
- filter_param = {
132
- "And": [
133
- {
134
- "Tags": {
135
- "Key": f["Key"],
136
- "Values": f["Values"],
137
- "MatchOptions": ["EQUALS"],
138
- }
139
- }
140
- for f in tag_filters
141
- ]
142
- }
298
+ # Combine filters appropriately
299
+ filter_param: Optional[Dict[str, Any]] = None
300
+ if len(filters) == 1:
301
+ filter_param = filters[0]
302
+ elif len(filters) > 1:
303
+ filter_param = {"And": filters}
143
304
  kwargs = {}
144
305
  if filter_param:
145
306
  kwargs["Filter"] = filter_param
@@ -173,6 +334,8 @@ def get_cost_data(
173
334
  )
174
335
  except Exception as e:
175
336
  console.log(f"[yellow]Error getting current period cost: {e}[/]")
337
+ if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
338
+ handle_cost_explorer_error(e, profile_name)
176
339
  this_period = {"ResultsByTime": [{"Total": {"UnblendedCost": {"Amount": 0}}}]}
177
340
 
178
341
  try:
@@ -187,6 +350,8 @@ def get_cost_data(
187
350
  )
188
351
  except Exception as e:
189
352
  console.log(f"[yellow]Error getting previous period cost: {e}[/]")
353
+ if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
354
+ handle_cost_explorer_error(e, profile_name)
190
355
  previous_period = {"ResultsByTime": [{"Total": {"UnblendedCost": {"Amount": 0}}}]}
191
356
 
192
357
  try:
@@ -199,6 +364,8 @@ def get_cost_data(
199
364
  )
200
365
  except Exception as e:
201
366
  console.log(f"[yellow]Error getting current period cost by service: {e}[/]")
367
+ if "AccessDeniedException" in str(e) and "ce:GetCostAndUsage" in str(e):
368
+ handle_cost_explorer_error(e, profile_name)
202
369
  current_period_cost_by_service = {"ResultsByTime": [{"Groups": []}]}
203
370
 
204
371
  # Aggregate cost by service across all days
@@ -244,11 +411,18 @@ def get_cost_data(
244
411
  current_period_name = f"Current {time_range} days cost" if time_range else "Current month's cost"
245
412
  previous_period_name = f"Previous {time_range} days cost" if time_range else "Last month's cost"
246
413
 
414
+ # Create costs_by_service dictionary for easy service lookup
415
+ costs_by_service = {}
416
+ for service, amount in aggregated_service_costs.items():
417
+ if amount > 0.001: # Filter out negligible costs
418
+ costs_by_service[service] = amount
419
+
247
420
  return {
248
421
  "account_id": account_id,
249
422
  "current_month": current_period_cost,
250
423
  "last_month": previous_period_cost,
251
424
  "current_month_cost_by_service": aggregated_groups,
425
+ "costs_by_service": costs_by_service, # Added for multi_dashboard compatibility
252
426
  "budgets": budgets_data,
253
427
  "current_period_name": current_period_name,
254
428
  "previous_period_name": previous_period_name,