aws-cis-controls-assessment 1.0.3__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 +11 -0
- aws_cis_assessment/cli/__init__.py +3 -0
- aws_cis_assessment/cli/examples.py +274 -0
- aws_cis_assessment/cli/main.py +1259 -0
- aws_cis_assessment/cli/utils.py +356 -0
- aws_cis_assessment/config/__init__.py +1 -0
- aws_cis_assessment/config/config_loader.py +328 -0
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
- aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
- aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
- aws_cis_assessment/controls/__init__.py +1 -0
- aws_cis_assessment/controls/base_control.py +400 -0
- aws_cis_assessment/controls/ig1/__init__.py +239 -0
- aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
- aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
- aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
- aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
- aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
- aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
- aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
- aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
- aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
- aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
- aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
- aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
- aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
- aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
- aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
- aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
- aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
- aws_cis_assessment/controls/ig2/__init__.py +172 -0
- aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
- aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
- aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
- aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
- aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
- aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
- aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
- aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
- aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
- aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
- aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
- aws_cis_assessment/controls/ig3/__init__.py +49 -0
- aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
- aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
- aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
- aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
- aws_cis_assessment/core/__init__.py +1 -0
- aws_cis_assessment/core/accuracy_validator.py +425 -0
- aws_cis_assessment/core/assessment_engine.py +1266 -0
- aws_cis_assessment/core/audit_trail.py +491 -0
- aws_cis_assessment/core/aws_client_factory.py +313 -0
- aws_cis_assessment/core/error_handler.py +607 -0
- aws_cis_assessment/core/models.py +166 -0
- aws_cis_assessment/core/scoring_engine.py +459 -0
- aws_cis_assessment/reporters/__init__.py +8 -0
- aws_cis_assessment/reporters/base_reporter.py +454 -0
- aws_cis_assessment/reporters/csv_reporter.py +835 -0
- aws_cis_assessment/reporters/html_reporter.py +2162 -0
- aws_cis_assessment/reporters/json_reporter.py +561 -0
- aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
- aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
- aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
- aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
- aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
- aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
- docs/README.md +94 -0
- docs/assessment-logic.md +766 -0
- docs/cli-reference.md +698 -0
- docs/config-rule-mappings.md +393 -0
- docs/developer-guide.md +858 -0
- docs/installation.md +299 -0
- docs/troubleshooting.md +634 -0
- docs/user-guide.md +487 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Control 3.11: Encrypt Sensitive Data at Rest - Advanced encryption assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
import logging
|
|
5
|
+
from botocore.exceptions import ClientError
|
|
6
|
+
|
|
7
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
8
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
9
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SecretsManagerUsingCMKAssessment(BaseConfigRuleAssessment):
|
|
15
|
+
"""Assessment for secretsmanager-using-cmk AWS Config rule."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
super().__init__(
|
|
19
|
+
rule_name="secretsmanager-using-cmk",
|
|
20
|
+
control_id="3.11",
|
|
21
|
+
resource_types=["AWS::SecretsManager::Secret"]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
25
|
+
"""Get Secrets Manager secrets."""
|
|
26
|
+
if resource_type != "AWS::SecretsManager::Secret":
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
secrets_client = aws_factory.get_client('secretsmanager', region)
|
|
31
|
+
|
|
32
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
33
|
+
lambda: secrets_client.list_secrets()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
secrets = []
|
|
37
|
+
for secret in response.get('SecretList', []):
|
|
38
|
+
secrets.append({
|
|
39
|
+
'Name': secret.get('Name'),
|
|
40
|
+
'ARN': secret.get('ARN'),
|
|
41
|
+
'KmsKeyId': secret.get('KmsKeyId')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
return secrets
|
|
45
|
+
|
|
46
|
+
except ClientError as e:
|
|
47
|
+
logger.error(f"Error retrieving Secrets Manager secrets in region {region}: {e}")
|
|
48
|
+
raise
|
|
49
|
+
|
|
50
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
51
|
+
"""Evaluate if secret uses customer-managed KMS key."""
|
|
52
|
+
secret_name = resource.get('Name', 'unknown')
|
|
53
|
+
kms_key_id = resource.get('KmsKeyId')
|
|
54
|
+
|
|
55
|
+
if kms_key_id and not kms_key_id.startswith('alias/aws/secretsmanager'):
|
|
56
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
57
|
+
evaluation_reason = f"Secret {secret_name} uses customer-managed KMS key"
|
|
58
|
+
else:
|
|
59
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
60
|
+
evaluation_reason = f"Secret {secret_name} uses AWS managed key or no encryption"
|
|
61
|
+
|
|
62
|
+
return ComplianceResult(
|
|
63
|
+
resource_id=secret_name,
|
|
64
|
+
resource_type="AWS::SecretsManager::Secret",
|
|
65
|
+
compliance_status=compliance_status,
|
|
66
|
+
evaluation_reason=evaluation_reason,
|
|
67
|
+
config_rule_name=self.rule_name,
|
|
68
|
+
region=region
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SNSEncryptedKMSAssessment(BaseConfigRuleAssessment):
|
|
73
|
+
"""Assessment for sns-encrypted-kms AWS Config rule."""
|
|
74
|
+
|
|
75
|
+
def __init__(self):
|
|
76
|
+
super().__init__(
|
|
77
|
+
rule_name="sns-encrypted-kms",
|
|
78
|
+
control_id="3.11",
|
|
79
|
+
resource_types=["AWS::SNS::Topic"]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
83
|
+
"""Get SNS topics."""
|
|
84
|
+
if resource_type != "AWS::SNS::Topic":
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
sns_client = aws_factory.get_client('sns', region)
|
|
89
|
+
|
|
90
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
91
|
+
lambda: sns_client.list_topics()
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
topics = []
|
|
95
|
+
for topic in response.get('Topics', []):
|
|
96
|
+
topic_arn = topic.get('TopicArn')
|
|
97
|
+
topics.append({
|
|
98
|
+
'TopicArn': topic_arn,
|
|
99
|
+
'TopicName': topic_arn.split(':')[-1] if topic_arn else 'unknown'
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return topics
|
|
103
|
+
|
|
104
|
+
except ClientError as e:
|
|
105
|
+
logger.error(f"Error retrieving SNS topics in region {region}: {e}")
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
109
|
+
"""Evaluate if SNS topic is encrypted with KMS."""
|
|
110
|
+
topic_arn = resource.get('TopicArn', 'unknown')
|
|
111
|
+
topic_name = resource.get('TopicName', 'unknown')
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
sns_client = aws_factory.get_client('sns', region)
|
|
115
|
+
|
|
116
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
117
|
+
lambda: sns_client.get_topic_attributes(TopicArn=topic_arn)
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
attributes = response.get('Attributes', {})
|
|
121
|
+
kms_key_id = attributes.get('KmsMasterKeyId')
|
|
122
|
+
|
|
123
|
+
if kms_key_id:
|
|
124
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
125
|
+
evaluation_reason = f"SNS topic {topic_name} is encrypted with KMS"
|
|
126
|
+
else:
|
|
127
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
128
|
+
evaluation_reason = f"SNS topic {topic_name} is not encrypted with KMS"
|
|
129
|
+
|
|
130
|
+
except ClientError as e:
|
|
131
|
+
compliance_status = ComplianceStatus.ERROR
|
|
132
|
+
evaluation_reason = f"Error checking encryption for SNS topic {topic_name}: {str(e)}"
|
|
133
|
+
|
|
134
|
+
return ComplianceResult(
|
|
135
|
+
resource_id=topic_name,
|
|
136
|
+
resource_type="AWS::SNS::Topic",
|
|
137
|
+
compliance_status=compliance_status,
|
|
138
|
+
evaluation_reason=evaluation_reason,
|
|
139
|
+
config_rule_name=self.rule_name,
|
|
140
|
+
region=region
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class SQSQueueEncryptedKMSAssessment(BaseConfigRuleAssessment):
|
|
145
|
+
"""Assessment for sqs-queue-encrypted-kms AWS Config rule."""
|
|
146
|
+
|
|
147
|
+
def __init__(self):
|
|
148
|
+
super().__init__(
|
|
149
|
+
rule_name="sqs-queue-encrypted-kms",
|
|
150
|
+
control_id="3.11",
|
|
151
|
+
resource_types=["AWS::SQS::Queue"]
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
155
|
+
"""Get SQS queues."""
|
|
156
|
+
if resource_type != "AWS::SQS::Queue":
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
sqs_client = aws_factory.get_client('sqs', region)
|
|
161
|
+
|
|
162
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
163
|
+
lambda: sqs_client.list_queues()
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
queues = []
|
|
167
|
+
for queue_url in response.get('QueueUrls', []):
|
|
168
|
+
queue_name = queue_url.split('/')[-1]
|
|
169
|
+
queues.append({
|
|
170
|
+
'QueueUrl': queue_url,
|
|
171
|
+
'QueueName': queue_name
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
return queues
|
|
175
|
+
|
|
176
|
+
except ClientError as e:
|
|
177
|
+
logger.error(f"Error retrieving SQS queues in region {region}: {e}")
|
|
178
|
+
raise
|
|
179
|
+
|
|
180
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
181
|
+
"""Evaluate if SQS queue is encrypted with KMS."""
|
|
182
|
+
queue_url = resource.get('QueueUrl', 'unknown')
|
|
183
|
+
queue_name = resource.get('QueueName', 'unknown')
|
|
184
|
+
|
|
185
|
+
try:
|
|
186
|
+
sqs_client = aws_factory.get_client('sqs', region)
|
|
187
|
+
|
|
188
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
189
|
+
lambda: sqs_client.get_queue_attributes(
|
|
190
|
+
QueueUrl=queue_url,
|
|
191
|
+
AttributeNames=['KmsMasterKeyId']
|
|
192
|
+
)
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
attributes = response.get('Attributes', {})
|
|
196
|
+
kms_key_id = attributes.get('KmsMasterKeyId')
|
|
197
|
+
|
|
198
|
+
if kms_key_id:
|
|
199
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
200
|
+
evaluation_reason = f"SQS queue {queue_name} is encrypted with KMS"
|
|
201
|
+
else:
|
|
202
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
203
|
+
evaluation_reason = f"SQS queue {queue_name} is not encrypted with KMS"
|
|
204
|
+
|
|
205
|
+
except ClientError as e:
|
|
206
|
+
compliance_status = ComplianceStatus.ERROR
|
|
207
|
+
evaluation_reason = f"Error checking encryption for SQS queue {queue_name}: {str(e)}"
|
|
208
|
+
|
|
209
|
+
return ComplianceResult(
|
|
210
|
+
resource_id=queue_name,
|
|
211
|
+
resource_type="AWS::SQS::Queue",
|
|
212
|
+
compliance_status=compliance_status,
|
|
213
|
+
evaluation_reason=evaluation_reason,
|
|
214
|
+
config_rule_name=self.rule_name,
|
|
215
|
+
region=region
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class KinesisStreamEncryptedAssessment(BaseConfigRuleAssessment):
|
|
220
|
+
"""Assessment for kinesis-stream-encrypted AWS Config rule."""
|
|
221
|
+
|
|
222
|
+
def __init__(self):
|
|
223
|
+
super().__init__(
|
|
224
|
+
rule_name="kinesis-stream-encrypted",
|
|
225
|
+
control_id="3.11",
|
|
226
|
+
resource_types=["AWS::Kinesis::Stream"]
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
230
|
+
"""Get Kinesis streams."""
|
|
231
|
+
if resource_type != "AWS::Kinesis::Stream":
|
|
232
|
+
return []
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
kinesis_client = aws_factory.get_client('kinesis', region)
|
|
236
|
+
|
|
237
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
238
|
+
lambda: kinesis_client.list_streams()
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
streams = []
|
|
242
|
+
for stream_name in response.get('StreamNames', []):
|
|
243
|
+
streams.append({
|
|
244
|
+
'StreamName': stream_name
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
return streams
|
|
248
|
+
|
|
249
|
+
except ClientError as e:
|
|
250
|
+
logger.error(f"Error retrieving Kinesis streams in region {region}: {e}")
|
|
251
|
+
raise
|
|
252
|
+
|
|
253
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
254
|
+
"""Evaluate if Kinesis stream is encrypted."""
|
|
255
|
+
stream_name = resource.get('StreamName', 'unknown')
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
kinesis_client = aws_factory.get_client('kinesis', region)
|
|
259
|
+
|
|
260
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
261
|
+
lambda: kinesis_client.describe_stream(StreamName=stream_name)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
stream_description = response.get('StreamDescription', {})
|
|
265
|
+
encryption_type = stream_description.get('EncryptionType')
|
|
266
|
+
|
|
267
|
+
if encryption_type == 'KMS':
|
|
268
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
269
|
+
evaluation_reason = f"Kinesis stream {stream_name} is encrypted with KMS"
|
|
270
|
+
else:
|
|
271
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
272
|
+
evaluation_reason = f"Kinesis stream {stream_name} is not encrypted"
|
|
273
|
+
|
|
274
|
+
except ClientError as e:
|
|
275
|
+
compliance_status = ComplianceStatus.ERROR
|
|
276
|
+
evaluation_reason = f"Error checking encryption for Kinesis stream {stream_name}: {str(e)}"
|
|
277
|
+
|
|
278
|
+
return ComplianceResult(
|
|
279
|
+
resource_id=stream_name,
|
|
280
|
+
resource_type="AWS::Kinesis::Stream",
|
|
281
|
+
compliance_status=compliance_status,
|
|
282
|
+
evaluation_reason=evaluation_reason,
|
|
283
|
+
config_rule_name=self.rule_name,
|
|
284
|
+
region=region
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class ElasticsearchEncryptedAtRestAssessment(BaseConfigRuleAssessment):
|
|
289
|
+
"""Assessment for elasticsearch-encrypted-at-rest AWS Config rule."""
|
|
290
|
+
|
|
291
|
+
def __init__(self):
|
|
292
|
+
super().__init__(
|
|
293
|
+
rule_name="elasticsearch-encrypted-at-rest",
|
|
294
|
+
control_id="3.11",
|
|
295
|
+
resource_types=["AWS::Elasticsearch::Domain"]
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
299
|
+
"""Get Elasticsearch domains."""
|
|
300
|
+
if resource_type != "AWS::Elasticsearch::Domain":
|
|
301
|
+
return []
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
es_client = aws_factory.get_client('es', region)
|
|
305
|
+
|
|
306
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
307
|
+
lambda: es_client.list_domain_names()
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
domains = []
|
|
311
|
+
for domain in response.get('DomainNames', []):
|
|
312
|
+
domains.append({
|
|
313
|
+
'DomainName': domain.get('DomainName')
|
|
314
|
+
})
|
|
315
|
+
|
|
316
|
+
return domains
|
|
317
|
+
|
|
318
|
+
except ClientError as e:
|
|
319
|
+
logger.error(f"Error retrieving Elasticsearch domains in region {region}: {e}")
|
|
320
|
+
raise
|
|
321
|
+
|
|
322
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
323
|
+
"""Evaluate if Elasticsearch domain has encryption at rest enabled."""
|
|
324
|
+
domain_name = resource.get('DomainName', 'unknown')
|
|
325
|
+
|
|
326
|
+
try:
|
|
327
|
+
es_client = aws_factory.get_client('es', region)
|
|
328
|
+
|
|
329
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
330
|
+
lambda: es_client.describe_elasticsearch_domain(DomainName=domain_name)
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
domain_status = response.get('DomainStatus', {})
|
|
334
|
+
encryption_config = domain_status.get('EncryptionAtRestOptions', {})
|
|
335
|
+
encryption_enabled = encryption_config.get('Enabled', False)
|
|
336
|
+
|
|
337
|
+
if encryption_enabled:
|
|
338
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
339
|
+
evaluation_reason = f"Elasticsearch domain {domain_name} has encryption at rest enabled"
|
|
340
|
+
else:
|
|
341
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
342
|
+
evaluation_reason = f"Elasticsearch domain {domain_name} does not have encryption at rest enabled"
|
|
343
|
+
|
|
344
|
+
except ClientError as e:
|
|
345
|
+
compliance_status = ComplianceStatus.ERROR
|
|
346
|
+
evaluation_reason = f"Error checking encryption for Elasticsearch domain {domain_name}: {str(e)}"
|
|
347
|
+
|
|
348
|
+
return ComplianceResult(
|
|
349
|
+
resource_id=domain_name,
|
|
350
|
+
resource_type="AWS::Elasticsearch::Domain",
|
|
351
|
+
compliance_status=compliance_status,
|
|
352
|
+
evaluation_reason=evaluation_reason,
|
|
353
|
+
config_rule_name=self.rule_name,
|
|
354
|
+
region=region
|
|
355
|
+
)
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""CodeBuild Security Rules - AWS Config rule assessments."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List, Any
|
|
4
|
+
import logging
|
|
5
|
+
from botocore.exceptions import ClientError
|
|
6
|
+
|
|
7
|
+
from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
|
|
8
|
+
from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
|
|
9
|
+
from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CodeBuildProjectEnvironmentPrivilegedCheckAssessment(BaseConfigRuleAssessment):
|
|
15
|
+
"""Assessment for codebuild-project-environment-privileged-check AWS Config rule."""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
super().__init__(
|
|
19
|
+
rule_name="codebuild-project-environment-privileged-check",
|
|
20
|
+
control_id="3.3",
|
|
21
|
+
resource_types=["AWS::CodeBuild::Project"]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
25
|
+
"""Get CodeBuild projects."""
|
|
26
|
+
if resource_type != "AWS::CodeBuild::Project":
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
codebuild_client = aws_factory.get_client('codebuild', region)
|
|
31
|
+
|
|
32
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
33
|
+
lambda: codebuild_client.list_projects()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
projects = []
|
|
37
|
+
for project_name in response.get('projects', []):
|
|
38
|
+
projects.append({
|
|
39
|
+
'name': project_name
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return projects
|
|
43
|
+
|
|
44
|
+
except ClientError as e:
|
|
45
|
+
logger.error(f"Error retrieving CodeBuild projects in region {region}: {e}")
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
49
|
+
"""Evaluate if CodeBuild project environment is not running in privileged mode."""
|
|
50
|
+
project_name = resource.get('name', 'unknown')
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
codebuild_client = aws_factory.get_client('codebuild', region)
|
|
54
|
+
|
|
55
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
56
|
+
lambda: codebuild_client.batch_get_projects(names=[project_name])
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
projects = response.get('projects', [])
|
|
60
|
+
if projects:
|
|
61
|
+
project = projects[0]
|
|
62
|
+
environment = project.get('environment', {})
|
|
63
|
+
privileged_mode = environment.get('privilegedMode', False)
|
|
64
|
+
|
|
65
|
+
if not privileged_mode:
|
|
66
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
67
|
+
evaluation_reason = f"CodeBuild project {project_name} is not running in privileged mode"
|
|
68
|
+
else:
|
|
69
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
70
|
+
evaluation_reason = f"CodeBuild project {project_name} is running in privileged mode"
|
|
71
|
+
else:
|
|
72
|
+
compliance_status = ComplianceStatus.ERROR
|
|
73
|
+
evaluation_reason = f"Could not retrieve details for CodeBuild project {project_name}"
|
|
74
|
+
|
|
75
|
+
except ClientError as e:
|
|
76
|
+
compliance_status = ComplianceStatus.ERROR
|
|
77
|
+
evaluation_reason = f"Error checking privileged mode for project {project_name}: {str(e)}"
|
|
78
|
+
|
|
79
|
+
return ComplianceResult(
|
|
80
|
+
resource_id=project_name,
|
|
81
|
+
resource_type="AWS::CodeBuild::Project",
|
|
82
|
+
compliance_status=compliance_status,
|
|
83
|
+
evaluation_reason=evaluation_reason,
|
|
84
|
+
config_rule_name=self.rule_name,
|
|
85
|
+
region=region
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class CodeBuildProjectEnvVarAWSCredCheckAssessment(BaseConfigRuleAssessment):
|
|
90
|
+
"""Assessment for codebuild-project-envvar-awscred-check AWS Config rule."""
|
|
91
|
+
|
|
92
|
+
def __init__(self):
|
|
93
|
+
super().__init__(
|
|
94
|
+
rule_name="codebuild-project-envvar-awscred-check",
|
|
95
|
+
control_id="3.3",
|
|
96
|
+
resource_types=["AWS::CodeBuild::Project"]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
100
|
+
"""Get CodeBuild projects."""
|
|
101
|
+
if resource_type != "AWS::CodeBuild::Project":
|
|
102
|
+
return []
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
codebuild_client = aws_factory.get_client('codebuild', region)
|
|
106
|
+
|
|
107
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
108
|
+
lambda: codebuild_client.list_projects()
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
projects = []
|
|
112
|
+
for project_name in response.get('projects', []):
|
|
113
|
+
projects.append({
|
|
114
|
+
'name': project_name
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
return projects
|
|
118
|
+
|
|
119
|
+
except ClientError as e:
|
|
120
|
+
logger.error(f"Error retrieving CodeBuild projects in region {region}: {e}")
|
|
121
|
+
raise
|
|
122
|
+
|
|
123
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
124
|
+
"""Evaluate if CodeBuild project does not expose AWS credentials in environment variables."""
|
|
125
|
+
project_name = resource.get('name', 'unknown')
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
codebuild_client = aws_factory.get_client('codebuild', region)
|
|
129
|
+
|
|
130
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
131
|
+
lambda: codebuild_client.batch_get_projects(names=[project_name])
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
projects = response.get('projects', [])
|
|
135
|
+
if projects:
|
|
136
|
+
project = projects[0]
|
|
137
|
+
environment = project.get('environment', {})
|
|
138
|
+
env_vars = environment.get('environmentVariables', [])
|
|
139
|
+
|
|
140
|
+
# Check for AWS credential environment variables
|
|
141
|
+
aws_cred_vars = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN']
|
|
142
|
+
exposed_creds = []
|
|
143
|
+
|
|
144
|
+
for env_var in env_vars:
|
|
145
|
+
var_name = env_var.get('name', '')
|
|
146
|
+
var_type = env_var.get('type', 'PLAINTEXT')
|
|
147
|
+
|
|
148
|
+
if var_name in aws_cred_vars and var_type == 'PLAINTEXT':
|
|
149
|
+
exposed_creds.append(var_name)
|
|
150
|
+
|
|
151
|
+
if not exposed_creds:
|
|
152
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
153
|
+
evaluation_reason = f"CodeBuild project {project_name} does not expose AWS credentials in environment variables"
|
|
154
|
+
else:
|
|
155
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
156
|
+
evaluation_reason = f"CodeBuild project {project_name} exposes AWS credentials: {', '.join(exposed_creds)}"
|
|
157
|
+
else:
|
|
158
|
+
compliance_status = ComplianceStatus.ERROR
|
|
159
|
+
evaluation_reason = f"Could not retrieve details for CodeBuild project {project_name}"
|
|
160
|
+
|
|
161
|
+
except ClientError as e:
|
|
162
|
+
compliance_status = ComplianceStatus.ERROR
|
|
163
|
+
evaluation_reason = f"Error checking environment variables for project {project_name}: {str(e)}"
|
|
164
|
+
|
|
165
|
+
return ComplianceResult(
|
|
166
|
+
resource_id=project_name,
|
|
167
|
+
resource_type="AWS::CodeBuild::Project",
|
|
168
|
+
compliance_status=compliance_status,
|
|
169
|
+
evaluation_reason=evaluation_reason,
|
|
170
|
+
config_rule_name=self.rule_name,
|
|
171
|
+
region=region
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class CodeBuildProjectSourceRepoURLCheckAssessment(BaseConfigRuleAssessment):
|
|
176
|
+
"""Assessment for codebuild-project-source-repo-url-check AWS Config rule."""
|
|
177
|
+
|
|
178
|
+
def __init__(self):
|
|
179
|
+
super().__init__(
|
|
180
|
+
rule_name="codebuild-project-source-repo-url-check",
|
|
181
|
+
control_id="3.3",
|
|
182
|
+
resource_types=["AWS::CodeBuild::Project"]
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
186
|
+
"""Get CodeBuild projects."""
|
|
187
|
+
if resource_type != "AWS::CodeBuild::Project":
|
|
188
|
+
return []
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
codebuild_client = aws_factory.get_client('codebuild', region)
|
|
192
|
+
|
|
193
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
194
|
+
lambda: codebuild_client.list_projects()
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
projects = []
|
|
198
|
+
for project_name in response.get('projects', []):
|
|
199
|
+
projects.append({
|
|
200
|
+
'name': project_name
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
return projects
|
|
204
|
+
|
|
205
|
+
except ClientError as e:
|
|
206
|
+
logger.error(f"Error retrieving CodeBuild projects in region {region}: {e}")
|
|
207
|
+
raise
|
|
208
|
+
|
|
209
|
+
def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
|
|
210
|
+
"""Evaluate if CodeBuild project source repository URL is from approved sources."""
|
|
211
|
+
project_name = resource.get('name', 'unknown')
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
codebuild_client = aws_factory.get_client('codebuild', region)
|
|
215
|
+
|
|
216
|
+
response = aws_factory.aws_api_call_with_retry(
|
|
217
|
+
lambda: codebuild_client.batch_get_projects(names=[project_name])
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
projects = response.get('projects', [])
|
|
221
|
+
if projects:
|
|
222
|
+
project = projects[0]
|
|
223
|
+
source = project.get('source', {})
|
|
224
|
+
source_type = source.get('type', '')
|
|
225
|
+
location = source.get('location', '')
|
|
226
|
+
|
|
227
|
+
# Define approved source types and patterns
|
|
228
|
+
approved_sources = [
|
|
229
|
+
'CODECOMMIT',
|
|
230
|
+
'CODEPIPELINE',
|
|
231
|
+
'S3'
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
# Check for GitHub/Bitbucket with HTTPS
|
|
235
|
+
if source_type in ['GITHUB', 'BITBUCKET', 'GITHUB_ENTERPRISE']:
|
|
236
|
+
if location.startswith('https://'):
|
|
237
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
238
|
+
evaluation_reason = f"CodeBuild project {project_name} uses secure HTTPS source URL"
|
|
239
|
+
else:
|
|
240
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
241
|
+
evaluation_reason = f"CodeBuild project {project_name} uses insecure source URL (not HTTPS)"
|
|
242
|
+
elif source_type in approved_sources:
|
|
243
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
244
|
+
evaluation_reason = f"CodeBuild project {project_name} uses approved source type: {source_type}"
|
|
245
|
+
else:
|
|
246
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
247
|
+
evaluation_reason = f"CodeBuild project {project_name} uses unapproved source type: {source_type}"
|
|
248
|
+
else:
|
|
249
|
+
compliance_status = ComplianceStatus.ERROR
|
|
250
|
+
evaluation_reason = f"Could not retrieve details for CodeBuild project {project_name}"
|
|
251
|
+
|
|
252
|
+
except ClientError as e:
|
|
253
|
+
compliance_status = ComplianceStatus.ERROR
|
|
254
|
+
evaluation_reason = f"Error checking source repository for project {project_name}: {str(e)}"
|
|
255
|
+
|
|
256
|
+
return ComplianceResult(
|
|
257
|
+
resource_id=project_name,
|
|
258
|
+
resource_type="AWS::CodeBuild::Project",
|
|
259
|
+
compliance_status=compliance_status,
|
|
260
|
+
evaluation_reason=evaluation_reason,
|
|
261
|
+
config_rule_name=self.rule_name,
|
|
262
|
+
region=region
|
|
263
|
+
)
|