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,2162 @@
|
|
|
1
|
+
"""HTML Reporter for CIS Controls compliance assessment reports."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Dict, Any, List, Optional
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import base64
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
from aws_cis_assessment.reporters.base_reporter import ReportGenerator
|
|
10
|
+
from aws_cis_assessment.core.models import (
|
|
11
|
+
AssessmentResult, ComplianceSummary, RemediationGuidance,
|
|
12
|
+
IGScore, ControlScore, ComplianceResult
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HTMLReporter(ReportGenerator):
|
|
19
|
+
"""HTML format reporter for compliance assessment results.
|
|
20
|
+
|
|
21
|
+
Generates interactive web-based reports with executive dashboard,
|
|
22
|
+
compliance summaries, charts, detailed drill-down capabilities,
|
|
23
|
+
and responsive design for mobile and desktop viewing.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, template_dir: Optional[str] = None, include_charts: bool = True):
|
|
27
|
+
"""Initialize HTML reporter.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
template_dir: Optional path to custom report templates
|
|
31
|
+
include_charts: Whether to include interactive charts (default: True)
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(template_dir)
|
|
34
|
+
self.include_charts = include_charts
|
|
35
|
+
logger.info(f"Initialized HTMLReporter with charts={include_charts}")
|
|
36
|
+
|
|
37
|
+
def generate_report(self, assessment_result: AssessmentResult,
|
|
38
|
+
compliance_summary: ComplianceSummary,
|
|
39
|
+
output_path: Optional[str] = None) -> str:
|
|
40
|
+
"""Generate HTML format compliance assessment report.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
assessment_result: Complete assessment result data
|
|
44
|
+
compliance_summary: Executive summary of compliance status
|
|
45
|
+
output_path: Optional path to save the HTML report
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
HTML formatted report content as string
|
|
49
|
+
"""
|
|
50
|
+
# Handle None inputs
|
|
51
|
+
if assessment_result is None or compliance_summary is None:
|
|
52
|
+
logger.error("Assessment result or compliance summary is None")
|
|
53
|
+
return ""
|
|
54
|
+
|
|
55
|
+
logger.info(f"Generating HTML report for account {assessment_result.account_id}")
|
|
56
|
+
|
|
57
|
+
# Validate input data
|
|
58
|
+
if not self.validate_assessment_data(assessment_result, compliance_summary):
|
|
59
|
+
logger.error("Assessment data validation failed")
|
|
60
|
+
return ""
|
|
61
|
+
|
|
62
|
+
# Prepare structured report data
|
|
63
|
+
report_data = self._prepare_report_data(assessment_result, compliance_summary)
|
|
64
|
+
|
|
65
|
+
# Validate prepared data
|
|
66
|
+
if not self._validate_report_data(report_data):
|
|
67
|
+
logger.error("Report data validation failed")
|
|
68
|
+
return ""
|
|
69
|
+
|
|
70
|
+
# Enhance HTML-specific data structure
|
|
71
|
+
html_report_data = self._enhance_html_structure(report_data)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
# Generate HTML content
|
|
75
|
+
html_content = self._generate_html_content(html_report_data)
|
|
76
|
+
|
|
77
|
+
logger.info(f"Generated HTML report with {len(html_content)} characters")
|
|
78
|
+
|
|
79
|
+
# Save to file if path provided
|
|
80
|
+
if output_path:
|
|
81
|
+
if self._save_report_to_file(html_content, output_path):
|
|
82
|
+
logger.info(f"HTML report saved to {output_path}")
|
|
83
|
+
else:
|
|
84
|
+
logger.error(f"Failed to save HTML report to {output_path}")
|
|
85
|
+
|
|
86
|
+
return html_content
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.error(f"Failed to generate HTML report: {e}")
|
|
90
|
+
return ""
|
|
91
|
+
|
|
92
|
+
def get_supported_formats(self) -> List[str]:
|
|
93
|
+
"""Get list of supported output formats.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
List containing 'html' format
|
|
97
|
+
"""
|
|
98
|
+
return ['html']
|
|
99
|
+
|
|
100
|
+
def _enhance_html_structure(self, report_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
101
|
+
"""Enhance report data structure for HTML-specific requirements.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
report_data: Base report data from parent class
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
Enhanced data structure optimized for HTML output
|
|
108
|
+
"""
|
|
109
|
+
# Create enhanced HTML structure
|
|
110
|
+
html_data = {
|
|
111
|
+
"report_format": "html",
|
|
112
|
+
"report_version": "1.0",
|
|
113
|
+
"include_charts": self.include_charts,
|
|
114
|
+
**report_data
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Add HTML-specific metadata
|
|
118
|
+
html_data["metadata"]["report_format"] = "html"
|
|
119
|
+
html_data["metadata"]["interactive"] = True
|
|
120
|
+
html_data["metadata"]["responsive_design"] = True
|
|
121
|
+
|
|
122
|
+
# Enhance executive summary with visual indicators
|
|
123
|
+
exec_summary = html_data["executive_summary"]
|
|
124
|
+
exec_summary["compliance_grade"] = self._calculate_compliance_grade(
|
|
125
|
+
exec_summary["overall_compliance_percentage"]
|
|
126
|
+
)
|
|
127
|
+
exec_summary["risk_level"] = self._calculate_risk_level(
|
|
128
|
+
exec_summary["overall_compliance_percentage"]
|
|
129
|
+
)
|
|
130
|
+
exec_summary["status_color"] = self._get_status_color(
|
|
131
|
+
exec_summary["overall_compliance_percentage"]
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Add chart data for Implementation Groups
|
|
135
|
+
html_data["chart_data"] = self._prepare_chart_data(html_data)
|
|
136
|
+
|
|
137
|
+
# Enhance Implementation Group data with visual elements
|
|
138
|
+
for ig_name, ig_data in html_data["implementation_groups"].items():
|
|
139
|
+
ig_data["status_color"] = self._get_status_color(ig_data["compliance_percentage"])
|
|
140
|
+
ig_data["progress_width"] = ig_data["compliance_percentage"]
|
|
141
|
+
|
|
142
|
+
# Enhance control data with visual indicators
|
|
143
|
+
for control_id, control_data in ig_data["controls"].items():
|
|
144
|
+
control_data["status_color"] = self._get_status_color(
|
|
145
|
+
control_data["compliance_percentage"]
|
|
146
|
+
)
|
|
147
|
+
control_data["progress_width"] = control_data["compliance_percentage"]
|
|
148
|
+
control_data["severity_badge"] = self._get_severity_badge(control_data)
|
|
149
|
+
|
|
150
|
+
# Process findings for display
|
|
151
|
+
control_data["display_findings"] = self._prepare_findings_for_display(
|
|
152
|
+
control_data.get("non_compliant_findings", [])
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Enhance remediation priorities with visual elements
|
|
156
|
+
for remediation in html_data["remediation_priorities"]:
|
|
157
|
+
remediation["priority_badge"] = self._get_priority_badge(remediation["priority"])
|
|
158
|
+
remediation["effort_badge"] = self._get_effort_badge(remediation["estimated_effort"])
|
|
159
|
+
|
|
160
|
+
# Add navigation structure
|
|
161
|
+
html_data["navigation"] = self._build_navigation_structure(html_data)
|
|
162
|
+
|
|
163
|
+
return html_data
|
|
164
|
+
|
|
165
|
+
def _generate_html_content(self, html_data: Dict[str, Any]) -> str:
|
|
166
|
+
"""Generate complete HTML content from data.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
html_data: Enhanced HTML report data
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
Complete HTML document as string
|
|
173
|
+
"""
|
|
174
|
+
# Build HTML document sections
|
|
175
|
+
html_head = self._generate_html_head(html_data)
|
|
176
|
+
html_body = self._generate_html_body(html_data)
|
|
177
|
+
|
|
178
|
+
# Combine into complete document
|
|
179
|
+
html_content = f"""<!DOCTYPE html>
|
|
180
|
+
<html lang="en">
|
|
181
|
+
{html_head}
|
|
182
|
+
{html_body}
|
|
183
|
+
</html>"""
|
|
184
|
+
|
|
185
|
+
return html_content
|
|
186
|
+
|
|
187
|
+
def _generate_html_head(self, html_data: Dict[str, Any]) -> str:
|
|
188
|
+
"""Generate HTML head section with styles and scripts.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
html_data: Enhanced HTML report data
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
HTML head section as string
|
|
195
|
+
"""
|
|
196
|
+
metadata = html_data["metadata"]
|
|
197
|
+
exec_summary = html_data["executive_summary"]
|
|
198
|
+
|
|
199
|
+
# Include Chart.js if charts are enabled
|
|
200
|
+
chart_script = ""
|
|
201
|
+
if self.include_charts:
|
|
202
|
+
chart_script = '<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>'
|
|
203
|
+
|
|
204
|
+
head_content = f"""<head>
|
|
205
|
+
<meta charset="UTF-8">
|
|
206
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
207
|
+
<title>CIS Controls Compliance Report - {metadata.get('account_id', 'Unknown')}</title>
|
|
208
|
+
<meta name="description" content="AWS CIS Controls compliance assessment report">
|
|
209
|
+
<meta name="author" content="AWS CIS Assessment Tool">
|
|
210
|
+
<meta name="report-date" content="{metadata.get('report_generated_at', '')}">
|
|
211
|
+
|
|
212
|
+
{chart_script}
|
|
213
|
+
|
|
214
|
+
<style>
|
|
215
|
+
{self._get_css_styles()}
|
|
216
|
+
</style>
|
|
217
|
+
|
|
218
|
+
<script>
|
|
219
|
+
{self._get_javascript_code(html_data)}
|
|
220
|
+
</script>
|
|
221
|
+
</head>"""
|
|
222
|
+
|
|
223
|
+
return head_content
|
|
224
|
+
|
|
225
|
+
def _generate_html_body(self, html_data: Dict[str, Any]) -> str:
|
|
226
|
+
"""Generate HTML body section with content.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
html_data: Enhanced HTML report data
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
HTML body section as string
|
|
233
|
+
"""
|
|
234
|
+
# Generate main content sections
|
|
235
|
+
header = self._generate_header(html_data)
|
|
236
|
+
navigation = self._generate_navigation(html_data)
|
|
237
|
+
executive_dashboard = self._generate_executive_dashboard(html_data)
|
|
238
|
+
implementation_groups = self._generate_implementation_groups_section(html_data)
|
|
239
|
+
detailed_findings = self._generate_detailed_findings_section(html_data)
|
|
240
|
+
resource_details = self._generate_resource_details_section(html_data)
|
|
241
|
+
remediation_section = self._generate_remediation_section(html_data)
|
|
242
|
+
footer = self._generate_footer(html_data)
|
|
243
|
+
|
|
244
|
+
body_content = f"""<body>
|
|
245
|
+
<div class="container">
|
|
246
|
+
{header}
|
|
247
|
+
{navigation}
|
|
248
|
+
{executive_dashboard}
|
|
249
|
+
{implementation_groups}
|
|
250
|
+
{detailed_findings}
|
|
251
|
+
{resource_details}
|
|
252
|
+
{remediation_section}
|
|
253
|
+
{footer}
|
|
254
|
+
</div>
|
|
255
|
+
|
|
256
|
+
<script>
|
|
257
|
+
// Initialize interactive features after DOM load
|
|
258
|
+
document.addEventListener('DOMContentLoaded', function() {{
|
|
259
|
+
initializeCharts();
|
|
260
|
+
initializeInteractivity();
|
|
261
|
+
}});
|
|
262
|
+
</script>
|
|
263
|
+
</body>"""
|
|
264
|
+
|
|
265
|
+
return body_content
|
|
266
|
+
|
|
267
|
+
def _get_css_styles(self) -> str:
|
|
268
|
+
"""Get CSS styles for the HTML report.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
CSS styles as string
|
|
272
|
+
"""
|
|
273
|
+
return """
|
|
274
|
+
/* Reset and base styles */
|
|
275
|
+
* {
|
|
276
|
+
margin: 0;
|
|
277
|
+
padding: 0;
|
|
278
|
+
box-sizing: border-box;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
body {
|
|
282
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
283
|
+
line-height: 1.6;
|
|
284
|
+
color: #333;
|
|
285
|
+
background-color: #f5f5f5;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.container {
|
|
289
|
+
max-width: 1200px;
|
|
290
|
+
margin: 0 auto;
|
|
291
|
+
padding: 20px;
|
|
292
|
+
background-color: white;
|
|
293
|
+
box-shadow: 0 0 20px rgba(0,0,0,0.1);
|
|
294
|
+
min-height: 100vh;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/* Header styles */
|
|
298
|
+
.header {
|
|
299
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
300
|
+
color: white;
|
|
301
|
+
padding: 30px;
|
|
302
|
+
border-radius: 10px;
|
|
303
|
+
margin-bottom: 30px;
|
|
304
|
+
text-align: center;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.header h1 {
|
|
308
|
+
font-size: 2.5em;
|
|
309
|
+
margin-bottom: 10px;
|
|
310
|
+
font-weight: 300;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.header .subtitle {
|
|
314
|
+
font-size: 1.2em;
|
|
315
|
+
opacity: 0.9;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/* Navigation styles */
|
|
319
|
+
.navigation {
|
|
320
|
+
background-color: #2c3e50;
|
|
321
|
+
border-radius: 8px;
|
|
322
|
+
margin-bottom: 30px;
|
|
323
|
+
overflow: hidden;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
.nav-list {
|
|
327
|
+
display: flex;
|
|
328
|
+
list-style: none;
|
|
329
|
+
flex-wrap: wrap;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
.nav-item {
|
|
333
|
+
flex: 1;
|
|
334
|
+
min-width: 150px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.nav-link {
|
|
338
|
+
display: block;
|
|
339
|
+
padding: 15px 20px;
|
|
340
|
+
color: white;
|
|
341
|
+
text-decoration: none;
|
|
342
|
+
text-align: center;
|
|
343
|
+
transition: background-color 0.3s;
|
|
344
|
+
border-right: 1px solid #34495e;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.nav-link:hover, .nav-link.active {
|
|
348
|
+
background-color: #3498db;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/* Dashboard styles */
|
|
352
|
+
.dashboard {
|
|
353
|
+
margin-bottom: 40px;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.dashboard-grid {
|
|
357
|
+
display: grid;
|
|
358
|
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
359
|
+
gap: 20px;
|
|
360
|
+
margin-bottom: 30px;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
.metric-card {
|
|
364
|
+
background: white;
|
|
365
|
+
border-radius: 10px;
|
|
366
|
+
padding: 25px;
|
|
367
|
+
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
368
|
+
border-left: 4px solid #3498db;
|
|
369
|
+
transition: transform 0.2s;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.metric-card:hover {
|
|
373
|
+
transform: translateY(-2px);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.metric-card.excellent { border-left-color: #27ae60; }
|
|
377
|
+
.metric-card.good { border-left-color: #2ecc71; }
|
|
378
|
+
.metric-card.fair { border-left-color: #f39c12; }
|
|
379
|
+
.metric-card.poor { border-left-color: #e67e22; }
|
|
380
|
+
.metric-card.critical { border-left-color: #e74c3c; }
|
|
381
|
+
|
|
382
|
+
.metric-value {
|
|
383
|
+
font-size: 2.5em;
|
|
384
|
+
font-weight: bold;
|
|
385
|
+
margin-bottom: 5px;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.metric-label {
|
|
389
|
+
color: #666;
|
|
390
|
+
font-size: 0.9em;
|
|
391
|
+
text-transform: uppercase;
|
|
392
|
+
letter-spacing: 1px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.metric-trend {
|
|
396
|
+
font-size: 0.8em;
|
|
397
|
+
margin-top: 10px;
|
|
398
|
+
padding: 5px 10px;
|
|
399
|
+
border-radius: 15px;
|
|
400
|
+
display: inline-block;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
.trend-up { background-color: #d5f4e6; color: #27ae60; }
|
|
404
|
+
.trend-down { background-color: #ffeaa7; color: #e17055; }
|
|
405
|
+
.trend-stable { background-color: #e3f2fd; color: #2196f3; }
|
|
406
|
+
|
|
407
|
+
/* Progress bars */
|
|
408
|
+
.progress-container {
|
|
409
|
+
background-color: #ecf0f1;
|
|
410
|
+
border-radius: 10px;
|
|
411
|
+
height: 20px;
|
|
412
|
+
margin: 10px 0;
|
|
413
|
+
overflow: hidden;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
.progress-bar {
|
|
417
|
+
height: 100%;
|
|
418
|
+
border-radius: 10px;
|
|
419
|
+
transition: width 0.8s ease-in-out;
|
|
420
|
+
position: relative;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
.progress-bar.excellent { background-color: #27ae60; }
|
|
424
|
+
.progress-bar.good { background-color: #2ecc71; }
|
|
425
|
+
.progress-bar.fair { background-color: #f39c12; }
|
|
426
|
+
.progress-bar.poor { background-color: #e67e22; }
|
|
427
|
+
.progress-bar.critical { background-color: #e74c3c; }
|
|
428
|
+
|
|
429
|
+
.progress-text {
|
|
430
|
+
position: absolute;
|
|
431
|
+
right: 10px;
|
|
432
|
+
top: 50%;
|
|
433
|
+
transform: translateY(-50%);
|
|
434
|
+
color: white;
|
|
435
|
+
font-weight: bold;
|
|
436
|
+
font-size: 0.8em;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* Implementation Groups */
|
|
440
|
+
.ig-section {
|
|
441
|
+
margin-bottom: 40px;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.ig-header {
|
|
445
|
+
background: linear-gradient(90deg, #74b9ff 0%, #0984e3 100%);
|
|
446
|
+
color: white;
|
|
447
|
+
padding: 20px;
|
|
448
|
+
border-radius: 10px 10px 0 0;
|
|
449
|
+
display: flex;
|
|
450
|
+
justify-content: space-between;
|
|
451
|
+
align-items: center;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.ig-title {
|
|
455
|
+
font-size: 1.5em;
|
|
456
|
+
font-weight: 600;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
.ig-score {
|
|
460
|
+
font-size: 2em;
|
|
461
|
+
font-weight: bold;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.ig-content {
|
|
465
|
+
background: white;
|
|
466
|
+
border: 1px solid #ddd;
|
|
467
|
+
border-top: none;
|
|
468
|
+
border-radius: 0 0 10px 10px;
|
|
469
|
+
padding: 20px;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
.controls-grid {
|
|
473
|
+
display: grid;
|
|
474
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
475
|
+
gap: 20px;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.control-card {
|
|
479
|
+
border: 1px solid #e0e0e0;
|
|
480
|
+
border-radius: 8px;
|
|
481
|
+
padding: 20px;
|
|
482
|
+
background: #fafafa;
|
|
483
|
+
transition: box-shadow 0.2s;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.control-card:hover {
|
|
487
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
.control-header {
|
|
491
|
+
display: flex;
|
|
492
|
+
justify-content: space-between;
|
|
493
|
+
align-items: center;
|
|
494
|
+
margin-bottom: 15px;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
.control-id {
|
|
498
|
+
font-weight: bold;
|
|
499
|
+
color: #2c3e50;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
.control-title {
|
|
503
|
+
font-size: 0.9em;
|
|
504
|
+
color: #666;
|
|
505
|
+
margin-bottom: 10px;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/* Badges */
|
|
509
|
+
.badge {
|
|
510
|
+
padding: 4px 8px;
|
|
511
|
+
border-radius: 12px;
|
|
512
|
+
font-size: 0.75em;
|
|
513
|
+
font-weight: bold;
|
|
514
|
+
text-transform: uppercase;
|
|
515
|
+
letter-spacing: 0.5px;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
.badge.high { background-color: #e74c3c; color: white; }
|
|
519
|
+
.badge.medium { background-color: #f39c12; color: white; }
|
|
520
|
+
.badge.low { background-color: #27ae60; color: white; }
|
|
521
|
+
|
|
522
|
+
.badge.effort-minimal { background-color: #2ecc71; color: white; }
|
|
523
|
+
.badge.effort-moderate { background-color: #f39c12; color: white; }
|
|
524
|
+
.badge.effort-significant { background-color: #e67e22; color: white; }
|
|
525
|
+
.badge.effort-extensive { background-color: #e74c3c; color: white; }
|
|
526
|
+
|
|
527
|
+
.badge.compliant { background-color: #27ae60; color: white; }
|
|
528
|
+
.badge.non_compliant { background-color: #e74c3c; color: white; }
|
|
529
|
+
|
|
530
|
+
/* Inheritance indicators */
|
|
531
|
+
.inheritance-note {
|
|
532
|
+
color: #666;
|
|
533
|
+
font-style: italic;
|
|
534
|
+
display: block;
|
|
535
|
+
margin-top: 5px;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
.ig-explanation {
|
|
539
|
+
background-color: #e8f4fd;
|
|
540
|
+
border-left: 4px solid #3498db;
|
|
541
|
+
padding: 15px;
|
|
542
|
+
margin-bottom: 30px;
|
|
543
|
+
border-radius: 5px;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
.ig-scope {
|
|
547
|
+
color: #666;
|
|
548
|
+
font-size: 0.9em;
|
|
549
|
+
margin-top: 5px;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/* Resource Details Section */
|
|
553
|
+
.resource-details {
|
|
554
|
+
margin-bottom: 40px;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.resource-summary {
|
|
558
|
+
margin-bottom: 30px;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.resource-stats-grid {
|
|
562
|
+
display: grid;
|
|
563
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
564
|
+
gap: 20px;
|
|
565
|
+
margin-bottom: 20px;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
.resource-stat-card {
|
|
569
|
+
background: white;
|
|
570
|
+
border-radius: 8px;
|
|
571
|
+
padding: 20px;
|
|
572
|
+
text-align: center;
|
|
573
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
574
|
+
border-left: 4px solid #3498db;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.resource-stat-card.compliant {
|
|
578
|
+
border-left-color: #27ae60;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.resource-stat-card.non-compliant {
|
|
582
|
+
border-left-color: #e74c3c;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.stat-value {
|
|
586
|
+
font-size: 2em;
|
|
587
|
+
font-weight: bold;
|
|
588
|
+
margin-bottom: 5px;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.stat-label {
|
|
592
|
+
color: #666;
|
|
593
|
+
font-size: 0.9em;
|
|
594
|
+
text-transform: uppercase;
|
|
595
|
+
letter-spacing: 1px;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
.resource-type-breakdown {
|
|
599
|
+
margin-bottom: 30px;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
.resource-type-grid {
|
|
603
|
+
display: grid;
|
|
604
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
605
|
+
gap: 15px;
|
|
606
|
+
margin-top: 15px;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.resource-type-stat {
|
|
610
|
+
background: white;
|
|
611
|
+
border-radius: 8px;
|
|
612
|
+
padding: 15px;
|
|
613
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
.resource-type-header {
|
|
617
|
+
display: flex;
|
|
618
|
+
justify-content: space-between;
|
|
619
|
+
align-items: center;
|
|
620
|
+
margin-bottom: 10px;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.resource-type-name {
|
|
624
|
+
font-weight: 600;
|
|
625
|
+
color: #2c3e50;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
.resource-type-count {
|
|
629
|
+
font-size: 0.9em;
|
|
630
|
+
color: #666;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
.resource-table-container {
|
|
634
|
+
margin-bottom: 20px;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.resource-filters {
|
|
638
|
+
display: flex;
|
|
639
|
+
gap: 15px;
|
|
640
|
+
margin-bottom: 20px;
|
|
641
|
+
flex-wrap: wrap;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.search-input, .filter-select {
|
|
645
|
+
padding: 10px;
|
|
646
|
+
border: 1px solid #ddd;
|
|
647
|
+
border-radius: 5px;
|
|
648
|
+
font-size: 14px;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.search-input {
|
|
652
|
+
flex: 1;
|
|
653
|
+
min-width: 200px;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
.filter-select {
|
|
657
|
+
min-width: 150px;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.resource-table {
|
|
661
|
+
font-size: 0.9em;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
.resource-table th {
|
|
665
|
+
cursor: pointer;
|
|
666
|
+
user-select: none;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.resource-table th:hover {
|
|
670
|
+
background-color: #2c3e50;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
.resource-row.compliant {
|
|
674
|
+
background-color: #f8fff8;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
.resource-row.non_compliant {
|
|
678
|
+
background-color: #fff8f8;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.evaluation-reason {
|
|
682
|
+
max-width: 300px;
|
|
683
|
+
word-wrap: break-word;
|
|
684
|
+
font-size: 0.85em;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.resource-export {
|
|
688
|
+
text-align: center;
|
|
689
|
+
margin-top: 20px;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.export-btn {
|
|
693
|
+
background-color: #3498db;
|
|
694
|
+
color: white;
|
|
695
|
+
border: none;
|
|
696
|
+
padding: 10px 20px;
|
|
697
|
+
border-radius: 5px;
|
|
698
|
+
cursor: pointer;
|
|
699
|
+
margin: 0 10px;
|
|
700
|
+
font-size: 14px;
|
|
701
|
+
transition: background-color 0.3s;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
.export-btn:hover {
|
|
705
|
+
background-color: #2980b9;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
/* Tables */
|
|
709
|
+
.findings-table {
|
|
710
|
+
width: 100%;
|
|
711
|
+
border-collapse: collapse;
|
|
712
|
+
margin-top: 20px;
|
|
713
|
+
background: white;
|
|
714
|
+
border-radius: 8px;
|
|
715
|
+
overflow: hidden;
|
|
716
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
.findings-table th {
|
|
720
|
+
background-color: #34495e;
|
|
721
|
+
color: white;
|
|
722
|
+
padding: 15px;
|
|
723
|
+
text-align: left;
|
|
724
|
+
font-weight: 600;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
.findings-table td {
|
|
728
|
+
padding: 12px 15px;
|
|
729
|
+
border-bottom: 1px solid #eee;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
.findings-table tr:hover {
|
|
733
|
+
background-color: #f8f9fa;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/* Collapsible sections */
|
|
737
|
+
.collapsible {
|
|
738
|
+
cursor: pointer;
|
|
739
|
+
padding: 15px;
|
|
740
|
+
background-color: #f1f2f6;
|
|
741
|
+
border: none;
|
|
742
|
+
text-align: left;
|
|
743
|
+
outline: none;
|
|
744
|
+
font-size: 1em;
|
|
745
|
+
width: 100%;
|
|
746
|
+
border-radius: 5px;
|
|
747
|
+
margin-bottom: 5px;
|
|
748
|
+
transition: background-color 0.3s;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
.collapsible:hover {
|
|
752
|
+
background-color: #ddd;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
.collapsible.active {
|
|
756
|
+
background-color: #3498db;
|
|
757
|
+
color: white;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
.collapsible-content {
|
|
761
|
+
padding: 0 15px;
|
|
762
|
+
max-height: 0;
|
|
763
|
+
overflow: hidden;
|
|
764
|
+
transition: max-height 0.3s ease-out;
|
|
765
|
+
background-color: white;
|
|
766
|
+
border-radius: 0 0 5px 5px;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
.collapsible-content.active {
|
|
770
|
+
max-height: 1000px;
|
|
771
|
+
padding: 15px;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
/* Charts */
|
|
775
|
+
.chart-container {
|
|
776
|
+
position: relative;
|
|
777
|
+
height: 400px;
|
|
778
|
+
margin: 20px 0;
|
|
779
|
+
background: white;
|
|
780
|
+
border-radius: 10px;
|
|
781
|
+
padding: 20px;
|
|
782
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/* Footer */
|
|
786
|
+
.footer {
|
|
787
|
+
margin-top: 50px;
|
|
788
|
+
padding: 30px;
|
|
789
|
+
background-color: #2c3e50;
|
|
790
|
+
color: white;
|
|
791
|
+
border-radius: 10px;
|
|
792
|
+
text-align: center;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
.footer-content {
|
|
796
|
+
display: grid;
|
|
797
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
798
|
+
gap: 20px;
|
|
799
|
+
margin-bottom: 20px;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.footer-section h4 {
|
|
803
|
+
margin-bottom: 10px;
|
|
804
|
+
color: #3498db;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* Responsive design */
|
|
808
|
+
@media (max-width: 768px) {
|
|
809
|
+
.container {
|
|
810
|
+
padding: 10px;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
.header h1 {
|
|
814
|
+
font-size: 2em;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.nav-list {
|
|
818
|
+
flex-direction: column;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
.nav-link {
|
|
822
|
+
border-right: none;
|
|
823
|
+
border-bottom: 1px solid #34495e;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
.dashboard-grid {
|
|
827
|
+
grid-template-columns: 1fr;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.controls-grid {
|
|
831
|
+
grid-template-columns: 1fr;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.ig-header {
|
|
835
|
+
flex-direction: column;
|
|
836
|
+
text-align: center;
|
|
837
|
+
gap: 10px;
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
.findings-table {
|
|
841
|
+
font-size: 0.8em;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
.findings-table th,
|
|
845
|
+
.findings-table td {
|
|
846
|
+
padding: 8px;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
@media (max-width: 480px) {
|
|
851
|
+
.header h1 {
|
|
852
|
+
font-size: 1.5em;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
.metric-value {
|
|
856
|
+
font-size: 2em;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
.chart-container {
|
|
860
|
+
height: 300px;
|
|
861
|
+
padding: 10px;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/* Print styles */
|
|
866
|
+
@media print {
|
|
867
|
+
.navigation {
|
|
868
|
+
display: none;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
.container {
|
|
872
|
+
box-shadow: none;
|
|
873
|
+
max-width: none;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.collapsible-content {
|
|
877
|
+
max-height: none !important;
|
|
878
|
+
padding: 15px !important;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
.chart-container {
|
|
882
|
+
break-inside: avoid;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
"""
|
|
886
|
+
|
|
887
|
+
def _get_javascript_code(self, html_data: Dict[str, Any]) -> str:
|
|
888
|
+
"""Get JavaScript code for interactive features.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
html_data: Enhanced HTML report data
|
|
892
|
+
|
|
893
|
+
Returns:
|
|
894
|
+
JavaScript code as string
|
|
895
|
+
"""
|
|
896
|
+
chart_data_json = str(html_data.get("chart_data", {})).replace("'", '"')
|
|
897
|
+
|
|
898
|
+
return f"""
|
|
899
|
+
// Chart data
|
|
900
|
+
const chartData = {chart_data_json};
|
|
901
|
+
|
|
902
|
+
// Initialize charts
|
|
903
|
+
function initializeCharts() {{
|
|
904
|
+
if (typeof Chart === 'undefined') {{
|
|
905
|
+
console.log('Chart.js not loaded, skipping chart initialization');
|
|
906
|
+
return;
|
|
907
|
+
}}
|
|
908
|
+
|
|
909
|
+
// Implementation Groups Compliance Chart
|
|
910
|
+
const igChartCtx = document.getElementById('igComplianceChart');
|
|
911
|
+
if (igChartCtx) {{
|
|
912
|
+
new Chart(igChartCtx, {{
|
|
913
|
+
type: 'doughnut',
|
|
914
|
+
data: chartData.igCompliance,
|
|
915
|
+
options: {{
|
|
916
|
+
responsive: true,
|
|
917
|
+
maintainAspectRatio: false,
|
|
918
|
+
plugins: {{
|
|
919
|
+
legend: {{
|
|
920
|
+
position: 'bottom'
|
|
921
|
+
}},
|
|
922
|
+
title: {{
|
|
923
|
+
display: true,
|
|
924
|
+
text: 'Implementation Groups Compliance'
|
|
925
|
+
}}
|
|
926
|
+
}}
|
|
927
|
+
}}
|
|
928
|
+
}});
|
|
929
|
+
}}
|
|
930
|
+
|
|
931
|
+
// Overall Compliance Trend Chart
|
|
932
|
+
const trendChartCtx = document.getElementById('complianceTrendChart');
|
|
933
|
+
if (trendChartCtx) {{
|
|
934
|
+
new Chart(trendChartCtx, {{
|
|
935
|
+
type: 'bar',
|
|
936
|
+
data: chartData.complianceTrend,
|
|
937
|
+
options: {{
|
|
938
|
+
responsive: true,
|
|
939
|
+
maintainAspectRatio: false,
|
|
940
|
+
scales: {{
|
|
941
|
+
y: {{
|
|
942
|
+
beginAtZero: true,
|
|
943
|
+
max: 100
|
|
944
|
+
}}
|
|
945
|
+
}},
|
|
946
|
+
plugins: {{
|
|
947
|
+
legend: {{
|
|
948
|
+
display: false
|
|
949
|
+
}},
|
|
950
|
+
title: {{
|
|
951
|
+
display: true,
|
|
952
|
+
text: 'Compliance by Implementation Group'
|
|
953
|
+
}}
|
|
954
|
+
}}
|
|
955
|
+
}}
|
|
956
|
+
}});
|
|
957
|
+
}}
|
|
958
|
+
|
|
959
|
+
// Risk Distribution Chart
|
|
960
|
+
const riskChartCtx = document.getElementById('riskDistributionChart');
|
|
961
|
+
if (riskChartCtx) {{
|
|
962
|
+
new Chart(riskChartCtx, {{
|
|
963
|
+
type: 'pie',
|
|
964
|
+
data: chartData.riskDistribution,
|
|
965
|
+
options: {{
|
|
966
|
+
responsive: true,
|
|
967
|
+
maintainAspectRatio: false,
|
|
968
|
+
plugins: {{
|
|
969
|
+
legend: {{
|
|
970
|
+
position: 'right'
|
|
971
|
+
}},
|
|
972
|
+
title: {{
|
|
973
|
+
display: true,
|
|
974
|
+
text: 'Risk Level Distribution'
|
|
975
|
+
}}
|
|
976
|
+
}}
|
|
977
|
+
}}
|
|
978
|
+
}});
|
|
979
|
+
}}
|
|
980
|
+
}}
|
|
981
|
+
|
|
982
|
+
// Initialize interactive features
|
|
983
|
+
function initializeInteractivity() {{
|
|
984
|
+
// Collapsible sections
|
|
985
|
+
const collapsibles = document.querySelectorAll('.collapsible');
|
|
986
|
+
collapsibles.forEach(function(collapsible) {{
|
|
987
|
+
collapsible.addEventListener('click', function() {{
|
|
988
|
+
this.classList.toggle('active');
|
|
989
|
+
const content = this.nextElementSibling;
|
|
990
|
+
content.classList.toggle('active');
|
|
991
|
+
}});
|
|
992
|
+
}});
|
|
993
|
+
|
|
994
|
+
// Navigation smooth scrolling
|
|
995
|
+
const navLinks = document.querySelectorAll('.nav-link');
|
|
996
|
+
navLinks.forEach(function(link) {{
|
|
997
|
+
link.addEventListener('click', function(e) {{
|
|
998
|
+
e.preventDefault();
|
|
999
|
+
const targetId = this.getAttribute('href').substring(1);
|
|
1000
|
+
const targetElement = document.getElementById(targetId);
|
|
1001
|
+
if (targetElement) {{
|
|
1002
|
+
targetElement.scrollIntoView({{
|
|
1003
|
+
behavior: 'smooth',
|
|
1004
|
+
block: 'start'
|
|
1005
|
+
}});
|
|
1006
|
+
|
|
1007
|
+
// Update active nav item
|
|
1008
|
+
navLinks.forEach(nl => nl.classList.remove('active'));
|
|
1009
|
+
this.classList.add('active');
|
|
1010
|
+
}}
|
|
1011
|
+
}});
|
|
1012
|
+
}});
|
|
1013
|
+
|
|
1014
|
+
// Progress bar animations
|
|
1015
|
+
const progressBars = document.querySelectorAll('.progress-bar');
|
|
1016
|
+
const observer = new IntersectionObserver(function(entries) {{
|
|
1017
|
+
entries.forEach(function(entry) {{
|
|
1018
|
+
if (entry.isIntersecting) {{
|
|
1019
|
+
const progressBar = entry.target;
|
|
1020
|
+
const width = progressBar.getAttribute('data-width');
|
|
1021
|
+
progressBar.style.width = width + '%';
|
|
1022
|
+
}}
|
|
1023
|
+
}});
|
|
1024
|
+
}});
|
|
1025
|
+
|
|
1026
|
+
progressBars.forEach(function(bar) {{
|
|
1027
|
+
observer.observe(bar);
|
|
1028
|
+
}});
|
|
1029
|
+
|
|
1030
|
+
// Table sorting
|
|
1031
|
+
const tables = document.querySelectorAll('.findings-table');
|
|
1032
|
+
tables.forEach(function(table) {{
|
|
1033
|
+
const headers = table.querySelectorAll('th');
|
|
1034
|
+
headers.forEach(function(header, index) {{
|
|
1035
|
+
header.style.cursor = 'pointer';
|
|
1036
|
+
header.addEventListener('click', function() {{
|
|
1037
|
+
sortTable(table, index);
|
|
1038
|
+
}});
|
|
1039
|
+
}});
|
|
1040
|
+
}});
|
|
1041
|
+
}}
|
|
1042
|
+
|
|
1043
|
+
// Table sorting function
|
|
1044
|
+
function sortTable(table, columnIndex) {{
|
|
1045
|
+
const tbody = table.querySelector('tbody');
|
|
1046
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
1047
|
+
|
|
1048
|
+
const isNumeric = rows.every(row => {{
|
|
1049
|
+
const cell = row.cells[columnIndex];
|
|
1050
|
+
return cell && !isNaN(parseFloat(cell.textContent));
|
|
1051
|
+
}});
|
|
1052
|
+
|
|
1053
|
+
rows.sort(function(a, b) {{
|
|
1054
|
+
const aVal = a.cells[columnIndex].textContent.trim();
|
|
1055
|
+
const bVal = b.cells[columnIndex].textContent.trim();
|
|
1056
|
+
|
|
1057
|
+
if (isNumeric) {{
|
|
1058
|
+
return parseFloat(aVal) - parseFloat(bVal);
|
|
1059
|
+
}} else {{
|
|
1060
|
+
return aVal.localeCompare(bVal);
|
|
1061
|
+
}}
|
|
1062
|
+
}});
|
|
1063
|
+
|
|
1064
|
+
rows.forEach(function(row) {{
|
|
1065
|
+
tbody.appendChild(row);
|
|
1066
|
+
}});
|
|
1067
|
+
}}
|
|
1068
|
+
|
|
1069
|
+
// Search functionality
|
|
1070
|
+
function searchFindings(searchTerm) {{
|
|
1071
|
+
const tables = document.querySelectorAll('.findings-table tbody tr');
|
|
1072
|
+
tables.forEach(function(row) {{
|
|
1073
|
+
const text = row.textContent.toLowerCase();
|
|
1074
|
+
const matches = text.includes(searchTerm.toLowerCase());
|
|
1075
|
+
row.style.display = matches ? '' : 'none';
|
|
1076
|
+
}});
|
|
1077
|
+
}}
|
|
1078
|
+
|
|
1079
|
+
// Export functionality
|
|
1080
|
+
function exportToCSV() {{
|
|
1081
|
+
const tables = document.querySelectorAll('.findings-table');
|
|
1082
|
+
let csvContent = '';
|
|
1083
|
+
|
|
1084
|
+
tables.forEach(function(table) {{
|
|
1085
|
+
const rows = table.querySelectorAll('tr');
|
|
1086
|
+
rows.forEach(function(row) {{
|
|
1087
|
+
const cells = row.querySelectorAll('th, td');
|
|
1088
|
+
const rowData = Array.from(cells).map(cell =>
|
|
1089
|
+
'"' + cell.textContent.replace(/"/g, '""') + '"'
|
|
1090
|
+
).join(',');
|
|
1091
|
+
csvContent += rowData + '\\n';
|
|
1092
|
+
}});
|
|
1093
|
+
csvContent += '\\n';
|
|
1094
|
+
}});
|
|
1095
|
+
|
|
1096
|
+
const blob = new Blob([csvContent], {{ type: 'text/csv' }});
|
|
1097
|
+
const url = window.URL.createObjectURL(blob);
|
|
1098
|
+
const a = document.createElement('a');
|
|
1099
|
+
a.href = url;
|
|
1100
|
+
a.download = 'cis-compliance-findings.csv';
|
|
1101
|
+
a.click();
|
|
1102
|
+
window.URL.revokeObjectURL(url);
|
|
1103
|
+
}}
|
|
1104
|
+
|
|
1105
|
+
// Resource filtering functionality
|
|
1106
|
+
function filterResources() {{
|
|
1107
|
+
const searchTerm = document.getElementById('resourceSearch').value.toLowerCase();
|
|
1108
|
+
const statusFilter = document.getElementById('statusFilter').value;
|
|
1109
|
+
const typeFilter = document.getElementById('typeFilter').value;
|
|
1110
|
+
const rows = document.querySelectorAll('#resourceTable tbody tr');
|
|
1111
|
+
|
|
1112
|
+
rows.forEach(function(row) {{
|
|
1113
|
+
const cells = row.querySelectorAll('td');
|
|
1114
|
+
const resourceId = cells[0].textContent.toLowerCase();
|
|
1115
|
+
const resourceType = cells[1].textContent;
|
|
1116
|
+
const status = cells[3].textContent.includes('COMPLIANT') ?
|
|
1117
|
+
(cells[3].textContent.includes('NON_COMPLIANT') ? 'NON_COMPLIANT' : 'COMPLIANT') : 'NON_COMPLIANT';
|
|
1118
|
+
const evaluationReason = cells[6].textContent.toLowerCase();
|
|
1119
|
+
|
|
1120
|
+
const matchesSearch = resourceId.includes(searchTerm) ||
|
|
1121
|
+
resourceType.toLowerCase().includes(searchTerm) ||
|
|
1122
|
+
evaluationReason.includes(searchTerm);
|
|
1123
|
+
const matchesStatus = !statusFilter || status === statusFilter;
|
|
1124
|
+
const matchesType = !typeFilter || resourceType === typeFilter;
|
|
1125
|
+
|
|
1126
|
+
row.style.display = (matchesSearch && matchesStatus && matchesType) ? '' : 'none';
|
|
1127
|
+
}});
|
|
1128
|
+
}}
|
|
1129
|
+
|
|
1130
|
+
// Resource table sorting
|
|
1131
|
+
function sortResourceTable(columnIndex) {{
|
|
1132
|
+
const table = document.getElementById('resourceTable');
|
|
1133
|
+
const tbody = table.querySelector('tbody');
|
|
1134
|
+
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
1135
|
+
|
|
1136
|
+
const isNumeric = columnIndex === 3; // Status column - sort by compliance status
|
|
1137
|
+
|
|
1138
|
+
rows.sort(function(a, b) {{
|
|
1139
|
+
const aVal = a.cells[columnIndex].textContent.trim();
|
|
1140
|
+
const bVal = b.cells[columnIndex].textContent.trim();
|
|
1141
|
+
|
|
1142
|
+
if (columnIndex === 3) {{ // Status column - COMPLIANT before NON_COMPLIANT
|
|
1143
|
+
const aCompliant = aVal.includes('✓');
|
|
1144
|
+
const bCompliant = bVal.includes('✓');
|
|
1145
|
+
return bCompliant - aCompliant;
|
|
1146
|
+
}} else {{
|
|
1147
|
+
return aVal.localeCompare(bVal);
|
|
1148
|
+
}}
|
|
1149
|
+
}});
|
|
1150
|
+
|
|
1151
|
+
rows.forEach(function(row) {{
|
|
1152
|
+
tbody.appendChild(row);
|
|
1153
|
+
}});
|
|
1154
|
+
}}
|
|
1155
|
+
|
|
1156
|
+
// Export resources to CSV
|
|
1157
|
+
function exportResourcesToCSV() {{
|
|
1158
|
+
const table = document.getElementById('resourceTable');
|
|
1159
|
+
const rows = table.querySelectorAll('tr');
|
|
1160
|
+
let csvContent = '';
|
|
1161
|
+
|
|
1162
|
+
rows.forEach(function(row) {{
|
|
1163
|
+
const cells = row.querySelectorAll('th, td');
|
|
1164
|
+
const rowData = Array.from(cells).map(cell =>
|
|
1165
|
+
'"' + cell.textContent.replace(/"/g, '""').replace(/\\s+/g, ' ').trim() + '"'
|
|
1166
|
+
).join(',');
|
|
1167
|
+
csvContent += rowData + '\\n';
|
|
1168
|
+
}});
|
|
1169
|
+
|
|
1170
|
+
const blob = new Blob([csvContent], {{ type: 'text/csv' }});
|
|
1171
|
+
const url = window.URL.createObjectURL(blob);
|
|
1172
|
+
const a = document.createElement('a');
|
|
1173
|
+
a.href = url;
|
|
1174
|
+
a.download = 'cis-compliance-resources.csv';
|
|
1175
|
+
a.click();
|
|
1176
|
+
window.URL.revokeObjectURL(url);
|
|
1177
|
+
}}
|
|
1178
|
+
|
|
1179
|
+
// Export resources to JSON
|
|
1180
|
+
function exportResourcesToJSON() {{
|
|
1181
|
+
const table = document.getElementById('resourceTable');
|
|
1182
|
+
const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim());
|
|
1183
|
+
const rows = Array.from(table.querySelectorAll('tbody tr'));
|
|
1184
|
+
|
|
1185
|
+
const data = rows.map(row => {{
|
|
1186
|
+
const cells = Array.from(row.querySelectorAll('td'));
|
|
1187
|
+
const rowData = {{}};
|
|
1188
|
+
headers.forEach((header, index) => {{
|
|
1189
|
+
rowData[header] = cells[index] ? cells[index].textContent.replace(/\\s+/g, ' ').trim() : '';
|
|
1190
|
+
}});
|
|
1191
|
+
return rowData;
|
|
1192
|
+
}});
|
|
1193
|
+
|
|
1194
|
+
const jsonContent = JSON.stringify(data, null, 2);
|
|
1195
|
+
const blob = new Blob([jsonContent], {{ type: 'application/json' }});
|
|
1196
|
+
const url = window.URL.createObjectURL(blob);
|
|
1197
|
+
const a = document.createElement('a');
|
|
1198
|
+
a.href = url;
|
|
1199
|
+
a.download = 'cis-compliance-resources.json';
|
|
1200
|
+
a.click();
|
|
1201
|
+
window.URL.revokeObjectURL(url);
|
|
1202
|
+
}}
|
|
1203
|
+
"""
|
|
1204
|
+
|
|
1205
|
+
def _generate_header(self, html_data: Dict[str, Any]) -> str:
|
|
1206
|
+
"""Generate header section.
|
|
1207
|
+
|
|
1208
|
+
Args:
|
|
1209
|
+
html_data: Enhanced HTML report data
|
|
1210
|
+
|
|
1211
|
+
Returns:
|
|
1212
|
+
Header HTML as string
|
|
1213
|
+
"""
|
|
1214
|
+
metadata = html_data["metadata"]
|
|
1215
|
+
exec_summary = html_data["executive_summary"]
|
|
1216
|
+
|
|
1217
|
+
return f"""
|
|
1218
|
+
<header class="header">
|
|
1219
|
+
<h1>CIS Controls Compliance Report</h1>
|
|
1220
|
+
<div class="subtitle">
|
|
1221
|
+
AWS Account: {metadata.get('account_id', 'Unknown')} |
|
|
1222
|
+
Assessment Date: {datetime.fromisoformat(metadata.get('assessment_timestamp', '')).strftime('%B %d, %Y') if metadata.get('assessment_timestamp') else 'Unknown'} |
|
|
1223
|
+
Overall Compliance: {exec_summary.get('overall_compliance_percentage', 0):.1f}%
|
|
1224
|
+
</div>
|
|
1225
|
+
</header>
|
|
1226
|
+
"""
|
|
1227
|
+
|
|
1228
|
+
def _generate_navigation(self, html_data: Dict[str, Any]) -> str:
|
|
1229
|
+
"""Generate navigation section.
|
|
1230
|
+
|
|
1231
|
+
Args:
|
|
1232
|
+
html_data: Enhanced HTML report data
|
|
1233
|
+
|
|
1234
|
+
Returns:
|
|
1235
|
+
Navigation HTML as string
|
|
1236
|
+
"""
|
|
1237
|
+
nav_items = html_data.get("navigation", {}).get("sections", [])
|
|
1238
|
+
|
|
1239
|
+
nav_links = ""
|
|
1240
|
+
for item in nav_items:
|
|
1241
|
+
nav_links += f'<li class="nav-item"><a href="#{item["id"]}" class="nav-link">{item["title"]}</a></li>'
|
|
1242
|
+
|
|
1243
|
+
return f"""
|
|
1244
|
+
<nav class="navigation">
|
|
1245
|
+
<ul class="nav-list">
|
|
1246
|
+
{nav_links}
|
|
1247
|
+
</ul>
|
|
1248
|
+
</nav>
|
|
1249
|
+
"""
|
|
1250
|
+
|
|
1251
|
+
def _generate_executive_dashboard(self, html_data: Dict[str, Any]) -> str:
|
|
1252
|
+
"""Generate executive dashboard section.
|
|
1253
|
+
|
|
1254
|
+
Args:
|
|
1255
|
+
html_data: Enhanced HTML report data
|
|
1256
|
+
|
|
1257
|
+
Returns:
|
|
1258
|
+
Dashboard HTML as string
|
|
1259
|
+
"""
|
|
1260
|
+
exec_summary = html_data["executive_summary"]
|
|
1261
|
+
metadata = html_data["metadata"]
|
|
1262
|
+
|
|
1263
|
+
# Generate metric cards
|
|
1264
|
+
overall_status = self._get_status_class(exec_summary.get("overall_compliance_percentage", 0))
|
|
1265
|
+
|
|
1266
|
+
metric_cards = f"""
|
|
1267
|
+
<div class="metric-card {overall_status}">
|
|
1268
|
+
<div class="metric-value">{exec_summary.get('overall_compliance_percentage', 0):.1f}%</div>
|
|
1269
|
+
<div class="metric-label">Overall Compliance</div>
|
|
1270
|
+
<div class="metric-trend trend-stable">Grade: {exec_summary.get('compliance_grade', 'N/A')}</div>
|
|
1271
|
+
</div>
|
|
1272
|
+
|
|
1273
|
+
<div class="metric-card">
|
|
1274
|
+
<div class="metric-value">{exec_summary.get('total_resources', 0):,}</div>
|
|
1275
|
+
<div class="metric-label">Resources Evaluated</div>
|
|
1276
|
+
<div class="metric-trend trend-up">Across {len(metadata.get('regions_assessed', []))} regions</div>
|
|
1277
|
+
</div>
|
|
1278
|
+
|
|
1279
|
+
<div class="metric-card">
|
|
1280
|
+
<div class="metric-value">{exec_summary.get('compliant_resources', 0):,}</div>
|
|
1281
|
+
<div class="metric-label">Compliant Resources</div>
|
|
1282
|
+
<div class="metric-trend trend-up">{(exec_summary.get('compliant_resources', 0) / max(exec_summary.get('total_resources', 1), 1) * 100):.1f}% of total</div>
|
|
1283
|
+
</div>
|
|
1284
|
+
|
|
1285
|
+
<div class="metric-card">
|
|
1286
|
+
<div class="metric-value">{exec_summary.get('non_compliant_resources', 0):,}</div>
|
|
1287
|
+
<div class="metric-label">Non-Compliant Resources</div>
|
|
1288
|
+
<div class="metric-trend trend-down">Require attention</div>
|
|
1289
|
+
</div>
|
|
1290
|
+
"""
|
|
1291
|
+
|
|
1292
|
+
# Generate IG progress bars
|
|
1293
|
+
ig_progress = ""
|
|
1294
|
+
for ig in ['ig1', 'ig2', 'ig3']:
|
|
1295
|
+
ig_key = f"{ig}_compliance_percentage"
|
|
1296
|
+
ig_value = exec_summary.get(ig_key, 0)
|
|
1297
|
+
ig_status = self._get_status_class(ig_value)
|
|
1298
|
+
ig_name = ig.upper()
|
|
1299
|
+
|
|
1300
|
+
ig_progress += f"""
|
|
1301
|
+
<div class="ig-progress">
|
|
1302
|
+
<div class="ig-progress-header">
|
|
1303
|
+
<span>{ig_name} Compliance</span>
|
|
1304
|
+
<span>{ig_value:.1f}%</span>
|
|
1305
|
+
</div>
|
|
1306
|
+
<div class="progress-container">
|
|
1307
|
+
<div class="progress-bar {ig_status}" data-width="{ig_value}">
|
|
1308
|
+
<span class="progress-text">{ig_value:.1f}%</span>
|
|
1309
|
+
</div>
|
|
1310
|
+
</div>
|
|
1311
|
+
</div>
|
|
1312
|
+
"""
|
|
1313
|
+
|
|
1314
|
+
# Generate charts section
|
|
1315
|
+
charts_section = ""
|
|
1316
|
+
if self.include_charts:
|
|
1317
|
+
charts_section = f"""
|
|
1318
|
+
<div class="charts-section">
|
|
1319
|
+
<div class="chart-container">
|
|
1320
|
+
<canvas id="igComplianceChart"></canvas>
|
|
1321
|
+
</div>
|
|
1322
|
+
<div class="chart-container">
|
|
1323
|
+
<canvas id="complianceTrendChart"></canvas>
|
|
1324
|
+
</div>
|
|
1325
|
+
<div class="chart-container">
|
|
1326
|
+
<canvas id="riskDistributionChart"></canvas>
|
|
1327
|
+
</div>
|
|
1328
|
+
</div>
|
|
1329
|
+
"""
|
|
1330
|
+
|
|
1331
|
+
return f"""
|
|
1332
|
+
<section id="dashboard" class="dashboard">
|
|
1333
|
+
<h2>Executive Dashboard</h2>
|
|
1334
|
+
<div class="dashboard-grid">
|
|
1335
|
+
{metric_cards}
|
|
1336
|
+
</div>
|
|
1337
|
+
|
|
1338
|
+
<div class="ig-progress-section">
|
|
1339
|
+
<h3>Implementation Groups Progress</h3>
|
|
1340
|
+
{ig_progress}
|
|
1341
|
+
</div>
|
|
1342
|
+
|
|
1343
|
+
{charts_section}
|
|
1344
|
+
</section>
|
|
1345
|
+
"""
|
|
1346
|
+
|
|
1347
|
+
def _generate_implementation_groups_section(self, html_data: Dict[str, Any]) -> str:
|
|
1348
|
+
"""Generate Implementation Groups section with unique controls per IG.
|
|
1349
|
+
|
|
1350
|
+
Args:
|
|
1351
|
+
html_data: Enhanced HTML report data
|
|
1352
|
+
|
|
1353
|
+
Returns:
|
|
1354
|
+
Implementation Groups HTML as string
|
|
1355
|
+
"""
|
|
1356
|
+
ig_sections = ""
|
|
1357
|
+
|
|
1358
|
+
# Define which controls are unique to each IG to avoid duplication
|
|
1359
|
+
unique_controls = self._get_unique_controls_per_ig(html_data["implementation_groups"])
|
|
1360
|
+
|
|
1361
|
+
for ig_name, ig_data in html_data["implementation_groups"].items():
|
|
1362
|
+
controls_html = ""
|
|
1363
|
+
|
|
1364
|
+
# Only show controls that are unique to this IG or inherited controls for context
|
|
1365
|
+
controls_to_show = unique_controls.get(ig_name, {})
|
|
1366
|
+
|
|
1367
|
+
for control_id, control_data in controls_to_show.items():
|
|
1368
|
+
findings_count = len(control_data.get("non_compliant_findings", []))
|
|
1369
|
+
status_class = self._get_status_class(control_data["compliance_percentage"])
|
|
1370
|
+
|
|
1371
|
+
# Add inheritance indicator for inherited controls
|
|
1372
|
+
inheritance_indicator = ""
|
|
1373
|
+
if ig_name != "IG1" and control_id in unique_controls.get("IG1", {}):
|
|
1374
|
+
inheritance_indicator = f'<small class="inheritance-note">Inherited from IG1</small>'
|
|
1375
|
+
elif ig_name == "IG3" and control_id in unique_controls.get("IG2", {}):
|
|
1376
|
+
inheritance_indicator = f'<small class="inheritance-note">Inherited from IG2</small>'
|
|
1377
|
+
|
|
1378
|
+
controls_html += f"""
|
|
1379
|
+
<div class="control-card">
|
|
1380
|
+
<div class="control-header">
|
|
1381
|
+
<div class="control-id">{control_id}</div>
|
|
1382
|
+
<div class="badge {control_data.get('severity_badge', 'medium')}">{findings_count} Issues</div>
|
|
1383
|
+
</div>
|
|
1384
|
+
<div class="control-title">{control_data.get('title', f'CIS Control {control_id}')}</div>
|
|
1385
|
+
{inheritance_indicator}
|
|
1386
|
+
<div class="progress-container">
|
|
1387
|
+
<div class="progress-bar {status_class}" data-width="{control_data['compliance_percentage']}">
|
|
1388
|
+
<span class="progress-text">{control_data['compliance_percentage']:.1f}%</span>
|
|
1389
|
+
</div>
|
|
1390
|
+
</div>
|
|
1391
|
+
<div class="control-stats">
|
|
1392
|
+
<small>{control_data['compliant_resources']}/{control_data['total_resources']} resources compliant</small>
|
|
1393
|
+
</div>
|
|
1394
|
+
</div>
|
|
1395
|
+
"""
|
|
1396
|
+
|
|
1397
|
+
ig_status_class = self._get_status_class(ig_data["compliance_percentage"])
|
|
1398
|
+
|
|
1399
|
+
# Show summary of what this IG includes
|
|
1400
|
+
ig_description = self._get_ig_description_with_inheritance(ig_name)
|
|
1401
|
+
|
|
1402
|
+
ig_sections += f"""
|
|
1403
|
+
<div class="ig-section">
|
|
1404
|
+
<div class="ig-header">
|
|
1405
|
+
<div class="ig-title">{ig_name} - {ig_description}</div>
|
|
1406
|
+
<div class="ig-score">{ig_data['compliance_percentage']:.1f}%</div>
|
|
1407
|
+
</div>
|
|
1408
|
+
<div class="ig-content">
|
|
1409
|
+
<div class="ig-summary">
|
|
1410
|
+
<p><strong>{ig_data['compliant_controls']}</strong> of <strong>{ig_data['total_controls']}</strong> controls are compliant</p>
|
|
1411
|
+
<p class="ig-scope">{self._get_ig_scope_description(ig_name, len(controls_to_show))}</p>
|
|
1412
|
+
</div>
|
|
1413
|
+
<div class="controls-grid">
|
|
1414
|
+
{controls_html}
|
|
1415
|
+
</div>
|
|
1416
|
+
</div>
|
|
1417
|
+
</div>
|
|
1418
|
+
"""
|
|
1419
|
+
|
|
1420
|
+
return f"""
|
|
1421
|
+
<section id="implementation-groups" class="implementation-groups">
|
|
1422
|
+
<h2>Implementation Groups</h2>
|
|
1423
|
+
<div class="ig-explanation">
|
|
1424
|
+
<p><strong>Note:</strong> Implementation Groups are cumulative. IG2 includes all IG1 controls plus additional ones. IG3 includes all IG1 and IG2 controls plus advanced controls.</p>
|
|
1425
|
+
</div>
|
|
1426
|
+
{ig_sections}
|
|
1427
|
+
</section>
|
|
1428
|
+
"""
|
|
1429
|
+
|
|
1430
|
+
def _generate_detailed_findings_section(self, html_data: Dict[str, Any]) -> str:
|
|
1431
|
+
"""Generate detailed findings section.
|
|
1432
|
+
|
|
1433
|
+
Args:
|
|
1434
|
+
html_data: Enhanced HTML report data
|
|
1435
|
+
|
|
1436
|
+
Returns:
|
|
1437
|
+
Detailed findings HTML as string
|
|
1438
|
+
"""
|
|
1439
|
+
findings_sections = ""
|
|
1440
|
+
|
|
1441
|
+
for ig_name, ig_findings in html_data["detailed_findings"].items():
|
|
1442
|
+
ig_content = ""
|
|
1443
|
+
|
|
1444
|
+
for control_id, control_findings in ig_findings.items():
|
|
1445
|
+
if not control_findings:
|
|
1446
|
+
continue
|
|
1447
|
+
|
|
1448
|
+
findings_rows = ""
|
|
1449
|
+
for finding in control_findings:
|
|
1450
|
+
if finding["compliance_status"] == "NON_COMPLIANT":
|
|
1451
|
+
findings_rows += f"""
|
|
1452
|
+
<tr>
|
|
1453
|
+
<td>{finding['resource_id']}</td>
|
|
1454
|
+
<td>{finding['resource_type']}</td>
|
|
1455
|
+
<td>{finding['region']}</td>
|
|
1456
|
+
<td><span class="badge {finding['compliance_status'].lower()}">{finding['compliance_status']}</span></td>
|
|
1457
|
+
<td>{finding['evaluation_reason']}</td>
|
|
1458
|
+
<td>{finding['config_rule_name']}</td>
|
|
1459
|
+
</tr>
|
|
1460
|
+
"""
|
|
1461
|
+
|
|
1462
|
+
if findings_rows:
|
|
1463
|
+
ig_content += f"""
|
|
1464
|
+
<button class="collapsible">{control_id} - Non-Compliant Resources ({len([f for f in control_findings if f['compliance_status'] == 'NON_COMPLIANT'])} items)</button>
|
|
1465
|
+
<div class="collapsible-content">
|
|
1466
|
+
<table class="findings-table">
|
|
1467
|
+
<thead>
|
|
1468
|
+
<tr>
|
|
1469
|
+
<th>Resource ID</th>
|
|
1470
|
+
<th>Resource Type</th>
|
|
1471
|
+
<th>Region</th>
|
|
1472
|
+
<th>Compliance Status</th>
|
|
1473
|
+
<th>Reason</th>
|
|
1474
|
+
<th>Config Rule</th>
|
|
1475
|
+
</tr>
|
|
1476
|
+
</thead>
|
|
1477
|
+
<tbody>
|
|
1478
|
+
{findings_rows}
|
|
1479
|
+
</tbody>
|
|
1480
|
+
</table>
|
|
1481
|
+
</div>
|
|
1482
|
+
"""
|
|
1483
|
+
|
|
1484
|
+
if ig_content:
|
|
1485
|
+
findings_sections += f"""
|
|
1486
|
+
<div class="ig-findings">
|
|
1487
|
+
<h3>{ig_name} Detailed Findings</h3>
|
|
1488
|
+
{ig_content}
|
|
1489
|
+
</div>
|
|
1490
|
+
"""
|
|
1491
|
+
|
|
1492
|
+
return f"""
|
|
1493
|
+
<section id="detailed-findings" class="detailed-findings">
|
|
1494
|
+
<h2>Detailed Findings</h2>
|
|
1495
|
+
<div class="search-container">
|
|
1496
|
+
<input type="text" placeholder="Search findings..." onkeyup="searchFindings(this.value)" style="width: 100%; padding: 10px; margin-bottom: 20px; border: 1px solid #ddd; border-radius: 5px;">
|
|
1497
|
+
</div>
|
|
1498
|
+
{findings_sections}
|
|
1499
|
+
</section>
|
|
1500
|
+
"""
|
|
1501
|
+
|
|
1502
|
+
def _generate_remediation_section(self, html_data: Dict[str, Any]) -> str:
|
|
1503
|
+
"""Generate remediation section.
|
|
1504
|
+
|
|
1505
|
+
Args:
|
|
1506
|
+
html_data: Enhanced HTML report data
|
|
1507
|
+
|
|
1508
|
+
Returns:
|
|
1509
|
+
Remediation HTML as string
|
|
1510
|
+
"""
|
|
1511
|
+
remediation_items = ""
|
|
1512
|
+
|
|
1513
|
+
for remediation in html_data["remediation_priorities"]:
|
|
1514
|
+
steps_html = ""
|
|
1515
|
+
for step in remediation["remediation_steps"]:
|
|
1516
|
+
steps_html += f"<li>{step}</li>"
|
|
1517
|
+
|
|
1518
|
+
remediation_items += f"""
|
|
1519
|
+
<div class="remediation-item">
|
|
1520
|
+
<div class="remediation-header">
|
|
1521
|
+
<h4>{remediation['control_id']} - {remediation['config_rule_name']}</h4>
|
|
1522
|
+
<div class="remediation-badges">
|
|
1523
|
+
<span class="badge {remediation['priority_badge']}">{remediation['priority']}</span>
|
|
1524
|
+
<span class="badge {remediation['effort_badge']}">{remediation['estimated_effort']}</span>
|
|
1525
|
+
</div>
|
|
1526
|
+
</div>
|
|
1527
|
+
<div class="remediation-content">
|
|
1528
|
+
<h5>Remediation Steps:</h5>
|
|
1529
|
+
<ol>
|
|
1530
|
+
{steps_html}
|
|
1531
|
+
</ol>
|
|
1532
|
+
<p><strong>Documentation:</strong> <a href="{remediation['aws_documentation_link']}" target="_blank">AWS Documentation</a></p>
|
|
1533
|
+
</div>
|
|
1534
|
+
</div>
|
|
1535
|
+
"""
|
|
1536
|
+
|
|
1537
|
+
return f"""
|
|
1538
|
+
<section id="remediation" class="remediation">
|
|
1539
|
+
<h2>Remediation Priorities</h2>
|
|
1540
|
+
<div class="remediation-list">
|
|
1541
|
+
{remediation_items}
|
|
1542
|
+
</div>
|
|
1543
|
+
<div class="export-actions">
|
|
1544
|
+
<button onclick="exportToCSV()" class="export-btn">Export Findings to CSV</button>
|
|
1545
|
+
</div>
|
|
1546
|
+
</section>
|
|
1547
|
+
"""
|
|
1548
|
+
|
|
1549
|
+
def _generate_footer(self, html_data: Dict[str, Any]) -> str:
|
|
1550
|
+
"""Generate footer section.
|
|
1551
|
+
|
|
1552
|
+
Args:
|
|
1553
|
+
html_data: Enhanced HTML report data
|
|
1554
|
+
|
|
1555
|
+
Returns:
|
|
1556
|
+
Footer HTML as string
|
|
1557
|
+
"""
|
|
1558
|
+
metadata = html_data["metadata"]
|
|
1559
|
+
|
|
1560
|
+
return f"""
|
|
1561
|
+
<footer class="footer">
|
|
1562
|
+
<div class="footer-content">
|
|
1563
|
+
<div class="footer-section">
|
|
1564
|
+
<h4>Report Information</h4>
|
|
1565
|
+
<p>Generated: {datetime.fromisoformat(metadata.get('report_generated_at', '')).strftime('%B %d, %Y at %I:%M %p') if metadata.get('report_generated_at') else 'Unknown'}</p>
|
|
1566
|
+
<p>Assessment Duration: {metadata.get('assessment_duration', 'Unknown')}</p>
|
|
1567
|
+
<p>Report Version: {html_data.get('report_version', '1.0')}</p>
|
|
1568
|
+
</div>
|
|
1569
|
+
<div class="footer-section">
|
|
1570
|
+
<h4>Assessment Scope</h4>
|
|
1571
|
+
<p>AWS Account: {metadata.get('account_id', 'Unknown')}</p>
|
|
1572
|
+
<p>Regions: {', '.join(metadata.get('regions_assessed', []))}</p>
|
|
1573
|
+
<p>Total Resources: {metadata.get('total_resources_evaluated', 0):,}</p>
|
|
1574
|
+
</div>
|
|
1575
|
+
<div class="footer-section">
|
|
1576
|
+
<h4>About CIS Controls</h4>
|
|
1577
|
+
<p>The CIS Controls are a prioritized set of cybersecurity best practices developed by the Center for Internet Security.</p>
|
|
1578
|
+
<p>This report evaluates AWS configurations against CIS Controls Implementation Groups.</p>
|
|
1579
|
+
</div>
|
|
1580
|
+
</div>
|
|
1581
|
+
<div class="footer-bottom">
|
|
1582
|
+
<p>© 2024 AWS CIS Assessment Tool. Generated with HTML Reporter v{html_data.get('report_version', '1.0')}</p>
|
|
1583
|
+
</div>
|
|
1584
|
+
</footer>
|
|
1585
|
+
"""
|
|
1586
|
+
|
|
1587
|
+
def _prepare_chart_data(self, html_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1588
|
+
"""Prepare data for charts.
|
|
1589
|
+
|
|
1590
|
+
Args:
|
|
1591
|
+
html_data: Enhanced HTML report data
|
|
1592
|
+
|
|
1593
|
+
Returns:
|
|
1594
|
+
Chart data dictionary
|
|
1595
|
+
"""
|
|
1596
|
+
exec_summary = html_data["executive_summary"]
|
|
1597
|
+
|
|
1598
|
+
# Implementation Groups compliance chart
|
|
1599
|
+
ig_compliance = {
|
|
1600
|
+
"labels": ["IG1", "IG2", "IG3"],
|
|
1601
|
+
"datasets": [{
|
|
1602
|
+
"data": [
|
|
1603
|
+
exec_summary.get("ig1_compliance_percentage", 0),
|
|
1604
|
+
exec_summary.get("ig2_compliance_percentage", 0),
|
|
1605
|
+
exec_summary.get("ig3_compliance_percentage", 0)
|
|
1606
|
+
],
|
|
1607
|
+
"backgroundColor": ["#3498db", "#2ecc71", "#e74c3c"],
|
|
1608
|
+
"borderWidth": 2,
|
|
1609
|
+
"borderColor": "#fff"
|
|
1610
|
+
}]
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
# Compliance trend chart
|
|
1614
|
+
compliance_trend = {
|
|
1615
|
+
"labels": ["IG1", "IG2", "IG3"],
|
|
1616
|
+
"datasets": [{
|
|
1617
|
+
"label": "Compliance %",
|
|
1618
|
+
"data": [
|
|
1619
|
+
exec_summary.get("ig1_compliance_percentage", 0),
|
|
1620
|
+
exec_summary.get("ig2_compliance_percentage", 0),
|
|
1621
|
+
exec_summary.get("ig3_compliance_percentage", 0)
|
|
1622
|
+
],
|
|
1623
|
+
"backgroundColor": ["#3498db", "#2ecc71", "#e74c3c"],
|
|
1624
|
+
"borderColor": ["#2980b9", "#27ae60", "#c0392b"],
|
|
1625
|
+
"borderWidth": 1
|
|
1626
|
+
}]
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
# Risk distribution chart
|
|
1630
|
+
total_resources = exec_summary.get("total_resources", 1)
|
|
1631
|
+
compliant = exec_summary.get("compliant_resources", 0)
|
|
1632
|
+
non_compliant = exec_summary.get("non_compliant_resources", 0)
|
|
1633
|
+
|
|
1634
|
+
risk_distribution = {
|
|
1635
|
+
"labels": ["Compliant", "Non-Compliant"],
|
|
1636
|
+
"datasets": [{
|
|
1637
|
+
"data": [compliant, non_compliant],
|
|
1638
|
+
"backgroundColor": ["#27ae60", "#e74c3c"],
|
|
1639
|
+
"borderWidth": 2,
|
|
1640
|
+
"borderColor": "#fff"
|
|
1641
|
+
}]
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
return {
|
|
1645
|
+
"igCompliance": ig_compliance,
|
|
1646
|
+
"complianceTrend": compliance_trend,
|
|
1647
|
+
"riskDistribution": risk_distribution
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
def _build_navigation_structure(self, html_data: Dict[str, Any]) -> Dict[str, Any]:
|
|
1651
|
+
"""Build navigation structure for the report.
|
|
1652
|
+
|
|
1653
|
+
Args:
|
|
1654
|
+
html_data: Enhanced HTML report data
|
|
1655
|
+
|
|
1656
|
+
Returns:
|
|
1657
|
+
Navigation structure dictionary
|
|
1658
|
+
"""
|
|
1659
|
+
return {
|
|
1660
|
+
"sections": [
|
|
1661
|
+
{"id": "dashboard", "title": "Dashboard"},
|
|
1662
|
+
{"id": "implementation-groups", "title": "Implementation Groups"},
|
|
1663
|
+
{"id": "detailed-findings", "title": "Detailed Findings"},
|
|
1664
|
+
{"id": "resource-details", "title": "Resource Details"},
|
|
1665
|
+
{"id": "remediation", "title": "Remediation"}
|
|
1666
|
+
]
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
def _calculate_compliance_grade(self, compliance_percentage: float) -> str:
|
|
1670
|
+
"""Calculate compliance grade based on percentage."""
|
|
1671
|
+
if compliance_percentage >= 95.0:
|
|
1672
|
+
return "A"
|
|
1673
|
+
elif compliance_percentage >= 85.0:
|
|
1674
|
+
return "B"
|
|
1675
|
+
elif compliance_percentage >= 75.0:
|
|
1676
|
+
return "C"
|
|
1677
|
+
elif compliance_percentage >= 60.0:
|
|
1678
|
+
return "D"
|
|
1679
|
+
else:
|
|
1680
|
+
return "F"
|
|
1681
|
+
|
|
1682
|
+
def _calculate_risk_level(self, compliance_percentage: float) -> str:
|
|
1683
|
+
"""Calculate risk level based on compliance percentage."""
|
|
1684
|
+
if compliance_percentage >= 90.0:
|
|
1685
|
+
return "LOW"
|
|
1686
|
+
elif compliance_percentage >= 75.0:
|
|
1687
|
+
return "MEDIUM"
|
|
1688
|
+
elif compliance_percentage >= 50.0:
|
|
1689
|
+
return "HIGH"
|
|
1690
|
+
else:
|
|
1691
|
+
return "CRITICAL"
|
|
1692
|
+
|
|
1693
|
+
def _get_status_color(self, compliance_percentage: float) -> str:
|
|
1694
|
+
"""Get status color based on compliance percentage."""
|
|
1695
|
+
if compliance_percentage >= 90.0:
|
|
1696
|
+
return "#27ae60" # Green
|
|
1697
|
+
elif compliance_percentage >= 75.0:
|
|
1698
|
+
return "#f39c12" # Orange
|
|
1699
|
+
elif compliance_percentage >= 50.0:
|
|
1700
|
+
return "#e67e22" # Dark orange
|
|
1701
|
+
else:
|
|
1702
|
+
return "#e74c3c" # Red
|
|
1703
|
+
|
|
1704
|
+
def _get_status_class(self, compliance_percentage: float) -> str:
|
|
1705
|
+
"""Get CSS status class based on compliance percentage."""
|
|
1706
|
+
if compliance_percentage >= 95.0:
|
|
1707
|
+
return "excellent"
|
|
1708
|
+
elif compliance_percentage >= 80.0:
|
|
1709
|
+
return "good"
|
|
1710
|
+
elif compliance_percentage >= 60.0:
|
|
1711
|
+
return "fair"
|
|
1712
|
+
elif compliance_percentage >= 40.0:
|
|
1713
|
+
return "poor"
|
|
1714
|
+
else:
|
|
1715
|
+
return "critical"
|
|
1716
|
+
|
|
1717
|
+
def _get_severity_badge(self, control_data: Dict[str, Any]) -> str:
|
|
1718
|
+
"""Get severity badge class for control."""
|
|
1719
|
+
findings_count = len(control_data.get("non_compliant_findings", []))
|
|
1720
|
+
if findings_count > 10:
|
|
1721
|
+
return "high"
|
|
1722
|
+
elif findings_count > 3:
|
|
1723
|
+
return "medium"
|
|
1724
|
+
else:
|
|
1725
|
+
return "low"
|
|
1726
|
+
|
|
1727
|
+
def _get_priority_badge(self, priority: str) -> str:
|
|
1728
|
+
"""Get priority badge class."""
|
|
1729
|
+
return priority.lower()
|
|
1730
|
+
|
|
1731
|
+
def _get_effort_badge(self, effort: str) -> str:
|
|
1732
|
+
"""Get effort badge class."""
|
|
1733
|
+
effort_lower = effort.lower()
|
|
1734
|
+
if "low" in effort_lower or "minimal" in effort_lower:
|
|
1735
|
+
return "effort-minimal"
|
|
1736
|
+
elif "medium" in effort_lower or "moderate" in effort_lower:
|
|
1737
|
+
return "effort-moderate"
|
|
1738
|
+
elif "high" in effort_lower or "significant" in effort_lower:
|
|
1739
|
+
return "effort-significant"
|
|
1740
|
+
else:
|
|
1741
|
+
return "effort-extensive"
|
|
1742
|
+
|
|
1743
|
+
def _get_ig_description(self, ig_name: str) -> str:
|
|
1744
|
+
"""Get Implementation Group description."""
|
|
1745
|
+
descriptions = {
|
|
1746
|
+
"IG1": "Essential Cyber Hygiene",
|
|
1747
|
+
"IG2": "Enhanced Security",
|
|
1748
|
+
"IG3": "Advanced Security"
|
|
1749
|
+
}
|
|
1750
|
+
return descriptions.get(ig_name, "Unknown Implementation Group")
|
|
1751
|
+
|
|
1752
|
+
def _prepare_findings_for_display(self, findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
1753
|
+
"""Prepare findings for HTML display."""
|
|
1754
|
+
display_findings = []
|
|
1755
|
+
for finding in findings:
|
|
1756
|
+
display_finding = finding.copy()
|
|
1757
|
+
# Truncate long resource IDs for display
|
|
1758
|
+
if len(display_finding["resource_id"]) > 50:
|
|
1759
|
+
display_finding["resource_id_display"] = display_finding["resource_id"][:47] + "..."
|
|
1760
|
+
else:
|
|
1761
|
+
display_finding["resource_id_display"] = display_finding["resource_id"]
|
|
1762
|
+
display_findings.append(display_finding)
|
|
1763
|
+
return display_findings
|
|
1764
|
+
|
|
1765
|
+
def set_chart_options(self, include_charts: bool = True) -> None:
|
|
1766
|
+
"""Configure chart inclusion options.
|
|
1767
|
+
|
|
1768
|
+
Args:
|
|
1769
|
+
include_charts: Whether to include interactive charts
|
|
1770
|
+
"""
|
|
1771
|
+
self.include_charts = include_charts
|
|
1772
|
+
logger.debug(f"Updated chart options: include_charts={include_charts}")
|
|
1773
|
+
|
|
1774
|
+
def validate_html_output(self, html_content: str) -> bool:
|
|
1775
|
+
"""Validate that the generated HTML is well-formed.
|
|
1776
|
+
|
|
1777
|
+
Args:
|
|
1778
|
+
html_content: HTML content string to validate
|
|
1779
|
+
|
|
1780
|
+
Returns:
|
|
1781
|
+
True if HTML appears valid, False otherwise
|
|
1782
|
+
"""
|
|
1783
|
+
# Basic HTML validation checks
|
|
1784
|
+
required_elements = ['<!DOCTYPE html>', '<html', '<head>', '<body>', '</html>']
|
|
1785
|
+
|
|
1786
|
+
for element in required_elements:
|
|
1787
|
+
if element not in html_content:
|
|
1788
|
+
logger.error(f"HTML validation failed: missing {element}")
|
|
1789
|
+
return False
|
|
1790
|
+
|
|
1791
|
+
# Check for balanced tags (basic check)
|
|
1792
|
+
open_tags = html_content.count('<div')
|
|
1793
|
+
close_tags = html_content.count('</div>')
|
|
1794
|
+
|
|
1795
|
+
if abs(open_tags - close_tags) > 5: # Allow some tolerance
|
|
1796
|
+
logger.warning(f"HTML validation warning: unbalanced div tags ({open_tags} open, {close_tags} close)")
|
|
1797
|
+
|
|
1798
|
+
logger.debug("HTML validation passed")
|
|
1799
|
+
return True
|
|
1800
|
+
|
|
1801
|
+
def validate_assessment_data(self, assessment_result: AssessmentResult,
|
|
1802
|
+
compliance_summary: ComplianceSummary) -> bool:
|
|
1803
|
+
"""Validate input assessment data before report generation.
|
|
1804
|
+
|
|
1805
|
+
Args:
|
|
1806
|
+
assessment_result: Assessment result to validate
|
|
1807
|
+
compliance_summary: Compliance summary to validate
|
|
1808
|
+
|
|
1809
|
+
Returns:
|
|
1810
|
+
True if data is valid, False otherwise
|
|
1811
|
+
"""
|
|
1812
|
+
if not assessment_result.account_id:
|
|
1813
|
+
logger.error("Assessment result missing account_id")
|
|
1814
|
+
return False
|
|
1815
|
+
|
|
1816
|
+
if not assessment_result.regions_assessed:
|
|
1817
|
+
logger.error("Assessment result missing regions_assessed")
|
|
1818
|
+
return False
|
|
1819
|
+
|
|
1820
|
+
# Allow empty IG scores for HTML reporter (can handle empty data)
|
|
1821
|
+
# if not assessment_result.ig_scores:
|
|
1822
|
+
# logger.error("Assessment result missing ig_scores")
|
|
1823
|
+
# return False
|
|
1824
|
+
|
|
1825
|
+
# Validate compliance summary
|
|
1826
|
+
if compliance_summary.overall_compliance_percentage < 0 or compliance_summary.overall_compliance_percentage > 100:
|
|
1827
|
+
logger.error(f"Invalid overall compliance percentage: {compliance_summary.overall_compliance_percentage}")
|
|
1828
|
+
return False
|
|
1829
|
+
|
|
1830
|
+
logger.debug("Assessment data validation passed")
|
|
1831
|
+
return True
|
|
1832
|
+
|
|
1833
|
+
def validate_assessment_data(self, assessment_result: AssessmentResult,
|
|
1834
|
+
compliance_summary: ComplianceSummary) -> bool:
|
|
1835
|
+
"""Validate input assessment data before report generation.
|
|
1836
|
+
|
|
1837
|
+
Args:
|
|
1838
|
+
assessment_result: Assessment result to validate
|
|
1839
|
+
compliance_summary: Compliance summary to validate
|
|
1840
|
+
|
|
1841
|
+
Returns:
|
|
1842
|
+
True if data is valid, False otherwise
|
|
1843
|
+
"""
|
|
1844
|
+
if not assessment_result.account_id:
|
|
1845
|
+
logger.error("Assessment result missing account_id")
|
|
1846
|
+
return False
|
|
1847
|
+
|
|
1848
|
+
if not assessment_result.regions_assessed:
|
|
1849
|
+
logger.error("Assessment result missing regions_assessed")
|
|
1850
|
+
return False
|
|
1851
|
+
|
|
1852
|
+
# Allow empty IG scores for HTML reporter (will show empty state)
|
|
1853
|
+
# if not assessment_result.ig_scores:
|
|
1854
|
+
# logger.error("Assessment result missing ig_scores")
|
|
1855
|
+
# return False
|
|
1856
|
+
|
|
1857
|
+
# Validate compliance summary
|
|
1858
|
+
if compliance_summary.overall_compliance_percentage < 0 or compliance_summary.overall_compliance_percentage > 100:
|
|
1859
|
+
logger.error(f"Invalid overall compliance percentage: {compliance_summary.overall_compliance_percentage}")
|
|
1860
|
+
return False
|
|
1861
|
+
|
|
1862
|
+
logger.debug("Assessment data validation passed")
|
|
1863
|
+
return True
|
|
1864
|
+
|
|
1865
|
+
def extract_summary_data(self, html_content: str) -> Optional[Dict[str, Any]]:
|
|
1866
|
+
"""Extract summary data from generated HTML report.
|
|
1867
|
+
|
|
1868
|
+
Args:
|
|
1869
|
+
html_content: HTML report content
|
|
1870
|
+
|
|
1871
|
+
Returns:
|
|
1872
|
+
Dictionary containing summary data or None if extraction fails
|
|
1873
|
+
"""
|
|
1874
|
+
try:
|
|
1875
|
+
# Simple extraction using string parsing
|
|
1876
|
+
# In a production system, would use proper HTML parsing
|
|
1877
|
+
|
|
1878
|
+
summary_data = {}
|
|
1879
|
+
|
|
1880
|
+
# Extract account ID
|
|
1881
|
+
if 'AWS Account:' in html_content:
|
|
1882
|
+
start = html_content.find('AWS Account:') + len('AWS Account:')
|
|
1883
|
+
end = html_content.find('|', start)
|
|
1884
|
+
if end > start:
|
|
1885
|
+
summary_data['account_id'] = html_content[start:end].strip()
|
|
1886
|
+
|
|
1887
|
+
# Extract overall compliance
|
|
1888
|
+
if 'Overall Compliance:' in html_content:
|
|
1889
|
+
start = html_content.find('Overall Compliance:') + len('Overall Compliance:')
|
|
1890
|
+
end = html_content.find('%', start)
|
|
1891
|
+
if end > start:
|
|
1892
|
+
try:
|
|
1893
|
+
summary_data['overall_compliance'] = float(html_content[start:end].strip())
|
|
1894
|
+
except ValueError:
|
|
1895
|
+
pass
|
|
1896
|
+
|
|
1897
|
+
return summary_data if summary_data else None
|
|
1898
|
+
|
|
1899
|
+
except Exception as e:
|
|
1900
|
+
logger.error(f"Failed to extract summary data from HTML: {e}")
|
|
1901
|
+
return None
|
|
1902
|
+
def _get_unique_controls_per_ig(self, implementation_groups: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
1903
|
+
"""Get unique controls per Implementation Group to avoid duplication.
|
|
1904
|
+
|
|
1905
|
+
Args:
|
|
1906
|
+
implementation_groups: Implementation groups data
|
|
1907
|
+
|
|
1908
|
+
Returns:
|
|
1909
|
+
Dictionary mapping IG names to their unique controls
|
|
1910
|
+
"""
|
|
1911
|
+
unique_controls = {}
|
|
1912
|
+
|
|
1913
|
+
# IG1 controls are always unique to IG1
|
|
1914
|
+
if "IG1" in implementation_groups:
|
|
1915
|
+
unique_controls["IG1"] = implementation_groups["IG1"]["controls"]
|
|
1916
|
+
|
|
1917
|
+
# IG2 unique controls (excluding IG1 controls)
|
|
1918
|
+
if "IG2" in implementation_groups:
|
|
1919
|
+
ig1_control_ids = set(implementation_groups.get("IG1", {}).get("controls", {}).keys())
|
|
1920
|
+
ig2_unique = {}
|
|
1921
|
+
for control_id, control_data in implementation_groups["IG2"]["controls"].items():
|
|
1922
|
+
if control_id not in ig1_control_ids:
|
|
1923
|
+
ig2_unique[control_id] = control_data
|
|
1924
|
+
unique_controls["IG2"] = ig2_unique
|
|
1925
|
+
|
|
1926
|
+
# IG3 unique controls (excluding IG1 and IG2 controls)
|
|
1927
|
+
if "IG3" in implementation_groups:
|
|
1928
|
+
ig1_control_ids = set(implementation_groups.get("IG1", {}).get("controls", {}).keys())
|
|
1929
|
+
ig2_control_ids = set(implementation_groups.get("IG2", {}).get("controls", {}).keys())
|
|
1930
|
+
ig3_unique = {}
|
|
1931
|
+
for control_id, control_data in implementation_groups["IG3"]["controls"].items():
|
|
1932
|
+
if control_id not in ig1_control_ids and control_id not in ig2_control_ids:
|
|
1933
|
+
ig3_unique[control_id] = control_data
|
|
1934
|
+
unique_controls["IG3"] = ig3_unique
|
|
1935
|
+
|
|
1936
|
+
return unique_controls
|
|
1937
|
+
|
|
1938
|
+
def _get_ig_description_with_inheritance(self, ig_name: str) -> str:
|
|
1939
|
+
"""Get IG description with inheritance information.
|
|
1940
|
+
|
|
1941
|
+
Args:
|
|
1942
|
+
ig_name: Implementation Group name
|
|
1943
|
+
|
|
1944
|
+
Returns:
|
|
1945
|
+
Description string with inheritance info
|
|
1946
|
+
"""
|
|
1947
|
+
descriptions = {
|
|
1948
|
+
"IG1": "Essential Cyber Hygiene",
|
|
1949
|
+
"IG2": "Enhanced Security (includes IG1)",
|
|
1950
|
+
"IG3": "Advanced Security (includes IG1 + IG2)"
|
|
1951
|
+
}
|
|
1952
|
+
return descriptions.get(ig_name, "Unknown Implementation Group")
|
|
1953
|
+
|
|
1954
|
+
def _get_ig_scope_description(self, ig_name: str, unique_controls_count: int) -> str:
|
|
1955
|
+
"""Get scope description for an Implementation Group.
|
|
1956
|
+
|
|
1957
|
+
Args:
|
|
1958
|
+
ig_name: Implementation Group name
|
|
1959
|
+
unique_controls_count: Number of unique controls in this IG
|
|
1960
|
+
|
|
1961
|
+
Returns:
|
|
1962
|
+
Scope description string
|
|
1963
|
+
"""
|
|
1964
|
+
if ig_name == "IG1":
|
|
1965
|
+
return f"Showing {unique_controls_count} foundational controls essential for all organizations."
|
|
1966
|
+
elif ig_name == "IG2":
|
|
1967
|
+
return f"Showing {unique_controls_count} additional controls beyond IG1 for enhanced security."
|
|
1968
|
+
elif ig_name == "IG3":
|
|
1969
|
+
return f"Showing {unique_controls_count} advanced controls beyond IG1 and IG2 for comprehensive security."
|
|
1970
|
+
else:
|
|
1971
|
+
return f"Showing {unique_controls_count} controls for this implementation group."
|
|
1972
|
+
def _generate_resource_details_section(self, html_data: Dict[str, Any]) -> str:
|
|
1973
|
+
"""Generate comprehensive resource details section.
|
|
1974
|
+
|
|
1975
|
+
Args:
|
|
1976
|
+
html_data: Enhanced HTML report data
|
|
1977
|
+
|
|
1978
|
+
Returns:
|
|
1979
|
+
Resource details HTML as string
|
|
1980
|
+
"""
|
|
1981
|
+
# Collect all resources from all IGs and controls
|
|
1982
|
+
all_resources = []
|
|
1983
|
+
resource_ids_seen = set()
|
|
1984
|
+
|
|
1985
|
+
for ig_name, ig_data in html_data["implementation_groups"].items():
|
|
1986
|
+
for control_id, control_data in ig_data["controls"].items():
|
|
1987
|
+
# Add both compliant and non-compliant findings
|
|
1988
|
+
for finding in control_data.get("non_compliant_findings", []):
|
|
1989
|
+
resource_key = f"{finding['resource_id']}_{finding['resource_type']}_{finding['region']}"
|
|
1990
|
+
if resource_key not in resource_ids_seen:
|
|
1991
|
+
all_resources.append({
|
|
1992
|
+
"resource_id": finding["resource_id"],
|
|
1993
|
+
"resource_type": finding["resource_type"],
|
|
1994
|
+
"region": finding["region"],
|
|
1995
|
+
"compliance_status": finding["compliance_status"],
|
|
1996
|
+
"evaluation_reason": finding["evaluation_reason"],
|
|
1997
|
+
"config_rule_name": finding["config_rule_name"],
|
|
1998
|
+
"control_id": control_id,
|
|
1999
|
+
"implementation_group": ig_name
|
|
2000
|
+
})
|
|
2001
|
+
resource_ids_seen.add(resource_key)
|
|
2002
|
+
|
|
2003
|
+
# Add compliant findings (we need to get these from the detailed findings)
|
|
2004
|
+
for finding in control_data.get("compliant_findings", []):
|
|
2005
|
+
resource_key = f"{finding['resource_id']}_{finding['resource_type']}_{finding['region']}"
|
|
2006
|
+
if resource_key not in resource_ids_seen:
|
|
2007
|
+
all_resources.append({
|
|
2008
|
+
"resource_id": finding["resource_id"],
|
|
2009
|
+
"resource_type": finding["resource_type"],
|
|
2010
|
+
"region": finding["region"],
|
|
2011
|
+
"compliance_status": finding["compliance_status"],
|
|
2012
|
+
"evaluation_reason": finding.get("evaluation_reason", "Resource is compliant"),
|
|
2013
|
+
"config_rule_name": finding["config_rule_name"],
|
|
2014
|
+
"control_id": control_id,
|
|
2015
|
+
"implementation_group": ig_name
|
|
2016
|
+
})
|
|
2017
|
+
resource_ids_seen.add(resource_key)
|
|
2018
|
+
|
|
2019
|
+
# Sort resources by compliance status (non-compliant first), then by resource type
|
|
2020
|
+
all_resources.sort(key=lambda x: (x["compliance_status"] == "COMPLIANT", x["resource_type"], x["resource_id"]))
|
|
2021
|
+
|
|
2022
|
+
# Generate resource table rows
|
|
2023
|
+
resource_rows = ""
|
|
2024
|
+
for resource in all_resources:
|
|
2025
|
+
status_class = "compliant" if resource["compliance_status"] == "COMPLIANT" else "non_compliant"
|
|
2026
|
+
status_icon = "✓" if resource["compliance_status"] == "COMPLIANT" else "✗"
|
|
2027
|
+
|
|
2028
|
+
resource_rows += f"""
|
|
2029
|
+
<tr class="resource-row {status_class}">
|
|
2030
|
+
<td><code>{resource['resource_id']}</code></td>
|
|
2031
|
+
<td>{resource['resource_type']}</td>
|
|
2032
|
+
<td>{resource['region']}</td>
|
|
2033
|
+
<td>
|
|
2034
|
+
<span class="badge {status_class}">
|
|
2035
|
+
{status_icon} {resource['compliance_status']}
|
|
2036
|
+
</span>
|
|
2037
|
+
</td>
|
|
2038
|
+
<td>{resource['control_id']}</td>
|
|
2039
|
+
<td>{resource['config_rule_name']}</td>
|
|
2040
|
+
<td class="evaluation-reason">{resource['evaluation_reason']}</td>
|
|
2041
|
+
</tr>
|
|
2042
|
+
"""
|
|
2043
|
+
|
|
2044
|
+
# Calculate summary statistics
|
|
2045
|
+
total_resources = len(all_resources)
|
|
2046
|
+
compliant_resources = len([r for r in all_resources if r["compliance_status"] == "COMPLIANT"])
|
|
2047
|
+
non_compliant_resources = total_resources - compliant_resources
|
|
2048
|
+
compliance_percentage = (compliant_resources / total_resources * 100) if total_resources > 0 else 0
|
|
2049
|
+
|
|
2050
|
+
# Generate resource type breakdown
|
|
2051
|
+
resource_type_stats = {}
|
|
2052
|
+
for resource in all_resources:
|
|
2053
|
+
resource_type = resource["resource_type"]
|
|
2054
|
+
if resource_type not in resource_type_stats:
|
|
2055
|
+
resource_type_stats[resource_type] = {"total": 0, "compliant": 0}
|
|
2056
|
+
resource_type_stats[resource_type]["total"] += 1
|
|
2057
|
+
if resource["compliance_status"] == "COMPLIANT":
|
|
2058
|
+
resource_type_stats[resource_type]["compliant"] += 1
|
|
2059
|
+
|
|
2060
|
+
resource_type_breakdown = ""
|
|
2061
|
+
for resource_type, stats in sorted(resource_type_stats.items()):
|
|
2062
|
+
type_compliance = (stats["compliant"] / stats["total"] * 100) if stats["total"] > 0 else 0
|
|
2063
|
+
status_class = self._get_status_class(type_compliance)
|
|
2064
|
+
|
|
2065
|
+
resource_type_breakdown += f"""
|
|
2066
|
+
<div class="resource-type-stat">
|
|
2067
|
+
<div class="resource-type-header">
|
|
2068
|
+
<span class="resource-type-name">{resource_type}</span>
|
|
2069
|
+
<span class="resource-type-count">{stats['compliant']}/{stats['total']}</span>
|
|
2070
|
+
</div>
|
|
2071
|
+
<div class="progress-container">
|
|
2072
|
+
<div class="progress-bar {status_class}" data-width="{type_compliance}">
|
|
2073
|
+
<span class="progress-text">{type_compliance:.1f}%</span>
|
|
2074
|
+
</div>
|
|
2075
|
+
</div>
|
|
2076
|
+
</div>
|
|
2077
|
+
"""
|
|
2078
|
+
|
|
2079
|
+
return f"""
|
|
2080
|
+
<section id="resource-details" class="resource-details">
|
|
2081
|
+
<h2>Resource Details</h2>
|
|
2082
|
+
|
|
2083
|
+
<div class="resource-summary">
|
|
2084
|
+
<div class="resource-stats-grid">
|
|
2085
|
+
<div class="resource-stat-card">
|
|
2086
|
+
<div class="stat-value">{total_resources}</div>
|
|
2087
|
+
<div class="stat-label">Total Resources</div>
|
|
2088
|
+
</div>
|
|
2089
|
+
<div class="resource-stat-card compliant">
|
|
2090
|
+
<div class="stat-value">{compliant_resources}</div>
|
|
2091
|
+
<div class="stat-label">Compliant</div>
|
|
2092
|
+
</div>
|
|
2093
|
+
<div class="resource-stat-card non-compliant">
|
|
2094
|
+
<div class="stat-value">{non_compliant_resources}</div>
|
|
2095
|
+
<div class="stat-label">Non-Compliant</div>
|
|
2096
|
+
</div>
|
|
2097
|
+
<div class="resource-stat-card">
|
|
2098
|
+
<div class="stat-value">{compliance_percentage:.1f}%</div>
|
|
2099
|
+
<div class="stat-label">Compliance Rate</div>
|
|
2100
|
+
</div>
|
|
2101
|
+
</div>
|
|
2102
|
+
</div>
|
|
2103
|
+
|
|
2104
|
+
<div class="resource-type-breakdown">
|
|
2105
|
+
<h3>Compliance by Resource Type</h3>
|
|
2106
|
+
<div class="resource-type-grid">
|
|
2107
|
+
{resource_type_breakdown}
|
|
2108
|
+
</div>
|
|
2109
|
+
</div>
|
|
2110
|
+
|
|
2111
|
+
<div class="resource-table-container">
|
|
2112
|
+
<div class="resource-filters">
|
|
2113
|
+
<input type="text" id="resourceSearch" placeholder="Search resources..." onkeyup="filterResources()" class="search-input">
|
|
2114
|
+
<select id="statusFilter" onchange="filterResources()" class="filter-select">
|
|
2115
|
+
<option value="">All Status</option>
|
|
2116
|
+
<option value="COMPLIANT">Compliant Only</option>
|
|
2117
|
+
<option value="NON_COMPLIANT">Non-Compliant Only</option>
|
|
2118
|
+
</select>
|
|
2119
|
+
<select id="typeFilter" onchange="filterResources()" class="filter-select">
|
|
2120
|
+
<option value="">All Types</option>
|
|
2121
|
+
{self._generate_resource_type_options(resource_type_stats)}
|
|
2122
|
+
</select>
|
|
2123
|
+
</div>
|
|
2124
|
+
|
|
2125
|
+
<table class="findings-table resource-table" id="resourceTable">
|
|
2126
|
+
<thead>
|
|
2127
|
+
<tr>
|
|
2128
|
+
<th onclick="sortResourceTable(0)">Resource ID ↕</th>
|
|
2129
|
+
<th onclick="sortResourceTable(1)">Resource Type ↕</th>
|
|
2130
|
+
<th onclick="sortResourceTable(2)">Region ↕</th>
|
|
2131
|
+
<th onclick="sortResourceTable(3)">Status ↕</th>
|
|
2132
|
+
<th onclick="sortResourceTable(4)">Control ↕</th>
|
|
2133
|
+
<th onclick="sortResourceTable(5)">Config Rule ↕</th>
|
|
2134
|
+
<th>Evaluation Details</th>
|
|
2135
|
+
</tr>
|
|
2136
|
+
</thead>
|
|
2137
|
+
<tbody>
|
|
2138
|
+
{resource_rows}
|
|
2139
|
+
</tbody>
|
|
2140
|
+
</table>
|
|
2141
|
+
</div>
|
|
2142
|
+
|
|
2143
|
+
<div class="resource-export">
|
|
2144
|
+
<button onclick="exportResourcesToCSV()" class="export-btn">Export to CSV</button>
|
|
2145
|
+
<button onclick="exportResourcesToJSON()" class="export-btn">Export to JSON</button>
|
|
2146
|
+
</div>
|
|
2147
|
+
</section>
|
|
2148
|
+
"""
|
|
2149
|
+
|
|
2150
|
+
def _generate_resource_type_options(self, resource_type_stats: Dict[str, Dict[str, int]]) -> str:
|
|
2151
|
+
"""Generate option elements for resource type filter.
|
|
2152
|
+
|
|
2153
|
+
Args:
|
|
2154
|
+
resource_type_stats: Resource type statistics
|
|
2155
|
+
|
|
2156
|
+
Returns:
|
|
2157
|
+
HTML option elements
|
|
2158
|
+
"""
|
|
2159
|
+
options = ""
|
|
2160
|
+
for resource_type in sorted(resource_type_stats.keys()):
|
|
2161
|
+
options += f'<option value="{resource_type}">{resource_type}</option>'
|
|
2162
|
+
return options
|