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.
Files changed (77) hide show
  1. aws_cis_assessment/__init__.py +11 -0
  2. aws_cis_assessment/cli/__init__.py +3 -0
  3. aws_cis_assessment/cli/examples.py +274 -0
  4. aws_cis_assessment/cli/main.py +1259 -0
  5. aws_cis_assessment/cli/utils.py +356 -0
  6. aws_cis_assessment/config/__init__.py +1 -0
  7. aws_cis_assessment/config/config_loader.py +328 -0
  8. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
  9. aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
  10. aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
  11. aws_cis_assessment/controls/__init__.py +1 -0
  12. aws_cis_assessment/controls/base_control.py +400 -0
  13. aws_cis_assessment/controls/ig1/__init__.py +239 -0
  14. aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
  15. aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
  16. aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
  17. aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
  18. aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
  19. aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
  20. aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
  21. aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
  22. aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
  23. aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
  24. aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
  25. aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
  26. aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
  27. aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
  28. aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
  29. aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
  30. aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
  31. aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
  32. aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
  33. aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
  34. aws_cis_assessment/controls/ig2/__init__.py +172 -0
  35. aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
  36. aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
  37. aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
  38. aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
  39. aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
  40. aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
  41. aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
  42. aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
  43. aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
  44. aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
  45. aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
  46. aws_cis_assessment/controls/ig3/__init__.py +49 -0
  47. aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
  48. aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
  49. aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
  50. aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
  51. aws_cis_assessment/core/__init__.py +1 -0
  52. aws_cis_assessment/core/accuracy_validator.py +425 -0
  53. aws_cis_assessment/core/assessment_engine.py +1266 -0
  54. aws_cis_assessment/core/audit_trail.py +491 -0
  55. aws_cis_assessment/core/aws_client_factory.py +313 -0
  56. aws_cis_assessment/core/error_handler.py +607 -0
  57. aws_cis_assessment/core/models.py +166 -0
  58. aws_cis_assessment/core/scoring_engine.py +459 -0
  59. aws_cis_assessment/reporters/__init__.py +8 -0
  60. aws_cis_assessment/reporters/base_reporter.py +454 -0
  61. aws_cis_assessment/reporters/csv_reporter.py +835 -0
  62. aws_cis_assessment/reporters/html_reporter.py +2162 -0
  63. aws_cis_assessment/reporters/json_reporter.py +561 -0
  64. aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
  65. aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
  66. aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
  67. aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
  68. aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
  69. aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
  70. docs/README.md +94 -0
  71. docs/assessment-logic.md +766 -0
  72. docs/cli-reference.md +698 -0
  73. docs/config-rule-mappings.md +393 -0
  74. docs/developer-guide.md +858 -0
  75. docs/installation.md +299 -0
  76. docs/troubleshooting.md +634 -0
  77. 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>&copy; 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