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,197 @@
1
+ """
2
+ CIS Control 8.2 - CloudTrail and Logging Controls
3
+ Critical logging and audit trail controls for compliance and security monitoring.
4
+ """
5
+
6
+ import logging
7
+ from typing import List, Dict, Any, Optional
8
+ import boto3
9
+ import json
10
+ from botocore.exceptions import ClientError, NoCredentialsError
11
+
12
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
13
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
14
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class CloudTrailEnabledAssessment(BaseConfigRuleAssessment):
20
+ """
21
+ CIS Control 8.2 - Collect Audit Logs
22
+ AWS Config Rule: cloudtrail-enabled
23
+
24
+ Ensures CloudTrail is enabled to record AWS Management Console actions and API calls.
25
+ """
26
+
27
+ def __init__(self):
28
+ super().__init__(
29
+ rule_name="cloudtrail-enabled",
30
+ control_id="8.2",
31
+ resource_types=["AWS::::Account"]
32
+ )
33
+
34
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
35
+ """Get CloudTrail configuration for the account."""
36
+ if resource_type != "AWS::::Account":
37
+ return []
38
+
39
+ try:
40
+ cloudtrail_client = aws_factory.get_client('cloudtrail', region)
41
+
42
+ # Get all trails in this region
43
+ response = cloudtrail_client.describe_trails()
44
+ trails = response.get('trailList', [])
45
+
46
+ # Get trail status for each trail
47
+ trail_statuses = []
48
+ for trail in trails:
49
+ trail_name = trail.get('Name', '')
50
+ trail_arn = trail.get('TrailARN', '')
51
+
52
+ try:
53
+ # Get trail status
54
+ status_response = cloudtrail_client.get_trail_status(Name=trail_name)
55
+ is_logging = status_response.get('IsLogging', False)
56
+
57
+ # Get event selectors to check what's being logged
58
+ try:
59
+ selectors_response = cloudtrail_client.get_event_selectors(TrailName=trail_name)
60
+ event_selectors = selectors_response.get('EventSelectors', [])
61
+ except ClientError:
62
+ event_selectors = []
63
+
64
+ trail_statuses.append({
65
+ 'TrailName': trail_name,
66
+ 'TrailARN': trail_arn,
67
+ 'IsLogging': is_logging,
68
+ 'IsMultiRegionTrail': trail.get('IsMultiRegionTrail', False),
69
+ 'IncludeGlobalServiceEvents': trail.get('IncludeGlobalServiceEvents', False),
70
+ 'S3BucketName': trail.get('S3BucketName', ''),
71
+ 'EventSelectors': event_selectors,
72
+ 'Region': region
73
+ })
74
+
75
+ except ClientError as e:
76
+ logger.warning(f"Error getting status for trail {trail_name}: {e}")
77
+ continue
78
+
79
+ # Return account-level resource with trail information
80
+ return [{
81
+ 'AccountId': aws_factory.get_account_info().get('account_id', 'unknown'),
82
+ 'Region': region,
83
+ 'Trails': trail_statuses,
84
+ 'HasActiveTrails': any(trail['IsLogging'] for trail in trail_statuses)
85
+ }]
86
+
87
+ except ClientError as e:
88
+ logger.error(f"Error retrieving CloudTrail information from {region}: {e}")
89
+ raise
90
+ except Exception as e:
91
+ logger.error(f"Unexpected error retrieving CloudTrail information from {region}: {e}")
92
+ raise
93
+
94
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
95
+ """Evaluate if CloudTrail is enabled and logging."""
96
+ account_id = resource.get('AccountId', 'unknown')
97
+ trails = resource.get('Trails', [])
98
+ has_active_trails = resource.get('HasActiveTrails', False)
99
+
100
+ if has_active_trails:
101
+ # Check for at least one properly configured trail
102
+ active_trails = [trail for trail in trails if trail['IsLogging']]
103
+ trail_names = [trail['TrailName'] for trail in active_trails]
104
+
105
+ return ComplianceResult(
106
+ resource_id=account_id,
107
+ resource_type="AWS::::Account",
108
+ compliance_status=ComplianceStatus.COMPLIANT,
109
+ evaluation_reason=f"CloudTrail is enabled with {len(active_trails)} active trail(s): {', '.join(trail_names)}",
110
+ config_rule_name=self.rule_name,
111
+ region=region
112
+ )
113
+ else:
114
+ return ComplianceResult(
115
+ resource_id=account_id,
116
+ resource_type="AWS::::Account",
117
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
118
+ evaluation_reason="CloudTrail is not enabled or no trails are actively logging",
119
+ config_rule_name=self.rule_name,
120
+ region=region
121
+ )
122
+
123
+
124
+ class CloudWatchLogGroupEncryptedAssessment(BaseConfigRuleAssessment):
125
+ """
126
+ CIS Control 3.11 - Encrypt Sensitive Data at Rest
127
+ AWS Config Rule: cloudwatch-log-group-encrypted
128
+
129
+ Ensures CloudWatch Log Groups are encrypted with KMS keys.
130
+ """
131
+
132
+ def __init__(self):
133
+ super().__init__(
134
+ rule_name="cloudwatch-log-group-encrypted",
135
+ control_id="3.11",
136
+ resource_types=["AWS::Logs::LogGroup"]
137
+ )
138
+
139
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
140
+ """Get all CloudWatch Log Groups in the region."""
141
+ if resource_type != "AWS::Logs::LogGroup":
142
+ return []
143
+
144
+ try:
145
+ logs_client = aws_factory.get_client('logs', region)
146
+
147
+ log_groups = []
148
+ paginator = logs_client.get_paginator('describe_log_groups')
149
+
150
+ for page in paginator.paginate():
151
+ for log_group in page.get('logGroups', []):
152
+ log_group_name = log_group.get('logGroupName', '')
153
+ kms_key_id = log_group.get('kmsKeyId', '')
154
+
155
+ log_groups.append({
156
+ 'LogGroupName': log_group_name,
157
+ 'KmsKeyId': kms_key_id,
158
+ 'IsEncrypted': bool(kms_key_id),
159
+ 'CreationTime': log_group.get('creationTime', 0),
160
+ 'RetentionInDays': log_group.get('retentionInDays'),
161
+ 'StoredBytes': log_group.get('storedBytes', 0)
162
+ })
163
+
164
+ logger.debug(f"Found {len(log_groups)} CloudWatch Log Groups in {region}")
165
+ return log_groups
166
+
167
+ except ClientError as e:
168
+ logger.error(f"Error retrieving CloudWatch Log Groups from {region}: {e}")
169
+ raise
170
+ except Exception as e:
171
+ logger.error(f"Unexpected error retrieving CloudWatch Log Groups from {region}: {e}")
172
+ raise
173
+
174
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], aws_factory: AWSClientFactory, region: str) -> ComplianceResult:
175
+ """Evaluate if CloudWatch Log Group is encrypted."""
176
+ log_group_name = resource.get('LogGroupName', 'unknown')
177
+ is_encrypted = resource.get('IsEncrypted', False)
178
+ kms_key_id = resource.get('KmsKeyId', '')
179
+
180
+ if is_encrypted:
181
+ return ComplianceResult(
182
+ resource_id=log_group_name,
183
+ resource_type="AWS::Logs::LogGroup",
184
+ compliance_status=ComplianceStatus.COMPLIANT,
185
+ evaluation_reason=f"CloudWatch Log Group is encrypted with KMS key: {kms_key_id}",
186
+ config_rule_name=self.rule_name,
187
+ region=region
188
+ )
189
+ else:
190
+ return ComplianceResult(
191
+ resource_id=log_group_name,
192
+ resource_type="AWS::Logs::LogGroup",
193
+ compliance_status=ComplianceStatus.NON_COMPLIANT,
194
+ evaluation_reason="CloudWatch Log Group is not encrypted with KMS",
195
+ config_rule_name=self.rule_name,
196
+ region=region
197
+ )
@@ -0,0 +1,422 @@
1
+ """
2
+ Critical Security Foundation Controls for CIS Assessment.
3
+ Implements the most critical security controls that should be prioritized.
4
+ """
5
+
6
+ from typing import List, Dict, Any, Optional
7
+ import logging
8
+ from botocore.exceptions import ClientError, NoCredentialsError
9
+
10
+ from aws_cis_assessment.controls.base_control import BaseConfigRuleAssessment
11
+ from aws_cis_assessment.core.models import ComplianceResult, ComplianceStatus
12
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class RootAccountHardwareMFAEnabledAssessment(BaseConfigRuleAssessment):
18
+ """Assessment for root-account-hardware-mfa-enabled AWS Config rule."""
19
+
20
+ def __init__(self):
21
+ super().__init__(
22
+ rule_name="root-account-hardware-mfa-enabled",
23
+ control_id="1.5",
24
+ resource_types=["AWS::IAM::Root"]
25
+ )
26
+
27
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
28
+ """Get root account MFA configuration."""
29
+ if resource_type != "AWS::IAM::Root":
30
+ return []
31
+
32
+ try:
33
+ iam_client = aws_factory.get_client('iam', region)
34
+
35
+ # Get account summary which includes MFA device count
36
+ account_summary = iam_client.get_account_summary()
37
+
38
+ # List MFA devices for root account (empty user name for root)
39
+ mfa_devices = iam_client.list_mfa_devices()
40
+
41
+ # Get virtual MFA devices to differentiate from hardware
42
+ virtual_mfa_devices = iam_client.list_virtual_mfa_devices()
43
+
44
+ return [{
45
+ 'account_id': aws_factory.account_id,
46
+ 'account_summary': account_summary.get('SummaryMap', {}),
47
+ 'mfa_devices': mfa_devices.get('MFADevices', []),
48
+ 'virtual_mfa_devices': virtual_mfa_devices.get('VirtualMFADevices', [])
49
+ }]
50
+
51
+ except ClientError as e:
52
+ logger.error(f"Error getting root account MFA configuration: {e}")
53
+ return []
54
+ except Exception as e:
55
+ logger.error(f"Unexpected error in root account MFA check: {e}")
56
+ return []
57
+
58
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], **kwargs) -> ComplianceResult:
59
+ """Evaluate root account hardware MFA compliance."""
60
+ try:
61
+ account_summary = resource.get('account_summary', {})
62
+ mfa_devices = resource.get('mfa_devices', [])
63
+ virtual_mfa_devices = resource.get('virtual_mfa_devices', [])
64
+
65
+ # Check if root account has any MFA devices
66
+ account_mfa_enabled = account_summary.get('AccountMFAEnabled', 0)
67
+
68
+ if account_mfa_enabled == 0:
69
+ return ComplianceResult(
70
+ status=ComplianceStatus.NON_COMPLIANT,
71
+ reason="Root account does not have MFA enabled",
72
+ resource_id=resource['account_id'],
73
+ resource_type="AWS::IAM::Root"
74
+ )
75
+
76
+ # Check if there are any MFA devices for root (empty UserName indicates root)
77
+ root_mfa_devices = [device for device in mfa_devices if not device.get('UserName')]
78
+
79
+ if not root_mfa_devices:
80
+ return ComplianceResult(
81
+ status=ComplianceStatus.NON_COMPLIANT,
82
+ reason="Root account MFA is enabled but no MFA devices found",
83
+ resource_id=resource['account_id'],
84
+ resource_type="AWS::IAM::Root"
85
+ )
86
+
87
+ # Check if any of the root MFA devices are hardware (not virtual)
88
+ virtual_mfa_serial_numbers = {device.get('SerialNumber') for device in virtual_mfa_devices}
89
+
90
+ hardware_mfa_devices = [
91
+ device for device in root_mfa_devices
92
+ if device.get('SerialNumber') not in virtual_mfa_serial_numbers
93
+ ]
94
+
95
+ if not hardware_mfa_devices:
96
+ return ComplianceResult(
97
+ status=ComplianceStatus.NON_COMPLIANT,
98
+ reason="Root account only has virtual MFA devices, hardware MFA required",
99
+ resource_id=resource['account_id'],
100
+ resource_type="AWS::IAM::Root"
101
+ )
102
+
103
+ return ComplianceResult(
104
+ status=ComplianceStatus.COMPLIANT,
105
+ reason=f"Root account has {len(hardware_mfa_devices)} hardware MFA device(s) enabled",
106
+ resource_id=resource['account_id'],
107
+ resource_type="AWS::IAM::Root"
108
+ )
109
+
110
+ except Exception as e:
111
+ logger.error(f"Error evaluating root account hardware MFA compliance: {e}")
112
+ return ComplianceResult(
113
+ status=ComplianceStatus.NOT_APPLICABLE,
114
+ reason=f"Error evaluating compliance: {str(e)}",
115
+ resource_id=resource.get('account_id', 'unknown'),
116
+ resource_type="AWS::IAM::Root"
117
+ )
118
+
119
+
120
+ class OpenSearchInVPCOnlyAssessment(BaseConfigRuleAssessment):
121
+ """Assessment for opensearch-in-vpc-only AWS Config rule."""
122
+
123
+ def __init__(self):
124
+ super().__init__(
125
+ rule_name="opensearch-in-vpc-only",
126
+ control_id="2.2.1",
127
+ resource_types=["AWS::OpenSearch::Domain"]
128
+ )
129
+
130
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
131
+ """Get OpenSearch domains to evaluate."""
132
+ if resource_type != "AWS::OpenSearch::Domain":
133
+ return []
134
+
135
+ try:
136
+ opensearch_client = aws_factory.get_client('opensearch', region)
137
+
138
+ # List all OpenSearch domain names
139
+ domain_names_response = opensearch_client.list_domain_names()
140
+ domain_names = [domain['DomainName'] for domain in domain_names_response.get('DomainNames', [])]
141
+
142
+ if not domain_names:
143
+ return []
144
+
145
+ # Get detailed information for each domain
146
+ domains_response = opensearch_client.describe_domains(DomainNames=domain_names)
147
+ domains = domains_response.get('DomainStatusList', [])
148
+
149
+ return domains
150
+
151
+ except ClientError as e:
152
+ if e.response['Error']['Code'] in ['UnauthorizedOperation', 'AccessDenied']:
153
+ logger.warning("Insufficient permissions to list OpenSearch domains")
154
+ return []
155
+ logger.error(f"Error listing OpenSearch domains: {e}")
156
+ return []
157
+ except Exception as e:
158
+ logger.error(f"Unexpected error listing OpenSearch domains: {e}")
159
+ return []
160
+
161
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], **kwargs) -> ComplianceResult:
162
+ """Evaluate OpenSearch domain VPC compliance."""
163
+ try:
164
+ domain_name = resource.get('DomainName', 'unknown')
165
+ vpc_options = resource.get('VPCOptions', {})
166
+
167
+ # Check if domain is deployed in VPC
168
+ vpc_id = vpc_options.get('VPCId')
169
+
170
+ if not vpc_id:
171
+ return ComplianceResult(
172
+ status=ComplianceStatus.NON_COMPLIANT,
173
+ reason="OpenSearch domain is not deployed within a VPC",
174
+ resource_id=domain_name,
175
+ resource_type="AWS::OpenSearch::Domain"
176
+ )
177
+
178
+ # Additional checks for VPC configuration
179
+ subnet_ids = vpc_options.get('SubnetIds', [])
180
+ security_group_ids = vpc_options.get('SecurityGroupIds', [])
181
+
182
+ if not subnet_ids:
183
+ return ComplianceResult(
184
+ status=ComplianceStatus.NON_COMPLIANT,
185
+ reason="OpenSearch domain VPC configuration is incomplete - no subnets specified",
186
+ resource_id=domain_name,
187
+ resource_type="AWS::OpenSearch::Domain"
188
+ )
189
+
190
+ if not security_group_ids:
191
+ return ComplianceResult(
192
+ status=ComplianceStatus.NON_COMPLIANT,
193
+ reason="OpenSearch domain VPC configuration is incomplete - no security groups specified",
194
+ resource_id=domain_name,
195
+ resource_type="AWS::OpenSearch::Domain"
196
+ )
197
+
198
+ return ComplianceResult(
199
+ status=ComplianceStatus.COMPLIANT,
200
+ reason=f"OpenSearch domain is properly deployed in VPC {vpc_id} with {len(subnet_ids)} subnets",
201
+ resource_id=domain_name,
202
+ resource_type="AWS::OpenSearch::Domain"
203
+ )
204
+
205
+ except Exception as e:
206
+ logger.error(f"Error evaluating OpenSearch domain VPC compliance: {e}")
207
+ return ComplianceResult(
208
+ status=ComplianceStatus.NOT_APPLICABLE,
209
+ reason=f"Error evaluating compliance: {str(e)}",
210
+ resource_id=resource.get('DomainName', 'unknown'),
211
+ resource_type="AWS::OpenSearch::Domain"
212
+ )
213
+
214
+
215
+ class ECSTaskDefinitionNonRootUserAssessment(BaseConfigRuleAssessment):
216
+ """Assessment for ecs-task-definition-nonroot-user AWS Config rule."""
217
+
218
+ def __init__(self):
219
+ super().__init__(
220
+ rule_name="ecs-task-definition-nonroot-user",
221
+ control_id="4.1",
222
+ resource_types=["AWS::ECS::TaskDefinition"]
223
+ )
224
+
225
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
226
+ """Get ECS task definitions to evaluate."""
227
+ if resource_type != "AWS::ECS::TaskDefinition":
228
+ return []
229
+
230
+ try:
231
+ ecs_client = aws_factory.get_client('ecs', region)
232
+
233
+ # List all task definition families
234
+ families_response = ecs_client.list_task_definition_families(status='ACTIVE')
235
+ families = families_response.get('families', [])
236
+
237
+ task_definitions = []
238
+
239
+ for family in families:
240
+ try:
241
+ # Get the latest active revision for each family
242
+ task_def_response = ecs_client.describe_task_definition(
243
+ taskDefinition=family,
244
+ include=['TAGS']
245
+ )
246
+ task_definitions.append(task_def_response.get('taskDefinition', {}))
247
+ except ClientError as e:
248
+ logger.warning(f"Error describing task definition {family}: {e}")
249
+ continue
250
+
251
+ return task_definitions
252
+
253
+ except ClientError as e:
254
+ if e.response['Error']['Code'] in ['UnauthorizedOperation', 'AccessDenied']:
255
+ logger.warning("Insufficient permissions to list ECS task definitions")
256
+ return []
257
+ logger.error(f"Error listing ECS task definitions: {e}")
258
+ return []
259
+ except Exception as e:
260
+ logger.error(f"Unexpected error listing ECS task definitions: {e}")
261
+ return []
262
+
263
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], **kwargs) -> ComplianceResult:
264
+ """Evaluate ECS task definition non-root user compliance."""
265
+ try:
266
+ task_def_arn = resource.get('taskDefinitionArn', 'unknown')
267
+ family = resource.get('family', 'unknown')
268
+ revision = resource.get('revision', 'unknown')
269
+ container_definitions = resource.get('containerDefinitions', [])
270
+
271
+ if not container_definitions:
272
+ return ComplianceResult(
273
+ status=ComplianceStatus.NOT_APPLICABLE,
274
+ reason="Task definition has no container definitions",
275
+ resource_id=f"{family}:{revision}",
276
+ resource_type="AWS::ECS::TaskDefinition"
277
+ )
278
+
279
+ non_compliant_containers = []
280
+
281
+ for container in container_definitions:
282
+ container_name = container.get('name', 'unknown')
283
+ user = container.get('user')
284
+
285
+ # Check if user is specified and is not root
286
+ if user is None:
287
+ # No user specified - defaults to root in most images
288
+ non_compliant_containers.append(f"{container_name} (no user specified)")
289
+ elif user == 'root' or user == '0':
290
+ # Explicitly set to root
291
+ non_compliant_containers.append(f"{container_name} (user: {user})")
292
+ # If user is specified and not root, it's compliant
293
+
294
+ if non_compliant_containers:
295
+ return ComplianceResult(
296
+ status=ComplianceStatus.NON_COMPLIANT,
297
+ reason=f"Containers running as root: {', '.join(non_compliant_containers)}",
298
+ resource_id=f"{family}:{revision}",
299
+ resource_type="AWS::ECS::TaskDefinition"
300
+ )
301
+
302
+ return ComplianceResult(
303
+ status=ComplianceStatus.COMPLIANT,
304
+ reason=f"All {len(container_definitions)} containers specify non-root users",
305
+ resource_id=f"{family}:{revision}",
306
+ resource_type="AWS::ECS::TaskDefinition"
307
+ )
308
+
309
+ except Exception as e:
310
+ logger.error(f"Error evaluating ECS task definition compliance: {e}")
311
+ return ComplianceResult(
312
+ status=ComplianceStatus.NOT_APPLICABLE,
313
+ reason=f"Error evaluating compliance: {str(e)}",
314
+ resource_id=resource.get('family', 'unknown'),
315
+ resource_type="AWS::ECS::TaskDefinition"
316
+ )
317
+
318
+
319
+ class SecurityHubEnabledAssessment(BaseConfigRuleAssessment):
320
+ """Assessment for securityhub-enabled AWS Config rule."""
321
+
322
+ def __init__(self):
323
+ super().__init__(
324
+ rule_name="securityhub-enabled",
325
+ control_id="8.8",
326
+ resource_types=["AWS::SecurityHub::Hub"]
327
+ )
328
+
329
+ def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
330
+ """Get Security Hub configuration for current region."""
331
+ if resource_type != "AWS::SecurityHub::Hub":
332
+ return []
333
+
334
+ try:
335
+ securityhub_client = aws_factory.get_client('securityhub', region)
336
+
337
+ # Check if Security Hub is enabled
338
+ try:
339
+ hub_response = securityhub_client.describe_hub()
340
+
341
+ # Get enabled standards
342
+ standards_response = securityhub_client.get_enabled_standards()
343
+
344
+ return [{
345
+ 'region': region,
346
+ 'hub_arn': hub_response.get('HubArn'),
347
+ 'subscribed_at': hub_response.get('SubscribedAt'),
348
+ 'auto_enable_controls': hub_response.get('AutoEnableControls'),
349
+ 'enabled_standards': standards_response.get('StandardsSubscriptions', [])
350
+ }]
351
+
352
+ except ClientError as e:
353
+ if e.response['Error']['Code'] == 'InvalidAccessException':
354
+ # Security Hub is not enabled
355
+ return [{
356
+ 'region': region,
357
+ 'enabled': False,
358
+ 'error': 'Security Hub is not enabled'
359
+ }]
360
+ else:
361
+ raise
362
+
363
+ except ClientError as e:
364
+ if e.response['Error']['Code'] in ['UnauthorizedOperation', 'AccessDenied']:
365
+ logger.warning("Insufficient permissions to check Security Hub status")
366
+ return []
367
+ logger.error(f"Error checking Security Hub status: {e}")
368
+ return []
369
+ except Exception as e:
370
+ logger.error(f"Unexpected error checking Security Hub: {e}")
371
+ return []
372
+
373
+ def _evaluate_resource_compliance(self, resource: Dict[str, Any], **kwargs) -> ComplianceResult:
374
+ """Evaluate Security Hub enabled compliance."""
375
+ try:
376
+ region = resource.get('region', 'unknown')
377
+
378
+ # Check if Security Hub is enabled
379
+ if resource.get('enabled') is False:
380
+ return ComplianceResult(
381
+ status=ComplianceStatus.NON_COMPLIANT,
382
+ reason="AWS Security Hub is not enabled in this region",
383
+ resource_id=region,
384
+ resource_type="AWS::SecurityHub::Hub"
385
+ )
386
+
387
+ hub_arn = resource.get('hub_arn')
388
+ if not hub_arn:
389
+ return ComplianceResult(
390
+ status=ComplianceStatus.NON_COMPLIANT,
391
+ reason="Security Hub configuration is incomplete",
392
+ resource_id=region,
393
+ resource_type="AWS::SecurityHub::Hub"
394
+ )
395
+
396
+ # Check if any security standards are enabled
397
+ enabled_standards = resource.get('enabled_standards', [])
398
+ active_standards = [std for std in enabled_standards if std.get('StandardsStatus') == 'READY']
399
+
400
+ if not active_standards:
401
+ return ComplianceResult(
402
+ status=ComplianceStatus.NON_COMPLIANT,
403
+ reason="Security Hub is enabled but no security standards are active",
404
+ resource_id=region,
405
+ resource_type="AWS::SecurityHub::Hub"
406
+ )
407
+
408
+ return ComplianceResult(
409
+ status=ComplianceStatus.COMPLIANT,
410
+ reason=f"Security Hub is enabled with {len(active_standards)} active security standards",
411
+ resource_id=region,
412
+ resource_type="AWS::SecurityHub::Hub"
413
+ )
414
+
415
+ except Exception as e:
416
+ logger.error(f"Error evaluating Security Hub compliance: {e}")
417
+ return ComplianceResult(
418
+ status=ComplianceStatus.NOT_APPLICABLE,
419
+ reason=f"Error evaluating compliance: {str(e)}",
420
+ resource_id=resource.get('region', 'unknown'),
421
+ resource_type="AWS::SecurityHub::Hub"
422
+ )