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,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
|
+
]
|