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.
Files changed (77) hide show
  1. aws_cis_assessment/__init__.py +11 -0
  2. aws_cis_assessment/cli/__init__.py +3 -0
  3. aws_cis_assessment/cli/examples.py +274 -0
  4. aws_cis_assessment/cli/main.py +1259 -0
  5. aws_cis_assessment/cli/utils.py +356 -0
  6. aws_cis_assessment/config/__init__.py +1 -0
  7. aws_cis_assessment/config/config_loader.py +328 -0
  8. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
  9. aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
  10. aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
  11. aws_cis_assessment/controls/__init__.py +1 -0
  12. aws_cis_assessment/controls/base_control.py +400 -0
  13. aws_cis_assessment/controls/ig1/__init__.py +239 -0
  14. aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
  15. aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
  16. aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
  17. aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
  18. aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
  19. aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
  20. aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
  21. aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
  22. aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
  23. aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
  24. aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
  25. aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
  26. aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
  27. aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
  28. aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
  29. aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
  30. aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
  31. aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
  32. aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
  33. aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
  34. aws_cis_assessment/controls/ig2/__init__.py +172 -0
  35. aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
  36. aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
  37. aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
  38. aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
  39. aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
  40. aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
  41. aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
  42. aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
  43. aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
  44. aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
  45. aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
  46. aws_cis_assessment/controls/ig3/__init__.py +49 -0
  47. aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
  48. aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
  49. aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
  50. aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
  51. aws_cis_assessment/core/__init__.py +1 -0
  52. aws_cis_assessment/core/accuracy_validator.py +425 -0
  53. aws_cis_assessment/core/assessment_engine.py +1266 -0
  54. aws_cis_assessment/core/audit_trail.py +491 -0
  55. aws_cis_assessment/core/aws_client_factory.py +313 -0
  56. aws_cis_assessment/core/error_handler.py +607 -0
  57. aws_cis_assessment/core/models.py +166 -0
  58. aws_cis_assessment/core/scoring_engine.py +459 -0
  59. aws_cis_assessment/reporters/__init__.py +8 -0
  60. aws_cis_assessment/reporters/base_reporter.py +454 -0
  61. aws_cis_assessment/reporters/csv_reporter.py +835 -0
  62. aws_cis_assessment/reporters/html_reporter.py +2162 -0
  63. aws_cis_assessment/reporters/json_reporter.py +561 -0
  64. aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
  65. aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
  66. aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
  67. aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
  68. aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
  69. aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
  70. docs/README.md +94 -0
  71. docs/assessment-logic.md +766 -0
  72. docs/cli-reference.md +698 -0
  73. docs/config-rule-mappings.md +393 -0
  74. docs/developer-guide.md +858 -0
  75. docs/installation.md +299 -0
  76. docs/troubleshooting.md +634 -0
  77. 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
+ )