aws-cis-controls-assessment 1.1.4__py3-none-any.whl → 1.2.0__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.
- aws_cis_assessment/__init__.py +4 -4
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +365 -2
- aws_cis_assessment/controls/ig1/control_access_analyzer.py +198 -0
- aws_cis_assessment/controls/ig1/control_access_asset_mgmt.py +360 -0
- aws_cis_assessment/controls/ig1/control_access_control.py +323 -0
- aws_cis_assessment/controls/ig1/control_backup_security.py +579 -0
- aws_cis_assessment/controls/ig1/control_cloudfront_logging.py +215 -0
- aws_cis_assessment/controls/ig1/control_configuration_mgmt.py +407 -0
- aws_cis_assessment/controls/ig1/control_data_classification.py +255 -0
- aws_cis_assessment/controls/ig1/control_dynamodb_encryption.py +279 -0
- aws_cis_assessment/controls/ig1/control_ebs_encryption.py +177 -0
- aws_cis_assessment/controls/ig1/control_efs_encryption.py +243 -0
- aws_cis_assessment/controls/ig1/control_elb_logging.py +195 -0
- aws_cis_assessment/controls/ig1/control_guardduty.py +156 -0
- aws_cis_assessment/controls/ig1/control_inspector.py +184 -0
- aws_cis_assessment/controls/ig1/control_inventory.py +511 -0
- aws_cis_assessment/controls/ig1/control_macie.py +165 -0
- aws_cis_assessment/controls/ig1/control_messaging_encryption.py +419 -0
- aws_cis_assessment/controls/ig1/control_mfa.py +485 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +194 -619
- aws_cis_assessment/controls/ig1/control_patch_management.py +626 -0
- aws_cis_assessment/controls/ig1/control_rds_encryption.py +228 -0
- aws_cis_assessment/controls/ig1/control_s3_encryption.py +383 -0
- aws_cis_assessment/controls/ig1/control_tls_ssl.py +556 -0
- aws_cis_assessment/controls/ig1/control_version_mgmt.py +329 -0
- aws_cis_assessment/controls/ig1/control_vpc_flow_logs.py +205 -0
- aws_cis_assessment/controls/ig1/control_waf_logging.py +226 -0
- aws_cis_assessment/core/models.py +20 -1
- aws_cis_assessment/core/scoring_engine.py +98 -1
- aws_cis_assessment/reporters/base_reporter.py +31 -1
- aws_cis_assessment/reporters/html_reporter.py +163 -0
- aws_cis_controls_assessment-1.2.0.dist-info/METADATA +320 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/RECORD +39 -15
- docs/developer-guide.md +204 -5
- docs/user-guide.md +137 -4
- aws_cis_controls_assessment-1.1.4.dist-info/METADATA +0 -404
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CIS Control 3.12 - Data Classification
|
|
3
|
+
Ensures proper data classification tagging across AWS resources.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
9
|
+
|
|
10
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
11
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
12
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataClassificationTaggingAssessment(BaseConfigRuleAssessment):
|
|
18
|
+
"""
|
|
19
|
+
CIS Control 3.12 - Segment Data Processing and Storage Based on Sensitivity
|
|
20
|
+
AWS Config Rule: data-classification-tagging
|
|
21
|
+
|
|
22
|
+
Ensures data resources have proper classification tags.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
super().__init__(
|
|
27
|
+
rule_name="data-classification-tagging",
|
|
28
|
+
control_id="3.12",
|
|
29
|
+
resource_types=["AWS::RDS::DBInstance", "AWS::DynamoDB::Table"]
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
33
|
+
"""Get data resources and check for classification tags."""
|
|
34
|
+
resources = []
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
if resource_type == "AWS::RDS::DBInstance":
|
|
38
|
+
rds_client = aws_factory.get_client('rds', region)
|
|
39
|
+
response = rds_client.describe_db_instances()
|
|
40
|
+
|
|
41
|
+
for db in response.get('DBInstances', []):
|
|
42
|
+
tags = {tag['Key']: tag['Value'] for tag in db.get('TagList', [])}
|
|
43
|
+
|
|
44
|
+
has_classification = bool(
|
|
45
|
+
tags.get('DataClassification') or
|
|
46
|
+
tags.get('Sensitivity') or
|
|
47
|
+
tags.get('Compliance')
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
resources.append({
|
|
51
|
+
'ResourceId': db.get('DBInstanceIdentifier'),
|
|
52
|
+
'ResourceType': 'AWS::RDS::DBInstance',
|
|
53
|
+
'Tags': tags,
|
|
54
|
+
'HasClassification': has_classification
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
elif resource_type == "AWS::DynamoDB::Table":
|
|
58
|
+
dynamodb_client = aws_factory.get_client('dynamodb', region)
|
|
59
|
+
response = dynamodb_client.list_tables()
|
|
60
|
+
|
|
61
|
+
for table_name in response.get('TableNames', []):
|
|
62
|
+
try:
|
|
63
|
+
table_arn = f"arn:aws:dynamodb:{region}:*:table/{table_name}"
|
|
64
|
+
tags_response = dynamodb_client.list_tags_of_resource(ResourceArn=table_arn)
|
|
65
|
+
tags = {tag['Key']: tag['Value'] for tag in tags_response.get('Tags', [])}
|
|
66
|
+
|
|
67
|
+
has_classification = bool(
|
|
68
|
+
tags.get('DataClassification') or
|
|
69
|
+
tags.get('Sensitivity') or
|
|
70
|
+
tags.get('Compliance')
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
resources.append({
|
|
74
|
+
'ResourceId': table_name,
|
|
75
|
+
'ResourceType': 'AWS::DynamoDB::Table',
|
|
76
|
+
'Tags': tags,
|
|
77
|
+
'HasClassification': has_classification
|
|
78
|
+
})
|
|
79
|
+
except ClientError:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
return resources
|
|
83
|
+
|
|
84
|
+
except ClientError as e:
|
|
85
|
+
logger.error(f"Error retrieving {resource_type} in {region}: {e}")
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
def _evaluate_resource_compliance(
|
|
89
|
+
self,
|
|
90
|
+
resource: Dict[str, Any],
|
|
91
|
+
aws_factory: AWSClientFactory,
|
|
92
|
+
region: str
|
|
93
|
+
) -> ComplianceResult:
|
|
94
|
+
"""Evaluate if resource has classification tags."""
|
|
95
|
+
resource_id = resource.get('ResourceId', 'unknown')
|
|
96
|
+
resource_type = resource.get('ResourceType', 'Unknown')
|
|
97
|
+
has_classification = resource.get('HasClassification', False)
|
|
98
|
+
|
|
99
|
+
if has_classification:
|
|
100
|
+
evaluation_reason = f"Resource {resource_id} has data classification tags"
|
|
101
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
102
|
+
else:
|
|
103
|
+
evaluation_reason = f"Resource {resource_id} missing classification tags (DataClassification, Sensitivity, or Compliance)"
|
|
104
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
105
|
+
|
|
106
|
+
return ComplianceResult(
|
|
107
|
+
resource_id=resource_id,
|
|
108
|
+
resource_type=resource_type,
|
|
109
|
+
compliance_status=compliance_status,
|
|
110
|
+
evaluation_reason=evaluation_reason,
|
|
111
|
+
config_rule_name=self.rule_name,
|
|
112
|
+
region=region
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
116
|
+
"""Get remediation steps for data classification tagging."""
|
|
117
|
+
return [
|
|
118
|
+
"1. Tag RDS instance with classification:",
|
|
119
|
+
" aws rds add-tags-to-resource \\",
|
|
120
|
+
" --resource-name <db-arn> \\",
|
|
121
|
+
" --tags Key=DataClassification,Value=confidential \\",
|
|
122
|
+
" Key=Sensitivity,Value=high \\",
|
|
123
|
+
" Key=Compliance,Value=pci-dss",
|
|
124
|
+
"",
|
|
125
|
+
"2. Tag DynamoDB table:",
|
|
126
|
+
" aws dynamodb tag-resource \\",
|
|
127
|
+
" --resource-arn <table-arn> \\",
|
|
128
|
+
" --tags Key=DataClassification,Value=confidential",
|
|
129
|
+
"",
|
|
130
|
+
"3. Recommended classification levels:",
|
|
131
|
+
" - Public: Non-sensitive data",
|
|
132
|
+
" - Internal: Internal use only",
|
|
133
|
+
" - Confidential: Sensitive business data",
|
|
134
|
+
" - Restricted: Highly sensitive (PII, PHI, PCI)",
|
|
135
|
+
"",
|
|
136
|
+
"Priority: HIGH - Critical for data governance",
|
|
137
|
+
"Effort: Low - Simple tagging operation",
|
|
138
|
+
"",
|
|
139
|
+
"AWS Documentation:",
|
|
140
|
+
"https://docs.aws.amazon.com/general/latest/gr/aws_tagging.html"
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class S3BucketClassificationTagsAssessment(BaseConfigRuleAssessment):
|
|
145
|
+
"""
|
|
146
|
+
CIS Control 3.12 - Segment Data Processing and Storage Based on Sensitivity
|
|
147
|
+
AWS Config Rule: s3-bucket-classification-tags
|
|
148
|
+
|
|
149
|
+
Ensures S3 buckets have proper data classification tags.
|
|
150
|
+
"""
|
|
151
|
+
|
|
152
|
+
def __init__(self):
|
|
153
|
+
super().__init__(
|
|
154
|
+
rule_name="s3-bucket-classification-tags",
|
|
155
|
+
control_id="3.12",
|
|
156
|
+
resource_types=["AWS::S3::Bucket"]
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
160
|
+
"""Get S3 buckets and check for classification tags."""
|
|
161
|
+
if resource_type != "AWS::S3::Bucket":
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
# S3 is global, only process in us-east-1
|
|
165
|
+
if region != 'us-east-1':
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
try:
|
|
169
|
+
s3_client = aws_factory.get_client('s3', region)
|
|
170
|
+
|
|
171
|
+
response = s3_client.list_buckets()
|
|
172
|
+
buckets = []
|
|
173
|
+
|
|
174
|
+
for bucket in response.get('Buckets', []):
|
|
175
|
+
bucket_name = bucket.get('Name')
|
|
176
|
+
|
|
177
|
+
try:
|
|
178
|
+
tags_response = s3_client.get_bucket_tagging(Bucket=bucket_name)
|
|
179
|
+
tags = {tag['Key']: tag['Value'] for tag in tags_response.get('TagSet', [])}
|
|
180
|
+
except ClientError as e:
|
|
181
|
+
if e.response.get('Error', {}).get('Code') == 'NoSuchTagSet':
|
|
182
|
+
tags = {}
|
|
183
|
+
else:
|
|
184
|
+
continue
|
|
185
|
+
|
|
186
|
+
has_classification = bool(
|
|
187
|
+
tags.get('DataClassification') or
|
|
188
|
+
tags.get('Sensitivity') or
|
|
189
|
+
tags.get('Compliance')
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
buckets.append({
|
|
193
|
+
'BucketName': bucket_name,
|
|
194
|
+
'Tags': tags,
|
|
195
|
+
'HasClassification': has_classification
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
return buckets
|
|
199
|
+
|
|
200
|
+
except ClientError as e:
|
|
201
|
+
logger.error(f"Error retrieving S3 buckets: {e}")
|
|
202
|
+
return []
|
|
203
|
+
|
|
204
|
+
def _evaluate_resource_compliance(
|
|
205
|
+
self,
|
|
206
|
+
resource: Dict[str, Any],
|
|
207
|
+
aws_factory: AWSClientFactory,
|
|
208
|
+
region: str
|
|
209
|
+
) -> ComplianceResult:
|
|
210
|
+
"""Evaluate if S3 bucket has classification tags."""
|
|
211
|
+
bucket_name = resource.get('BucketName', 'unknown')
|
|
212
|
+
has_classification = resource.get('HasClassification', False)
|
|
213
|
+
|
|
214
|
+
if has_classification:
|
|
215
|
+
evaluation_reason = f"S3 bucket '{bucket_name}' has data classification tags"
|
|
216
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
217
|
+
else:
|
|
218
|
+
evaluation_reason = f"S3 bucket '{bucket_name}' missing classification tags (DataClassification, Sensitivity, or Compliance)"
|
|
219
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
220
|
+
|
|
221
|
+
return ComplianceResult(
|
|
222
|
+
resource_id=bucket_name,
|
|
223
|
+
resource_type="AWS::S3::Bucket",
|
|
224
|
+
compliance_status=compliance_status,
|
|
225
|
+
evaluation_reason=evaluation_reason,
|
|
226
|
+
config_rule_name=self.rule_name,
|
|
227
|
+
region=region
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
231
|
+
"""Get remediation steps for S3 bucket classification."""
|
|
232
|
+
return [
|
|
233
|
+
"1. Tag S3 bucket with classification:",
|
|
234
|
+
" aws s3api put-bucket-tagging \\",
|
|
235
|
+
" --bucket <bucket-name> \\",
|
|
236
|
+
" --tagging 'TagSet=[{Key=DataClassification,Value=confidential},{Key=Sensitivity,Value=high}]'",
|
|
237
|
+
"",
|
|
238
|
+
"2. Tag multiple buckets (script):",
|
|
239
|
+
" for bucket in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do",
|
|
240
|
+
" aws s3api put-bucket-tagging --bucket $bucket \\",
|
|
241
|
+
" --tagging 'TagSet=[{Key=DataClassification,Value=internal}]'",
|
|
242
|
+
" done",
|
|
243
|
+
"",
|
|
244
|
+
"3. Classification guidelines:",
|
|
245
|
+
" - Public: Publicly accessible data",
|
|
246
|
+
" - Internal: Internal business data",
|
|
247
|
+
" - Confidential: Sensitive business information",
|
|
248
|
+
" - Restricted: Regulated data (PII, PHI, PCI, etc.)",
|
|
249
|
+
"",
|
|
250
|
+
"Priority: HIGH - Essential for data governance",
|
|
251
|
+
"Effort: Low - Simple tagging",
|
|
252
|
+
"",
|
|
253
|
+
"AWS Documentation:",
|
|
254
|
+
"https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-tagging.html"
|
|
255
|
+
]
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CIS Control 3.11 - DynamoDB Table Encryption with KMS
|
|
3
|
+
Ensures DynamoDB tables are encrypted with customer-managed KMS keys for data at rest protection.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
9
|
+
|
|
10
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
11
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
12
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DynamoDBTableEncryptedKMSAssessment(BaseConfigRuleAssessment):
|
|
18
|
+
"""
|
|
19
|
+
CIS Control 3.11 - Encrypt Sensitive Data at Rest
|
|
20
|
+
AWS Config Rule: dynamodb-table-encrypted-kms
|
|
21
|
+
|
|
22
|
+
Ensures DynamoDB tables are encrypted with customer-managed KMS keys.
|
|
23
|
+
While DynamoDB encrypts all tables by default with AWS owned keys,
|
|
24
|
+
using customer-managed KMS keys provides better control and auditability.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__(
|
|
29
|
+
rule_name="dynamodb-table-encrypted-kms",
|
|
30
|
+
control_id="3.11",
|
|
31
|
+
resource_types=["AWS::DynamoDB::Table"]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
35
|
+
"""Get all DynamoDB tables in the region."""
|
|
36
|
+
if resource_type != "AWS::DynamoDB::Table":
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
dynamodb_client = aws_factory.get_client('dynamodb', region)
|
|
41
|
+
|
|
42
|
+
tables = []
|
|
43
|
+
|
|
44
|
+
# List all tables
|
|
45
|
+
paginator = dynamodb_client.get_paginator('list_tables')
|
|
46
|
+
table_names = []
|
|
47
|
+
|
|
48
|
+
for page in paginator.paginate():
|
|
49
|
+
table_names.extend(page.get('TableNames', []))
|
|
50
|
+
|
|
51
|
+
# Get details for each table
|
|
52
|
+
for table_name in table_names:
|
|
53
|
+
try:
|
|
54
|
+
response = dynamodb_client.describe_table(TableName=table_name)
|
|
55
|
+
table = response.get('Table', {})
|
|
56
|
+
|
|
57
|
+
# Get SSE (Server-Side Encryption) description
|
|
58
|
+
sse_description = table.get('SSEDescription', {})
|
|
59
|
+
sse_status = sse_description.get('Status', 'DISABLED')
|
|
60
|
+
sse_type = sse_description.get('SSEType', '')
|
|
61
|
+
kms_key_arn = sse_description.get('KMSMasterKeyArn', '')
|
|
62
|
+
|
|
63
|
+
# Determine encryption type
|
|
64
|
+
# SSEType can be: KMS (customer-managed), DEFAULT (AWS owned), or empty
|
|
65
|
+
if sse_status == 'ENABLED' and sse_type == 'KMS':
|
|
66
|
+
encryption_type = 'KMS'
|
|
67
|
+
encrypted_with_kms = True
|
|
68
|
+
elif sse_status == 'ENABLED':
|
|
69
|
+
# Default encryption with AWS owned keys
|
|
70
|
+
encryption_type = 'DEFAULT'
|
|
71
|
+
encrypted_with_kms = False
|
|
72
|
+
else:
|
|
73
|
+
# Older tables might not have SSE explicitly enabled
|
|
74
|
+
# but DynamoDB encrypts all tables by default with AWS owned keys
|
|
75
|
+
encryption_type = 'DEFAULT'
|
|
76
|
+
encrypted_with_kms = False
|
|
77
|
+
|
|
78
|
+
tables.append({
|
|
79
|
+
'TableName': table_name,
|
|
80
|
+
'TableArn': table.get('TableArn', ''),
|
|
81
|
+
'TableStatus': table.get('TableStatus', ''),
|
|
82
|
+
'SSEStatus': sse_status,
|
|
83
|
+
'SSEType': sse_type,
|
|
84
|
+
'KMSMasterKeyArn': kms_key_arn,
|
|
85
|
+
'EncryptionType': encryption_type,
|
|
86
|
+
'EncryptedWithKMS': encrypted_with_kms,
|
|
87
|
+
'ItemCount': table.get('ItemCount', 0),
|
|
88
|
+
'TableSizeBytes': table.get('TableSizeBytes', 0)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
except ClientError as e:
|
|
92
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
93
|
+
if error_code == 'ResourceNotFoundException':
|
|
94
|
+
logger.debug(f"Table {table_name} not found in {region} - may have been deleted")
|
|
95
|
+
else:
|
|
96
|
+
logger.warning(f"Error describing table {table_name}: {e}")
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
logger.debug(f"Found {len(tables)} DynamoDB tables in {region}")
|
|
100
|
+
return tables
|
|
101
|
+
|
|
102
|
+
except ClientError as e:
|
|
103
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
104
|
+
if error_code == 'AccessDeniedException':
|
|
105
|
+
logger.warning(f"Access denied to list DynamoDB tables in {region}")
|
|
106
|
+
else:
|
|
107
|
+
logger.error(f"Error retrieving DynamoDB tables from {region}: {e}")
|
|
108
|
+
return []
|
|
109
|
+
except Exception as e:
|
|
110
|
+
logger.error(f"Unexpected error retrieving DynamoDB tables from {region}: {e}")
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
def _evaluate_resource_compliance(
|
|
114
|
+
self,
|
|
115
|
+
resource: Dict[str, Any],
|
|
116
|
+
aws_factory: AWSClientFactory,
|
|
117
|
+
region: str
|
|
118
|
+
) -> ComplianceResult:
|
|
119
|
+
"""Evaluate if DynamoDB table is encrypted with customer-managed KMS key."""
|
|
120
|
+
table_name = resource.get('TableName', 'unknown')
|
|
121
|
+
encrypted_with_kms = resource.get('EncryptedWithKMS', False)
|
|
122
|
+
encryption_type = resource.get('EncryptionType', 'UNKNOWN')
|
|
123
|
+
kms_key_arn = resource.get('KMSMasterKeyArn', '')
|
|
124
|
+
table_status = resource.get('TableStatus', '')
|
|
125
|
+
|
|
126
|
+
# Check if table is encrypted with customer-managed KMS key
|
|
127
|
+
is_compliant = encrypted_with_kms
|
|
128
|
+
|
|
129
|
+
if is_compliant:
|
|
130
|
+
evaluation_reason = (
|
|
131
|
+
f"DynamoDB table '{table_name}' is encrypted with customer-managed KMS key: "
|
|
132
|
+
f"{kms_key_arn}"
|
|
133
|
+
)
|
|
134
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
135
|
+
else:
|
|
136
|
+
if encryption_type == 'DEFAULT':
|
|
137
|
+
evaluation_reason = (
|
|
138
|
+
f"DynamoDB table '{table_name}' is encrypted with AWS owned keys (default encryption). "
|
|
139
|
+
f"Consider using customer-managed KMS keys for better control and auditability. "
|
|
140
|
+
f"Status: {table_status}"
|
|
141
|
+
)
|
|
142
|
+
else:
|
|
143
|
+
evaluation_reason = (
|
|
144
|
+
f"DynamoDB table '{table_name}' is not encrypted with customer-managed KMS key. "
|
|
145
|
+
f"Encryption type: {encryption_type}. Status: {table_status}"
|
|
146
|
+
)
|
|
147
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
148
|
+
|
|
149
|
+
return ComplianceResult(
|
|
150
|
+
resource_id=table_name,
|
|
151
|
+
resource_type="AWS::DynamoDB::Table",
|
|
152
|
+
compliance_status=compliance_status,
|
|
153
|
+
evaluation_reason=evaluation_reason,
|
|
154
|
+
config_rule_name=self.rule_name,
|
|
155
|
+
region=region
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
159
|
+
"""Get remediation steps for enabling DynamoDB KMS encryption."""
|
|
160
|
+
return [
|
|
161
|
+
"1. IMPORTANT: You CAN enable KMS encryption on an existing table.",
|
|
162
|
+
" However, this requires updating the table and may cause brief unavailability.",
|
|
163
|
+
"",
|
|
164
|
+
"2. Enable KMS encryption on an existing table using AWS CLI:",
|
|
165
|
+
" aws dynamodb update-table \\",
|
|
166
|
+
" --table-name <table-name> \\",
|
|
167
|
+
" --sse-specification Enabled=true,SSEType=KMS,KMSMasterKeyId=<kms-key-id> \\",
|
|
168
|
+
" --region <region>",
|
|
169
|
+
"",
|
|
170
|
+
"3. Wait for the table update to complete:",
|
|
171
|
+
" aws dynamodb describe-table \\",
|
|
172
|
+
" --table-name <table-name> \\",
|
|
173
|
+
" --query 'Table.TableStatus' \\",
|
|
174
|
+
" --output text \\",
|
|
175
|
+
" --region <region>",
|
|
176
|
+
"",
|
|
177
|
+
" # Status should be 'ACTIVE' when complete",
|
|
178
|
+
"",
|
|
179
|
+
"4. Verify KMS encryption is enabled:",
|
|
180
|
+
" aws dynamodb describe-table \\",
|
|
181
|
+
" --table-name <table-name> \\",
|
|
182
|
+
" --query 'Table.SSEDescription' \\",
|
|
183
|
+
" --region <region>",
|
|
184
|
+
"",
|
|
185
|
+
"5. For NEW tables, enable KMS encryption at creation:",
|
|
186
|
+
" aws dynamodb create-table \\",
|
|
187
|
+
" --table-name <table-name> \\",
|
|
188
|
+
" --attribute-definitions AttributeName=id,AttributeType=S \\",
|
|
189
|
+
" --key-schema AttributeName=id,KeyType=HASH \\",
|
|
190
|
+
" --billing-mode PAY_PER_REQUEST \\",
|
|
191
|
+
" --sse-specification Enabled=true,SSEType=KMS,KMSMasterKeyId=<kms-key-id> \\",
|
|
192
|
+
" --region <region>",
|
|
193
|
+
"",
|
|
194
|
+
"6. Console method for existing tables:",
|
|
195
|
+
" - Navigate to DynamoDB service",
|
|
196
|
+
" - Select the table",
|
|
197
|
+
" - Click 'Additional settings' tab",
|
|
198
|
+
" - Under 'Encryption at rest', click 'Manage encryption'",
|
|
199
|
+
" - Select 'AWS Key Management Service key (KMS)'",
|
|
200
|
+
" - Choose 'Select from your AWS KMS keys' or 'Enter KMS key ARN'",
|
|
201
|
+
" - Select or enter your KMS key",
|
|
202
|
+
" - Click 'Save changes'",
|
|
203
|
+
"",
|
|
204
|
+
"7. Create a customer-managed KMS key if you don't have one:",
|
|
205
|
+
" aws kms create-key \\",
|
|
206
|
+
" --description 'DynamoDB encryption key' \\",
|
|
207
|
+
" --key-policy file://key-policy.json \\",
|
|
208
|
+
" --region <region>",
|
|
209
|
+
"",
|
|
210
|
+
" # Create an alias for easier reference",
|
|
211
|
+
" aws kms create-alias \\",
|
|
212
|
+
" --alias-name alias/dynamodb-encryption \\",
|
|
213
|
+
" --target-key-id <key-id> \\",
|
|
214
|
+
" --region <region>",
|
|
215
|
+
"",
|
|
216
|
+
"8. Example KMS key policy (key-policy.json):",
|
|
217
|
+
" {",
|
|
218
|
+
' "Version": "2012-10-17",',
|
|
219
|
+
' "Statement": [',
|
|
220
|
+
" {",
|
|
221
|
+
' "Sid": "Enable IAM User Permissions",',
|
|
222
|
+
' "Effect": "Allow",',
|
|
223
|
+
' "Principal": {"AWS": "arn:aws:iam::<account-id>:root"},',
|
|
224
|
+
' "Action": "kms:*",',
|
|
225
|
+
' "Resource": "*"',
|
|
226
|
+
" },",
|
|
227
|
+
" {",
|
|
228
|
+
' "Sid": "Allow DynamoDB to use the key",',
|
|
229
|
+
' "Effect": "Allow",',
|
|
230
|
+
' "Principal": {"Service": "dynamodb.amazonaws.com"},',
|
|
231
|
+
' "Action": ["kms:Decrypt", "kms:DescribeKey", "kms:CreateGrant"],',
|
|
232
|
+
' "Resource": "*",',
|
|
233
|
+
' "Condition": {',
|
|
234
|
+
' "StringEquals": {"kms:ViaService": "dynamodb.<region>.amazonaws.com"}',
|
|
235
|
+
" }",
|
|
236
|
+
" }",
|
|
237
|
+
" ]",
|
|
238
|
+
" }",
|
|
239
|
+
"",
|
|
240
|
+
"9. Best practices:",
|
|
241
|
+
" - Use customer-managed KMS keys for all production tables",
|
|
242
|
+
" - Enable automatic key rotation for KMS keys",
|
|
243
|
+
" - Grant appropriate IAM permissions for KMS key usage",
|
|
244
|
+
" - Use separate KMS keys for different environments (dev/staging/prod)",
|
|
245
|
+
" - Document the KMS key used for each table",
|
|
246
|
+
" - Monitor KMS key usage with CloudWatch",
|
|
247
|
+
" - Set up CloudTrail logging for KMS key operations",
|
|
248
|
+
"",
|
|
249
|
+
"10. Important notes:",
|
|
250
|
+
" - Encryption can be changed from AWS owned to KMS without data migration",
|
|
251
|
+
" - Changing encryption type may cause brief table unavailability",
|
|
252
|
+
" - Once KMS encryption is enabled, you can change the KMS key",
|
|
253
|
+
" - You cannot disable encryption (only change the key type)",
|
|
254
|
+
" - Global tables must use the same encryption type across all regions",
|
|
255
|
+
" - Backups inherit encryption from source table",
|
|
256
|
+
" - Point-in-time recovery (PITR) backups use the same encryption",
|
|
257
|
+
" - Performance impact is negligible",
|
|
258
|
+
" - KMS key costs apply (per API call pricing)",
|
|
259
|
+
"",
|
|
260
|
+
"11. Cost considerations:",
|
|
261
|
+
" - AWS owned keys: No additional cost",
|
|
262
|
+
" - Customer-managed KMS keys: $1/month per key + API call costs",
|
|
263
|
+
" - API calls are typically minimal for DynamoDB encryption",
|
|
264
|
+
" - Consider using one KMS key for multiple tables to reduce costs",
|
|
265
|
+
"",
|
|
266
|
+
"12. Verify encryption after update:",
|
|
267
|
+
" # Check SSE status",
|
|
268
|
+
" aws dynamodb describe-table \\",
|
|
269
|
+
" --table-name <table-name> \\",
|
|
270
|
+
" --query 'Table.SSEDescription.{Status:Status,Type:SSEType,Key:KMSMasterKeyArn}' \\",
|
|
271
|
+
" --output table \\",
|
|
272
|
+
" --region <region>",
|
|
273
|
+
"",
|
|
274
|
+
"Priority: MEDIUM - KMS encryption provides better control but AWS owned keys still encrypt data",
|
|
275
|
+
"Effort: Low - Can be enabled with a single API call, minimal downtime",
|
|
276
|
+
"",
|
|
277
|
+
"AWS Documentation:",
|
|
278
|
+
"https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/EncryptionAtRest.html"
|
|
279
|
+
]
|