aws-cis-controls-assessment 1.0.10__py3-none-any.whl → 1.1.1__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 +2 -2
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +1 -1
- aws_cis_assessment/config/rules/cis_controls_ig2.yaml +599 -2
- aws_cis_assessment/controls/ig2/__init__.py +62 -1
- aws_cis_assessment/controls/ig2/control_4_5_6_access_configuration.py +2638 -0
- aws_cis_assessment/controls/ig2/control_8_audit_logging.py +984 -0
- aws_cis_assessment/core/assessment_engine.py +54 -0
- aws_cis_assessment/reporters/html_reporter.py +281 -129
- {aws_cis_controls_assessment-1.0.10.dist-info → aws_cis_controls_assessment-1.1.1.dist-info}/METADATA +160 -52
- {aws_cis_controls_assessment-1.0.10.dist-info → aws_cis_controls_assessment-1.1.1.dist-info}/RECORD +16 -14
- docs/cli-reference.md +1 -1
- docs/config-rule-mappings.md +423 -6
- {aws_cis_controls_assessment-1.0.10.dist-info → aws_cis_controls_assessment-1.1.1.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.0.10.dist-info → aws_cis_controls_assessment-1.1.1.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.0.10.dist-info → aws_cis_controls_assessment-1.1.1.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.0.10.dist-info → aws_cis_controls_assessment-1.1.1.dist-info}/top_level.txt +0 -0
|
@@ -161,6 +161,31 @@ from aws_cis_assessment.controls.ig2.control_aws_backup_ig2 import (
|
|
|
161
161
|
BackupReportPlanExistsCheckAssessment,
|
|
162
162
|
BackupRestoreTestingPlanExistsCheckAssessment
|
|
163
163
|
)
|
|
164
|
+
from aws_cis_assessment.controls.ig2.control_8_audit_logging import (
|
|
165
|
+
Route53QueryLoggingAssessment,
|
|
166
|
+
ALBAccessLogsEnabledAssessment,
|
|
167
|
+
CloudFrontAccessLogsEnabledAssessment,
|
|
168
|
+
CloudWatchLogRetentionCheckAssessment,
|
|
169
|
+
CloudTrailInsightsEnabledAssessment,
|
|
170
|
+
ConfigRecordingAllResourcesAssessment,
|
|
171
|
+
WAFLoggingEnabledAssessment
|
|
172
|
+
)
|
|
173
|
+
from aws_cis_assessment.controls.ig2.control_4_5_6_access_configuration import (
|
|
174
|
+
IAMMaxSessionDurationCheckAssessment,
|
|
175
|
+
SecurityGroupDefaultRulesCheckAssessment,
|
|
176
|
+
VPCDnsResolutionEnabledAssessment,
|
|
177
|
+
RDSDefaultAdminCheckAssessment,
|
|
178
|
+
EC2InstanceProfileLeastPrivilegeAssessment,
|
|
179
|
+
IAMServiceAccountInventoryCheckAssessment,
|
|
180
|
+
IAMAdminPolicyAttachedToRoleCheckAssessment,
|
|
181
|
+
SSOEnabledCheckAssessment,
|
|
182
|
+
IAMUserNoInlinePoliciesAssessment,
|
|
183
|
+
IAMAccessAnalyzerEnabledAssessment,
|
|
184
|
+
IAMPermissionBoundariesCheckAssessment,
|
|
185
|
+
OrganizationsSCPEnabledCheckAssessment,
|
|
186
|
+
CognitoUserPoolMFAEnabledAssessment,
|
|
187
|
+
VPNConnectionMFAEnabledAssessment
|
|
188
|
+
)
|
|
164
189
|
from aws_cis_assessment.controls.ig3.control_3_14 import (
|
|
165
190
|
APIGatewayExecutionLoggingEnabledAssessment, CloudTrailS3DataEventsEnabledAssessment,
|
|
166
191
|
MultiRegionCloudTrailEnabledAssessment, CloudTrailCloudWatchLogsEnabledAssessment
|
|
@@ -508,6 +533,35 @@ class AssessmentEngine:
|
|
|
508
533
|
'backup-vault-lock-check': BackupVaultLockCheckAssessment(),
|
|
509
534
|
'backup-report-plan-exists-check': BackupReportPlanExistsCheckAssessment(),
|
|
510
535
|
'backup-restore-testing-plan-exists-check': BackupRestoreTestingPlanExistsCheckAssessment(),
|
|
536
|
+
|
|
537
|
+
# Control 8.2 - Audit Log Management
|
|
538
|
+
'route53-query-logging-enabled': Route53QueryLoggingAssessment(),
|
|
539
|
+
'alb-access-logs-enabled': ALBAccessLogsEnabledAssessment(),
|
|
540
|
+
'cloudfront-access-logs-enabled': CloudFrontAccessLogsEnabledAssessment(),
|
|
541
|
+
'cloudwatch-log-retention-check': CloudWatchLogRetentionCheckAssessment(),
|
|
542
|
+
'cloudtrail-insights-enabled': CloudTrailInsightsEnabledAssessment(),
|
|
543
|
+
'config-recording-all-resources': ConfigRecordingAllResourcesAssessment(),
|
|
544
|
+
'waf-logging-enabled': WAFLoggingEnabledAssessment(),
|
|
545
|
+
|
|
546
|
+
# Control 4 - Secure Configuration
|
|
547
|
+
'iam-max-session-duration-check': IAMMaxSessionDurationCheckAssessment(),
|
|
548
|
+
'security-group-default-rules-check': SecurityGroupDefaultRulesCheckAssessment(),
|
|
549
|
+
'vpc-dns-resolution-enabled': VPCDnsResolutionEnabledAssessment(),
|
|
550
|
+
'rds-default-admin-check': RDSDefaultAdminCheckAssessment(),
|
|
551
|
+
'ec2-instance-profile-least-privilege': EC2InstanceProfileLeastPrivilegeAssessment(),
|
|
552
|
+
|
|
553
|
+
# Control 5 - Account Management
|
|
554
|
+
'iam-service-account-inventory-check': IAMServiceAccountInventoryCheckAssessment(),
|
|
555
|
+
'iam-admin-policy-attached-to-role-check': IAMAdminPolicyAttachedToRoleCheckAssessment(),
|
|
556
|
+
'sso-enabled-check': SSOEnabledCheckAssessment(),
|
|
557
|
+
'iam-user-no-inline-policies': IAMUserNoInlinePoliciesAssessment(),
|
|
558
|
+
|
|
559
|
+
# Control 6 - Access Control Management
|
|
560
|
+
'iam-access-analyzer-enabled': IAMAccessAnalyzerEnabledAssessment(),
|
|
561
|
+
'iam-permission-boundaries-check': IAMPermissionBoundariesCheckAssessment(),
|
|
562
|
+
'organizations-scp-enabled-check': OrganizationsSCPEnabledCheckAssessment(),
|
|
563
|
+
'cognito-user-pool-mfa-enabled': CognitoUserPoolMFAEnabledAssessment(),
|
|
564
|
+
'vpn-connection-mfa-enabled': VPNConnectionMFAEnabledAssessment(),
|
|
511
565
|
},
|
|
512
566
|
'IG3': {
|
|
513
567
|
# Control 3.14 - Sensitive Data Logging
|
|
@@ -70,6 +70,7 @@ class HTMLReporter(ReportGenerator):
|
|
|
70
70
|
"""
|
|
71
71
|
super().__init__(template_dir)
|
|
72
72
|
self.include_charts = include_charts
|
|
73
|
+
self._control_titles_cache = {} # Cache for control titles from YAML
|
|
73
74
|
logger.info(f"Initialized HTMLReporter with charts={include_charts}")
|
|
74
75
|
|
|
75
76
|
def generate_report(self, assessment_result: AssessmentResult,
|
|
@@ -770,6 +771,30 @@ class HTMLReporter(ReportGenerator):
|
|
|
770
771
|
background-color: #2c3e50;
|
|
771
772
|
}
|
|
772
773
|
|
|
774
|
+
/* Resource ID column width constraint */
|
|
775
|
+
.resource-table td:first-child {
|
|
776
|
+
max-width: 200px;
|
|
777
|
+
overflow: hidden;
|
|
778
|
+
text-overflow: ellipsis;
|
|
779
|
+
white-space: nowrap;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
.resource-table td:first-child:hover {
|
|
783
|
+
overflow: visible;
|
|
784
|
+
white-space: normal;
|
|
785
|
+
word-wrap: break-word;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/* Visual frames around each resource row */
|
|
789
|
+
.resource-row {
|
|
790
|
+
border: 1px solid #e0e0e0;
|
|
791
|
+
border-radius: 4px;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
.resource-row:hover {
|
|
795
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
796
|
+
}
|
|
797
|
+
|
|
773
798
|
.resource-row.compliant {
|
|
774
799
|
background-color: #f8fff8;
|
|
775
800
|
}
|
|
@@ -1117,7 +1142,97 @@ class HTMLReporter(ReportGenerator):
|
|
|
1117
1142
|
text-decoration: underline;
|
|
1118
1143
|
}
|
|
1119
1144
|
|
|
1145
|
+
/* Remediation Section Styles */
|
|
1146
|
+
.remediation {
|
|
1147
|
+
margin-bottom: 40px;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
.remediation-list {
|
|
1151
|
+
display: flex;
|
|
1152
|
+
flex-direction: column;
|
|
1153
|
+
gap: 20px;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
.remediation-item {
|
|
1157
|
+
background: white;
|
|
1158
|
+
border: 1px solid #e0e0e0;
|
|
1159
|
+
border-radius: 8px;
|
|
1160
|
+
padding: 20px;
|
|
1161
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
1162
|
+
transition: box-shadow 0.2s;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
.remediation-item:hover {
|
|
1166
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
.remediation-header {
|
|
1170
|
+
display: flex;
|
|
1171
|
+
justify-content: space-between;
|
|
1172
|
+
align-items: flex-start;
|
|
1173
|
+
margin-bottom: 15px;
|
|
1174
|
+
padding-bottom: 15px;
|
|
1175
|
+
border-bottom: 2px solid #f0f0f0;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.remediation-header h4 {
|
|
1179
|
+
margin: 0;
|
|
1180
|
+
color: #2c3e50;
|
|
1181
|
+
font-size: 1.1em;
|
|
1182
|
+
flex: 1;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
.remediation-badges {
|
|
1186
|
+
display: flex;
|
|
1187
|
+
gap: 10px;
|
|
1188
|
+
flex-shrink: 0;
|
|
1189
|
+
margin-left: 15px;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.remediation-content {
|
|
1193
|
+
color: #555;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
.remediation-content h5 {
|
|
1197
|
+
margin: 15px 0 10px 0;
|
|
1198
|
+
color: #2c3e50;
|
|
1199
|
+
font-size: 1em;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
.remediation-content ol {
|
|
1203
|
+
margin: 10px 0;
|
|
1204
|
+
padding-left: 25px;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
.remediation-content ol li {
|
|
1208
|
+
margin: 8px 0;
|
|
1209
|
+
line-height: 1.6;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
.remediation-content p {
|
|
1213
|
+
margin: 15px 0 5px 0;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
.remediation-content a {
|
|
1217
|
+
color: #3498db;
|
|
1218
|
+
text-decoration: none;
|
|
1219
|
+
font-weight: 500;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
.remediation-content a:hover {
|
|
1223
|
+
text-decoration: underline;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1120
1226
|
@media (max-width: 768px) {
|
|
1227
|
+
.remediation-header {
|
|
1228
|
+
flex-direction: column;
|
|
1229
|
+
gap: 10px;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
.remediation-badges {
|
|
1233
|
+
margin-left: 0;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1121
1236
|
.comparison-grid {
|
|
1122
1237
|
grid-template-columns: 1fr;
|
|
1123
1238
|
}
|
|
@@ -1171,56 +1286,6 @@ class HTMLReporter(ReportGenerator):
|
|
|
1171
1286
|
return;
|
|
1172
1287
|
}}
|
|
1173
1288
|
|
|
1174
|
-
// Implementation Groups Compliance Chart
|
|
1175
|
-
const igChartCtx = document.getElementById('igComplianceChart');
|
|
1176
|
-
if (igChartCtx) {{
|
|
1177
|
-
new Chart(igChartCtx, {{
|
|
1178
|
-
type: 'doughnut',
|
|
1179
|
-
data: chartData.igCompliance,
|
|
1180
|
-
options: {{
|
|
1181
|
-
responsive: true,
|
|
1182
|
-
maintainAspectRatio: false,
|
|
1183
|
-
plugins: {{
|
|
1184
|
-
legend: {{
|
|
1185
|
-
position: 'bottom'
|
|
1186
|
-
}},
|
|
1187
|
-
title: {{
|
|
1188
|
-
display: true,
|
|
1189
|
-
text: 'Implementation Groups Compliance'
|
|
1190
|
-
}}
|
|
1191
|
-
}}
|
|
1192
|
-
}}
|
|
1193
|
-
}});
|
|
1194
|
-
}}
|
|
1195
|
-
|
|
1196
|
-
// Overall Compliance Trend Chart
|
|
1197
|
-
const trendChartCtx = document.getElementById('complianceTrendChart');
|
|
1198
|
-
if (trendChartCtx) {{
|
|
1199
|
-
new Chart(trendChartCtx, {{
|
|
1200
|
-
type: 'bar',
|
|
1201
|
-
data: chartData.complianceTrend,
|
|
1202
|
-
options: {{
|
|
1203
|
-
responsive: true,
|
|
1204
|
-
maintainAspectRatio: false,
|
|
1205
|
-
scales: {{
|
|
1206
|
-
y: {{
|
|
1207
|
-
beginAtZero: true,
|
|
1208
|
-
max: 100
|
|
1209
|
-
}}
|
|
1210
|
-
}},
|
|
1211
|
-
plugins: {{
|
|
1212
|
-
legend: {{
|
|
1213
|
-
display: false
|
|
1214
|
-
}},
|
|
1215
|
-
title: {{
|
|
1216
|
-
display: true,
|
|
1217
|
-
text: 'Compliance by Implementation Group'
|
|
1218
|
-
}}
|
|
1219
|
-
}}
|
|
1220
|
-
}}
|
|
1221
|
-
}});
|
|
1222
|
-
}}
|
|
1223
|
-
|
|
1224
1289
|
// Risk Distribution Chart
|
|
1225
1290
|
const riskChartCtx = document.getElementById('riskDistributionChart');
|
|
1226
1291
|
if (riskChartCtx) {{
|
|
@@ -1331,13 +1396,51 @@ class HTMLReporter(ReportGenerator):
|
|
|
1331
1396
|
}});
|
|
1332
1397
|
}}
|
|
1333
1398
|
|
|
1334
|
-
// Search functionality
|
|
1399
|
+
// Search functionality for detailed findings
|
|
1335
1400
|
function searchFindings(searchTerm) {{
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1401
|
+
const term = searchTerm.toLowerCase();
|
|
1402
|
+
const controlSections = document.querySelectorAll('.collapsible-content');
|
|
1403
|
+
|
|
1404
|
+
controlSections.forEach(function(section) {{
|
|
1405
|
+
const rows = section.querySelectorAll('.findings-table tbody tr');
|
|
1406
|
+
let visibleCount = 0;
|
|
1407
|
+
|
|
1408
|
+
rows.forEach(function(row) {{
|
|
1409
|
+
const cells = row.querySelectorAll('td');
|
|
1410
|
+
if (cells.length === 0) return;
|
|
1411
|
+
|
|
1412
|
+
// Search across: resource ID, resource type, region, evaluation reason, config rule name
|
|
1413
|
+
const resourceId = cells[0] ? cells[0].textContent.toLowerCase() : '';
|
|
1414
|
+
const resourceType = cells[1] ? cells[1].textContent.toLowerCase() : '';
|
|
1415
|
+
const region = cells[2] ? cells[2].textContent.toLowerCase() : '';
|
|
1416
|
+
const configRule = cells[4] ? cells[4].textContent.toLowerCase() : '';
|
|
1417
|
+
const evaluationReason = cells[5] ? cells[5].textContent.toLowerCase() : '';
|
|
1418
|
+
|
|
1419
|
+
const matches = resourceId.includes(term) ||
|
|
1420
|
+
resourceType.includes(term) ||
|
|
1421
|
+
region.includes(term) ||
|
|
1422
|
+
configRule.includes(term) ||
|
|
1423
|
+
evaluationReason.includes(term);
|
|
1424
|
+
|
|
1425
|
+
if (matches || term === '') {{
|
|
1426
|
+
row.style.display = '';
|
|
1427
|
+
visibleCount++;
|
|
1428
|
+
}} else {{
|
|
1429
|
+
row.style.display = 'none';
|
|
1430
|
+
}}
|
|
1431
|
+
}});
|
|
1432
|
+
|
|
1433
|
+
// Update the count in the collapsible button if it exists
|
|
1434
|
+
const collapsibleBtn = section.previousElementSibling;
|
|
1435
|
+
if (collapsibleBtn && collapsibleBtn.classList.contains('collapsible')) {{
|
|
1436
|
+
const originalText = collapsibleBtn.textContent.split('(')[0].trim();
|
|
1437
|
+
const totalCount = rows.length;
|
|
1438
|
+
if (term === '') {{
|
|
1439
|
+
collapsibleBtn.textContent = `${{originalText}} (${{totalCount}} findings)`;
|
|
1440
|
+
}} else {{
|
|
1441
|
+
collapsibleBtn.textContent = `${{originalText}} (${{visibleCount}} of ${{totalCount}} findings)`;
|
|
1442
|
+
}}
|
|
1443
|
+
}}
|
|
1341
1444
|
}});
|
|
1342
1445
|
}}
|
|
1343
1446
|
|
|
@@ -1345,17 +1448,34 @@ class HTMLReporter(ReportGenerator):
|
|
|
1345
1448
|
function exportToCSV() {{
|
|
1346
1449
|
const tables = document.querySelectorAll('.findings-table');
|
|
1347
1450
|
let csvContent = '';
|
|
1451
|
+
let headersAdded = false;
|
|
1348
1452
|
|
|
1349
1453
|
tables.forEach(function(table) {{
|
|
1350
1454
|
const rows = table.querySelectorAll('tr');
|
|
1351
|
-
rows.forEach(function(row) {{
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1455
|
+
rows.forEach(function(row, index) {{
|
|
1456
|
+
// Add headers only once (from first table)
|
|
1457
|
+
if (index === 0) {{
|
|
1458
|
+
if (!headersAdded) {{
|
|
1459
|
+
const cells = row.querySelectorAll('th');
|
|
1460
|
+
if (cells.length > 0) {{
|
|
1461
|
+
const rowData = Array.from(cells).map(cell =>
|
|
1462
|
+
'"' + cell.textContent.replace(/"/g, '""') + '"'
|
|
1463
|
+
).join(',');
|
|
1464
|
+
csvContent += rowData + '\\n';
|
|
1465
|
+
headersAdded = true;
|
|
1466
|
+
}}
|
|
1467
|
+
}}
|
|
1468
|
+
}} else {{
|
|
1469
|
+
// Add data rows (skip header rows from subsequent tables)
|
|
1470
|
+
const cells = row.querySelectorAll('td');
|
|
1471
|
+
if (cells.length > 0) {{
|
|
1472
|
+
const rowData = Array.from(cells).map(cell =>
|
|
1473
|
+
'"' + cell.textContent.replace(/"/g, '""') + '"'
|
|
1474
|
+
).join(',');
|
|
1475
|
+
csvContent += rowData + '\\n';
|
|
1476
|
+
}}
|
|
1477
|
+
}}
|
|
1357
1478
|
}});
|
|
1358
|
-
csvContent += '\\n';
|
|
1359
1479
|
}});
|
|
1360
1480
|
|
|
1361
1481
|
const blob = new Blob([csvContent], {{ type: 'text/csv' }});
|
|
@@ -1528,6 +1648,9 @@ class HTMLReporter(ReportGenerator):
|
|
|
1528
1648
|
def _generate_executive_dashboard(self, html_data: Dict[str, Any]) -> str:
|
|
1529
1649
|
"""Generate executive dashboard section.
|
|
1530
1650
|
|
|
1651
|
+
Modified in v1.1.1 to remove pie chart (igComplianceChart) and bar chart
|
|
1652
|
+
(complianceTrendChart), keeping only risk distribution chart.
|
|
1653
|
+
|
|
1531
1654
|
Args:
|
|
1532
1655
|
html_data: Enhanced HTML report data
|
|
1533
1656
|
|
|
@@ -1602,12 +1725,6 @@ class HTMLReporter(ReportGenerator):
|
|
|
1602
1725
|
if self.include_charts:
|
|
1603
1726
|
charts_section = f"""
|
|
1604
1727
|
<div class="charts-section">
|
|
1605
|
-
<div class="chart-container">
|
|
1606
|
-
<canvas id="igComplianceChart"></canvas>
|
|
1607
|
-
</div>
|
|
1608
|
-
<div class="chart-container">
|
|
1609
|
-
<canvas id="complianceTrendChart"></canvas>
|
|
1610
|
-
</div>
|
|
1611
1728
|
<div class="chart-container">
|
|
1612
1729
|
<canvas id="riskDistributionChart"></canvas>
|
|
1613
1730
|
</div>
|
|
@@ -1851,13 +1968,19 @@ class HTMLReporter(ReportGenerator):
|
|
|
1851
1968
|
for step in remediation["remediation_steps"]:
|
|
1852
1969
|
steps_html += f"<li>{step}</li>"
|
|
1853
1970
|
|
|
1971
|
+
# Normalize priority and effort text to proper capitalization
|
|
1972
|
+
priority_text = remediation['priority'].capitalize()
|
|
1973
|
+
effort_text = remediation['estimated_effort'].capitalize()
|
|
1974
|
+
|
|
1854
1975
|
remediation_items += f"""
|
|
1855
1976
|
<div class="remediation-item">
|
|
1856
1977
|
<div class="remediation-header">
|
|
1857
|
-
<
|
|
1978
|
+
<div>
|
|
1979
|
+
<h4>{remediation['control_id']} - {remediation['config_rule_name']}</h4>
|
|
1980
|
+
</div>
|
|
1858
1981
|
<div class="remediation-badges">
|
|
1859
|
-
<span class="badge {remediation['priority_badge']}">{
|
|
1860
|
-
<span class="badge {remediation['effort_badge']}">{
|
|
1982
|
+
<span class="badge {remediation['priority_badge']}">{priority_text}</span>
|
|
1983
|
+
<span class="badge {remediation['effort_badge']}">{effort_text}</span>
|
|
1861
1984
|
</div>
|
|
1862
1985
|
</div>
|
|
1863
1986
|
<div class="remediation-content">
|
|
@@ -2061,8 +2184,33 @@ class HTMLReporter(ReportGenerator):
|
|
|
2061
2184
|
return "low"
|
|
2062
2185
|
|
|
2063
2186
|
def _get_priority_badge(self, priority: str) -> str:
|
|
2064
|
-
"""Get priority badge class.
|
|
2065
|
-
|
|
2187
|
+
"""Get priority badge class ensuring single value.
|
|
2188
|
+
|
|
2189
|
+
Modified in v1.1.1 to normalize priority values and handle duplicates.
|
|
2190
|
+
Fixes issues like "High High" → "high" and "High Medium" → "high".
|
|
2191
|
+
|
|
2192
|
+
Args:
|
|
2193
|
+
priority: Priority string (may contain multiple values like "High High" or "High Medium")
|
|
2194
|
+
|
|
2195
|
+
Returns:
|
|
2196
|
+
Single priority class: 'high', 'medium', or 'low'
|
|
2197
|
+
"""
|
|
2198
|
+
# Extract first priority if multiple exist
|
|
2199
|
+
priority_lower = priority.lower().strip()
|
|
2200
|
+
|
|
2201
|
+
# Handle multiple priorities (take first one)
|
|
2202
|
+
if ' ' in priority_lower:
|
|
2203
|
+
priority_lower = priority_lower.split()[0]
|
|
2204
|
+
|
|
2205
|
+
# Normalize to standard values
|
|
2206
|
+
if 'high' in priority_lower:
|
|
2207
|
+
return 'high'
|
|
2208
|
+
elif 'medium' in priority_lower or 'med' in priority_lower:
|
|
2209
|
+
return 'medium'
|
|
2210
|
+
elif 'low' in priority_lower:
|
|
2211
|
+
return 'low'
|
|
2212
|
+
else:
|
|
2213
|
+
return 'medium' # Default fallback
|
|
2066
2214
|
|
|
2067
2215
|
def _get_effort_badge(self, effort: str) -> str:
|
|
2068
2216
|
"""Get effort badge class."""
|
|
@@ -2103,40 +2251,27 @@ class HTMLReporter(ReportGenerator):
|
|
|
2103
2251
|
score_diff: float) -> str:
|
|
2104
2252
|
"""Generate scoring methodology comparison section.
|
|
2105
2253
|
|
|
2254
|
+
Modified in v1.1.1 to remove "our approach" phrase, "Reflects actual security
|
|
2255
|
+
posture" text, and score difference warning for cleaner presentation.
|
|
2256
|
+
|
|
2106
2257
|
Args:
|
|
2107
|
-
weighted_score:
|
|
2258
|
+
weighted_score: Weighted compliance score
|
|
2108
2259
|
aws_config_score: AWS Config Conformance Pack style score
|
|
2109
|
-
score_diff: Difference between the two scores
|
|
2260
|
+
score_diff: Difference between the two scores (not displayed)
|
|
2110
2261
|
|
|
2111
2262
|
Returns:
|
|
2112
2263
|
HTML section comparing the two scoring approaches
|
|
2113
2264
|
"""
|
|
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
2265
|
return f"""
|
|
2131
2266
|
<div class="score-comparison-section">
|
|
2132
2267
|
<h3>Scoring Methodology Comparison</h3>
|
|
2133
2268
|
<div class="comparison-grid">
|
|
2134
2269
|
<div class="comparison-card">
|
|
2135
|
-
<h4>Weighted Score
|
|
2270
|
+
<h4>Weighted Score</h4>
|
|
2136
2271
|
<div class="comparison-value">{weighted_score:.1f}%</div>
|
|
2137
2272
|
<p class="comparison-description">
|
|
2138
2273
|
Uses risk-based weighting where critical controls (encryption, access control)
|
|
2139
|
-
have higher impact on the overall score.
|
|
2274
|
+
have higher impact on the overall score.
|
|
2140
2275
|
</p>
|
|
2141
2276
|
<ul class="comparison-features">
|
|
2142
2277
|
<li>✓ Prioritizes critical security controls</li>
|
|
@@ -2159,26 +2294,6 @@ class HTMLReporter(ReportGenerator):
|
|
|
2159
2294
|
</ul>
|
|
2160
2295
|
</div>
|
|
2161
2296
|
</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
2297
|
</div>
|
|
2183
2298
|
"""
|
|
2184
2299
|
|
|
@@ -2615,47 +2730,84 @@ class HTMLReporter(ReportGenerator):
|
|
|
2615
2730
|
options += f'<option value="{resource_type}">{resource_type}</option>'
|
|
2616
2731
|
return options
|
|
2617
2732
|
|
|
2733
|
+
def _load_control_titles(self) -> Dict[str, str]:
|
|
2734
|
+
"""Load control titles from YAML configuration files.
|
|
2735
|
+
|
|
2736
|
+
Returns:
|
|
2737
|
+
Dictionary mapping control IDs to their titles
|
|
2738
|
+
"""
|
|
2739
|
+
if self._control_titles_cache:
|
|
2740
|
+
return self._control_titles_cache
|
|
2741
|
+
|
|
2742
|
+
from aws_cis_assessment.config.config_loader import ConfigRuleLoader
|
|
2743
|
+
|
|
2744
|
+
try:
|
|
2745
|
+
loader = ConfigRuleLoader()
|
|
2746
|
+
all_controls = loader.get_all_controls()
|
|
2747
|
+
|
|
2748
|
+
# Build a map of control_id -> title
|
|
2749
|
+
# Since controls can appear in multiple IGs, we'll use the first title we find
|
|
2750
|
+
for unique_key, control in all_controls.items():
|
|
2751
|
+
control_id = control.control_id
|
|
2752
|
+
if control_id not in self._control_titles_cache and control.title:
|
|
2753
|
+
self._control_titles_cache[control_id] = control.title
|
|
2754
|
+
|
|
2755
|
+
logger.info(f"Loaded {len(self._control_titles_cache)} control titles from YAML")
|
|
2756
|
+
except Exception as e:
|
|
2757
|
+
logger.warning(f"Failed to load control titles from YAML: {e}")
|
|
2758
|
+
self._control_titles_cache = {}
|
|
2759
|
+
|
|
2760
|
+
return self._control_titles_cache
|
|
2761
|
+
|
|
2618
2762
|
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
|
|
2763
|
+
"""Format control display name combining ID and title.
|
|
2620
2764
|
|
|
2621
2765
|
Creates a human-readable display name that shows both the control identifier
|
|
2622
|
-
and the
|
|
2623
|
-
each control
|
|
2766
|
+
and the control title from the YAML configuration, making it easier for users
|
|
2767
|
+
to understand what each control is about without looking up documentation.
|
|
2624
2768
|
|
|
2625
2769
|
Args:
|
|
2626
2770
|
control_id: Control identifier (e.g., "1.5", "2.1")
|
|
2627
2771
|
config_rule_name: AWS Config rule name (e.g., "root-account-hardware-mfa-enabled")
|
|
2628
|
-
title: Optional human-readable title for the control
|
|
2772
|
+
title: Optional human-readable title for the control (if not provided, loads from YAML)
|
|
2629
2773
|
|
|
2630
2774
|
Returns:
|
|
2631
2775
|
Formatted string for display in the following formats:
|
|
2632
|
-
- With title: "{control_id}: {title}
|
|
2776
|
+
- With title: "{control_id}: {title}"
|
|
2633
2777
|
- Without title: "{control_id}: {config_rule_name}"
|
|
2634
2778
|
- Fallback (no rule name): "{control_id}"
|
|
2635
2779
|
|
|
2636
2780
|
Examples:
|
|
2637
|
-
>>> _format_control_display_name("1.
|
|
2638
|
-
"1.
|
|
2781
|
+
>>> _format_control_display_name("1.1", "eip-attached")
|
|
2782
|
+
"1.1: Establish and Maintain Detailed Enterprise Asset Inventory"
|
|
2639
2783
|
|
|
2640
|
-
>>> _format_control_display_name("
|
|
2641
|
-
"
|
|
2784
|
+
>>> _format_control_display_name("3.3", "s3-bucket-ssl-requests-only")
|
|
2785
|
+
"3.3: Configure Data Access Control Lists"
|
|
2642
2786
|
|
|
2643
2787
|
>>> _format_control_display_name("3.1", "")
|
|
2644
2788
|
"3.1"
|
|
2645
2789
|
|
|
2646
2790
|
Notes:
|
|
2647
|
-
-
|
|
2791
|
+
- Loads control titles from YAML configuration files
|
|
2792
|
+
- Gracefully handles missing titles by falling back to config_rule_name
|
|
2648
2793
|
- Used in both Implementation Groups and Detailed Findings sections
|
|
2649
2794
|
- Display names longer than 50 characters are truncated with tooltips
|
|
2650
2795
|
"""
|
|
2651
|
-
if not
|
|
2652
|
-
|
|
2653
|
-
|
|
2796
|
+
# Load control titles from YAML if not already loaded
|
|
2797
|
+
if not title:
|
|
2798
|
+
control_titles = self._load_control_titles()
|
|
2799
|
+
title = control_titles.get(control_id)
|
|
2654
2800
|
|
|
2801
|
+
# If we have a title, use it
|
|
2655
2802
|
if title:
|
|
2656
|
-
return f"{control_id}: {title}
|
|
2657
|
-
|
|
2803
|
+
return f"{control_id}: {title}"
|
|
2804
|
+
|
|
2805
|
+
# Fallback to config_rule_name if no title
|
|
2806
|
+
if config_rule_name:
|
|
2658
2807
|
return f"{control_id}: {config_rule_name}"
|
|
2808
|
+
|
|
2809
|
+
# Last resort: just the control_id
|
|
2810
|
+
return control_id
|
|
2659
2811
|
|
|
2660
2812
|
def _get_ig_badge_class(self, ig_name: str) -> str:
|
|
2661
2813
|
"""Get CSS class for IG badge styling.
|