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