aws-cis-controls-assessment 1.0.10__py3-none-any.whl → 1.1.1__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.
@@ -0,0 +1,984 @@
1
+ """Control 8.2: Collect Audit Logs - Audit logging assessments for Phase 1.
2
+
3
+ This module implements 7 critical audit logging assessment classes for CIS Control 8
4
+ (Audit Log Management). These assessments evaluate AWS resources for comprehensive
5
+ audit logging compliance across multiple services:
6
+
7
+ 1. Route53QueryLoggingAssessment - Validates DNS query logging for Route 53 hosted zones
8
+ 2. ALBAccessLogsEnabledAssessment - Ensures Application Load Balancers have access logging
9
+ 3. CloudFrontAccessLogsEnabledAssessment - Validates CloudFront distribution access logging
10
+ 4. CloudWatchLogRetentionCheckAssessment - Ensures CloudWatch log groups have appropriate retention
11
+ 5. CloudTrailInsightsEnabledAssessment - Validates CloudTrail Insights for anomaly detection
12
+ 6. ConfigRecordingAllResourcesAssessment - Ensures AWS Config records all resource types
13
+ 7. WAFLoggingEnabledAssessment - Validates WAF web ACL logging configuration
14
+
15
+ These rules address the highest priority compliance gap identified in the CIS Controls
16
+ Gap Analysis and increase the total rule count from 142 to 149.
17
+ """
18
+
19
+ from typing import Dict, List, Any
20
+ import logging
21
+ from botocore.exceptions import ClientError
22
+
23
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
24
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
25
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ # ============================================================================
31
+ # 1. Route 53 Query Logging Assessment
32
+ # ============================================================================
33
+
34
+ class Route53QueryLoggingAssessment(BaseConfigRuleAssessment):
35
+ """Assessment for route53-query-logging-enabled AWS Config rule.
36
+
37
+ Validates that Route 53 hosted zones have query logging enabled to track
38
+ DNS queries for security investigations and compliance.
39
+
40
+ This is a global service assessment that only runs in us-east-1.
41
+ """
42
+
43
+ def __init__(self):
44
+ super().__init__(
45
+ rule_name="route53-query-logging-enabled",
46
+ control_id="8.2",
47
+ resource_types=["AWS::Route53::HostedZone"]
48
+ )
49
+
50
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
51
+ """Get Route 53 hosted zones.
52
+
53
+ Route 53 is a global service, so we only query in us-east-1.
54
+
55
+ Args:
56
+ aws_factory: AWS client factory for API access
57
+ resource_type: AWS resource type (should be AWS::Route53::HostedZone)
58
+ region: AWS region (should be us-east-1 for Route 53)
59
+
60
+ Returns:
61
+ List of hosted zone dictionaries with Id, Name, Config
62
+ """
63
+ if resource_type != "AWS::Route53::HostedZone":
64
+ return []
65
+
66
+ # Route 53 is a global service - only evaluate in us-east-1
67
+ if region != 'us-east-1':
68
+ logger.debug(f"Skipping Route 53 evaluation in {region} - global service evaluated in us-east-1 only")
69
+ return []
70
+
71
+ try:
72
+ route53_client = aws_factory.get_client('route53', region)
73
+
74
+ # List all hosted zones with pagination support
75
+ hosted_zones = []
76
+ marker = None
77
+
78
+ while True:
79
+ if marker:
80
+ response = aws_factory.aws_api_call_with_retry(
81
+ lambda: route53_client.list_hosted_zones(Marker=marker)
82
+ )
83
+ else:
84
+ response = aws_factory.aws_api_call_with_retry(
85
+ lambda: route53_client.list_hosted_zones()
86
+ )
87
+
88
+ hosted_zones.extend(response.get('HostedZones', []))
89
+
90
+ # Check if there are more results
91
+ if response.get('IsTruncated', False):
92
+ marker = response.get('NextMarker')
93
+ else:
94
+ break
95
+
96
+ logger.debug(f"Found {len(hosted_zones)} Route 53 hosted zones")
97
+ return hosted_zones
98
+
99
+ except ClientError as e:
100
+ logger.error(f"Error retrieving Route 53 hosted zones: {e}")
101
+ raise
102
+
103
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
104
+ """Evaluate if Route 53 hosted zone has query logging enabled.
105
+
106
+ Args:
107
+ resource: Hosted zone resource dictionary
108
+ aws_factory: AWS client factory for additional API calls
109
+ region: AWS region
110
+
111
+ Returns:
112
+ ComplianceResult indicating whether query logging is enabled
113
+ """
114
+ hosted_zone_id = resource.get('Id', 'unknown')
115
+ hosted_zone_name = resource.get('Name', 'unknown')
116
+
117
+ # Extract just the zone ID (remove /hostedzone/ prefix if present)
118
+ if hosted_zone_id.startswith('/hostedzone/'):
119
+ zone_id = hosted_zone_id.split('/')[-1]
120
+ else:
121
+ zone_id = hosted_zone_id
122
+
123
+ try:
124
+ route53_client = aws_factory.get_client('route53', region)
125
+
126
+ # Check if query logging is configured for this hosted zone
127
+ response = aws_factory.aws_api_call_with_retry(
128
+ lambda: route53_client.list_query_logging_configs(HostedZoneId=zone_id)
129
+ )
130
+
131
+ query_logging_configs = response.get('QueryLoggingConfigs', [])
132
+
133
+ if query_logging_configs:
134
+ compliance_status = ComplianceStatus.COMPLIANT
135
+ evaluation_reason = f"Route 53 hosted zone {hosted_zone_name} ({zone_id}) has query logging enabled"
136
+ else:
137
+ compliance_status = ComplianceStatus.NON_COMPLIANT
138
+ evaluation_reason = (
139
+ f"Route 53 hosted zone {hosted_zone_name} ({zone_id}) does not have query logging enabled. "
140
+ f"Enable Route 53 query logging for this hosted zone. Create a CloudWatch Logs log group "
141
+ f"and configure query logging using the AWS Console, CLI, or API. Query logging helps track "
142
+ f"DNS queries for security investigations and compliance."
143
+ )
144
+
145
+ except ClientError as e:
146
+ error_code = e.response.get('Error', {}).get('Code', '')
147
+
148
+ if error_code == 'AccessDenied':
149
+ compliance_status = ComplianceStatus.ERROR
150
+ evaluation_reason = f"Insufficient permissions to check query logging for hosted zone {zone_id}: {str(e)}"
151
+ elif error_code == 'NoSuchHostedZone':
152
+ compliance_status = ComplianceStatus.ERROR
153
+ evaluation_reason = f"Hosted zone {zone_id} not found (may have been deleted)"
154
+ else:
155
+ compliance_status = ComplianceStatus.ERROR
156
+ evaluation_reason = f"Error checking query logging for hosted zone {zone_id}: {str(e)}"
157
+
158
+ return ComplianceResult(
159
+ resource_id=zone_id,
160
+ resource_type="AWS::Route53::HostedZone",
161
+ compliance_status=compliance_status,
162
+ evaluation_reason=evaluation_reason,
163
+ config_rule_name=self.rule_name,
164
+ region=region
165
+ )
166
+
167
+
168
+ # ============================================================================
169
+ # 2. Application Load Balancer Access Logs Assessment
170
+ # ============================================================================
171
+
172
+ class ALBAccessLogsEnabledAssessment(BaseConfigRuleAssessment):
173
+ """Assessment for alb-access-logs-enabled AWS Config rule.
174
+
175
+ Ensures Application Load Balancers have access logging enabled to analyze
176
+ traffic patterns and investigate security incidents.
177
+
178
+ This is a regional service assessment that runs in all active regions.
179
+ """
180
+
181
+ def __init__(self):
182
+ super().__init__(
183
+ rule_name="alb-access-logs-enabled",
184
+ control_id="8.2",
185
+ resource_types=["AWS::ElasticLoadBalancingV2::LoadBalancer"]
186
+ )
187
+
188
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
189
+ """Get Application Load Balancers in the specified region.
190
+
191
+ Filters for Type='application' to exclude Network Load Balancers and Gateway Load Balancers.
192
+
193
+ Args:
194
+ aws_factory: AWS client factory for API access
195
+ resource_type: AWS resource type (should be AWS::ElasticLoadBalancingV2::LoadBalancer)
196
+ region: AWS region to query
197
+
198
+ Returns:
199
+ List of ALB dictionaries with LoadBalancerArn, LoadBalancerName, Type
200
+ """
201
+ if resource_type != "AWS::ElasticLoadBalancingV2::LoadBalancer":
202
+ return []
203
+
204
+ try:
205
+ elbv2_client = aws_factory.get_client('elbv2', region)
206
+
207
+ # List all load balancers with pagination support
208
+ load_balancers = []
209
+ marker = None
210
+
211
+ while True:
212
+ if marker:
213
+ response = aws_factory.aws_api_call_with_retry(
214
+ lambda: elbv2_client.describe_load_balancers(Marker=marker)
215
+ )
216
+ else:
217
+ response = aws_factory.aws_api_call_with_retry(
218
+ lambda: elbv2_client.describe_load_balancers()
219
+ )
220
+
221
+ # Filter for Application Load Balancers only (exclude NLB and Gateway LB)
222
+ albs = [lb for lb in response.get('LoadBalancers', []) if lb.get('Type') == 'application']
223
+ load_balancers.extend(albs)
224
+
225
+ # Check if there are more results
226
+ if 'NextMarker' in response:
227
+ marker = response['NextMarker']
228
+ else:
229
+ break
230
+
231
+ logger.debug(f"Found {len(load_balancers)} Application Load Balancers in {region}")
232
+ return load_balancers
233
+
234
+ except ClientError as e:
235
+ logger.error(f"Error retrieving Application Load Balancers in {region}: {e}")
236
+ raise
237
+
238
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
239
+ """Evaluate if Application Load Balancer has access logging enabled.
240
+
241
+ Args:
242
+ resource: Load balancer resource dictionary
243
+ aws_factory: AWS client factory for additional API calls
244
+ region: AWS region
245
+
246
+ Returns:
247
+ ComplianceResult indicating whether access logging is enabled
248
+ """
249
+ lb_arn = resource.get('LoadBalancerArn', 'unknown')
250
+ lb_name = resource.get('LoadBalancerName', 'unknown')
251
+
252
+ try:
253
+ elbv2_client = aws_factory.get_client('elbv2', region)
254
+
255
+ # Get load balancer attributes
256
+ response = aws_factory.aws_api_call_with_retry(
257
+ lambda: elbv2_client.describe_load_balancer_attributes(LoadBalancerArn=lb_arn)
258
+ )
259
+
260
+ attributes = response.get('Attributes', [])
261
+
262
+ # Find the access_logs.s3.enabled attribute
263
+ access_logs_enabled = False
264
+ for attr in attributes:
265
+ if attr.get('Key') == 'access_logs.s3.enabled':
266
+ access_logs_enabled = attr.get('Value', 'false').lower() == 'true'
267
+ break
268
+
269
+ if access_logs_enabled:
270
+ compliance_status = ComplianceStatus.COMPLIANT
271
+ evaluation_reason = f"Application Load Balancer {lb_name} has access logging enabled"
272
+ else:
273
+ compliance_status = ComplianceStatus.NON_COMPLIANT
274
+ evaluation_reason = (
275
+ f"Application Load Balancer {lb_name} does not have access logging enabled. "
276
+ f"Enable access logging for this Application Load Balancer. Configure an S3 bucket "
277
+ f"to store access logs using the AWS Console, CLI, or API. Access logs help analyze "
278
+ f"traffic patterns and investigate security incidents."
279
+ )
280
+
281
+ except ClientError as e:
282
+ error_code = e.response.get('Error', {}).get('Code', '')
283
+
284
+ if error_code == 'AccessDenied':
285
+ compliance_status = ComplianceStatus.ERROR
286
+ evaluation_reason = f"Insufficient permissions to check access logging for ALB {lb_name}: {str(e)}"
287
+ elif error_code == 'LoadBalancerNotFound':
288
+ compliance_status = ComplianceStatus.ERROR
289
+ evaluation_reason = f"Load balancer {lb_name} not found (may have been deleted)"
290
+ else:
291
+ compliance_status = ComplianceStatus.ERROR
292
+ evaluation_reason = f"Error checking access logging for ALB {lb_name}: {str(e)}"
293
+
294
+ return ComplianceResult(
295
+ resource_id=lb_arn,
296
+ resource_type="AWS::ElasticLoadBalancingV2::LoadBalancer",
297
+ compliance_status=compliance_status,
298
+ evaluation_reason=evaluation_reason,
299
+ config_rule_name=self.rule_name,
300
+ region=region
301
+ )
302
+
303
+
304
+ # ============================================================================
305
+ # 3. CloudFront Access Logs Assessment
306
+ # ============================================================================
307
+
308
+ class CloudFrontAccessLogsEnabledAssessment(BaseConfigRuleAssessment):
309
+ """Assessment for cloudfront-access-logs-enabled AWS Config rule.
310
+
311
+ Validates that CloudFront distributions have access logging enabled to track
312
+ content delivery requests and detect anomalous access patterns.
313
+
314
+ This is a global service assessment that only runs in us-east-1.
315
+ """
316
+
317
+ def __init__(self):
318
+ super().__init__(
319
+ rule_name="cloudfront-access-logs-enabled",
320
+ control_id="8.2",
321
+ resource_types=["AWS::CloudFront::Distribution"]
322
+ )
323
+
324
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
325
+ """Get CloudFront distributions.
326
+
327
+ CloudFront is a global service, so we only query in us-east-1.
328
+
329
+ Args:
330
+ aws_factory: AWS client factory for API access
331
+ resource_type: AWS resource type (should be AWS::CloudFront::Distribution)
332
+ region: AWS region (should be us-east-1 for CloudFront)
333
+
334
+ Returns:
335
+ List of distribution dictionaries with Id, ARN, Status, DomainName
336
+ """
337
+ if resource_type != "AWS::CloudFront::Distribution":
338
+ return []
339
+
340
+ # CloudFront is a global service - only evaluate in us-east-1
341
+ if region != 'us-east-1':
342
+ logger.debug(f"Skipping CloudFront evaluation in {region} - global service evaluated in us-east-1 only")
343
+ return []
344
+
345
+ try:
346
+ cloudfront_client = aws_factory.get_client('cloudfront', region)
347
+
348
+ # List all distributions with pagination support
349
+ distributions = []
350
+ marker = None
351
+
352
+ while True:
353
+ if marker:
354
+ response = aws_factory.aws_api_call_with_retry(
355
+ lambda: cloudfront_client.list_distributions(Marker=marker)
356
+ )
357
+ else:
358
+ response = aws_factory.aws_api_call_with_retry(
359
+ lambda: cloudfront_client.list_distributions()
360
+ )
361
+
362
+ distribution_list = response.get('DistributionList', {})
363
+ items = distribution_list.get('Items', [])
364
+ distributions.extend(items)
365
+
366
+ # Check if there are more results
367
+ if distribution_list.get('IsTruncated', False):
368
+ marker = distribution_list.get('NextMarker')
369
+ else:
370
+ break
371
+
372
+ logger.debug(f"Found {len(distributions)} CloudFront distributions")
373
+ return distributions
374
+
375
+ except ClientError as e:
376
+ logger.error(f"Error retrieving CloudFront distributions: {e}")
377
+ raise
378
+
379
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
380
+ """Evaluate if CloudFront distribution has access logging enabled.
381
+
382
+ Args:
383
+ resource: Distribution resource dictionary
384
+ aws_factory: AWS client factory for additional API calls
385
+ region: AWS region
386
+
387
+ Returns:
388
+ ComplianceResult indicating whether access logging is enabled
389
+ """
390
+ distribution_id = resource.get('Id', 'unknown')
391
+ distribution_domain = resource.get('DomainName', 'unknown')
392
+
393
+ try:
394
+ cloudfront_client = aws_factory.get_client('cloudfront', region)
395
+
396
+ # Get distribution configuration
397
+ response = aws_factory.aws_api_call_with_retry(
398
+ lambda: cloudfront_client.get_distribution_config(Id=distribution_id)
399
+ )
400
+
401
+ distribution_config = response.get('DistributionConfig', {})
402
+ logging_config = distribution_config.get('Logging', {})
403
+
404
+ # Check if logging is enabled and bucket is configured
405
+ logging_enabled = logging_config.get('Enabled', False)
406
+ logging_bucket = logging_config.get('Bucket', '')
407
+
408
+ if logging_enabled and logging_bucket:
409
+ compliance_status = ComplianceStatus.COMPLIANT
410
+ evaluation_reason = f"CloudFront distribution {distribution_id} ({distribution_domain}) has access logging enabled"
411
+ else:
412
+ compliance_status = ComplianceStatus.NON_COMPLIANT
413
+ if not logging_enabled:
414
+ evaluation_reason = (
415
+ f"CloudFront distribution {distribution_id} ({distribution_domain}) does not have access logging enabled. "
416
+ f"Enable access logging for this CloudFront distribution. Configure an S3 bucket to store access logs "
417
+ f"using the AWS Console, CLI, or API. Access logs help track content delivery requests and detect "
418
+ f"anomalous access patterns."
419
+ )
420
+ else:
421
+ evaluation_reason = (
422
+ f"CloudFront distribution {distribution_id} ({distribution_domain}) has logging enabled but no S3 bucket configured. "
423
+ f"Configure an S3 bucket to store access logs using the AWS Console, CLI, or API."
424
+ )
425
+
426
+ except ClientError as e:
427
+ error_code = e.response.get('Error', {}).get('Code', '')
428
+
429
+ if error_code == 'AccessDenied':
430
+ compliance_status = ComplianceStatus.ERROR
431
+ evaluation_reason = f"Insufficient permissions to check access logging for CloudFront distribution {distribution_id}: {str(e)}"
432
+ elif error_code == 'NoSuchDistribution':
433
+ compliance_status = ComplianceStatus.ERROR
434
+ evaluation_reason = f"CloudFront distribution {distribution_id} not found (may have been deleted)"
435
+ else:
436
+ compliance_status = ComplianceStatus.ERROR
437
+ evaluation_reason = f"Error checking access logging for CloudFront distribution {distribution_id}: {str(e)}"
438
+
439
+ return ComplianceResult(
440
+ resource_id=distribution_id,
441
+ resource_type="AWS::CloudFront::Distribution",
442
+ compliance_status=compliance_status,
443
+ evaluation_reason=evaluation_reason,
444
+ config_rule_name=self.rule_name,
445
+ region=region
446
+ )
447
+
448
+
449
+ # ============================================================================
450
+ # 4. CloudWatch Log Retention Assessment
451
+ # ============================================================================
452
+
453
+ class CloudWatchLogRetentionCheckAssessment(BaseConfigRuleAssessment):
454
+ """Assessment for cloudwatch-log-retention-check AWS Config rule.
455
+
456
+ Ensures CloudWatch log groups have appropriate retention periods so that logs
457
+ are retained long enough for compliance and investigation purposes.
458
+
459
+ This is a regional service assessment that runs in all active regions.
460
+ """
461
+
462
+ def __init__(self):
463
+ super().__init__(
464
+ rule_name="cloudwatch-log-retention-check",
465
+ control_id="8.2",
466
+ resource_types=["AWS::Logs::LogGroup"],
467
+ parameters={'minimumRetentionDays': 90}
468
+ )
469
+
470
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
471
+ """Get CloudWatch log groups in the specified region.
472
+
473
+ Uses pagination to handle large numbers of log groups.
474
+
475
+ Args:
476
+ aws_factory: AWS client factory for API access
477
+ resource_type: AWS resource type (should be AWS::Logs::LogGroup)
478
+ region: AWS region to query
479
+
480
+ Returns:
481
+ List of log group dictionaries with logGroupName, retentionInDays, creationTime, storedBytes
482
+ """
483
+ if resource_type != "AWS::Logs::LogGroup":
484
+ return []
485
+
486
+ try:
487
+ logs_client = aws_factory.get_client('logs', region)
488
+
489
+ # Use paginator for describe_log_groups to handle large result sets
490
+ paginator = logs_client.get_paginator('describe_log_groups')
491
+
492
+ log_groups = []
493
+ for page in paginator.paginate():
494
+ log_groups.extend(page.get('logGroups', []))
495
+
496
+ logger.debug(f"Found {len(log_groups)} CloudWatch log groups in {region}")
497
+ return log_groups
498
+
499
+ except ClientError as e:
500
+ logger.error(f"Error retrieving CloudWatch log groups in {region}: {e}")
501
+ raise
502
+
503
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
504
+ """Evaluate if CloudWatch log group has appropriate retention period.
505
+
506
+ Args:
507
+ resource: Log group resource dictionary
508
+ aws_factory: AWS client factory for additional API calls
509
+ region: AWS region
510
+
511
+ Returns:
512
+ ComplianceResult indicating whether retention period is appropriate
513
+ """
514
+ log_group_name = resource.get('logGroupName', 'unknown')
515
+ retention_in_days = resource.get('retentionInDays')
516
+
517
+ # Get minimum retention from parameters (default 90 days)
518
+ minimum_retention = self.parameters.get('minimumRetentionDays', 90)
519
+
520
+ try:
521
+ if retention_in_days is None:
522
+ # Indefinite retention (None) is considered non-compliant
523
+ compliance_status = ComplianceStatus.NON_COMPLIANT
524
+ evaluation_reason = (
525
+ f"CloudWatch log group {log_group_name} has indefinite retention (no retention period set). "
526
+ f"Set a retention period of at least {minimum_retention} days for this log group using the AWS Console, "
527
+ f"CLI, or API. Proper retention ensures logs are available for compliance and investigation while "
528
+ f"managing storage costs."
529
+ )
530
+ elif retention_in_days < minimum_retention:
531
+ # Retention less than minimum is non-compliant
532
+ compliance_status = ComplianceStatus.NON_COMPLIANT
533
+ evaluation_reason = (
534
+ f"CloudWatch log group {log_group_name} has retention period of {retention_in_days} days, "
535
+ f"which is less than the required {minimum_retention} days. "
536
+ f"Increase the retention period to at least {minimum_retention} days using the AWS Console, CLI, or API."
537
+ )
538
+ else:
539
+ # Retention meets or exceeds minimum
540
+ compliance_status = ComplianceStatus.COMPLIANT
541
+ evaluation_reason = f"CloudWatch log group {log_group_name} has retention period of {retention_in_days} days (>= {minimum_retention} days)"
542
+
543
+ except Exception as e:
544
+ compliance_status = ComplianceStatus.ERROR
545
+ evaluation_reason = f"Error evaluating retention for log group {log_group_name}: {str(e)}"
546
+
547
+ return ComplianceResult(
548
+ resource_id=log_group_name,
549
+ resource_type="AWS::Logs::LogGroup",
550
+ compliance_status=compliance_status,
551
+ evaluation_reason=evaluation_reason,
552
+ config_rule_name=self.rule_name,
553
+ region=region
554
+ )
555
+
556
+
557
+ # ============================================================================
558
+ # 5. CloudTrail Insights Assessment
559
+ # ============================================================================
560
+
561
+ class CloudTrailInsightsEnabledAssessment(BaseConfigRuleAssessment):
562
+ """Assessment for cloudtrail-insights-enabled AWS Config rule.
563
+
564
+ Validates that CloudTrail Insights is enabled for anomaly detection so that
565
+ anomalous API activity can be automatically detected.
566
+
567
+ This is an account-level check that verifies at least one trail has Insights enabled.
568
+ """
569
+
570
+ def __init__(self):
571
+ super().__init__(
572
+ rule_name="cloudtrail-insights-enabled",
573
+ control_id="8.2",
574
+ resource_types=["AWS::CloudTrail::Trail"]
575
+ )
576
+
577
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
578
+ """Get CloudTrail trails in the specified region.
579
+
580
+ Args:
581
+ aws_factory: AWS client factory for API access
582
+ resource_type: AWS resource type (should be AWS::CloudTrail::Trail)
583
+ region: AWS region to query
584
+
585
+ Returns:
586
+ List of trail dictionaries with Name, TrailARN, IsMultiRegionTrail, IsLogging
587
+ """
588
+ if resource_type != "AWS::CloudTrail::Trail":
589
+ return []
590
+
591
+ try:
592
+ cloudtrail_client = aws_factory.get_client('cloudtrail', region)
593
+
594
+ # Get all trails
595
+ response = aws_factory.aws_api_call_with_retry(
596
+ lambda: cloudtrail_client.describe_trails()
597
+ )
598
+
599
+ trails = response.get('trailList', [])
600
+
601
+ # Get status for each trail
602
+ trails_with_status = []
603
+ for trail in trails:
604
+ trail_arn = trail.get('TrailARN', '')
605
+ try:
606
+ status_response = aws_factory.aws_api_call_with_retry(
607
+ lambda: cloudtrail_client.get_trail_status(Name=trail_arn)
608
+ )
609
+ trail['IsLogging'] = status_response.get('IsLogging', False)
610
+ except ClientError as e:
611
+ logger.warning(f"Error getting status for trail {trail_arn}: {e}")
612
+ trail['IsLogging'] = False
613
+
614
+ trails_with_status.append(trail)
615
+
616
+ logger.debug(f"Found {len(trails_with_status)} CloudTrail trails in {region}")
617
+ return trails_with_status
618
+
619
+ except ClientError as e:
620
+ logger.error(f"Error retrieving CloudTrail trails in {region}: {e}")
621
+ raise
622
+
623
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
624
+ """Evaluate if CloudTrail trail has Insights enabled.
625
+
626
+ This is an account-level check - we check if at least one active trail has Insights enabled.
627
+
628
+ Args:
629
+ resource: Trail resource dictionary
630
+ aws_factory: AWS client factory for additional API calls
631
+ region: AWS region
632
+
633
+ Returns:
634
+ ComplianceResult indicating whether Insights is enabled
635
+ """
636
+ trail_name = resource.get('Name', 'unknown')
637
+ trail_arn = resource.get('TrailARN', 'unknown')
638
+ is_logging = resource.get('IsLogging', False)
639
+
640
+ try:
641
+ cloudtrail_client = aws_factory.get_client('cloudtrail', region)
642
+
643
+ # Only check Insights for active trails
644
+ if not is_logging:
645
+ compliance_status = ComplianceStatus.NON_COMPLIANT
646
+ evaluation_reason = (
647
+ f"CloudTrail trail {trail_name} is not actively logging. "
648
+ f"Enable logging for this trail and configure CloudTrail Insights for anomaly detection."
649
+ )
650
+ else:
651
+ # Get insight selectors for the trail
652
+ try:
653
+ response = aws_factory.aws_api_call_with_retry(
654
+ lambda: cloudtrail_client.get_insight_selectors(TrailName=trail_arn)
655
+ )
656
+
657
+ insight_selectors = response.get('InsightSelectors', [])
658
+
659
+ if insight_selectors:
660
+ compliance_status = ComplianceStatus.COMPLIANT
661
+ evaluation_reason = f"CloudTrail trail {trail_name} has Insights enabled for anomaly detection"
662
+ else:
663
+ compliance_status = ComplianceStatus.NON_COMPLIANT
664
+ evaluation_reason = (
665
+ f"CloudTrail trail {trail_name} does not have Insights enabled. "
666
+ f"Enable CloudTrail Insights for this trail to detect anomalous API activity. "
667
+ f"Configure Insights using the AWS Console, CLI, or API."
668
+ )
669
+
670
+ except ClientError as e:
671
+ error_code = e.response.get('Error', {}).get('Code', '')
672
+ if error_code == 'InsightNotEnabledException':
673
+ compliance_status = ComplianceStatus.NON_COMPLIANT
674
+ evaluation_reason = (
675
+ f"CloudTrail trail {trail_name} does not have Insights enabled. "
676
+ f"Enable CloudTrail Insights for this trail to detect anomalous API activity."
677
+ )
678
+ else:
679
+ raise
680
+
681
+ except ClientError as e:
682
+ error_code = e.response.get('Error', {}).get('Code', '')
683
+
684
+ if error_code == 'AccessDenied':
685
+ compliance_status = ComplianceStatus.ERROR
686
+ evaluation_reason = f"Insufficient permissions to check Insights for CloudTrail trail {trail_name}: {str(e)}"
687
+ elif error_code == 'TrailNotFoundException':
688
+ compliance_status = ComplianceStatus.ERROR
689
+ evaluation_reason = f"CloudTrail trail {trail_name} not found (may have been deleted)"
690
+ else:
691
+ compliance_status = ComplianceStatus.ERROR
692
+ evaluation_reason = f"Error checking Insights for CloudTrail trail {trail_name}: {str(e)}"
693
+
694
+ return ComplianceResult(
695
+ resource_id=trail_arn,
696
+ resource_type="AWS::CloudTrail::Trail",
697
+ compliance_status=compliance_status,
698
+ evaluation_reason=evaluation_reason,
699
+ config_rule_name=self.rule_name,
700
+ region=region
701
+ )
702
+
703
+
704
+ # ============================================================================
705
+ # 6. AWS Config Recording Assessment
706
+ # ============================================================================
707
+
708
+ class ConfigRecordingAllResourcesAssessment(BaseConfigRuleAssessment):
709
+ """Assessment for config-recording-all-resources AWS Config rule.
710
+
711
+ Ensures AWS Config is recording all resource types so that configuration
712
+ changes are tracked for compliance and security analysis.
713
+
714
+ This is a regional service assessment that runs in all active regions.
715
+ """
716
+
717
+ def __init__(self):
718
+ super().__init__(
719
+ rule_name="config-recording-all-resources",
720
+ control_id="8.2",
721
+ resource_types=["AWS::Config::ConfigurationRecorder"]
722
+ )
723
+
724
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
725
+ """Get AWS Config configuration recorders in the specified region.
726
+
727
+ Args:
728
+ aws_factory: AWS client factory for API access
729
+ resource_type: AWS resource type (should be AWS::Config::ConfigurationRecorder)
730
+ region: AWS region to query
731
+
732
+ Returns:
733
+ List of configuration recorder dictionaries with name, roleARN, recordingGroup, recording status
734
+ """
735
+ if resource_type != "AWS::Config::ConfigurationRecorder":
736
+ return []
737
+
738
+ try:
739
+ config_client = aws_factory.get_client('config', region)
740
+
741
+ # Get all configuration recorders
742
+ response = aws_factory.aws_api_call_with_retry(
743
+ lambda: config_client.describe_configuration_recorders()
744
+ )
745
+
746
+ recorders = response.get('ConfigurationRecorders', [])
747
+
748
+ # Get status for each recorder
749
+ recorders_with_status = []
750
+ for recorder in recorders:
751
+ recorder_name = recorder.get('name', '')
752
+ try:
753
+ status_response = aws_factory.aws_api_call_with_retry(
754
+ lambda: config_client.describe_configuration_recorder_status(
755
+ ConfigurationRecorderNames=[recorder_name]
756
+ )
757
+ )
758
+
759
+ statuses = status_response.get('ConfigurationRecordersStatus', [])
760
+ if statuses:
761
+ recorder['recording'] = statuses[0].get('recording', False)
762
+ else:
763
+ recorder['recording'] = False
764
+
765
+ except ClientError as e:
766
+ logger.warning(f"Error getting status for recorder {recorder_name}: {e}")
767
+ recorder['recording'] = False
768
+
769
+ recorders_with_status.append(recorder)
770
+
771
+ logger.debug(f"Found {len(recorders_with_status)} AWS Config recorders in {region}")
772
+ return recorders_with_status
773
+
774
+ except ClientError as e:
775
+ logger.error(f"Error retrieving AWS Config recorders in {region}: {e}")
776
+ raise
777
+
778
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
779
+ """Evaluate if AWS Config recorder is recording all resource types.
780
+
781
+ Args:
782
+ resource: Configuration recorder resource dictionary
783
+ aws_factory: AWS client factory for additional API calls
784
+ region: AWS region
785
+
786
+ Returns:
787
+ ComplianceResult indicating whether recorder is recording all resources
788
+ """
789
+ recorder_name = resource.get('name', 'unknown')
790
+ recording_group = resource.get('recordingGroup', {})
791
+ all_supported = recording_group.get('allSupported', False)
792
+ is_recording = resource.get('recording', False)
793
+
794
+ try:
795
+ if all_supported and is_recording:
796
+ compliance_status = ComplianceStatus.COMPLIANT
797
+ evaluation_reason = f"AWS Config recorder {recorder_name} is recording all resource types and is active"
798
+ elif not all_supported and not is_recording:
799
+ compliance_status = ComplianceStatus.NON_COMPLIANT
800
+ evaluation_reason = (
801
+ f"AWS Config recorder {recorder_name} is not recording all resource types and is not active. "
802
+ f"Configure AWS Config to record all resource types (set allSupported=true) and start the recorder "
803
+ f"using the AWS Console, CLI, or API. Recording all resources ensures comprehensive configuration tracking."
804
+ )
805
+ elif not all_supported:
806
+ compliance_status = ComplianceStatus.NON_COMPLIANT
807
+ evaluation_reason = (
808
+ f"AWS Config recorder {recorder_name} is not recording all resource types (allSupported=false). "
809
+ f"Configure AWS Config to record all resource types using the AWS Console, CLI, or API."
810
+ )
811
+ else: # not is_recording
812
+ compliance_status = ComplianceStatus.NON_COMPLIANT
813
+ evaluation_reason = (
814
+ f"AWS Config recorder {recorder_name} is not actively recording (recording=false). "
815
+ f"Start the configuration recorder using the AWS Console, CLI, or API."
816
+ )
817
+
818
+ except Exception as e:
819
+ compliance_status = ComplianceStatus.ERROR
820
+ evaluation_reason = f"Error evaluating AWS Config recorder {recorder_name}: {str(e)}"
821
+
822
+ return ComplianceResult(
823
+ resource_id=recorder_name,
824
+ resource_type="AWS::Config::ConfigurationRecorder",
825
+ compliance_status=compliance_status,
826
+ evaluation_reason=evaluation_reason,
827
+ config_rule_name=self.rule_name,
828
+ region=region
829
+ )
830
+
831
+
832
+ # ============================================================================
833
+ # 7. WAF Logging Assessment
834
+ # ============================================================================
835
+
836
+ class WAFLoggingEnabledAssessment(BaseConfigRuleAssessment):
837
+ """Assessment for waf-logging-enabled AWS Config rule.
838
+
839
+ Validates that WAF web ACLs have logging enabled so that web application
840
+ firewall events are captured for security analysis.
841
+
842
+ This assessment handles both REGIONAL and CLOUDFRONT scopes:
843
+ - REGIONAL scope: Evaluated in all active regions
844
+ - CLOUDFRONT scope: Evaluated in us-east-1 only
845
+ """
846
+
847
+ def __init__(self):
848
+ super().__init__(
849
+ rule_name="waf-logging-enabled",
850
+ control_id="8.2",
851
+ resource_types=["AWS::WAFv2::WebACL"]
852
+ )
853
+
854
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
855
+ """Get WAF web ACLs in the specified region.
856
+
857
+ Handles both REGIONAL and CLOUDFRONT scopes. CLOUDFRONT scope is only
858
+ available in us-east-1.
859
+
860
+ Args:
861
+ aws_factory: AWS client factory for API access
862
+ resource_type: AWS resource type (should be AWS::WAFv2::WebACL)
863
+ region: AWS region to query
864
+
865
+ Returns:
866
+ List of web ACL dictionaries with Name, Id, ARN, Scope
867
+ """
868
+ if resource_type != "AWS::WAFv2::WebACL":
869
+ return []
870
+
871
+ try:
872
+ wafv2_client = aws_factory.get_client('wafv2', region)
873
+
874
+ web_acls = []
875
+
876
+ # Get REGIONAL web ACLs
877
+ try:
878
+ response = aws_factory.aws_api_call_with_retry(
879
+ lambda: wafv2_client.list_web_acls(Scope='REGIONAL')
880
+ )
881
+
882
+ regional_acls = response.get('WebACLs', [])
883
+ for acl in regional_acls:
884
+ acl['Scope'] = 'REGIONAL'
885
+ web_acls.extend(regional_acls)
886
+
887
+ except ClientError as e:
888
+ logger.warning(f"Error retrieving REGIONAL WAF web ACLs in {region}: {e}")
889
+
890
+ # Get CLOUDFRONT web ACLs (only in us-east-1)
891
+ if region == 'us-east-1':
892
+ try:
893
+ response = aws_factory.aws_api_call_with_retry(
894
+ lambda: wafv2_client.list_web_acls(Scope='CLOUDFRONT')
895
+ )
896
+
897
+ cloudfront_acls = response.get('WebACLs', [])
898
+ for acl in cloudfront_acls:
899
+ acl['Scope'] = 'CLOUDFRONT'
900
+ web_acls.extend(cloudfront_acls)
901
+
902
+ except ClientError as e:
903
+ logger.warning(f"Error retrieving CLOUDFRONT WAF web ACLs: {e}")
904
+
905
+ logger.debug(f"Found {len(web_acls)} WAF web ACLs in {region}")
906
+ return web_acls
907
+
908
+ except ClientError as e:
909
+ logger.error(f"Error retrieving WAF web ACLs in {region}: {e}")
910
+ raise
911
+
912
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
913
+ """Evaluate if WAF web ACL has logging enabled.
914
+
915
+ Args:
916
+ resource: Web ACL resource dictionary
917
+ aws_factory: AWS client factory for additional API calls
918
+ region: AWS region
919
+
920
+ Returns:
921
+ ComplianceResult indicating whether logging is enabled
922
+ """
923
+ web_acl_name = resource.get('Name', 'unknown')
924
+ web_acl_arn = resource.get('ARN', 'unknown')
925
+ web_acl_scope = resource.get('Scope', 'REGIONAL')
926
+
927
+ try:
928
+ wafv2_client = aws_factory.get_client('wafv2', region)
929
+
930
+ # Get logging configuration for the web ACL
931
+ try:
932
+ response = aws_factory.aws_api_call_with_retry(
933
+ lambda: wafv2_client.get_logging_configuration(ResourceArn=web_acl_arn)
934
+ )
935
+
936
+ logging_config = response.get('LoggingConfiguration', {})
937
+ log_destinations = logging_config.get('LogDestinationConfigs', [])
938
+
939
+ if log_destinations:
940
+ compliance_status = ComplianceStatus.COMPLIANT
941
+ evaluation_reason = f"WAF web ACL {web_acl_name} ({web_acl_scope}) has logging enabled"
942
+ else:
943
+ compliance_status = ComplianceStatus.NON_COMPLIANT
944
+ evaluation_reason = (
945
+ f"WAF web ACL {web_acl_name} ({web_acl_scope}) has logging configuration but no log destinations. "
946
+ f"Configure a log destination (Kinesis Data Firehose, S3, or CloudWatch Logs) for this web ACL."
947
+ )
948
+
949
+ except ClientError as e:
950
+ error_code = e.response.get('Error', {}).get('Code', '')
951
+
952
+ if error_code == 'WAFNonexistentItemException':
953
+ # No logging configuration exists
954
+ compliance_status = ComplianceStatus.NON_COMPLIANT
955
+ evaluation_reason = (
956
+ f"WAF web ACL {web_acl_name} ({web_acl_scope}) does not have logging enabled. "
957
+ f"Enable logging for this WAF web ACL. Configure a log destination (Kinesis Data Firehose, "
958
+ f"S3, or CloudWatch Logs) using the AWS Console, CLI, or API. WAF logs help analyze web "
959
+ f"application firewall events for security analysis."
960
+ )
961
+ else:
962
+ raise
963
+
964
+ except ClientError as e:
965
+ error_code = e.response.get('Error', {}).get('Code', '')
966
+
967
+ if error_code == 'AccessDenied':
968
+ compliance_status = ComplianceStatus.ERROR
969
+ evaluation_reason = f"Insufficient permissions to check logging for WAF web ACL {web_acl_name}: {str(e)}"
970
+ elif error_code == 'WAFNonexistentItemException':
971
+ compliance_status = ComplianceStatus.ERROR
972
+ evaluation_reason = f"WAF web ACL {web_acl_name} not found (may have been deleted)"
973
+ else:
974
+ compliance_status = ComplianceStatus.ERROR
975
+ evaluation_reason = f"Error checking logging for WAF web ACL {web_acl_name}: {str(e)}"
976
+
977
+ return ComplianceResult(
978
+ resource_id=web_acl_arn,
979
+ resource_type="AWS::WAFv2::WebACL",
980
+ compliance_status=compliance_status,
981
+ evaluation_reason=evaluation_reason,
982
+ config_rule_name=self.rule_name,
983
+ region=region
984
+ )