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,454 @@
|
|
|
1
|
+
"""Base Report Generator for CIS Controls compliance assessment reports."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from typing import Dict, Any, List, Optional
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from aws_cis_assessment.core.models import (
|
|
10
|
+
AssessmentResult, ComplianceSummary, RemediationGuidance,
|
|
11
|
+
IGScore, ControlScore, ComplianceResult
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ReportGenerator(ABC):
|
|
18
|
+
"""Abstract base class for generating compliance assessment reports."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, template_dir: Optional[str] = None):
|
|
21
|
+
"""Initialize report generator with optional template directory.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
template_dir: Optional path to custom report templates
|
|
25
|
+
"""
|
|
26
|
+
self.template_dir = Path(template_dir) if template_dir else None
|
|
27
|
+
self.report_metadata = {}
|
|
28
|
+
logger.info(f"Initialized {self.__class__.__name__} with template_dir: {template_dir}")
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def generate_report(self, assessment_result: AssessmentResult,
|
|
32
|
+
compliance_summary: ComplianceSummary,
|
|
33
|
+
output_path: Optional[str] = None) -> str:
|
|
34
|
+
"""Generate compliance assessment report.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
assessment_result: Complete assessment result data
|
|
38
|
+
compliance_summary: Executive summary of compliance status
|
|
39
|
+
output_path: Optional path to save the report
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Generated report content as string
|
|
43
|
+
"""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
@abstractmethod
|
|
47
|
+
def get_supported_formats(self) -> List[str]:
|
|
48
|
+
"""Get list of supported output formats.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
List of supported format strings (e.g., ['json', 'html', 'csv'])
|
|
52
|
+
"""
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def set_report_metadata(self, metadata: Dict[str, Any]):
|
|
56
|
+
"""Set additional metadata for the report.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
metadata: Dictionary containing report metadata
|
|
60
|
+
"""
|
|
61
|
+
self.report_metadata.update(metadata)
|
|
62
|
+
logger.debug(f"Updated report metadata: {metadata}")
|
|
63
|
+
|
|
64
|
+
def get_report_metadata(self) -> Dict[str, Any]:
|
|
65
|
+
"""Get current report metadata.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Dictionary containing current report metadata
|
|
69
|
+
"""
|
|
70
|
+
return self.report_metadata.copy()
|
|
71
|
+
|
|
72
|
+
def _prepare_report_data(self, assessment_result: AssessmentResult,
|
|
73
|
+
compliance_summary: ComplianceSummary) -> Dict[str, Any]:
|
|
74
|
+
"""Prepare standardized data structure for report generation.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
assessment_result: Complete assessment result data
|
|
78
|
+
compliance_summary: Executive summary of compliance status
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Dictionary containing standardized report data
|
|
82
|
+
"""
|
|
83
|
+
# Calculate additional metrics
|
|
84
|
+
total_resources = sum(
|
|
85
|
+
sum(len(control.findings) for control in ig.control_scores.values())
|
|
86
|
+
for ig in assessment_result.ig_scores.values()
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
total_compliant = sum(
|
|
90
|
+
sum(control.compliant_resources for control in ig.control_scores.values())
|
|
91
|
+
for ig in assessment_result.ig_scores.values()
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
total_non_compliant = sum(
|
|
95
|
+
sum(len([f for f in control.findings if f.compliance_status.value == 'NON_COMPLIANT'])
|
|
96
|
+
for control in ig.control_scores.values())
|
|
97
|
+
for ig in assessment_result.ig_scores.values()
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
# Prepare standardized data structure
|
|
101
|
+
report_data = {
|
|
102
|
+
'metadata': {
|
|
103
|
+
'report_generated_at': datetime.now().isoformat(),
|
|
104
|
+
'assessment_timestamp': assessment_result.timestamp.isoformat(),
|
|
105
|
+
'account_id': assessment_result.account_id,
|
|
106
|
+
'regions_assessed': assessment_result.regions_assessed,
|
|
107
|
+
'assessment_duration': str(assessment_result.assessment_duration) if assessment_result.assessment_duration else None,
|
|
108
|
+
'total_resources_evaluated': assessment_result.total_resources_evaluated,
|
|
109
|
+
**self.report_metadata
|
|
110
|
+
},
|
|
111
|
+
'executive_summary': {
|
|
112
|
+
'overall_compliance_percentage': compliance_summary.overall_compliance_percentage,
|
|
113
|
+
'ig1_compliance_percentage': compliance_summary.ig1_compliance_percentage,
|
|
114
|
+
'ig2_compliance_percentage': compliance_summary.ig2_compliance_percentage,
|
|
115
|
+
'ig3_compliance_percentage': compliance_summary.ig3_compliance_percentage,
|
|
116
|
+
'total_resources': total_resources,
|
|
117
|
+
'compliant_resources': total_compliant,
|
|
118
|
+
'non_compliant_resources': total_non_compliant,
|
|
119
|
+
'top_risk_areas': compliance_summary.top_risk_areas,
|
|
120
|
+
'compliance_trend': compliance_summary.compliance_trend
|
|
121
|
+
},
|
|
122
|
+
'implementation_groups': self._prepare_ig_data(assessment_result.ig_scores),
|
|
123
|
+
'remediation_priorities': self._prepare_remediation_data(compliance_summary.remediation_priorities),
|
|
124
|
+
'detailed_findings': self._prepare_findings_data(assessment_result.ig_scores)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return report_data
|
|
128
|
+
|
|
129
|
+
def _prepare_ig_data(self, ig_scores: Dict[str, IGScore]) -> Dict[str, Any]:
|
|
130
|
+
"""Prepare Implementation Group data for reporting.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
ig_scores: Dictionary of IG scores
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dictionary containing IG data structured for reporting
|
|
137
|
+
"""
|
|
138
|
+
ig_data = {}
|
|
139
|
+
|
|
140
|
+
for ig_name, ig_score in ig_scores.items():
|
|
141
|
+
ig_data[ig_name] = {
|
|
142
|
+
'implementation_group': ig_score.implementation_group,
|
|
143
|
+
'total_controls': ig_score.total_controls,
|
|
144
|
+
'compliant_controls': ig_score.compliant_controls,
|
|
145
|
+
'compliance_percentage': ig_score.compliance_percentage,
|
|
146
|
+
'controls': self._prepare_control_data(ig_score.control_scores)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return ig_data
|
|
150
|
+
|
|
151
|
+
def _prepare_control_data(self, control_scores: Dict[str, ControlScore]) -> Dict[str, Any]:
|
|
152
|
+
"""Prepare Control data for reporting.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
control_scores: Dictionary of control scores
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Dictionary containing control data structured for reporting
|
|
159
|
+
"""
|
|
160
|
+
control_data = {}
|
|
161
|
+
|
|
162
|
+
for control_id, control_score in control_scores.items():
|
|
163
|
+
control_data[control_id] = {
|
|
164
|
+
'control_id': control_score.control_id,
|
|
165
|
+
'title': control_score.title,
|
|
166
|
+
'implementation_group': control_score.implementation_group,
|
|
167
|
+
'total_resources': control_score.total_resources,
|
|
168
|
+
'compliant_resources': control_score.compliant_resources,
|
|
169
|
+
'compliance_percentage': control_score.compliance_percentage,
|
|
170
|
+
'config_rules_evaluated': control_score.config_rules_evaluated,
|
|
171
|
+
'findings_count': len(control_score.findings),
|
|
172
|
+
'non_compliant_findings': [
|
|
173
|
+
self._prepare_finding_data(finding)
|
|
174
|
+
for finding in control_score.findings
|
|
175
|
+
if finding.compliance_status.value == 'NON_COMPLIANT'
|
|
176
|
+
],
|
|
177
|
+
'compliant_findings': [
|
|
178
|
+
self._prepare_finding_data(finding)
|
|
179
|
+
for finding in control_score.findings
|
|
180
|
+
if finding.compliance_status.value == 'COMPLIANT'
|
|
181
|
+
]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return control_data
|
|
185
|
+
|
|
186
|
+
def _prepare_finding_data(self, finding: ComplianceResult) -> Dict[str, Any]:
|
|
187
|
+
"""Prepare individual finding data for reporting.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
finding: ComplianceResult object
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Dictionary containing finding data structured for reporting
|
|
194
|
+
"""
|
|
195
|
+
return {
|
|
196
|
+
'resource_id': finding.resource_id,
|
|
197
|
+
'resource_type': finding.resource_type,
|
|
198
|
+
'compliance_status': finding.compliance_status.value,
|
|
199
|
+
'evaluation_reason': finding.evaluation_reason,
|
|
200
|
+
'config_rule_name': finding.config_rule_name,
|
|
201
|
+
'region': finding.region,
|
|
202
|
+
'timestamp': finding.timestamp.isoformat(),
|
|
203
|
+
'remediation_guidance': finding.remediation_guidance
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
def _prepare_remediation_data(self, remediation_priorities: List[RemediationGuidance]) -> List[Dict[str, Any]]:
|
|
207
|
+
"""Prepare remediation guidance data for reporting.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
remediation_priorities: List of RemediationGuidance objects
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
List of dictionaries containing remediation data structured for reporting
|
|
214
|
+
"""
|
|
215
|
+
remediation_data = []
|
|
216
|
+
|
|
217
|
+
for guidance in remediation_priorities:
|
|
218
|
+
remediation_data.append({
|
|
219
|
+
'config_rule_name': guidance.config_rule_name,
|
|
220
|
+
'control_id': guidance.control_id,
|
|
221
|
+
'priority': guidance.priority,
|
|
222
|
+
'estimated_effort': guidance.estimated_effort,
|
|
223
|
+
'remediation_steps': guidance.remediation_steps,
|
|
224
|
+
'aws_documentation_link': guidance.aws_documentation_link
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
return remediation_data
|
|
228
|
+
|
|
229
|
+
def _prepare_findings_data(self, ig_scores: Dict[str, IGScore]) -> Dict[str, Any]:
|
|
230
|
+
"""Prepare detailed findings data for reporting.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
ig_scores: Dictionary of IG scores
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Dictionary containing findings data structured for reporting
|
|
237
|
+
"""
|
|
238
|
+
findings_data = {}
|
|
239
|
+
|
|
240
|
+
for ig_name, ig_score in ig_scores.items():
|
|
241
|
+
ig_findings = {}
|
|
242
|
+
for control_id, control_score in ig_score.control_scores.items():
|
|
243
|
+
control_findings = []
|
|
244
|
+
for finding in control_score.findings:
|
|
245
|
+
control_findings.append(self._prepare_finding_data(finding))
|
|
246
|
+
ig_findings[control_id] = control_findings
|
|
247
|
+
findings_data[ig_name] = ig_findings
|
|
248
|
+
|
|
249
|
+
return findings_data
|
|
250
|
+
|
|
251
|
+
def _validate_report_data(self, report_data: Dict[str, Any]) -> bool:
|
|
252
|
+
"""Validate report data structure for consistency.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
report_data: Prepared report data dictionary
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
True if data is valid, False otherwise
|
|
259
|
+
"""
|
|
260
|
+
required_sections = ['metadata', 'executive_summary', 'implementation_groups',
|
|
261
|
+
'remediation_priorities', 'detailed_findings']
|
|
262
|
+
|
|
263
|
+
for section in required_sections:
|
|
264
|
+
if section not in report_data:
|
|
265
|
+
logger.error(f"Missing required section in report data: {section}")
|
|
266
|
+
return False
|
|
267
|
+
|
|
268
|
+
# Validate metadata section
|
|
269
|
+
metadata = report_data['metadata']
|
|
270
|
+
required_metadata = ['report_generated_at', 'assessment_timestamp', 'account_id']
|
|
271
|
+
for field in required_metadata:
|
|
272
|
+
if field not in metadata:
|
|
273
|
+
logger.error(f"Missing required metadata field: {field}")
|
|
274
|
+
return False
|
|
275
|
+
|
|
276
|
+
# Validate executive summary
|
|
277
|
+
summary = report_data['executive_summary']
|
|
278
|
+
required_summary_fields = ['overall_compliance_percentage', 'total_resources']
|
|
279
|
+
for field in required_summary_fields:
|
|
280
|
+
if field not in summary:
|
|
281
|
+
logger.error(f"Missing required executive summary field: {field}")
|
|
282
|
+
return False
|
|
283
|
+
|
|
284
|
+
logger.debug("Report data validation passed")
|
|
285
|
+
return True
|
|
286
|
+
|
|
287
|
+
def _save_report_to_file(self, content: str, output_path: str) -> bool:
|
|
288
|
+
"""Save report content to file.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
content: Report content to save
|
|
292
|
+
output_path: Path where to save the report
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
True if saved successfully, False otherwise
|
|
296
|
+
"""
|
|
297
|
+
try:
|
|
298
|
+
output_file = Path(output_path)
|
|
299
|
+
output_file.parent.mkdir(parents=True, exist_ok=True)
|
|
300
|
+
|
|
301
|
+
with open(output_file, 'w', encoding='utf-8') as f:
|
|
302
|
+
f.write(content)
|
|
303
|
+
|
|
304
|
+
logger.info(f"Report saved to: {output_path}")
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
except Exception as e:
|
|
308
|
+
logger.error(f"Failed to save report to {output_path}: {e}")
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
def validate_assessment_data(self, assessment_result: AssessmentResult,
|
|
312
|
+
compliance_summary: ComplianceSummary) -> bool:
|
|
313
|
+
"""Validate input assessment data before report generation.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
assessment_result: Assessment result to validate
|
|
317
|
+
compliance_summary: Compliance summary to validate
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
True if data is valid, False otherwise
|
|
321
|
+
"""
|
|
322
|
+
if not assessment_result.account_id:
|
|
323
|
+
logger.error("Assessment result missing account_id")
|
|
324
|
+
return False
|
|
325
|
+
|
|
326
|
+
if not assessment_result.regions_assessed:
|
|
327
|
+
logger.error("Assessment result missing regions_assessed")
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
if not assessment_result.ig_scores:
|
|
331
|
+
logger.error("Assessment result missing ig_scores")
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
# Validate compliance summary
|
|
335
|
+
if compliance_summary.overall_compliance_percentage < 0 or compliance_summary.overall_compliance_percentage > 100:
|
|
336
|
+
logger.error(f"Invalid overall compliance percentage: {compliance_summary.overall_compliance_percentage}")
|
|
337
|
+
return False
|
|
338
|
+
|
|
339
|
+
logger.debug("Assessment data validation passed")
|
|
340
|
+
return True
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class ReportTemplateEngine:
|
|
344
|
+
"""Template engine for generating formatted reports."""
|
|
345
|
+
|
|
346
|
+
def __init__(self, template_dir: Optional[str] = None):
|
|
347
|
+
"""Initialize template engine.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
template_dir: Optional path to custom templates
|
|
351
|
+
"""
|
|
352
|
+
self.template_dir = Path(template_dir) if template_dir else None
|
|
353
|
+
self.templates = {}
|
|
354
|
+
logger.info(f"Initialized ReportTemplateEngine with template_dir: {template_dir}")
|
|
355
|
+
|
|
356
|
+
def load_template(self, template_name: str) -> str:
|
|
357
|
+
"""Load template content from file or built-in templates.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
template_name: Name of the template to load
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
Template content as string
|
|
364
|
+
"""
|
|
365
|
+
# Check for custom template first
|
|
366
|
+
if self.template_dir:
|
|
367
|
+
custom_template_path = self.template_dir / f"{template_name}.template"
|
|
368
|
+
if custom_template_path.exists():
|
|
369
|
+
with open(custom_template_path, 'r', encoding='utf-8') as f:
|
|
370
|
+
template_content = f.read()
|
|
371
|
+
logger.debug(f"Loaded custom template: {template_name}")
|
|
372
|
+
return template_content
|
|
373
|
+
|
|
374
|
+
# Fall back to built-in templates
|
|
375
|
+
built_in_template = self._get_builtin_template(template_name)
|
|
376
|
+
if built_in_template:
|
|
377
|
+
logger.debug(f"Using built-in template: {template_name}")
|
|
378
|
+
return built_in_template
|
|
379
|
+
|
|
380
|
+
logger.warning(f"Template not found: {template_name}")
|
|
381
|
+
return ""
|
|
382
|
+
|
|
383
|
+
def _get_builtin_template(self, template_name: str) -> Optional[str]:
|
|
384
|
+
"""Get built-in template content.
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
template_name: Name of the built-in template
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Template content or None if not found
|
|
391
|
+
"""
|
|
392
|
+
builtin_templates = {
|
|
393
|
+
'executive_summary': """
|
|
394
|
+
# Executive Summary
|
|
395
|
+
|
|
396
|
+
**Overall Compliance:** {overall_compliance_percentage:.1f}%
|
|
397
|
+
**Assessment Date:** {assessment_timestamp}
|
|
398
|
+
**AWS Account:** {account_id}
|
|
399
|
+
|
|
400
|
+
## Implementation Group Compliance
|
|
401
|
+
- **IG1 (Essential Cyber Hygiene):** {ig1_compliance_percentage:.1f}%
|
|
402
|
+
- **IG2 (Enhanced Security):** {ig2_compliance_percentage:.1f}%
|
|
403
|
+
- **IG3 (Advanced Security):** {ig3_compliance_percentage:.1f}%
|
|
404
|
+
|
|
405
|
+
## Resource Summary
|
|
406
|
+
- **Total Resources Evaluated:** {total_resources}
|
|
407
|
+
- **Compliant Resources:** {compliant_resources}
|
|
408
|
+
- **Non-Compliant Resources:** {non_compliant_resources}
|
|
409
|
+
|
|
410
|
+
## Top Risk Areas
|
|
411
|
+
{top_risk_areas}
|
|
412
|
+
""",
|
|
413
|
+
'control_detail': """
|
|
414
|
+
## Control {control_id}: {title}
|
|
415
|
+
|
|
416
|
+
**Implementation Group:** {implementation_group}
|
|
417
|
+
**Compliance:** {compliant_resources}/{total_resources} ({compliance_percentage:.1f}%)
|
|
418
|
+
**Config Rules:** {config_rules_evaluated}
|
|
419
|
+
|
|
420
|
+
### Non-Compliant Resources
|
|
421
|
+
{non_compliant_findings}
|
|
422
|
+
""",
|
|
423
|
+
'remediation_guidance': """
|
|
424
|
+
# Remediation Priorities
|
|
425
|
+
|
|
426
|
+
{remediation_items}
|
|
427
|
+
"""
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return builtin_templates.get(template_name)
|
|
431
|
+
|
|
432
|
+
def render_template(self, template_content: str, data: Dict[str, Any]) -> str:
|
|
433
|
+
"""Render template with provided data.
|
|
434
|
+
|
|
435
|
+
Args:
|
|
436
|
+
template_content: Template content with placeholders
|
|
437
|
+
data: Data dictionary for template substitution
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
Rendered template content
|
|
441
|
+
"""
|
|
442
|
+
try:
|
|
443
|
+
# Simple template rendering using string formatting
|
|
444
|
+
# For more complex templating, could integrate Jinja2 or similar
|
|
445
|
+
rendered_content = template_content.format(**data)
|
|
446
|
+
logger.debug("Template rendered successfully")
|
|
447
|
+
return rendered_content
|
|
448
|
+
|
|
449
|
+
except KeyError as e:
|
|
450
|
+
logger.error(f"Template rendering failed - missing key: {e}")
|
|
451
|
+
return template_content
|
|
452
|
+
except Exception as e:
|
|
453
|
+
logger.error(f"Template rendering failed: {e}")
|
|
454
|
+
return template_content
|