aws-cis-controls-assessment 1.0.6__py3-none-any.whl → 1.0.8__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 +1 -1
- aws_cis_assessment/controls/ig1/control_advanced_security.py +2 -2
- aws_cis_assessment/controls/ig1/control_critical_security.py +33 -36
- aws_cis_assessment/reporters/html_reporter.py +547 -38
- {aws_cis_controls_assessment-1.0.6.dist-info → aws_cis_controls_assessment-1.0.8.dist-info}/METADATA +2 -1
- {aws_cis_controls_assessment-1.0.6.dist-info → aws_cis_controls_assessment-1.0.8.dist-info}/RECORD +14 -13
- docs/README.md +1 -0
- docs/html-report-improvements.md +422 -0
- docs/installation.md +78 -27
- docs/user-guide.md +7 -1
- {aws_cis_controls_assessment-1.0.6.dist-info → aws_cis_controls_assessment-1.0.8.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.0.6.dist-info → aws_cis_controls_assessment-1.0.8.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.0.6.dist-info → aws_cis_controls_assessment-1.0.8.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.0.6.dist-info → aws_cis_controls_assessment-1.0.8.dist-info}/top_level.txt +0 -0
|
@@ -21,6 +21,44 @@ class HTMLReporter(ReportGenerator):
|
|
|
21
21
|
Generates interactive web-based reports with executive dashboard,
|
|
22
22
|
compliance summaries, charts, detailed drill-down capabilities,
|
|
23
23
|
and responsive design for mobile and desktop viewing.
|
|
24
|
+
|
|
25
|
+
Features:
|
|
26
|
+
- Executive dashboard with key compliance metrics
|
|
27
|
+
- Implementation Groups section showing unique controls per IG
|
|
28
|
+
- Control display names combining control ID and AWS Config rule name
|
|
29
|
+
- IG membership badges indicating which IGs include each control
|
|
30
|
+
- Consolidated detailed findings (deduplicated across IGs)
|
|
31
|
+
- Interactive charts and collapsible sections
|
|
32
|
+
- Resource details with filtering and export capabilities
|
|
33
|
+
- Responsive design for mobile and desktop
|
|
34
|
+
- Print-friendly layout
|
|
35
|
+
|
|
36
|
+
Display Format Examples:
|
|
37
|
+
Control cards show formatted names like:
|
|
38
|
+
- "1.5: root-account-hardware-mfa-enabled"
|
|
39
|
+
- "2.1: IAM Password Policy (iam-password-policy)"
|
|
40
|
+
|
|
41
|
+
IG badges indicate membership:
|
|
42
|
+
- Blue badge (IG1) for foundational controls
|
|
43
|
+
- Green badge (IG2) for enhanced security controls
|
|
44
|
+
- Purple badge (IG3) for advanced security controls
|
|
45
|
+
|
|
46
|
+
CSS Classes for Custom Styling:
|
|
47
|
+
- .ig-badge-1: Blue badge for IG1 controls
|
|
48
|
+
- .ig-badge-2: Green badge for IG2 controls
|
|
49
|
+
- .ig-badge-3: Purple badge for IG3 controls
|
|
50
|
+
- .control-display-name: Formatted control name display
|
|
51
|
+
- .control-display-name.truncated: Truncated names with tooltip
|
|
52
|
+
- .ig-membership-badges: Container for IG membership badges
|
|
53
|
+
- .ig-membership-badge: Individual IG badge element
|
|
54
|
+
- .ig-explanation: Informational box explaining IG cumulative nature
|
|
55
|
+
- .ig-scope: Scope description for each IG section
|
|
56
|
+
|
|
57
|
+
Backward Compatibility:
|
|
58
|
+
- Works with existing AssessmentResult data structures
|
|
59
|
+
- Gracefully falls back to control ID only if config_rule_name is missing
|
|
60
|
+
- Preserves all existing sections and functionality
|
|
61
|
+
- Maintains existing CSS classes for compatibility
|
|
24
62
|
"""
|
|
25
63
|
|
|
26
64
|
def __init__(self, template_dir: Optional[str] = None, include_charts: bool = True):
|
|
@@ -147,9 +185,19 @@ class HTMLReporter(ReportGenerator):
|
|
|
147
185
|
control_data["progress_width"] = control_data["compliance_percentage"]
|
|
148
186
|
control_data["severity_badge"] = self._get_severity_badge(control_data)
|
|
149
187
|
|
|
188
|
+
# Enrich control metadata with display name, IG badges, etc.
|
|
189
|
+
enriched_control = self._enrich_control_metadata(
|
|
190
|
+
control_data,
|
|
191
|
+
control_id,
|
|
192
|
+
ig_name,
|
|
193
|
+
html_data["implementation_groups"]
|
|
194
|
+
)
|
|
195
|
+
# Update control_data with enriched metadata
|
|
196
|
+
ig_data["controls"][control_id] = enriched_control
|
|
197
|
+
|
|
150
198
|
# Process findings for display
|
|
151
|
-
|
|
152
|
-
|
|
199
|
+
enriched_control["display_findings"] = self._prepare_findings_for_display(
|
|
200
|
+
enriched_control.get("non_compliant_findings", [])
|
|
153
201
|
)
|
|
154
202
|
|
|
155
203
|
# Enhance remediation priorities with visual elements
|
|
@@ -505,6 +553,58 @@ class HTMLReporter(ReportGenerator):
|
|
|
505
553
|
margin-bottom: 10px;
|
|
506
554
|
}
|
|
507
555
|
|
|
556
|
+
/* Control display name styles */
|
|
557
|
+
.control-display-name {
|
|
558
|
+
font-weight: 600;
|
|
559
|
+
color: #2c3e50;
|
|
560
|
+
margin-bottom: 5px;
|
|
561
|
+
font-size: 0.95em;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.control-display-name.truncated {
|
|
565
|
+
overflow: hidden;
|
|
566
|
+
text-overflow: ellipsis;
|
|
567
|
+
white-space: nowrap;
|
|
568
|
+
cursor: help;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* IG membership badges */
|
|
572
|
+
.ig-membership-badges {
|
|
573
|
+
display: flex;
|
|
574
|
+
gap: 5px;
|
|
575
|
+
margin-top: 5px;
|
|
576
|
+
margin-bottom: 10px;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.ig-membership-badge {
|
|
580
|
+
font-size: 0.7em;
|
|
581
|
+
padding: 2px 6px;
|
|
582
|
+
border-radius: 10px;
|
|
583
|
+
font-weight: bold;
|
|
584
|
+
text-transform: uppercase;
|
|
585
|
+
letter-spacing: 0.5px;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
.ig-badge-1 {
|
|
589
|
+
background-color: #3498db;
|
|
590
|
+
color: white;
|
|
591
|
+
} /* Blue for IG1 */
|
|
592
|
+
|
|
593
|
+
.ig-badge-2 {
|
|
594
|
+
background-color: #27ae60;
|
|
595
|
+
color: white;
|
|
596
|
+
} /* Green for IG2 */
|
|
597
|
+
|
|
598
|
+
.ig-badge-3 {
|
|
599
|
+
background-color: #9b59b6;
|
|
600
|
+
color: white;
|
|
601
|
+
} /* Purple for IG3 */
|
|
602
|
+
|
|
603
|
+
.ig-badge-default {
|
|
604
|
+
background-color: #95a5a6;
|
|
605
|
+
color: white;
|
|
606
|
+
}
|
|
607
|
+
|
|
508
608
|
/* Badges */
|
|
509
609
|
.badge {
|
|
510
610
|
padding: 4px 8px;
|
|
@@ -1368,6 +1468,21 @@ class HTMLReporter(ReportGenerator):
|
|
|
1368
1468
|
findings_count = len(control_data.get("non_compliant_findings", []))
|
|
1369
1469
|
status_class = self._get_status_class(control_data["compliance_percentage"])
|
|
1370
1470
|
|
|
1471
|
+
# Get display name (enriched in _enhance_html_structure)
|
|
1472
|
+
display_name = control_data.get('display_name', control_id)
|
|
1473
|
+
needs_truncation = control_data.get('needs_truncation', False)
|
|
1474
|
+
|
|
1475
|
+
# Add title attribute for truncated names
|
|
1476
|
+
title_attr = f' title="{display_name}"' if needs_truncation else ''
|
|
1477
|
+
display_name_class = 'control-display-name truncated' if needs_truncation else 'control-display-name'
|
|
1478
|
+
|
|
1479
|
+
# Get IG membership badges
|
|
1480
|
+
originating_ig = control_data.get('originating_ig', 'IG1')
|
|
1481
|
+
ig_badge_class = control_data.get('ig_badge_class', 'ig-badge-1')
|
|
1482
|
+
|
|
1483
|
+
# Build IG membership badges HTML
|
|
1484
|
+
ig_badges_html = f'<span class="ig-membership-badge {ig_badge_class}">{originating_ig}</span>'
|
|
1485
|
+
|
|
1371
1486
|
# Add inheritance indicator for inherited controls
|
|
1372
1487
|
inheritance_indicator = ""
|
|
1373
1488
|
if ig_name != "IG1" and control_id in unique_controls.get("IG1", {}):
|
|
@@ -1378,10 +1493,12 @@ class HTMLReporter(ReportGenerator):
|
|
|
1378
1493
|
controls_html += f"""
|
|
1379
1494
|
<div class="control-card">
|
|
1380
1495
|
<div class="control-header">
|
|
1381
|
-
<div class="
|
|
1496
|
+
<div class="{display_name_class}"{title_attr}>{display_name}</div>
|
|
1382
1497
|
<div class="badge {control_data.get('severity_badge', 'medium')}">{findings_count} Issues</div>
|
|
1383
1498
|
</div>
|
|
1384
|
-
<div class="
|
|
1499
|
+
<div class="ig-membership-badges">
|
|
1500
|
+
{ig_badges_html}
|
|
1501
|
+
</div>
|
|
1385
1502
|
{inheritance_indicator}
|
|
1386
1503
|
<div class="progress-container">
|
|
1387
1504
|
<div class="progress-bar {status_class}" data-width="{control_data['compliance_percentage']}">
|
|
@@ -1430,39 +1547,73 @@ class HTMLReporter(ReportGenerator):
|
|
|
1430
1547
|
def _generate_detailed_findings_section(self, html_data: Dict[str, Any]) -> str:
|
|
1431
1548
|
"""Generate detailed findings section.
|
|
1432
1549
|
|
|
1550
|
+
This method consolidates findings by control ID only (not by IG) to eliminate
|
|
1551
|
+
duplication. Each control appears once with IG membership indicators.
|
|
1552
|
+
|
|
1433
1553
|
Args:
|
|
1434
1554
|
html_data: Enhanced HTML report data
|
|
1435
1555
|
|
|
1436
1556
|
Returns:
|
|
1437
1557
|
Detailed findings HTML as string
|
|
1438
1558
|
"""
|
|
1439
|
-
|
|
1559
|
+
# Consolidate findings by control ID (deduplicated across IGs)
|
|
1560
|
+
consolidated_findings = self._consolidate_findings_by_control(
|
|
1561
|
+
html_data.get("implementation_groups", {})
|
|
1562
|
+
)
|
|
1440
1563
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1564
|
+
findings_content = ""
|
|
1565
|
+
|
|
1566
|
+
# Generate findings grouped by control ID only (sorted alphanumerically)
|
|
1567
|
+
for control_id, control_data in consolidated_findings.items():
|
|
1568
|
+
findings = control_data.get('findings', [])
|
|
1569
|
+
|
|
1570
|
+
# Skip if no non-compliant findings
|
|
1571
|
+
if not findings:
|
|
1572
|
+
continue
|
|
1573
|
+
|
|
1574
|
+
# Get control metadata
|
|
1575
|
+
config_rule_name = control_data.get('config_rule_name', '')
|
|
1576
|
+
title = control_data.get('title', f'CIS Control {control_id}')
|
|
1577
|
+
member_igs = control_data.get('member_igs', [])
|
|
1578
|
+
|
|
1579
|
+
# Format display name for collapsible header
|
|
1580
|
+
display_name = self._format_control_display_name(control_id, config_rule_name, title)
|
|
1581
|
+
|
|
1582
|
+
# Generate IG membership badges
|
|
1583
|
+
ig_badges_html = ""
|
|
1584
|
+
for ig in member_igs:
|
|
1585
|
+
badge_class = self._get_ig_badge_class(ig)
|
|
1586
|
+
ig_badges_html += f'<span class="ig-membership-badge {badge_class}">{ig}</span>'
|
|
1587
|
+
|
|
1588
|
+
# Generate findings table rows
|
|
1589
|
+
findings_rows = ""
|
|
1590
|
+
for finding in findings:
|
|
1591
|
+
if finding.get("compliance_status") == "NON_COMPLIANT":
|
|
1592
|
+
findings_rows += f"""
|
|
1593
|
+
<tr>
|
|
1594
|
+
<td>{finding.get('resource_id', 'N/A')}</td>
|
|
1595
|
+
<td>{finding.get('resource_type', 'N/A')}</td>
|
|
1596
|
+
<td>{finding.get('region', 'N/A')}</td>
|
|
1597
|
+
<td><span class="badge {finding.get('compliance_status', '').lower()}">{finding.get('compliance_status', 'UNKNOWN')}</span></td>
|
|
1598
|
+
<td>{finding.get('evaluation_reason', 'N/A')}</td>
|
|
1599
|
+
<td>{finding.get('config_rule_name', config_rule_name)}</td>
|
|
1600
|
+
</tr>
|
|
1601
|
+
"""
|
|
1443
1602
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
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
|
-
"""
|
|
1603
|
+
# Only add control section if there are non-compliant findings
|
|
1604
|
+
if findings_rows:
|
|
1605
|
+
non_compliant_count = len([f for f in findings if f.get('compliance_status') == 'NON_COMPLIANT'])
|
|
1461
1606
|
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
<button class="collapsible">
|
|
1607
|
+
findings_content += f"""
|
|
1608
|
+
<div class="control-findings-section">
|
|
1609
|
+
<button class="collapsible">
|
|
1610
|
+
<span class="control-display-name">{display_name}</span>
|
|
1611
|
+
<span class="findings-count"> - Non-Compliant Resources ({non_compliant_count} items)</span>
|
|
1612
|
+
</button>
|
|
1465
1613
|
<div class="collapsible-content">
|
|
1614
|
+
<div class="ig-membership-badges" style="margin-bottom: 15px;">
|
|
1615
|
+
<strong>Implementation Groups:</strong> {ig_badges_html}
|
|
1616
|
+
</div>
|
|
1466
1617
|
<table class="findings-table">
|
|
1467
1618
|
<thead>
|
|
1468
1619
|
<tr>
|
|
@@ -1479,23 +1630,20 @@ class HTMLReporter(ReportGenerator):
|
|
|
1479
1630
|
</tbody>
|
|
1480
1631
|
</table>
|
|
1481
1632
|
</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
1633
|
</div>
|
|
1490
1634
|
"""
|
|
1491
1635
|
|
|
1492
1636
|
return f"""
|
|
1493
1637
|
<section id="detailed-findings" class="detailed-findings">
|
|
1494
1638
|
<h2>Detailed Findings</h2>
|
|
1639
|
+
<p class="section-description">
|
|
1640
|
+
Findings are grouped by control ID and deduplicated across Implementation Groups.
|
|
1641
|
+
Each control shows which IGs include it.
|
|
1642
|
+
</p>
|
|
1495
1643
|
<div class="search-container">
|
|
1496
1644
|
<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
1645
|
</div>
|
|
1498
|
-
{
|
|
1646
|
+
{findings_content if findings_content else '<p>No non-compliant findings to display.</p>'}
|
|
1499
1647
|
</section>
|
|
1500
1648
|
"""
|
|
1501
1649
|
|
|
@@ -1902,11 +2050,45 @@ class HTMLReporter(ReportGenerator):
|
|
|
1902
2050
|
def _get_unique_controls_per_ig(self, implementation_groups: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
1903
2051
|
"""Get unique controls per Implementation Group to avoid duplication.
|
|
1904
2052
|
|
|
2053
|
+
Filters controls to show only those unique to each IG level, eliminating
|
|
2054
|
+
redundancy in the Implementation Groups section. IG2 shows only controls
|
|
2055
|
+
not in IG1, and IG3 shows only controls not in IG1 or IG2.
|
|
2056
|
+
|
|
1905
2057
|
Args:
|
|
1906
|
-
implementation_groups: Implementation groups data
|
|
2058
|
+
implementation_groups: Implementation groups data with all controls
|
|
1907
2059
|
|
|
1908
2060
|
Returns:
|
|
1909
|
-
Dictionary mapping IG names to their unique controls
|
|
2061
|
+
Dictionary mapping IG names to their unique controls:
|
|
2062
|
+
- IG1: All IG1 controls (foundational)
|
|
2063
|
+
- IG2: Only controls unique to IG2 (not in IG1)
|
|
2064
|
+
- IG3: Only controls unique to IG3 (not in IG1 or IG2)
|
|
2065
|
+
|
|
2066
|
+
Examples:
|
|
2067
|
+
Input:
|
|
2068
|
+
{
|
|
2069
|
+
'IG1': {'controls': {'1.1': {...}, '1.5': {...}}},
|
|
2070
|
+
'IG2': {'controls': {'1.1': {...}, '1.5': {...}, '5.2': {...}}},
|
|
2071
|
+
'IG3': {'controls': {'1.1': {...}, '1.5': {...}, '5.2': {...}, '13.1': {...}}}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
Output:
|
|
2075
|
+
{
|
|
2076
|
+
'IG1': {'1.1': {...}, '1.5': {...}}, # All IG1 controls
|
|
2077
|
+
'IG2': {'5.2': {...}}, # Only 5.2 is unique to IG2
|
|
2078
|
+
'IG3': {'13.1': {...}} # Only 13.1 is unique to IG3
|
|
2079
|
+
}
|
|
2080
|
+
|
|
2081
|
+
Rationale:
|
|
2082
|
+
- Eliminates duplicate control cards across IG sections
|
|
2083
|
+
- Users see each control once in its originating IG
|
|
2084
|
+
- Reduces visual clutter and improves report readability
|
|
2085
|
+
- IG membership badges show which IGs include each control
|
|
2086
|
+
|
|
2087
|
+
Notes:
|
|
2088
|
+
- IG1 controls are always shown in full (foundational set)
|
|
2089
|
+
- Higher IGs show only their incremental additions
|
|
2090
|
+
- Cumulative nature is explained in the IG explanation box
|
|
2091
|
+
- Used by _generate_implementation_groups_section()
|
|
1910
2092
|
"""
|
|
1911
2093
|
unique_controls = {}
|
|
1912
2094
|
|
|
@@ -2159,4 +2341,331 @@ class HTMLReporter(ReportGenerator):
|
|
|
2159
2341
|
options = ""
|
|
2160
2342
|
for resource_type in sorted(resource_type_stats.keys()):
|
|
2161
2343
|
options += f'<option value="{resource_type}">{resource_type}</option>'
|
|
2162
|
-
return options
|
|
2344
|
+
return options
|
|
2345
|
+
|
|
2346
|
+
def _format_control_display_name(self, control_id: str, config_rule_name: str, title: Optional[str] = None) -> str:
|
|
2347
|
+
"""Format control display name combining ID, rule name, and optional title.
|
|
2348
|
+
|
|
2349
|
+
Creates a human-readable display name that shows both the control identifier
|
|
2350
|
+
and the AWS Config rule name, making it easier for users to understand what
|
|
2351
|
+
each control checks without looking up documentation.
|
|
2352
|
+
|
|
2353
|
+
Args:
|
|
2354
|
+
control_id: Control identifier (e.g., "1.5", "2.1")
|
|
2355
|
+
config_rule_name: AWS Config rule name (e.g., "root-account-hardware-mfa-enabled")
|
|
2356
|
+
title: Optional human-readable title for the control
|
|
2357
|
+
|
|
2358
|
+
Returns:
|
|
2359
|
+
Formatted string for display in the following formats:
|
|
2360
|
+
- With title: "{control_id}: {title} ({config_rule_name})"
|
|
2361
|
+
- Without title: "{control_id}: {config_rule_name}"
|
|
2362
|
+
- Fallback (no rule name): "{control_id}"
|
|
2363
|
+
|
|
2364
|
+
Examples:
|
|
2365
|
+
>>> _format_control_display_name("1.5", "root-account-hardware-mfa-enabled")
|
|
2366
|
+
"1.5: root-account-hardware-mfa-enabled"
|
|
2367
|
+
|
|
2368
|
+
>>> _format_control_display_name("2.1", "iam-password-policy", "IAM Password Policy")
|
|
2369
|
+
"2.1: IAM Password Policy (iam-password-policy)"
|
|
2370
|
+
|
|
2371
|
+
>>> _format_control_display_name("3.1", "")
|
|
2372
|
+
"3.1"
|
|
2373
|
+
|
|
2374
|
+
Notes:
|
|
2375
|
+
- Gracefully handles missing config_rule_name by falling back to control_id only
|
|
2376
|
+
- Used in both Implementation Groups and Detailed Findings sections
|
|
2377
|
+
- Display names longer than 50 characters are truncated with tooltips
|
|
2378
|
+
"""
|
|
2379
|
+
if not config_rule_name:
|
|
2380
|
+
# Fallback to control_id only if config_rule_name is missing
|
|
2381
|
+
return control_id
|
|
2382
|
+
|
|
2383
|
+
if title:
|
|
2384
|
+
return f"{control_id}: {title} ({config_rule_name})"
|
|
2385
|
+
else:
|
|
2386
|
+
return f"{control_id}: {config_rule_name}"
|
|
2387
|
+
|
|
2388
|
+
def _get_ig_badge_class(self, ig_name: str) -> str:
|
|
2389
|
+
"""Get CSS class for IG badge styling.
|
|
2390
|
+
|
|
2391
|
+
Returns the appropriate CSS class for styling Implementation Group badges
|
|
2392
|
+
with consistent color coding across the report.
|
|
2393
|
+
|
|
2394
|
+
Args:
|
|
2395
|
+
ig_name: Implementation Group name (IG1, IG2, or IG3)
|
|
2396
|
+
|
|
2397
|
+
Returns:
|
|
2398
|
+
CSS class name for the badge:
|
|
2399
|
+
- 'ig-badge-1' for IG1 (blue styling)
|
|
2400
|
+
- 'ig-badge-2' for IG2 (green styling)
|
|
2401
|
+
- 'ig-badge-3' for IG3 (purple styling)
|
|
2402
|
+
- 'ig-badge-default' for unknown IGs (gray styling)
|
|
2403
|
+
|
|
2404
|
+
Examples:
|
|
2405
|
+
>>> _get_ig_badge_class("IG1")
|
|
2406
|
+
"ig-badge-1"
|
|
2407
|
+
|
|
2408
|
+
>>> _get_ig_badge_class("IG2")
|
|
2409
|
+
"ig-badge-2"
|
|
2410
|
+
|
|
2411
|
+
>>> _get_ig_badge_class("UNKNOWN")
|
|
2412
|
+
"ig-badge-default"
|
|
2413
|
+
|
|
2414
|
+
CSS Styling:
|
|
2415
|
+
.ig-badge-1 { background-color: #3498db; color: white; } /* Blue */
|
|
2416
|
+
.ig-badge-2 { background-color: #27ae60; color: white; } /* Green */
|
|
2417
|
+
.ig-badge-3 { background-color: #9b59b6; color: white; } /* Purple */
|
|
2418
|
+
|
|
2419
|
+
Notes:
|
|
2420
|
+
- Used consistently across Implementation Groups and Detailed Findings sections
|
|
2421
|
+
- Provides visual hierarchy for IG levels
|
|
2422
|
+
- Can be customized via CSS for different color schemes
|
|
2423
|
+
"""
|
|
2424
|
+
badge_classes = {
|
|
2425
|
+
'IG1': 'ig-badge-1', # Blue
|
|
2426
|
+
'IG2': 'ig-badge-2', # Green
|
|
2427
|
+
'IG3': 'ig-badge-3' # Purple
|
|
2428
|
+
}
|
|
2429
|
+
return badge_classes.get(ig_name, 'ig-badge-default')
|
|
2430
|
+
|
|
2431
|
+
def _enrich_control_metadata(self, control_data: Dict[str, Any], control_id: str, ig_name: str,
|
|
2432
|
+
all_igs: Dict[str, Any]) -> Dict[str, Any]:
|
|
2433
|
+
"""Add display metadata to control data for enhanced HTML presentation.
|
|
2434
|
+
|
|
2435
|
+
Enriches control data with additional fields needed for improved display,
|
|
2436
|
+
including formatted names, IG membership badges, and truncation indicators.
|
|
2437
|
+
|
|
2438
|
+
Args:
|
|
2439
|
+
control_data: Existing control data dictionary
|
|
2440
|
+
control_id: Control identifier (e.g., "1.5")
|
|
2441
|
+
ig_name: Implementation Group name (e.g., "IG1")
|
|
2442
|
+
all_igs: All implementation groups data for determining originating IG
|
|
2443
|
+
|
|
2444
|
+
Returns:
|
|
2445
|
+
Enhanced control data with additional fields:
|
|
2446
|
+
- display_name: Formatted name combining control ID and rule name
|
|
2447
|
+
- originating_ig: Which IG introduced this control (IG1, IG2, or IG3)
|
|
2448
|
+
- ig_badge_class: CSS class for IG badge styling
|
|
2449
|
+
- needs_truncation: Boolean indicating if display name exceeds 50 characters
|
|
2450
|
+
|
|
2451
|
+
Examples:
|
|
2452
|
+
Input control_data:
|
|
2453
|
+
{
|
|
2454
|
+
'control_id': '1.5',
|
|
2455
|
+
'config_rule_name': 'root-account-hardware-mfa-enabled',
|
|
2456
|
+
'compliance_percentage': 0.0,
|
|
2457
|
+
'total_resources': 1
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
Output enriched data (includes all input fields plus):
|
|
2461
|
+
{
|
|
2462
|
+
...original fields...,
|
|
2463
|
+
'display_name': '1.5: root-account-hardware-mfa-enabled',
|
|
2464
|
+
'originating_ig': 'IG1',
|
|
2465
|
+
'ig_badge_class': 'ig-badge-1',
|
|
2466
|
+
'needs_truncation': False
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
Notes:
|
|
2470
|
+
- Called during _enhance_html_structure() for each control
|
|
2471
|
+
- Truncation threshold is 50 characters
|
|
2472
|
+
- Gracefully handles missing config_rule_name
|
|
2473
|
+
- Originating IG is determined by checking IG1, IG2, IG3 in order
|
|
2474
|
+
"""
|
|
2475
|
+
enriched = control_data.copy()
|
|
2476
|
+
|
|
2477
|
+
# Format display name
|
|
2478
|
+
enriched['display_name'] = self._format_control_display_name(
|
|
2479
|
+
control_id,
|
|
2480
|
+
control_data.get('config_rule_name', ''),
|
|
2481
|
+
control_data.get('title')
|
|
2482
|
+
)
|
|
2483
|
+
|
|
2484
|
+
# Determine originating IG (which IG introduced this control)
|
|
2485
|
+
originating_ig = self._determine_originating_ig(control_id, all_igs)
|
|
2486
|
+
enriched['originating_ig'] = originating_ig
|
|
2487
|
+
|
|
2488
|
+
# Get badge class for the originating IG
|
|
2489
|
+
enriched['ig_badge_class'] = self._get_ig_badge_class(originating_ig)
|
|
2490
|
+
|
|
2491
|
+
# Check if truncation is needed (threshold: 50 characters)
|
|
2492
|
+
enriched['needs_truncation'] = len(enriched['display_name']) > 50
|
|
2493
|
+
|
|
2494
|
+
return enriched
|
|
2495
|
+
|
|
2496
|
+
def _determine_originating_ig(self, control_id: str, all_igs: Dict[str, Any]) -> str:
|
|
2497
|
+
"""Determine which Implementation Group introduced a control.
|
|
2498
|
+
|
|
2499
|
+
Args:
|
|
2500
|
+
control_id: Control identifier
|
|
2501
|
+
all_igs: All implementation groups data
|
|
2502
|
+
|
|
2503
|
+
Returns:
|
|
2504
|
+
Name of the IG that introduced this control (IG1, IG2, or IG3)
|
|
2505
|
+
"""
|
|
2506
|
+
# Check in order: IG1, IG2, IG3
|
|
2507
|
+
# The first IG that contains the control is the originating IG
|
|
2508
|
+
for ig_name in ['IG1', 'IG2', 'IG3']:
|
|
2509
|
+
if ig_name in all_igs:
|
|
2510
|
+
if control_id in all_igs[ig_name].get('controls', {}):
|
|
2511
|
+
return ig_name
|
|
2512
|
+
|
|
2513
|
+
# Default to IG1 if not found
|
|
2514
|
+
return 'IG1'
|
|
2515
|
+
|
|
2516
|
+
def _consolidate_findings_by_control(self, implementation_groups: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
2517
|
+
"""Consolidate findings from all IGs, grouped by control ID only.
|
|
2518
|
+
|
|
2519
|
+
This method deduplicates findings across Implementation Groups so that each
|
|
2520
|
+
finding appears only once in the Detailed Findings section, eliminating
|
|
2521
|
+
redundancy when a control appears in multiple IGs.
|
|
2522
|
+
|
|
2523
|
+
Args:
|
|
2524
|
+
implementation_groups: Implementation groups data with controls and findings
|
|
2525
|
+
|
|
2526
|
+
Returns:
|
|
2527
|
+
Dictionary mapping control_id -> consolidated control data with:
|
|
2528
|
+
- findings: List of deduplicated non-compliant findings
|
|
2529
|
+
- member_igs: List of IGs that include this control (e.g., ["IG1", "IG2"])
|
|
2530
|
+
- config_rule_name: AWS Config rule name for the control
|
|
2531
|
+
- title: Human-readable title for the control
|
|
2532
|
+
|
|
2533
|
+
Results are sorted by control ID in alphanumeric order.
|
|
2534
|
+
|
|
2535
|
+
Examples:
|
|
2536
|
+
Input: Control "1.5" appears in IG1, IG2, and IG3 with same findings
|
|
2537
|
+
Output: Single entry for "1.5" with:
|
|
2538
|
+
{
|
|
2539
|
+
'1.5': {
|
|
2540
|
+
'findings': [...deduplicated findings...],
|
|
2541
|
+
'member_igs': ['IG1', 'IG2', 'IG3'],
|
|
2542
|
+
'config_rule_name': 'root-account-hardware-mfa-enabled',
|
|
2543
|
+
'title': 'Root Account Hardware MFA'
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
Deduplication Strategy:
|
|
2548
|
+
- Uses (resource_id, control_id, region) tuple as unique key
|
|
2549
|
+
- Prevents same resource from appearing multiple times
|
|
2550
|
+
- Preserves all unique findings across IGs
|
|
2551
|
+
|
|
2552
|
+
Sorting:
|
|
2553
|
+
- Controls are sorted alphanumerically (1.1, 1.2, ..., 1.10, 2.1, ...)
|
|
2554
|
+
- Uses _sort_control_id() helper for proper numeric sorting
|
|
2555
|
+
|
|
2556
|
+
Notes:
|
|
2557
|
+
- Eliminates "IG1 Detailed Findings", "IG2 Detailed Findings" subsections
|
|
2558
|
+
- Each control appears once with IG membership indicators
|
|
2559
|
+
- Improves report readability and reduces redundancy
|
|
2560
|
+
"""
|
|
2561
|
+
consolidated = {}
|
|
2562
|
+
seen_findings = set() # Track (resource_id, control_id, region) tuples for deduplication
|
|
2563
|
+
|
|
2564
|
+
for ig_name, ig_data in implementation_groups.items():
|
|
2565
|
+
for control_id, control_data in ig_data.get('controls', {}).items():
|
|
2566
|
+
# Initialize control entry if not exists
|
|
2567
|
+
if control_id not in consolidated:
|
|
2568
|
+
consolidated[control_id] = {
|
|
2569
|
+
'findings': [],
|
|
2570
|
+
'member_igs': [],
|
|
2571
|
+
'config_rule_name': control_data.get('config_rule_name', ''),
|
|
2572
|
+
'title': control_data.get('title', f'CIS Control {control_id}')
|
|
2573
|
+
}
|
|
2574
|
+
|
|
2575
|
+
# Track which IGs include this control
|
|
2576
|
+
consolidated[control_id]['member_igs'].append(ig_name)
|
|
2577
|
+
|
|
2578
|
+
# Add non-compliant findings (deduplicated)
|
|
2579
|
+
for finding in control_data.get('non_compliant_findings', []):
|
|
2580
|
+
finding_key = (finding.get('resource_id', ''), control_id, finding.get('region', ''))
|
|
2581
|
+
if finding_key not in seen_findings:
|
|
2582
|
+
consolidated[control_id]['findings'].append(finding)
|
|
2583
|
+
seen_findings.add(finding_key)
|
|
2584
|
+
|
|
2585
|
+
# Sort by control ID using alphanumeric sorting
|
|
2586
|
+
return dict(sorted(consolidated.items(), key=lambda x: self._sort_control_id(x[0])))
|
|
2587
|
+
|
|
2588
|
+
def _get_control_ig_membership(self, control_id: str, implementation_groups: Dict[str, Any]) -> List[str]:
|
|
2589
|
+
"""Determine which Implementation Groups include a specific control.
|
|
2590
|
+
|
|
2591
|
+
Checks all Implementation Groups (IG1, IG2, IG3) to identify which ones
|
|
2592
|
+
contain the specified control, enabling display of IG membership badges.
|
|
2593
|
+
|
|
2594
|
+
Args:
|
|
2595
|
+
control_id: Control identifier (e.g., "1.5", "2.1")
|
|
2596
|
+
implementation_groups: All IG data from the assessment
|
|
2597
|
+
|
|
2598
|
+
Returns:
|
|
2599
|
+
List of IG names that include this control, in order.
|
|
2600
|
+
Examples:
|
|
2601
|
+
- ["IG1", "IG2", "IG3"] for a control in all IGs
|
|
2602
|
+
- ["IG1", "IG2"] for a control in IG1 and IG2 only
|
|
2603
|
+
- ["IG3"] for a control unique to IG3
|
|
2604
|
+
- [] for a control not found in any IG
|
|
2605
|
+
|
|
2606
|
+
Examples:
|
|
2607
|
+
>>> _get_control_ig_membership("1.5", implementation_groups)
|
|
2608
|
+
["IG1", "IG2", "IG3"] # Control 1.5 is in all IGs
|
|
2609
|
+
|
|
2610
|
+
>>> _get_control_ig_membership("5.2", implementation_groups)
|
|
2611
|
+
["IG2", "IG3"] # Control 5.2 is only in IG2 and IG3
|
|
2612
|
+
|
|
2613
|
+
Notes:
|
|
2614
|
+
- Used to display IG membership badges in Detailed Findings section
|
|
2615
|
+
- Helps users understand which IGs require remediation for each control
|
|
2616
|
+
- Checks IGs in order: IG1, IG2, IG3
|
|
2617
|
+
"""
|
|
2618
|
+
member_igs = []
|
|
2619
|
+
for ig_name in ['IG1', 'IG2', 'IG3']:
|
|
2620
|
+
if ig_name in implementation_groups:
|
|
2621
|
+
if control_id in implementation_groups[ig_name].get('controls', {}):
|
|
2622
|
+
member_igs.append(ig_name)
|
|
2623
|
+
return member_igs
|
|
2624
|
+
|
|
2625
|
+
def _sort_control_id(self, control_id: str) -> tuple:
|
|
2626
|
+
"""Helper for alphanumeric sorting of control IDs.
|
|
2627
|
+
|
|
2628
|
+
Converts control IDs like "1.1", "1.10", "2.1" into tuples of integers
|
|
2629
|
+
for proper alphanumeric sorting. This ensures controls are displayed in
|
|
2630
|
+
the correct order (1.1, 1.2, ..., 1.9, 1.10, 2.1, ...) rather than
|
|
2631
|
+
lexicographic order (1.1, 1.10, 1.2, ...).
|
|
2632
|
+
|
|
2633
|
+
Args:
|
|
2634
|
+
control_id: Control identifier (e.g., "1.1", "1.10", "2.1")
|
|
2635
|
+
|
|
2636
|
+
Returns:
|
|
2637
|
+
Tuple of integers for sorting (e.g., (1, 1), (1, 10), (2, 1))
|
|
2638
|
+
Returns (0, 0) for non-standard control IDs as fallback
|
|
2639
|
+
|
|
2640
|
+
Examples:
|
|
2641
|
+
>>> _sort_control_id("1.1")
|
|
2642
|
+
(1, 1)
|
|
2643
|
+
|
|
2644
|
+
>>> _sort_control_id("1.10")
|
|
2645
|
+
(1, 10)
|
|
2646
|
+
|
|
2647
|
+
>>> _sort_control_id("2.1")
|
|
2648
|
+
(2, 1)
|
|
2649
|
+
|
|
2650
|
+
>>> _sort_control_id("invalid")
|
|
2651
|
+
(0, 0) # Fallback for non-standard IDs
|
|
2652
|
+
|
|
2653
|
+
Sorting Behavior:
|
|
2654
|
+
Without this helper:
|
|
2655
|
+
["1.1", "1.10", "1.2", "2.1"] # Incorrect lexicographic order
|
|
2656
|
+
|
|
2657
|
+
With this helper:
|
|
2658
|
+
["1.1", "1.2", "1.10", "2.1"] # Correct numeric order
|
|
2659
|
+
|
|
2660
|
+
Notes:
|
|
2661
|
+
- Used in _consolidate_findings_by_control() for sorting
|
|
2662
|
+
- Handles multi-level control IDs (e.g., "1.2.3" -> (1, 2, 3))
|
|
2663
|
+
- Gracefully handles malformed control IDs
|
|
2664
|
+
"""
|
|
2665
|
+
try:
|
|
2666
|
+
# Split by '.' and convert to integers
|
|
2667
|
+
parts = control_id.split('.')
|
|
2668
|
+
return tuple(int(part) for part in parts)
|
|
2669
|
+
except (ValueError, AttributeError):
|
|
2670
|
+
# Fallback for non-standard control IDs
|
|
2671
|
+
return (0, 0)
|