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,523 @@
1
+ """Control 3.14: Log Sensitive Data Access assessments."""
2
+
3
+ from typing import Dict, List, Any
4
+ import logging
5
+ import json
6
+ from botocore.exceptions import ClientError
7
+
8
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
9
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
10
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class APIGatewayExecutionLoggingEnabledAssessment(BaseConfigRuleAssessment):
16
+ """Assessment for api-gw-execution-logging-enabled Config rule."""
17
+
18
+ def __init__(self):
19
+ """Initialize API Gateway execution logging enabled assessment."""
20
+ super().__init__(
21
+ rule_name="api-gw-execution-logging-enabled",
22
+ control_id="3.14",
23
+ resource_types=["AWS::ApiGateway::Stage"]
24
+ )
25
+
26
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
27
+ """Get all API Gateway stages in the region."""
28
+ if resource_type != "AWS::ApiGateway::Stage":
29
+ return []
30
+
31
+ try:
32
+ apigateway_client = aws_factory.get_client('apigateway', region)
33
+
34
+ # First get all REST APIs
35
+ apis_response = aws_factory.aws_api_call_with_retry(
36
+ lambda: apigateway_client.get_rest_apis()
37
+ )
38
+
39
+ stages = []
40
+ for api in apis_response.get('items', []):
41
+ api_id = api.get('id')
42
+ api_name = api.get('name', 'unknown')
43
+
44
+ try:
45
+ # Get stages for this API
46
+ stages_response = aws_factory.aws_api_call_with_retry(
47
+ lambda: apigateway_client.get_stages(restApiId=api_id)
48
+ )
49
+
50
+ for stage in stages_response.get('item', []):
51
+ stages.append({
52
+ 'restApiId': api_id,
53
+ 'apiName': api_name,
54
+ 'stageName': stage.get('stageName'),
55
+ 'deploymentId': stage.get('deploymentId'),
56
+ 'methodSettings': stage.get('methodSettings', {}),
57
+ 'accessLogSettings': stage.get('accessLogSettings', {}),
58
+ 'createdDate': stage.get('createdDate'),
59
+ 'lastUpdatedDate': stage.get('lastUpdatedDate'),
60
+ 'tags': stage.get('tags', {})
61
+ })
62
+
63
+ except ClientError as e:
64
+ logger.warning(f"Could not get stages for API {api_id}: {e}")
65
+ continue
66
+
67
+ logger.debug(f"Found {len(stages)} API Gateway stages in region {region}")
68
+ return stages
69
+
70
+ except ClientError as e:
71
+ logger.error(f"Error retrieving API Gateway stages in region {region}: {e}")
72
+ raise
73
+ except Exception as e:
74
+ logger.error(f"Unexpected error retrieving API Gateway stages in region {region}: {e}")
75
+ raise
76
+
77
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
78
+ """Evaluate if API Gateway stage has execution logging enabled."""
79
+ api_id = resource.get('restApiId', 'unknown')
80
+ stage_name = resource.get('stageName', 'unknown')
81
+ api_name = resource.get('apiName', 'unknown')
82
+ resource_id = f"{api_id}/{stage_name}"
83
+
84
+ method_settings = resource.get('methodSettings', {})
85
+ access_log_settings = resource.get('accessLogSettings', {})
86
+
87
+ # Check if execution logging is enabled
88
+ # Look for logging level in method settings for */* (all methods)
89
+ execution_logging_enabled = False
90
+ logging_level = None
91
+
92
+ # Check method settings for logging configuration
93
+ for method_key, settings in method_settings.items():
94
+ if method_key == '*/*' or method_key.startswith('*/'):
95
+ logging_level = settings.get('loggingLevel')
96
+ if logging_level and logging_level.upper() in ['INFO', 'ERROR']:
97
+ execution_logging_enabled = True
98
+ break
99
+
100
+ # Also check if access logging is configured (alternative form of logging)
101
+ access_logging_enabled = bool(access_log_settings.get('destinationArn'))
102
+
103
+ if execution_logging_enabled or access_logging_enabled:
104
+ compliance_status = ComplianceStatus.COMPLIANT
105
+ if execution_logging_enabled:
106
+ evaluation_reason = f"API Gateway stage {stage_name} in API {api_name} has execution logging enabled (level: {logging_level})"
107
+ else:
108
+ evaluation_reason = f"API Gateway stage {stage_name} in API {api_name} has access logging enabled"
109
+ else:
110
+ compliance_status = ComplianceStatus.NON_COMPLIANT
111
+ evaluation_reason = f"API Gateway stage {stage_name} in API {api_name} does not have execution or access logging enabled"
112
+
113
+ return ComplianceResult(
114
+ resource_id=resource_id,
115
+ resource_type="AWS::ApiGateway::Stage",
116
+ compliance_status=compliance_status,
117
+ evaluation_reason=evaluation_reason,
118
+ config_rule_name=self.rule_name,
119
+ region=region
120
+ )
121
+
122
+ def _get_rule_remediation_steps(self) -> List[str]:
123
+ """Get specific remediation steps for API Gateway execution logging."""
124
+ return [
125
+ "Identify API Gateway stages without execution logging enabled",
126
+ "For each non-compliant stage:",
127
+ " 1. Enable execution logging by configuring method settings",
128
+ " 2. Set logging level to INFO or ERROR for comprehensive logging",
129
+ " 3. Ensure CloudWatch Logs permissions are configured",
130
+ " 4. Consider enabling access logging for additional visibility",
131
+ "Use AWS CLI to enable execution logging:",
132
+ "aws apigateway update-stage --rest-api-id <api-id> --stage-name <stage-name> --patch-ops op=replace,path=/*/logging/loglevel,value=INFO",
133
+ "Enable access logging:",
134
+ "aws apigateway update-stage --rest-api-id <api-id> --stage-name <stage-name> --patch-ops op=replace,path=/accessLogSettings/destinationArn,value=<cloudwatch-log-group-arn>",
135
+ "Create CloudWatch Log Group if needed:",
136
+ "aws logs create-log-group --log-group-name /aws/apigateway/<api-name>",
137
+ "Set up log retention policies to manage storage costs",
138
+ "Monitor logs for security events and API usage patterns",
139
+ "Consider implementing log analysis and alerting for suspicious activity"
140
+ ]
141
+
142
+
143
+ class CloudTrailS3DataEventsEnabledAssessment(BaseConfigRuleAssessment):
144
+ """Assessment for cloudtrail-s3-dataevents-enabled Config rule."""
145
+
146
+ def __init__(self):
147
+ """Initialize CloudTrail S3 data events enabled assessment."""
148
+ super().__init__(
149
+ rule_name="cloudtrail-s3-dataevents-enabled",
150
+ control_id="3.14",
151
+ resource_types=["AWS::CloudTrail::Trail"]
152
+ )
153
+
154
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
155
+ """Get all CloudTrail trails in the region."""
156
+ if resource_type != "AWS::CloudTrail::Trail":
157
+ return []
158
+
159
+ try:
160
+ cloudtrail_client = aws_factory.get_client('cloudtrail', region)
161
+
162
+ response = aws_factory.aws_api_call_with_retry(
163
+ lambda: cloudtrail_client.describe_trails()
164
+ )
165
+
166
+ trails = []
167
+ for trail in response.get('trailList', []):
168
+ trail_arn = trail.get('TrailARN')
169
+ trail_name = trail.get('Name')
170
+
171
+ # Get event selectors for this trail to check for S3 data events
172
+ try:
173
+ selectors_response = aws_factory.aws_api_call_with_retry(
174
+ lambda: cloudtrail_client.get_event_selectors(TrailName=trail_arn)
175
+ )
176
+ event_selectors = selectors_response.get('EventSelectors', [])
177
+ except ClientError as e:
178
+ logger.warning(f"Could not get event selectors for trail {trail_name}: {e}")
179
+ event_selectors = []
180
+
181
+ trails.append({
182
+ 'TrailARN': trail_arn,
183
+ 'Name': trail_name,
184
+ 'S3BucketName': trail.get('S3BucketName'),
185
+ 'S3KeyPrefix': trail.get('S3KeyPrefix'),
186
+ 'IncludeGlobalServiceEvents': trail.get('IncludeGlobalServiceEvents'),
187
+ 'IsMultiRegionTrail': trail.get('IsMultiRegionTrail'),
188
+ 'IsLogging': trail.get('IsLogging'),
189
+ 'EventSelectors': event_selectors,
190
+ 'HomeRegion': trail.get('HomeRegion')
191
+ })
192
+
193
+ logger.debug(f"Found {len(trails)} CloudTrail trails in region {region}")
194
+ return trails
195
+
196
+ except ClientError as e:
197
+ logger.error(f"Error retrieving CloudTrail trails in region {region}: {e}")
198
+ raise
199
+ except Exception as e:
200
+ logger.error(f"Unexpected error retrieving CloudTrail trails in region {region}: {e}")
201
+ raise
202
+
203
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
204
+ """Evaluate if CloudTrail trail has S3 data events enabled."""
205
+ trail_arn = resource.get('TrailARN', 'unknown')
206
+ trail_name = resource.get('Name', 'unknown')
207
+ event_selectors = resource.get('EventSelectors', [])
208
+ is_logging = resource.get('IsLogging', False)
209
+
210
+ # Check if trail is actively logging
211
+ if not is_logging:
212
+ compliance_status = ComplianceStatus.NON_COMPLIANT
213
+ evaluation_reason = f"CloudTrail trail {trail_name} is not actively logging"
214
+ else:
215
+ # Check if any event selector includes S3 data events
216
+ s3_data_events_enabled = False
217
+
218
+ for selector in event_selectors:
219
+ read_write_type = selector.get('ReadWriteType', 'All')
220
+ include_management_events = selector.get('IncludeManagementEvents', True)
221
+ data_resources = selector.get('DataResources', [])
222
+
223
+ # Look for S3 data resources
224
+ for data_resource in data_resources:
225
+ resource_type = data_resource.get('Type', '')
226
+ if resource_type == 'AWS::S3::Object':
227
+ s3_data_events_enabled = True
228
+ break
229
+
230
+ if s3_data_events_enabled:
231
+ break
232
+
233
+ if s3_data_events_enabled:
234
+ compliance_status = ComplianceStatus.COMPLIANT
235
+ evaluation_reason = f"CloudTrail trail {trail_name} has S3 data events logging enabled"
236
+ else:
237
+ compliance_status = ComplianceStatus.NON_COMPLIANT
238
+ evaluation_reason = f"CloudTrail trail {trail_name} does not have S3 data events logging enabled"
239
+
240
+ return ComplianceResult(
241
+ resource_id=trail_arn,
242
+ resource_type="AWS::CloudTrail::Trail",
243
+ compliance_status=compliance_status,
244
+ evaluation_reason=evaluation_reason,
245
+ config_rule_name=self.rule_name,
246
+ region=region
247
+ )
248
+
249
+ def _get_rule_remediation_steps(self) -> List[str]:
250
+ """Get specific remediation steps for CloudTrail S3 data events."""
251
+ return [
252
+ "Identify CloudTrail trails without S3 data events logging",
253
+ "For each non-compliant trail:",
254
+ " 1. Configure event selectors to include S3 data events",
255
+ " 2. Specify S3 buckets or use wildcard for all buckets",
256
+ " 3. Choose appropriate read/write event types",
257
+ " 4. Ensure trail is actively logging",
258
+ "Use AWS CLI to enable S3 data events:",
259
+ "aws cloudtrail put-event-selectors --trail-name <trail-name> --event-selectors '[{\"ReadWriteType\":\"All\",\"IncludeManagementEvents\":true,\"DataResources\":[{\"Type\":\"AWS::S3::Object\",\"Values\":[\"arn:aws:s3:::*/*\"]}]}]'",
260
+ "For specific buckets only:",
261
+ "aws cloudtrail put-event-selectors --trail-name <trail-name> --event-selectors '[{\"ReadWriteType\":\"All\",\"IncludeManagementEvents\":true,\"DataResources\":[{\"Type\":\"AWS::S3::Object\",\"Values\":[\"arn:aws:s3:::sensitive-bucket/*\"]}]}]'",
262
+ "Verify trail is logging:",
263
+ "aws cloudtrail get-trail-status --name <trail-name>",
264
+ "Monitor CloudTrail costs as data events can increase charges significantly",
265
+ "Consider using CloudWatch Insights to analyze S3 access patterns",
266
+ "Set up alerts for unusual S3 data access patterns"
267
+ ]
268
+
269
+
270
+ class MultiRegionCloudTrailEnabledAssessment(BaseConfigRuleAssessment):
271
+ """Assessment for multi-region-cloudtrail-enabled Config rule."""
272
+
273
+ def __init__(self):
274
+ """Initialize multi-region CloudTrail enabled assessment."""
275
+ super().__init__(
276
+ rule_name="multi-region-cloudtrail-enabled",
277
+ control_id="3.14",
278
+ resource_types=["AWS::::Account"]
279
+ )
280
+
281
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
282
+ """Get account-level resource for multi-region CloudTrail assessment."""
283
+ if resource_type != "AWS::::Account":
284
+ return []
285
+
286
+ # Return a single account resource for this assessment
287
+ account_info = aws_factory.get_account_info()
288
+ return [{
289
+ 'accountId': account_info.get('account_id', 'unknown'),
290
+ 'region': region
291
+ }]
292
+
293
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
294
+ """Evaluate if account has multi-region CloudTrail enabled."""
295
+ account_id = resource.get('accountId', 'unknown')
296
+ resource_id = f"account-{account_id}"
297
+
298
+ try:
299
+ cloudtrail_client = aws_factory.get_client('cloudtrail', region)
300
+
301
+ # Get all trails (this will include trails from all regions if multi-region)
302
+ response = aws_factory.aws_api_call_with_retry(
303
+ lambda: cloudtrail_client.describe_trails()
304
+ )
305
+
306
+ trails = response.get('trailList', [])
307
+
308
+ # Look for at least one multi-region trail that is logging
309
+ multi_region_trails = []
310
+ for trail in trails:
311
+ is_multi_region = trail.get('IsMultiRegionTrail', False)
312
+ is_logging = trail.get('IsLogging', False)
313
+ trail_name = trail.get('Name', 'unknown')
314
+
315
+ if is_multi_region and is_logging:
316
+ multi_region_trails.append(trail_name)
317
+
318
+ if multi_region_trails:
319
+ compliance_status = ComplianceStatus.COMPLIANT
320
+ evaluation_reason = f"Account {account_id} has {len(multi_region_trails)} active multi-region CloudTrail trail(s): {', '.join(multi_region_trails)}"
321
+ else:
322
+ # Check if there are any trails at all
323
+ if not trails:
324
+ compliance_status = ComplianceStatus.NON_COMPLIANT
325
+ evaluation_reason = f"Account {account_id} has no CloudTrail trails configured"
326
+ else:
327
+ single_region_trails = [t.get('Name', 'unknown') for t in trails if not t.get('IsMultiRegionTrail', False)]
328
+ if single_region_trails:
329
+ compliance_status = ComplianceStatus.NON_COMPLIANT
330
+ evaluation_reason = f"Account {account_id} has only single-region CloudTrail trails: {', '.join(single_region_trails)}"
331
+ else:
332
+ compliance_status = ComplianceStatus.NON_COMPLIANT
333
+ evaluation_reason = f"Account {account_id} has multi-region trails but they are not actively logging"
334
+
335
+ except ClientError as e:
336
+ error_code = e.response.get('Error', {}).get('Code', '')
337
+ if error_code in ['AccessDenied', 'UnauthorizedOperation']:
338
+ compliance_status = ComplianceStatus.ERROR
339
+ evaluation_reason = f"Insufficient permissions to check CloudTrail configuration for account {account_id}"
340
+ else:
341
+ compliance_status = ComplianceStatus.ERROR
342
+ evaluation_reason = f"Error checking CloudTrail configuration for account {account_id}: {str(e)}"
343
+ except Exception as e:
344
+ compliance_status = ComplianceStatus.ERROR
345
+ evaluation_reason = f"Unexpected error checking CloudTrail configuration for account {account_id}: {str(e)}"
346
+
347
+ return ComplianceResult(
348
+ resource_id=resource_id,
349
+ resource_type="AWS::::Account",
350
+ compliance_status=compliance_status,
351
+ evaluation_reason=evaluation_reason,
352
+ config_rule_name=self.rule_name,
353
+ region=region
354
+ )
355
+
356
+ def _get_rule_remediation_steps(self) -> List[str]:
357
+ """Get specific remediation steps for multi-region CloudTrail."""
358
+ return [
359
+ "Ensure at least one multi-region CloudTrail trail is configured and active",
360
+ "If no CloudTrail exists:",
361
+ " 1. Create a new CloudTrail trail",
362
+ " 2. Enable multi-region logging",
363
+ " 3. Configure S3 bucket for log storage",
364
+ " 4. Enable log file validation",
365
+ "If single-region trails exist:",
366
+ " 1. Modify existing trail to enable multi-region logging",
367
+ " 2. Or create a new multi-region trail",
368
+ "Use AWS CLI to create multi-region trail:",
369
+ "aws cloudtrail create-trail --name <trail-name> --s3-bucket-name <bucket-name> --is-multi-region-trail",
370
+ "Enable logging:",
371
+ "aws cloudtrail start-logging --name <trail-name>",
372
+ "Enable log file validation:",
373
+ "aws cloudtrail update-trail --name <trail-name> --enable-log-file-validation",
374
+ "Configure CloudWatch Logs integration:",
375
+ "aws cloudtrail update-trail --name <trail-name> --cloud-watch-logs-log-group-arn <log-group-arn> --cloud-watch-logs-role-arn <role-arn>",
376
+ "Set up S3 bucket policy to allow CloudTrail access",
377
+ "Configure SNS notifications for log file delivery",
378
+ "Monitor CloudTrail costs and set up billing alerts",
379
+ "Regularly review CloudTrail logs for security events"
380
+ ]
381
+
382
+
383
+ class CloudTrailCloudWatchLogsEnabledAssessment(BaseConfigRuleAssessment):
384
+ """Assessment for cloud-trail-cloud-watch-logs-enabled Config rule."""
385
+
386
+ def __init__(self):
387
+ """Initialize CloudTrail CloudWatch Logs enabled assessment."""
388
+ super().__init__(
389
+ rule_name="cloud-trail-cloud-watch-logs-enabled",
390
+ control_id="3.14",
391
+ resource_types=["AWS::CloudTrail::Trail"]
392
+ )
393
+
394
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
395
+ """Get all CloudTrail trails in the region."""
396
+ if resource_type != "AWS::CloudTrail::Trail":
397
+ return []
398
+
399
+ try:
400
+ cloudtrail_client = aws_factory.get_client('cloudtrail', region)
401
+
402
+ response = aws_factory.aws_api_call_with_retry(
403
+ lambda: cloudtrail_client.describe_trails()
404
+ )
405
+
406
+ trails = []
407
+ for trail in response.get('trailList', []):
408
+ trails.append({
409
+ 'TrailARN': trail.get('TrailARN'),
410
+ 'Name': trail.get('Name'),
411
+ 'S3BucketName': trail.get('S3BucketName'),
412
+ 'S3KeyPrefix': trail.get('S3KeyPrefix'),
413
+ 'IncludeGlobalServiceEvents': trail.get('IncludeGlobalServiceEvents'),
414
+ 'IsMultiRegionTrail': trail.get('IsMultiRegionTrail'),
415
+ 'IsLogging': trail.get('IsLogging'),
416
+ 'CloudWatchLogsLogGroupArn': trail.get('CloudWatchLogsLogGroupArn'),
417
+ 'CloudWatchLogsRoleArn': trail.get('CloudWatchLogsRoleArn'),
418
+ 'HomeRegion': trail.get('HomeRegion')
419
+ })
420
+
421
+ logger.debug(f"Found {len(trails)} CloudTrail trails in region {region}")
422
+ return trails
423
+
424
+ except ClientError as e:
425
+ logger.error(f"Error retrieving CloudTrail trails in region {region}: {e}")
426
+ raise
427
+ except Exception as e:
428
+ logger.error(f"Unexpected error retrieving CloudTrail trails in region {region}: {e}")
429
+ raise
430
+
431
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
432
+ """Evaluate if CloudTrail trail has CloudWatch Logs integration enabled."""
433
+ trail_arn = resource.get('TrailARN', 'unknown')
434
+ trail_name = resource.get('Name', 'unknown')
435
+ cloudwatch_logs_group_arn = resource.get('CloudWatchLogsLogGroupArn')
436
+ cloudwatch_logs_role_arn = resource.get('CloudWatchLogsRoleArn')
437
+ is_logging = resource.get('IsLogging', False)
438
+
439
+ # Check if trail is actively logging
440
+ if not is_logging:
441
+ compliance_status = ComplianceStatus.NON_COMPLIANT
442
+ evaluation_reason = f"CloudTrail trail {trail_name} is not actively logging"
443
+ else:
444
+ # Check if CloudWatch Logs integration is configured
445
+ if cloudwatch_logs_group_arn and cloudwatch_logs_role_arn:
446
+ # Verify the CloudWatch Logs group exists and is accessible
447
+ try:
448
+ logs_client = aws_factory.get_client('logs', region)
449
+
450
+ # Extract log group name from ARN
451
+ # ARN format: arn:aws:logs:region:account:log-group:log-group-name:*
452
+ log_group_name = cloudwatch_logs_group_arn.split(':')[-2] if ':' in cloudwatch_logs_group_arn else cloudwatch_logs_group_arn
453
+
454
+ # Check if log group exists
455
+ response = aws_factory.aws_api_call_with_retry(
456
+ lambda: logs_client.describe_log_groups(
457
+ logGroupNamePrefix=log_group_name,
458
+ limit=1
459
+ )
460
+ )
461
+
462
+ log_groups = response.get('logGroups', [])
463
+ log_group_exists = any(lg.get('logGroupName') == log_group_name for lg in log_groups)
464
+
465
+ if log_group_exists:
466
+ compliance_status = ComplianceStatus.COMPLIANT
467
+ evaluation_reason = f"CloudTrail trail {trail_name} has CloudWatch Logs integration enabled with log group {log_group_name}"
468
+ else:
469
+ compliance_status = ComplianceStatus.NON_COMPLIANT
470
+ evaluation_reason = f"CloudTrail trail {trail_name} has CloudWatch Logs configured but log group {log_group_name} does not exist"
471
+
472
+ except ClientError as e:
473
+ logger.warning(f"Could not verify CloudWatch Logs group for trail {trail_name}: {e}")
474
+ # Assume compliant if we can't verify but configuration exists
475
+ compliance_status = ComplianceStatus.COMPLIANT
476
+ evaluation_reason = f"CloudTrail trail {trail_name} has CloudWatch Logs integration configured (verification limited by permissions)"
477
+
478
+ except Exception as e:
479
+ logger.warning(f"Error verifying CloudWatch Logs group for trail {trail_name}: {e}")
480
+ compliance_status = ComplianceStatus.COMPLIANT
481
+ evaluation_reason = f"CloudTrail trail {trail_name} has CloudWatch Logs integration configured"
482
+
483
+ else:
484
+ compliance_status = ComplianceStatus.NON_COMPLIANT
485
+ if not cloudwatch_logs_group_arn and not cloudwatch_logs_role_arn:
486
+ evaluation_reason = f"CloudTrail trail {trail_name} does not have CloudWatch Logs integration configured"
487
+ elif not cloudwatch_logs_group_arn:
488
+ evaluation_reason = f"CloudTrail trail {trail_name} is missing CloudWatch Logs group ARN"
489
+ else:
490
+ evaluation_reason = f"CloudTrail trail {trail_name} is missing CloudWatch Logs role ARN"
491
+
492
+ return ComplianceResult(
493
+ resource_id=trail_arn,
494
+ resource_type="AWS::CloudTrail::Trail",
495
+ compliance_status=compliance_status,
496
+ evaluation_reason=evaluation_reason,
497
+ config_rule_name=self.rule_name,
498
+ region=region
499
+ )
500
+
501
+ def _get_rule_remediation_steps(self) -> List[str]:
502
+ """Get specific remediation steps for CloudTrail CloudWatch Logs integration."""
503
+ return [
504
+ "Configure CloudWatch Logs integration for CloudTrail trails",
505
+ "For each non-compliant trail:",
506
+ " 1. Create CloudWatch Logs group for CloudTrail",
507
+ " 2. Create IAM role for CloudTrail to write to CloudWatch Logs",
508
+ " 3. Update trail configuration with CloudWatch Logs settings",
509
+ " 4. Verify log delivery is working",
510
+ "Create CloudWatch Logs group:",
511
+ "aws logs create-log-group --log-group-name /aws/cloudtrail/<trail-name>",
512
+ "Create IAM role with trust policy for CloudTrail:",
513
+ "aws iam create-role --role-name CloudTrail_CloudWatchLogs_Role --assume-role-policy-document file://trust-policy.json",
514
+ "Attach policy for CloudWatch Logs access:",
515
+ "aws iam attach-role-policy --role-name CloudTrail_CloudWatchLogs_Role --policy-arn arn:aws:iam::aws:policy/CloudWatchLogsFullAccess",
516
+ "Update CloudTrail with CloudWatch Logs configuration:",
517
+ "aws cloudtrail update-trail --name <trail-name> --cloud-watch-logs-log-group-arn <log-group-arn> --cloud-watch-logs-role-arn <role-arn>",
518
+ "Set up log retention policy to manage costs:",
519
+ "aws logs put-retention-policy --log-group-name /aws/cloudtrail/<trail-name> --retention-in-days 90",
520
+ "Create CloudWatch alarms for security events",
521
+ "Set up log insights queries for security analysis",
522
+ "Monitor CloudWatch Logs costs and usage"
523
+ ]