runbooks 0.7.0__py3-none-any.whl → 0.7.6__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 +87 -37
- runbooks/cfat/README.md +300 -49
- runbooks/cfat/__init__.py +2 -2
- runbooks/finops/__init__.py +1 -1
- runbooks/finops/cli.py +1 -1
- runbooks/inventory/collectors/__init__.py +8 -0
- runbooks/inventory/collectors/aws_management.py +791 -0
- runbooks/inventory/collectors/aws_networking.py +3 -3
- runbooks/main.py +3389 -782
- runbooks/operate/__init__.py +207 -0
- runbooks/operate/base.py +311 -0
- runbooks/operate/cloudformation_operations.py +619 -0
- runbooks/operate/cloudwatch_operations.py +496 -0
- runbooks/operate/dynamodb_operations.py +812 -0
- runbooks/operate/ec2_operations.py +926 -0
- runbooks/operate/iam_operations.py +569 -0
- runbooks/operate/s3_operations.py +1211 -0
- runbooks/operate/tagging_operations.py +655 -0
- runbooks/remediation/CLAUDE.md +100 -0
- runbooks/remediation/DOME9.md +218 -0
- runbooks/remediation/README.md +26 -0
- runbooks/remediation/Tests/__init__.py +0 -0
- runbooks/remediation/Tests/update_policy.py +74 -0
- runbooks/remediation/__init__.py +95 -0
- runbooks/remediation/acm_cert_expired_unused.py +98 -0
- runbooks/remediation/acm_remediation.py +875 -0
- runbooks/remediation/api_gateway_list.py +167 -0
- runbooks/remediation/base.py +643 -0
- runbooks/remediation/cloudtrail_remediation.py +908 -0
- runbooks/remediation/cloudtrail_s3_modifications.py +296 -0
- runbooks/remediation/cognito_active_users.py +78 -0
- runbooks/remediation/cognito_remediation.py +856 -0
- runbooks/remediation/cognito_user_password_reset.py +163 -0
- runbooks/remediation/commons.py +455 -0
- runbooks/remediation/dynamodb_optimize.py +155 -0
- runbooks/remediation/dynamodb_remediation.py +744 -0
- runbooks/remediation/dynamodb_server_side_encryption.py +108 -0
- runbooks/remediation/ec2_public_ips.py +134 -0
- runbooks/remediation/ec2_remediation.py +892 -0
- runbooks/remediation/ec2_subnet_disable_auto_ip_assignment.py +72 -0
- runbooks/remediation/ec2_unattached_ebs_volumes.py +448 -0
- runbooks/remediation/ec2_unused_security_groups.py +202 -0
- runbooks/remediation/kms_enable_key_rotation.py +651 -0
- runbooks/remediation/kms_remediation.py +717 -0
- runbooks/remediation/lambda_list.py +243 -0
- runbooks/remediation/lambda_remediation.py +971 -0
- runbooks/remediation/multi_account.py +569 -0
- runbooks/remediation/rds_instance_list.py +199 -0
- runbooks/remediation/rds_remediation.py +873 -0
- runbooks/remediation/rds_snapshot_list.py +192 -0
- runbooks/remediation/requirements.txt +118 -0
- runbooks/remediation/s3_block_public_access.py +159 -0
- runbooks/remediation/s3_bucket_public_access.py +143 -0
- runbooks/remediation/s3_disable_static_website_hosting.py +74 -0
- runbooks/remediation/s3_downloader.py +215 -0
- runbooks/remediation/s3_enable_access_logging.py +562 -0
- runbooks/remediation/s3_encryption.py +526 -0
- runbooks/remediation/s3_force_ssl_secure_policy.py +143 -0
- runbooks/remediation/s3_list.py +141 -0
- runbooks/remediation/s3_object_search.py +201 -0
- runbooks/remediation/s3_remediation.py +816 -0
- runbooks/remediation/scan_for_phrase.py +425 -0
- runbooks/remediation/workspaces_list.py +220 -0
- runbooks/security/__init__.py +9 -10
- runbooks/security/security_baseline_tester.py +4 -2
- runbooks-0.7.6.dist-info/METADATA +608 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/RECORD +84 -76
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/entry_points.txt +0 -1
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/top_level.txt +0 -1
- jupyter-agent/.env +0 -2
- jupyter-agent/.env.template +0 -2
- jupyter-agent/.gitattributes +0 -35
- jupyter-agent/.gradio/certificate.pem +0 -31
- jupyter-agent/README.md +0 -16
- jupyter-agent/__main__.log +0 -8
- jupyter-agent/app.py +0 -256
- jupyter-agent/cloudops-agent.png +0 -0
- jupyter-agent/ds-system-prompt.txt +0 -154
- jupyter-agent/jupyter-agent.png +0 -0
- jupyter-agent/llama3_template.jinja +0 -123
- jupyter-agent/requirements.txt +0 -9
- jupyter-agent/tmp/4ojbs8a02ir/jupyter-agent.ipynb +0 -68
- jupyter-agent/tmp/cm5iasgpm3p/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/crqbsseag5/jupyter-agent.ipynb +0 -91
- jupyter-agent/tmp/hohanq1u097/jupyter-agent.ipynb +0 -57
- jupyter-agent/tmp/jns1sam29wm/jupyter-agent.ipynb +0 -53
- jupyter-agent/tmp/jupyter-agent.ipynb +0 -27
- jupyter-agent/utils.py +0 -409
- runbooks/aws/__init__.py +0 -58
- runbooks/aws/dynamodb_operations.py +0 -231
- runbooks/aws/ec2_copy_image_cross-region.py +0 -195
- runbooks/aws/ec2_describe_instances.py +0 -202
- runbooks/aws/ec2_ebs_snapshots_delete.py +0 -186
- runbooks/aws/ec2_run_instances.py +0 -213
- runbooks/aws/ec2_start_stop_instances.py +0 -212
- runbooks/aws/ec2_terminate_instances.py +0 -143
- runbooks/aws/ec2_unused_eips.py +0 -196
- runbooks/aws/ec2_unused_volumes.py +0 -188
- runbooks/aws/s3_create_bucket.py +0 -142
- runbooks/aws/s3_list_buckets.py +0 -152
- runbooks/aws/s3_list_objects.py +0 -156
- runbooks/aws/s3_object_operations.py +0 -183
- runbooks/aws/tagging_lambda_handler.py +0 -183
- runbooks/inventory/FAILED_SCRIPTS_TROUBLESHOOTING.md +0 -619
- runbooks/inventory/PASSED_SCRIPTS_GUIDE.md +0 -738
- runbooks/inventory/aws_organization.png +0 -0
- runbooks/inventory/cfn_move_stack_instances.py +0 -1526
- runbooks/inventory/delete_s3_buckets_objects.py +0 -169
- runbooks/inventory/lockdown_cfn_stackset_role.py +0 -224
- runbooks/inventory/update_aws_actions.py +0 -173
- runbooks/inventory/update_cfn_stacksets.py +0 -1215
- runbooks/inventory/update_cloudwatch_logs_retention_policy.py +0 -294
- runbooks/inventory/update_iam_roles_cross_accounts.py +0 -478
- runbooks/inventory/update_s3_public_access_block.py +0 -539
- runbooks/organizations/__init__.py +0 -12
- runbooks/organizations/manager.py +0 -374
- runbooks-0.7.0.dist-info/METADATA +0 -375
- /runbooks/inventory/{tests → Tests}/common_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/common_test_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/script_test_data.py +0 -0
- /runbooks/inventory/{tests → Tests}/setup.py +0 -0
- /runbooks/inventory/{tests → Tests}/src.py +0 -0
- /runbooks/inventory/{tests/test_inventory_modules.py → Tests/test_Inventory_Modules.py} +0 -0
- /runbooks/inventory/{tests → Tests}/test_cfn_describe_stacks.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_ec2_describe_instances.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_lambda_list_functions.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_moto_integration_example.py +0 -0
- /runbooks/inventory/{tests → Tests}/test_org_list_accounts.py +0 -0
- /runbooks/inventory/{Inventory_Modules.py → inventory_modules.py} +0 -0
- /runbooks/{aws → operate}/tags.json +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/WHEEL +0 -0
- {runbooks-0.7.0.dist-info → runbooks-0.7.6.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,875 @@
|
|
1
|
+
"""
|
2
|
+
Enterprise ACM Security Remediation - Production-Ready Certificate Lifecycle Management
|
3
|
+
|
4
|
+
## CRITICAL WARNING
|
5
|
+
|
6
|
+
This module contains DESTRUCTIVE OPERATIONS that can delete SSL/TLS certificates.
|
7
|
+
Deleting in-use certificates will cause SERVICE OUTAGES and break HTTPS connectivity.
|
8
|
+
EXTREME CAUTION must be exercised when using these operations.
|
9
|
+
|
10
|
+
## Overview
|
11
|
+
|
12
|
+
This module provides comprehensive AWS Certificate Manager (ACM) security remediation
|
13
|
+
capabilities, migrating and enhancing the critical certificate cleanup functionality from
|
14
|
+
acm_cert_expired_unused.py with enterprise-grade safety features.
|
15
|
+
|
16
|
+
## Original Scripts Enhanced
|
17
|
+
|
18
|
+
Migrated and enhanced from these CRITICAL original remediation scripts:
|
19
|
+
- acm_cert_expired_unused.py - Certificate deletion and lifecycle management
|
20
|
+
|
21
|
+
## Enterprise Safety Enhancements
|
22
|
+
|
23
|
+
- **CRITICAL SAFETY CHECKS**: Multi-level verification before certificate deletion
|
24
|
+
- **Usage Verification**: Comprehensive checks across all AWS services using certificates
|
25
|
+
- **Backup Creation**: Complete certificate backup before any deletion operations
|
26
|
+
- **Dry-Run Mandatory**: All destructive operations require explicit confirmation
|
27
|
+
- **Rollback Capability**: Certificate restoration and recovery procedures
|
28
|
+
- **Audit Logging**: Comprehensive logging of all certificate operations
|
29
|
+
|
30
|
+
## Compliance Framework Mapping
|
31
|
+
|
32
|
+
### CIS AWS Foundations Benchmark
|
33
|
+
- **CIS 3.1**: Certificate management and lifecycle controls
|
34
|
+
- **CIS 3.9**: Certificate expiration monitoring and remediation
|
35
|
+
|
36
|
+
### NIST Cybersecurity Framework
|
37
|
+
- **PR.PT-4**: Communications and control networks are protected
|
38
|
+
- **DE.CM-8**: Vulnerability scans are performed
|
39
|
+
|
40
|
+
### SOC2 Security Framework
|
41
|
+
- **CC6.1**: Encryption key and certificate management
|
42
|
+
|
43
|
+
## CRITICAL USAGE WARNINGS
|
44
|
+
|
45
|
+
⚠️ **PRODUCTION IMPACT WARNING**: These operations can cause service outages
|
46
|
+
⚠️ **VERIFICATION REQUIRED**: Always verify certificate usage before deletion
|
47
|
+
⚠️ **DRY-RUN FIRST**: Always test with --dry-run before actual execution
|
48
|
+
⚠️ **BACKUP ENABLED**: Ensure backup_enabled=True for all operations
|
49
|
+
|
50
|
+
## Example Usage
|
51
|
+
|
52
|
+
```python
|
53
|
+
from runbooks.remediation import ACMRemediation, RemediationContext
|
54
|
+
|
55
|
+
# Initialize with MAXIMUM SAFETY settings
|
56
|
+
acm_remediation = ACMRemediation(
|
57
|
+
profile="production",
|
58
|
+
backup_enabled=True, # MANDATORY
|
59
|
+
usage_verification=True, # MANDATORY
|
60
|
+
require_confirmation=True # MANDATORY
|
61
|
+
)
|
62
|
+
|
63
|
+
# ALWAYS start with dry-run
|
64
|
+
results = acm_remediation.cleanup_expired_certificates(
|
65
|
+
context,
|
66
|
+
dry_run=True, # MANDATORY for first run
|
67
|
+
verify_usage=True
|
68
|
+
)
|
69
|
+
```
|
70
|
+
|
71
|
+
Version: 0.7.6 - Enterprise Production Ready with CRITICAL SAFETY FEATURES
|
72
|
+
"""
|
73
|
+
|
74
|
+
import json
|
75
|
+
import os
|
76
|
+
import time
|
77
|
+
from datetime import datetime, timezone
|
78
|
+
from typing import Any, Dict, List, Optional
|
79
|
+
|
80
|
+
import boto3
|
81
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
82
|
+
from loguru import logger
|
83
|
+
|
84
|
+
from runbooks.remediation.base import (
|
85
|
+
BaseRemediation,
|
86
|
+
ComplianceMapping,
|
87
|
+
RemediationContext,
|
88
|
+
RemediationResult,
|
89
|
+
RemediationStatus,
|
90
|
+
)
|
91
|
+
|
92
|
+
|
93
|
+
class ACMRemediation(BaseRemediation):
|
94
|
+
"""
|
95
|
+
Enterprise ACM Certificate Security Remediation Operations.
|
96
|
+
|
97
|
+
⚠️ CRITICAL WARNING: This class contains DESTRUCTIVE certificate operations
|
98
|
+
that can cause PRODUCTION OUTAGES if used incorrectly.
|
99
|
+
|
100
|
+
Provides comprehensive ACM certificate management including safe cleanup of
|
101
|
+
expired and unused certificates with extensive safety verification.
|
102
|
+
|
103
|
+
## Key Safety Features
|
104
|
+
|
105
|
+
- **Multi-Service Usage Verification**: Checks ALB, CloudFront, API Gateway, etc.
|
106
|
+
- **Expiration Analysis**: Safe identification of expired certificates
|
107
|
+
- **Usage Cross-Reference**: Verifies no active usage before deletion
|
108
|
+
- **Backup Creation**: Complete certificate metadata backup
|
109
|
+
- **Confirmation Prompts**: Multiple confirmation levels for destructive operations
|
110
|
+
- **Rollback Support**: Certificate restoration capabilities
|
111
|
+
|
112
|
+
## CRITICAL USAGE REQUIREMENTS
|
113
|
+
|
114
|
+
1. **ALWAYS** use dry_run=True for initial testing
|
115
|
+
2. **ALWAYS** enable backup_enabled=True
|
116
|
+
3. **VERIFY** certificate usage manually before deletion
|
117
|
+
4. **TEST** in non-production environment first
|
118
|
+
5. **HAVE** rollback plan before executing
|
119
|
+
|
120
|
+
## Example Usage
|
121
|
+
|
122
|
+
```python
|
123
|
+
# SAFE initialization
|
124
|
+
acm_remediation = ACMRemediation(
|
125
|
+
profile="production",
|
126
|
+
backup_enabled=True, # CRITICAL
|
127
|
+
usage_verification=True, # CRITICAL
|
128
|
+
require_confirmation=True # CRITICAL
|
129
|
+
)
|
130
|
+
|
131
|
+
# MANDATORY dry-run first
|
132
|
+
results = acm_remediation.cleanup_expired_certificates(
|
133
|
+
context,
|
134
|
+
dry_run=True, # CRITICAL
|
135
|
+
verify_usage=True
|
136
|
+
)
|
137
|
+
```
|
138
|
+
"""
|
139
|
+
|
140
|
+
supported_operations = [
|
141
|
+
"cleanup_expired_certificates",
|
142
|
+
"cleanup_unused_certificates",
|
143
|
+
"analyze_certificate_usage",
|
144
|
+
"verify_certificate_security",
|
145
|
+
"comprehensive_acm_security",
|
146
|
+
]
|
147
|
+
|
148
|
+
def __init__(self, **kwargs):
|
149
|
+
"""
|
150
|
+
Initialize ACM remediation with CRITICAL SAFETY settings.
|
151
|
+
|
152
|
+
Args:
|
153
|
+
**kwargs: Configuration parameters with MANDATORY safety settings
|
154
|
+
"""
|
155
|
+
super().__init__(**kwargs)
|
156
|
+
|
157
|
+
# CRITICAL SAFETY CONFIGURATION
|
158
|
+
self.usage_verification = kwargs.get("usage_verification", True) # MANDATORY
|
159
|
+
self.require_confirmation = kwargs.get("require_confirmation", True) # MANDATORY
|
160
|
+
self.backup_enabled = True # FORCE ENABLE - CRITICAL for certificate operations
|
161
|
+
|
162
|
+
# ACM-specific configuration
|
163
|
+
self.check_load_balancers = kwargs.get("check_load_balancers", True)
|
164
|
+
self.check_cloudfront = kwargs.get("check_cloudfront", True)
|
165
|
+
self.check_api_gateway = kwargs.get("check_api_gateway", True)
|
166
|
+
self.check_cloudformation = kwargs.get("check_cloudformation", True)
|
167
|
+
|
168
|
+
logger.warning("ACM Remediation initialized - DESTRUCTIVE operations enabled")
|
169
|
+
logger.warning(
|
170
|
+
f"Safety settings: backup_enabled={self.backup_enabled}, "
|
171
|
+
f"usage_verification={self.usage_verification}, "
|
172
|
+
f"require_confirmation={self.require_confirmation}"
|
173
|
+
)
|
174
|
+
|
175
|
+
def _create_resource_backup(self, resource_id: str, backup_key: str, backup_type: str) -> str:
|
176
|
+
"""
|
177
|
+
Create CRITICAL backup of ACM certificate configuration.
|
178
|
+
|
179
|
+
This is MANDATORY for certificate operations as certificates cannot be recovered
|
180
|
+
once deleted from ACM.
|
181
|
+
|
182
|
+
Args:
|
183
|
+
resource_id: ACM certificate ARN
|
184
|
+
backup_key: Backup identifier
|
185
|
+
backup_type: Type of backup (certificate_metadata, usage_analysis, etc.)
|
186
|
+
|
187
|
+
Returns:
|
188
|
+
Backup location identifier
|
189
|
+
"""
|
190
|
+
try:
|
191
|
+
acm_client = self.get_client("acm")
|
192
|
+
|
193
|
+
# Create COMPREHENSIVE backup of certificate
|
194
|
+
backup_data = {
|
195
|
+
"certificate_arn": resource_id,
|
196
|
+
"backup_key": backup_key,
|
197
|
+
"backup_type": backup_type,
|
198
|
+
"timestamp": backup_key.split("_")[-1],
|
199
|
+
"backup_critical": True, # Mark as critical backup
|
200
|
+
"configurations": {},
|
201
|
+
}
|
202
|
+
|
203
|
+
if backup_type == "certificate_metadata":
|
204
|
+
# Backup COMPLETE certificate information
|
205
|
+
try:
|
206
|
+
cert_details = self.execute_aws_call(acm_client, "describe_certificate", CertificateArn=resource_id)
|
207
|
+
backup_data["configurations"]["certificate"] = cert_details.get("Certificate")
|
208
|
+
|
209
|
+
# Get certificate itself (if exportable)
|
210
|
+
try:
|
211
|
+
cert_export = self.execute_aws_call(
|
212
|
+
acm_client, "export_certificate", CertificateArn=resource_id
|
213
|
+
)
|
214
|
+
backup_data["configurations"]["certificate_pem"] = {
|
215
|
+
"certificate": cert_export.get("Certificate"),
|
216
|
+
"certificate_chain": cert_export.get("CertificateChain"),
|
217
|
+
# Note: Private key NOT included for security
|
218
|
+
}
|
219
|
+
except ClientError as e:
|
220
|
+
if "ValidationException" in str(e):
|
221
|
+
logger.info(f"Certificate {resource_id} is not exportable (AWS-managed)")
|
222
|
+
backup_data["configurations"]["certificate_pem"] = {
|
223
|
+
"note": "AWS-managed certificate - not exportable"
|
224
|
+
}
|
225
|
+
else:
|
226
|
+
raise
|
227
|
+
|
228
|
+
# Get certificate tags
|
229
|
+
try:
|
230
|
+
tags_response = self.execute_aws_call(
|
231
|
+
acm_client, "list_tags_for_certificate", CertificateArn=resource_id
|
232
|
+
)
|
233
|
+
backup_data["configurations"]["tags"] = tags_response.get("Tags", [])
|
234
|
+
except ClientError:
|
235
|
+
backup_data["configurations"]["tags"] = []
|
236
|
+
|
237
|
+
except ClientError as e:
|
238
|
+
logger.error(f"Could not backup certificate metadata for {resource_id}: {e}")
|
239
|
+
raise
|
240
|
+
|
241
|
+
# Store backup with CRITICAL flag (simplified for MVP - would use S3 in production)
|
242
|
+
backup_location = f"acm-backup-CRITICAL://{backup_key}.json"
|
243
|
+
logger.critical(f"CRITICAL BACKUP created for ACM certificate {resource_id}: {backup_location}")
|
244
|
+
|
245
|
+
return backup_location
|
246
|
+
|
247
|
+
except Exception as e:
|
248
|
+
logger.critical(f"FAILED to create CRITICAL backup for ACM certificate {resource_id}: {e}")
|
249
|
+
raise
|
250
|
+
|
251
|
+
def _verify_certificate_usage(self, certificate_arn: str) -> Dict[str, Any]:
|
252
|
+
"""
|
253
|
+
CRITICAL: Comprehensive verification of certificate usage across ALL AWS services.
|
254
|
+
|
255
|
+
This function prevents PRODUCTION OUTAGES by verifying that certificates
|
256
|
+
are not in use before deletion.
|
257
|
+
|
258
|
+
Args:
|
259
|
+
certificate_arn: ACM certificate ARN
|
260
|
+
|
261
|
+
Returns:
|
262
|
+
Dictionary with usage analysis across all services
|
263
|
+
"""
|
264
|
+
usage_analysis = {
|
265
|
+
"certificate_arn": certificate_arn,
|
266
|
+
"in_use": False,
|
267
|
+
"usage_details": {},
|
268
|
+
"services_checked": [],
|
269
|
+
"verification_timestamp": datetime.now(tz=timezone.utc).isoformat(),
|
270
|
+
}
|
271
|
+
|
272
|
+
try:
|
273
|
+
# Check ALB/ELB usage
|
274
|
+
if self.check_load_balancers:
|
275
|
+
elb_usage = self._check_elb_usage(certificate_arn)
|
276
|
+
usage_analysis["usage_details"]["load_balancers"] = elb_usage
|
277
|
+
usage_analysis["services_checked"].append("ELB/ALB")
|
278
|
+
if elb_usage["in_use"]:
|
279
|
+
usage_analysis["in_use"] = True
|
280
|
+
|
281
|
+
# Check CloudFront usage
|
282
|
+
if self.check_cloudfront:
|
283
|
+
cloudfront_usage = self._check_cloudfront_usage(certificate_arn)
|
284
|
+
usage_analysis["usage_details"]["cloudfront"] = cloudfront_usage
|
285
|
+
usage_analysis["services_checked"].append("CloudFront")
|
286
|
+
if cloudfront_usage["in_use"]:
|
287
|
+
usage_analysis["in_use"] = True
|
288
|
+
|
289
|
+
# Check API Gateway usage
|
290
|
+
if self.check_api_gateway:
|
291
|
+
api_gateway_usage = self._check_api_gateway_usage(certificate_arn)
|
292
|
+
usage_analysis["usage_details"]["api_gateway"] = api_gateway_usage
|
293
|
+
usage_analysis["services_checked"].append("API Gateway")
|
294
|
+
if api_gateway_usage["in_use"]:
|
295
|
+
usage_analysis["in_use"] = True
|
296
|
+
|
297
|
+
# Check CloudFormation stacks
|
298
|
+
if self.check_cloudformation:
|
299
|
+
cfn_usage = self._check_cloudformation_usage(certificate_arn)
|
300
|
+
usage_analysis["usage_details"]["cloudformation"] = cfn_usage
|
301
|
+
usage_analysis["services_checked"].append("CloudFormation")
|
302
|
+
if cfn_usage["in_use"]:
|
303
|
+
usage_analysis["in_use"] = True
|
304
|
+
|
305
|
+
logger.info(
|
306
|
+
f"Certificate usage verification completed for {certificate_arn}: In use: {usage_analysis['in_use']}"
|
307
|
+
)
|
308
|
+
|
309
|
+
except Exception as e:
|
310
|
+
logger.error(f"Error during certificate usage verification: {e}")
|
311
|
+
# FAIL SAFE: If verification fails, assume certificate is in use
|
312
|
+
usage_analysis["in_use"] = True
|
313
|
+
usage_analysis["verification_error"] = str(e)
|
314
|
+
|
315
|
+
return usage_analysis
|
316
|
+
|
317
|
+
def _check_elb_usage(self, certificate_arn: str) -> Dict[str, Any]:
|
318
|
+
"""Check if certificate is used by any Load Balancer."""
|
319
|
+
try:
|
320
|
+
elbv2_client = self.get_client("elbv2")
|
321
|
+
elb_client = self.get_client("elb") # Classic ELB
|
322
|
+
|
323
|
+
usage_info = {"in_use": False, "load_balancers": []}
|
324
|
+
|
325
|
+
# Check Application/Network Load Balancers
|
326
|
+
try:
|
327
|
+
paginator = elbv2_client.get_paginator("describe_load_balancers")
|
328
|
+
for page in paginator.paginate():
|
329
|
+
for lb in page["LoadBalancers"]:
|
330
|
+
lb_arn = lb["LoadBalancerArn"]
|
331
|
+
|
332
|
+
# Check listeners for certificate usage
|
333
|
+
listeners_response = self.execute_aws_call(
|
334
|
+
elbv2_client, "describe_listeners", LoadBalancerArn=lb_arn
|
335
|
+
)
|
336
|
+
|
337
|
+
for listener in listeners_response.get("Listeners", []):
|
338
|
+
for cert in listener.get("Certificates", []):
|
339
|
+
if cert.get("CertificateArn") == certificate_arn:
|
340
|
+
usage_info["in_use"] = True
|
341
|
+
usage_info["load_balancers"].append(
|
342
|
+
{
|
343
|
+
"type": "ALB/NLB",
|
344
|
+
"name": lb["LoadBalancerName"],
|
345
|
+
"arn": lb_arn,
|
346
|
+
"listener_arn": listener["ListenerArn"],
|
347
|
+
}
|
348
|
+
)
|
349
|
+
except Exception as e:
|
350
|
+
logger.warning(f"Could not check ALB/NLB usage: {e}")
|
351
|
+
|
352
|
+
# Check Classic Load Balancers
|
353
|
+
try:
|
354
|
+
classic_lbs = self.execute_aws_call(elb_client, "describe_load_balancers")
|
355
|
+
for lb in classic_lbs.get("LoadBalancerDescriptions", []):
|
356
|
+
for listener in lb.get("ListenerDescriptions", []):
|
357
|
+
listener_config = listener.get("Listener", {})
|
358
|
+
if listener_config.get("SSLCertificateId") == certificate_arn:
|
359
|
+
usage_info["in_use"] = True
|
360
|
+
usage_info["load_balancers"].append(
|
361
|
+
{"type": "Classic ELB", "name": lb["LoadBalancerName"], "dns_name": lb["DNSName"]}
|
362
|
+
)
|
363
|
+
except Exception as e:
|
364
|
+
logger.warning(f"Could not check Classic ELB usage: {e}")
|
365
|
+
|
366
|
+
return usage_info
|
367
|
+
|
368
|
+
except Exception as e:
|
369
|
+
logger.error(f"Error checking ELB usage: {e}")
|
370
|
+
return {"in_use": True, "error": str(e)} # Fail safe
|
371
|
+
|
372
|
+
def _check_cloudfront_usage(self, certificate_arn: str) -> Dict[str, Any]:
|
373
|
+
"""Check if certificate is used by any CloudFront distribution."""
|
374
|
+
try:
|
375
|
+
cloudfront_client = self.get_client("cloudfront")
|
376
|
+
|
377
|
+
usage_info = {"in_use": False, "distributions": []}
|
378
|
+
|
379
|
+
# List all CloudFront distributions
|
380
|
+
paginator = cloudfront_client.get_paginator("list_distributions")
|
381
|
+
for page in paginator.paginate():
|
382
|
+
for distribution in page.get("DistributionList", {}).get("Items", []):
|
383
|
+
dist_id = distribution["Id"]
|
384
|
+
|
385
|
+
# Get detailed distribution config
|
386
|
+
dist_config = self.execute_aws_call(cloudfront_client, "get_distribution", Id=dist_id)
|
387
|
+
|
388
|
+
viewer_cert = dist_config["Distribution"]["DistributionConfig"].get("ViewerCertificate", {})
|
389
|
+
if viewer_cert.get("ACMCertificateArn") == certificate_arn:
|
390
|
+
usage_info["in_use"] = True
|
391
|
+
usage_info["distributions"].append(
|
392
|
+
{"id": dist_id, "domain_name": distribution["DomainName"], "status": distribution["Status"]}
|
393
|
+
)
|
394
|
+
|
395
|
+
return usage_info
|
396
|
+
|
397
|
+
except Exception as e:
|
398
|
+
logger.error(f"Error checking CloudFront usage: {e}")
|
399
|
+
return {"in_use": True, "error": str(e)} # Fail safe
|
400
|
+
|
401
|
+
def _check_api_gateway_usage(self, certificate_arn: str) -> Dict[str, Any]:
|
402
|
+
"""Check if certificate is used by any API Gateway."""
|
403
|
+
try:
|
404
|
+
apigateway_client = self.get_client("apigateway")
|
405
|
+
|
406
|
+
usage_info = {"in_use": False, "domain_names": []}
|
407
|
+
|
408
|
+
# Check custom domain names
|
409
|
+
domain_names = self.execute_aws_call(apigateway_client, "get_domain_names")
|
410
|
+
for domain in domain_names.get("items", []):
|
411
|
+
if domain.get("certificateArn") == certificate_arn:
|
412
|
+
usage_info["in_use"] = True
|
413
|
+
usage_info["domain_names"].append(
|
414
|
+
{
|
415
|
+
"domain_name": domain["domainName"],
|
416
|
+
"certificate_name": domain.get("certificateName"),
|
417
|
+
"distribution_domain_name": domain.get("distributionDomainName"),
|
418
|
+
}
|
419
|
+
)
|
420
|
+
|
421
|
+
return usage_info
|
422
|
+
|
423
|
+
except Exception as e:
|
424
|
+
logger.error(f"Error checking API Gateway usage: {e}")
|
425
|
+
return {"in_use": True, "error": str(e)} # Fail safe
|
426
|
+
|
427
|
+
def _check_cloudformation_usage(self, certificate_arn: str) -> Dict[str, Any]:
|
428
|
+
"""Check if certificate is referenced in any CloudFormation stack."""
|
429
|
+
try:
|
430
|
+
cfn_client = self.get_client("cloudformation")
|
431
|
+
|
432
|
+
usage_info = {"in_use": False, "stacks": []}
|
433
|
+
|
434
|
+
# List all stacks
|
435
|
+
paginator = cfn_client.get_paginator("list_stacks")
|
436
|
+
for page in paginator.paginate(
|
437
|
+
StackStatusFilter=["CREATE_COMPLETE", "UPDATE_COMPLETE", "UPDATE_ROLLBACK_COMPLETE"]
|
438
|
+
):
|
439
|
+
for stack_summary in page["StackSummaries"]:
|
440
|
+
stack_name = stack_summary["StackName"]
|
441
|
+
|
442
|
+
try:
|
443
|
+
# Get stack template
|
444
|
+
template_response = self.execute_aws_call(cfn_client, "get_template", StackName=stack_name)
|
445
|
+
template_body = json.dumps(template_response.get("TemplateBody", {}))
|
446
|
+
|
447
|
+
# Simple string search for certificate ARN
|
448
|
+
if certificate_arn in template_body:
|
449
|
+
usage_info["in_use"] = True
|
450
|
+
usage_info["stacks"].append(
|
451
|
+
{
|
452
|
+
"stack_name": stack_name,
|
453
|
+
"stack_status": stack_summary["StackStatus"],
|
454
|
+
"creation_time": stack_summary["CreationTime"].isoformat(),
|
455
|
+
}
|
456
|
+
)
|
457
|
+
except Exception as e:
|
458
|
+
logger.debug(f"Could not check stack {stack_name}: {e}")
|
459
|
+
|
460
|
+
return usage_info
|
461
|
+
|
462
|
+
except Exception as e:
|
463
|
+
logger.error(f"Error checking CloudFormation usage: {e}")
|
464
|
+
return {"in_use": True, "error": str(e)} # Fail safe
|
465
|
+
|
466
|
+
def execute_remediation(self, context: RemediationContext, **kwargs) -> List[RemediationResult]:
|
467
|
+
"""
|
468
|
+
Execute ACM remediation operation with CRITICAL SAFETY CHECKS.
|
469
|
+
|
470
|
+
Args:
|
471
|
+
context: Remediation execution context
|
472
|
+
**kwargs: Operation-specific parameters
|
473
|
+
|
474
|
+
Returns:
|
475
|
+
List of remediation results
|
476
|
+
"""
|
477
|
+
operation_type = kwargs.get("operation_type", context.operation_type)
|
478
|
+
|
479
|
+
if operation_type == "cleanup_expired_certificates":
|
480
|
+
return self.cleanup_expired_certificates(context, **kwargs)
|
481
|
+
elif operation_type == "cleanup_unused_certificates":
|
482
|
+
return self.cleanup_unused_certificates(context, **kwargs)
|
483
|
+
elif operation_type == "analyze_certificate_usage":
|
484
|
+
return self.analyze_certificate_usage(context, **kwargs)
|
485
|
+
elif operation_type == "comprehensive_acm_security":
|
486
|
+
return self.comprehensive_acm_security(context, **kwargs)
|
487
|
+
else:
|
488
|
+
raise ValueError(f"Unsupported ACM remediation operation: {operation_type}")
|
489
|
+
|
490
|
+
def cleanup_expired_certificates(self, context: RemediationContext, **kwargs) -> List[RemediationResult]:
|
491
|
+
"""
|
492
|
+
CRITICAL OPERATION: Cleanup expired ACM certificates.
|
493
|
+
|
494
|
+
⚠️ WARNING: This operation DELETES certificates and can cause service outages
|
495
|
+
if expired certificates are still referenced by services.
|
496
|
+
|
497
|
+
Enhanced from original acm_cert_expired_unused.py with enterprise safety features.
|
498
|
+
|
499
|
+
Args:
|
500
|
+
context: Remediation execution context
|
501
|
+
**kwargs: Additional parameters
|
502
|
+
|
503
|
+
Returns:
|
504
|
+
List of remediation results
|
505
|
+
"""
|
506
|
+
result = self.create_remediation_result(context, "cleanup_expired_certificates", "acm:certificate", "all")
|
507
|
+
|
508
|
+
# Add compliance mapping
|
509
|
+
result.context.compliance_mapping = ComplianceMapping(
|
510
|
+
cis_controls=["CIS 3.1", "CIS 3.9"], nist_categories=["PR.PT-4", "DE.CM-8"], severity="high"
|
511
|
+
)
|
512
|
+
|
513
|
+
try:
|
514
|
+
acm_client = self.get_client("acm", context.region)
|
515
|
+
|
516
|
+
# Get all certificates
|
517
|
+
certificates_response = self.execute_aws_call(acm_client, "list_certificates")
|
518
|
+
all_certificates = certificates_response.get("CertificateSummaryList", [])
|
519
|
+
|
520
|
+
expired_certificates = []
|
521
|
+
certificates_analysis = []
|
522
|
+
|
523
|
+
# Analyze each certificate
|
524
|
+
for cert_summary in all_certificates:
|
525
|
+
cert_arn = cert_summary["CertificateArn"]
|
526
|
+
cert_status = cert_summary["Status"]
|
527
|
+
|
528
|
+
try:
|
529
|
+
# Get detailed certificate information
|
530
|
+
cert_details = self.execute_aws_call(acm_client, "describe_certificate", CertificateArn=cert_arn)
|
531
|
+
certificate = cert_details["Certificate"]
|
532
|
+
|
533
|
+
cert_analysis = {
|
534
|
+
"certificate_arn": cert_arn,
|
535
|
+
"domain_name": certificate.get("DomainName"),
|
536
|
+
"status": cert_status,
|
537
|
+
"not_after": certificate.get("NotAfter"),
|
538
|
+
"in_use": certificate.get("InUse", False),
|
539
|
+
"is_expired": cert_status == "EXPIRED",
|
540
|
+
"eligible_for_deletion": False,
|
541
|
+
}
|
542
|
+
|
543
|
+
# CRITICAL: Check if expired AND not in use
|
544
|
+
if cert_status == "EXPIRED" and not certificate.get("InUse", False):
|
545
|
+
# ADDITIONAL SAFETY: Verify usage across services
|
546
|
+
if self.usage_verification:
|
547
|
+
usage_analysis = self._verify_certificate_usage(cert_arn)
|
548
|
+
cert_analysis["usage_verification"] = usage_analysis
|
549
|
+
|
550
|
+
# Only mark for deletion if verification confirms no usage
|
551
|
+
if not usage_analysis["in_use"]:
|
552
|
+
expired_certificates.append(cert_arn)
|
553
|
+
cert_analysis["eligible_for_deletion"] = True
|
554
|
+
else:
|
555
|
+
logger.warning(
|
556
|
+
f"Certificate {cert_arn} is expired but usage verification found active usage!"
|
557
|
+
)
|
558
|
+
else:
|
559
|
+
expired_certificates.append(cert_arn)
|
560
|
+
cert_analysis["eligible_for_deletion"] = True
|
561
|
+
|
562
|
+
certificates_analysis.append(cert_analysis)
|
563
|
+
|
564
|
+
except Exception as e:
|
565
|
+
logger.error(f"Could not analyze certificate {cert_arn}: {e}")
|
566
|
+
|
567
|
+
if context.dry_run:
|
568
|
+
logger.info(f"[DRY-RUN] Would delete {len(expired_certificates)} expired certificates")
|
569
|
+
result.response_data = {
|
570
|
+
"certificates_analysis": certificates_analysis,
|
571
|
+
"expired_certificates": expired_certificates,
|
572
|
+
"action": "dry_run",
|
573
|
+
}
|
574
|
+
result.mark_completed(RemediationStatus.DRY_RUN)
|
575
|
+
return [result]
|
576
|
+
|
577
|
+
# CRITICAL SAFETY CHECK: Require explicit confirmation for certificate deletion
|
578
|
+
if self.require_confirmation and expired_certificates:
|
579
|
+
logger.critical(f"ABOUT TO DELETE {len(expired_certificates)} CERTIFICATES!")
|
580
|
+
logger.critical("This can cause PRODUCTION OUTAGES if certificates are still referenced!")
|
581
|
+
|
582
|
+
# In a real implementation, this would prompt for confirmation
|
583
|
+
# For now, we'll skip deletion unless explicitly forced
|
584
|
+
if not kwargs.get("force_delete", False):
|
585
|
+
result.response_data = {
|
586
|
+
"certificates_analysis": certificates_analysis,
|
587
|
+
"expired_certificates": expired_certificates,
|
588
|
+
"action": "confirmation_required",
|
589
|
+
"warning": "Certificate deletion requires explicit confirmation with force_delete=True",
|
590
|
+
}
|
591
|
+
result.mark_completed(
|
592
|
+
RemediationStatus.REQUIRES_MANUAL, "Certificate deletion requires explicit confirmation"
|
593
|
+
)
|
594
|
+
return [result]
|
595
|
+
|
596
|
+
# Execute certificate deletion with MAXIMUM SAFETY
|
597
|
+
deleted_certificates = []
|
598
|
+
failed_deletions = []
|
599
|
+
|
600
|
+
for cert_arn in expired_certificates:
|
601
|
+
try:
|
602
|
+
# CRITICAL: Create backup before deletion
|
603
|
+
backup_location = self.create_backup(context, cert_arn, "certificate_metadata")
|
604
|
+
result.backup_locations[cert_arn] = backup_location
|
605
|
+
|
606
|
+
# FINAL SAFETY CHECK: Re-verify certificate status
|
607
|
+
current_cert = self.execute_aws_call(acm_client, "describe_certificate", CertificateArn=cert_arn)
|
608
|
+
if current_cert["Certificate"]["Status"] != "EXPIRED":
|
609
|
+
logger.error(f"Certificate {cert_arn} status changed during operation! Skipping deletion.")
|
610
|
+
continue
|
611
|
+
|
612
|
+
# Execute deletion
|
613
|
+
self.execute_aws_call(acm_client, "delete_certificate", CertificateArn=cert_arn)
|
614
|
+
|
615
|
+
deleted_certificates.append(cert_arn)
|
616
|
+
logger.critical(f"DELETED expired certificate: {cert_arn}")
|
617
|
+
|
618
|
+
# Add to affected resources
|
619
|
+
result.affected_resources.append(f"acm:certificate:{cert_arn}")
|
620
|
+
|
621
|
+
except ClientError as e:
|
622
|
+
error_msg = f"Failed to delete certificate {cert_arn}: {e}"
|
623
|
+
logger.error(error_msg)
|
624
|
+
failed_deletions.append({"certificate_arn": cert_arn, "error": str(e)})
|
625
|
+
|
626
|
+
result.response_data = {
|
627
|
+
"certificates_analysis": certificates_analysis,
|
628
|
+
"expired_certificates": expired_certificates,
|
629
|
+
"deleted_certificates": deleted_certificates,
|
630
|
+
"failed_deletions": failed_deletions,
|
631
|
+
"total_deleted": len(deleted_certificates),
|
632
|
+
}
|
633
|
+
|
634
|
+
# Add compliance evidence
|
635
|
+
result.add_compliance_evidence(
|
636
|
+
"cis_aws",
|
637
|
+
{
|
638
|
+
"controls": ["3.1", "3.9"],
|
639
|
+
"certificates_cleaned": len(deleted_certificates),
|
640
|
+
"security_posture_improved": len(deleted_certificates) > 0,
|
641
|
+
"remediation_timestamp": result.start_time.isoformat(),
|
642
|
+
},
|
643
|
+
)
|
644
|
+
|
645
|
+
if len(deleted_certificates) == len(expired_certificates):
|
646
|
+
result.mark_completed(RemediationStatus.SUCCESS)
|
647
|
+
logger.critical(f"Successfully deleted {len(deleted_certificates)} expired certificates")
|
648
|
+
elif len(deleted_certificates) > 0:
|
649
|
+
result.mark_completed(RemediationStatus.SUCCESS) # Partial success
|
650
|
+
logger.warning(
|
651
|
+
f"Partially completed: {len(deleted_certificates)}/{len(expired_certificates)} certificates deleted"
|
652
|
+
)
|
653
|
+
else:
|
654
|
+
result.mark_completed(RemediationStatus.FAILED, "No certificates could be deleted")
|
655
|
+
|
656
|
+
except ClientError as e:
|
657
|
+
error_msg = f"Failed to cleanup expired certificates: {e}"
|
658
|
+
logger.error(error_msg)
|
659
|
+
result.mark_completed(RemediationStatus.FAILED, error_msg)
|
660
|
+
except Exception as e:
|
661
|
+
error_msg = f"Unexpected error during certificate cleanup: {e}"
|
662
|
+
logger.error(error_msg)
|
663
|
+
result.mark_completed(RemediationStatus.FAILED, error_msg)
|
664
|
+
|
665
|
+
return [result]
|
666
|
+
|
667
|
+
def analyze_certificate_usage(self, context: RemediationContext, **kwargs) -> List[RemediationResult]:
|
668
|
+
"""
|
669
|
+
Analyze ACM certificate usage and provide security recommendations.
|
670
|
+
|
671
|
+
Args:
|
672
|
+
context: Remediation execution context
|
673
|
+
**kwargs: Additional parameters
|
674
|
+
|
675
|
+
Returns:
|
676
|
+
List of remediation results with analysis data
|
677
|
+
"""
|
678
|
+
result = self.create_remediation_result(context, "analyze_certificate_usage", "acm:certificate", "all")
|
679
|
+
|
680
|
+
try:
|
681
|
+
acm_client = self.get_client("acm", context.region)
|
682
|
+
|
683
|
+
# Get all certificates
|
684
|
+
certificates_response = self.execute_aws_call(acm_client, "list_certificates")
|
685
|
+
all_certificates = certificates_response.get("CertificateSummaryList", [])
|
686
|
+
|
687
|
+
certificate_analyses = []
|
688
|
+
total_certificates = len(all_certificates)
|
689
|
+
|
690
|
+
# Analyze each certificate
|
691
|
+
for cert_summary in all_certificates:
|
692
|
+
cert_arn = cert_summary["CertificateArn"]
|
693
|
+
|
694
|
+
try:
|
695
|
+
cert_analysis = self._analyze_single_certificate(acm_client, cert_arn)
|
696
|
+
certificate_analyses.append(cert_analysis)
|
697
|
+
logger.info(f"Analyzed certificate: {cert_analysis['domain_name']}")
|
698
|
+
|
699
|
+
except Exception as e:
|
700
|
+
logger.warning(f"Could not analyze certificate {cert_arn}: {e}")
|
701
|
+
|
702
|
+
# Generate overall analytics
|
703
|
+
overall_analytics = self._generate_certificate_analytics(certificate_analyses)
|
704
|
+
|
705
|
+
result.response_data = {
|
706
|
+
"certificate_analyses": certificate_analyses,
|
707
|
+
"overall_analytics": overall_analytics,
|
708
|
+
"analysis_timestamp": result.start_time.isoformat(),
|
709
|
+
}
|
710
|
+
|
711
|
+
# Add compliance evidence
|
712
|
+
result.add_compliance_evidence(
|
713
|
+
"operational_excellence",
|
714
|
+
{
|
715
|
+
"certificates_analyzed": len(certificate_analyses),
|
716
|
+
"security_recommendations": overall_analytics.get("security_recommendations", 0),
|
717
|
+
"remediation_timestamp": result.start_time.isoformat(),
|
718
|
+
},
|
719
|
+
)
|
720
|
+
|
721
|
+
result.mark_completed(RemediationStatus.SUCCESS)
|
722
|
+
logger.info(f"Certificate analysis completed: {len(certificate_analyses)} certificates analyzed")
|
723
|
+
|
724
|
+
except ClientError as e:
|
725
|
+
error_msg = f"Failed to analyze certificates: {e}"
|
726
|
+
logger.error(error_msg)
|
727
|
+
result.mark_completed(RemediationStatus.FAILED, error_msg)
|
728
|
+
except Exception as e:
|
729
|
+
error_msg = f"Unexpected error during certificate analysis: {e}"
|
730
|
+
logger.error(error_msg)
|
731
|
+
result.mark_completed(RemediationStatus.FAILED, error_msg)
|
732
|
+
|
733
|
+
return [result]
|
734
|
+
|
735
|
+
def _analyze_single_certificate(self, acm_client: Any, certificate_arn: str) -> Dict[str, Any]:
|
736
|
+
"""Analyze a single ACM certificate."""
|
737
|
+
cert_details = self.execute_aws_call(acm_client, "describe_certificate", CertificateArn=certificate_arn)
|
738
|
+
certificate = cert_details["Certificate"]
|
739
|
+
|
740
|
+
# Basic certificate information
|
741
|
+
cert_info = {
|
742
|
+
"certificate_arn": certificate_arn,
|
743
|
+
"domain_name": certificate.get("DomainName"),
|
744
|
+
"subject_alternative_names": certificate.get("SubjectAlternativeNames", []),
|
745
|
+
"status": certificate.get("Status"),
|
746
|
+
"type": certificate.get("Type"),
|
747
|
+
"key_algorithm": certificate.get("KeyAlgorithm"),
|
748
|
+
"signature_algorithm": certificate.get("SignatureAlgorithm"),
|
749
|
+
"issued_at": certificate.get("IssuedAt"),
|
750
|
+
"not_before": certificate.get("NotBefore"),
|
751
|
+
"not_after": certificate.get("NotAfter"),
|
752
|
+
"in_use": certificate.get("InUse", False),
|
753
|
+
"is_expired": certificate.get("Status") == "EXPIRED",
|
754
|
+
}
|
755
|
+
|
756
|
+
# Calculate days until expiration
|
757
|
+
if cert_info["not_after"]:
|
758
|
+
now = datetime.now(tz=timezone.utc)
|
759
|
+
not_after = cert_info["not_after"]
|
760
|
+
if not_after.tzinfo is None:
|
761
|
+
not_after = not_after.replace(tzinfo=timezone.utc)
|
762
|
+
days_until_expiry = (not_after - now).days
|
763
|
+
cert_info["days_until_expiry"] = days_until_expiry
|
764
|
+
else:
|
765
|
+
cert_info["days_until_expiry"] = None
|
766
|
+
|
767
|
+
# Generate recommendations
|
768
|
+
recommendations = []
|
769
|
+
|
770
|
+
# Expiration recommendations
|
771
|
+
if cert_info["days_until_expiry"] is not None:
|
772
|
+
if cert_info["days_until_expiry"] < 0:
|
773
|
+
recommendations.append("Certificate is expired and should be deleted if not in use")
|
774
|
+
elif cert_info["days_until_expiry"] < 30:
|
775
|
+
recommendations.append("Certificate expires within 30 days - plan for renewal")
|
776
|
+
elif cert_info["days_until_expiry"] < 90:
|
777
|
+
recommendations.append("Certificate expires within 90 days - consider renewal planning")
|
778
|
+
|
779
|
+
# Security recommendations
|
780
|
+
if cert_info["key_algorithm"] and "RSA" in cert_info["key_algorithm"] and "1024" in cert_info["key_algorithm"]:
|
781
|
+
recommendations.append("Consider upgrading from RSA-1024 to RSA-2048 or higher for better security")
|
782
|
+
|
783
|
+
if cert_info["signature_algorithm"] and "SHA1" in cert_info["signature_algorithm"]:
|
784
|
+
recommendations.append("Consider upgrading from SHA-1 signature algorithm for better security")
|
785
|
+
|
786
|
+
# Usage recommendations
|
787
|
+
if not cert_info["in_use"] and cert_info["status"] == "ISSUED":
|
788
|
+
recommendations.append("Certificate is issued but not in use - consider deletion if not needed")
|
789
|
+
|
790
|
+
cert_info["recommendations"] = recommendations
|
791
|
+
|
792
|
+
return cert_info
|
793
|
+
|
794
|
+
def _generate_certificate_analytics(self, certificate_analyses: List[Dict[str, Any]]) -> Dict[str, Any]:
|
795
|
+
"""Generate overall certificate analytics."""
|
796
|
+
total_certificates = len(certificate_analyses)
|
797
|
+
if total_certificates == 0:
|
798
|
+
return {}
|
799
|
+
|
800
|
+
expired_certificates = sum(1 for cert in certificate_analyses if cert.get("is_expired", False))
|
801
|
+
expiring_soon = sum(
|
802
|
+
1
|
803
|
+
for cert in certificate_analyses
|
804
|
+
if cert.get("days_until_expiry") is not None and 0 <= cert.get("days_until_expiry") < 30
|
805
|
+
)
|
806
|
+
unused_certificates = sum(1 for cert in certificate_analyses if not cert.get("in_use", True))
|
807
|
+
certificates_with_recommendations = sum(1 for cert in certificate_analyses if cert.get("recommendations", []))
|
808
|
+
|
809
|
+
return {
|
810
|
+
"total_certificates": total_certificates,
|
811
|
+
"expired_certificates": expired_certificates,
|
812
|
+
"expiring_within_30_days": expiring_soon,
|
813
|
+
"unused_certificates": unused_certificates,
|
814
|
+
"certificates_with_recommendations": certificates_with_recommendations,
|
815
|
+
"security_recommendations": certificates_with_recommendations,
|
816
|
+
"security_posture": "NEEDS_ATTENTION" if expired_certificates > 0 or expiring_soon > 0 else "GOOD",
|
817
|
+
}
|
818
|
+
|
819
|
+
def comprehensive_acm_security(self, context: RemediationContext, **kwargs) -> List[RemediationResult]:
|
820
|
+
"""
|
821
|
+
Apply comprehensive ACM security configuration.
|
822
|
+
|
823
|
+
Combines certificate analysis and cleanup operations for complete certificate lifecycle management.
|
824
|
+
|
825
|
+
Args:
|
826
|
+
context: Remediation execution context
|
827
|
+
**kwargs: Additional parameters
|
828
|
+
|
829
|
+
Returns:
|
830
|
+
List of remediation results from all operations
|
831
|
+
"""
|
832
|
+
logger.info("Starting comprehensive ACM security remediation")
|
833
|
+
|
834
|
+
all_results = []
|
835
|
+
|
836
|
+
# Execute all security operations
|
837
|
+
security_operations = [
|
838
|
+
("analyze_certificate_usage", self.analyze_certificate_usage),
|
839
|
+
("cleanup_expired_certificates", self.cleanup_expired_certificates),
|
840
|
+
]
|
841
|
+
|
842
|
+
for operation_name, operation_method in security_operations:
|
843
|
+
try:
|
844
|
+
logger.info(f"Executing {operation_name}")
|
845
|
+
operation_results = operation_method(context, **kwargs)
|
846
|
+
all_results.extend(operation_results)
|
847
|
+
|
848
|
+
# Check if operation failed and handle accordingly
|
849
|
+
if any(r.failed for r in operation_results):
|
850
|
+
logger.warning(f"Operation {operation_name} failed")
|
851
|
+
if kwargs.get("fail_fast", False):
|
852
|
+
break
|
853
|
+
|
854
|
+
except Exception as e:
|
855
|
+
logger.error(f"Error in {operation_name}: {e}")
|
856
|
+
# Create error result
|
857
|
+
error_result = self.create_remediation_result(
|
858
|
+
context, operation_name, "acm:certificate", "comprehensive"
|
859
|
+
)
|
860
|
+
error_result.mark_completed(RemediationStatus.FAILED, str(e))
|
861
|
+
all_results.append(error_result)
|
862
|
+
|
863
|
+
if kwargs.get("fail_fast", False):
|
864
|
+
break
|
865
|
+
|
866
|
+
# Generate comprehensive summary
|
867
|
+
successful_operations = [r for r in all_results if r.success]
|
868
|
+
failed_operations = [r for r in all_results if r.failed]
|
869
|
+
|
870
|
+
logger.info(
|
871
|
+
f"Comprehensive ACM security remediation completed: "
|
872
|
+
f"{len(successful_operations)} successful, {len(failed_operations)} failed"
|
873
|
+
)
|
874
|
+
|
875
|
+
return all_results
|