aws-cis-controls-assessment 1.1.4__py3-none-any.whl → 1.2.2__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 +4 -4
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +365 -2
- aws_cis_assessment/controls/base_control.py +106 -24
- aws_cis_assessment/controls/ig1/__init__.py +144 -15
- aws_cis_assessment/controls/ig1/control_4_1.py +4 -4
- aws_cis_assessment/controls/ig1/control_access_analyzer.py +198 -0
- aws_cis_assessment/controls/ig1/control_access_asset_mgmt.py +360 -0
- aws_cis_assessment/controls/ig1/control_access_control.py +323 -0
- aws_cis_assessment/controls/ig1/control_backup_security.py +579 -0
- aws_cis_assessment/controls/ig1/control_cloudfront_logging.py +215 -0
- aws_cis_assessment/controls/ig1/control_configuration_mgmt.py +407 -0
- aws_cis_assessment/controls/ig1/control_data_classification.py +255 -0
- aws_cis_assessment/controls/ig1/control_dynamodb_encryption.py +279 -0
- aws_cis_assessment/controls/ig1/control_ebs_encryption.py +177 -0
- aws_cis_assessment/controls/ig1/control_efs_encryption.py +243 -0
- aws_cis_assessment/controls/ig1/control_elb_logging.py +195 -0
- aws_cis_assessment/controls/ig1/control_guardduty.py +156 -0
- aws_cis_assessment/controls/ig1/control_inspector.py +184 -0
- aws_cis_assessment/controls/ig1/control_inventory.py +511 -0
- aws_cis_assessment/controls/ig1/control_macie.py +165 -0
- aws_cis_assessment/controls/ig1/control_messaging_encryption.py +419 -0
- aws_cis_assessment/controls/ig1/control_mfa.py +485 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +194 -619
- aws_cis_assessment/controls/ig1/control_patch_management.py +626 -0
- aws_cis_assessment/controls/ig1/control_rds_encryption.py +228 -0
- aws_cis_assessment/controls/ig1/control_s3_encryption.py +383 -0
- aws_cis_assessment/controls/ig1/control_tls_ssl.py +556 -0
- aws_cis_assessment/controls/ig1/control_version_mgmt.py +337 -0
- aws_cis_assessment/controls/ig1/control_vpc_flow_logs.py +205 -0
- aws_cis_assessment/controls/ig1/control_waf_logging.py +226 -0
- aws_cis_assessment/core/assessment_engine.py +160 -11
- aws_cis_assessment/core/aws_client_factory.py +17 -5
- aws_cis_assessment/core/models.py +20 -1
- aws_cis_assessment/core/scoring_engine.py +102 -1
- aws_cis_assessment/reporters/base_reporter.py +58 -13
- aws_cis_assessment/reporters/html_reporter.py +186 -9
- aws_cis_controls_assessment-1.2.2.dist-info/METADATA +320 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/RECORD +44 -20
- docs/developer-guide.md +204 -5
- docs/user-guide.md +137 -4
- aws_cis_controls_assessment-1.1.4.dist-info/METADATA +0 -404
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CIS Control 8.2 - WAF Logging
|
|
3
|
+
Ensures AWS WAF web ACLs have logging enabled.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
from typing import List, Dict, Any
|
|
8
|
+
from botocore.exceptions import ClientError
|
|
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 WAFLoggingEnabledAssessment(BaseConfigRuleAssessment):
|
|
18
|
+
"""
|
|
19
|
+
CIS Control 8.2 - Collect Audit Logs
|
|
20
|
+
AWS Config Rule: waf-logging-enabled
|
|
21
|
+
|
|
22
|
+
Ensures AWS WAF web ACLs have logging enabled.
|
|
23
|
+
WAF logs contain detailed information about requests analyzed by the web ACL,
|
|
24
|
+
essential for security analysis, threat detection, and compliance.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
super().__init__(
|
|
29
|
+
rule_name="waf-logging-enabled",
|
|
30
|
+
control_id="8.2",
|
|
31
|
+
resource_types=["AWS::WAFv2::WebACL"]
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def _get_resources(self, aws_factory: AWSClientFactory, resource_type: str, region: str) -> List[Dict[str, Any]]:
|
|
35
|
+
"""Get WAF web ACLs and their logging configuration."""
|
|
36
|
+
if resource_type != "AWS::WAFv2::WebACL":
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
wafv2_client = aws_factory.get_client('wafv2', region)
|
|
41
|
+
|
|
42
|
+
# List regional web ACLs
|
|
43
|
+
web_acls = []
|
|
44
|
+
try:
|
|
45
|
+
response = wafv2_client.list_web_acls(Scope='REGIONAL')
|
|
46
|
+
web_acls.extend(response.get('WebACLs', []))
|
|
47
|
+
except ClientError as e:
|
|
48
|
+
logger.debug(f"Error listing regional web ACLs in {region}: {e}")
|
|
49
|
+
|
|
50
|
+
# For us-east-1, also check CloudFront (CLOUDFRONT scope)
|
|
51
|
+
if region == 'us-east-1':
|
|
52
|
+
try:
|
|
53
|
+
cf_response = wafv2_client.list_web_acls(Scope='CLOUDFRONT')
|
|
54
|
+
cloudfront_acls = cf_response.get('WebACLs', [])
|
|
55
|
+
for acl in cloudfront_acls:
|
|
56
|
+
acl['Scope'] = 'CLOUDFRONT'
|
|
57
|
+
web_acls.extend(cloudfront_acls)
|
|
58
|
+
except ClientError as e:
|
|
59
|
+
logger.debug(f"Error listing CloudFront web ACLs: {e}")
|
|
60
|
+
|
|
61
|
+
if not web_acls:
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
# Get logging configuration for each web ACL
|
|
65
|
+
acl_resources = []
|
|
66
|
+
for acl in web_acls:
|
|
67
|
+
acl_arn = acl.get('ARN', '')
|
|
68
|
+
acl_name = acl.get('Name', '')
|
|
69
|
+
acl_scope = acl.get('Scope', 'REGIONAL')
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
# Get logging configuration
|
|
73
|
+
logging_config = None
|
|
74
|
+
log_destinations = []
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
log_response = wafv2_client.get_logging_configuration(
|
|
78
|
+
ResourceArn=acl_arn
|
|
79
|
+
)
|
|
80
|
+
logging_config = log_response.get('LoggingConfiguration', {})
|
|
81
|
+
log_destinations = logging_config.get('LogDestinationConfigs', [])
|
|
82
|
+
except ClientError as e:
|
|
83
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
84
|
+
if error_code != 'WAFNonexistentItemException':
|
|
85
|
+
logger.debug(f"Error getting logging config for {acl_name}: {e}")
|
|
86
|
+
|
|
87
|
+
acl_resources.append({
|
|
88
|
+
'WebACLArn': acl_arn,
|
|
89
|
+
'WebACLName': acl_name,
|
|
90
|
+
'WebACLId': acl.get('Id', ''),
|
|
91
|
+
'Scope': acl_scope,
|
|
92
|
+
'Region': region if acl_scope == 'REGIONAL' else 'global',
|
|
93
|
+
'LoggingEnabled': len(log_destinations) > 0,
|
|
94
|
+
'LogDestinations': log_destinations
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
except ClientError as e:
|
|
98
|
+
logger.warning(f"Error processing web ACL {acl_name}: {e}")
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
return acl_resources
|
|
102
|
+
|
|
103
|
+
except ClientError as e:
|
|
104
|
+
error_code = e.response.get('Error', {}).get('Code', '')
|
|
105
|
+
if error_code == 'AccessDeniedException':
|
|
106
|
+
logger.warning(f"Access denied to WAFv2 in {region}")
|
|
107
|
+
else:
|
|
108
|
+
logger.error(f"Error listing WAF web ACLs in {region}: {e}")
|
|
109
|
+
return []
|
|
110
|
+
|
|
111
|
+
def _evaluate_resource_compliance(
|
|
112
|
+
self,
|
|
113
|
+
resource: Dict[str, Any],
|
|
114
|
+
aws_factory: AWSClientFactory,
|
|
115
|
+
region: str
|
|
116
|
+
) -> ComplianceResult:
|
|
117
|
+
"""Evaluate if WAF web ACL has logging enabled."""
|
|
118
|
+
acl_arn = resource.get('WebACLArn', '')
|
|
119
|
+
acl_name = resource.get('WebACLName', '')
|
|
120
|
+
logging_enabled = resource.get('LoggingEnabled', False)
|
|
121
|
+
log_destinations = resource.get('LogDestinations', [])
|
|
122
|
+
scope = resource.get('Scope', 'REGIONAL')
|
|
123
|
+
|
|
124
|
+
# Check if logging is enabled
|
|
125
|
+
is_compliant = logging_enabled and len(log_destinations) > 0
|
|
126
|
+
|
|
127
|
+
if is_compliant:
|
|
128
|
+
# Determine destination type
|
|
129
|
+
dest_types = []
|
|
130
|
+
for dest in log_destinations:
|
|
131
|
+
if 'logs' in dest:
|
|
132
|
+
dest_types.append('CloudWatch Logs')
|
|
133
|
+
elif 'firehose' in dest:
|
|
134
|
+
dest_types.append('Kinesis Firehose')
|
|
135
|
+
elif 's3' in dest:
|
|
136
|
+
dest_types.append('S3')
|
|
137
|
+
|
|
138
|
+
evaluation_reason = (
|
|
139
|
+
f"WAF web ACL '{acl_name}' ({scope}) has logging enabled. "
|
|
140
|
+
f"Destinations: {', '.join(dest_types)}"
|
|
141
|
+
)
|
|
142
|
+
compliance_status = ComplianceStatus.COMPLIANT
|
|
143
|
+
else:
|
|
144
|
+
evaluation_reason = f"WAF web ACL '{acl_name}' ({scope}) does not have logging enabled."
|
|
145
|
+
compliance_status = ComplianceStatus.NON_COMPLIANT
|
|
146
|
+
|
|
147
|
+
return ComplianceResult(
|
|
148
|
+
resource_id=acl_arn,
|
|
149
|
+
resource_type="AWS::WAFv2::WebACL",
|
|
150
|
+
compliance_status=compliance_status,
|
|
151
|
+
evaluation_reason=evaluation_reason,
|
|
152
|
+
config_rule_name=self.rule_name,
|
|
153
|
+
region=resource.get('Region', region)
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _get_rule_remediation_steps(self) -> List[str]:
|
|
157
|
+
"""Get remediation steps for enabling WAF logging."""
|
|
158
|
+
return [
|
|
159
|
+
"1. Enable WAF logging in the AWS Console:",
|
|
160
|
+
" - Navigate to WAF & Shield service",
|
|
161
|
+
" - Select 'Web ACLs'",
|
|
162
|
+
" - Select the web ACL",
|
|
163
|
+
" - Click 'Logging and metrics' tab",
|
|
164
|
+
" - Click 'Enable logging'",
|
|
165
|
+
" - Choose log destination:",
|
|
166
|
+
" * CloudWatch Logs log group",
|
|
167
|
+
" * Kinesis Data Firehose delivery stream",
|
|
168
|
+
" * S3 bucket",
|
|
169
|
+
" - Click 'Enable logging'",
|
|
170
|
+
"",
|
|
171
|
+
"2. Create log destination (if needed):",
|
|
172
|
+
" # For CloudWatch Logs",
|
|
173
|
+
" aws logs create-log-group \\",
|
|
174
|
+
" --log-group-name aws-waf-logs-<name> \\",
|
|
175
|
+
" --region <region>",
|
|
176
|
+
"",
|
|
177
|
+
" # For Kinesis Firehose (more complex, see AWS docs)",
|
|
178
|
+
"",
|
|
179
|
+
"3. Enable WAF logging using AWS CLI:",
|
|
180
|
+
" aws wafv2 put-logging-configuration \\",
|
|
181
|
+
" --logging-configuration '{",
|
|
182
|
+
' "ResourceArn": "<web-acl-arn>",',
|
|
183
|
+
' "LogDestinationConfigs": [',
|
|
184
|
+
' "arn:aws:logs:<region>:<account-id>:log-group:aws-waf-logs-<name>"',
|
|
185
|
+
" ]",
|
|
186
|
+
" }' \\",
|
|
187
|
+
" --region <region>",
|
|
188
|
+
"",
|
|
189
|
+
"4. For CloudFront web ACLs (use us-east-1):",
|
|
190
|
+
" aws wafv2 put-logging-configuration \\",
|
|
191
|
+
" --logging-configuration '{",
|
|
192
|
+
' "ResourceArn": "<web-acl-arn>",',
|
|
193
|
+
' "LogDestinationConfigs": [',
|
|
194
|
+
' "arn:aws:logs:us-east-1:<account-id>:log-group:aws-waf-logs-<name>"',
|
|
195
|
+
" ]",
|
|
196
|
+
" }' \\",
|
|
197
|
+
" --region us-east-1",
|
|
198
|
+
"",
|
|
199
|
+
"5. Best practices:",
|
|
200
|
+
" - Use CloudWatch Logs for real-time analysis",
|
|
201
|
+
" - Use Kinesis Firehose + S3 for long-term storage",
|
|
202
|
+
" - Set appropriate log retention (30-90 days)",
|
|
203
|
+
" - Enable for all web ACLs (regional and CloudFront)",
|
|
204
|
+
" - Use log filtering to reduce volume if needed",
|
|
205
|
+
"",
|
|
206
|
+
"6. Analyze WAF logs:",
|
|
207
|
+
" - Use CloudWatch Insights for queries",
|
|
208
|
+
" - Use Athena for S3-based logs",
|
|
209
|
+
" - Look for:",
|
|
210
|
+
" * Blocked requests (potential attacks)",
|
|
211
|
+
" * Rule match patterns",
|
|
212
|
+
" * Geographic distribution of threats",
|
|
213
|
+
" * Rate-based rule triggers",
|
|
214
|
+
" * False positives (legitimate traffic blocked)",
|
|
215
|
+
"",
|
|
216
|
+
"7. Important notes:",
|
|
217
|
+
" - Log group name must start with 'aws-waf-logs-'",
|
|
218
|
+
" - CloudFront web ACLs must log to us-east-1",
|
|
219
|
+
" - Logging incurs CloudWatch Logs charges",
|
|
220
|
+
"",
|
|
221
|
+
"Priority: HIGH - WAF logs are critical for threat detection",
|
|
222
|
+
"Effort: Low - Can be enabled in minutes per web ACL",
|
|
223
|
+
"",
|
|
224
|
+
"AWS Documentation:",
|
|
225
|
+
"https://docs.aws.amazon.com/waf/latest/developerguide/logging.html"
|
|
226
|
+
]
|
|
@@ -59,10 +59,8 @@ from aws_cis_assessment.controls.ig1.control_data_protection import (
|
|
|
59
59
|
S3BucketLevelPublicAccessProhibitedAssessment
|
|
60
60
|
)
|
|
61
61
|
from aws_cis_assessment.controls.ig1.control_network_security import (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
LambdaFunctionPublicAccessProhibitedAssessment, SageMakerNotebookNoDirectInternetAccessAssessment,
|
|
65
|
-
SubnetAutoAssignPublicIPDisabledAssessment
|
|
62
|
+
NetworkFirewallDeployedAssessment,
|
|
63
|
+
Route53ResolverFirewallEnabledAssessment
|
|
66
64
|
)
|
|
67
65
|
from aws_cis_assessment.controls.ig1.control_iam_governance import (
|
|
68
66
|
IAMGroupHasUsersCheckAssessment, IAMPolicyNoStatementsWithFullAccessAssessment,
|
|
@@ -106,6 +104,82 @@ from aws_cis_assessment.controls.ig1.control_s3_enhancements import (
|
|
|
106
104
|
from aws_cis_assessment.controls.ig1.control_instance_optimization import (
|
|
107
105
|
EBSOptimizedInstanceAssessment
|
|
108
106
|
)
|
|
107
|
+
|
|
108
|
+
# Phase 1-4: CIS Controls v8.1 IG1 Expansion (50 new rules)
|
|
109
|
+
from aws_cis_assessment.controls.ig1.control_guardduty import GuardDutyEnabledAssessment
|
|
110
|
+
from aws_cis_assessment.controls.ig1.control_inspector import InspectorEnabledAssessment
|
|
111
|
+
from aws_cis_assessment.controls.ig1.control_macie import MacieEnabledAssessment
|
|
112
|
+
from aws_cis_assessment.controls.ig1.control_access_analyzer import IAMAccessAnalyzerEnabledAssessment
|
|
113
|
+
from aws_cis_assessment.controls.ig1.control_vpc_flow_logs import VPCFlowLogsEnabledAssessment
|
|
114
|
+
from aws_cis_assessment.controls.ig1.control_elb_logging import ELBLoggingEnabledAssessment
|
|
115
|
+
from aws_cis_assessment.controls.ig1.control_cloudfront_logging import CloudFrontLoggingEnabledAssessment
|
|
116
|
+
from aws_cis_assessment.controls.ig1.control_waf_logging import WAFLoggingEnabledAssessment
|
|
117
|
+
from aws_cis_assessment.controls.ig1.control_ebs_encryption import EBSEncryptionByDefaultAssessment
|
|
118
|
+
from aws_cis_assessment.controls.ig1.control_rds_encryption import RDSStorageEncryptedAssessment as RDSStorageEncryptedIG1Assessment
|
|
119
|
+
from aws_cis_assessment.controls.ig1.control_efs_encryption import EFSEncryptedCheckAssessment
|
|
120
|
+
from aws_cis_assessment.controls.ig1.control_dynamodb_encryption import DynamoDBTableEncryptedKMSAssessment as DynamoDBTableEncryptedKMSIG1Assessment
|
|
121
|
+
from aws_cis_assessment.controls.ig1.control_s3_encryption import S3DefaultEncryptionKMSAssessment as S3DefaultEncryptionKMSIG1Assessment
|
|
122
|
+
from aws_cis_assessment.controls.ig1.control_patch_management import (
|
|
123
|
+
SSMPatchManagerEnabledAssessment,
|
|
124
|
+
SSMPatchBaselineConfiguredAssessment,
|
|
125
|
+
EC2PatchComplianceStatusAssessment
|
|
126
|
+
)
|
|
127
|
+
from aws_cis_assessment.controls.ig1.control_access_control import (
|
|
128
|
+
SSOEnabledCheckAssessment,
|
|
129
|
+
IdentityCenterConfiguredAssessment
|
|
130
|
+
)
|
|
131
|
+
from aws_cis_assessment.controls.ig1.control_mfa import (
|
|
132
|
+
IAMAdminMFARequiredAssessment,
|
|
133
|
+
CognitoMFAEnabledAssessment,
|
|
134
|
+
VPNMFAEnabledAssessment
|
|
135
|
+
)
|
|
136
|
+
from aws_cis_assessment.controls.ig1.control_tls_ssl import (
|
|
137
|
+
ALBHTTPToHTTPSRedirectionAssessment as ALBHTTPToHTTPSRedirectionIG1Assessment,
|
|
138
|
+
ELBTLSHTTPSListenersOnlyAssessment as ELBTLSHTTPSListenersOnlyIG1Assessment,
|
|
139
|
+
RDSSSLConnectionRequiredAssessment,
|
|
140
|
+
APIGatewaySSLEnabledAssessment as APIGatewaySSLEnabledIG1Assessment,
|
|
141
|
+
RedshiftRequireTLSSSLAssessment as RedshiftRequireTLSSSLIG1Assessment
|
|
142
|
+
)
|
|
143
|
+
from aws_cis_assessment.controls.ig1.control_messaging_encryption import (
|
|
144
|
+
SNSEncryptedKMSAssessment,
|
|
145
|
+
SQSQueueEncryptedAssessment,
|
|
146
|
+
CloudTrailS3DataEventsEnabledAssessment
|
|
147
|
+
)
|
|
148
|
+
from aws_cis_assessment.controls.ig1.control_inventory import (
|
|
149
|
+
SSMInventoryEnabledAssessment,
|
|
150
|
+
ConfigEnabledAllRegionsAssessment,
|
|
151
|
+
AMIInventoryTrackingAssessment,
|
|
152
|
+
LambdaRuntimeInventoryAssessment,
|
|
153
|
+
IAMUserInventoryCheckAssessment
|
|
154
|
+
)
|
|
155
|
+
from aws_cis_assessment.controls.ig1.control_configuration_mgmt import (
|
|
156
|
+
ConfigConformancePackDeployedAssessment,
|
|
157
|
+
SecurityHubStandardsEnabledAssessment,
|
|
158
|
+
AssetTaggingComplianceAssessment,
|
|
159
|
+
InspectorAssessmentEnabledAssessment
|
|
160
|
+
)
|
|
161
|
+
from aws_cis_assessment.controls.ig1.control_version_mgmt import (
|
|
162
|
+
EC2OSVersionSupportedAssessment,
|
|
163
|
+
RDSEngineVersionSupportedAssessment,
|
|
164
|
+
LambdaRuntimeSupportedAssessment
|
|
165
|
+
)
|
|
166
|
+
from aws_cis_assessment.controls.ig1.control_access_asset_mgmt import (
|
|
167
|
+
IAMUserLastAccessCheckAssessment,
|
|
168
|
+
SSMSessionManagerEnabledAssessment,
|
|
169
|
+
UnauthorizedAssetDetectionAssessment
|
|
170
|
+
)
|
|
171
|
+
from aws_cis_assessment.controls.ig1.control_data_classification import (
|
|
172
|
+
DataClassificationTaggingAssessment,
|
|
173
|
+
S3BucketClassificationTagsAssessment
|
|
174
|
+
)
|
|
175
|
+
from aws_cis_assessment.controls.ig1.control_backup_security import (
|
|
176
|
+
BackupVaultEncryptionEnabledAssessment,
|
|
177
|
+
BackupCrossRegionCopyEnabledAssessment,
|
|
178
|
+
BackupVaultLockEnabledAssessment,
|
|
179
|
+
Route53QueryLoggingEnabledAssessment,
|
|
180
|
+
RDSBackupRetentionCheckAssessment
|
|
181
|
+
)
|
|
182
|
+
|
|
109
183
|
from aws_cis_assessment.controls.ig2.control_3_10 import (
|
|
110
184
|
APIGatewaySSLEnabledAssessment, ALBHTTPToHTTPSRedirectionAssessment,
|
|
111
185
|
ELBTLSHTTPSListenersOnlyAssessment, S3BucketSSLRequestsOnlyAssessment,
|
|
@@ -379,13 +453,6 @@ class AssessmentEngine:
|
|
|
379
453
|
'rds-instance-public-access-check': RDSInstancePublicAccessCheckAssessment(),
|
|
380
454
|
'redshift-cluster-public-access-check': RedshiftClusterPublicAccessCheckAssessment(),
|
|
381
455
|
's3-bucket-level-public-access-prohibited': S3BucketLevelPublicAccessProhibitedAssessment(),
|
|
382
|
-
'dms-replication-not-public': DMSReplicationNotPublicAssessment(),
|
|
383
|
-
'elasticsearch-in-vpc-only': ElasticsearchInVPCOnlyAssessment(),
|
|
384
|
-
'ec2-instances-in-vpc': EC2InstancesInVPCAssessment(),
|
|
385
|
-
'emr-master-no-public-ip': EMRMasterNoPublicIPAssessment(),
|
|
386
|
-
'lambda-function-public-access-prohibited': LambdaFunctionPublicAccessProhibitedAssessment(),
|
|
387
|
-
'sagemaker-notebook-no-direct-internet-access': SageMakerNotebookNoDirectInternetAccessAssessment(),
|
|
388
|
-
'subnet-auto-assign-public-ip-disabled': SubnetAutoAssignPublicIPDisabledAssessment(),
|
|
389
456
|
'iam-group-has-users-check': IAMGroupHasUsersCheckAssessment(),
|
|
390
457
|
'iam-policy-no-statements-with-full-access': IAMPolicyNoStatementsWithFullAccessAssessment(),
|
|
391
458
|
'iam-user-no-policies-check': IAMUserNoPoliciesCheckAssessment(),
|
|
@@ -458,6 +525,88 @@ class AssessmentEngine:
|
|
|
458
525
|
|
|
459
526
|
# Instance Optimization
|
|
460
527
|
'ebs-optimized-instance': EBSOptimizedInstanceAssessment(),
|
|
528
|
+
|
|
529
|
+
# Phase 1-4: CIS Controls v8.1 IG1 Expansion (50 new rules)
|
|
530
|
+
# Phase 1 - Quick Wins: Security Services (4 rules)
|
|
531
|
+
'guardduty-enabled-centralized': GuardDutyEnabledAssessment(),
|
|
532
|
+
'inspector-enabled': InspectorEnabledAssessment(),
|
|
533
|
+
'macie-enabled': MacieEnabledAssessment(),
|
|
534
|
+
'iam-access-analyzer-enabled': IAMAccessAnalyzerEnabledAssessment(),
|
|
535
|
+
|
|
536
|
+
# Phase 1 - Quick Wins: Logging (4 rules)
|
|
537
|
+
'vpc-flow-logs-enabled': VPCFlowLogsEnabledAssessment(),
|
|
538
|
+
'elb-logging-enabled': ELBLoggingEnabledAssessment(),
|
|
539
|
+
'cloudfront-accesslogs-enabled': CloudFrontLoggingEnabledAssessment(),
|
|
540
|
+
'wafv2-logging-enabled': WAFLoggingEnabledAssessment(),
|
|
541
|
+
|
|
542
|
+
# Phase 1 - Quick Wins: Encryption (5 rules)
|
|
543
|
+
'ebs-encryption-by-default': EBSEncryptionByDefaultAssessment(),
|
|
544
|
+
'rds-storage-encrypted-ig1': RDSStorageEncryptedIG1Assessment(),
|
|
545
|
+
'efs-encrypted-check-ig1': EFSEncryptedCheckAssessment(),
|
|
546
|
+
'dynamodb-table-encrypted-kms-ig1': DynamoDBTableEncryptedKMSIG1Assessment(),
|
|
547
|
+
's3-default-encryption-kms-ig1': S3DefaultEncryptionKMSIG1Assessment(),
|
|
548
|
+
|
|
549
|
+
# Phase 2 - Core Security: Patch Management (3 rules)
|
|
550
|
+
'ssm-patch-manager-enabled': SSMPatchManagerEnabledAssessment(),
|
|
551
|
+
'ssm-patch-baseline-configured': SSMPatchBaselineConfiguredAssessment(),
|
|
552
|
+
'ec2-patch-compliance-status': EC2PatchComplianceStatusAssessment(),
|
|
553
|
+
|
|
554
|
+
# Phase 2 - Core Security: Access Control (5 rules)
|
|
555
|
+
'sso-enabled-check': SSOEnabledCheckAssessment(),
|
|
556
|
+
'identity-center-configured': IdentityCenterConfiguredAssessment(),
|
|
557
|
+
'iam-admin-mfa-required': IAMAdminMFARequiredAssessment(),
|
|
558
|
+
'cognito-mfa-enabled': CognitoMFAEnabledAssessment(),
|
|
559
|
+
'vpn-mfa-enabled': VPNMFAEnabledAssessment(),
|
|
560
|
+
|
|
561
|
+
# Phase 2 - Core Security: TLS/SSL (5 rules)
|
|
562
|
+
'alb-http-to-https-redirection-check': ALBHTTPToHTTPSRedirectionIG1Assessment(),
|
|
563
|
+
'elb-tls-https-listeners-only-ig1': ELBTLSHTTPSListenersOnlyIG1Assessment(),
|
|
564
|
+
'rds-ssl-connection-required': RDSSSLConnectionRequiredAssessment(),
|
|
565
|
+
'api-gateway-ssl-enabled-ig1': APIGatewaySSLEnabledIG1Assessment(),
|
|
566
|
+
'redshift-require-tls-ssl-ig1': RedshiftRequireTLSSSLIG1Assessment(),
|
|
567
|
+
|
|
568
|
+
# Phase 2 - Core Security: Additional Encryption (3 rules)
|
|
569
|
+
'sns-encrypted-kms': SNSEncryptedKMSAssessment(),
|
|
570
|
+
'sqs-queue-encrypted': SQSQueueEncryptedAssessment(),
|
|
571
|
+
'cloudtrail-s3-dataevents-enabled': CloudTrailS3DataEventsEnabledAssessment(),
|
|
572
|
+
|
|
573
|
+
# Phase 3 - Advanced: Inventory (5 rules)
|
|
574
|
+
'ssm-inventory-enabled': SSMInventoryEnabledAssessment(),
|
|
575
|
+
'config-enabled-all-regions': ConfigEnabledAllRegionsAssessment(),
|
|
576
|
+
'ami-inventory-tracking': AMIInventoryTrackingAssessment(),
|
|
577
|
+
'lambda-runtime-inventory': LambdaRuntimeInventoryAssessment(),
|
|
578
|
+
'iam-user-inventory-check': IAMUserInventoryCheckAssessment(),
|
|
579
|
+
|
|
580
|
+
# Phase 3 - Advanced: Configuration Management (4 rules)
|
|
581
|
+
'config-conformance-pack-deployed': ConfigConformancePackDeployedAssessment(),
|
|
582
|
+
'securityhub-standards-enabled': SecurityHubStandardsEnabledAssessment(),
|
|
583
|
+
'asset-tagging-compliance': AssetTaggingComplianceAssessment(),
|
|
584
|
+
'inspector-assessment-enabled': InspectorAssessmentEnabledAssessment(),
|
|
585
|
+
|
|
586
|
+
# Phase 3 - Advanced: Version Management (3 rules)
|
|
587
|
+
'ec2-os-version-supported': EC2OSVersionSupportedAssessment(),
|
|
588
|
+
'rds-engine-version-supported': RDSEngineVersionSupportedAssessment(),
|
|
589
|
+
'lambda-runtime-supported': LambdaRuntimeSupportedAssessment(),
|
|
590
|
+
|
|
591
|
+
# Phase 3 - Advanced: Access/Asset Management (3 rules)
|
|
592
|
+
'iam-user-last-access-check': IAMUserLastAccessCheckAssessment(),
|
|
593
|
+
'ssm-session-manager-enabled': SSMSessionManagerEnabledAssessment(),
|
|
594
|
+
'unauthorized-asset-detection': UnauthorizedAssetDetectionAssessment(),
|
|
595
|
+
|
|
596
|
+
# Phase 4 - Enhanced: Data Classification (2 rules)
|
|
597
|
+
'data-classification-tagging': DataClassificationTaggingAssessment(),
|
|
598
|
+
's3-bucket-classification-tags': S3BucketClassificationTagsAssessment(),
|
|
599
|
+
|
|
600
|
+
# Phase 4 - Enhanced: Network Security (2 rules)
|
|
601
|
+
'network-firewall-deployed': NetworkFirewallDeployedAssessment(),
|
|
602
|
+
'route53-resolver-firewall-enabled': Route53ResolverFirewallEnabledAssessment(),
|
|
603
|
+
|
|
604
|
+
# Phase 4 - Enhanced: Backup Security (5 rules)
|
|
605
|
+
'backup-vault-encryption-enabled': BackupVaultEncryptionEnabledAssessment(),
|
|
606
|
+
'backup-cross-region-copy-enabled': BackupCrossRegionCopyEnabledAssessment(),
|
|
607
|
+
'backup-vault-lock-enabled': BackupVaultLockEnabledAssessment(),
|
|
608
|
+
'route53-query-logging-enabled': Route53QueryLoggingEnabledAssessment(),
|
|
609
|
+
'rds-backup-retention-check': RDSBackupRetentionCheckAssessment(),
|
|
461
610
|
},
|
|
462
611
|
'IG2': {
|
|
463
612
|
# Control 3.10 - Encryption in Transit
|
|
@@ -69,24 +69,32 @@ class AWSClientFactory:
|
|
|
69
69
|
logger.error(f"Failed to initialize AWS session: {e}")
|
|
70
70
|
raise
|
|
71
71
|
|
|
72
|
-
def get_client(self, service_name: str, region: Optional[str] = None
|
|
72
|
+
def get_client(self, service_name: str, region: Optional[str] = None,
|
|
73
|
+
allow_global_region: bool = False) -> boto3.client:
|
|
73
74
|
"""Get AWS service client for specified service and region.
|
|
74
75
|
|
|
75
76
|
Args:
|
|
76
77
|
service_name: AWS service name (e.g., 'ec2', 'iam', 's3')
|
|
77
78
|
region: AWS region. If None, uses first region from regions list.
|
|
79
|
+
allow_global_region: If True, allows us-east-1 even if not in regions list.
|
|
80
|
+
This is used for account-level and global resources that
|
|
81
|
+
must be evaluated in us-east-1 regardless of configured regions.
|
|
78
82
|
|
|
79
83
|
Returns:
|
|
80
84
|
Boto3 client for the specified service
|
|
81
85
|
|
|
82
86
|
Raises:
|
|
83
|
-
ValueError: If region is not in supported regions list
|
|
87
|
+
ValueError: If region is not in supported regions list (unless allow_global_region=True for us-east-1)
|
|
84
88
|
ClientError: If client creation fails
|
|
85
89
|
"""
|
|
86
90
|
if region is None:
|
|
87
91
|
region = self.regions[0]
|
|
88
92
|
|
|
89
|
-
if
|
|
93
|
+
# Allow us-east-1 for global/account-level resources even if not in regions list
|
|
94
|
+
if allow_global_region and region == 'us-east-1':
|
|
95
|
+
# us-east-1 is allowed for global resources
|
|
96
|
+
logger.debug(f"Allowing global region us-east-1 for {service_name} (not in configured regions)")
|
|
97
|
+
elif region not in self.regions:
|
|
90
98
|
raise ValueError(f"Region {region} not in supported regions: {self.regions}")
|
|
91
99
|
|
|
92
100
|
# Create cache key
|
|
@@ -283,8 +291,12 @@ class AWSClientFactory:
|
|
|
283
291
|
raise
|
|
284
292
|
|
|
285
293
|
except Exception as e:
|
|
286
|
-
#
|
|
287
|
-
|
|
294
|
+
# Log connection errors at DEBUG level (expected for cross-region S3 buckets)
|
|
295
|
+
error_str = str(e)
|
|
296
|
+
if "Could not connect to the endpoint URL" in error_str or "Connection" in error_str:
|
|
297
|
+
logger.debug(f"Connection error in AWS API call (may be cross-region resource): {e}")
|
|
298
|
+
else:
|
|
299
|
+
logger.error(f"Non-retryable error in AWS API call: {e}")
|
|
288
300
|
raise
|
|
289
301
|
|
|
290
302
|
# This should never be reached due to the raise in the loop
|
|
@@ -155,6 +155,24 @@ class RemediationGuidance:
|
|
|
155
155
|
raise ValueError(f"Invalid priority: {self.priority}")
|
|
156
156
|
|
|
157
157
|
|
|
158
|
+
@dataclass
|
|
159
|
+
class CoverageMetrics:
|
|
160
|
+
"""CIS Controls coverage metrics for Implementation Groups."""
|
|
161
|
+
implementation_group: str
|
|
162
|
+
total_safeguards: int
|
|
163
|
+
covered_safeguards: int
|
|
164
|
+
coverage_percentage: float
|
|
165
|
+
implemented_rules: int
|
|
166
|
+
safeguard_details: Dict[str, Any] = field(default_factory=dict)
|
|
167
|
+
|
|
168
|
+
def __post_init__(self):
|
|
169
|
+
"""Calculate coverage percentage if not provided."""
|
|
170
|
+
if self.total_safeguards > 0:
|
|
171
|
+
calculated_percentage = (self.covered_safeguards / self.total_safeguards) * 100
|
|
172
|
+
if abs(self.coverage_percentage - calculated_percentage) > 0.01:
|
|
173
|
+
self.coverage_percentage = calculated_percentage
|
|
174
|
+
|
|
175
|
+
|
|
158
176
|
@dataclass
|
|
159
177
|
class ComplianceSummary:
|
|
160
178
|
"""Executive summary of compliance assessment."""
|
|
@@ -164,4 +182,5 @@ class ComplianceSummary:
|
|
|
164
182
|
ig3_compliance_percentage: float
|
|
165
183
|
top_risk_areas: List[str] = field(default_factory=list)
|
|
166
184
|
remediation_priorities: List[RemediationGuidance] = field(default_factory=list)
|
|
167
|
-
compliance_trend: Optional[str] = None
|
|
185
|
+
compliance_trend: Optional[str] = None
|
|
186
|
+
coverage_metrics: Dict[str, CoverageMetrics] = field(default_factory=dict)
|
|
@@ -245,6 +245,9 @@ class ScoringEngine:
|
|
|
245
245
|
# Determine compliance trend (would require historical data)
|
|
246
246
|
compliance_trend = self._determine_compliance_trend(assessment_result)
|
|
247
247
|
|
|
248
|
+
# Calculate coverage metrics for each IG
|
|
249
|
+
coverage_metrics = self._calculate_coverage_metrics(assessment_result.ig_scores)
|
|
250
|
+
|
|
248
251
|
return ComplianceSummary(
|
|
249
252
|
overall_compliance_percentage=assessment_result.overall_score,
|
|
250
253
|
ig1_compliance_percentage=ig1_compliance,
|
|
@@ -252,7 +255,8 @@ class ScoringEngine:
|
|
|
252
255
|
ig3_compliance_percentage=ig3_compliance,
|
|
253
256
|
top_risk_areas=top_risk_areas,
|
|
254
257
|
remediation_priorities=remediation_priorities,
|
|
255
|
-
compliance_trend=compliance_trend
|
|
258
|
+
compliance_trend=compliance_trend,
|
|
259
|
+
coverage_metrics=coverage_metrics
|
|
256
260
|
)
|
|
257
261
|
|
|
258
262
|
def _identify_risk_areas(self, ig_scores: Dict[str, IGScore],
|
|
@@ -436,6 +440,103 @@ class ScoringEngine:
|
|
|
436
440
|
# For now, return None to indicate no trend data available
|
|
437
441
|
return None
|
|
438
442
|
|
|
443
|
+
def _calculate_coverage_metrics(self, ig_scores: Dict[str, IGScore]) -> Dict[str, 'CoverageMetrics']:
|
|
444
|
+
"""Calculate CIS Controls coverage metrics for each Implementation Group.
|
|
445
|
+
|
|
446
|
+
Based on CIS Controls v8.1 safeguard counts:
|
|
447
|
+
- IG1: 56 total safeguards
|
|
448
|
+
- IG2: 74 total safeguards (cumulative, includes IG1)
|
|
449
|
+
- IG3: 153 total safeguards (cumulative, includes IG1 and IG2)
|
|
450
|
+
|
|
451
|
+
Args:
|
|
452
|
+
ig_scores: Dictionary of IG scores with control assessments
|
|
453
|
+
|
|
454
|
+
Returns:
|
|
455
|
+
Dictionary mapping IG names to CoverageMetrics objects
|
|
456
|
+
"""
|
|
457
|
+
from aws_cis_assessment.core.models import CoverageMetrics
|
|
458
|
+
|
|
459
|
+
# CIS Controls v8.1 safeguard counts per IG
|
|
460
|
+
total_safeguards = {
|
|
461
|
+
'IG1': 56,
|
|
462
|
+
'IG2': 74, # Cumulative
|
|
463
|
+
'IG3': 153 # Cumulative
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
# Dynamically count implemented rules from actual assessment data
|
|
467
|
+
# This replaces hardcoded values to ensure accuracy
|
|
468
|
+
safeguard_coverage = {}
|
|
469
|
+
|
|
470
|
+
for ig_name, ig_score in ig_scores.items():
|
|
471
|
+
if ig_name not in total_safeguards:
|
|
472
|
+
continue
|
|
473
|
+
|
|
474
|
+
# Count unique config rules across all controls in this IG
|
|
475
|
+
unique_rules = set()
|
|
476
|
+
for control_score in ig_score.control_scores.values():
|
|
477
|
+
# Each control may evaluate multiple config rules
|
|
478
|
+
if hasattr(control_score, 'config_rules_evaluated'):
|
|
479
|
+
unique_rules.update(control_score.config_rules_evaluated)
|
|
480
|
+
|
|
481
|
+
# Store the actual count of rules
|
|
482
|
+
safeguard_coverage[ig_name] = {
|
|
483
|
+
'covered': 42 if ig_name == 'IG1' else (30 if ig_name == 'IG2' else 15), # Safeguards covered (estimated)
|
|
484
|
+
'rules': len(unique_rules) # Actual rule count from assessment
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
coverage_metrics = {}
|
|
488
|
+
|
|
489
|
+
for ig_name, ig_score in ig_scores.items():
|
|
490
|
+
if ig_name not in total_safeguards:
|
|
491
|
+
continue
|
|
492
|
+
|
|
493
|
+
total = total_safeguards[ig_name]
|
|
494
|
+
coverage_data = safeguard_coverage.get(ig_name, {'covered': 0, 'rules': 0})
|
|
495
|
+
covered = coverage_data['covered']
|
|
496
|
+
rules = coverage_data['rules']
|
|
497
|
+
|
|
498
|
+
# Calculate coverage percentage
|
|
499
|
+
coverage_pct = (covered / total * 100) if total > 0 else 0.0
|
|
500
|
+
|
|
501
|
+
# Build safeguard details
|
|
502
|
+
safeguard_details = {
|
|
503
|
+
'total_safeguards': total,
|
|
504
|
+
'covered_safeguards': covered,
|
|
505
|
+
'implemented_rules': rules,
|
|
506
|
+
'controls_assessed': len(ig_score.control_scores),
|
|
507
|
+
'coverage_description': self._get_coverage_description(ig_name, coverage_pct)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
coverage_metrics[ig_name] = CoverageMetrics(
|
|
511
|
+
implementation_group=ig_name,
|
|
512
|
+
total_safeguards=total,
|
|
513
|
+
covered_safeguards=covered,
|
|
514
|
+
coverage_percentage=coverage_pct,
|
|
515
|
+
implemented_rules=rules,
|
|
516
|
+
safeguard_details=safeguard_details
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
return coverage_metrics
|
|
520
|
+
|
|
521
|
+
def _get_coverage_description(self, ig_name: str, coverage_pct: float) -> str:
|
|
522
|
+
"""Get human-readable coverage description.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
ig_name: Implementation Group name
|
|
526
|
+
coverage_pct: Coverage percentage
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
Description of coverage level
|
|
530
|
+
"""
|
|
531
|
+
if coverage_pct >= 75:
|
|
532
|
+
return f"Comprehensive coverage of {ig_name} safeguards"
|
|
533
|
+
elif coverage_pct >= 50:
|
|
534
|
+
return f"Good coverage of {ig_name} safeguards"
|
|
535
|
+
elif coverage_pct >= 25:
|
|
536
|
+
return f"Moderate coverage of {ig_name} safeguards"
|
|
537
|
+
else:
|
|
538
|
+
return f"Limited coverage of {ig_name} safeguards"
|
|
539
|
+
|
|
439
540
|
def calculate_resource_count_by_status(self, ig_scores: Dict[str, IGScore]) -> Dict[str, int]:
|
|
440
541
|
"""Calculate resource counts by compliance status across all IGs.
|
|
441
542
|
|