runbooks 0.9.0__py3-none-any.whl → 0.9.2__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 (46) hide show
  1. runbooks/__init__.py +1 -1
  2. runbooks/cfat/assessment/compliance.py +4 -1
  3. runbooks/cloudops/__init__.py +123 -0
  4. runbooks/cloudops/base.py +385 -0
  5. runbooks/cloudops/cost_optimizer.py +811 -0
  6. runbooks/cloudops/infrastructure_optimizer.py +29 -0
  7. runbooks/cloudops/interfaces.py +828 -0
  8. runbooks/cloudops/lifecycle_manager.py +29 -0
  9. runbooks/cloudops/mcp_cost_validation.py +678 -0
  10. runbooks/cloudops/models.py +251 -0
  11. runbooks/cloudops/monitoring_automation.py +29 -0
  12. runbooks/cloudops/notebook_framework.py +676 -0
  13. runbooks/cloudops/security_enforcer.py +449 -0
  14. runbooks/common/mcp_cost_explorer_integration.py +900 -0
  15. runbooks/common/mcp_integration.py +19 -10
  16. runbooks/common/rich_utils.py +1 -1
  17. runbooks/finops/README.md +31 -0
  18. runbooks/finops/cost_optimizer.py +1340 -0
  19. runbooks/finops/finops_dashboard.py +211 -5
  20. runbooks/finops/schemas.py +589 -0
  21. runbooks/inventory/runbooks.inventory.organizations_discovery.log +0 -0
  22. runbooks/inventory/runbooks.security.security_export.log +0 -0
  23. runbooks/main.py +525 -0
  24. runbooks/operate/ec2_operations.py +428 -0
  25. runbooks/operate/iam_operations.py +598 -3
  26. runbooks/operate/rds_operations.py +508 -0
  27. runbooks/operate/s3_operations.py +508 -0
  28. runbooks/remediation/base.py +5 -3
  29. runbooks/security/__init__.py +101 -0
  30. runbooks/security/cloudops_automation_security_validator.py +1164 -0
  31. runbooks/security/compliance_automation_engine.py +4 -4
  32. runbooks/security/enterprise_security_framework.py +4 -5
  33. runbooks/security/executive_security_dashboard.py +1247 -0
  34. runbooks/security/multi_account_security_controls.py +2254 -0
  35. runbooks/security/real_time_security_monitor.py +1196 -0
  36. runbooks/security/security_baseline_tester.py +3 -3
  37. runbooks/sre/production_monitoring_framework.py +584 -0
  38. runbooks/validation/mcp_validator.py +29 -15
  39. runbooks/vpc/networking_wrapper.py +6 -3
  40. runbooks-0.9.2.dist-info/METADATA +525 -0
  41. {runbooks-0.9.0.dist-info → runbooks-0.9.2.dist-info}/RECORD +45 -23
  42. runbooks-0.9.0.dist-info/METADATA +0 -718
  43. {runbooks-0.9.0.dist-info → runbooks-0.9.2.dist-info}/WHEEL +0 -0
  44. {runbooks-0.9.0.dist-info → runbooks-0.9.2.dist-info}/entry_points.txt +0 -0
  45. {runbooks-0.9.0.dist-info → runbooks-0.9.2.dist-info}/licenses/LICENSE +0 -0
  46. {runbooks-0.9.0.dist-info → runbooks-0.9.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,508 @@
1
+ """
2
+ AWS RDS Operations for cost optimization and security compliance.
3
+
4
+ This module provides comprehensive RDS database management capabilities including
5
+ cost optimization through underutilized instance cleanup, security compliance
6
+ through access control automation, and reserved instance management.
7
+
8
+ Extracted from unSkript notebooks and enhanced with enterprise patterns.
9
+ """
10
+
11
+ import asyncio
12
+ from datetime import datetime, timedelta, timezone
13
+ from typing import Any, Dict, List, Optional, Tuple
14
+
15
+ import boto3
16
+ from botocore.exceptions import ClientError
17
+ from rich.console import Console
18
+ from rich.panel import Panel
19
+ from rich.progress import track
20
+ from rich.table import Table
21
+
22
+ from runbooks.common.aws_helpers import get_paginated_results
23
+ from runbooks.common.profile_utils import get_profile_for_operation
24
+ from runbooks.common.rich_utils import (
25
+ console,
26
+ create_table,
27
+ format_cost,
28
+ print_error,
29
+ print_header,
30
+ print_info,
31
+ print_success,
32
+ print_warning,
33
+ )
34
+ from runbooks.operate.base import BaseOperation, OperationContext, OperationResult, OperationStatus
35
+
36
+
37
+ class RDSOperations(BaseOperation):
38
+ """AWS RDS operations for cost optimization and security compliance."""
39
+
40
+ def __init__(self, profile: Optional[str] = None, region: Optional[str] = None, dry_run: bool = True):
41
+ """Initialize RDS operations."""
42
+ super().__init__(profile=profile, region=region, dry_run=dry_run)
43
+ self.service = 'rds'
44
+ self.supported_operations = {
45
+ 'find_low_cpu_instances',
46
+ 'delete_instance_safely',
47
+ 'get_publicly_accessible_instances',
48
+ 'secure_public_instances',
49
+ 'analyze_reserved_instance_opportunities',
50
+ 'get_instance_metrics',
51
+ 'list_instances',
52
+ 'get_instance_details'
53
+ }
54
+
55
+ def find_low_cpu_instances(
56
+ self,
57
+ context: OperationContext,
58
+ utilization_threshold: float = 10.0,
59
+ duration_minutes: int = 60
60
+ ) -> List[OperationResult]:
61
+ """
62
+ Find RDS instances with low CPU utilization.
63
+
64
+ Extracted from: AWS_Delete_RDS_Instances_with_Low_CPU_Utilization.ipynb
65
+
66
+ Args:
67
+ context: Operation context
68
+ utilization_threshold: CPU threshold percentage (default 10%)
69
+ duration_minutes: Duration to analyze metrics (default 60 minutes)
70
+
71
+ Returns:
72
+ List of operation results with low CPU instances
73
+ """
74
+ print_header("RDS Low CPU Analysis", "Cost Optimization")
75
+
76
+ results = []
77
+ regions_to_check = [context.region] if context.region else self._get_all_regions()
78
+
79
+ for region in track(regions_to_check, description="Analyzing regions..."):
80
+ try:
81
+ # Get RDS client for region
82
+ session = self._get_aws_session(context.profile)
83
+ rds_client = session.client('rds', region_name=region)
84
+ cloudwatch_client = session.client('cloudwatch', region_name=region)
85
+
86
+ # Get all RDS instances in region
87
+ instances = get_paginated_results(
88
+ rds_client,
89
+ 'describe_db_instances',
90
+ 'DBInstances'
91
+ )
92
+
93
+ region_low_cpu_instances = []
94
+
95
+ for db in instances:
96
+ try:
97
+ db_identifier = db['DBInstanceIdentifier']
98
+
99
+ # Get CPU metrics from CloudWatch
100
+ end_time = datetime.utcnow()
101
+ start_time = end_time - timedelta(minutes=duration_minutes)
102
+
103
+ response = cloudwatch_client.get_metric_data(
104
+ MetricDataQueries=[
105
+ {
106
+ 'Id': 'cpu',
107
+ 'MetricStat': {
108
+ 'Metric': {
109
+ 'Namespace': 'AWS/RDS',
110
+ 'MetricName': 'CPUUtilization',
111
+ 'Dimensions': [
112
+ {
113
+ 'Name': 'DBInstanceIdentifier',
114
+ 'Value': db_identifier
115
+ }
116
+ ]
117
+ },
118
+ 'Period': 3600, # 1 hour periods
119
+ 'Stat': 'Average'
120
+ },
121
+ 'ReturnData': True
122
+ }
123
+ ],
124
+ StartTime=start_time,
125
+ EndTime=end_time
126
+ )
127
+
128
+ # Process CPU utilization data
129
+ if (response['MetricDataResults'] and
130
+ response['MetricDataResults'][0]['Values']):
131
+
132
+ cpu_values = response['MetricDataResults'][0]['Values']
133
+ avg_cpu = sum(cpu_values) / len(cpu_values)
134
+
135
+ if avg_cpu < utilization_threshold:
136
+ instance_info = {
137
+ 'region': region,
138
+ 'instance_id': db_identifier,
139
+ 'instance_class': db.get('DBInstanceClass'),
140
+ 'engine': db.get('Engine'),
141
+ 'avg_cpu_utilization': round(avg_cpu, 2),
142
+ 'status': db.get('DBInstanceStatus'),
143
+ 'creation_time': db.get('InstanceCreateTime')
144
+ }
145
+ region_low_cpu_instances.append(instance_info)
146
+
147
+ except ClientError as e:
148
+ print_warning(f"Could not get metrics for {db_identifier}: {e}")
149
+ continue
150
+
151
+ if region_low_cpu_instances:
152
+ results.append(OperationResult(
153
+ status=OperationStatus.SUCCESS,
154
+ data=region_low_cpu_instances,
155
+ metadata={
156
+ 'region': region,
157
+ 'threshold': utilization_threshold,
158
+ 'duration_minutes': duration_minutes,
159
+ 'instances_found': len(region_low_cpu_instances)
160
+ }
161
+ ))
162
+
163
+ print_info(f"Found {len(region_low_cpu_instances)} low CPU instances in {region}")
164
+
165
+ except ClientError as e:
166
+ print_error(f"Error analyzing region {region}: {e}")
167
+ results.append(OperationResult(
168
+ status=OperationStatus.FAILED,
169
+ error=str(e),
170
+ metadata={'region': region}
171
+ ))
172
+
173
+ if results:
174
+ total_instances = sum(
175
+ len(r.data) for r in results
176
+ if r.status == OperationStatus.SUCCESS and r.data
177
+ )
178
+ print_success(f"Analysis complete. Found {total_instances} instances below {utilization_threshold}% CPU")
179
+ else:
180
+ print_info("No instances found below threshold")
181
+
182
+ return results
183
+
184
+ def get_publicly_accessible_instances(self, context: OperationContext) -> List[OperationResult]:
185
+ """
186
+ Get all publicly accessible RDS instances.
187
+
188
+ Extracted from: AWS_Secure_Publicly_Accessible_RDS_Instances.ipynb
189
+
190
+ Args:
191
+ context: Operation context
192
+
193
+ Returns:
194
+ List of operation results with publicly accessible instances
195
+ """
196
+ print_header("RDS Public Access Analysis", "Security Compliance")
197
+
198
+ results = []
199
+ regions_to_check = [context.region] if context.region else self._get_all_regions()
200
+
201
+ for region in track(regions_to_check, description="Scanning regions..."):
202
+ try:
203
+ session = self._get_aws_session(context.profile)
204
+ rds_client = session.client('rds', region_name=region)
205
+
206
+ # Get all RDS instances
207
+ instances = get_paginated_results(
208
+ rds_client,
209
+ 'describe_db_instances',
210
+ 'DBInstances'
211
+ )
212
+
213
+ public_instances = []
214
+
215
+ for db in instances:
216
+ if db.get('PubliclyAccessible', False):
217
+ instance_info = {
218
+ 'region': region,
219
+ 'instance_id': db['DBInstanceIdentifier'],
220
+ 'instance_class': db.get('DBInstanceClass'),
221
+ 'engine': db.get('Engine'),
222
+ 'status': db.get('DBInstanceStatus'),
223
+ 'endpoint': db.get('Endpoint', {}).get('Address'),
224
+ 'port': db.get('Endpoint', {}).get('Port'),
225
+ 'vpc_id': db.get('DBSubnetGroup', {}).get('VpcId') if db.get('DBSubnetGroup') else None
226
+ }
227
+ public_instances.append(instance_info)
228
+
229
+ if public_instances:
230
+ results.append(OperationResult(
231
+ status=OperationStatus.SUCCESS,
232
+ data=public_instances,
233
+ metadata={
234
+ 'region': region,
235
+ 'public_instances_found': len(public_instances)
236
+ }
237
+ ))
238
+ print_warning(f"Found {len(public_instances)} publicly accessible instances in {region}")
239
+
240
+ except ClientError as e:
241
+ print_error(f"Error scanning region {region}: {e}")
242
+ results.append(OperationResult(
243
+ status=OperationStatus.FAILED,
244
+ error=str(e),
245
+ metadata={'region': region}
246
+ ))
247
+
248
+ total_public = sum(
249
+ len(r.data) for r in results
250
+ if r.status == OperationStatus.SUCCESS and r.data
251
+ )
252
+
253
+ if total_public > 0:
254
+ print_warning(f"Security Alert: {total_public} publicly accessible RDS instances found")
255
+ else:
256
+ print_success("No publicly accessible RDS instances found")
257
+
258
+ return results
259
+
260
+ def secure_public_instances(
261
+ self,
262
+ context: OperationContext,
263
+ instance_identifiers: List[str]
264
+ ) -> List[OperationResult]:
265
+ """
266
+ Make RDS instances not publicly accessible.
267
+
268
+ Extracted from: AWS_Secure_Publicly_Accessible_RDS_Instances.ipynb
269
+
270
+ Args:
271
+ context: Operation context
272
+ instance_identifiers: List of instance identifiers to secure
273
+
274
+ Returns:
275
+ List of operation results
276
+ """
277
+ print_header("RDS Security Remediation", "Making Instances Private")
278
+
279
+ if self.dry_run:
280
+ print_info("DRY RUN: Would secure the following instances:")
281
+ for instance_id in instance_identifiers:
282
+ print_info(f" - {instance_id} (would set PubliclyAccessible=False)")
283
+ return [OperationResult(
284
+ status=OperationStatus.SUCCESS,
285
+ data={'dry_run': True, 'instances': instance_identifiers},
286
+ metadata={'operation': 'secure_instances_dry_run'}
287
+ )]
288
+
289
+ results = []
290
+
291
+ for instance_identifier in track(instance_identifiers, description="Securing instances..."):
292
+ try:
293
+ # Determine region for instance (simplified - assumes current region)
294
+ session = self._get_aws_session(context.profile)
295
+ rds_client = session.client('rds', region_name=context.region)
296
+
297
+ # Modify instance to make it not publicly accessible
298
+ response = rds_client.modify_db_instance(
299
+ DBInstanceIdentifier=instance_identifier,
300
+ PubliclyAccessible=False
301
+ )
302
+
303
+ results.append(OperationResult(
304
+ status=OperationStatus.SUCCESS,
305
+ data={'instance_id': instance_identifier, 'response': response},
306
+ metadata={'operation': 'modify_public_access'}
307
+ ))
308
+
309
+ print_success(f"Initiated security modification for {instance_identifier}")
310
+
311
+ except ClientError as e:
312
+ print_error(f"Failed to secure instance {instance_identifier}: {e}")
313
+ results.append(OperationResult(
314
+ status=OperationStatus.FAILED,
315
+ error=str(e),
316
+ metadata={'instance_id': instance_identifier}
317
+ ))
318
+
319
+ return results
320
+
321
+ def analyze_reserved_instance_opportunities(
322
+ self,
323
+ context: OperationContext,
324
+ threshold_days: int = 30
325
+ ) -> List[OperationResult]:
326
+ """
327
+ Find long-running instances without reserved instances.
328
+
329
+ Extracted from: AWS_Purchase_Reserved_Instances_For_Long_Running_RDS_Instances.ipynb
330
+
331
+ Args:
332
+ context: Operation context
333
+ threshold_days: Minimum days running to consider for RI (default 30)
334
+
335
+ Returns:
336
+ List of operation results with RI opportunities
337
+ """
338
+ print_header("RDS Reserved Instance Analysis", "Cost Optimization")
339
+
340
+ results = []
341
+ regions_to_check = [context.region] if context.region else self._get_all_regions()
342
+
343
+ for region in track(regions_to_check, description="Analyzing RI opportunities..."):
344
+ try:
345
+ session = self._get_aws_session(context.profile)
346
+ rds_client = session.client('rds', region_name=region)
347
+
348
+ # Get reserved instances per region
349
+ reserved_response = rds_client.describe_reserved_db_instances()
350
+ reserved_by_class = {}
351
+
352
+ for reserved in reserved_response.get('ReservedDBInstances', []):
353
+ instance_class = reserved['DBInstanceClass']
354
+ reserved_by_class[instance_class] = reserved_by_class.get(instance_class, 0) + reserved.get('DBInstanceCount', 1)
355
+
356
+ # Get running instances
357
+ instances = get_paginated_results(
358
+ rds_client,
359
+ 'describe_db_instances',
360
+ 'DBInstances'
361
+ )
362
+
363
+ ri_opportunities = []
364
+
365
+ for db in instances:
366
+ if db.get('DBInstanceStatus') == 'available':
367
+ # Check if instance has been running long enough
368
+ create_time = db.get('InstanceCreateTime')
369
+ if create_time:
370
+ uptime = datetime.now(timezone.utc) - create_time
371
+ if uptime > timedelta(days=threshold_days):
372
+
373
+ instance_class = db.get('DBInstanceClass')
374
+ reserved_count = reserved_by_class.get(instance_class, 0)
375
+
376
+ # Count running instances of this class
377
+ running_count = sum(
378
+ 1 for inst in instances
379
+ if (inst.get('DBInstanceClass') == instance_class and
380
+ inst.get('DBInstanceStatus') == 'available')
381
+ )
382
+
383
+ if running_count > reserved_count:
384
+ ri_opportunity = {
385
+ 'region': region,
386
+ 'instance_id': db['DBInstanceIdentifier'],
387
+ 'instance_class': instance_class,
388
+ 'engine': db.get('Engine'),
389
+ 'running_days': uptime.days,
390
+ 'reserved_count': reserved_count,
391
+ 'running_count': running_count,
392
+ 'ri_gap': running_count - reserved_count
393
+ }
394
+ ri_opportunities.append(ri_opportunity)
395
+
396
+ if ri_opportunities:
397
+ results.append(OperationResult(
398
+ status=OperationStatus.SUCCESS,
399
+ data=ri_opportunities,
400
+ metadata={
401
+ 'region': region,
402
+ 'threshold_days': threshold_days,
403
+ 'opportunities_found': len(ri_opportunities)
404
+ }
405
+ ))
406
+
407
+ print_info(f"Found {len(ri_opportunities)} RI opportunities in {region}")
408
+
409
+ except ClientError as e:
410
+ print_error(f"Error analyzing RI opportunities in {region}: {e}")
411
+ results.append(OperationResult(
412
+ status=OperationStatus.FAILED,
413
+ error=str(e),
414
+ metadata={'region': region}
415
+ ))
416
+
417
+ return results
418
+
419
+ def delete_instance_safely(
420
+ self,
421
+ context: OperationContext,
422
+ instance_identifier: str,
423
+ skip_final_snapshot: bool = False
424
+ ) -> OperationResult:
425
+ """
426
+ Safely delete an RDS instance with proper safeguards.
427
+
428
+ Extracted from: AWS_Delete_RDS_Instances_with_Low_CPU_Utilization.ipynb
429
+
430
+ Args:
431
+ context: Operation context
432
+ instance_identifier: RDS instance identifier
433
+ skip_final_snapshot: Whether to skip final snapshot (default False)
434
+
435
+ Returns:
436
+ Operation result
437
+ """
438
+ print_header(f"RDS Instance Deletion", f"Instance: {instance_identifier}")
439
+
440
+ if self.dry_run:
441
+ print_warning(f"DRY RUN: Would delete RDS instance {instance_identifier}")
442
+ return OperationResult(
443
+ status=OperationStatus.SUCCESS,
444
+ data={'dry_run': True, 'instance_id': instance_identifier},
445
+ metadata={'operation': 'delete_instance_dry_run'}
446
+ )
447
+
448
+ # Safety confirmation for destructive operation
449
+ print_warning(f"DESTRUCTIVE OPERATION: Preparing to delete RDS instance {instance_identifier}")
450
+
451
+ try:
452
+ session = self._get_aws_session(context.profile)
453
+ rds_client = session.client('rds', region_name=context.region)
454
+
455
+ delete_params = {
456
+ 'DBInstanceIdentifier': instance_identifier,
457
+ 'SkipFinalSnapshot': skip_final_snapshot
458
+ }
459
+
460
+ if not skip_final_snapshot:
461
+ # Create snapshot with timestamp
462
+ timestamp = datetime.utcnow().strftime('%Y-%m-%d-%H-%M')
463
+ delete_params['FinalDBSnapshotIdentifier'] = f"{instance_identifier}-final-{timestamp}"
464
+ print_info(f"Creating final snapshot: {delete_params['FinalDBSnapshotIdentifier']}")
465
+
466
+ response = rds_client.delete_db_instance(**delete_params)
467
+
468
+ print_success(f"RDS instance {instance_identifier} deletion initiated")
469
+
470
+ return OperationResult(
471
+ status=OperationStatus.SUCCESS,
472
+ data={'instance_id': instance_identifier, 'response': response},
473
+ metadata={'operation': 'delete_instance', 'final_snapshot': not skip_final_snapshot}
474
+ )
475
+
476
+ except ClientError as e:
477
+ print_error(f"Failed to delete instance {instance_identifier}: {e}")
478
+ return OperationResult(
479
+ status=OperationStatus.FAILED,
480
+ error=str(e),
481
+ metadata={'instance_id': instance_identifier}
482
+ )
483
+
484
+ def execute_operation(self, operation: str, context: OperationContext, **kwargs) -> List[OperationResult]:
485
+ """Execute the specified RDS operation."""
486
+ if operation not in self.supported_operations:
487
+ raise ValueError(f"Unsupported operation: {operation}")
488
+
489
+ operation_method = getattr(self, operation)
490
+ return operation_method(context, **kwargs)
491
+
492
+ def _get_all_regions(self) -> List[str]:
493
+ """Get all available AWS regions for RDS."""
494
+ try:
495
+ session = boto3.Session()
496
+ return session.get_available_regions('rds')
497
+ except Exception:
498
+ # Fallback to common regions if API call fails
499
+ return [
500
+ 'us-east-1', 'us-west-2', 'eu-west-1', 'ap-southeast-1',
501
+ 'us-west-1', 'eu-central-1', 'ap-southeast-2'
502
+ ]
503
+
504
+ def _get_aws_session(self, profile: Optional[str] = None) -> boto3.Session:
505
+ """Get AWS session with proper profile handling."""
506
+ if profile:
507
+ return boto3.Session(profile_name=profile)
508
+ return boto3.Session()