runbooks 0.7.6__py3-none-any.whl → 0.7.9__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/base.py +5 -1
  3. runbooks/cfat/__init__.py +8 -4
  4. runbooks/cfat/assessment/collectors.py +171 -14
  5. runbooks/cfat/assessment/compliance.py +871 -0
  6. runbooks/cfat/assessment/runner.py +122 -11
  7. runbooks/cfat/models.py +6 -2
  8. runbooks/common/logger.py +14 -0
  9. runbooks/common/rich_utils.py +451 -0
  10. runbooks/enterprise/__init__.py +68 -0
  11. runbooks/enterprise/error_handling.py +411 -0
  12. runbooks/enterprise/logging.py +439 -0
  13. runbooks/enterprise/multi_tenant.py +583 -0
  14. runbooks/finops/README.md +468 -241
  15. runbooks/finops/__init__.py +39 -3
  16. runbooks/finops/cli.py +83 -18
  17. runbooks/finops/cross_validation.py +375 -0
  18. runbooks/finops/dashboard_runner.py +812 -164
  19. runbooks/finops/enhanced_dashboard_runner.py +525 -0
  20. runbooks/finops/finops_dashboard.py +1892 -0
  21. runbooks/finops/helpers.py +485 -51
  22. runbooks/finops/optimizer.py +823 -0
  23. runbooks/finops/tests/__init__.py +19 -0
  24. runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
  25. runbooks/finops/tests/run_comprehensive_tests.py +421 -0
  26. runbooks/finops/tests/run_tests.py +305 -0
  27. runbooks/finops/tests/test_finops_dashboard.py +705 -0
  28. runbooks/finops/tests/test_integration.py +477 -0
  29. runbooks/finops/tests/test_performance.py +380 -0
  30. runbooks/finops/tests/test_performance_benchmarks.py +500 -0
  31. runbooks/finops/tests/test_reference_images_validation.py +867 -0
  32. runbooks/finops/tests/test_single_account_features.py +715 -0
  33. runbooks/finops/tests/validate_test_suite.py +220 -0
  34. runbooks/finops/types.py +1 -1
  35. runbooks/hitl/enhanced_workflow_engine.py +725 -0
  36. runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
  37. runbooks/inventory/collectors/aws_comprehensive.py +442 -0
  38. runbooks/inventory/collectors/enterprise_scale.py +281 -0
  39. runbooks/inventory/core/collector.py +172 -13
  40. runbooks/inventory/discovery.md +1 -1
  41. runbooks/inventory/list_ec2_instances.py +18 -20
  42. runbooks/inventory/list_ssm_parameters.py +31 -3
  43. runbooks/inventory/organizations_discovery.py +1269 -0
  44. runbooks/inventory/rich_inventory_display.py +393 -0
  45. runbooks/inventory/run_on_multi_accounts.py +35 -19
  46. runbooks/inventory/runbooks.security.report_generator.log +0 -0
  47. runbooks/inventory/runbooks.security.run_script.log +0 -0
  48. runbooks/inventory/vpc_flow_analyzer.py +1030 -0
  49. runbooks/main.py +2215 -119
  50. runbooks/metrics/dora_metrics_engine.py +599 -0
  51. runbooks/operate/__init__.py +2 -2
  52. runbooks/operate/base.py +122 -10
  53. runbooks/operate/deployment_framework.py +1032 -0
  54. runbooks/operate/deployment_validator.py +853 -0
  55. runbooks/operate/dynamodb_operations.py +10 -6
  56. runbooks/operate/ec2_operations.py +319 -11
  57. runbooks/operate/executive_dashboard.py +779 -0
  58. runbooks/operate/mcp_integration.py +750 -0
  59. runbooks/operate/nat_gateway_operations.py +1120 -0
  60. runbooks/operate/networking_cost_heatmap.py +685 -0
  61. runbooks/operate/privatelink_operations.py +940 -0
  62. runbooks/operate/s3_operations.py +10 -6
  63. runbooks/operate/vpc_endpoints.py +644 -0
  64. runbooks/operate/vpc_operations.py +1038 -0
  65. runbooks/remediation/__init__.py +2 -2
  66. runbooks/remediation/acm_remediation.py +1 -1
  67. runbooks/remediation/base.py +1 -1
  68. runbooks/remediation/cloudtrail_remediation.py +1 -1
  69. runbooks/remediation/cognito_remediation.py +1 -1
  70. runbooks/remediation/dynamodb_remediation.py +1 -1
  71. runbooks/remediation/ec2_remediation.py +1 -1
  72. runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
  73. runbooks/remediation/kms_enable_key_rotation.py +1 -1
  74. runbooks/remediation/kms_remediation.py +1 -1
  75. runbooks/remediation/lambda_remediation.py +1 -1
  76. runbooks/remediation/multi_account.py +1 -1
  77. runbooks/remediation/rds_remediation.py +1 -1
  78. runbooks/remediation/s3_block_public_access.py +1 -1
  79. runbooks/remediation/s3_enable_access_logging.py +1 -1
  80. runbooks/remediation/s3_encryption.py +1 -1
  81. runbooks/remediation/s3_remediation.py +1 -1
  82. runbooks/remediation/vpc_remediation.py +475 -0
  83. runbooks/security/__init__.py +3 -1
  84. runbooks/security/compliance_automation.py +632 -0
  85. runbooks/security/report_generator.py +10 -0
  86. runbooks/security/run_script.py +31 -5
  87. runbooks/security/security_baseline_tester.py +169 -30
  88. runbooks/security/security_export.py +477 -0
  89. runbooks/validation/__init__.py +10 -0
  90. runbooks/validation/benchmark.py +484 -0
  91. runbooks/validation/cli.py +356 -0
  92. runbooks/validation/mcp_validator.py +768 -0
  93. runbooks/vpc/__init__.py +38 -0
  94. runbooks/vpc/config.py +212 -0
  95. runbooks/vpc/cost_engine.py +347 -0
  96. runbooks/vpc/heatmap_engine.py +605 -0
  97. runbooks/vpc/manager_interface.py +634 -0
  98. runbooks/vpc/networking_wrapper.py +1260 -0
  99. runbooks/vpc/rich_formatters.py +679 -0
  100. runbooks/vpc/tests/__init__.py +5 -0
  101. runbooks/vpc/tests/conftest.py +356 -0
  102. runbooks/vpc/tests/test_cli_integration.py +530 -0
  103. runbooks/vpc/tests/test_config.py +458 -0
  104. runbooks/vpc/tests/test_cost_engine.py +479 -0
  105. runbooks/vpc/tests/test_networking_wrapper.py +512 -0
  106. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
  107. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
  108. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
  109. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
  110. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
  111. {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,940 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ AWS PrivateLink Operations for CloudOps Runbooks Platform
4
+
5
+ This module provides enterprise-grade AWS PrivateLink service management with
6
+ comprehensive service discovery, security compliance, and cost optimization
7
+ following McKinsey operational excellence principles.
8
+
9
+ Addresses GitHub Issue #96 expanded scope: AWS PrivateLink management for
10
+ enterprise service connectivity with security and cost optimization.
11
+
12
+ Features:
13
+ - PrivateLink service lifecycle management (create, modify, delete)
14
+ - Enterprise service catalog integration and discovery
15
+ - Security compliance validation and automated assessments
16
+ - Cross-account service sharing management
17
+ - Cost optimization analysis for private connectivity
18
+ - McKinsey-style ROI frameworks for PrivateLink investments
19
+ - Integration with existing VPC and endpoint operations
20
+
21
+ Author: CloudOps Runbooks Team
22
+ Version: 0.7.8
23
+ Enhanced for Sprint 2 VPC Scope Expansion with PrivateLink
24
+ """
25
+
26
+ import json
27
+ import logging
28
+ from datetime import datetime, timedelta
29
+ from typing import Any, Dict, List, Optional, Set, Tuple
30
+
31
+ import boto3
32
+ from botocore.exceptions import BotoCoreError, ClientError
33
+
34
+ from runbooks.operate.base import BaseOperation, OperationResult
35
+ from runbooks.common.rich_utils import (
36
+ console,
37
+ create_panel,
38
+ create_table,
39
+ create_tree,
40
+ format_cost,
41
+ print_error,
42
+ print_status,
43
+ print_success,
44
+ print_warning,
45
+ )
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ class PrivateLinkOperations(BaseOperation):
51
+ """
52
+ Enterprise AWS PrivateLink operations with service discovery and cost optimization.
53
+
54
+ Extends BaseOperation following CloudOps Runbooks patterns for:
55
+ - PrivateLink service endpoint management
56
+ - Cross-account service sharing and discovery
57
+ - Security compliance and access control
58
+ - Enterprise service catalog integration
59
+ - McKinsey-style cost-benefit analysis for private connectivity
60
+
61
+ GitHub Issue #96 - VPC & Infrastructure Enhancement (PrivateLink scope)
62
+ """
63
+
64
+ service_name = "ec2"
65
+ supported_operations = {
66
+ "create_service",
67
+ "delete_service",
68
+ "modify_service",
69
+ "describe_services",
70
+ "manage_service_permissions",
71
+ "discover_available_services",
72
+ "create_service_connection",
73
+ "analyze_privatelink_costs",
74
+ "security_compliance_check",
75
+ "optimize_service_placement",
76
+ "generate_service_catalog",
77
+ }
78
+ requires_confirmation = True # PrivateLink services have cost and security implications
79
+
80
+ # PrivateLink pricing (monthly estimates in USD)
81
+ PRIVATELINK_PRICING = {
82
+ "vpc_endpoint_service_hour": 0.01, # $0.01/hour per VPC endpoint service
83
+ "network_load_balancer_hour": 0.0225, # $0.0225/hour per NLB (required for PrivateLink)
84
+ "data_processing_gb": 0.01, # $0.01 per GB processed
85
+ "cross_az_data_transfer": 0.01, # $0.01 per GB for cross-AZ data transfer
86
+ }
87
+
88
+ def __init__(self, profile: str, region: str, dry_run: bool = True):
89
+ """Initialize PrivateLink operations."""
90
+ super().__init__(profile, region, dry_run)
91
+ self.ec2_client = self.session.client("ec2")
92
+ self.elbv2_client = self.session.client("elbv2")
93
+ self.organizations_client = None
94
+ try:
95
+ self.organizations_client = self.session.client("organizations")
96
+ except Exception:
97
+ logger.info("Organizations client not available - cross-account features limited")
98
+
99
+ def create_service(
100
+ self,
101
+ load_balancer_arns: List[str],
102
+ service_name: Optional[str] = None,
103
+ acceptance_required: bool = True,
104
+ allowed_principals: Optional[List[str]] = None,
105
+ gateway_load_balancer_arns: Optional[List[str]] = None,
106
+ tags: Optional[Dict[str, str]] = None,
107
+ ) -> OperationResult:
108
+ """
109
+ Create a PrivateLink service endpoint with enterprise security and cost analysis.
110
+
111
+ Args:
112
+ load_balancer_arns: Network Load Balancer ARNs to expose via PrivateLink
113
+ service_name: Custom service name (optional)
114
+ acceptance_required: Whether connections require manual acceptance
115
+ allowed_principals: AWS principals allowed to connect
116
+ gateway_load_balancer_arns: Gateway Load Balancer ARNs (optional)
117
+ tags: Resource tags for governance and cost tracking
118
+
119
+ Returns:
120
+ OperationResult with service creation details and cost analysis
121
+ """
122
+ try:
123
+ print_status("Creating PrivateLink service endpoint with cost analysis...", "info")
124
+
125
+ # Validate load balancers and calculate cost impact
126
+ validation_result = self._validate_load_balancers(load_balancer_arns)
127
+ if not validation_result["valid"]:
128
+ return self.create_result(
129
+ success=False,
130
+ message=f"Load balancer validation failed: {validation_result['message']}",
131
+ data=validation_result,
132
+ )
133
+
134
+ # Calculate estimated costs
135
+ cost_analysis = self._calculate_service_costs(load_balancer_arns)
136
+
137
+ # Display cost analysis
138
+ self._display_service_cost_analysis(cost_analysis)
139
+
140
+ if self.dry_run:
141
+ print_status("DRY RUN: PrivateLink service creation simulated", "warning")
142
+ return self.create_result(
143
+ success=True,
144
+ message="DRY RUN: Would create PrivateLink service",
145
+ data={
146
+ "load_balancer_arns": load_balancer_arns,
147
+ "estimated_monthly_cost": cost_analysis["monthly_cost"],
148
+ "cost_analysis": cost_analysis,
149
+ "dry_run": True,
150
+ },
151
+ )
152
+
153
+ # Create the service
154
+ create_params = {"NetworkLoadBalancerArns": load_balancer_arns, "AcceptanceRequired": acceptance_required}
155
+
156
+ if gateway_load_balancer_arns:
157
+ create_params["GatewayLoadBalancerArns"] = gateway_load_balancer_arns
158
+
159
+ if tags:
160
+ tag_specs = [
161
+ {"ResourceType": "vpc-endpoint-service", "Tags": [{"Key": k, "Value": v} for k, v in tags.items()]}
162
+ ]
163
+ create_params["TagSpecifications"] = tag_specs
164
+
165
+ response = self.ec2_client.create_vpc_endpoint_service_configuration(**create_params)
166
+ service_config = response["ServiceConfiguration"]
167
+ service_name = service_config["ServiceName"]
168
+ service_id = service_config["ServiceId"]
169
+
170
+ # Set up allowed principals if specified
171
+ if allowed_principals:
172
+ self._manage_service_permissions_internal(service_name, allowed_principals, "add")
173
+
174
+ # Add cost tracking and governance tags
175
+ governance_tags = {
176
+ "CloudOps-ServiceType": "PrivateLink",
177
+ "CloudOps-CostCenter": "NetworkOptimization",
178
+ "CloudOps-CreatedBy": "PrivateLinkOperations",
179
+ "CloudOps-EstimatedMonthlyCost": str(cost_analysis["monthly_cost"]),
180
+ "CloudOps-CreatedAt": datetime.now().isoformat(),
181
+ "CloudOps-ComplianceRequired": "true",
182
+ }
183
+
184
+ self._tag_service(service_name, governance_tags)
185
+
186
+ print_success(f"PrivateLink service created: {service_name}")
187
+
188
+ return self.create_result(
189
+ success=True,
190
+ message=f"PrivateLink service created successfully: {service_name}",
191
+ data={
192
+ "service_name": service_name,
193
+ "service_id": service_id,
194
+ "service_type": service_config["ServiceType"],
195
+ "service_state": service_config["ServiceState"],
196
+ "acceptance_required": service_config["AcceptanceRequired"],
197
+ "manages_vpc_endpoints": service_config["ManagesVpcEndpoints"],
198
+ "estimated_monthly_cost": cost_analysis["monthly_cost"],
199
+ "cost_analysis": cost_analysis,
200
+ "created_at": datetime.now().isoformat(),
201
+ },
202
+ )
203
+
204
+ except ClientError as e:
205
+ error_msg = f"Failed to create PrivateLink service: {e.response['Error']['Message']}"
206
+ print_error(error_msg, e)
207
+ return self.create_result(
208
+ success=False, message=error_msg, data={"error_code": e.response["Error"]["Code"]}
209
+ )
210
+ except Exception as e:
211
+ error_msg = f"Unexpected error creating PrivateLink service: {str(e)}"
212
+ print_error(error_msg, e)
213
+ return self.create_result(success=False, message=error_msg, data={"error": str(e)})
214
+
215
+ def delete_service(self, service_name: str) -> OperationResult:
216
+ """
217
+ Delete a PrivateLink service with impact analysis.
218
+
219
+ Args:
220
+ service_name: PrivateLink service name to delete
221
+
222
+ Returns:
223
+ OperationResult with deletion status and impact analysis
224
+ """
225
+ try:
226
+ print_status(f"Deleting PrivateLink service {service_name}", "warning")
227
+
228
+ # Get service details for impact analysis
229
+ service_details = self._get_service_details(service_name)
230
+ if not service_details:
231
+ return self.create_result(
232
+ success=False,
233
+ message=f"PrivateLink service {service_name} not found",
234
+ data={"service_name": service_name},
235
+ )
236
+
237
+ # Analyze deletion impact
238
+ impact_analysis = self._analyze_deletion_impact(service_name, service_details)
239
+
240
+ # Check for active connections
241
+ connections = self._get_service_connections(service_name)
242
+ if connections and len(connections) > 0:
243
+ print_warning(f"Service has {len(connections)} active connections that will be terminated")
244
+ impact_analysis["active_connections"] = len(connections)
245
+ impact_analysis["connection_impact"] = "HIGH"
246
+
247
+ if self.dry_run:
248
+ print_status("DRY RUN: PrivateLink service deletion simulated", "warning")
249
+ return self.create_result(
250
+ success=True,
251
+ message=f"DRY RUN: Would delete service {service_name}",
252
+ data={"service_name": service_name, "impact_analysis": impact_analysis, "dry_run": True},
253
+ )
254
+
255
+ # Perform deletion
256
+ response = self.ec2_client.delete_vpc_endpoint_service_configurations(
257
+ ServiceIds=[service_details["ServiceId"]]
258
+ )
259
+
260
+ if response["Unsuccessful"]:
261
+ error_detail = response["Unsuccessful"][0]
262
+ error_msg = f"Failed to delete service {service_name}: {error_detail['Error']['Message']}"
263
+ print_error(error_msg)
264
+ return self.create_result(success=False, message=error_msg, data={"error": error_detail["Error"]})
265
+
266
+ print_success(f"PrivateLink service {service_name} deleted successfully")
267
+
268
+ return self.create_result(
269
+ success=True,
270
+ message=f"PrivateLink service deleted successfully: {service_name}",
271
+ data={
272
+ "service_name": service_name,
273
+ "impact_analysis": impact_analysis,
274
+ "deleted_at": datetime.now().isoformat(),
275
+ },
276
+ )
277
+
278
+ except ClientError as e:
279
+ error_msg = f"Failed to delete PrivateLink service: {e.response['Error']['Message']}"
280
+ print_error(error_msg, e)
281
+ return self.create_result(
282
+ success=False, message=error_msg, data={"error_code": e.response["Error"]["Code"]}
283
+ )
284
+ except Exception as e:
285
+ error_msg = f"Unexpected error deleting PrivateLink service: {str(e)}"
286
+ print_error(error_msg, e)
287
+ return self.create_result(success=False, message=error_msg, data={"error": str(e)})
288
+
289
+ def describe_services(self, service_names: Optional[List[str]] = None) -> OperationResult:
290
+ """
291
+ Describe PrivateLink services with comprehensive analysis and optimization recommendations.
292
+
293
+ Args:
294
+ service_names: Specific service names to describe (optional)
295
+
296
+ Returns:
297
+ OperationResult with service details, cost analysis, and recommendations
298
+ """
299
+ try:
300
+ print_status("Retrieving PrivateLink services with analysis...", "info")
301
+
302
+ describe_params = {}
303
+ if service_names:
304
+ describe_params["ServiceNames"] = service_names
305
+
306
+ # Get service configurations
307
+ response = self.ec2_client.describe_vpc_endpoint_service_configurations(**describe_params)
308
+ services = response["ServiceConfigurations"]
309
+
310
+ if not services:
311
+ print_status("No PrivateLink services found", "info")
312
+ return self.create_result(
313
+ success=True, message="No PrivateLink services found", data={"services": [], "total_count": 0}
314
+ )
315
+
316
+ # Enhance services with detailed analysis
317
+ enhanced_services = []
318
+ total_monthly_cost = 0.0
319
+
320
+ for service in services:
321
+ enhanced_service = self._enhance_service_with_analysis(service)
322
+ enhanced_services.append(enhanced_service)
323
+ total_monthly_cost += enhanced_service.get("estimated_monthly_cost", 0)
324
+
325
+ # Display services table
326
+ self._display_services_table(enhanced_services, total_monthly_cost)
327
+
328
+ # Generate enterprise recommendations
329
+ recommendations = self._generate_enterprise_recommendations(enhanced_services)
330
+
331
+ print_success(
332
+ f"Found {len(services)} PrivateLink services with total estimated cost: ${total_monthly_cost:.2f}/month"
333
+ )
334
+
335
+ return self.create_result(
336
+ success=True,
337
+ message=f"Retrieved {len(services)} PrivateLink services",
338
+ data={
339
+ "services": enhanced_services,
340
+ "total_count": len(services),
341
+ "total_monthly_cost": total_monthly_cost,
342
+ "enterprise_recommendations": recommendations,
343
+ },
344
+ )
345
+
346
+ except ClientError as e:
347
+ error_msg = f"Failed to describe PrivateLink services: {e.response['Error']['Message']}"
348
+ print_error(error_msg, e)
349
+ return self.create_result(
350
+ success=False, message=error_msg, data={"error_code": e.response["Error"]["Code"]}
351
+ )
352
+ except Exception as e:
353
+ error_msg = f"Unexpected error describing PrivateLink services: {str(e)}"
354
+ print_error(error_msg, e)
355
+ return self.create_result(success=False, message=error_msg, data={"error": str(e)})
356
+
357
+ def discover_available_services(self, service_name_filter: Optional[str] = None) -> OperationResult:
358
+ """
359
+ Discover available PrivateLink services for connection with enterprise filtering.
360
+
361
+ Args:
362
+ service_name_filter: Filter services by name pattern
363
+
364
+ Returns:
365
+ OperationResult with available services and connection recommendations
366
+ """
367
+ try:
368
+ print_status("Discovering available PrivateLink services...", "info")
369
+
370
+ describe_params = {}
371
+ if service_name_filter:
372
+ describe_params["Filters"] = [{"Name": "service-name", "Values": [f"*{service_name_filter}*"]}]
373
+
374
+ # Discover services
375
+ response = self.ec2_client.describe_vpc_endpoint_services(**describe_params)
376
+ services = response.get("ServiceDetails", [])
377
+ service_names = response.get("ServiceNames", [])
378
+
379
+ # Enhance discovery results with enterprise context
380
+ enhanced_discovery = {
381
+ "available_services": len(service_names),
382
+ "detailed_services": len(services),
383
+ "aws_managed_services": [name for name in service_names if name.startswith("com.amazonaws")],
384
+ "customer_managed_services": [name for name in service_names if not name.startswith("com.amazonaws")],
385
+ "discovery_timestamp": datetime.now().isoformat(),
386
+ }
387
+
388
+ # Analyze services by category
389
+ service_categories = self._categorize_available_services(service_names)
390
+
391
+ # Generate connection recommendations
392
+ connection_recommendations = self._generate_connection_recommendations(services, service_categories)
393
+
394
+ # Display discovery results
395
+ self._display_service_discovery_results(enhanced_discovery, service_categories)
396
+
397
+ print_success(f"Discovered {len(service_names)} available PrivateLink services")
398
+
399
+ return self.create_result(
400
+ success=True,
401
+ message=f"Discovered {len(service_names)} available PrivateLink services",
402
+ data={
403
+ "discovery_summary": enhanced_discovery,
404
+ "service_names": service_names,
405
+ "service_details": services,
406
+ "service_categories": service_categories,
407
+ "connection_recommendations": connection_recommendations,
408
+ },
409
+ )
410
+
411
+ except ClientError as e:
412
+ error_msg = f"Failed to discover PrivateLink services: {e.response['Error']['Message']}"
413
+ print_error(error_msg, e)
414
+ return self.create_result(
415
+ success=False, message=error_msg, data={"error_code": e.response["Error"]["Code"]}
416
+ )
417
+ except Exception as e:
418
+ error_msg = f"Unexpected error discovering PrivateLink services: {str(e)}"
419
+ print_error(error_msg, e)
420
+ return self.create_result(success=False, message=error_msg, data={"error": str(e)})
421
+
422
+ def security_compliance_check(self, service_name: str) -> OperationResult:
423
+ """
424
+ Perform comprehensive security compliance check on PrivateLink service.
425
+
426
+ Args:
427
+ service_name: PrivateLink service name to check
428
+
429
+ Returns:
430
+ OperationResult with detailed security compliance assessment
431
+ """
432
+ try:
433
+ print_status(f"Performing security compliance check for {service_name}...", "info")
434
+
435
+ # Get service configuration
436
+ service_details = self._get_service_details(service_name)
437
+ if not service_details:
438
+ return self.create_result(
439
+ success=False,
440
+ message=f"Service {service_name} not found for compliance check",
441
+ data={"service_name": service_name},
442
+ )
443
+
444
+ compliance_results = {
445
+ "service_name": service_name,
446
+ "assessment_timestamp": datetime.now().isoformat(),
447
+ "compliance_score": 0,
448
+ "total_checks": 0,
449
+ "passed_checks": 0,
450
+ "failed_checks": 0,
451
+ "checks": [],
452
+ "recommendations": [],
453
+ "risk_level": "UNKNOWN",
454
+ }
455
+
456
+ # Security check 1: Acceptance requirement
457
+ check_result = self._check_acceptance_requirement(service_details)
458
+ compliance_results["checks"].append(check_result)
459
+ compliance_results["total_checks"] += 1
460
+ if check_result["passed"]:
461
+ compliance_results["passed_checks"] += 1
462
+ else:
463
+ compliance_results["failed_checks"] += 1
464
+ compliance_results["recommendations"].append(check_result["recommendation"])
465
+
466
+ # Security check 2: Principal restrictions
467
+ check_result = self._check_principal_restrictions(service_name)
468
+ compliance_results["checks"].append(check_result)
469
+ compliance_results["total_checks"] += 1
470
+ if check_result["passed"]:
471
+ compliance_results["passed_checks"] += 1
472
+ else:
473
+ compliance_results["failed_checks"] += 1
474
+ compliance_results["recommendations"].append(check_result["recommendation"])
475
+
476
+ # Security check 3: Load balancer security
477
+ check_result = self._check_load_balancer_security(service_details)
478
+ compliance_results["checks"].append(check_result)
479
+ compliance_results["total_checks"] += 1
480
+ if check_result["passed"]:
481
+ compliance_results["passed_checks"] += 1
482
+ else:
483
+ compliance_results["failed_checks"] += 1
484
+ compliance_results["recommendations"].append(check_result["recommendation"])
485
+
486
+ # Calculate compliance score and risk level
487
+ compliance_results["compliance_score"] = (
488
+ compliance_results["passed_checks"] / compliance_results["total_checks"] * 100
489
+ if compliance_results["total_checks"] > 0
490
+ else 0
491
+ )
492
+
493
+ if compliance_results["compliance_score"] >= 90:
494
+ compliance_results["risk_level"] = "LOW"
495
+ elif compliance_results["compliance_score"] >= 70:
496
+ compliance_results["risk_level"] = "MEDIUM"
497
+ else:
498
+ compliance_results["risk_level"] = "HIGH"
499
+
500
+ # Display compliance results
501
+ self._display_compliance_results(compliance_results)
502
+
503
+ print_success(f"Security compliance check completed: {compliance_results['compliance_score']:.1f}% score")
504
+
505
+ return self.create_result(
506
+ success=True, message=f"Security compliance check completed for {service_name}", data=compliance_results
507
+ )
508
+
509
+ except ClientError as e:
510
+ error_msg = f"Failed to perform compliance check: {e.response['Error']['Message']}"
511
+ print_error(error_msg, e)
512
+ return self.create_result(
513
+ success=False, message=error_msg, data={"error_code": e.response["Error"]["Code"]}
514
+ )
515
+ except Exception as e:
516
+ error_msg = f"Unexpected error during compliance check: {str(e)}"
517
+ print_error(error_msg, e)
518
+ return self.create_result(success=False, message=error_msg, data={"error": str(e)})
519
+
520
+ def _validate_load_balancers(self, load_balancer_arns: List[str]) -> Dict[str, Any]:
521
+ """Validate load balancers for PrivateLink service creation."""
522
+ try:
523
+ # Check if load balancers exist and are Network Load Balancers
524
+ response = self.elbv2_client.describe_load_balancers(LoadBalancerArns=load_balancer_arns)
525
+ load_balancers = response["LoadBalancers"]
526
+
527
+ for lb in load_balancers:
528
+ if lb["Type"] != "network":
529
+ return {
530
+ "valid": False,
531
+ "message": f"Load balancer {lb['LoadBalancerArn']} is not a Network Load Balancer",
532
+ }
533
+ if lb["State"]["Code"] != "active":
534
+ return {"valid": False, "message": f"Load balancer {lb['LoadBalancerArn']} is not in active state"}
535
+
536
+ return {"valid": True, "message": "Load balancers validation successful", "load_balancers": load_balancers}
537
+
538
+ except ClientError as e:
539
+ return {"valid": False, "message": f"Load balancer validation error: {e.response['Error']['Message']}"}
540
+
541
+ def _calculate_service_costs(self, load_balancer_arns: List[str]) -> Dict[str, Any]:
542
+ """Calculate estimated costs for PrivateLink service."""
543
+ # Base service cost (per hour)
544
+ service_hours_month = 24 * 30 # 720 hours per month
545
+ service_monthly_cost = self.PRIVATELINK_PRICING["vpc_endpoint_service_hour"] * service_hours_month
546
+
547
+ # Network Load Balancer costs (required for PrivateLink)
548
+ nlb_monthly_cost = (
549
+ self.PRIVATELINK_PRICING["network_load_balancer_hour"] * service_hours_month * len(load_balancer_arns)
550
+ )
551
+
552
+ # Estimated data processing (conservative estimate)
553
+ estimated_monthly_gb = 100
554
+ data_processing_cost = estimated_monthly_gb * self.PRIVATELINK_PRICING["data_processing_gb"]
555
+
556
+ total_monthly_cost = service_monthly_cost + nlb_monthly_cost + data_processing_cost
557
+
558
+ return {
559
+ "service_cost": service_monthly_cost,
560
+ "nlb_cost": nlb_monthly_cost,
561
+ "data_processing_cost": data_processing_cost,
562
+ "total_monthly_cost": total_monthly_cost,
563
+ "estimated_monthly_gb": estimated_monthly_gb,
564
+ "cost_breakdown": {
565
+ "vpc_endpoint_service": service_monthly_cost,
566
+ "network_load_balancers": nlb_monthly_cost,
567
+ "data_processing": data_processing_cost,
568
+ },
569
+ }
570
+
571
+ def _get_service_details(self, service_name: str) -> Optional[Dict[str, Any]]:
572
+ """Get detailed information about a PrivateLink service."""
573
+ try:
574
+ response = self.ec2_client.describe_vpc_endpoint_service_configurations(ServiceNames=[service_name])
575
+ services = response["ServiceConfigurations"]
576
+ return services[0] if services else None
577
+ except ClientError:
578
+ return None
579
+
580
+ def _get_service_connections(self, service_name: str) -> List[Dict[str, Any]]:
581
+ """Get active connections to a PrivateLink service."""
582
+ try:
583
+ response = self.ec2_client.describe_vpc_endpoint_connections(
584
+ Filters=[{"Name": "service-name", "Values": [service_name]}]
585
+ )
586
+ return response.get("VpcEndpointConnections", [])
587
+ except ClientError:
588
+ return []
589
+
590
+ def _enhance_service_with_analysis(self, service: Dict[str, Any]) -> Dict[str, Any]:
591
+ """Enhance service data with cost analysis and metadata."""
592
+ service_name = service.get("ServiceName", "unknown")
593
+ nlb_arns = service.get("NetworkLoadBalancerArns", [])
594
+
595
+ # Calculate costs
596
+ cost_analysis = self._calculate_service_costs(nlb_arns)
597
+
598
+ # Get connection count
599
+ connections = self._get_service_connections(service_name)
600
+
601
+ # Enhance service data
602
+ enhanced = service.copy()
603
+ enhanced.update(
604
+ {
605
+ "estimated_monthly_cost": cost_analysis["total_monthly_cost"],
606
+ "cost_analysis": cost_analysis,
607
+ "active_connections": len(connections),
608
+ "connection_details": connections,
609
+ "nlb_count": len(nlb_arns),
610
+ "analysis_timestamp": datetime.now().isoformat(),
611
+ }
612
+ )
613
+
614
+ return enhanced
615
+
616
+ def _display_service_cost_analysis(self, cost_analysis: Dict[str, Any]) -> None:
617
+ """Display service cost analysis in formatted panel."""
618
+ analysis_text = f"""
619
+ [bold]PrivateLink Service Cost Analysis[/bold]
620
+
621
+ Service Cost: ${cost_analysis["service_cost"]:.2f}/month
622
+ Network Load Balancer Cost: ${cost_analysis["nlb_cost"]:.2f}/month
623
+ Data Processing Cost: ${cost_analysis["data_processing_cost"]:.2f}/month
624
+
625
+ [bold]Total Monthly Cost: ${cost_analysis["total_monthly_cost"]:.2f}[/bold]
626
+
627
+ Estimated Data Processing: {cost_analysis["estimated_monthly_gb"]} GB/month
628
+ """
629
+
630
+ panel = create_panel(analysis_text, title="PrivateLink Cost Analysis", border_style="cyan")
631
+
632
+ console.print(panel)
633
+
634
+ def _display_services_table(self, services: List[Dict[str, Any]], total_cost: float) -> None:
635
+ """Display services in formatted table."""
636
+ table = create_table(
637
+ title=f"PrivateLink Services - Total Monthly Cost: ${total_cost:.2f}",
638
+ columns=[
639
+ {"name": "Service Name", "style": "cyan", "justify": "left"},
640
+ {"name": "Type", "style": "blue", "justify": "center"},
641
+ {"name": "State", "style": "green", "justify": "center"},
642
+ {"name": "Connections", "style": "yellow", "justify": "center"},
643
+ {"name": "Monthly Cost", "style": "red", "justify": "right"},
644
+ {"name": "Acceptance", "style": "dim", "justify": "center"},
645
+ ],
646
+ )
647
+
648
+ for service in services:
649
+ service_name = service.get("ServiceName", "unknown")
650
+ table.add_row(
651
+ service_name.split(".")[-1] if len(service_name) > 40 else service_name,
652
+ service.get("ServiceType", "Interface"),
653
+ service.get("ServiceState", "Unknown"),
654
+ str(service.get("active_connections", 0)),
655
+ f"${service.get('estimated_monthly_cost', 0):.2f}",
656
+ "Required" if service.get("AcceptanceRequired", True) else "Auto",
657
+ )
658
+
659
+ console.print(table)
660
+
661
+ def _categorize_available_services(self, service_names: List[str]) -> Dict[str, List[str]]:
662
+ """Categorize available services by type and provider."""
663
+ categories = {
664
+ "aws_managed": [],
665
+ "customer_managed": [],
666
+ "compute": [],
667
+ "storage": [],
668
+ "database": [],
669
+ "analytics": [],
670
+ "security": [],
671
+ "other": [],
672
+ }
673
+
674
+ for service_name in service_names:
675
+ if service_name.startswith("com.amazonaws"):
676
+ categories["aws_managed"].append(service_name)
677
+
678
+ # Categorize by service type
679
+ if any(svc in service_name for svc in ["ec2", "ecs", "lambda"]):
680
+ categories["compute"].append(service_name)
681
+ elif any(svc in service_name for svc in ["s3", "efs", "fsx"]):
682
+ categories["storage"].append(service_name)
683
+ elif any(svc in service_name for svc in ["rds", "dynamodb", "redshift"]):
684
+ categories["database"].append(service_name)
685
+ elif any(svc in service_name for svc in ["kinesis", "glue", "emr"]):
686
+ categories["analytics"].append(service_name)
687
+ elif any(svc in service_name for svc in ["kms", "secretsmanager", "ssm"]):
688
+ categories["security"].append(service_name)
689
+ else:
690
+ categories["other"].append(service_name)
691
+ else:
692
+ categories["customer_managed"].append(service_name)
693
+
694
+ return categories
695
+
696
+ def _generate_connection_recommendations(
697
+ self, services: List[Dict[str, Any]], categories: Dict[str, List[str]]
698
+ ) -> List[Dict[str, Any]]:
699
+ """Generate recommendations for service connections."""
700
+ recommendations = []
701
+
702
+ # Recommend high-value AWS services
703
+ high_value_services = [
704
+ "com.amazonaws.us-east-1.s3",
705
+ "com.amazonaws.us-east-1.dynamodb",
706
+ "com.amazonaws.us-east-1.secretsmanager",
707
+ "com.amazonaws.us-east-1.ssm",
708
+ ]
709
+
710
+ for service_name in categories.get("aws_managed", []):
711
+ if service_name in high_value_services:
712
+ recommendations.append(
713
+ {
714
+ "type": "HIGH_VALUE_CONNECTION",
715
+ "service": service_name,
716
+ "benefit": "Reduces NAT Gateway costs and improves security",
717
+ "priority": "HIGH",
718
+ }
719
+ )
720
+
721
+ # Recommend consolidation for customer services
722
+ if len(categories.get("customer_managed", [])) > 3:
723
+ recommendations.append(
724
+ {
725
+ "type": "SERVICE_CONSOLIDATION",
726
+ "count": len(categories["customer_managed"]),
727
+ "benefit": "Consider consolidating multiple customer services",
728
+ "priority": "MEDIUM",
729
+ }
730
+ )
731
+
732
+ return recommendations
733
+
734
+ def _display_service_discovery_results(self, discovery: Dict[str, Any], categories: Dict[str, List[str]]) -> None:
735
+ """Display service discovery results in tree format."""
736
+ tree = create_tree(f"PrivateLink Service Discovery ({discovery['available_services']} total)")
737
+
738
+ aws_branch = tree.add("AWS Managed Services")
739
+ for category, services in categories.items():
740
+ if category not in ["aws_managed", "customer_managed"] and services:
741
+ cat_branch = aws_branch.add(f"{category.title()} ({len(services)})")
742
+ for service in services[:3]: # Show first 3
743
+ cat_branch.add(service.split(".")[-1])
744
+ if len(services) > 3:
745
+ cat_branch.add(f"... and {len(services) - 3} more")
746
+
747
+ if categories["customer_managed"]:
748
+ customer_branch = tree.add(f"Customer Managed Services ({len(categories['customer_managed'])})")
749
+ for service in categories["customer_managed"][:5]: # Show first 5
750
+ customer_branch.add(service)
751
+ if len(categories["customer_managed"]) > 5:
752
+ customer_branch.add(f"... and {len(categories['customer_managed']) - 5} more")
753
+
754
+ console.print(tree)
755
+
756
+ def _check_acceptance_requirement(self, service_details: Dict[str, Any]) -> Dict[str, Any]:
757
+ """Check if service requires connection acceptance (security best practice)."""
758
+ acceptance_required = service_details.get("AcceptanceRequired", False)
759
+
760
+ return {
761
+ "check_name": "Connection Acceptance Requirement",
762
+ "passed": acceptance_required,
763
+ "description": "Service should require manual acceptance for security",
764
+ "finding": "Acceptance required" if acceptance_required else "Auto-acceptance enabled",
765
+ "recommendation": "Enable acceptance requirement for better security control"
766
+ if not acceptance_required
767
+ else None,
768
+ "severity": "MEDIUM" if not acceptance_required else None,
769
+ }
770
+
771
+ def _check_principal_restrictions(self, service_name: str) -> Dict[str, Any]:
772
+ """Check if service has principal restrictions configured."""
773
+ try:
774
+ response = self.ec2_client.describe_vpc_endpoint_service_permissions(ServiceName=service_name)
775
+ allowed_principals = response.get("AllowedPrincipals", [])
776
+
777
+ has_restrictions = len(allowed_principals) > 0
778
+
779
+ return {
780
+ "check_name": "Principal Access Restrictions",
781
+ "passed": has_restrictions,
782
+ "description": "Service should have explicit principal restrictions",
783
+ "finding": f"{len(allowed_principals)} allowed principals configured"
784
+ if has_restrictions
785
+ else "No principal restrictions",
786
+ "recommendation": "Configure explicit allowed principals for access control"
787
+ if not has_restrictions
788
+ else None,
789
+ "severity": "HIGH" if not has_restrictions else None,
790
+ }
791
+ except ClientError:
792
+ return {
793
+ "check_name": "Principal Access Restrictions",
794
+ "passed": False,
795
+ "description": "Could not verify principal restrictions",
796
+ "finding": "Unable to retrieve principal permissions",
797
+ "recommendation": "Verify service permissions are properly configured",
798
+ "severity": "MEDIUM",
799
+ }
800
+
801
+ def _check_load_balancer_security(self, service_details: Dict[str, Any]) -> Dict[str, Any]:
802
+ """Check load balancer security configuration."""
803
+ nlb_arns = service_details.get("NetworkLoadBalancerArns", [])
804
+
805
+ # This is a placeholder - in practice, would check NLB security groups, etc.
806
+ has_security_config = len(nlb_arns) > 0
807
+
808
+ return {
809
+ "check_name": "Load Balancer Security",
810
+ "passed": has_security_config,
811
+ "description": "Load balancers should have proper security configuration",
812
+ "finding": f"{len(nlb_arns)} Network Load Balancers configured",
813
+ "recommendation": "Review NLB security groups and access logs" if not has_security_config else None,
814
+ "severity": "MEDIUM" if not has_security_config else None,
815
+ }
816
+
817
+ def _display_compliance_results(self, results: Dict[str, Any]) -> None:
818
+ """Display security compliance results."""
819
+ score = results["compliance_score"]
820
+ risk_level = results["risk_level"]
821
+
822
+ # Color code based on score
823
+ if score >= 90:
824
+ score_color = "green"
825
+ elif score >= 70:
826
+ score_color = "yellow"
827
+ else:
828
+ score_color = "red"
829
+
830
+ compliance_text = f"""
831
+ [bold]Security Compliance Assessment[/bold]
832
+
833
+ Service: {results["service_name"]}
834
+ Compliance Score: [{score_color}]{score:.1f}%[/{score_color}]
835
+ Risk Level: [{score_color}]{risk_level}[/{score_color}]
836
+
837
+ Checks Passed: {results["passed_checks"]}/{results["total_checks"]}
838
+ Failed Checks: {results["failed_checks"]}
839
+
840
+ [bold]Recommendations:[/bold]
841
+ """
842
+
843
+ for recommendation in results.get("recommendations", []):
844
+ if recommendation:
845
+ compliance_text += f"\n• {recommendation}"
846
+
847
+ panel = create_panel(compliance_text, title="Security Compliance Results", border_style=score_color)
848
+
849
+ console.print(panel)
850
+
851
+ def _generate_enterprise_recommendations(self, services: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
852
+ """Generate enterprise-level recommendations for PrivateLink optimization."""
853
+ recommendations = []
854
+
855
+ # Cost optimization recommendations
856
+ high_cost_services = [s for s in services if s.get("estimated_monthly_cost", 0) > 100]
857
+ if high_cost_services:
858
+ total_high_cost = sum(s["estimated_monthly_cost"] for s in high_cost_services)
859
+ recommendations.append(
860
+ {
861
+ "type": "COST_OPTIMIZATION",
862
+ "priority": "HIGH",
863
+ "description": f"{len(high_cost_services)} high-cost services (>${total_high_cost:.2f}/month)",
864
+ "action": "Review usage patterns and consider optimization",
865
+ "potential_savings": total_high_cost * 0.3, # Conservative 30% savings estimate
866
+ }
867
+ )
868
+
869
+ # Security recommendations
870
+ services_without_acceptance = [s for s in services if not s.get("AcceptanceRequired", True)]
871
+ if services_without_acceptance:
872
+ recommendations.append(
873
+ {
874
+ "type": "SECURITY_ENHANCEMENT",
875
+ "priority": "MEDIUM",
876
+ "description": f"{len(services_without_acceptance)} services with auto-acceptance",
877
+ "action": "Enable acceptance requirement for better security control",
878
+ }
879
+ )
880
+
881
+ # Utilization recommendations
882
+ low_connection_services = [s for s in services if s.get("active_connections", 0) == 0]
883
+ if low_connection_services:
884
+ recommendations.append(
885
+ {
886
+ "type": "UTILIZATION_REVIEW",
887
+ "priority": "MEDIUM",
888
+ "description": f"{len(low_connection_services)} services with no active connections",
889
+ "action": "Review necessity and consider decommissioning unused services",
890
+ "potential_savings": sum(s.get("estimated_monthly_cost", 0) for s in low_connection_services),
891
+ }
892
+ )
893
+
894
+ return recommendations
895
+
896
+ def _analyze_deletion_impact(self, service_name: str, service_details: Dict[str, Any]) -> Dict[str, Any]:
897
+ """Analyze impact of deleting a PrivateLink service."""
898
+ connections = self._get_service_connections(service_name)
899
+ cost_analysis = self._calculate_service_costs(service_details.get("NetworkLoadBalancerArns", []))
900
+
901
+ return {
902
+ "service_name": service_name,
903
+ "active_connections": len(connections),
904
+ "monthly_cost_saving": cost_analysis["total_monthly_cost"],
905
+ "annual_cost_saving": cost_analysis["total_monthly_cost"] * 12,
906
+ "business_impact": "HIGH" if len(connections) > 5 else "MEDIUM" if len(connections) > 0 else "LOW",
907
+ "technical_impact": "Service consumers will lose private connectivity",
908
+ "recommendations": [
909
+ "Notify all service consumers before deletion",
910
+ "Consider migration timeline for dependent services",
911
+ "Document alternative connectivity methods",
912
+ ],
913
+ }
914
+
915
+ def _tag_service(self, service_name: str, tags: Dict[str, str]) -> None:
916
+ """Add tags to a PrivateLink service (placeholder - actual implementation would use resource ARN)."""
917
+ try:
918
+ # Note: VPC Endpoint Services don't support direct tagging via service name
919
+ # This would require getting the service ARN and using resource-based tagging
920
+ logger.info(f"Would tag service {service_name} with governance tags")
921
+ except Exception as e:
922
+ logger.warning(f"Failed to tag service {service_name}: {e}")
923
+
924
+ def _manage_service_permissions_internal(self, service_name: str, principals: List[str], action: str) -> None:
925
+ """Internal method to manage service permissions."""
926
+ try:
927
+ if action == "add":
928
+ self.ec2_client.modify_vpc_endpoint_service_permissions(
929
+ ServiceName=service_name, AddAllowedPrincipals=principals
930
+ )
931
+ elif action == "remove":
932
+ self.ec2_client.modify_vpc_endpoint_service_permissions(
933
+ ServiceName=service_name, RemoveAllowedPrincipals=principals
934
+ )
935
+ except ClientError as e:
936
+ logger.error(f"Failed to {action} principals for service {service_name}: {e}")
937
+
938
+
939
+ # Export the operations class
940
+ __all__ = ["PrivateLinkOperations"]