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.
- runbooks/__init__.py +1 -1
- runbooks/base.py +5 -1
- runbooks/cfat/__init__.py +8 -4
- runbooks/cfat/assessment/collectors.py +171 -14
- runbooks/cfat/assessment/compliance.py +871 -0
- runbooks/cfat/assessment/runner.py +122 -11
- runbooks/cfat/models.py +6 -2
- runbooks/common/logger.py +14 -0
- runbooks/common/rich_utils.py +451 -0
- runbooks/enterprise/__init__.py +68 -0
- runbooks/enterprise/error_handling.py +411 -0
- runbooks/enterprise/logging.py +439 -0
- runbooks/enterprise/multi_tenant.py +583 -0
- runbooks/finops/README.md +468 -241
- runbooks/finops/__init__.py +39 -3
- runbooks/finops/cli.py +83 -18
- runbooks/finops/cross_validation.py +375 -0
- runbooks/finops/dashboard_runner.py +812 -164
- runbooks/finops/enhanced_dashboard_runner.py +525 -0
- runbooks/finops/finops_dashboard.py +1892 -0
- runbooks/finops/helpers.py +485 -51
- runbooks/finops/optimizer.py +823 -0
- runbooks/finops/tests/__init__.py +19 -0
- runbooks/finops/tests/results_test_finops_dashboard.xml +1 -0
- runbooks/finops/tests/run_comprehensive_tests.py +421 -0
- runbooks/finops/tests/run_tests.py +305 -0
- runbooks/finops/tests/test_finops_dashboard.py +705 -0
- runbooks/finops/tests/test_integration.py +477 -0
- runbooks/finops/tests/test_performance.py +380 -0
- runbooks/finops/tests/test_performance_benchmarks.py +500 -0
- runbooks/finops/tests/test_reference_images_validation.py +867 -0
- runbooks/finops/tests/test_single_account_features.py +715 -0
- runbooks/finops/tests/validate_test_suite.py +220 -0
- runbooks/finops/types.py +1 -1
- runbooks/hitl/enhanced_workflow_engine.py +725 -0
- runbooks/inventory/artifacts/scale-optimize-status.txt +12 -0
- runbooks/inventory/collectors/aws_comprehensive.py +442 -0
- runbooks/inventory/collectors/enterprise_scale.py +281 -0
- runbooks/inventory/core/collector.py +172 -13
- runbooks/inventory/discovery.md +1 -1
- runbooks/inventory/list_ec2_instances.py +18 -20
- runbooks/inventory/list_ssm_parameters.py +31 -3
- runbooks/inventory/organizations_discovery.py +1269 -0
- runbooks/inventory/rich_inventory_display.py +393 -0
- runbooks/inventory/run_on_multi_accounts.py +35 -19
- runbooks/inventory/runbooks.security.report_generator.log +0 -0
- runbooks/inventory/runbooks.security.run_script.log +0 -0
- runbooks/inventory/vpc_flow_analyzer.py +1030 -0
- runbooks/main.py +2215 -119
- runbooks/metrics/dora_metrics_engine.py +599 -0
- runbooks/operate/__init__.py +2 -2
- runbooks/operate/base.py +122 -10
- runbooks/operate/deployment_framework.py +1032 -0
- runbooks/operate/deployment_validator.py +853 -0
- runbooks/operate/dynamodb_operations.py +10 -6
- runbooks/operate/ec2_operations.py +319 -11
- runbooks/operate/executive_dashboard.py +779 -0
- runbooks/operate/mcp_integration.py +750 -0
- runbooks/operate/nat_gateway_operations.py +1120 -0
- runbooks/operate/networking_cost_heatmap.py +685 -0
- runbooks/operate/privatelink_operations.py +940 -0
- runbooks/operate/s3_operations.py +10 -6
- runbooks/operate/vpc_endpoints.py +644 -0
- runbooks/operate/vpc_operations.py +1038 -0
- runbooks/remediation/__init__.py +2 -2
- runbooks/remediation/acm_remediation.py +1 -1
- runbooks/remediation/base.py +1 -1
- runbooks/remediation/cloudtrail_remediation.py +1 -1
- runbooks/remediation/cognito_remediation.py +1 -1
- runbooks/remediation/dynamodb_remediation.py +1 -1
- runbooks/remediation/ec2_remediation.py +1 -1
- runbooks/remediation/ec2_unattached_ebs_volumes.py +1 -1
- runbooks/remediation/kms_enable_key_rotation.py +1 -1
- runbooks/remediation/kms_remediation.py +1 -1
- runbooks/remediation/lambda_remediation.py +1 -1
- runbooks/remediation/multi_account.py +1 -1
- runbooks/remediation/rds_remediation.py +1 -1
- runbooks/remediation/s3_block_public_access.py +1 -1
- runbooks/remediation/s3_enable_access_logging.py +1 -1
- runbooks/remediation/s3_encryption.py +1 -1
- runbooks/remediation/s3_remediation.py +1 -1
- runbooks/remediation/vpc_remediation.py +475 -0
- runbooks/security/__init__.py +3 -1
- runbooks/security/compliance_automation.py +632 -0
- runbooks/security/report_generator.py +10 -0
- runbooks/security/run_script.py +31 -5
- runbooks/security/security_baseline_tester.py +169 -30
- runbooks/security/security_export.py +477 -0
- runbooks/validation/__init__.py +10 -0
- runbooks/validation/benchmark.py +484 -0
- runbooks/validation/cli.py +356 -0
- runbooks/validation/mcp_validator.py +768 -0
- runbooks/vpc/__init__.py +38 -0
- runbooks/vpc/config.py +212 -0
- runbooks/vpc/cost_engine.py +347 -0
- runbooks/vpc/heatmap_engine.py +605 -0
- runbooks/vpc/manager_interface.py +634 -0
- runbooks/vpc/networking_wrapper.py +1260 -0
- runbooks/vpc/rich_formatters.py +679 -0
- runbooks/vpc/tests/__init__.py +5 -0
- runbooks/vpc/tests/conftest.py +356 -0
- runbooks/vpc/tests/test_cli_integration.py +530 -0
- runbooks/vpc/tests/test_config.py +458 -0
- runbooks/vpc/tests/test_cost_engine.py +479 -0
- runbooks/vpc/tests/test_networking_wrapper.py +512 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/METADATA +40 -12
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/RECORD +111 -50
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/WHEEL +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/entry_points.txt +0 -0
- {runbooks-0.7.6.dist-info → runbooks-0.7.9.dist-info}/licenses/LICENSE +0 -0
- {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"]
|