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.
@@ -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 tables = document.querySelectorAll('.findings-table tbody tr');
1337
- tables.forEach(function(row) {{
1338
- const text = row.textContent.toLowerCase();
1339
- const matches = text.includes(searchTerm.toLowerCase());
1340
- row.style.display = matches ? '' : 'none';
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
- const cells = row.querySelectorAll('th, td');
1353
- const rowData = Array.from(cells).map(cell =>
1354
- '"' + cell.textContent.replace(/"/g, '""') + '"'
1355
- ).join(',');
1356
- csvContent += rowData + '\\n';
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
- <h4>{remediation['control_id']} - {remediation['config_rule_name']}</h4>
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']}">{remediation['priority']}</span>
1860
- <span class="badge {remediation['effort_badge']}">{remediation['estimated_effort']}</span>
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
- return priority.lower()
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: Our weighted compliance 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 (Our Approach)</h4>
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. Reflects actual security posture.
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, rule name, and optional title.
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 AWS Config rule name, making it easier for users to understand what
2623
- each control checks without looking up documentation.
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} ({config_rule_name})"
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.5", "root-account-hardware-mfa-enabled")
2638
- "1.5: root-account-hardware-mfa-enabled"
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("2.1", "iam-password-policy", "IAM Password Policy")
2641
- "2.1: IAM Password Policy (iam-password-policy)"
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
- - Gracefully handles missing config_rule_name by falling back to control_id only
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 config_rule_name:
2652
- # Fallback to control_id only if config_rule_name is missing
2653
- return control_id
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} ({config_rule_name})"
2657
- else:
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.