aws-cis-controls-assessment 1.0.7__py3-none-any.whl → 1.0.9__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/core/assessment_engine.py +4 -0
- aws_cis_assessment/core/models.py +1 -0
- aws_cis_assessment/core/scoring_engine.py +30 -0
- aws_cis_assessment/reporters/base_reporter.py +2 -0
- aws_cis_assessment/reporters/html_reporter.py +826 -45
- {aws_cis_controls_assessment-1.0.7.dist-info → aws_cis_controls_assessment-1.0.9.dist-info}/METADATA +6 -1
- {aws_cis_controls_assessment-1.0.7.dist-info → aws_cis_controls_assessment-1.0.9.dist-info}/RECORD +20 -16
- docs/README.md +1 -0
- docs/dual-scoring-implementation.md +303 -0
- docs/html-report-improvements.md +422 -0
- docs/installation.md +78 -27
- docs/scoring-comparison-aws-config.md +379 -0
- docs/scoring-methodology.md +350 -0
- docs/user-guide.md +7 -1
- {aws_cis_controls_assessment-1.0.7.dist-info → aws_cis_controls_assessment-1.0.9.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.0.7.dist-info → aws_cis_controls_assessment-1.0.9.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.0.7.dist-info → aws_cis_controls_assessment-1.0.9.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.0.7.dist-info → aws_cis_controls_assessment-1.0.9.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;
|
|
@@ -862,6 +962,171 @@ class HTMLReporter(ReportGenerator):
|
|
|
862
962
|
}
|
|
863
963
|
}
|
|
864
964
|
|
|
965
|
+
/* Score Comparison Styles */
|
|
966
|
+
.score-comparison-section {
|
|
967
|
+
background: white;
|
|
968
|
+
border-radius: 8px;
|
|
969
|
+
padding: 25px;
|
|
970
|
+
margin-bottom: 30px;
|
|
971
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
.comparison-grid {
|
|
975
|
+
display: grid;
|
|
976
|
+
grid-template-columns: 1fr 1fr;
|
|
977
|
+
gap: 20px;
|
|
978
|
+
margin-bottom: 20px;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.comparison-card {
|
|
982
|
+
background: #f8f9fa;
|
|
983
|
+
border-radius: 8px;
|
|
984
|
+
padding: 20px;
|
|
985
|
+
border: 2px solid #e9ecef;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
.comparison-card h4 {
|
|
989
|
+
margin: 0 0 15px 0;
|
|
990
|
+
color: #2c3e50;
|
|
991
|
+
font-size: 1.1em;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
.comparison-value {
|
|
995
|
+
font-size: 2.5em;
|
|
996
|
+
font-weight: bold;
|
|
997
|
+
margin: 10px 0;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
.comparison-card.weighted .comparison-value {
|
|
1001
|
+
color: #3498db;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.comparison-card.aws-config .comparison-value {
|
|
1005
|
+
color: #9b59b6;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.comparison-features {
|
|
1009
|
+
list-style: none;
|
|
1010
|
+
padding: 0;
|
|
1011
|
+
margin: 15px 0 0 0;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.comparison-features li {
|
|
1015
|
+
padding: 5px 0;
|
|
1016
|
+
color: #666;
|
|
1017
|
+
font-size: 0.9em;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
.comparison-features li:before {
|
|
1021
|
+
content: "✓ ";
|
|
1022
|
+
color: #27ae60;
|
|
1023
|
+
font-weight: bold;
|
|
1024
|
+
margin-right: 5px;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
.score-difference {
|
|
1028
|
+
background: #fff3cd;
|
|
1029
|
+
border: 1px solid #ffc107;
|
|
1030
|
+
border-radius: 8px;
|
|
1031
|
+
padding: 15px;
|
|
1032
|
+
margin-top: 20px;
|
|
1033
|
+
display: flex;
|
|
1034
|
+
align-items: center;
|
|
1035
|
+
gap: 15px;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
.diff-icon {
|
|
1039
|
+
font-size: 2em;
|
|
1040
|
+
flex-shrink: 0;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
.diff-content {
|
|
1044
|
+
flex-grow: 1;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.diff-content strong {
|
|
1048
|
+
display: block;
|
|
1049
|
+
margin-bottom: 5px;
|
|
1050
|
+
color: #856404;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.diff-content p {
|
|
1054
|
+
margin: 0;
|
|
1055
|
+
color: #856404;
|
|
1056
|
+
font-size: 0.9em;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
.score-difference.neutral {
|
|
1060
|
+
background: #d1ecf1;
|
|
1061
|
+
border-color: #17a2b8;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
.score-difference.neutral .diff-content strong,
|
|
1065
|
+
.score-difference.neutral .diff-content p {
|
|
1066
|
+
color: #0c5460;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
.score-difference.warning {
|
|
1070
|
+
background: #fff3cd;
|
|
1071
|
+
border-color: #ffc107;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
.score-difference.warning .diff-content strong,
|
|
1075
|
+
.score-difference.warning .diff-content p {
|
|
1076
|
+
color: #856404;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
.score-difference.positive {
|
|
1080
|
+
background: #d4edda;
|
|
1081
|
+
border-color: #28a745;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
.score-difference.positive .diff-content strong,
|
|
1085
|
+
.score-difference.positive .diff-content p {
|
|
1086
|
+
color: #155724;
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
.methodology-note {
|
|
1090
|
+
background: #e7f3ff;
|
|
1091
|
+
border-left: 4px solid #3498db;
|
|
1092
|
+
padding: 15px;
|
|
1093
|
+
margin-top: 20px;
|
|
1094
|
+
border-radius: 4px;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
.methodology-note h5 {
|
|
1098
|
+
margin: 0 0 10px 0;
|
|
1099
|
+
color: #2c3e50;
|
|
1100
|
+
font-size: 1em;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
.methodology-note p {
|
|
1104
|
+
margin: 5px 0;
|
|
1105
|
+
color: #555;
|
|
1106
|
+
font-size: 0.9em;
|
|
1107
|
+
line-height: 1.6;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
.methodology-note a {
|
|
1111
|
+
color: #3498db;
|
|
1112
|
+
text-decoration: none;
|
|
1113
|
+
font-weight: 500;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
.methodology-note a:hover {
|
|
1117
|
+
text-decoration: underline;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
@media (max-width: 768px) {
|
|
1121
|
+
.comparison-grid {
|
|
1122
|
+
grid-template-columns: 1fr;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
.comparison-value {
|
|
1126
|
+
font-size: 2em;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
|
|
865
1130
|
/* Print styles */
|
|
866
1131
|
@media print {
|
|
867
1132
|
.navigation {
|
|
@@ -1200,6 +1465,18 @@ class HTMLReporter(ReportGenerator):
|
|
|
1200
1465
|
a.click();
|
|
1201
1466
|
window.URL.revokeObjectURL(url);
|
|
1202
1467
|
}}
|
|
1468
|
+
|
|
1469
|
+
// Toggle scoring details
|
|
1470
|
+
function toggleScoringDetails() {{
|
|
1471
|
+
const detailsSection = document.getElementById('scoringDetails');
|
|
1472
|
+
if (detailsSection) {{
|
|
1473
|
+
if (detailsSection.style.display === 'none') {{
|
|
1474
|
+
detailsSection.style.display = 'block';
|
|
1475
|
+
}} else {{
|
|
1476
|
+
detailsSection.style.display = 'none';
|
|
1477
|
+
}}
|
|
1478
|
+
}}
|
|
1479
|
+
}}
|
|
1203
1480
|
"""
|
|
1204
1481
|
|
|
1205
1482
|
def _generate_header(self, html_data: Dict[str, Any]) -> str:
|
|
@@ -1262,14 +1539,22 @@ class HTMLReporter(ReportGenerator):
|
|
|
1262
1539
|
|
|
1263
1540
|
# Generate metric cards
|
|
1264
1541
|
overall_status = self._get_status_class(exec_summary.get("overall_compliance_percentage", 0))
|
|
1542
|
+
aws_config_score = exec_summary.get('aws_config_style_score', 0)
|
|
1543
|
+
score_diff = exec_summary.get('score_difference', 0)
|
|
1265
1544
|
|
|
1266
1545
|
metric_cards = f"""
|
|
1267
1546
|
<div class="metric-card {overall_status}">
|
|
1268
1547
|
<div class="metric-value">{exec_summary.get('overall_compliance_percentage', 0):.1f}%</div>
|
|
1269
|
-
<div class="metric-label">
|
|
1548
|
+
<div class="metric-label">Weighted Compliance Score</div>
|
|
1270
1549
|
<div class="metric-trend trend-stable">Grade: {exec_summary.get('compliance_grade', 'N/A')}</div>
|
|
1271
1550
|
</div>
|
|
1272
1551
|
|
|
1552
|
+
<div class="metric-card">
|
|
1553
|
+
<div class="metric-value">{aws_config_score:.1f}%</div>
|
|
1554
|
+
<div class="metric-label">AWS Config Style Score</div>
|
|
1555
|
+
<div class="metric-trend trend-stable">Unweighted</div>
|
|
1556
|
+
</div>
|
|
1557
|
+
|
|
1273
1558
|
<div class="metric-card">
|
|
1274
1559
|
<div class="metric-value">{exec_summary.get('total_resources', 0):,}</div>
|
|
1275
1560
|
<div class="metric-label">Resources Evaluated</div>
|
|
@@ -1281,14 +1566,15 @@ class HTMLReporter(ReportGenerator):
|
|
|
1281
1566
|
<div class="metric-label">Compliant Resources</div>
|
|
1282
1567
|
<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
1568
|
</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
1569
|
"""
|
|
1291
1570
|
|
|
1571
|
+
# Add scoring comparison section
|
|
1572
|
+
score_comparison = self._generate_score_comparison_section(
|
|
1573
|
+
exec_summary.get('overall_compliance_percentage', 0),
|
|
1574
|
+
aws_config_score,
|
|
1575
|
+
score_diff
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1292
1578
|
# Generate IG progress bars
|
|
1293
1579
|
ig_progress = ""
|
|
1294
1580
|
for ig in ['ig1', 'ig2', 'ig3']:
|
|
@@ -1335,6 +1621,8 @@ class HTMLReporter(ReportGenerator):
|
|
|
1335
1621
|
{metric_cards}
|
|
1336
1622
|
</div>
|
|
1337
1623
|
|
|
1624
|
+
{score_comparison}
|
|
1625
|
+
|
|
1338
1626
|
<div class="ig-progress-section">
|
|
1339
1627
|
<h3>Implementation Groups Progress</h3>
|
|
1340
1628
|
{ig_progress}
|
|
@@ -1368,6 +1656,21 @@ class HTMLReporter(ReportGenerator):
|
|
|
1368
1656
|
findings_count = len(control_data.get("non_compliant_findings", []))
|
|
1369
1657
|
status_class = self._get_status_class(control_data["compliance_percentage"])
|
|
1370
1658
|
|
|
1659
|
+
# Get display name (enriched in _enhance_html_structure)
|
|
1660
|
+
display_name = control_data.get('display_name', control_id)
|
|
1661
|
+
needs_truncation = control_data.get('needs_truncation', False)
|
|
1662
|
+
|
|
1663
|
+
# Add title attribute for truncated names
|
|
1664
|
+
title_attr = f' title="{display_name}"' if needs_truncation else ''
|
|
1665
|
+
display_name_class = 'control-display-name truncated' if needs_truncation else 'control-display-name'
|
|
1666
|
+
|
|
1667
|
+
# Get IG membership badges
|
|
1668
|
+
originating_ig = control_data.get('originating_ig', 'IG1')
|
|
1669
|
+
ig_badge_class = control_data.get('ig_badge_class', 'ig-badge-1')
|
|
1670
|
+
|
|
1671
|
+
# Build IG membership badges HTML
|
|
1672
|
+
ig_badges_html = f'<span class="ig-membership-badge {ig_badge_class}">{originating_ig}</span>'
|
|
1673
|
+
|
|
1371
1674
|
# Add inheritance indicator for inherited controls
|
|
1372
1675
|
inheritance_indicator = ""
|
|
1373
1676
|
if ig_name != "IG1" and control_id in unique_controls.get("IG1", {}):
|
|
@@ -1378,10 +1681,12 @@ class HTMLReporter(ReportGenerator):
|
|
|
1378
1681
|
controls_html += f"""
|
|
1379
1682
|
<div class="control-card">
|
|
1380
1683
|
<div class="control-header">
|
|
1381
|
-
<div class="
|
|
1684
|
+
<div class="{display_name_class}"{title_attr}>{display_name}</div>
|
|
1382
1685
|
<div class="badge {control_data.get('severity_badge', 'medium')}">{findings_count} Issues</div>
|
|
1383
1686
|
</div>
|
|
1384
|
-
<div class="
|
|
1687
|
+
<div class="ig-membership-badges">
|
|
1688
|
+
{ig_badges_html}
|
|
1689
|
+
</div>
|
|
1385
1690
|
{inheritance_indicator}
|
|
1386
1691
|
<div class="progress-container">
|
|
1387
1692
|
<div class="progress-bar {status_class}" data-width="{control_data['compliance_percentage']}">
|
|
@@ -1430,39 +1735,73 @@ class HTMLReporter(ReportGenerator):
|
|
|
1430
1735
|
def _generate_detailed_findings_section(self, html_data: Dict[str, Any]) -> str:
|
|
1431
1736
|
"""Generate detailed findings section.
|
|
1432
1737
|
|
|
1738
|
+
This method consolidates findings by control ID only (not by IG) to eliminate
|
|
1739
|
+
duplication. Each control appears once with IG membership indicators.
|
|
1740
|
+
|
|
1433
1741
|
Args:
|
|
1434
1742
|
html_data: Enhanced HTML report data
|
|
1435
1743
|
|
|
1436
1744
|
Returns:
|
|
1437
1745
|
Detailed findings HTML as string
|
|
1438
1746
|
"""
|
|
1439
|
-
|
|
1747
|
+
# Consolidate findings by control ID (deduplicated across IGs)
|
|
1748
|
+
consolidated_findings = self._consolidate_findings_by_control(
|
|
1749
|
+
html_data.get("implementation_groups", {})
|
|
1750
|
+
)
|
|
1440
1751
|
|
|
1441
|
-
|
|
1442
|
-
|
|
1752
|
+
findings_content = ""
|
|
1753
|
+
|
|
1754
|
+
# Generate findings grouped by control ID only (sorted alphanumerically)
|
|
1755
|
+
for control_id, control_data in consolidated_findings.items():
|
|
1756
|
+
findings = control_data.get('findings', [])
|
|
1757
|
+
|
|
1758
|
+
# Skip if no non-compliant findings
|
|
1759
|
+
if not findings:
|
|
1760
|
+
continue
|
|
1761
|
+
|
|
1762
|
+
# Get control metadata
|
|
1763
|
+
config_rule_name = control_data.get('config_rule_name', '')
|
|
1764
|
+
title = control_data.get('title', f'CIS Control {control_id}')
|
|
1765
|
+
member_igs = control_data.get('member_igs', [])
|
|
1766
|
+
|
|
1767
|
+
# Format display name for collapsible header
|
|
1768
|
+
display_name = self._format_control_display_name(control_id, config_rule_name, title)
|
|
1769
|
+
|
|
1770
|
+
# Generate IG membership badges
|
|
1771
|
+
ig_badges_html = ""
|
|
1772
|
+
for ig in member_igs:
|
|
1773
|
+
badge_class = self._get_ig_badge_class(ig)
|
|
1774
|
+
ig_badges_html += f'<span class="ig-membership-badge {badge_class}">{ig}</span>'
|
|
1775
|
+
|
|
1776
|
+
# Generate findings table rows
|
|
1777
|
+
findings_rows = ""
|
|
1778
|
+
for finding in findings:
|
|
1779
|
+
if finding.get("compliance_status") == "NON_COMPLIANT":
|
|
1780
|
+
findings_rows += f"""
|
|
1781
|
+
<tr>
|
|
1782
|
+
<td>{finding.get('resource_id', 'N/A')}</td>
|
|
1783
|
+
<td>{finding.get('resource_type', 'N/A')}</td>
|
|
1784
|
+
<td>{finding.get('region', 'N/A')}</td>
|
|
1785
|
+
<td><span class="badge {finding.get('compliance_status', '').lower()}">{finding.get('compliance_status', 'UNKNOWN')}</span></td>
|
|
1786
|
+
<td>{finding.get('evaluation_reason', 'N/A')}</td>
|
|
1787
|
+
<td>{finding.get('config_rule_name', config_rule_name)}</td>
|
|
1788
|
+
</tr>
|
|
1789
|
+
"""
|
|
1443
1790
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1791
|
+
# Only add control section if there are non-compliant findings
|
|
1792
|
+
if findings_rows:
|
|
1793
|
+
non_compliant_count = len([f for f in findings if f.get('compliance_status') == 'NON_COMPLIANT'])
|
|
1447
1794
|
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
<
|
|
1453
|
-
|
|
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>
|
|
1795
|
+
findings_content += f"""
|
|
1796
|
+
<div class="control-findings-section">
|
|
1797
|
+
<button class="collapsible">
|
|
1798
|
+
<span class="control-display-name">{display_name}</span>
|
|
1799
|
+
<span class="findings-count"> - Non-Compliant Resources ({non_compliant_count} items)</span>
|
|
1800
|
+
</button>
|
|
1465
1801
|
<div class="collapsible-content">
|
|
1802
|
+
<div class="ig-membership-badges" style="margin-bottom: 15px;">
|
|
1803
|
+
<strong>Implementation Groups:</strong> {ig_badges_html}
|
|
1804
|
+
</div>
|
|
1466
1805
|
<table class="findings-table">
|
|
1467
1806
|
<thead>
|
|
1468
1807
|
<tr>
|
|
@@ -1479,23 +1818,20 @@ class HTMLReporter(ReportGenerator):
|
|
|
1479
1818
|
</tbody>
|
|
1480
1819
|
</table>
|
|
1481
1820
|
</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
1821
|
</div>
|
|
1490
1822
|
"""
|
|
1491
1823
|
|
|
1492
1824
|
return f"""
|
|
1493
1825
|
<section id="detailed-findings" class="detailed-findings">
|
|
1494
1826
|
<h2>Detailed Findings</h2>
|
|
1827
|
+
<p class="section-description">
|
|
1828
|
+
Findings are grouped by control ID and deduplicated across Implementation Groups.
|
|
1829
|
+
Each control shows which IGs include it.
|
|
1830
|
+
</p>
|
|
1495
1831
|
<div class="search-container">
|
|
1496
1832
|
<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
1833
|
</div>
|
|
1498
|
-
{
|
|
1834
|
+
{findings_content if findings_content else '<p>No non-compliant findings to display.</p>'}
|
|
1499
1835
|
</section>
|
|
1500
1836
|
"""
|
|
1501
1837
|
|
|
@@ -1762,6 +2098,90 @@ class HTMLReporter(ReportGenerator):
|
|
|
1762
2098
|
display_findings.append(display_finding)
|
|
1763
2099
|
return display_findings
|
|
1764
2100
|
|
|
2101
|
+
def _generate_score_comparison_section(self, weighted_score: float,
|
|
2102
|
+
aws_config_score: float,
|
|
2103
|
+
score_diff: float) -> str:
|
|
2104
|
+
"""Generate scoring methodology comparison section.
|
|
2105
|
+
|
|
2106
|
+
Args:
|
|
2107
|
+
weighted_score: Our weighted compliance score
|
|
2108
|
+
aws_config_score: AWS Config Conformance Pack style score
|
|
2109
|
+
score_diff: Difference between the two scores
|
|
2110
|
+
|
|
2111
|
+
Returns:
|
|
2112
|
+
HTML section comparing the two scoring approaches
|
|
2113
|
+
"""
|
|
2114
|
+
# Determine interpretation based on difference
|
|
2115
|
+
if abs(score_diff) < 1.0:
|
|
2116
|
+
interpretation = "Both scoring approaches show similar results, indicating balanced compliance across all control priorities."
|
|
2117
|
+
icon = "ℹ️"
|
|
2118
|
+
diff_class = "neutral"
|
|
2119
|
+
elif score_diff < 0:
|
|
2120
|
+
# Weighted score is lower
|
|
2121
|
+
interpretation = f"The weighted score is {abs(score_diff):.1f}% lower, indicating critical security controls need attention despite good overall resource compliance."
|
|
2122
|
+
icon = "⚠️"
|
|
2123
|
+
diff_class = "warning"
|
|
2124
|
+
else:
|
|
2125
|
+
# Weighted score is higher
|
|
2126
|
+
interpretation = f"The weighted score is {score_diff:.1f}% higher, indicating strong performance in critical security controls despite some gaps in less critical areas."
|
|
2127
|
+
icon = "✓"
|
|
2128
|
+
diff_class = "positive"
|
|
2129
|
+
|
|
2130
|
+
return f"""
|
|
2131
|
+
<div class="score-comparison-section">
|
|
2132
|
+
<h3>Scoring Methodology Comparison</h3>
|
|
2133
|
+
<div class="comparison-grid">
|
|
2134
|
+
<div class="comparison-card">
|
|
2135
|
+
<h4>Weighted Score (Our Approach)</h4>
|
|
2136
|
+
<div class="comparison-value">{weighted_score:.1f}%</div>
|
|
2137
|
+
<p class="comparison-description">
|
|
2138
|
+
Uses risk-based weighting where critical controls (encryption, access control)
|
|
2139
|
+
have higher impact on the overall score. Reflects actual security posture.
|
|
2140
|
+
</p>
|
|
2141
|
+
<ul class="comparison-features">
|
|
2142
|
+
<li>✓ Prioritizes critical security controls</li>
|
|
2143
|
+
<li>✓ Prevents resource count skew</li>
|
|
2144
|
+
<li>✓ Guides remediation priorities</li>
|
|
2145
|
+
</ul>
|
|
2146
|
+
</div>
|
|
2147
|
+
|
|
2148
|
+
<div class="comparison-card">
|
|
2149
|
+
<h4>AWS Config Style Score</h4>
|
|
2150
|
+
<div class="comparison-value">{aws_config_score:.1f}%</div>
|
|
2151
|
+
<p class="comparison-description">
|
|
2152
|
+
Simple unweighted calculation: compliant resources divided by total resources.
|
|
2153
|
+
All rules treated equally regardless of security criticality.
|
|
2154
|
+
</p>
|
|
2155
|
+
<ul class="comparison-features">
|
|
2156
|
+
<li>✓ Simple and straightforward</li>
|
|
2157
|
+
<li>✓ Easy to audit</li>
|
|
2158
|
+
<li>✓ Resource-level tracking</li>
|
|
2159
|
+
</ul>
|
|
2160
|
+
</div>
|
|
2161
|
+
</div>
|
|
2162
|
+
|
|
2163
|
+
<div class="score-difference {diff_class}">
|
|
2164
|
+
<span class="diff-icon">{icon}</span>
|
|
2165
|
+
<div class="diff-content">
|
|
2166
|
+
<strong>Score Difference: {score_diff:+.1f}%</strong>
|
|
2167
|
+
<p>{interpretation}</p>
|
|
2168
|
+
</div>
|
|
2169
|
+
</div>
|
|
2170
|
+
|
|
2171
|
+
<div class="methodology-note">
|
|
2172
|
+
<strong>Which score should I use?</strong>
|
|
2173
|
+
<ul>
|
|
2174
|
+
<li><strong>Weighted Score:</strong> Use for security decision-making, risk prioritization, and remediation planning</li>
|
|
2175
|
+
<li><strong>AWS Config Style:</strong> Use for compliance reporting, audits, and simple stakeholder communication</li>
|
|
2176
|
+
<li><strong>Both:</strong> Track both metrics for comprehensive security program management</li>
|
|
2177
|
+
</ul>
|
|
2178
|
+
<p>
|
|
2179
|
+
<a href="#" onclick="toggleScoringDetails(); return false;">Learn more about scoring methodologies →</a>
|
|
2180
|
+
</p>
|
|
2181
|
+
</div>
|
|
2182
|
+
</div>
|
|
2183
|
+
"""
|
|
2184
|
+
|
|
1765
2185
|
def set_chart_options(self, include_charts: bool = True) -> None:
|
|
1766
2186
|
"""Configure chart inclusion options.
|
|
1767
2187
|
|
|
@@ -1902,11 +2322,45 @@ class HTMLReporter(ReportGenerator):
|
|
|
1902
2322
|
def _get_unique_controls_per_ig(self, implementation_groups: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
1903
2323
|
"""Get unique controls per Implementation Group to avoid duplication.
|
|
1904
2324
|
|
|
2325
|
+
Filters controls to show only those unique to each IG level, eliminating
|
|
2326
|
+
redundancy in the Implementation Groups section. IG2 shows only controls
|
|
2327
|
+
not in IG1, and IG3 shows only controls not in IG1 or IG2.
|
|
2328
|
+
|
|
1905
2329
|
Args:
|
|
1906
|
-
implementation_groups: Implementation groups data
|
|
2330
|
+
implementation_groups: Implementation groups data with all controls
|
|
1907
2331
|
|
|
1908
2332
|
Returns:
|
|
1909
|
-
Dictionary mapping IG names to their unique controls
|
|
2333
|
+
Dictionary mapping IG names to their unique controls:
|
|
2334
|
+
- IG1: All IG1 controls (foundational)
|
|
2335
|
+
- IG2: Only controls unique to IG2 (not in IG1)
|
|
2336
|
+
- IG3: Only controls unique to IG3 (not in IG1 or IG2)
|
|
2337
|
+
|
|
2338
|
+
Examples:
|
|
2339
|
+
Input:
|
|
2340
|
+
{
|
|
2341
|
+
'IG1': {'controls': {'1.1': {...}, '1.5': {...}}},
|
|
2342
|
+
'IG2': {'controls': {'1.1': {...}, '1.5': {...}, '5.2': {...}}},
|
|
2343
|
+
'IG3': {'controls': {'1.1': {...}, '1.5': {...}, '5.2': {...}, '13.1': {...}}}
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
Output:
|
|
2347
|
+
{
|
|
2348
|
+
'IG1': {'1.1': {...}, '1.5': {...}}, # All IG1 controls
|
|
2349
|
+
'IG2': {'5.2': {...}}, # Only 5.2 is unique to IG2
|
|
2350
|
+
'IG3': {'13.1': {...}} # Only 13.1 is unique to IG3
|
|
2351
|
+
}
|
|
2352
|
+
|
|
2353
|
+
Rationale:
|
|
2354
|
+
- Eliminates duplicate control cards across IG sections
|
|
2355
|
+
- Users see each control once in its originating IG
|
|
2356
|
+
- Reduces visual clutter and improves report readability
|
|
2357
|
+
- IG membership badges show which IGs include each control
|
|
2358
|
+
|
|
2359
|
+
Notes:
|
|
2360
|
+
- IG1 controls are always shown in full (foundational set)
|
|
2361
|
+
- Higher IGs show only their incremental additions
|
|
2362
|
+
- Cumulative nature is explained in the IG explanation box
|
|
2363
|
+
- Used by _generate_implementation_groups_section()
|
|
1910
2364
|
"""
|
|
1911
2365
|
unique_controls = {}
|
|
1912
2366
|
|
|
@@ -2159,4 +2613,331 @@ class HTMLReporter(ReportGenerator):
|
|
|
2159
2613
|
options = ""
|
|
2160
2614
|
for resource_type in sorted(resource_type_stats.keys()):
|
|
2161
2615
|
options += f'<option value="{resource_type}">{resource_type}</option>'
|
|
2162
|
-
return options
|
|
2616
|
+
return options
|
|
2617
|
+
|
|
2618
|
+
def _format_control_display_name(self, control_id: str, config_rule_name: str, title: Optional[str] = None) -> str:
|
|
2619
|
+
"""Format control display name combining ID, rule name, and optional title.
|
|
2620
|
+
|
|
2621
|
+
Creates a human-readable display name that shows both the control identifier
|
|
2622
|
+
and the AWS Config rule name, making it easier for users to understand what
|
|
2623
|
+
each control checks without looking up documentation.
|
|
2624
|
+
|
|
2625
|
+
Args:
|
|
2626
|
+
control_id: Control identifier (e.g., "1.5", "2.1")
|
|
2627
|
+
config_rule_name: AWS Config rule name (e.g., "root-account-hardware-mfa-enabled")
|
|
2628
|
+
title: Optional human-readable title for the control
|
|
2629
|
+
|
|
2630
|
+
Returns:
|
|
2631
|
+
Formatted string for display in the following formats:
|
|
2632
|
+
- With title: "{control_id}: {title} ({config_rule_name})"
|
|
2633
|
+
- Without title: "{control_id}: {config_rule_name}"
|
|
2634
|
+
- Fallback (no rule name): "{control_id}"
|
|
2635
|
+
|
|
2636
|
+
Examples:
|
|
2637
|
+
>>> _format_control_display_name("1.5", "root-account-hardware-mfa-enabled")
|
|
2638
|
+
"1.5: root-account-hardware-mfa-enabled"
|
|
2639
|
+
|
|
2640
|
+
>>> _format_control_display_name("2.1", "iam-password-policy", "IAM Password Policy")
|
|
2641
|
+
"2.1: IAM Password Policy (iam-password-policy)"
|
|
2642
|
+
|
|
2643
|
+
>>> _format_control_display_name("3.1", "")
|
|
2644
|
+
"3.1"
|
|
2645
|
+
|
|
2646
|
+
Notes:
|
|
2647
|
+
- Gracefully handles missing config_rule_name by falling back to control_id only
|
|
2648
|
+
- Used in both Implementation Groups and Detailed Findings sections
|
|
2649
|
+
- Display names longer than 50 characters are truncated with tooltips
|
|
2650
|
+
"""
|
|
2651
|
+
if not config_rule_name:
|
|
2652
|
+
# Fallback to control_id only if config_rule_name is missing
|
|
2653
|
+
return control_id
|
|
2654
|
+
|
|
2655
|
+
if title:
|
|
2656
|
+
return f"{control_id}: {title} ({config_rule_name})"
|
|
2657
|
+
else:
|
|
2658
|
+
return f"{control_id}: {config_rule_name}"
|
|
2659
|
+
|
|
2660
|
+
def _get_ig_badge_class(self, ig_name: str) -> str:
|
|
2661
|
+
"""Get CSS class for IG badge styling.
|
|
2662
|
+
|
|
2663
|
+
Returns the appropriate CSS class for styling Implementation Group badges
|
|
2664
|
+
with consistent color coding across the report.
|
|
2665
|
+
|
|
2666
|
+
Args:
|
|
2667
|
+
ig_name: Implementation Group name (IG1, IG2, or IG3)
|
|
2668
|
+
|
|
2669
|
+
Returns:
|
|
2670
|
+
CSS class name for the badge:
|
|
2671
|
+
- 'ig-badge-1' for IG1 (blue styling)
|
|
2672
|
+
- 'ig-badge-2' for IG2 (green styling)
|
|
2673
|
+
- 'ig-badge-3' for IG3 (purple styling)
|
|
2674
|
+
- 'ig-badge-default' for unknown IGs (gray styling)
|
|
2675
|
+
|
|
2676
|
+
Examples:
|
|
2677
|
+
>>> _get_ig_badge_class("IG1")
|
|
2678
|
+
"ig-badge-1"
|
|
2679
|
+
|
|
2680
|
+
>>> _get_ig_badge_class("IG2")
|
|
2681
|
+
"ig-badge-2"
|
|
2682
|
+
|
|
2683
|
+
>>> _get_ig_badge_class("UNKNOWN")
|
|
2684
|
+
"ig-badge-default"
|
|
2685
|
+
|
|
2686
|
+
CSS Styling:
|
|
2687
|
+
.ig-badge-1 { background-color: #3498db; color: white; } /* Blue */
|
|
2688
|
+
.ig-badge-2 { background-color: #27ae60; color: white; } /* Green */
|
|
2689
|
+
.ig-badge-3 { background-color: #9b59b6; color: white; } /* Purple */
|
|
2690
|
+
|
|
2691
|
+
Notes:
|
|
2692
|
+
- Used consistently across Implementation Groups and Detailed Findings sections
|
|
2693
|
+
- Provides visual hierarchy for IG levels
|
|
2694
|
+
- Can be customized via CSS for different color schemes
|
|
2695
|
+
"""
|
|
2696
|
+
badge_classes = {
|
|
2697
|
+
'IG1': 'ig-badge-1', # Blue
|
|
2698
|
+
'IG2': 'ig-badge-2', # Green
|
|
2699
|
+
'IG3': 'ig-badge-3' # Purple
|
|
2700
|
+
}
|
|
2701
|
+
return badge_classes.get(ig_name, 'ig-badge-default')
|
|
2702
|
+
|
|
2703
|
+
def _enrich_control_metadata(self, control_data: Dict[str, Any], control_id: str, ig_name: str,
|
|
2704
|
+
all_igs: Dict[str, Any]) -> Dict[str, Any]:
|
|
2705
|
+
"""Add display metadata to control data for enhanced HTML presentation.
|
|
2706
|
+
|
|
2707
|
+
Enriches control data with additional fields needed for improved display,
|
|
2708
|
+
including formatted names, IG membership badges, and truncation indicators.
|
|
2709
|
+
|
|
2710
|
+
Args:
|
|
2711
|
+
control_data: Existing control data dictionary
|
|
2712
|
+
control_id: Control identifier (e.g., "1.5")
|
|
2713
|
+
ig_name: Implementation Group name (e.g., "IG1")
|
|
2714
|
+
all_igs: All implementation groups data for determining originating IG
|
|
2715
|
+
|
|
2716
|
+
Returns:
|
|
2717
|
+
Enhanced control data with additional fields:
|
|
2718
|
+
- display_name: Formatted name combining control ID and rule name
|
|
2719
|
+
- originating_ig: Which IG introduced this control (IG1, IG2, or IG3)
|
|
2720
|
+
- ig_badge_class: CSS class for IG badge styling
|
|
2721
|
+
- needs_truncation: Boolean indicating if display name exceeds 50 characters
|
|
2722
|
+
|
|
2723
|
+
Examples:
|
|
2724
|
+
Input control_data:
|
|
2725
|
+
{
|
|
2726
|
+
'control_id': '1.5',
|
|
2727
|
+
'config_rule_name': 'root-account-hardware-mfa-enabled',
|
|
2728
|
+
'compliance_percentage': 0.0,
|
|
2729
|
+
'total_resources': 1
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
Output enriched data (includes all input fields plus):
|
|
2733
|
+
{
|
|
2734
|
+
...original fields...,
|
|
2735
|
+
'display_name': '1.5: root-account-hardware-mfa-enabled',
|
|
2736
|
+
'originating_ig': 'IG1',
|
|
2737
|
+
'ig_badge_class': 'ig-badge-1',
|
|
2738
|
+
'needs_truncation': False
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
Notes:
|
|
2742
|
+
- Called during _enhance_html_structure() for each control
|
|
2743
|
+
- Truncation threshold is 50 characters
|
|
2744
|
+
- Gracefully handles missing config_rule_name
|
|
2745
|
+
- Originating IG is determined by checking IG1, IG2, IG3 in order
|
|
2746
|
+
"""
|
|
2747
|
+
enriched = control_data.copy()
|
|
2748
|
+
|
|
2749
|
+
# Format display name
|
|
2750
|
+
enriched['display_name'] = self._format_control_display_name(
|
|
2751
|
+
control_id,
|
|
2752
|
+
control_data.get('config_rule_name', ''),
|
|
2753
|
+
control_data.get('title')
|
|
2754
|
+
)
|
|
2755
|
+
|
|
2756
|
+
# Determine originating IG (which IG introduced this control)
|
|
2757
|
+
originating_ig = self._determine_originating_ig(control_id, all_igs)
|
|
2758
|
+
enriched['originating_ig'] = originating_ig
|
|
2759
|
+
|
|
2760
|
+
# Get badge class for the originating IG
|
|
2761
|
+
enriched['ig_badge_class'] = self._get_ig_badge_class(originating_ig)
|
|
2762
|
+
|
|
2763
|
+
# Check if truncation is needed (threshold: 50 characters)
|
|
2764
|
+
enriched['needs_truncation'] = len(enriched['display_name']) > 50
|
|
2765
|
+
|
|
2766
|
+
return enriched
|
|
2767
|
+
|
|
2768
|
+
def _determine_originating_ig(self, control_id: str, all_igs: Dict[str, Any]) -> str:
|
|
2769
|
+
"""Determine which Implementation Group introduced a control.
|
|
2770
|
+
|
|
2771
|
+
Args:
|
|
2772
|
+
control_id: Control identifier
|
|
2773
|
+
all_igs: All implementation groups data
|
|
2774
|
+
|
|
2775
|
+
Returns:
|
|
2776
|
+
Name of the IG that introduced this control (IG1, IG2, or IG3)
|
|
2777
|
+
"""
|
|
2778
|
+
# Check in order: IG1, IG2, IG3
|
|
2779
|
+
# The first IG that contains the control is the originating IG
|
|
2780
|
+
for ig_name in ['IG1', 'IG2', 'IG3']:
|
|
2781
|
+
if ig_name in all_igs:
|
|
2782
|
+
if control_id in all_igs[ig_name].get('controls', {}):
|
|
2783
|
+
return ig_name
|
|
2784
|
+
|
|
2785
|
+
# Default to IG1 if not found
|
|
2786
|
+
return 'IG1'
|
|
2787
|
+
|
|
2788
|
+
def _consolidate_findings_by_control(self, implementation_groups: Dict[str, Any]) -> Dict[str, Dict[str, Any]]:
|
|
2789
|
+
"""Consolidate findings from all IGs, grouped by control ID only.
|
|
2790
|
+
|
|
2791
|
+
This method deduplicates findings across Implementation Groups so that each
|
|
2792
|
+
finding appears only once in the Detailed Findings section, eliminating
|
|
2793
|
+
redundancy when a control appears in multiple IGs.
|
|
2794
|
+
|
|
2795
|
+
Args:
|
|
2796
|
+
implementation_groups: Implementation groups data with controls and findings
|
|
2797
|
+
|
|
2798
|
+
Returns:
|
|
2799
|
+
Dictionary mapping control_id -> consolidated control data with:
|
|
2800
|
+
- findings: List of deduplicated non-compliant findings
|
|
2801
|
+
- member_igs: List of IGs that include this control (e.g., ["IG1", "IG2"])
|
|
2802
|
+
- config_rule_name: AWS Config rule name for the control
|
|
2803
|
+
- title: Human-readable title for the control
|
|
2804
|
+
|
|
2805
|
+
Results are sorted by control ID in alphanumeric order.
|
|
2806
|
+
|
|
2807
|
+
Examples:
|
|
2808
|
+
Input: Control "1.5" appears in IG1, IG2, and IG3 with same findings
|
|
2809
|
+
Output: Single entry for "1.5" with:
|
|
2810
|
+
{
|
|
2811
|
+
'1.5': {
|
|
2812
|
+
'findings': [...deduplicated findings...],
|
|
2813
|
+
'member_igs': ['IG1', 'IG2', 'IG3'],
|
|
2814
|
+
'config_rule_name': 'root-account-hardware-mfa-enabled',
|
|
2815
|
+
'title': 'Root Account Hardware MFA'
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
Deduplication Strategy:
|
|
2820
|
+
- Uses (resource_id, control_id, region) tuple as unique key
|
|
2821
|
+
- Prevents same resource from appearing multiple times
|
|
2822
|
+
- Preserves all unique findings across IGs
|
|
2823
|
+
|
|
2824
|
+
Sorting:
|
|
2825
|
+
- Controls are sorted alphanumerically (1.1, 1.2, ..., 1.10, 2.1, ...)
|
|
2826
|
+
- Uses _sort_control_id() helper for proper numeric sorting
|
|
2827
|
+
|
|
2828
|
+
Notes:
|
|
2829
|
+
- Eliminates "IG1 Detailed Findings", "IG2 Detailed Findings" subsections
|
|
2830
|
+
- Each control appears once with IG membership indicators
|
|
2831
|
+
- Improves report readability and reduces redundancy
|
|
2832
|
+
"""
|
|
2833
|
+
consolidated = {}
|
|
2834
|
+
seen_findings = set() # Track (resource_id, control_id, region) tuples for deduplication
|
|
2835
|
+
|
|
2836
|
+
for ig_name, ig_data in implementation_groups.items():
|
|
2837
|
+
for control_id, control_data in ig_data.get('controls', {}).items():
|
|
2838
|
+
# Initialize control entry if not exists
|
|
2839
|
+
if control_id not in consolidated:
|
|
2840
|
+
consolidated[control_id] = {
|
|
2841
|
+
'findings': [],
|
|
2842
|
+
'member_igs': [],
|
|
2843
|
+
'config_rule_name': control_data.get('config_rule_name', ''),
|
|
2844
|
+
'title': control_data.get('title', f'CIS Control {control_id}')
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
# Track which IGs include this control
|
|
2848
|
+
consolidated[control_id]['member_igs'].append(ig_name)
|
|
2849
|
+
|
|
2850
|
+
# Add non-compliant findings (deduplicated)
|
|
2851
|
+
for finding in control_data.get('non_compliant_findings', []):
|
|
2852
|
+
finding_key = (finding.get('resource_id', ''), control_id, finding.get('region', ''))
|
|
2853
|
+
if finding_key not in seen_findings:
|
|
2854
|
+
consolidated[control_id]['findings'].append(finding)
|
|
2855
|
+
seen_findings.add(finding_key)
|
|
2856
|
+
|
|
2857
|
+
# Sort by control ID using alphanumeric sorting
|
|
2858
|
+
return dict(sorted(consolidated.items(), key=lambda x: self._sort_control_id(x[0])))
|
|
2859
|
+
|
|
2860
|
+
def _get_control_ig_membership(self, control_id: str, implementation_groups: Dict[str, Any]) -> List[str]:
|
|
2861
|
+
"""Determine which Implementation Groups include a specific control.
|
|
2862
|
+
|
|
2863
|
+
Checks all Implementation Groups (IG1, IG2, IG3) to identify which ones
|
|
2864
|
+
contain the specified control, enabling display of IG membership badges.
|
|
2865
|
+
|
|
2866
|
+
Args:
|
|
2867
|
+
control_id: Control identifier (e.g., "1.5", "2.1")
|
|
2868
|
+
implementation_groups: All IG data from the assessment
|
|
2869
|
+
|
|
2870
|
+
Returns:
|
|
2871
|
+
List of IG names that include this control, in order.
|
|
2872
|
+
Examples:
|
|
2873
|
+
- ["IG1", "IG2", "IG3"] for a control in all IGs
|
|
2874
|
+
- ["IG1", "IG2"] for a control in IG1 and IG2 only
|
|
2875
|
+
- ["IG3"] for a control unique to IG3
|
|
2876
|
+
- [] for a control not found in any IG
|
|
2877
|
+
|
|
2878
|
+
Examples:
|
|
2879
|
+
>>> _get_control_ig_membership("1.5", implementation_groups)
|
|
2880
|
+
["IG1", "IG2", "IG3"] # Control 1.5 is in all IGs
|
|
2881
|
+
|
|
2882
|
+
>>> _get_control_ig_membership("5.2", implementation_groups)
|
|
2883
|
+
["IG2", "IG3"] # Control 5.2 is only in IG2 and IG3
|
|
2884
|
+
|
|
2885
|
+
Notes:
|
|
2886
|
+
- Used to display IG membership badges in Detailed Findings section
|
|
2887
|
+
- Helps users understand which IGs require remediation for each control
|
|
2888
|
+
- Checks IGs in order: IG1, IG2, IG3
|
|
2889
|
+
"""
|
|
2890
|
+
member_igs = []
|
|
2891
|
+
for ig_name in ['IG1', 'IG2', 'IG3']:
|
|
2892
|
+
if ig_name in implementation_groups:
|
|
2893
|
+
if control_id in implementation_groups[ig_name].get('controls', {}):
|
|
2894
|
+
member_igs.append(ig_name)
|
|
2895
|
+
return member_igs
|
|
2896
|
+
|
|
2897
|
+
def _sort_control_id(self, control_id: str) -> tuple:
|
|
2898
|
+
"""Helper for alphanumeric sorting of control IDs.
|
|
2899
|
+
|
|
2900
|
+
Converts control IDs like "1.1", "1.10", "2.1" into tuples of integers
|
|
2901
|
+
for proper alphanumeric sorting. This ensures controls are displayed in
|
|
2902
|
+
the correct order (1.1, 1.2, ..., 1.9, 1.10, 2.1, ...) rather than
|
|
2903
|
+
lexicographic order (1.1, 1.10, 1.2, ...).
|
|
2904
|
+
|
|
2905
|
+
Args:
|
|
2906
|
+
control_id: Control identifier (e.g., "1.1", "1.10", "2.1")
|
|
2907
|
+
|
|
2908
|
+
Returns:
|
|
2909
|
+
Tuple of integers for sorting (e.g., (1, 1), (1, 10), (2, 1))
|
|
2910
|
+
Returns (0, 0) for non-standard control IDs as fallback
|
|
2911
|
+
|
|
2912
|
+
Examples:
|
|
2913
|
+
>>> _sort_control_id("1.1")
|
|
2914
|
+
(1, 1)
|
|
2915
|
+
|
|
2916
|
+
>>> _sort_control_id("1.10")
|
|
2917
|
+
(1, 10)
|
|
2918
|
+
|
|
2919
|
+
>>> _sort_control_id("2.1")
|
|
2920
|
+
(2, 1)
|
|
2921
|
+
|
|
2922
|
+
>>> _sort_control_id("invalid")
|
|
2923
|
+
(0, 0) # Fallback for non-standard IDs
|
|
2924
|
+
|
|
2925
|
+
Sorting Behavior:
|
|
2926
|
+
Without this helper:
|
|
2927
|
+
["1.1", "1.10", "1.2", "2.1"] # Incorrect lexicographic order
|
|
2928
|
+
|
|
2929
|
+
With this helper:
|
|
2930
|
+
["1.1", "1.2", "1.10", "2.1"] # Correct numeric order
|
|
2931
|
+
|
|
2932
|
+
Notes:
|
|
2933
|
+
- Used in _consolidate_findings_by_control() for sorting
|
|
2934
|
+
- Handles multi-level control IDs (e.g., "1.2.3" -> (1, 2, 3))
|
|
2935
|
+
- Gracefully handles malformed control IDs
|
|
2936
|
+
"""
|
|
2937
|
+
try:
|
|
2938
|
+
# Split by '.' and convert to integers
|
|
2939
|
+
parts = control_id.split('.')
|
|
2940
|
+
return tuple(int(part) for part in parts)
|
|
2941
|
+
except (ValueError, AttributeError):
|
|
2942
|
+
# Fallback for non-standard control IDs
|
|
2943
|
+
return (0, 0)
|