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,526 @@
|
|
1
|
+
"""
|
2
|
+
Enterprise S3 Encryption Management - Automated Data Protection at Rest
|
3
|
+
|
4
|
+
## Overview
|
5
|
+
|
6
|
+
This module provides comprehensive S3 bucket encryption management to enhance
|
7
|
+
data protection and compliance posture. S3 encryption at rest is a fundamental
|
8
|
+
security requirement for protecting sensitive data stored in cloud environments.
|
9
|
+
|
10
|
+
## Key Features
|
11
|
+
|
12
|
+
- **Comprehensive Detection**: Identifies buckets without encryption enabled
|
13
|
+
- **Flexible Encryption**: Supports SSE-S3, SSE-KMS, and SSE-C options
|
14
|
+
- **KMS Integration**: Creates and manages customer-managed encryption keys
|
15
|
+
- **Bulk Operations**: Efficiently processes all buckets in an account
|
16
|
+
- **Compliance Integration**: Supports CIS, NIST, SOC2, and PCI DSS requirements
|
17
|
+
- **Cost Optimization**: Balanced approach between security and cost
|
18
|
+
|
19
|
+
## Encryption Options
|
20
|
+
|
21
|
+
**SSE-S3 (Server-Side Encryption with Amazon S3-Managed Keys):**
|
22
|
+
- Simplest option with no additional cost
|
23
|
+
- Automatic key management by AWS
|
24
|
+
- Good for basic encryption requirements
|
25
|
+
|
26
|
+
**SSE-KMS (Server-Side Encryption with AWS KMS):**
|
27
|
+
- Customer-managed encryption keys
|
28
|
+
- Detailed access logging and control
|
29
|
+
- Integration with AWS CloudTrail
|
30
|
+
- Additional per-request charges apply
|
31
|
+
|
32
|
+
**SSE-C (Server-Side Encryption with Customer-Provided Keys):**
|
33
|
+
- Customer provides encryption keys
|
34
|
+
- Maximum control over key management
|
35
|
+
- Requires client-side key management
|
36
|
+
|
37
|
+
## Usage Examples
|
38
|
+
|
39
|
+
```python
|
40
|
+
# Audit mode - detect buckets without encryption (safe)
|
41
|
+
python s3_encryption.py --dry-run
|
42
|
+
|
43
|
+
# Enable SSE-S3 encryption (default)
|
44
|
+
python s3_encryption.py --encryption-type sse-s3
|
45
|
+
|
46
|
+
# Enable SSE-KMS with new customer-managed key
|
47
|
+
python s3_encryption.py --encryption-type sse-kms --create-kms-key
|
48
|
+
```
|
49
|
+
|
50
|
+
## Important Security Notes
|
51
|
+
|
52
|
+
⚠️ **COST IMPACT**: SSE-KMS incurs additional charges per request
|
53
|
+
⚠️ **KEY MANAGEMENT**: Customer-managed keys require proper lifecycle management
|
54
|
+
⚠️ **COMPLIANCE**: Some regulations require specific encryption types
|
55
|
+
|
56
|
+
Version: 0.7.6 - Enterprise Production Ready
|
57
|
+
Compliance: CIS AWS Foundations 2.1.1, SOC2 A1.2, PCI DSS 3.4
|
58
|
+
"""
|
59
|
+
|
60
|
+
import logging
|
61
|
+
from typing import Any, Dict, List, Optional, Tuple
|
62
|
+
|
63
|
+
import click
|
64
|
+
from botocore.exceptions import BotoCoreError, ClientError
|
65
|
+
|
66
|
+
from .commons import display_aws_account_info, get_client
|
67
|
+
|
68
|
+
# Configure enterprise logging
|
69
|
+
logger = logging.getLogger(__name__)
|
70
|
+
logger.setLevel(logging.INFO)
|
71
|
+
|
72
|
+
|
73
|
+
def check_bucket_encryption_status(bucket_name: str, s3_client) -> Tuple[bool, Optional[Dict[str, Any]]]:
|
74
|
+
"""
|
75
|
+
Check the current encryption configuration for an S3 bucket.
|
76
|
+
|
77
|
+
This function queries the S3 service to determine whether server-side encryption
|
78
|
+
is currently enabled for the specified bucket. It handles various edge cases
|
79
|
+
and permission scenarios that may occur in enterprise environments.
|
80
|
+
|
81
|
+
## Implementation Details
|
82
|
+
|
83
|
+
- Uses S3 GetBucketEncryption API
|
84
|
+
- Handles permission and access errors gracefully
|
85
|
+
- Returns both status and configuration details
|
86
|
+
- Provides structured error logging for troubleshooting
|
87
|
+
|
88
|
+
## Security Considerations
|
89
|
+
|
90
|
+
- Requires s3:GetBucketEncryption permission
|
91
|
+
- May encounter cross-region access restrictions
|
92
|
+
- Bucket policies may deny encryption configuration access
|
93
|
+
|
94
|
+
Args:
|
95
|
+
bucket_name (str): S3 bucket name to check
|
96
|
+
Must be a valid S3 bucket name format
|
97
|
+
s3_client: Initialized boto3 S3 client instance
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
Tuple[bool, Optional[Dict[str, Any]]]: (is_encrypted, encryption_config)
|
101
|
+
- is_encrypted: True if encryption is configured
|
102
|
+
- encryption_config: Current encryption configuration or None
|
103
|
+
|
104
|
+
Raises:
|
105
|
+
ClientError: If S3 API access fails with unexpected errors
|
106
|
+
ValueError: If bucket_name is invalid
|
107
|
+
|
108
|
+
Example:
|
109
|
+
>>> s3_client = boto3.client('s3')
|
110
|
+
>>> is_encrypted, config = check_bucket_encryption_status('my-bucket', s3_client)
|
111
|
+
>>> if is_encrypted:
|
112
|
+
... print(f"Encryption: {config['Rules'][0]['ApplyServerSideEncryptionByDefault']['SSEAlgorithm']}")
|
113
|
+
... else:
|
114
|
+
... print("Encryption is not configured")
|
115
|
+
"""
|
116
|
+
|
117
|
+
# Input validation
|
118
|
+
if not bucket_name or not isinstance(bucket_name, str):
|
119
|
+
raise ValueError(f"Invalid bucket_name: {bucket_name}. Must be a non-empty string.")
|
120
|
+
|
121
|
+
logger.debug(f"Checking encryption status for bucket: {bucket_name}")
|
122
|
+
|
123
|
+
try:
|
124
|
+
# Query bucket encryption configuration
|
125
|
+
encryption_response = s3_client.get_bucket_encryption(Bucket=bucket_name)
|
126
|
+
|
127
|
+
# Check if encryption configuration exists
|
128
|
+
encryption_config = encryption_response.get("ServerSideEncryptionConfiguration", {})
|
129
|
+
rules = encryption_config.get("Rules", [])
|
130
|
+
|
131
|
+
is_encrypted = bool(rules)
|
132
|
+
|
133
|
+
if is_encrypted:
|
134
|
+
# Extract encryption details
|
135
|
+
default_encryption = rules[0].get("ApplyServerSideEncryptionByDefault", {})
|
136
|
+
algorithm = default_encryption.get("SSEAlgorithm", "Unknown")
|
137
|
+
kms_key = default_encryption.get("KMSMasterKeyID", "")
|
138
|
+
|
139
|
+
logger.debug(
|
140
|
+
f"Bucket {bucket_name} has {algorithm} encryption" + (f" with key {kms_key}" if kms_key else "")
|
141
|
+
)
|
142
|
+
else:
|
143
|
+
logger.debug(f"Bucket {bucket_name} does not have encryption configured")
|
144
|
+
|
145
|
+
return is_encrypted, encryption_config
|
146
|
+
|
147
|
+
except ClientError as e:
|
148
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
149
|
+
error_message = e.response.get("Error", {}).get("Message", str(e))
|
150
|
+
|
151
|
+
# Handle specific S3 errors gracefully
|
152
|
+
if error_code == "NoSuchBucket":
|
153
|
+
logger.warning(f"Bucket not found: {bucket_name}")
|
154
|
+
return False, None
|
155
|
+
elif error_code in ["AccessDenied", "Forbidden"]:
|
156
|
+
logger.warning(f"Insufficient permissions to check encryption for bucket: {bucket_name}")
|
157
|
+
return False, None
|
158
|
+
elif error_code == "ServerSideEncryptionConfigurationNotFoundError":
|
159
|
+
logger.debug(f"No encryption configuration found for bucket: {bucket_name}")
|
160
|
+
return False, None
|
161
|
+
else:
|
162
|
+
logger.error(f"S3 API error checking encryption for {bucket_name}: {error_code} - {error_message}")
|
163
|
+
raise
|
164
|
+
|
165
|
+
except BotoCoreError as e:
|
166
|
+
logger.error(f"AWS service error checking encryption for {bucket_name}: {e}")
|
167
|
+
raise
|
168
|
+
|
169
|
+
except Exception as e:
|
170
|
+
logger.error(f"Unexpected error checking encryption for {bucket_name}: {e}")
|
171
|
+
raise
|
172
|
+
|
173
|
+
|
174
|
+
def create_kms_key_for_s3(description: str = "S3 Bucket Encryption Key", kms_client=None) -> Optional[str]:
|
175
|
+
"""
|
176
|
+
Create a new customer-managed KMS key for S3 bucket encryption.
|
177
|
+
|
178
|
+
This function creates a new KMS key specifically designed for S3 bucket
|
179
|
+
encryption. The key includes appropriate metadata and tags for identification
|
180
|
+
and management purposes.
|
181
|
+
|
182
|
+
## Implementation Details
|
183
|
+
|
184
|
+
- Uses KMS CreateKey API (not S3!)
|
185
|
+
- Configures key for symmetric encryption use
|
186
|
+
- Adds descriptive tags for management
|
187
|
+
- Returns key ID for immediate use
|
188
|
+
|
189
|
+
## Security Benefits
|
190
|
+
|
191
|
+
- **Customer Control**: Full control over key lifecycle
|
192
|
+
- **Access Logging**: CloudTrail logs all key usage
|
193
|
+
- **Key Rotation**: Supports automatic annual rotation
|
194
|
+
- **Cross-Account**: Can be shared across accounts if needed
|
195
|
+
|
196
|
+
Args:
|
197
|
+
description (str): Human-readable description for the KMS key
|
198
|
+
kms_client: Initialized boto3 KMS client instance
|
199
|
+
|
200
|
+
Returns:
|
201
|
+
Optional[str]: KMS key ID if creation successful, None otherwise
|
202
|
+
|
203
|
+
Raises:
|
204
|
+
ClientError: If KMS API access fails with unexpected errors
|
205
|
+
|
206
|
+
Example:
|
207
|
+
>>> kms_client = boto3.client('kms')
|
208
|
+
>>> key_id = create_kms_key_for_s3("Production S3 Encryption", kms_client)
|
209
|
+
>>> if key_id:
|
210
|
+
... print(f"Created KMS key: {key_id}")
|
211
|
+
... else:
|
212
|
+
... print("Failed to create KMS key")
|
213
|
+
"""
|
214
|
+
|
215
|
+
if not kms_client:
|
216
|
+
raise ValueError("KMS client is required for key creation")
|
217
|
+
|
218
|
+
logger.info("🔐 Creating new KMS key for S3 bucket encryption...")
|
219
|
+
|
220
|
+
try:
|
221
|
+
# Create KMS key with appropriate configuration
|
222
|
+
key_response = kms_client.create_key(
|
223
|
+
Description=description,
|
224
|
+
KeyUsage="ENCRYPT_DECRYPT",
|
225
|
+
Origin="AWS_KMS",
|
226
|
+
KeySpec="SYMMETRIC_DEFAULT", # Required for S3 encryption
|
227
|
+
Tags=[
|
228
|
+
{"TagKey": "Purpose", "TagValue": "S3-Bucket-Encryption"},
|
229
|
+
{"TagKey": "CreatedBy", "TagValue": "CloudOps-Remediation-Script"},
|
230
|
+
{"TagKey": "Service", "TagValue": "Amazon-S3"},
|
231
|
+
],
|
232
|
+
)
|
233
|
+
|
234
|
+
key_id = key_response["KeyMetadata"]["KeyId"]
|
235
|
+
key_arn = key_response["KeyMetadata"]["Arn"]
|
236
|
+
|
237
|
+
logger.info(f"✅ Successfully created KMS key: {key_id}")
|
238
|
+
logger.info(f" 📋 Key ARN: {key_arn}")
|
239
|
+
|
240
|
+
# Optionally enable key rotation
|
241
|
+
try:
|
242
|
+
kms_client.enable_key_rotation(KeyId=key_id)
|
243
|
+
logger.info(f"✅ Enabled automatic key rotation for: {key_id}")
|
244
|
+
except ClientError as e:
|
245
|
+
logger.warning(f"⚠️ Could not enable key rotation: {e}")
|
246
|
+
|
247
|
+
return key_id
|
248
|
+
|
249
|
+
except ClientError as e:
|
250
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
251
|
+
error_message = e.response.get("Error", {}).get("Message", str(e))
|
252
|
+
|
253
|
+
logger.error(f"❌ KMS API error creating key: {error_code} - {error_message}")
|
254
|
+
|
255
|
+
# Handle specific KMS errors
|
256
|
+
if error_code in ["AccessDenied", "UnauthorizedOperation"]:
|
257
|
+
logger.error("🔒 Insufficient permissions to create KMS keys")
|
258
|
+
logger.error(" Required permissions: kms:CreateKey, kms:TagResource, kms:EnableKeyRotation")
|
259
|
+
elif error_code == "LimitExceededException":
|
260
|
+
logger.error("📊 KMS key limit exceeded - consider deleting unused keys")
|
261
|
+
|
262
|
+
return None
|
263
|
+
|
264
|
+
except BotoCoreError as e:
|
265
|
+
logger.error(f"❌ AWS service error creating KMS key: {e}")
|
266
|
+
return None
|
267
|
+
|
268
|
+
except Exception as e:
|
269
|
+
logger.error(f"❌ Unexpected error creating KMS key: {e}")
|
270
|
+
raise
|
271
|
+
|
272
|
+
|
273
|
+
def enable_bucket_encryption(bucket_name: str, encryption_type: str, kms_key_id: Optional[str], s3_client) -> bool:
|
274
|
+
"""
|
275
|
+
Enable server-side encryption for a specific S3 bucket.
|
276
|
+
|
277
|
+
This function configures S3 server-side encryption for the specified bucket
|
278
|
+
using the requested encryption type and key. This is essential for data
|
279
|
+
protection and compliance requirements.
|
280
|
+
|
281
|
+
## Implementation Details
|
282
|
+
|
283
|
+
- Uses S3 PutBucketEncryption API
|
284
|
+
- Supports SSE-S3 and SSE-KMS encryption types
|
285
|
+
- Validates encryption configuration before application
|
286
|
+
- Provides comprehensive error handling and logging
|
287
|
+
|
288
|
+
## Security Benefits
|
289
|
+
|
290
|
+
- **Data Protection**: Encrypts data at rest automatically
|
291
|
+
- **Compliance**: Meets regulatory requirements for data encryption
|
292
|
+
- **Key Management**: Supports both AWS and customer-managed keys
|
293
|
+
- **Performance**: Encryption is transparent to applications
|
294
|
+
|
295
|
+
Args:
|
296
|
+
bucket_name (str): S3 bucket to enable encryption for
|
297
|
+
encryption_type (str): Encryption type ('sse-s3' or 'sse-kms')
|
298
|
+
kms_key_id (str): KMS key ID for SSE-KMS (optional for SSE-S3)
|
299
|
+
s3_client: Initialized boto3 S3 client instance
|
300
|
+
|
301
|
+
Returns:
|
302
|
+
bool: True if encryption was successfully enabled, False otherwise
|
303
|
+
|
304
|
+
Raises:
|
305
|
+
ValueError: If parameters are invalid
|
306
|
+
ClientError: If S3 API access fails with unexpected errors
|
307
|
+
|
308
|
+
Example:
|
309
|
+
>>> s3_client = boto3.client('s3')
|
310
|
+
>>> success = enable_bucket_encryption('my-bucket', 'sse-s3', None, s3_client)
|
311
|
+
>>> if success:
|
312
|
+
... print("Encryption enabled successfully")
|
313
|
+
... else:
|
314
|
+
... print("Failed to enable encryption - check logs")
|
315
|
+
"""
|
316
|
+
|
317
|
+
# Input validation
|
318
|
+
if not bucket_name or not isinstance(bucket_name, str):
|
319
|
+
raise ValueError(f"Invalid bucket_name: {bucket_name}. Must be a non-empty string.")
|
320
|
+
|
321
|
+
if encryption_type not in ["sse-s3", "sse-kms"]:
|
322
|
+
raise ValueError(f"Invalid encryption_type: {encryption_type}. Must be 'sse-s3' or 'sse-kms'.")
|
323
|
+
|
324
|
+
if encryption_type == "sse-kms" and not kms_key_id:
|
325
|
+
raise ValueError("KMS key ID is required for SSE-KMS encryption.")
|
326
|
+
|
327
|
+
logger.info(f"🔒 Enabling {encryption_type.upper()} encryption for bucket: {bucket_name}")
|
328
|
+
if kms_key_id:
|
329
|
+
logger.info(f" 🔑 Using KMS key: {kms_key_id}")
|
330
|
+
|
331
|
+
try:
|
332
|
+
# Build encryption configuration based on type
|
333
|
+
if encryption_type == "sse-s3":
|
334
|
+
encryption_config = {"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]}
|
335
|
+
else: # sse-kms
|
336
|
+
encryption_config = {
|
337
|
+
"Rules": [
|
338
|
+
{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "aws:kms", "KMSMasterKeyID": kms_key_id}}
|
339
|
+
]
|
340
|
+
}
|
341
|
+
|
342
|
+
# Apply encryption configuration
|
343
|
+
s3_client.put_bucket_encryption(Bucket=bucket_name, ServerSideEncryptionConfiguration=encryption_config)
|
344
|
+
|
345
|
+
logger.info(f"✅ Successfully enabled {encryption_type.upper()} encryption for bucket: {bucket_name}")
|
346
|
+
return True
|
347
|
+
|
348
|
+
except ClientError as e:
|
349
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
350
|
+
error_message = e.response.get("Error", {}).get("Message", str(e))
|
351
|
+
|
352
|
+
# Handle specific S3 errors with informative messages
|
353
|
+
if error_code == "NoSuchBucket":
|
354
|
+
logger.error(f"❌ Bucket not found: {bucket_name}")
|
355
|
+
elif error_code in ["AccessDenied", "Forbidden"]:
|
356
|
+
logger.error(f"🔒 Insufficient permissions to enable encryption for bucket: {bucket_name}")
|
357
|
+
logger.error(" Required permissions: s3:PutBucketEncryption")
|
358
|
+
elif error_code == "InvalidRequest":
|
359
|
+
logger.error(f"❌ Invalid encryption configuration for bucket: {bucket_name}")
|
360
|
+
logger.error(f" Check encryption type and KMS key: {kms_key_id}")
|
361
|
+
elif error_code == "KMSNotFoundException":
|
362
|
+
logger.error(f"❌ KMS key not found: {kms_key_id}")
|
363
|
+
else:
|
364
|
+
logger.error(f"❌ S3 API error enabling encryption for {bucket_name}: {error_code} - {error_message}")
|
365
|
+
|
366
|
+
return False
|
367
|
+
|
368
|
+
except BotoCoreError as e:
|
369
|
+
logger.error(f"❌ AWS service error enabling encryption for {bucket_name}: {e}")
|
370
|
+
return False
|
371
|
+
|
372
|
+
except Exception as e:
|
373
|
+
logger.error(f"❌ Unexpected error enabling encryption for {bucket_name}: {e}")
|
374
|
+
raise
|
375
|
+
|
376
|
+
|
377
|
+
@click.command()
|
378
|
+
@click.option(
|
379
|
+
"--dry-run", is_flag=True, default=True, help="Preview mode - show buckets that need encryption without enabling it"
|
380
|
+
)
|
381
|
+
@click.option(
|
382
|
+
"--encryption-type",
|
383
|
+
type=click.Choice(["sse-s3", "sse-kms"]),
|
384
|
+
default="sse-s3",
|
385
|
+
help="Encryption type: sse-s3 (free) or sse-kms (customer-managed)",
|
386
|
+
)
|
387
|
+
@click.option("--kms-key-id", type=str, help="Existing KMS key ID for SSE-KMS (creates new if not provided)")
|
388
|
+
@click.option("--create-kms-key", is_flag=True, help="Create a new KMS key for SSE-KMS encryption")
|
389
|
+
@click.option("--region", type=str, help="AWS region to scan (defaults to current region)")
|
390
|
+
@click.option("--bucket-filter", type=str, help="Filter buckets by name pattern (supports wildcards)")
|
391
|
+
@click.option("--output-file", type=str, help="Save results to CSV file")
|
392
|
+
def check_and_enable_encryption(
|
393
|
+
dry_run: bool,
|
394
|
+
encryption_type: str,
|
395
|
+
kms_key_id: Optional[str],
|
396
|
+
create_kms_key: bool,
|
397
|
+
region: Optional[str],
|
398
|
+
bucket_filter: Optional[str],
|
399
|
+
output_file: Optional[str],
|
400
|
+
):
|
401
|
+
"""
|
402
|
+
Enterprise S3 Encryption Management - Bulk encryption enablement for data protection.
|
403
|
+
|
404
|
+
This command provides comprehensive detection and enablement of S3 server-side encryption
|
405
|
+
across all buckets in your AWS account. Encryption at rest is a fundamental security
|
406
|
+
requirement for protecting sensitive data stored in cloud environments.
|
407
|
+
|
408
|
+
Examples:
|
409
|
+
# Safe audit of all buckets (recommended first step)
|
410
|
+
python s3_encryption.py --dry-run
|
411
|
+
|
412
|
+
# Enable SSE-S3 encryption (free)
|
413
|
+
python s3_encryption.py --no-dry-run --encryption-type sse-s3
|
414
|
+
"""
|
415
|
+
|
416
|
+
# Input validation
|
417
|
+
if encryption_type == "sse-kms" and not kms_key_id and not create_kms_key:
|
418
|
+
raise click.ClickException("For SSE-KMS encryption, either provide --kms-key-id or use --create-kms-key")
|
419
|
+
|
420
|
+
# Enhanced logging for operation start
|
421
|
+
operation_mode = "DRY-RUN (Safe Audit)" if dry_run else "ENABLEMENT (Configuration Change)"
|
422
|
+
logger.info(f"🔒 Starting S3 Encryption Analysis - Mode: {operation_mode}")
|
423
|
+
logger.info(f"📊 Configuration: region={region or 'default'}, filter={bucket_filter or 'none'}")
|
424
|
+
logger.info(
|
425
|
+
f"🔐 Encryption settings: type={encryption_type}, kms_key={kms_key_id or 'create new' if create_kms_key else 'N/A'}"
|
426
|
+
)
|
427
|
+
|
428
|
+
# Display account information for verification
|
429
|
+
account_info = display_aws_account_info()
|
430
|
+
logger.info(f"🏢 {account_info}")
|
431
|
+
|
432
|
+
if not dry_run:
|
433
|
+
logger.warning("⚠️ CONFIGURATION MODE ENABLED - Bucket encryption will be enabled!")
|
434
|
+
if encryption_type == "sse-kms":
|
435
|
+
logger.warning("⚠️ SSE-KMS incurs additional charges per request!")
|
436
|
+
|
437
|
+
try:
|
438
|
+
# Initialize AWS clients
|
439
|
+
s3_client = get_client("s3", region_name=region)
|
440
|
+
kms_client = get_client("kms", region_name=region) if encryption_type == "sse-kms" else None
|
441
|
+
logger.debug(f"Initialized AWS clients for region: {region or 'default'}")
|
442
|
+
|
443
|
+
# Create KMS key if needed for SSE-KMS
|
444
|
+
effective_kms_key_id = kms_key_id
|
445
|
+
if encryption_type == "sse-kms" and create_kms_key and not dry_run:
|
446
|
+
logger.info("🔐 Creating new KMS key for S3 encryption...")
|
447
|
+
effective_kms_key_id = create_kms_key_for_s3(
|
448
|
+
f"S3 Bucket Encryption Key - Created by CloudOps Remediation", kms_client
|
449
|
+
)
|
450
|
+
if not effective_kms_key_id:
|
451
|
+
logger.error("❌ Failed to create KMS key - aborting encryption enablement")
|
452
|
+
return
|
453
|
+
|
454
|
+
# List all buckets
|
455
|
+
try:
|
456
|
+
response = s3_client.list_buckets()
|
457
|
+
all_buckets = response.get("Buckets", [])
|
458
|
+
except ClientError as e:
|
459
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
460
|
+
logger.error(f"❌ Failed to list S3 buckets: {error_code}")
|
461
|
+
raise
|
462
|
+
|
463
|
+
logger.info(f"📋 Found {len(all_buckets)} total buckets to analyze")
|
464
|
+
|
465
|
+
# Process buckets
|
466
|
+
buckets_without_encryption = []
|
467
|
+
buckets_with_encryption = []
|
468
|
+
successful_enablements = 0
|
469
|
+
|
470
|
+
for bucket in all_buckets:
|
471
|
+
bucket_name = bucket["Name"]
|
472
|
+
|
473
|
+
# Apply bucket filtering if specified
|
474
|
+
if bucket_filter:
|
475
|
+
if bucket_filter.replace("*", "") not in bucket_name:
|
476
|
+
continue
|
477
|
+
|
478
|
+
try:
|
479
|
+
# Check current encryption status
|
480
|
+
is_encrypted, encryption_config = check_bucket_encryption_status(bucket_name, s3_client)
|
481
|
+
|
482
|
+
if is_encrypted:
|
483
|
+
buckets_with_encryption.append(bucket_name)
|
484
|
+
logger.debug(f"✅ ENCRYPTED: {bucket_name}")
|
485
|
+
else:
|
486
|
+
buckets_without_encryption.append(bucket_name)
|
487
|
+
logger.info(f"🎯 NEEDS ENCRYPTION: {bucket_name}")
|
488
|
+
|
489
|
+
# Enable encryption if not in dry-run mode
|
490
|
+
if not dry_run:
|
491
|
+
success = enable_bucket_encryption(
|
492
|
+
bucket_name, encryption_type, effective_kms_key_id, s3_client
|
493
|
+
)
|
494
|
+
if success:
|
495
|
+
successful_enablements += 1
|
496
|
+
|
497
|
+
except Exception as e:
|
498
|
+
logger.warning(f"⚠️ Could not analyze bucket {bucket_name}: {e}")
|
499
|
+
continue
|
500
|
+
|
501
|
+
# Generate summary
|
502
|
+
needs_encryption_count = len(buckets_without_encryption)
|
503
|
+
already_encrypted_count = len(buckets_with_encryption)
|
504
|
+
|
505
|
+
logger.info("📊 S3 ENCRYPTION ANALYSIS SUMMARY:")
|
506
|
+
logger.info(f" ✅ Buckets with encryption: {already_encrypted_count}")
|
507
|
+
logger.info(f" 🎯 Buckets needing encryption: {needs_encryption_count}")
|
508
|
+
|
509
|
+
if not dry_run and successful_enablements > 0:
|
510
|
+
logger.info(f" 🔒 Successfully enabled encryption: {successful_enablements} buckets")
|
511
|
+
|
512
|
+
# Final summary
|
513
|
+
if dry_run and needs_encryption_count > 0:
|
514
|
+
logger.info(
|
515
|
+
f"💡 To enable {encryption_type.upper()} encryption on {needs_encryption_count} buckets, run with --no-dry-run"
|
516
|
+
)
|
517
|
+
elif not dry_run:
|
518
|
+
logger.info("✅ ENCRYPTION ENABLEMENT COMPLETE")
|
519
|
+
|
520
|
+
except Exception as e:
|
521
|
+
logger.error(f"❌ Error during S3 encryption analysis: {e}")
|
522
|
+
raise
|
523
|
+
|
524
|
+
|
525
|
+
if __name__ == "__main__":
|
526
|
+
check_and_enable_encryption()
|
@@ -0,0 +1,143 @@
|
|
1
|
+
"""
|
2
|
+
S3 HTTPS-Only Policy - Enforce secure transport for all S3 bucket access.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
|
8
|
+
import click
|
9
|
+
from botocore.exceptions import ClientError
|
10
|
+
|
11
|
+
from .commons import display_aws_account_info, get_client
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
def check_https_only_policy(bucket_name):
|
17
|
+
"""Check if S3 bucket has HTTPS-only policy enabled."""
|
18
|
+
try:
|
19
|
+
s3 = get_client("s3")
|
20
|
+
|
21
|
+
# Get existing bucket policy
|
22
|
+
response = s3.get_bucket_policy(Bucket=bucket_name)
|
23
|
+
policy_json = json.loads(response["Policy"])
|
24
|
+
|
25
|
+
# Look for HTTPS-only deny statement
|
26
|
+
for statement in policy_json.get("Statement", []):
|
27
|
+
if (
|
28
|
+
statement.get("Effect") == "Deny"
|
29
|
+
and statement.get("Condition", {}).get("Bool", {}).get("aws:SecureTransport") == "false"
|
30
|
+
):
|
31
|
+
logger.debug(f"Bucket '{bucket_name}' has HTTPS-only policy")
|
32
|
+
return True
|
33
|
+
|
34
|
+
logger.debug(f"Bucket '{bucket_name}' does not have HTTPS-only policy")
|
35
|
+
return False
|
36
|
+
|
37
|
+
except ClientError as e:
|
38
|
+
error_code = e.response.get("Error", {}).get("Code", "Unknown")
|
39
|
+
if error_code == "NoSuchBucketPolicy":
|
40
|
+
logger.debug(f"Bucket '{bucket_name}' has no bucket policy")
|
41
|
+
return False
|
42
|
+
else:
|
43
|
+
logger.error(f"Failed to check bucket policy for '{bucket_name}': {e}")
|
44
|
+
return False
|
45
|
+
|
46
|
+
|
47
|
+
def update_https_only_policy(bucket_name):
|
48
|
+
"""Add HTTPS-only policy to S3 bucket."""
|
49
|
+
try:
|
50
|
+
s3 = get_client("s3")
|
51
|
+
|
52
|
+
# Get existing bucket policy or create new one
|
53
|
+
try:
|
54
|
+
response = s3.get_bucket_policy(Bucket=bucket_name)
|
55
|
+
policy_json = json.loads(response["Policy"])
|
56
|
+
except ClientError as e:
|
57
|
+
if e.response.get("Error", {}).get("Code") == "NoSuchBucketPolicy":
|
58
|
+
# Create new policy if none exists
|
59
|
+
policy_json = {"Version": "2012-10-17", "Statement": []}
|
60
|
+
else:
|
61
|
+
raise
|
62
|
+
|
63
|
+
# Create HTTPS-only policy statement
|
64
|
+
https_only_statement = {
|
65
|
+
"Sid": "AllowSSLRequestsOnly",
|
66
|
+
"Effect": "Deny",
|
67
|
+
"Principal": "*",
|
68
|
+
"Action": "s3:*",
|
69
|
+
"Resource": [f"arn:aws:s3:::{bucket_name}/*", f"arn:aws:s3:::{bucket_name}"],
|
70
|
+
"Condition": {"Bool": {"aws:SecureTransport": "false"}},
|
71
|
+
}
|
72
|
+
|
73
|
+
# Add the statement to the policy
|
74
|
+
policy_json["Statement"].append(https_only_statement)
|
75
|
+
|
76
|
+
# Apply the updated policy
|
77
|
+
s3.put_bucket_policy(Bucket=bucket_name, Policy=json.dumps(policy_json))
|
78
|
+
logger.info(f"Applied HTTPS-only policy to bucket '{bucket_name}'")
|
79
|
+
return True
|
80
|
+
|
81
|
+
except ClientError as e:
|
82
|
+
logger.error(f"Failed to update policy for bucket '{bucket_name}': {e}")
|
83
|
+
return False
|
84
|
+
|
85
|
+
|
86
|
+
@click.command()
|
87
|
+
@click.option("--dry-run", is_flag=True, default=True, help="Preview mode - show actions without making changes")
|
88
|
+
def enable_ssl_secure_on_all_buckets(dry_run: bool = True):
|
89
|
+
"""Enforce HTTPS-only access for all S3 buckets."""
|
90
|
+
logger.info(f"Checking S3 HTTPS policies in {display_aws_account_info()}")
|
91
|
+
|
92
|
+
try:
|
93
|
+
s3 = get_client("s3")
|
94
|
+
response = s3.list_buckets()
|
95
|
+
buckets = response.get("Buckets", [])
|
96
|
+
|
97
|
+
if not buckets:
|
98
|
+
logger.info("No S3 buckets found")
|
99
|
+
return
|
100
|
+
|
101
|
+
logger.info(f"Found {len(buckets)} buckets to check")
|
102
|
+
|
103
|
+
# Track results
|
104
|
+
buckets_with_https = []
|
105
|
+
buckets_without_https = []
|
106
|
+
buckets_updated = []
|
107
|
+
|
108
|
+
# Check each bucket
|
109
|
+
for bucket in buckets:
|
110
|
+
bucket_name = bucket["Name"]
|
111
|
+
logger.info(f"Checking bucket: {bucket_name}")
|
112
|
+
|
113
|
+
has_https_policy = check_https_only_policy(bucket_name)
|
114
|
+
|
115
|
+
if has_https_policy:
|
116
|
+
buckets_with_https.append(bucket_name)
|
117
|
+
logger.info(f" ✓ HTTPS-only policy already enabled")
|
118
|
+
else:
|
119
|
+
buckets_without_https.append(bucket_name)
|
120
|
+
logger.info(f" ✗ HTTPS-only policy not found")
|
121
|
+
|
122
|
+
# Apply policy if not in dry-run mode
|
123
|
+
if not dry_run:
|
124
|
+
logger.info(f" → Adding HTTPS-only policy to '{bucket_name}'...")
|
125
|
+
if update_https_only_policy(bucket_name):
|
126
|
+
buckets_updated.append(bucket_name)
|
127
|
+
logger.info(f" ✓ Successfully applied HTTPS-only policy")
|
128
|
+
else:
|
129
|
+
logger.error(f" ✗ Failed to apply HTTPS-only policy")
|
130
|
+
|
131
|
+
# Summary
|
132
|
+
logger.info("\n=== SUMMARY ===")
|
133
|
+
logger.info(f"Buckets with HTTPS policy: {len(buckets_with_https)}")
|
134
|
+
logger.info(f"Buckets without HTTPS policy: {len(buckets_without_https)}")
|
135
|
+
|
136
|
+
if dry_run and buckets_without_https:
|
137
|
+
logger.info(f"To apply HTTPS policies to {len(buckets_without_https)} buckets, run with --no-dry-run")
|
138
|
+
elif not dry_run:
|
139
|
+
logger.info(f"Successfully applied HTTPS policies to {len(buckets_updated)} buckets")
|
140
|
+
|
141
|
+
except Exception as e:
|
142
|
+
logger.error(f"Failed to process S3 HTTPS policies: {e}")
|
143
|
+
raise
|