aws-cis-controls-assessment 1.0.9__py3-none-any.whl → 1.1.0__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 +94 -1
- aws_cis_assessment/config/rules/cis_controls_ig2.yaml +680 -1
- aws_cis_assessment/controls/ig1/__init__.py +17 -0
- aws_cis_assessment/controls/ig1/control_aws_backup_service.py +1276 -0
- aws_cis_assessment/controls/ig2/__init__.py +74 -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/controls/ig2/control_aws_backup_ig2.py +23 -0
- aws_cis_assessment/core/assessment_engine.py +74 -0
- aws_cis_assessment/reporters/html_reporter.py +197 -35
- {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/METADATA +163 -12
- {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/RECORD +26 -21
- docs/README.md +14 -3
- docs/adding-aws-backup-controls.md +562 -0
- docs/assessment-logic.md +291 -3
- docs/cli-reference.md +1 -1
- docs/config-rule-mappings.md +465 -7
- docs/developer-guide.md +312 -3
- docs/installation.md +2 -2
- docs/troubleshooting.md +211 -2
- docs/user-guide.md +47 -2
- {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""AWS Backup Service Controls for IG2 - Advanced backup infrastructure assessment.
|
|
2
|
+
|
|
3
|
+
This module implements IG2-level AWS Backup service controls that assess
|
|
4
|
+
advanced backup capabilities like vault lock, reporting, and restore testing.
|
|
5
|
+
|
|
6
|
+
Controls:
|
|
7
|
+
- backup-vault-lock-check: Verifies vault lock (ransomware protection)
|
|
8
|
+
- backup-report-plan-exists-check: Validates backup compliance reporting
|
|
9
|
+
- backup-restore-testing-plan-exists-check: Ensures backups are recoverable
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Import the IG2 controls from the IG1 module since they're all in the same file
|
|
13
|
+
from aws_cis_assessment.controls.ig1.control_aws_backup_service import (
|
|
14
|
+
BackupVaultLockCheckAssessment,
|
|
15
|
+
BackupReportPlanExistsCheckAssessment,
|
|
16
|
+
BackupRestoreTestingPlanExistsCheckAssessment
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
'BackupVaultLockCheckAssessment',
|
|
21
|
+
'BackupReportPlanExistsCheckAssessment',
|
|
22
|
+
'BackupRestoreTestingPlanExistsCheckAssessment'
|
|
23
|
+
]
|
|
@@ -95,6 +95,11 @@ from aws_cis_assessment.controls.ig1.control_backup_recovery import (
|
|
|
95
95
|
DBInstanceBackupEnabledAssessment, RedshiftBackupEnabledAssessment, DynamoDBPITREnabledAssessment,
|
|
96
96
|
ElastiCacheRedisClusterAutomaticBackupCheckAssessment, S3BucketReplicationEnabledAssessment
|
|
97
97
|
)
|
|
98
|
+
from aws_cis_assessment.controls.ig1.control_aws_backup_service import (
|
|
99
|
+
BackupPlanMinFrequencyAndMinRetentionCheckAssessment,
|
|
100
|
+
BackupVaultAccessPolicyCheckAssessment,
|
|
101
|
+
BackupSelectionResourceCoverageCheckAssessment
|
|
102
|
+
)
|
|
98
103
|
from aws_cis_assessment.controls.ig1.control_s3_enhancements import (
|
|
99
104
|
S3AccountLevelPublicAccessBlocksPeriodicAssessment, S3BucketPublicWriteProhibitedAssessment
|
|
100
105
|
)
|
|
@@ -151,6 +156,36 @@ from aws_cis_assessment.controls.ig2.control_remaining_rules import (
|
|
|
151
156
|
RedshiftEnhancedVPCRoutingEnabledAssessment, RestrictedCommonPortsAssessment,
|
|
152
157
|
AuditLogPolicyExistsAssessment
|
|
153
158
|
)
|
|
159
|
+
from aws_cis_assessment.controls.ig2.control_aws_backup_ig2 import (
|
|
160
|
+
BackupVaultLockCheckAssessment,
|
|
161
|
+
BackupReportPlanExistsCheckAssessment,
|
|
162
|
+
BackupRestoreTestingPlanExistsCheckAssessment
|
|
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
|
+
)
|
|
154
189
|
from aws_cis_assessment.controls.ig3.control_3_14 import (
|
|
155
190
|
APIGatewayExecutionLoggingEnabledAssessment, CloudTrailS3DataEventsEnabledAssessment,
|
|
156
191
|
MultiRegionCloudTrailEnabledAssessment, CloudTrailCloudWatchLogsEnabledAssessment
|
|
@@ -412,6 +447,11 @@ class AssessmentEngine:
|
|
|
412
447
|
'elasticache-redis-cluster-automatic-backup-check': ElastiCacheRedisClusterAutomaticBackupCheckAssessment(),
|
|
413
448
|
's3-bucket-replication-enabled': S3BucketReplicationEnabledAssessment(),
|
|
414
449
|
|
|
450
|
+
# AWS Backup Service Controls (IG1)
|
|
451
|
+
'backup-plan-min-frequency-and-min-retention-check': BackupPlanMinFrequencyAndMinRetentionCheckAssessment(),
|
|
452
|
+
'backup-vault-access-policy-check': BackupVaultAccessPolicyCheckAssessment(),
|
|
453
|
+
'backup-selection-resource-coverage-check': BackupSelectionResourceCoverageCheckAssessment(),
|
|
454
|
+
|
|
415
455
|
# S3 Security Enhancements
|
|
416
456
|
's3-account-level-public-access-blocks-periodic': S3AccountLevelPublicAccessBlocksPeriodicAssessment(),
|
|
417
457
|
's3-bucket-public-write-prohibited': S3BucketPublicWriteProhibitedAssessment(),
|
|
@@ -488,6 +528,40 @@ class AssessmentEngine:
|
|
|
488
528
|
'redshift-enhanced-vpc-routing-enabled': RedshiftEnhancedVPCRoutingEnabledAssessment(),
|
|
489
529
|
'restricted-common-ports': RestrictedCommonPortsAssessment(),
|
|
490
530
|
'audit-log-policy-exists (Process check)': AuditLogPolicyExistsAssessment(),
|
|
531
|
+
|
|
532
|
+
# AWS Backup Service Controls (IG2)
|
|
533
|
+
'backup-vault-lock-check': BackupVaultLockCheckAssessment(),
|
|
534
|
+
'backup-report-plan-exists-check': BackupReportPlanExistsCheckAssessment(),
|
|
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(),
|
|
491
565
|
},
|
|
492
566
|
'IG3': {
|
|
493
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,
|
|
@@ -1117,7 +1118,97 @@ class HTMLReporter(ReportGenerator):
|
|
|
1117
1118
|
text-decoration: underline;
|
|
1118
1119
|
}
|
|
1119
1120
|
|
|
1121
|
+
/* Remediation Section Styles */
|
|
1122
|
+
.remediation {
|
|
1123
|
+
margin-bottom: 40px;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
.remediation-list {
|
|
1127
|
+
display: flex;
|
|
1128
|
+
flex-direction: column;
|
|
1129
|
+
gap: 20px;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
.remediation-item {
|
|
1133
|
+
background: white;
|
|
1134
|
+
border: 1px solid #e0e0e0;
|
|
1135
|
+
border-radius: 8px;
|
|
1136
|
+
padding: 20px;
|
|
1137
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
1138
|
+
transition: box-shadow 0.2s;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
.remediation-item:hover {
|
|
1142
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
.remediation-header {
|
|
1146
|
+
display: flex;
|
|
1147
|
+
justify-content: space-between;
|
|
1148
|
+
align-items: flex-start;
|
|
1149
|
+
margin-bottom: 15px;
|
|
1150
|
+
padding-bottom: 15px;
|
|
1151
|
+
border-bottom: 2px solid #f0f0f0;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
.remediation-header h4 {
|
|
1155
|
+
margin: 0;
|
|
1156
|
+
color: #2c3e50;
|
|
1157
|
+
font-size: 1.1em;
|
|
1158
|
+
flex: 1;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
.remediation-badges {
|
|
1162
|
+
display: flex;
|
|
1163
|
+
gap: 10px;
|
|
1164
|
+
flex-shrink: 0;
|
|
1165
|
+
margin-left: 15px;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
.remediation-content {
|
|
1169
|
+
color: #555;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
.remediation-content h5 {
|
|
1173
|
+
margin: 15px 0 10px 0;
|
|
1174
|
+
color: #2c3e50;
|
|
1175
|
+
font-size: 1em;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.remediation-content ol {
|
|
1179
|
+
margin: 10px 0;
|
|
1180
|
+
padding-left: 25px;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
.remediation-content ol li {
|
|
1184
|
+
margin: 8px 0;
|
|
1185
|
+
line-height: 1.6;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
.remediation-content p {
|
|
1189
|
+
margin: 15px 0 5px 0;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
.remediation-content a {
|
|
1193
|
+
color: #3498db;
|
|
1194
|
+
text-decoration: none;
|
|
1195
|
+
font-weight: 500;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
.remediation-content a:hover {
|
|
1199
|
+
text-decoration: underline;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1120
1202
|
@media (max-width: 768px) {
|
|
1203
|
+
.remediation-header {
|
|
1204
|
+
flex-direction: column;
|
|
1205
|
+
gap: 10px;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
.remediation-badges {
|
|
1209
|
+
margin-left: 0;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1121
1212
|
.comparison-grid {
|
|
1122
1213
|
grid-template-columns: 1fr;
|
|
1123
1214
|
}
|
|
@@ -1331,13 +1422,51 @@ class HTMLReporter(ReportGenerator):
|
|
|
1331
1422
|
}});
|
|
1332
1423
|
}}
|
|
1333
1424
|
|
|
1334
|
-
// Search functionality
|
|
1425
|
+
// Search functionality for detailed findings
|
|
1335
1426
|
function searchFindings(searchTerm) {{
|
|
1336
|
-
const
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1427
|
+
const term = searchTerm.toLowerCase();
|
|
1428
|
+
const controlSections = document.querySelectorAll('.collapsible-content');
|
|
1429
|
+
|
|
1430
|
+
controlSections.forEach(function(section) {{
|
|
1431
|
+
const rows = section.querySelectorAll('.findings-table tbody tr');
|
|
1432
|
+
let visibleCount = 0;
|
|
1433
|
+
|
|
1434
|
+
rows.forEach(function(row) {{
|
|
1435
|
+
const cells = row.querySelectorAll('td');
|
|
1436
|
+
if (cells.length === 0) return;
|
|
1437
|
+
|
|
1438
|
+
// Search across: resource ID, resource type, region, evaluation reason, config rule name
|
|
1439
|
+
const resourceId = cells[0] ? cells[0].textContent.toLowerCase() : '';
|
|
1440
|
+
const resourceType = cells[1] ? cells[1].textContent.toLowerCase() : '';
|
|
1441
|
+
const region = cells[2] ? cells[2].textContent.toLowerCase() : '';
|
|
1442
|
+
const configRule = cells[4] ? cells[4].textContent.toLowerCase() : '';
|
|
1443
|
+
const evaluationReason = cells[5] ? cells[5].textContent.toLowerCase() : '';
|
|
1444
|
+
|
|
1445
|
+
const matches = resourceId.includes(term) ||
|
|
1446
|
+
resourceType.includes(term) ||
|
|
1447
|
+
region.includes(term) ||
|
|
1448
|
+
configRule.includes(term) ||
|
|
1449
|
+
evaluationReason.includes(term);
|
|
1450
|
+
|
|
1451
|
+
if (matches || term === '') {{
|
|
1452
|
+
row.style.display = '';
|
|
1453
|
+
visibleCount++;
|
|
1454
|
+
}} else {{
|
|
1455
|
+
row.style.display = 'none';
|
|
1456
|
+
}}
|
|
1457
|
+
}});
|
|
1458
|
+
|
|
1459
|
+
// Update the count in the collapsible button if it exists
|
|
1460
|
+
const collapsibleBtn = section.previousElementSibling;
|
|
1461
|
+
if (collapsibleBtn && collapsibleBtn.classList.contains('collapsible')) {{
|
|
1462
|
+
const originalText = collapsibleBtn.textContent.split('(')[0].trim();
|
|
1463
|
+
const totalCount = rows.length;
|
|
1464
|
+
if (term === '') {{
|
|
1465
|
+
collapsibleBtn.textContent = `${{originalText}} (${{totalCount}} findings)`;
|
|
1466
|
+
}} else {{
|
|
1467
|
+
collapsibleBtn.textContent = `${{originalText}} (${{visibleCount}} of ${{totalCount}} findings)`;
|
|
1468
|
+
}}
|
|
1469
|
+
}}
|
|
1341
1470
|
}});
|
|
1342
1471
|
}}
|
|
1343
1472
|
|
|
@@ -1851,13 +1980,19 @@ class HTMLReporter(ReportGenerator):
|
|
|
1851
1980
|
for step in remediation["remediation_steps"]:
|
|
1852
1981
|
steps_html += f"<li>{step}</li>"
|
|
1853
1982
|
|
|
1983
|
+
# Normalize priority and effort text to proper capitalization
|
|
1984
|
+
priority_text = remediation['priority'].capitalize()
|
|
1985
|
+
effort_text = remediation['estimated_effort'].capitalize()
|
|
1986
|
+
|
|
1854
1987
|
remediation_items += f"""
|
|
1855
1988
|
<div class="remediation-item">
|
|
1856
1989
|
<div class="remediation-header">
|
|
1857
|
-
<
|
|
1990
|
+
<div>
|
|
1991
|
+
<h4>{remediation['control_id']} - {remediation['config_rule_name']}</h4>
|
|
1992
|
+
</div>
|
|
1858
1993
|
<div class="remediation-badges">
|
|
1859
|
-
<span class="badge {remediation['priority_badge']}">{
|
|
1860
|
-
<span class="badge {remediation['effort_badge']}">{
|
|
1994
|
+
<span class="badge {remediation['priority_badge']}">{priority_text}</span>
|
|
1995
|
+
<span class="badge {remediation['effort_badge']}">{effort_text}</span>
|
|
1861
1996
|
</div>
|
|
1862
1997
|
</div>
|
|
1863
1998
|
<div class="remediation-content">
|
|
@@ -2168,17 +2303,7 @@ class HTMLReporter(ReportGenerator):
|
|
|
2168
2303
|
</div>
|
|
2169
2304
|
</div>
|
|
2170
2305
|
|
|
2171
|
-
|
|
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>
|
|
2306
|
+
|
|
2182
2307
|
</div>
|
|
2183
2308
|
"""
|
|
2184
2309
|
|
|
@@ -2615,47 +2740,84 @@ class HTMLReporter(ReportGenerator):
|
|
|
2615
2740
|
options += f'<option value="{resource_type}">{resource_type}</option>'
|
|
2616
2741
|
return options
|
|
2617
2742
|
|
|
2743
|
+
def _load_control_titles(self) -> Dict[str, str]:
|
|
2744
|
+
"""Load control titles from YAML configuration files.
|
|
2745
|
+
|
|
2746
|
+
Returns:
|
|
2747
|
+
Dictionary mapping control IDs to their titles
|
|
2748
|
+
"""
|
|
2749
|
+
if self._control_titles_cache:
|
|
2750
|
+
return self._control_titles_cache
|
|
2751
|
+
|
|
2752
|
+
from aws_cis_assessment.config.config_loader import ConfigRuleLoader
|
|
2753
|
+
|
|
2754
|
+
try:
|
|
2755
|
+
loader = ConfigRuleLoader()
|
|
2756
|
+
all_controls = loader.get_all_controls()
|
|
2757
|
+
|
|
2758
|
+
# Build a map of control_id -> title
|
|
2759
|
+
# Since controls can appear in multiple IGs, we'll use the first title we find
|
|
2760
|
+
for unique_key, control in all_controls.items():
|
|
2761
|
+
control_id = control.control_id
|
|
2762
|
+
if control_id not in self._control_titles_cache and control.title:
|
|
2763
|
+
self._control_titles_cache[control_id] = control.title
|
|
2764
|
+
|
|
2765
|
+
logger.info(f"Loaded {len(self._control_titles_cache)} control titles from YAML")
|
|
2766
|
+
except Exception as e:
|
|
2767
|
+
logger.warning(f"Failed to load control titles from YAML: {e}")
|
|
2768
|
+
self._control_titles_cache = {}
|
|
2769
|
+
|
|
2770
|
+
return self._control_titles_cache
|
|
2771
|
+
|
|
2618
2772
|
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
|
|
2773
|
+
"""Format control display name combining ID and title.
|
|
2620
2774
|
|
|
2621
2775
|
Creates a human-readable display name that shows both the control identifier
|
|
2622
|
-
and the
|
|
2623
|
-
each control
|
|
2776
|
+
and the control title from the YAML configuration, making it easier for users
|
|
2777
|
+
to understand what each control is about without looking up documentation.
|
|
2624
2778
|
|
|
2625
2779
|
Args:
|
|
2626
2780
|
control_id: Control identifier (e.g., "1.5", "2.1")
|
|
2627
2781
|
config_rule_name: AWS Config rule name (e.g., "root-account-hardware-mfa-enabled")
|
|
2628
|
-
title: Optional human-readable title for the control
|
|
2782
|
+
title: Optional human-readable title for the control (if not provided, loads from YAML)
|
|
2629
2783
|
|
|
2630
2784
|
Returns:
|
|
2631
2785
|
Formatted string for display in the following formats:
|
|
2632
|
-
- With title: "{control_id}: {title}
|
|
2786
|
+
- With title: "{control_id}: {title}"
|
|
2633
2787
|
- Without title: "{control_id}: {config_rule_name}"
|
|
2634
2788
|
- Fallback (no rule name): "{control_id}"
|
|
2635
2789
|
|
|
2636
2790
|
Examples:
|
|
2637
|
-
>>> _format_control_display_name("1.
|
|
2638
|
-
"1.
|
|
2791
|
+
>>> _format_control_display_name("1.1", "eip-attached")
|
|
2792
|
+
"1.1: Establish and Maintain Detailed Enterprise Asset Inventory"
|
|
2639
2793
|
|
|
2640
|
-
>>> _format_control_display_name("
|
|
2641
|
-
"
|
|
2794
|
+
>>> _format_control_display_name("3.3", "s3-bucket-ssl-requests-only")
|
|
2795
|
+
"3.3: Configure Data Access Control Lists"
|
|
2642
2796
|
|
|
2643
2797
|
>>> _format_control_display_name("3.1", "")
|
|
2644
2798
|
"3.1"
|
|
2645
2799
|
|
|
2646
2800
|
Notes:
|
|
2647
|
-
-
|
|
2801
|
+
- Loads control titles from YAML configuration files
|
|
2802
|
+
- Gracefully handles missing titles by falling back to config_rule_name
|
|
2648
2803
|
- Used in both Implementation Groups and Detailed Findings sections
|
|
2649
2804
|
- Display names longer than 50 characters are truncated with tooltips
|
|
2650
2805
|
"""
|
|
2651
|
-
if not
|
|
2652
|
-
|
|
2653
|
-
|
|
2806
|
+
# Load control titles from YAML if not already loaded
|
|
2807
|
+
if not title:
|
|
2808
|
+
control_titles = self._load_control_titles()
|
|
2809
|
+
title = control_titles.get(control_id)
|
|
2654
2810
|
|
|
2811
|
+
# If we have a title, use it
|
|
2655
2812
|
if title:
|
|
2656
|
-
return f"{control_id}: {title}
|
|
2657
|
-
|
|
2813
|
+
return f"{control_id}: {title}"
|
|
2814
|
+
|
|
2815
|
+
# Fallback to config_rule_name if no title
|
|
2816
|
+
if config_rule_name:
|
|
2658
2817
|
return f"{control_id}: {config_rule_name}"
|
|
2818
|
+
|
|
2819
|
+
# Last resort: just the control_id
|
|
2820
|
+
return control_id
|
|
2659
2821
|
|
|
2660
2822
|
def _get_ig_badge_class(self, ig_name: str) -> str:
|
|
2661
2823
|
"""Get CSS class for IG badge styling.
|