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,166 @@
|
|
|
1
|
+
"""Data models for CIS Controls and AWS Config rule specifications."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from typing import Dict, List, Optional, Any
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ComplianceStatus(Enum):
|
|
10
|
+
"""Compliance status enumeration."""
|
|
11
|
+
COMPLIANT = "COMPLIANT"
|
|
12
|
+
NON_COMPLIANT = "NON_COMPLIANT"
|
|
13
|
+
NOT_APPLICABLE = "NOT_APPLICABLE"
|
|
14
|
+
ERROR = "ERROR"
|
|
15
|
+
INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ImplementationGroup(Enum):
|
|
19
|
+
"""CIS Controls Implementation Groups."""
|
|
20
|
+
IG1 = "IG1"
|
|
21
|
+
IG2 = "IG2"
|
|
22
|
+
IG3 = "IG3"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ConfigRule:
|
|
27
|
+
"""AWS Config rule specification for CIS Control assessment."""
|
|
28
|
+
name: str
|
|
29
|
+
control_id: str
|
|
30
|
+
resource_types: List[str]
|
|
31
|
+
parameters: Dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
implementation_group: str = "IG1"
|
|
33
|
+
description: str = ""
|
|
34
|
+
remediation_guidance: str = ""
|
|
35
|
+
|
|
36
|
+
def __post_init__(self):
|
|
37
|
+
"""Validate ConfigRule after initialization."""
|
|
38
|
+
if not self.name:
|
|
39
|
+
raise ValueError("ConfigRule name cannot be empty")
|
|
40
|
+
if not self.control_id:
|
|
41
|
+
raise ValueError("ConfigRule control_id cannot be empty")
|
|
42
|
+
if not self.resource_types:
|
|
43
|
+
raise ValueError("ConfigRule must have at least one resource type")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class CISControl:
|
|
48
|
+
"""CIS Control definition with associated Config rules."""
|
|
49
|
+
control_id: str
|
|
50
|
+
title: str
|
|
51
|
+
implementation_group: str
|
|
52
|
+
config_rules: List[ConfigRule] = field(default_factory=list)
|
|
53
|
+
weight: float = 1.0
|
|
54
|
+
|
|
55
|
+
def __post_init__(self):
|
|
56
|
+
"""Validate CISControl after initialization."""
|
|
57
|
+
if not self.control_id:
|
|
58
|
+
raise ValueError("CISControl control_id cannot be empty")
|
|
59
|
+
if not self.title:
|
|
60
|
+
raise ValueError("CISControl title cannot be empty")
|
|
61
|
+
if self.implementation_group not in [ig.value for ig in ImplementationGroup]:
|
|
62
|
+
raise ValueError(f"Invalid implementation group: {self.implementation_group}")
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ComplianceResult:
|
|
67
|
+
"""Individual resource compliance evaluation result."""
|
|
68
|
+
resource_id: str
|
|
69
|
+
resource_type: str
|
|
70
|
+
compliance_status: ComplianceStatus
|
|
71
|
+
evaluation_reason: str
|
|
72
|
+
config_rule_name: str
|
|
73
|
+
region: str
|
|
74
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
75
|
+
remediation_guidance: Optional[str] = None
|
|
76
|
+
|
|
77
|
+
def __post_init__(self):
|
|
78
|
+
"""Validate ComplianceResult after initialization."""
|
|
79
|
+
if not self.resource_id:
|
|
80
|
+
raise ValueError("ComplianceResult resource_id cannot be empty")
|
|
81
|
+
if not self.resource_type:
|
|
82
|
+
raise ValueError("ComplianceResult resource_type cannot be empty")
|
|
83
|
+
if not isinstance(self.compliance_status, ComplianceStatus):
|
|
84
|
+
raise ValueError("ComplianceResult compliance_status must be ComplianceStatus enum")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class ControlScore:
|
|
89
|
+
"""CIS Control compliance score."""
|
|
90
|
+
control_id: str
|
|
91
|
+
title: str
|
|
92
|
+
implementation_group: str
|
|
93
|
+
total_resources: int
|
|
94
|
+
compliant_resources: int
|
|
95
|
+
compliance_percentage: float
|
|
96
|
+
config_rules_evaluated: List[str] = field(default_factory=list)
|
|
97
|
+
findings: List[ComplianceResult] = field(default_factory=list)
|
|
98
|
+
|
|
99
|
+
def __post_init__(self):
|
|
100
|
+
"""Calculate compliance percentage if not provided."""
|
|
101
|
+
if self.total_resources > 0:
|
|
102
|
+
calculated_percentage = (self.compliant_resources / self.total_resources) * 100
|
|
103
|
+
if abs(self.compliance_percentage - calculated_percentage) > 0.01:
|
|
104
|
+
self.compliance_percentage = calculated_percentage
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class IGScore:
|
|
109
|
+
"""Implementation Group compliance score."""
|
|
110
|
+
implementation_group: str
|
|
111
|
+
total_controls: int
|
|
112
|
+
compliant_controls: int
|
|
113
|
+
compliance_percentage: float
|
|
114
|
+
control_scores: Dict[str, ControlScore] = field(default_factory=dict)
|
|
115
|
+
|
|
116
|
+
def __post_init__(self):
|
|
117
|
+
"""Validate IGScore after initialization."""
|
|
118
|
+
if self.implementation_group not in [ig.value for ig in ImplementationGroup]:
|
|
119
|
+
raise ValueError(f"Invalid implementation group: {self.implementation_group}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class AssessmentResult:
|
|
124
|
+
"""Complete assessment result."""
|
|
125
|
+
account_id: str
|
|
126
|
+
regions_assessed: List[str]
|
|
127
|
+
timestamp: datetime
|
|
128
|
+
overall_score: float
|
|
129
|
+
ig_scores: Dict[str, IGScore] = field(default_factory=dict)
|
|
130
|
+
total_resources_evaluated: int = 0
|
|
131
|
+
assessment_duration: Optional[timedelta] = None
|
|
132
|
+
|
|
133
|
+
def __post_init__(self):
|
|
134
|
+
"""Validate AssessmentResult after initialization."""
|
|
135
|
+
if not self.account_id:
|
|
136
|
+
raise ValueError("AssessmentResult account_id cannot be empty")
|
|
137
|
+
if not self.regions_assessed:
|
|
138
|
+
raise ValueError("AssessmentResult must assess at least one region")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class RemediationGuidance:
|
|
143
|
+
"""Remediation guidance for non-compliant resources."""
|
|
144
|
+
config_rule_name: str
|
|
145
|
+
control_id: str
|
|
146
|
+
remediation_steps: List[str]
|
|
147
|
+
aws_documentation_link: str
|
|
148
|
+
priority: str = "MEDIUM" # HIGH, MEDIUM, LOW
|
|
149
|
+
estimated_effort: str = "Unknown"
|
|
150
|
+
|
|
151
|
+
def __post_init__(self):
|
|
152
|
+
"""Validate RemediationGuidance after initialization."""
|
|
153
|
+
if self.priority not in ["HIGH", "MEDIUM", "LOW"]:
|
|
154
|
+
raise ValueError(f"Invalid priority: {self.priority}")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class ComplianceSummary:
|
|
159
|
+
"""Executive summary of compliance assessment."""
|
|
160
|
+
overall_compliance_percentage: float
|
|
161
|
+
ig1_compliance_percentage: float
|
|
162
|
+
ig2_compliance_percentage: float
|
|
163
|
+
ig3_compliance_percentage: float
|
|
164
|
+
top_risk_areas: List[str] = field(default_factory=list)
|
|
165
|
+
remediation_priorities: List[RemediationGuidance] = field(default_factory=list)
|
|
166
|
+
compliance_trend: Optional[str] = None
|
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""Scoring Engine for calculating CIS Controls compliance scores."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, List, Optional, Any
|
|
5
|
+
from datetime import datetime, timedelta
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
|
|
8
|
+
from aws_cis_assessment.core.models import (
|
|
9
|
+
ComplianceResult, ComplianceStatus, ControlScore, IGScore,
|
|
10
|
+
AssessmentResult, ComplianceSummary, RemediationGuidance
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ScoringEngine:
|
|
17
|
+
"""Calculate compliance scores based on Config rule evaluation results."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, control_weights: Optional[Dict[str, float]] = None,
|
|
20
|
+
ig_weights: Optional[Dict[str, float]] = None):
|
|
21
|
+
"""Initialize scoring engine with optional custom weights.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
control_weights: Optional dictionary mapping control IDs to weights
|
|
25
|
+
ig_weights: Optional dictionary mapping IG names to weights
|
|
26
|
+
"""
|
|
27
|
+
# Default control weights based on CIS Controls importance
|
|
28
|
+
self.control_weights = control_weights or {
|
|
29
|
+
'1.1': 1.0, # Asset Inventory - foundational
|
|
30
|
+
'3.3': 1.5, # Data Access Control - critical
|
|
31
|
+
'4.1': 1.2, # Secure Configuration - important
|
|
32
|
+
'5.2': 1.3, # Password Management - important
|
|
33
|
+
'3.10': 1.4, # Encryption in Transit - critical
|
|
34
|
+
'3.11': 1.4, # Encryption at Rest - critical
|
|
35
|
+
'7.1': 1.1, # Vulnerability Management - important
|
|
36
|
+
'3.14': 1.2, # Sensitive Data Logging - important
|
|
37
|
+
'12.8': 1.3, # Network Segmentation - important
|
|
38
|
+
'13.1': 1.2, # Network Monitoring - important
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Default IG weights - higher IGs have more weight
|
|
42
|
+
self.ig_weights = ig_weights or {
|
|
43
|
+
'IG1': 1.0,
|
|
44
|
+
'IG2': 1.5,
|
|
45
|
+
'IG3': 2.0
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
logger.info("ScoringEngine initialized with control and IG weights")
|
|
49
|
+
|
|
50
|
+
def calculate_control_score(self, control_id: str, rule_results: List[ComplianceResult],
|
|
51
|
+
control_title: str = "", implementation_group: str = "") -> ControlScore:
|
|
52
|
+
"""Calculate compliance score for individual CIS Control.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
control_id: CIS Control identifier (e.g., '1.1', '3.3')
|
|
56
|
+
rule_results: List of ComplianceResult objects for this control
|
|
57
|
+
control_title: Optional title for the control
|
|
58
|
+
implementation_group: Optional IG designation
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
ControlScore object with calculated compliance metrics
|
|
62
|
+
"""
|
|
63
|
+
if not rule_results:
|
|
64
|
+
return ControlScore(
|
|
65
|
+
control_id=control_id,
|
|
66
|
+
title=control_title or f"CIS Control {control_id}",
|
|
67
|
+
implementation_group=implementation_group,
|
|
68
|
+
total_resources=0,
|
|
69
|
+
compliant_resources=0,
|
|
70
|
+
compliance_percentage=0.0,
|
|
71
|
+
config_rules_evaluated=[],
|
|
72
|
+
findings=[]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Filter out error results for scoring (but keep them in findings)
|
|
76
|
+
scorable_results = [r for r in rule_results
|
|
77
|
+
if r.compliance_status in [ComplianceStatus.COMPLIANT,
|
|
78
|
+
ComplianceStatus.NON_COMPLIANT,
|
|
79
|
+
ComplianceStatus.NOT_APPLICABLE]]
|
|
80
|
+
|
|
81
|
+
# Calculate basic metrics
|
|
82
|
+
total_resources = len(scorable_results)
|
|
83
|
+
compliant_resources = sum(1 for r in scorable_results
|
|
84
|
+
if r.compliance_status == ComplianceStatus.COMPLIANT)
|
|
85
|
+
|
|
86
|
+
# Calculate compliance percentage
|
|
87
|
+
if total_resources > 0:
|
|
88
|
+
compliance_percentage = (compliant_resources / total_resources) * 100
|
|
89
|
+
else:
|
|
90
|
+
compliance_percentage = 0.0
|
|
91
|
+
|
|
92
|
+
# Get unique config rules evaluated
|
|
93
|
+
config_rules_evaluated = list(set(r.config_rule_name for r in rule_results))
|
|
94
|
+
|
|
95
|
+
# Apply control-specific weighting if needed
|
|
96
|
+
control_weight = self.control_weights.get(control_id, 1.0)
|
|
97
|
+
weighted_compliance = compliance_percentage * control_weight
|
|
98
|
+
|
|
99
|
+
logger.debug(f"Control {control_id}: {compliant_resources}/{total_resources} "
|
|
100
|
+
f"({compliance_percentage:.1f}%) compliant, weight: {control_weight}")
|
|
101
|
+
|
|
102
|
+
return ControlScore(
|
|
103
|
+
control_id=control_id,
|
|
104
|
+
title=control_title or f"CIS Control {control_id}",
|
|
105
|
+
implementation_group=implementation_group,
|
|
106
|
+
total_resources=total_resources,
|
|
107
|
+
compliant_resources=compliant_resources,
|
|
108
|
+
compliance_percentage=compliance_percentage,
|
|
109
|
+
config_rules_evaluated=config_rules_evaluated,
|
|
110
|
+
findings=rule_results
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def calculate_ig_score(self, implementation_group: str,
|
|
114
|
+
control_scores: Dict[str, ControlScore]) -> IGScore:
|
|
115
|
+
"""Calculate Implementation Group compliance score.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
implementation_group: IG1, IG2, or IG3
|
|
119
|
+
control_scores: Dictionary mapping control IDs to ControlScore objects
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
IGScore object with calculated IG-level metrics
|
|
123
|
+
"""
|
|
124
|
+
if not control_scores:
|
|
125
|
+
return IGScore(
|
|
126
|
+
implementation_group=implementation_group,
|
|
127
|
+
total_controls=0,
|
|
128
|
+
compliant_controls=0,
|
|
129
|
+
compliance_percentage=0.0,
|
|
130
|
+
control_scores={}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
total_controls = len(control_scores)
|
|
134
|
+
|
|
135
|
+
# Calculate weighted average compliance
|
|
136
|
+
total_weighted_score = 0.0
|
|
137
|
+
total_weight = 0.0
|
|
138
|
+
compliant_controls = 0
|
|
139
|
+
|
|
140
|
+
for control_id, control_score in control_scores.items():
|
|
141
|
+
control_weight = self.control_weights.get(control_id, 1.0)
|
|
142
|
+
total_weighted_score += control_score.compliance_percentage * control_weight
|
|
143
|
+
total_weight += control_weight
|
|
144
|
+
|
|
145
|
+
# Consider control compliant if >= 80% compliance
|
|
146
|
+
if control_score.compliance_percentage >= 80.0:
|
|
147
|
+
compliant_controls += 1
|
|
148
|
+
|
|
149
|
+
# Calculate overall IG compliance percentage
|
|
150
|
+
if total_weight > 0:
|
|
151
|
+
ig_compliance_percentage = total_weighted_score / total_weight
|
|
152
|
+
else:
|
|
153
|
+
ig_compliance_percentage = 0.0
|
|
154
|
+
|
|
155
|
+
logger.info(f"IG {implementation_group}: {compliant_controls}/{total_controls} "
|
|
156
|
+
f"controls compliant, overall: {ig_compliance_percentage:.1f}%")
|
|
157
|
+
|
|
158
|
+
return IGScore(
|
|
159
|
+
implementation_group=implementation_group,
|
|
160
|
+
total_controls=total_controls,
|
|
161
|
+
compliant_controls=compliant_controls,
|
|
162
|
+
compliance_percentage=ig_compliance_percentage,
|
|
163
|
+
control_scores=control_scores
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def calculate_overall_score(self, ig_scores: Dict[str, IGScore]) -> float:
|
|
167
|
+
"""Calculate overall compliance score across all Implementation Groups.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
ig_scores: Dictionary mapping IG names to IGScore objects
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Overall compliance percentage (0-100)
|
|
174
|
+
"""
|
|
175
|
+
if not ig_scores:
|
|
176
|
+
return 0.0
|
|
177
|
+
|
|
178
|
+
# Calculate weighted average across IGs
|
|
179
|
+
total_weighted_score = 0.0
|
|
180
|
+
total_weight = 0.0
|
|
181
|
+
|
|
182
|
+
for ig_name, ig_score in ig_scores.items():
|
|
183
|
+
ig_weight = self.ig_weights.get(ig_name, 1.0)
|
|
184
|
+
total_weighted_score += ig_score.compliance_percentage * ig_weight
|
|
185
|
+
total_weight += ig_weight
|
|
186
|
+
|
|
187
|
+
if total_weight > 0:
|
|
188
|
+
overall_score = total_weighted_score / total_weight
|
|
189
|
+
else:
|
|
190
|
+
overall_score = 0.0
|
|
191
|
+
|
|
192
|
+
logger.info(f"Overall compliance score: {overall_score:.1f}%")
|
|
193
|
+
return overall_score
|
|
194
|
+
|
|
195
|
+
def generate_compliance_summary(self, assessment_result: AssessmentResult) -> ComplianceSummary:
|
|
196
|
+
"""Generate executive summary of compliance status.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
assessment_result: Complete assessment result
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
ComplianceSummary with executive-level metrics and recommendations
|
|
203
|
+
"""
|
|
204
|
+
# Extract IG-specific compliance percentages
|
|
205
|
+
ig1_compliance = assessment_result.ig_scores.get('IG1', IGScore('IG1', 0, 0, 0.0)).compliance_percentage
|
|
206
|
+
ig2_compliance = assessment_result.ig_scores.get('IG2', IGScore('IG2', 0, 0, 0.0)).compliance_percentage
|
|
207
|
+
ig3_compliance = assessment_result.ig_scores.get('IG3', IGScore('IG3', 0, 0, 0.0)).compliance_percentage
|
|
208
|
+
|
|
209
|
+
# Identify top risk areas (controls with lowest compliance)
|
|
210
|
+
top_risk_areas = self._identify_risk_areas(assessment_result.ig_scores)
|
|
211
|
+
|
|
212
|
+
# Generate remediation priorities
|
|
213
|
+
remediation_priorities = self._generate_remediation_priorities(assessment_result.ig_scores)
|
|
214
|
+
|
|
215
|
+
# Determine compliance trend (would require historical data)
|
|
216
|
+
compliance_trend = self._determine_compliance_trend(assessment_result)
|
|
217
|
+
|
|
218
|
+
return ComplianceSummary(
|
|
219
|
+
overall_compliance_percentage=assessment_result.overall_score,
|
|
220
|
+
ig1_compliance_percentage=ig1_compliance,
|
|
221
|
+
ig2_compliance_percentage=ig2_compliance,
|
|
222
|
+
ig3_compliance_percentage=ig3_compliance,
|
|
223
|
+
top_risk_areas=top_risk_areas,
|
|
224
|
+
remediation_priorities=remediation_priorities,
|
|
225
|
+
compliance_trend=compliance_trend
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def _identify_risk_areas(self, ig_scores: Dict[str, IGScore],
|
|
229
|
+
max_risk_areas: int = 5) -> List[str]:
|
|
230
|
+
"""Identify top risk areas based on lowest compliance scores.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
ig_scores: Dictionary of IG scores
|
|
234
|
+
max_risk_areas: Maximum number of risk areas to return
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
List of risk area descriptions
|
|
238
|
+
"""
|
|
239
|
+
risk_areas = []
|
|
240
|
+
|
|
241
|
+
# Collect all control scores across IGs
|
|
242
|
+
all_control_scores = []
|
|
243
|
+
for ig_score in ig_scores.values():
|
|
244
|
+
for control_id, control_score in ig_score.control_scores.items():
|
|
245
|
+
all_control_scores.append((control_id, control_score))
|
|
246
|
+
|
|
247
|
+
# Sort by compliance percentage (lowest first)
|
|
248
|
+
all_control_scores.sort(key=lambda x: x[1].compliance_percentage)
|
|
249
|
+
|
|
250
|
+
# Generate risk area descriptions
|
|
251
|
+
for control_id, control_score in all_control_scores[:max_risk_areas]:
|
|
252
|
+
if control_score.compliance_percentage < 80.0: # Only include non-compliant controls
|
|
253
|
+
risk_description = f"Control {control_id} ({control_score.title}): " \
|
|
254
|
+
f"{control_score.compliance_percentage:.1f}% compliant"
|
|
255
|
+
risk_areas.append(risk_description)
|
|
256
|
+
|
|
257
|
+
return risk_areas
|
|
258
|
+
|
|
259
|
+
def _generate_remediation_priorities(self, ig_scores: Dict[str, IGScore],
|
|
260
|
+
max_priorities: int = 10) -> List[RemediationGuidance]:
|
|
261
|
+
"""Generate prioritized remediation guidance.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
ig_scores: Dictionary of IG scores
|
|
265
|
+
max_priorities: Maximum number of remediation items to return
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
List of RemediationGuidance objects prioritized by impact
|
|
269
|
+
"""
|
|
270
|
+
remediation_priorities = []
|
|
271
|
+
|
|
272
|
+
# Collect non-compliant findings across all controls
|
|
273
|
+
non_compliant_findings = []
|
|
274
|
+
for ig_score in ig_scores.values():
|
|
275
|
+
for control_score in ig_score.control_scores.values():
|
|
276
|
+
for finding in control_score.findings:
|
|
277
|
+
if finding.compliance_status == ComplianceStatus.NON_COMPLIANT:
|
|
278
|
+
non_compliant_findings.append((control_score, finding))
|
|
279
|
+
|
|
280
|
+
# Group by config rule and prioritize
|
|
281
|
+
rule_findings = defaultdict(list)
|
|
282
|
+
for control_score, finding in non_compliant_findings:
|
|
283
|
+
rule_findings[finding.config_rule_name].append((control_score, finding))
|
|
284
|
+
|
|
285
|
+
# Generate remediation guidance for top rules
|
|
286
|
+
for rule_name, findings in list(rule_findings.items())[:max_priorities]:
|
|
287
|
+
control_score, sample_finding = findings[0]
|
|
288
|
+
|
|
289
|
+
# Determine priority based on control weight and number of affected resources
|
|
290
|
+
control_weight = self.control_weights.get(control_score.control_id, 1.0)
|
|
291
|
+
affected_resources = len(findings)
|
|
292
|
+
|
|
293
|
+
if control_weight >= 1.4 or affected_resources >= 10:
|
|
294
|
+
priority = "HIGH"
|
|
295
|
+
elif control_weight >= 1.2 or affected_resources >= 5:
|
|
296
|
+
priority = "MEDIUM"
|
|
297
|
+
else:
|
|
298
|
+
priority = "LOW"
|
|
299
|
+
|
|
300
|
+
# Generate basic remediation steps
|
|
301
|
+
remediation_steps = self._generate_remediation_steps(rule_name)
|
|
302
|
+
|
|
303
|
+
remediation_guidance = RemediationGuidance(
|
|
304
|
+
config_rule_name=rule_name,
|
|
305
|
+
control_id=control_score.control_id,
|
|
306
|
+
remediation_steps=remediation_steps,
|
|
307
|
+
aws_documentation_link=f"https://docs.aws.amazon.com/config/latest/developerguide/{rule_name}.html",
|
|
308
|
+
priority=priority,
|
|
309
|
+
estimated_effort=self._estimate_remediation_effort(rule_name, affected_resources)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
remediation_priorities.append(remediation_guidance)
|
|
313
|
+
|
|
314
|
+
# Sort by priority (HIGH, MEDIUM, LOW)
|
|
315
|
+
priority_order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2}
|
|
316
|
+
remediation_priorities.sort(key=lambda x: priority_order.get(x.priority, 3))
|
|
317
|
+
|
|
318
|
+
return remediation_priorities
|
|
319
|
+
|
|
320
|
+
def _generate_remediation_steps(self, rule_name: str) -> List[str]:
|
|
321
|
+
"""Generate basic remediation steps for a Config rule.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
rule_name: AWS Config rule name
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
List of remediation step descriptions
|
|
328
|
+
"""
|
|
329
|
+
# Basic remediation steps based on rule patterns
|
|
330
|
+
if 'iam' in rule_name:
|
|
331
|
+
return [
|
|
332
|
+
"Review IAM policies and permissions",
|
|
333
|
+
"Remove unnecessary privileges",
|
|
334
|
+
"Enable MFA where required",
|
|
335
|
+
"Update password policies if applicable"
|
|
336
|
+
]
|
|
337
|
+
elif 'encrypt' in rule_name:
|
|
338
|
+
return [
|
|
339
|
+
"Enable encryption for the identified resources",
|
|
340
|
+
"Use AWS KMS for key management",
|
|
341
|
+
"Update resource configurations to require encryption",
|
|
342
|
+
"Verify encryption settings are applied"
|
|
343
|
+
]
|
|
344
|
+
elif 's3' in rule_name:
|
|
345
|
+
return [
|
|
346
|
+
"Review S3 bucket policies and ACLs",
|
|
347
|
+
"Remove public access if not required",
|
|
348
|
+
"Enable appropriate S3 security features",
|
|
349
|
+
"Update bucket configurations"
|
|
350
|
+
]
|
|
351
|
+
elif 'vpc' in rule_name or 'security-group' in rule_name:
|
|
352
|
+
return [
|
|
353
|
+
"Review network security group rules",
|
|
354
|
+
"Remove overly permissive rules",
|
|
355
|
+
"Implement principle of least privilege",
|
|
356
|
+
"Update VPC configurations as needed"
|
|
357
|
+
]
|
|
358
|
+
else:
|
|
359
|
+
return [
|
|
360
|
+
f"Review {rule_name} configuration",
|
|
361
|
+
"Apply AWS security best practices",
|
|
362
|
+
"Update resource settings to meet compliance requirements",
|
|
363
|
+
"Verify changes resolve the compliance issue"
|
|
364
|
+
]
|
|
365
|
+
|
|
366
|
+
def _estimate_remediation_effort(self, rule_name: str, affected_resources: int) -> str:
|
|
367
|
+
"""Estimate effort required for remediation.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
rule_name: AWS Config rule name
|
|
371
|
+
affected_resources: Number of affected resources
|
|
372
|
+
|
|
373
|
+
Returns:
|
|
374
|
+
Effort estimate string
|
|
375
|
+
"""
|
|
376
|
+
# Base effort on rule complexity and resource count
|
|
377
|
+
if affected_resources <= 5:
|
|
378
|
+
base_effort = "Low"
|
|
379
|
+
elif affected_resources <= 20:
|
|
380
|
+
base_effort = "Medium"
|
|
381
|
+
else:
|
|
382
|
+
base_effort = "High"
|
|
383
|
+
|
|
384
|
+
# Adjust for rule complexity
|
|
385
|
+
complex_rules = ['iam-password-policy', 'vpc-sg-open-only-to-authorized-ports',
|
|
386
|
+
'multi-region-cloudtrail-enabled']
|
|
387
|
+
|
|
388
|
+
if rule_name in complex_rules:
|
|
389
|
+
if base_effort == "Low":
|
|
390
|
+
base_effort = "Medium"
|
|
391
|
+
elif base_effort == "Medium":
|
|
392
|
+
base_effort = "High"
|
|
393
|
+
|
|
394
|
+
return base_effort
|
|
395
|
+
|
|
396
|
+
def _determine_compliance_trend(self, assessment_result: AssessmentResult) -> Optional[str]:
|
|
397
|
+
"""Determine compliance trend (requires historical data).
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
assessment_result: Current assessment result
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
Trend description or None if no historical data available
|
|
404
|
+
"""
|
|
405
|
+
# This would require historical assessment data to implement properly
|
|
406
|
+
# For now, return None to indicate no trend data available
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
def calculate_resource_count_by_status(self, ig_scores: Dict[str, IGScore]) -> Dict[str, int]:
|
|
410
|
+
"""Calculate resource counts by compliance status across all IGs.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
ig_scores: Dictionary of IG scores
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Dictionary mapping status names to resource counts
|
|
417
|
+
"""
|
|
418
|
+
status_counts = defaultdict(int)
|
|
419
|
+
|
|
420
|
+
for ig_score in ig_scores.values():
|
|
421
|
+
for control_score in ig_score.control_scores.values():
|
|
422
|
+
for finding in control_score.findings:
|
|
423
|
+
status_counts[finding.compliance_status.value] += 1
|
|
424
|
+
|
|
425
|
+
return dict(status_counts)
|
|
426
|
+
|
|
427
|
+
def get_control_weights(self) -> Dict[str, float]:
|
|
428
|
+
"""Get current control weights.
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
Dictionary mapping control IDs to weights
|
|
432
|
+
"""
|
|
433
|
+
return self.control_weights.copy()
|
|
434
|
+
|
|
435
|
+
def get_ig_weights(self) -> Dict[str, float]:
|
|
436
|
+
"""Get current IG weights.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Dictionary mapping IG names to weights
|
|
440
|
+
"""
|
|
441
|
+
return self.ig_weights.copy()
|
|
442
|
+
|
|
443
|
+
def update_control_weights(self, new_weights: Dict[str, float]):
|
|
444
|
+
"""Update control weights.
|
|
445
|
+
|
|
446
|
+
Args:
|
|
447
|
+
new_weights: Dictionary mapping control IDs to new weights
|
|
448
|
+
"""
|
|
449
|
+
self.control_weights.update(new_weights)
|
|
450
|
+
logger.info(f"Updated control weights: {new_weights}")
|
|
451
|
+
|
|
452
|
+
def update_ig_weights(self, new_weights: Dict[str, float]):
|
|
453
|
+
"""Update IG weights.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
new_weights: Dictionary mapping IG names to new weights
|
|
457
|
+
"""
|
|
458
|
+
self.ig_weights.update(new_weights)
|
|
459
|
+
logger.info(f"Updated IG weights: {new_weights}")
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Reporters package for CIS Controls compliance assessment reports."""
|
|
2
|
+
|
|
3
|
+
from .base_reporter import ReportGenerator, ReportTemplateEngine
|
|
4
|
+
from .json_reporter import JSONReporter
|
|
5
|
+
from .html_reporter import HTMLReporter
|
|
6
|
+
from .csv_reporter import CSVReporter
|
|
7
|
+
|
|
8
|
+
__all__ = ['ReportGenerator', 'ReportTemplateEngine', 'JSONReporter', 'HTMLReporter', 'CSVReporter']
|