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
docs/assessment-logic.md
ADDED
|
@@ -0,0 +1,766 @@
|
|
|
1
|
+
# Assessment Logic Documentation
|
|
2
|
+
|
|
3
|
+
This document provides detailed information about the assessment logic used by the AWS CIS Controls Compliance Assessment Framework - a production-ready, enterprise-grade solution with complete CIS Controls coverage.
|
|
4
|
+
|
|
5
|
+
## Production Framework Overview
|
|
6
|
+
|
|
7
|
+
**✅ Complete Implementation Status**
|
|
8
|
+
- 136 AWS Config rules implemented (131 CIS Controls + 5 bonus)
|
|
9
|
+
- 100% coverage across all Implementation Groups (IG1, IG2, IG3)
|
|
10
|
+
- Production-tested with enterprise-grade error handling
|
|
11
|
+
- Optimized for large-scale enterprise deployments
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
1. [Overview](#overview)
|
|
16
|
+
2. [Assessment Framework](#assessment-framework)
|
|
17
|
+
3. [Resource Discovery](#resource-discovery)
|
|
18
|
+
4. [Compliance Evaluation](#compliance-evaluation)
|
|
19
|
+
5. [Scoring Methodology](#scoring-methodology)
|
|
20
|
+
6. [Error Handling](#error-handling)
|
|
21
|
+
7. [Control-Specific Logic](#control-specific-logic)
|
|
22
|
+
8. [Performance Optimizations](#performance-optimizations)
|
|
23
|
+
|
|
24
|
+
## Overview
|
|
25
|
+
|
|
26
|
+
The assessment tool evaluates AWS account configurations against CIS Controls using the same logic as AWS Config rules, but without requiring AWS Config to be enabled. Each assessment follows a standardized process while implementing control-specific evaluation logic.
|
|
27
|
+
|
|
28
|
+
**Framework Scope**: 136 implemented rules covering all CIS Controls requirements plus 5 bonus security enhancements for additional value.
|
|
29
|
+
|
|
30
|
+
### Key Principles
|
|
31
|
+
|
|
32
|
+
1. **Config Rule Fidelity**: Assessment logic mirrors AWS Config rule specifications exactly
|
|
33
|
+
2. **Resource Coverage**: All applicable AWS resource types are evaluated
|
|
34
|
+
3. **Regional Scope**: Assessments are performed across all specified regions
|
|
35
|
+
4. **Error Resilience**: Graceful handling of API errors and service unavailability
|
|
36
|
+
5. **Performance**: Optimized for large-scale enterprise environments
|
|
37
|
+
|
|
38
|
+
## Assessment Framework
|
|
39
|
+
|
|
40
|
+
### Base Assessment Pattern
|
|
41
|
+
|
|
42
|
+
All assessments follow this standardized pattern:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
class BaseConfigRuleAssessment:
|
|
46
|
+
def evaluate_compliance(self, aws_factory, region):
|
|
47
|
+
"""Main assessment entry point."""
|
|
48
|
+
all_results = []
|
|
49
|
+
|
|
50
|
+
for resource_type in self.resource_types:
|
|
51
|
+
# 1. Discover resources
|
|
52
|
+
resources = self._get_resources(aws_factory, resource_type, region)
|
|
53
|
+
|
|
54
|
+
# 2. Evaluate each resource
|
|
55
|
+
for resource in resources:
|
|
56
|
+
result = self._evaluate_resource_compliance(resource, aws_factory)
|
|
57
|
+
all_results.append(result)
|
|
58
|
+
|
|
59
|
+
return all_results
|
|
60
|
+
|
|
61
|
+
def _get_resources(self, aws_factory, resource_type, region):
|
|
62
|
+
"""Discover resources of specified type in region."""
|
|
63
|
+
# Implementation varies by resource type
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
def _evaluate_resource_compliance(self, resource, aws_factory):
|
|
67
|
+
"""Evaluate compliance for individual resource."""
|
|
68
|
+
# Implementation varies by control logic
|
|
69
|
+
pass
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Assessment Lifecycle
|
|
73
|
+
|
|
74
|
+
1. **Initialization**: Load configuration and validate parameters
|
|
75
|
+
2. **Resource Discovery**: Find all applicable resources in target regions
|
|
76
|
+
3. **Compliance Evaluation**: Apply control-specific logic to each resource
|
|
77
|
+
4. **Result Aggregation**: Collect and format compliance results
|
|
78
|
+
5. **Scoring Calculation**: Calculate compliance percentages
|
|
79
|
+
6. **Report Generation**: Generate output in requested formats
|
|
80
|
+
|
|
81
|
+
## Resource Discovery
|
|
82
|
+
|
|
83
|
+
### Discovery Strategies
|
|
84
|
+
|
|
85
|
+
Different AWS services require different discovery approaches:
|
|
86
|
+
|
|
87
|
+
#### EC2 Resources
|
|
88
|
+
```python
|
|
89
|
+
def discover_ec2_instances(self, ec2_client, region):
|
|
90
|
+
"""Discover EC2 instances using describe_instances."""
|
|
91
|
+
try:
|
|
92
|
+
paginator = ec2_client.get_paginator('describe_instances')
|
|
93
|
+
|
|
94
|
+
instances = []
|
|
95
|
+
for page in paginator.paginate():
|
|
96
|
+
for reservation in page['Reservations']:
|
|
97
|
+
for instance in reservation['Instances']:
|
|
98
|
+
# Filter out terminated instances
|
|
99
|
+
if instance['State']['Name'] != 'terminated':
|
|
100
|
+
instances.append({
|
|
101
|
+
'InstanceId': instance['InstanceId'],
|
|
102
|
+
'InstanceType': instance['InstanceType'],
|
|
103
|
+
'State': instance['State']['Name'],
|
|
104
|
+
'Region': region,
|
|
105
|
+
'LaunchTime': instance['LaunchTime'],
|
|
106
|
+
'SecurityGroups': instance.get('SecurityGroups', []),
|
|
107
|
+
'IamInstanceProfile': instance.get('IamInstanceProfile'),
|
|
108
|
+
'Monitoring': instance.get('Monitoring', {}),
|
|
109
|
+
'Tags': instance.get('Tags', [])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return instances
|
|
113
|
+
|
|
114
|
+
except ClientError as e:
|
|
115
|
+
if e.response['Error']['Code'] == 'UnauthorizedOperation':
|
|
116
|
+
self.logger.warning(f"Insufficient permissions for EC2 in {region}")
|
|
117
|
+
return []
|
|
118
|
+
raise
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### S3 Resources
|
|
122
|
+
```python
|
|
123
|
+
def discover_s3_buckets(self, s3_client, region):
|
|
124
|
+
"""Discover S3 buckets in specific region."""
|
|
125
|
+
try:
|
|
126
|
+
# List all buckets (global operation)
|
|
127
|
+
response = s3_client.list_buckets()
|
|
128
|
+
|
|
129
|
+
regional_buckets = []
|
|
130
|
+
for bucket in response['Buckets']:
|
|
131
|
+
try:
|
|
132
|
+
# Get bucket region
|
|
133
|
+
bucket_region = s3_client.get_bucket_location(
|
|
134
|
+
Bucket=bucket['Name']
|
|
135
|
+
)['LocationConstraint']
|
|
136
|
+
|
|
137
|
+
# Handle us-east-1 special case
|
|
138
|
+
if bucket_region is None:
|
|
139
|
+
bucket_region = 'us-east-1'
|
|
140
|
+
|
|
141
|
+
if bucket_region == region:
|
|
142
|
+
regional_buckets.append({
|
|
143
|
+
'Name': bucket['Name'],
|
|
144
|
+
'CreationDate': bucket['CreationDate'],
|
|
145
|
+
'Region': region
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
except ClientError as e:
|
|
149
|
+
# Skip buckets we can't access
|
|
150
|
+
if e.response['Error']['Code'] in ['AccessDenied', 'NoSuchBucket']:
|
|
151
|
+
continue
|
|
152
|
+
raise
|
|
153
|
+
|
|
154
|
+
return regional_buckets
|
|
155
|
+
|
|
156
|
+
except ClientError as e:
|
|
157
|
+
if e.response['Error']['Code'] == 'AccessDenied':
|
|
158
|
+
self.logger.warning(f"Insufficient permissions for S3")
|
|
159
|
+
return []
|
|
160
|
+
raise
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
#### IAM Resources
|
|
164
|
+
```python
|
|
165
|
+
def discover_iam_users(self, iam_client):
|
|
166
|
+
"""Discover IAM users (global service)."""
|
|
167
|
+
try:
|
|
168
|
+
paginator = iam_client.get_paginator('list_users')
|
|
169
|
+
|
|
170
|
+
users = []
|
|
171
|
+
for page in paginator.paginate():
|
|
172
|
+
for user in page['Users']:
|
|
173
|
+
users.append({
|
|
174
|
+
'UserName': user['UserName'],
|
|
175
|
+
'UserId': user['UserId'],
|
|
176
|
+
'Arn': user['Arn'],
|
|
177
|
+
'CreateDate': user['CreateDate'],
|
|
178
|
+
'PasswordLastUsed': user.get('PasswordLastUsed'),
|
|
179
|
+
'Tags': user.get('Tags', [])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
return users
|
|
183
|
+
|
|
184
|
+
except ClientError as e:
|
|
185
|
+
if e.response['Error']['Code'] == 'AccessDenied':
|
|
186
|
+
self.logger.warning("Insufficient permissions for IAM")
|
|
187
|
+
return []
|
|
188
|
+
raise
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Pagination Handling
|
|
192
|
+
|
|
193
|
+
All discovery operations use proper pagination to handle large resource sets:
|
|
194
|
+
|
|
195
|
+
```python
|
|
196
|
+
def paginated_discovery(self, client, operation_name, **kwargs):
|
|
197
|
+
"""Generic paginated resource discovery."""
|
|
198
|
+
try:
|
|
199
|
+
if client.can_paginate(operation_name):
|
|
200
|
+
paginator = client.get_paginator(operation_name)
|
|
201
|
+
for page in paginator.paginate(**kwargs):
|
|
202
|
+
yield page
|
|
203
|
+
else:
|
|
204
|
+
# Single page operation
|
|
205
|
+
response = getattr(client, operation_name)(**kwargs)
|
|
206
|
+
yield response
|
|
207
|
+
|
|
208
|
+
except ClientError as e:
|
|
209
|
+
self.logger.error(f"Failed to paginate {operation_name}: {e}")
|
|
210
|
+
raise
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
## Compliance Evaluation
|
|
214
|
+
|
|
215
|
+
### Evaluation Patterns
|
|
216
|
+
|
|
217
|
+
Different controls use different evaluation patterns:
|
|
218
|
+
|
|
219
|
+
#### Binary Compliance
|
|
220
|
+
Simple yes/no compliance checks:
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
def evaluate_eip_attached(self, resource, aws_factory):
|
|
224
|
+
"""Evaluate if EIP is attached to an instance or ENI."""
|
|
225
|
+
eip_allocation_id = resource['AllocationId']
|
|
226
|
+
region = resource['Region']
|
|
227
|
+
|
|
228
|
+
# Check if EIP has InstanceId or NetworkInterfaceId
|
|
229
|
+
if resource.get('InstanceId') or resource.get('NetworkInterfaceId'):
|
|
230
|
+
return ComplianceResult(
|
|
231
|
+
resource_id=eip_allocation_id,
|
|
232
|
+
resource_type="AWS::EC2::EIP",
|
|
233
|
+
compliance_status="COMPLIANT",
|
|
234
|
+
evaluation_reason="EIP is attached to an instance or ENI",
|
|
235
|
+
config_rule_name="eip-attached",
|
|
236
|
+
region=region,
|
|
237
|
+
timestamp=datetime.now()
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
return ComplianceResult(
|
|
241
|
+
resource_id=eip_allocation_id,
|
|
242
|
+
resource_type="AWS::EC2::EIP",
|
|
243
|
+
compliance_status="NON_COMPLIANT",
|
|
244
|
+
evaluation_reason="EIP is not attached to any instance or ENI",
|
|
245
|
+
config_rule_name="eip-attached",
|
|
246
|
+
region=region,
|
|
247
|
+
timestamp=datetime.now(),
|
|
248
|
+
remediation_guidance="Attach the EIP to an EC2 instance or release it to avoid charges"
|
|
249
|
+
)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### Parameter-Based Compliance
|
|
253
|
+
Compliance based on configuration parameters:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
def evaluate_iam_password_policy(self, resource, aws_factory):
|
|
257
|
+
"""Evaluate IAM password policy against parameters."""
|
|
258
|
+
policy = resource['PasswordPolicy']
|
|
259
|
+
|
|
260
|
+
# Check all required parameters
|
|
261
|
+
compliance_issues = []
|
|
262
|
+
|
|
263
|
+
if not policy.get('RequireUppercaseCharacters', False):
|
|
264
|
+
compliance_issues.append("uppercase characters not required")
|
|
265
|
+
|
|
266
|
+
if not policy.get('RequireLowercaseCharacters', False):
|
|
267
|
+
compliance_issues.append("lowercase characters not required")
|
|
268
|
+
|
|
269
|
+
if not policy.get('RequireNumbers', False):
|
|
270
|
+
compliance_issues.append("numbers not required")
|
|
271
|
+
|
|
272
|
+
if not policy.get('RequireSymbols', False):
|
|
273
|
+
compliance_issues.append("symbols not required")
|
|
274
|
+
|
|
275
|
+
min_length = policy.get('MinimumPasswordLength', 0)
|
|
276
|
+
if min_length < self.parameters.get('MinimumPasswordLength', 14):
|
|
277
|
+
compliance_issues.append(f"minimum length {min_length} is too short")
|
|
278
|
+
|
|
279
|
+
if compliance_issues:
|
|
280
|
+
return ComplianceResult(
|
|
281
|
+
resource_id="account-password-policy",
|
|
282
|
+
resource_type="AWS::IAM::AccountPasswordPolicy",
|
|
283
|
+
compliance_status="NON_COMPLIANT",
|
|
284
|
+
evaluation_reason=f"Password policy issues: {', '.join(compliance_issues)}",
|
|
285
|
+
config_rule_name="iam-password-policy",
|
|
286
|
+
region="global",
|
|
287
|
+
timestamp=datetime.now(),
|
|
288
|
+
remediation_guidance="Update IAM password policy to meet security requirements"
|
|
289
|
+
)
|
|
290
|
+
else:
|
|
291
|
+
return ComplianceResult(
|
|
292
|
+
resource_id="account-password-policy",
|
|
293
|
+
resource_type="AWS::IAM::AccountPasswordPolicy",
|
|
294
|
+
compliance_status="COMPLIANT",
|
|
295
|
+
evaluation_reason="Password policy meets all requirements",
|
|
296
|
+
config_rule_name="iam-password-policy",
|
|
297
|
+
region="global",
|
|
298
|
+
timestamp=datetime.now()
|
|
299
|
+
)
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
#### Multi-Step Evaluation
|
|
303
|
+
Complex evaluations requiring multiple API calls:
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
def evaluate_s3_bucket_encryption(self, resource, aws_factory):
|
|
307
|
+
"""Evaluate S3 bucket encryption configuration."""
|
|
308
|
+
bucket_name = resource['Name']
|
|
309
|
+
region = resource['Region']
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
s3_client = aws_factory.get_client('s3', region)
|
|
313
|
+
|
|
314
|
+
# Step 1: Check bucket encryption
|
|
315
|
+
try:
|
|
316
|
+
encryption_response = s3_client.get_bucket_encryption(Bucket=bucket_name)
|
|
317
|
+
encryption_config = encryption_response.get('ServerSideEncryptionConfiguration', {})
|
|
318
|
+
rules = encryption_config.get('Rules', [])
|
|
319
|
+
|
|
320
|
+
if not rules:
|
|
321
|
+
return self._create_non_compliant_result(
|
|
322
|
+
bucket_name, region, "No encryption rules configured"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Step 2: Validate encryption algorithm
|
|
326
|
+
for rule in rules:
|
|
327
|
+
sse_config = rule.get('ApplyServerSideEncryptionByDefault', {})
|
|
328
|
+
algorithm = sse_config.get('SSEAlgorithm')
|
|
329
|
+
|
|
330
|
+
if algorithm not in ['AES256', 'aws:kms']:
|
|
331
|
+
return self._create_non_compliant_result(
|
|
332
|
+
bucket_name, region, f"Invalid encryption algorithm: {algorithm}"
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
# Step 3: For KMS, check key configuration
|
|
336
|
+
if algorithm == 'aws:kms':
|
|
337
|
+
kms_key_id = sse_config.get('KMSMasterKeyID')
|
|
338
|
+
if not kms_key_id:
|
|
339
|
+
return self._create_non_compliant_result(
|
|
340
|
+
bucket_name, region, "KMS encryption without key ID"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
return ComplianceResult(
|
|
344
|
+
resource_id=bucket_name,
|
|
345
|
+
resource_type="AWS::S3::Bucket",
|
|
346
|
+
compliance_status="COMPLIANT",
|
|
347
|
+
evaluation_reason="Bucket has proper encryption configuration",
|
|
348
|
+
config_rule_name="s3-bucket-server-side-encryption-enabled",
|
|
349
|
+
region=region,
|
|
350
|
+
timestamp=datetime.now()
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
except ClientError as e:
|
|
354
|
+
if e.response['Error']['Code'] == 'ServerSideEncryptionConfigurationNotFoundError':
|
|
355
|
+
return self._create_non_compliant_result(
|
|
356
|
+
bucket_name, region, "No server-side encryption configuration"
|
|
357
|
+
)
|
|
358
|
+
raise
|
|
359
|
+
|
|
360
|
+
except Exception as e:
|
|
361
|
+
return self._create_error_result(bucket_name, region, str(e))
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
## Scoring Methodology
|
|
365
|
+
|
|
366
|
+
### Control-Level Scoring
|
|
367
|
+
|
|
368
|
+
Each control's compliance score is calculated as:
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
def calculate_control_score(self, compliance_results):
|
|
372
|
+
"""Calculate compliance score for a control."""
|
|
373
|
+
if not compliance_results:
|
|
374
|
+
return ControlScore(
|
|
375
|
+
control_id=self.control_id,
|
|
376
|
+
title=self.title,
|
|
377
|
+
implementation_group=self.implementation_group,
|
|
378
|
+
total_resources=0,
|
|
379
|
+
compliant_resources=0,
|
|
380
|
+
compliance_percentage=0.0,
|
|
381
|
+
config_rules_evaluated=[],
|
|
382
|
+
findings=[]
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Count compliant resources
|
|
386
|
+
compliant_count = sum(1 for result in compliance_results
|
|
387
|
+
if result.compliance_status == 'COMPLIANT')
|
|
388
|
+
|
|
389
|
+
# Count total evaluable resources (exclude errors and not applicable)
|
|
390
|
+
evaluable_results = [result for result in compliance_results
|
|
391
|
+
if result.compliance_status in ['COMPLIANT', 'NON_COMPLIANT']]
|
|
392
|
+
|
|
393
|
+
total_count = len(evaluable_results)
|
|
394
|
+
|
|
395
|
+
if total_count == 0:
|
|
396
|
+
compliance_percentage = 0.0
|
|
397
|
+
else:
|
|
398
|
+
compliance_percentage = (compliant_count / total_count) * 100
|
|
399
|
+
|
|
400
|
+
return ControlScore(
|
|
401
|
+
control_id=self.control_id,
|
|
402
|
+
title=self.title,
|
|
403
|
+
implementation_group=self.implementation_group,
|
|
404
|
+
total_resources=total_count,
|
|
405
|
+
compliant_resources=compliant_count,
|
|
406
|
+
compliance_percentage=compliance_percentage,
|
|
407
|
+
config_rules_evaluated=list(set(result.config_rule_name for result in compliance_results)),
|
|
408
|
+
findings=evaluable_results
|
|
409
|
+
)
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Implementation Group Scoring
|
|
413
|
+
|
|
414
|
+
IG scores are calculated as weighted averages of control scores:
|
|
415
|
+
|
|
416
|
+
```python
|
|
417
|
+
def calculate_ig_score(self, control_scores):
|
|
418
|
+
"""Calculate Implementation Group compliance score."""
|
|
419
|
+
if not control_scores:
|
|
420
|
+
return IGScore(
|
|
421
|
+
implementation_group=self.ig_name,
|
|
422
|
+
total_controls=0,
|
|
423
|
+
compliant_controls=0,
|
|
424
|
+
compliance_percentage=0.0,
|
|
425
|
+
control_scores={}
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
# Calculate weighted average
|
|
429
|
+
total_weight = 0
|
|
430
|
+
weighted_sum = 0
|
|
431
|
+
|
|
432
|
+
for control_id, control_score in control_scores.items():
|
|
433
|
+
weight = control_score.weight
|
|
434
|
+
total_weight += weight
|
|
435
|
+
weighted_sum += control_score.compliance_percentage * weight
|
|
436
|
+
|
|
437
|
+
if total_weight == 0:
|
|
438
|
+
overall_percentage = 0.0
|
|
439
|
+
else:
|
|
440
|
+
overall_percentage = weighted_sum / total_weight
|
|
441
|
+
|
|
442
|
+
# Count fully compliant controls (100% compliance)
|
|
443
|
+
compliant_controls = sum(1 for score in control_scores.values()
|
|
444
|
+
if score.compliance_percentage == 100.0)
|
|
445
|
+
|
|
446
|
+
return IGScore(
|
|
447
|
+
implementation_group=self.ig_name,
|
|
448
|
+
total_controls=len(control_scores),
|
|
449
|
+
compliant_controls=compliant_controls,
|
|
450
|
+
compliance_percentage=overall_percentage,
|
|
451
|
+
control_scores=control_scores
|
|
452
|
+
)
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Overall Scoring
|
|
456
|
+
|
|
457
|
+
Overall compliance is calculated across all Implementation Groups:
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
def calculate_overall_score(self, ig_scores):
|
|
461
|
+
"""Calculate overall compliance score."""
|
|
462
|
+
if not ig_scores:
|
|
463
|
+
return 0.0
|
|
464
|
+
|
|
465
|
+
# Weight Implementation Groups
|
|
466
|
+
ig_weights = {
|
|
467
|
+
'IG1': 0.5, # 50% weight for essential controls
|
|
468
|
+
'IG2': 0.3, # 30% weight for enhanced controls
|
|
469
|
+
'IG3': 0.2 # 20% weight for advanced controls
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
total_weight = 0
|
|
473
|
+
weighted_sum = 0
|
|
474
|
+
|
|
475
|
+
for ig_name, ig_score in ig_scores.items():
|
|
476
|
+
weight = ig_weights.get(ig_name, 1.0)
|
|
477
|
+
total_weight += weight
|
|
478
|
+
weighted_sum += ig_score.compliance_percentage * weight
|
|
479
|
+
|
|
480
|
+
if total_weight == 0:
|
|
481
|
+
return 0.0
|
|
482
|
+
|
|
483
|
+
return weighted_sum / total_weight
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
## Error Handling
|
|
487
|
+
|
|
488
|
+
### Error Categories
|
|
489
|
+
|
|
490
|
+
The assessment tool handles various error conditions:
|
|
491
|
+
|
|
492
|
+
#### Permission Errors
|
|
493
|
+
```python
|
|
494
|
+
def handle_permission_error(self, error, resource_id, region):
|
|
495
|
+
"""Handle AWS permission errors."""
|
|
496
|
+
return ComplianceResult(
|
|
497
|
+
resource_id=resource_id,
|
|
498
|
+
resource_type=self.resource_type,
|
|
499
|
+
compliance_status="INSUFFICIENT_PERMISSIONS",
|
|
500
|
+
evaluation_reason=f"Insufficient permissions: {error.response['Error']['Code']}",
|
|
501
|
+
config_rule_name=self.rule_name,
|
|
502
|
+
region=region,
|
|
503
|
+
timestamp=datetime.now(),
|
|
504
|
+
remediation_guidance="Grant necessary IAM permissions for assessment"
|
|
505
|
+
)
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
#### Service Unavailable
|
|
509
|
+
```python
|
|
510
|
+
def handle_service_error(self, error, resource_id, region):
|
|
511
|
+
"""Handle AWS service unavailability."""
|
|
512
|
+
return ComplianceResult(
|
|
513
|
+
resource_id=resource_id,
|
|
514
|
+
resource_type=self.resource_type,
|
|
515
|
+
compliance_status="ERROR",
|
|
516
|
+
evaluation_reason=f"Service error: {error.response['Error']['Code']}",
|
|
517
|
+
config_rule_name=self.rule_name,
|
|
518
|
+
region=region,
|
|
519
|
+
timestamp=datetime.now(),
|
|
520
|
+
remediation_guidance="Retry assessment when service is available"
|
|
521
|
+
)
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
#### Resource Not Found
|
|
525
|
+
```python
|
|
526
|
+
def handle_not_found_error(self, error, resource_id, region):
|
|
527
|
+
"""Handle resource not found errors."""
|
|
528
|
+
return ComplianceResult(
|
|
529
|
+
resource_id=resource_id,
|
|
530
|
+
resource_type=self.resource_type,
|
|
531
|
+
compliance_status="NOT_APPLICABLE",
|
|
532
|
+
evaluation_reason="Resource not found or not applicable",
|
|
533
|
+
config_rule_name=self.rule_name,
|
|
534
|
+
region=region,
|
|
535
|
+
timestamp=datetime.now()
|
|
536
|
+
)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Retry Logic
|
|
540
|
+
|
|
541
|
+
Transient errors are handled with exponential backoff:
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
def retry_with_backoff(self, func, max_retries=3, base_delay=1):
|
|
545
|
+
"""Retry function with exponential backoff."""
|
|
546
|
+
for attempt in range(max_retries):
|
|
547
|
+
try:
|
|
548
|
+
return func()
|
|
549
|
+
except ClientError as e:
|
|
550
|
+
error_code = e.response['Error']['Code']
|
|
551
|
+
|
|
552
|
+
# Don't retry permission errors
|
|
553
|
+
if error_code in ['AccessDenied', 'UnauthorizedOperation']:
|
|
554
|
+
raise
|
|
555
|
+
|
|
556
|
+
# Retry throttling and service errors
|
|
557
|
+
if error_code in ['Throttling', 'ThrottlingException', 'ServiceUnavailable']:
|
|
558
|
+
if attempt < max_retries - 1:
|
|
559
|
+
delay = base_delay * (2 ** attempt)
|
|
560
|
+
time.sleep(delay)
|
|
561
|
+
continue
|
|
562
|
+
|
|
563
|
+
raise
|
|
564
|
+
|
|
565
|
+
raise Exception(f"Max retries ({max_retries}) exceeded")
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
## Control-Specific Logic
|
|
569
|
+
|
|
570
|
+
### Asset Inventory Controls (1.x)
|
|
571
|
+
|
|
572
|
+
Focus on resource discovery and management:
|
|
573
|
+
|
|
574
|
+
```python
|
|
575
|
+
class AssetInventoryAssessment(BaseConfigRuleAssessment):
|
|
576
|
+
"""Base class for asset inventory assessments."""
|
|
577
|
+
|
|
578
|
+
def evaluate_asset_management(self, resource, aws_factory):
|
|
579
|
+
"""Common asset management evaluation logic."""
|
|
580
|
+
# Check if resource is properly tagged
|
|
581
|
+
tags = resource.get('Tags', [])
|
|
582
|
+
required_tags = self.parameters.get('RequiredTags', [])
|
|
583
|
+
|
|
584
|
+
missing_tags = []
|
|
585
|
+
for required_tag in required_tags:
|
|
586
|
+
if not any(tag['Key'] == required_tag for tag in tags):
|
|
587
|
+
missing_tags.append(required_tag)
|
|
588
|
+
|
|
589
|
+
# Check if resource is managed by Systems Manager (for EC2)
|
|
590
|
+
if resource.get('ResourceType') == 'AWS::EC2::Instance':
|
|
591
|
+
ssm_managed = self.check_ssm_management(resource, aws_factory)
|
|
592
|
+
if not ssm_managed:
|
|
593
|
+
missing_tags.append('SSM_MANAGED')
|
|
594
|
+
|
|
595
|
+
return missing_tags
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Access Control Controls (3.x, 5.x, 6.x)
|
|
599
|
+
|
|
600
|
+
Focus on authentication, authorization, and access patterns:
|
|
601
|
+
|
|
602
|
+
```python
|
|
603
|
+
class AccessControlAssessment(BaseConfigRuleAssessment):
|
|
604
|
+
"""Base class for access control assessments."""
|
|
605
|
+
|
|
606
|
+
def evaluate_public_access(self, resource, aws_factory):
|
|
607
|
+
"""Evaluate if resource allows public access."""
|
|
608
|
+
resource_type = resource.get('ResourceType')
|
|
609
|
+
|
|
610
|
+
if resource_type == 'AWS::S3::Bucket':
|
|
611
|
+
return self.check_s3_public_access(resource, aws_factory)
|
|
612
|
+
elif resource_type == 'AWS::EC2::Instance':
|
|
613
|
+
return self.check_ec2_public_access(resource, aws_factory)
|
|
614
|
+
elif resource_type == 'AWS::RDS::DBInstance':
|
|
615
|
+
return self.check_rds_public_access(resource, aws_factory)
|
|
616
|
+
|
|
617
|
+
return False
|
|
618
|
+
|
|
619
|
+
def check_s3_public_access(self, bucket, aws_factory):
|
|
620
|
+
"""Check if S3 bucket allows public access."""
|
|
621
|
+
s3_client = aws_factory.get_client('s3', bucket['Region'])
|
|
622
|
+
|
|
623
|
+
try:
|
|
624
|
+
# Check bucket policy
|
|
625
|
+
policy_response = s3_client.get_bucket_policy(Bucket=bucket['Name'])
|
|
626
|
+
policy = json.loads(policy_response['Policy'])
|
|
627
|
+
|
|
628
|
+
for statement in policy.get('Statement', []):
|
|
629
|
+
principal = statement.get('Principal')
|
|
630
|
+
if principal == '*' or principal == {'AWS': '*'}:
|
|
631
|
+
return True
|
|
632
|
+
|
|
633
|
+
# Check bucket ACL
|
|
634
|
+
acl_response = s3_client.get_bucket_acl(Bucket=bucket['Name'])
|
|
635
|
+
for grant in acl_response.get('Grants', []):
|
|
636
|
+
grantee = grant.get('Grantee', {})
|
|
637
|
+
if grantee.get('URI') in [
|
|
638
|
+
'http://acs.amazonaws.com/groups/global/AllUsers',
|
|
639
|
+
'http://acs.amazonaws.com/groups/global/AuthenticatedUsers'
|
|
640
|
+
]:
|
|
641
|
+
return True
|
|
642
|
+
|
|
643
|
+
return False
|
|
644
|
+
|
|
645
|
+
except ClientError as e:
|
|
646
|
+
if e.response['Error']['Code'] == 'NoSuchBucketPolicy':
|
|
647
|
+
return False
|
|
648
|
+
raise
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Secure Configuration Controls (4.x)
|
|
652
|
+
|
|
653
|
+
Focus on configuration baselines and hardening:
|
|
654
|
+
|
|
655
|
+
```python
|
|
656
|
+
class SecureConfigurationAssessment(BaseConfigRuleAssessment):
|
|
657
|
+
"""Base class for secure configuration assessments."""
|
|
658
|
+
|
|
659
|
+
def evaluate_security_configuration(self, resource, aws_factory):
|
|
660
|
+
"""Evaluate security configuration settings."""
|
|
661
|
+
config_issues = []
|
|
662
|
+
|
|
663
|
+
# Check encryption settings
|
|
664
|
+
if not self.check_encryption_enabled(resource, aws_factory):
|
|
665
|
+
config_issues.append("encryption not enabled")
|
|
666
|
+
|
|
667
|
+
# Check logging settings
|
|
668
|
+
if not self.check_logging_enabled(resource, aws_factory):
|
|
669
|
+
config_issues.append("logging not enabled")
|
|
670
|
+
|
|
671
|
+
# Check monitoring settings
|
|
672
|
+
if not self.check_monitoring_enabled(resource, aws_factory):
|
|
673
|
+
config_issues.append("monitoring not enabled")
|
|
674
|
+
|
|
675
|
+
# Check update settings
|
|
676
|
+
if not self.check_auto_updates_enabled(resource, aws_factory):
|
|
677
|
+
config_issues.append("automatic updates not enabled")
|
|
678
|
+
|
|
679
|
+
return config_issues
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
## Performance Optimizations
|
|
683
|
+
|
|
684
|
+
### Concurrent Processing
|
|
685
|
+
|
|
686
|
+
Assessments are performed concurrently across regions and resource types:
|
|
687
|
+
|
|
688
|
+
```python
|
|
689
|
+
def run_concurrent_assessments(self, assessment_tasks):
|
|
690
|
+
"""Run assessments concurrently with proper resource management."""
|
|
691
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
692
|
+
# Submit all tasks
|
|
693
|
+
future_to_task = {
|
|
694
|
+
executor.submit(self.run_single_assessment, task): task
|
|
695
|
+
for task in assessment_tasks
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
# Collect results as they complete
|
|
699
|
+
results = []
|
|
700
|
+
for future in as_completed(future_to_task):
|
|
701
|
+
task = future_to_task[future]
|
|
702
|
+
try:
|
|
703
|
+
result = future.result(timeout=self.task_timeout)
|
|
704
|
+
results.append(result)
|
|
705
|
+
except Exception as e:
|
|
706
|
+
self.logger.error(f"Assessment task failed: {task}, error: {e}")
|
|
707
|
+
# Create error result
|
|
708
|
+
error_result = self.create_error_result(task, str(e))
|
|
709
|
+
results.append(error_result)
|
|
710
|
+
|
|
711
|
+
return results
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
### Caching
|
|
715
|
+
|
|
716
|
+
Resource discovery results are cached to avoid redundant API calls:
|
|
717
|
+
|
|
718
|
+
```python
|
|
719
|
+
class ResourceCache:
|
|
720
|
+
"""Cache for resource discovery results."""
|
|
721
|
+
|
|
722
|
+
def __init__(self, ttl_seconds=300):
|
|
723
|
+
self.cache = {}
|
|
724
|
+
self.ttl = ttl_seconds
|
|
725
|
+
|
|
726
|
+
def get(self, cache_key):
|
|
727
|
+
"""Get cached result if still valid."""
|
|
728
|
+
if cache_key in self.cache:
|
|
729
|
+
result, timestamp = self.cache[cache_key]
|
|
730
|
+
if time.time() - timestamp < self.ttl:
|
|
731
|
+
return result
|
|
732
|
+
else:
|
|
733
|
+
del self.cache[cache_key]
|
|
734
|
+
return None
|
|
735
|
+
|
|
736
|
+
def set(self, cache_key, result):
|
|
737
|
+
"""Cache result with timestamp."""
|
|
738
|
+
self.cache[cache_key] = (result, time.time())
|
|
739
|
+
|
|
740
|
+
def clear(self):
|
|
741
|
+
"""Clear all cached results."""
|
|
742
|
+
self.cache.clear()
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### Batch Operations
|
|
746
|
+
|
|
747
|
+
Where possible, resources are processed in batches:
|
|
748
|
+
|
|
749
|
+
```python
|
|
750
|
+
def batch_evaluate_resources(self, resources, batch_size=50):
|
|
751
|
+
"""Evaluate resources in batches for better performance."""
|
|
752
|
+
results = []
|
|
753
|
+
|
|
754
|
+
for i in range(0, len(resources), batch_size):
|
|
755
|
+
batch = resources[i:i + batch_size]
|
|
756
|
+
batch_results = self.evaluate_resource_batch(batch)
|
|
757
|
+
results.extend(batch_results)
|
|
758
|
+
|
|
759
|
+
# Add small delay between batches to avoid throttling
|
|
760
|
+
if i + batch_size < len(resources):
|
|
761
|
+
time.sleep(0.1)
|
|
762
|
+
|
|
763
|
+
return results
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
This comprehensive assessment logic ensures accurate, efficient, and reliable evaluation of AWS resources against CIS Controls while maintaining compatibility with AWS Config rule specifications.
|