aws-cis-controls-assessment 1.1.1__py3-none-any.whl → 1.1.3__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.
@@ -6,6 +6,6 @@ CIS Controls Implementation Groups (IG1, IG2, IG3). Implements 163 comprehensive
6
6
  across all implementation groups for complete security compliance assessment.
7
7
  """
8
8
 
9
- __version__ = "1.1.1"
9
+ __version__ = "1.1.3"
10
10
  __author__ = "AWS CIS Assessment Team"
11
11
  __description__ = "Production-ready AWS CIS Controls Compliance Assessment Framework"
@@ -39,8 +39,9 @@ class CloudTrailEnabledAssessment(BaseConfigRuleAssessment):
39
39
  try:
40
40
  cloudtrail_client = aws_factory.get_client('cloudtrail', region)
41
41
 
42
- # Get all trails in this region
43
- response = cloudtrail_client.describe_trails()
42
+ # Get all trails in this region, excluding shadow trails
43
+ # Shadow trails are replications from other regions or organization trails
44
+ response = cloudtrail_client.describe_trails(includeShadowTrails=False)
44
45
  trails = response.get('trailList', [])
45
46
 
46
47
  # Get trail status for each trail
@@ -48,6 +49,13 @@ class CloudTrailEnabledAssessment(BaseConfigRuleAssessment):
48
49
  for trail in trails:
49
50
  trail_name = trail.get('Name', '')
50
51
  trail_arn = trail.get('TrailARN', '')
52
+ home_region = trail.get('HomeRegion', '')
53
+
54
+ # Skip shadow trails (trails from other regions or organization trails)
55
+ # These are indicated by HomeRegion being different from current region
56
+ if home_region and home_region != region:
57
+ logger.debug(f"Skipping shadow trail {trail_name} (home region: {home_region}, current region: {region})")
58
+ continue
51
59
 
52
60
  try:
53
61
  # Get trail status
@@ -66,14 +74,22 @@ class CloudTrailEnabledAssessment(BaseConfigRuleAssessment):
66
74
  'TrailARN': trail_arn,
67
75
  'IsLogging': is_logging,
68
76
  'IsMultiRegionTrail': trail.get('IsMultiRegionTrail', False),
77
+ 'IsOrganizationTrail': trail.get('IsOrganizationTrail', False),
69
78
  'IncludeGlobalServiceEvents': trail.get('IncludeGlobalServiceEvents', False),
70
79
  'S3BucketName': trail.get('S3BucketName', ''),
80
+ 'HomeRegion': home_region,
71
81
  'EventSelectors': event_selectors,
72
82
  'Region': region
73
83
  })
74
84
 
75
85
  except ClientError as e:
76
- logger.warning(f"Error getting status for trail {trail_name}: {e}")
86
+ error_code = e.response.get('Error', {}).get('Code', '')
87
+
88
+ # Only log warning for unexpected errors, not for shadow trails
89
+ if error_code == 'TrailNotFoundException':
90
+ logger.debug(f"Trail {trail_name} not found in {region} - likely a shadow trail or deleted trail")
91
+ else:
92
+ logger.warning(f"Error getting status for trail {trail_name}: {e}")
77
93
  continue
78
94
 
79
95
  # Return account-level resource with trail information
@@ -100,13 +116,33 @@ class CloudTrailEnabledAssessment(BaseConfigRuleAssessment):
100
116
  if has_active_trails:
101
117
  # Check for at least one properly configured trail
102
118
  active_trails = [trail for trail in trails if trail['IsLogging']]
103
- trail_names = [trail['TrailName'] for trail in active_trails]
119
+
120
+ # Categorize trails
121
+ org_trails = [t for t in active_trails if t.get('IsOrganizationTrail', False)]
122
+ multi_region_trails = [t for t in active_trails if t.get('IsMultiRegionTrail', False)]
123
+ regional_trails = [t for t in active_trails if not t.get('IsMultiRegionTrail', False)]
124
+
125
+ # Build detailed reason
126
+ trail_details = []
127
+ for trail in active_trails:
128
+ trail_type = []
129
+ if trail.get('IsOrganizationTrail', False):
130
+ trail_type.append("organization")
131
+ if trail.get('IsMultiRegionTrail', False):
132
+ trail_type.append("multi-region")
133
+ else:
134
+ trail_type.append("regional")
135
+
136
+ trail_info = f"{trail['TrailName']} ({', '.join(trail_type)})"
137
+ trail_details.append(trail_info)
138
+
139
+ reason = f"CloudTrail is enabled with {len(active_trails)} active trail(s): {', '.join(trail_details)}"
104
140
 
105
141
  return ComplianceResult(
106
142
  resource_id=account_id,
107
143
  resource_type="AWS::::Account",
108
144
  compliance_status=ComplianceStatus.COMPLIANT,
109
- evaluation_reason=f"CloudTrail is enabled with {len(active_trails)} active trail(s): {', '.join(trail_names)}",
145
+ evaluation_reason=reason,
110
146
  config_rule_name=self.rule_name,
111
147
  region=region
112
148
  )
@@ -115,7 +151,7 @@ class CloudTrailEnabledAssessment(BaseConfigRuleAssessment):
115
151
  resource_id=account_id,
116
152
  resource_type="AWS::::Account",
117
153
  compliance_status=ComplianceStatus.NON_COMPLIANT,
118
- evaluation_reason="CloudTrail is not enabled or no trails are actively logging",
154
+ evaluation_reason="CloudTrail is not enabled or no trails are actively logging in this region",
119
155
  config_rule_name=self.rule_name,
120
156
  region=region
121
157
  )
@@ -274,6 +274,8 @@ class HTMLReporter(ReportGenerator):
274
274
  def _generate_html_body(self, html_data: Dict[str, Any]) -> str:
275
275
  """Generate HTML body section with content.
276
276
 
277
+ Modified in v1.1.2 to remove Detailed Findings and Remediation sections.
278
+
277
279
  Args:
278
280
  html_data: Enhanced HTML report data
279
281
 
@@ -285,9 +287,7 @@ class HTMLReporter(ReportGenerator):
285
287
  navigation = self._generate_navigation(html_data)
286
288
  executive_dashboard = self._generate_executive_dashboard(html_data)
287
289
  implementation_groups = self._generate_implementation_groups_section(html_data)
288
- detailed_findings = self._generate_detailed_findings_section(html_data)
289
290
  resource_details = self._generate_resource_details_section(html_data)
290
- remediation_section = self._generate_remediation_section(html_data)
291
291
  footer = self._generate_footer(html_data)
292
292
 
293
293
  body_content = f"""<body>
@@ -296,9 +296,7 @@ class HTMLReporter(ReportGenerator):
296
296
  {navigation}
297
297
  {executive_dashboard}
298
298
  {implementation_groups}
299
- {detailed_findings}
300
299
  {resource_details}
301
- {remediation_section}
302
300
  {footer}
303
301
  </div>
304
302
 
@@ -771,9 +769,9 @@ class HTMLReporter(ReportGenerator):
771
769
  background-color: #2c3e50;
772
770
  }
773
771
 
774
- /* Resource ID column width constraint */
772
+ /* Resource ID column width constraint - increased to 220px in v1.1.2 */
775
773
  .resource-table td:first-child {
776
- max-width: 200px;
774
+ max-width: 220px;
777
775
  overflow: hidden;
778
776
  text-overflow: ellipsis;
779
777
  white-space: nowrap;
@@ -785,6 +783,20 @@ class HTMLReporter(ReportGenerator):
785
783
  word-wrap: break-word;
786
784
  }
787
785
 
786
+ /* Resource Type column width constraint - added in v1.1.2 (reduced by 20%) */
787
+ .resource-table td:nth-child(2) {
788
+ max-width: 150px;
789
+ overflow: hidden;
790
+ text-overflow: ellipsis;
791
+ white-space: nowrap;
792
+ }
793
+
794
+ .resource-table td:nth-child(2):hover {
795
+ overflow: visible;
796
+ white-space: normal;
797
+ word-wrap: break-word;
798
+ }
799
+
788
800
  /* Visual frames around each resource row */
789
801
  .resource-row {
790
802
  border: 1px solid #e0e0e0;
@@ -1487,11 +1499,12 @@ class HTMLReporter(ReportGenerator):
1487
1499
  window.URL.revokeObjectURL(url);
1488
1500
  }}
1489
1501
 
1490
- // Resource filtering functionality
1502
+ // Resource filtering functionality (updated in v1.1.2 to support Control filter)
1491
1503
  function filterResources() {{
1492
1504
  const searchTerm = document.getElementById('resourceSearch').value.toLowerCase();
1493
1505
  const statusFilter = document.getElementById('statusFilter').value;
1494
1506
  const typeFilter = document.getElementById('typeFilter').value;
1507
+ const controlFilter = document.getElementById('controlFilter').value;
1495
1508
  const rows = document.querySelectorAll('#resourceTable tbody tr');
1496
1509
 
1497
1510
  rows.forEach(function(row) {{
@@ -1500,6 +1513,7 @@ class HTMLReporter(ReportGenerator):
1500
1513
  const resourceType = cells[1].textContent;
1501
1514
  const status = cells[3].textContent.includes('COMPLIANT') ?
1502
1515
  (cells[3].textContent.includes('NON_COMPLIANT') ? 'NON_COMPLIANT' : 'COMPLIANT') : 'NON_COMPLIANT';
1516
+ const controlId = cells[4].textContent;
1503
1517
  const evaluationReason = cells[6].textContent.toLowerCase();
1504
1518
 
1505
1519
  const matchesSearch = resourceId.includes(searchTerm) ||
@@ -1507,8 +1521,9 @@ class HTMLReporter(ReportGenerator):
1507
1521
  evaluationReason.includes(searchTerm);
1508
1522
  const matchesStatus = !statusFilter || status === statusFilter;
1509
1523
  const matchesType = !typeFilter || resourceType === typeFilter;
1524
+ const matchesControl = !controlFilter || controlId === controlFilter;
1510
1525
 
1511
- row.style.display = (matchesSearch && matchesStatus && matchesType) ? '' : 'none';
1526
+ row.style.display = (matchesSearch && matchesStatus && matchesType && matchesControl) ? '' : 'none';
1512
1527
  }});
1513
1528
  }}
1514
1529
 
@@ -1538,21 +1553,78 @@ class HTMLReporter(ReportGenerator):
1538
1553
  }});
1539
1554
  }}
1540
1555
 
1541
- // Export resources to CSV
1556
+ // Export resources to CSV (updated in v1.1.2 with ARN, aggregate filtering, and port truncation)
1542
1557
  function exportResourcesToCSV() {{
1543
1558
  const table = document.getElementById('resourceTable');
1544
1559
  const rows = table.querySelectorAll('tr');
1545
1560
  let csvContent = '';
1546
1561
 
1547
- rows.forEach(function(row) {{
1562
+ // Excluded aggregate row IDs (v1.1.2)
1563
+ const excludedIds = ['5631', '6460', '629'];
1564
+
1565
+ // Helper function to truncate port lists (v1.1.2)
1566
+ function truncatePortList(text) {{
1567
+ // Match port list patterns like [0, 1, 2, 3, ...]
1568
+ const portListRegex = /\\[(\\d+(?:,\\s*\\d+)*)\\]/g;
1569
+
1570
+ return text.replace(portListRegex, function(match, ports) {{
1571
+ const portArray = ports.split(',').map(p => p.trim());
1572
+ if (portArray.length > 10) {{
1573
+ const truncated = portArray.slice(0, 10).join(', ');
1574
+ return `[${{truncated}}, ...]`;
1575
+ }}
1576
+ return match;
1577
+ }});
1578
+ }}
1579
+
1580
+ rows.forEach(function(row, index) {{
1548
1581
  const cells = row.querySelectorAll('th, td');
1549
- const rowData = Array.from(cells).map(cell =>
1550
- '"' + cell.textContent.replace(/"/g, '""').replace(/\\s+/g, ' ').trim() + '"'
1551
- ).join(',');
1552
- csvContent += rowData + '\\n';
1582
+
1583
+ // Handle header row - change "Resource ID" to "Resource ARN"
1584
+ if (index === 0) {{
1585
+ const headerData = Array.from(cells).map((cell, cellIndex) => {{
1586
+ let headerText = cell.textContent.replace(/\\s+/g, ' ').trim();
1587
+ // Replace "Resource ID" with "Resource ARN" in header
1588
+ if (cellIndex === 0 && headerText.includes('Resource ID')) {{
1589
+ headerText = headerText.replace('Resource ID', 'Resource ARN');
1590
+ }}
1591
+ return '"' + headerText.replace(/"/g, '""') + '"';
1592
+ }}).join(',');
1593
+ csvContent += headerData + '\\n';
1594
+ }} else if (cells.length > 0) {{
1595
+ // Get resource ID to check if it should be excluded
1596
+ const resourceIdCell = cells[0];
1597
+ const resourceId = resourceIdCell.textContent.replace(/\\s+/g, ' ').trim();
1598
+
1599
+ // Skip aggregate rows (v1.1.2)
1600
+ if (excludedIds.includes(resourceId)) {{
1601
+ return;
1602
+ }}
1603
+
1604
+ // Get ARN from data attribute or fall back to resource ID
1605
+ const resourceArn = resourceIdCell.getAttribute('data-arn') || resourceId;
1606
+
1607
+ const rowData = Array.from(cells).map((cell, cellIndex) => {{
1608
+ let cellText = cell.textContent.replace(/\\s+/g, ' ').trim();
1609
+
1610
+ // Use ARN for first column instead of Resource ID (v1.1.2)
1611
+ if (cellIndex === 0) {{
1612
+ cellText = resourceArn;
1613
+ }}
1614
+
1615
+ // Apply port list truncation to evaluation reason column (v1.1.2)
1616
+ if (cellIndex === 6) {{
1617
+ cellText = truncatePortList(cellText);
1618
+ }}
1619
+
1620
+ return '"' + cellText.replace(/"/g, '""') + '"';
1621
+ }}).join(',');
1622
+ csvContent += rowData + '\\n';
1623
+ }}
1553
1624
  }});
1554
1625
 
1555
- const blob = new Blob([csvContent], {{ type: 'text/csv' }});
1626
+ // Add UTF-8 BOM to ensure proper encoding in Excel and other tools
1627
+ const blob = new Blob(['\ufeff' + csvContent], {{ type: 'text/csv;charset=utf-8;' }});
1556
1628
  const url = window.URL.createObjectURL(blob);
1557
1629
  const a = document.createElement('a');
1558
1630
  a.href = url;
@@ -1849,161 +1921,7 @@ class HTMLReporter(ReportGenerator):
1849
1921
  </section>
1850
1922
  """
1851
1923
 
1852
- def _generate_detailed_findings_section(self, html_data: Dict[str, Any]) -> str:
1853
- """Generate detailed findings section.
1854
-
1855
- This method consolidates findings by control ID only (not by IG) to eliminate
1856
- duplication. Each control appears once with IG membership indicators.
1857
-
1858
- Args:
1859
- html_data: Enhanced HTML report data
1860
-
1861
- Returns:
1862
- Detailed findings HTML as string
1863
- """
1864
- # Consolidate findings by control ID (deduplicated across IGs)
1865
- consolidated_findings = self._consolidate_findings_by_control(
1866
- html_data.get("implementation_groups", {})
1867
- )
1868
-
1869
- findings_content = ""
1870
-
1871
- # Generate findings grouped by control ID only (sorted alphanumerically)
1872
- for control_id, control_data in consolidated_findings.items():
1873
- findings = control_data.get('findings', [])
1874
-
1875
- # Skip if no non-compliant findings
1876
- if not findings:
1877
- continue
1878
-
1879
- # Get control metadata
1880
- config_rule_name = control_data.get('config_rule_name', '')
1881
- title = control_data.get('title', f'CIS Control {control_id}')
1882
- member_igs = control_data.get('member_igs', [])
1883
-
1884
- # Format display name for collapsible header
1885
- display_name = self._format_control_display_name(control_id, config_rule_name, title)
1886
-
1887
- # Generate IG membership badges
1888
- ig_badges_html = ""
1889
- for ig in member_igs:
1890
- badge_class = self._get_ig_badge_class(ig)
1891
- ig_badges_html += f'<span class="ig-membership-badge {badge_class}">{ig}</span>'
1892
-
1893
- # Generate findings table rows
1894
- findings_rows = ""
1895
- for finding in findings:
1896
- if finding.get("compliance_status") == "NON_COMPLIANT":
1897
- findings_rows += f"""
1898
- <tr>
1899
- <td>{finding.get('resource_id', 'N/A')}</td>
1900
- <td>{finding.get('resource_type', 'N/A')}</td>
1901
- <td>{finding.get('region', 'N/A')}</td>
1902
- <td><span class="badge {finding.get('compliance_status', '').lower()}">{finding.get('compliance_status', 'UNKNOWN')}</span></td>
1903
- <td>{finding.get('evaluation_reason', 'N/A')}</td>
1904
- <td>{finding.get('config_rule_name', config_rule_name)}</td>
1905
- </tr>
1906
- """
1907
-
1908
- # Only add control section if there are non-compliant findings
1909
- if findings_rows:
1910
- non_compliant_count = len([f for f in findings if f.get('compliance_status') == 'NON_COMPLIANT'])
1911
-
1912
- findings_content += f"""
1913
- <div class="control-findings-section">
1914
- <button class="collapsible">
1915
- <span class="control-display-name">{display_name}</span>
1916
- <span class="findings-count"> - Non-Compliant Resources ({non_compliant_count} items)</span>
1917
- </button>
1918
- <div class="collapsible-content">
1919
- <div class="ig-membership-badges" style="margin-bottom: 15px;">
1920
- <strong>Implementation Groups:</strong> {ig_badges_html}
1921
- </div>
1922
- <table class="findings-table">
1923
- <thead>
1924
- <tr>
1925
- <th>Resource ID</th>
1926
- <th>Resource Type</th>
1927
- <th>Region</th>
1928
- <th>Compliance Status</th>
1929
- <th>Reason</th>
1930
- <th>Config Rule</th>
1931
- </tr>
1932
- </thead>
1933
- <tbody>
1934
- {findings_rows}
1935
- </tbody>
1936
- </table>
1937
- </div>
1938
- </div>
1939
- """
1940
-
1941
- return f"""
1942
- <section id="detailed-findings" class="detailed-findings">
1943
- <h2>Detailed Findings</h2>
1944
- <p class="section-description">
1945
- Findings are grouped by control ID and deduplicated across Implementation Groups.
1946
- Each control shows which IGs include it.
1947
- </p>
1948
- <div class="search-container">
1949
- <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;">
1950
- </div>
1951
- {findings_content if findings_content else '<p>No non-compliant findings to display.</p>'}
1952
- </section>
1953
- """
1954
1924
 
1955
- def _generate_remediation_section(self, html_data: Dict[str, Any]) -> str:
1956
- """Generate remediation section.
1957
-
1958
- Args:
1959
- html_data: Enhanced HTML report data
1960
-
1961
- Returns:
1962
- Remediation HTML as string
1963
- """
1964
- remediation_items = ""
1965
-
1966
- for remediation in html_data["remediation_priorities"]:
1967
- steps_html = ""
1968
- for step in remediation["remediation_steps"]:
1969
- steps_html += f"<li>{step}</li>"
1970
-
1971
- # Normalize priority and effort text to proper capitalization
1972
- priority_text = remediation['priority'].capitalize()
1973
- effort_text = remediation['estimated_effort'].capitalize()
1974
-
1975
- remediation_items += f"""
1976
- <div class="remediation-item">
1977
- <div class="remediation-header">
1978
- <div>
1979
- <h4>{remediation['control_id']} - {remediation['config_rule_name']}</h4>
1980
- </div>
1981
- <div class="remediation-badges">
1982
- <span class="badge {remediation['priority_badge']}">{priority_text}</span>
1983
- <span class="badge {remediation['effort_badge']}">{effort_text}</span>
1984
- </div>
1985
- </div>
1986
- <div class="remediation-content">
1987
- <h5>Remediation Steps:</h5>
1988
- <ol>
1989
- {steps_html}
1990
- </ol>
1991
- <p><strong>Documentation:</strong> <a href="{remediation['aws_documentation_link']}" target="_blank">AWS Documentation</a></p>
1992
- </div>
1993
- </div>
1994
- """
1995
-
1996
- return f"""
1997
- <section id="remediation" class="remediation">
1998
- <h2>Remediation Priorities</h2>
1999
- <div class="remediation-list">
2000
- {remediation_items}
2001
- </div>
2002
- <div class="export-actions">
2003
- <button onclick="exportToCSV()" class="export-btn">Export Findings to CSV</button>
2004
- </div>
2005
- </section>
2006
- """
2007
1925
 
2008
1926
  def _generate_footer(self, html_data: Dict[str, Any]) -> str:
2009
1927
  """Generate footer section.
@@ -2038,7 +1956,7 @@ class HTMLReporter(ReportGenerator):
2038
1956
  </div>
2039
1957
  </div>
2040
1958
  <div class="footer-bottom">
2041
- <p>&copy; 2024 AWS CIS Assessment Tool. Generated with HTML Reporter v{html_data.get('report_version', '1.0')}</p>
1959
+ <p>&copy; {datetime.now().year} AWS CIS Assessment Tool. Generated with HTML Reporter v{html_data.get('report_version', '1.1.2')}</p>
2042
1960
  </div>
2043
1961
  </footer>
2044
1962
  """
@@ -2109,6 +2027,8 @@ class HTMLReporter(ReportGenerator):
2109
2027
  def _build_navigation_structure(self, html_data: Dict[str, Any]) -> Dict[str, Any]:
2110
2028
  """Build navigation structure for the report.
2111
2029
 
2030
+ Modified in v1.1.2 to remove Detailed Findings and Remediation sections.
2031
+
2112
2032
  Args:
2113
2033
  html_data: Enhanced HTML report data
2114
2034
 
@@ -2119,9 +2039,7 @@ class HTMLReporter(ReportGenerator):
2119
2039
  "sections": [
2120
2040
  {"id": "dashboard", "title": "Dashboard"},
2121
2041
  {"id": "implementation-groups", "title": "Implementation Groups"},
2122
- {"id": "detailed-findings", "title": "Detailed Findings"},
2123
- {"id": "resource-details", "title": "Resource Details"},
2124
- {"id": "remediation", "title": "Remediation"}
2042
+ {"id": "resource-details", "title": "Resource Details"}
2125
2043
  ]
2126
2044
  }
2127
2045
 
@@ -2592,16 +2510,21 @@ class HTMLReporter(ReportGenerator):
2592
2510
  resource_rows = ""
2593
2511
  for resource in all_resources:
2594
2512
  status_class = "compliant" if resource["compliance_status"] == "COMPLIANT" else "non_compliant"
2595
- status_icon = "" if resource["compliance_status"] == "COMPLIANT" else ""
2513
+ status_text = "COMPLIANT" if resource["compliance_status"] == "COMPLIANT" else "NON_COMPLIANT"
2514
+
2515
+ # Construct pseudo-ARN for CSV export (v1.1.2)
2516
+ # Format: arn:aws:service:region:account:resource-type/resource-id
2517
+ # Since we don't have account ID in resource data, we'll use a placeholder
2518
+ resource_arn = f"arn:aws:{resource['resource_type'].split('::')[1].lower() if '::' in resource['resource_type'] else 'unknown'}:{resource['region']}:*:{resource['resource_id']}"
2596
2519
 
2597
2520
  resource_rows += f"""
2598
2521
  <tr class="resource-row {status_class}">
2599
- <td><code>{resource['resource_id']}</code></td>
2522
+ <td data-arn="{resource_arn}"><code>{resource['resource_id']}</code></td>
2600
2523
  <td>{resource['resource_type']}</td>
2601
2524
  <td>{resource['region']}</td>
2602
2525
  <td>
2603
2526
  <span class="badge {status_class}">
2604
- {status_icon} {resource['compliance_status']}
2527
+ {status_text}
2605
2528
  </span>
2606
2529
  </td>
2607
2530
  <td>{resource['control_id']}</td>
@@ -2616,6 +2539,12 @@ class HTMLReporter(ReportGenerator):
2616
2539
  non_compliant_resources = total_resources - compliant_resources
2617
2540
  compliance_percentage = (compliant_resources / total_resources * 100) if total_resources > 0 else 0
2618
2541
 
2542
+ # Extract unique Control IDs for filter dropdown (v1.1.2)
2543
+ unique_control_ids = sorted(set(r["control_id"] for r in all_resources), key=self._sort_control_id)
2544
+ control_filter_options = ""
2545
+ for control_id in unique_control_ids:
2546
+ control_filter_options += f'<option value="{control_id}">{control_id}</option>'
2547
+
2619
2548
  # Generate resource type breakdown
2620
2549
  resource_type_stats = {}
2621
2550
  for resource in all_resources:
@@ -2689,17 +2618,21 @@ class HTMLReporter(ReportGenerator):
2689
2618
  <option value="">All Types</option>
2690
2619
  {self._generate_resource_type_options(resource_type_stats)}
2691
2620
  </select>
2621
+ <select id="controlFilter" onchange="filterResources()" class="filter-select">
2622
+ <option value="">All Controls</option>
2623
+ {control_filter_options}
2624
+ </select>
2692
2625
  </div>
2693
2626
 
2694
2627
  <table class="findings-table resource-table" id="resourceTable">
2695
2628
  <thead>
2696
2629
  <tr>
2697
- <th onclick="sortResourceTable(0)">Resource ID ↕</th>
2698
- <th onclick="sortResourceTable(1)">Resource Type ↕</th>
2699
- <th onclick="sortResourceTable(2)">Region ↕</th>
2700
- <th onclick="sortResourceTable(3)">Status ↕</th>
2701
- <th onclick="sortResourceTable(4)">Control ↕</th>
2702
- <th onclick="sortResourceTable(5)">Config Rule ↕</th>
2630
+ <th onclick="sortResourceTable(0)">Resource ID</th>
2631
+ <th onclick="sortResourceTable(1)">Resource Type</th>
2632
+ <th onclick="sortResourceTable(2)">Region</th>
2633
+ <th onclick="sortResourceTable(3)">Status</th>
2634
+ <th onclick="sortResourceTable(4)">Control</th>
2635
+ <th onclick="sortResourceTable(5)">Config Rule</th>
2703
2636
  <th>Evaluation Details</th>
2704
2637
  </tr>
2705
2638
  </thead>
@@ -2859,6 +2792,8 @@ class HTMLReporter(ReportGenerator):
2859
2792
  Enriches control data with additional fields needed for improved display,
2860
2793
  including formatted names, IG membership badges, and truncation indicators.
2861
2794
 
2795
+ Modified in v1.1.2 to remove duplicate Control ID prefix from display names.
2796
+
2862
2797
  Args:
2863
2798
  control_data: Existing control data dictionary
2864
2799
  control_id: Control identifier (e.g., "1.5")
@@ -2867,7 +2802,7 @@ class HTMLReporter(ReportGenerator):
2867
2802
 
2868
2803
  Returns:
2869
2804
  Enhanced control data with additional fields:
2870
- - display_name: Formatted name combining control ID and rule name
2805
+ - display_name: Formatted name without duplicate Control ID prefix
2871
2806
  - originating_ig: Which IG introduced this control (IG1, IG2, or IG3)
2872
2807
  - ig_badge_class: CSS class for IG badge styling
2873
2808
  - needs_truncation: Boolean indicating if display name exceeds 50 characters
@@ -2884,7 +2819,7 @@ class HTMLReporter(ReportGenerator):
2884
2819
  Output enriched data (includes all input fields plus):
2885
2820
  {
2886
2821
  ...original fields...,
2887
- 'display_name': '1.5: root-account-hardware-mfa-enabled',
2822
+ 'display_name': 'root-account-hardware-mfa-enabled', # No "1.5:" prefix
2888
2823
  'originating_ig': 'IG1',
2889
2824
  'ig_badge_class': 'ig-badge-1',
2890
2825
  'needs_truncation': False
@@ -2895,16 +2830,25 @@ class HTMLReporter(ReportGenerator):
2895
2830
  - Truncation threshold is 50 characters
2896
2831
  - Gracefully handles missing config_rule_name
2897
2832
  - Originating IG is determined by checking IG1, IG2, IG3 in order
2833
+ - v1.1.2: Removes duplicate Control ID prefix from display names
2898
2834
  """
2899
2835
  enriched = control_data.copy()
2900
2836
 
2901
2837
  # Format display name
2902
- enriched['display_name'] = self._format_control_display_name(
2838
+ display_name = self._format_control_display_name(
2903
2839
  control_id,
2904
2840
  control_data.get('config_rule_name', ''),
2905
2841
  control_data.get('title')
2906
2842
  )
2907
2843
 
2844
+ # Remove duplicate Control ID prefix if present (v1.1.2 improvement)
2845
+ # Check if display_name starts with "control_id: "
2846
+ prefix = f"{control_id}: "
2847
+ if display_name.startswith(prefix):
2848
+ display_name = display_name[len(prefix):]
2849
+
2850
+ enriched['display_name'] = display_name
2851
+
2908
2852
  # Determine originating IG (which IG introduced this control)
2909
2853
  originating_ig = self._determine_originating_ig(control_id, all_igs)
2910
2854
  enriched['originating_ig'] = originating_ig
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aws-cis-controls-assessment
3
- Version: 1.1.1
3
+ Version: 1.1.3
4
4
  Summary: Production-ready AWS CIS Controls compliance assessment framework with 145 comprehensive rules
5
5
  Author-email: AWS CIS Assessment Team <security@example.com>
6
6
  Maintainer-email: AWS CIS Assessment Team <security@example.com>
@@ -1,4 +1,4 @@
1
- aws_cis_assessment/__init__.py,sha256=ICafcznKTUU2j4VAbZ6-yjWXSQZGQRYzpzY7NIZ_23U,480
1
+ aws_cis_assessment/__init__.py,sha256=rJ8zffZgftMUnbpp_ElI6Lxf5RyebWV_n33Rmzn4rYQ,480
2
2
  aws_cis_assessment/cli/__init__.py,sha256=DYaGVAIoy5ucs9ubKQxX6Z3ZD46AGz9AaIaDQXzrzeY,100
3
3
  aws_cis_assessment/cli/examples.py,sha256=F9K2Fe297kUfwoq6Ine9Aj_IXNU-KwO9hd7SAPWeZHI,12884
4
4
  aws_cis_assessment/cli/main.py,sha256=i5QoqHXsPG_Kw0W7jM3Zj2YaAaCJnxxnfz82QBBHq-U,49441
@@ -20,7 +20,7 @@ aws_cis_assessment/controls/ig1/control_access_keys.py,sha256=Hj3G0Qpwa2EcJE-u49
20
20
  aws_cis_assessment/controls/ig1/control_advanced_security.py,sha256=PNtPfqSKGu7UYDx6PccO8tVT5ZL6YmzeH45Cew_UjLM,24256
21
21
  aws_cis_assessment/controls/ig1/control_aws_backup_service.py,sha256=_bUc6x7jXhav0Cm5jfX0_tk1UOa8qoso2ND1-6xsPtI,54651
22
22
  aws_cis_assessment/controls/ig1/control_backup_recovery.py,sha256=Y5za_4lCZmA5MYhHp4OCGyL4z97cj6dbO0KfabQ5Hr0,21465
23
- aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py,sha256=lQOjshW8BBymvzphtWuwg4wIyv6nH2mOSiogBe_Ejfo,8514
23
+ aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py,sha256=Y2KEIHcf7cDj_lbdNWk6WHrKvls79zJnpGXyKEoJ-CU,10567
24
24
  aws_cis_assessment/controls/ig1/control_critical_security.py,sha256=1MVMkfOAWcH5ppFv7psZvJvcOtpww6Pl5WFXrMyN158,20942
25
25
  aws_cis_assessment/controls/ig1/control_data_protection.py,sha256=-EDT-d0IcYpdv4cYSNfsSKwX7YzKZ9MiVY18-6YHcVE,44216
26
26
  aws_cis_assessment/controls/ig1/control_iam_advanced.py,sha256=FQA_8IV5CyD_49u0eLN8q-JM50g1-tilDu9Ww_R3o9s,27694
@@ -63,9 +63,9 @@ aws_cis_assessment/core/scoring_engine.py,sha256=ylx2urk_DxGzU_LZB0ip-qtUzOh4yu0
63
63
  aws_cis_assessment/reporters/__init__.py,sha256=GXdlY08kKy1Y3mMBv8Y0JuUB69u--e5DIu2jNJpc6QI,357
64
64
  aws_cis_assessment/reporters/base_reporter.py,sha256=joy_O4IL4Hs_qwAuPtl81GIPxLAbUAMFKiF8r5si2aw,18082
65
65
  aws_cis_assessment/reporters/csv_reporter.py,sha256=r83xzfP1t5AO9MfKawgN4eTeOU6eGZwJQgvNDLEd7NI,31419
66
- aws_cis_assessment/reporters/html_reporter.py,sha256=H5LkcaXbzArNNO-CLJT5oXMSNccxU58L-ba_Q769Yhs,118310
66
+ aws_cis_assessment/reporters/html_reporter.py,sha256=aAobXO3gsfE2ZmOhimNiRbvUad4DkXZQAbGPw_KHXhs,116399
67
67
  aws_cis_assessment/reporters/json_reporter.py,sha256=MObCzTc9nlGTEXeWc7P8tTMeKCpEaJNfcSYc79cHXhc,22250
68
- aws_cis_controls_assessment-1.1.1.dist-info/licenses/LICENSE,sha256=T_p0qKH4RoI3ejr3tktf3rx2Zart_9KeUmJd5iiqXW8,1079
68
+ aws_cis_controls_assessment-1.1.3.dist-info/licenses/LICENSE,sha256=T_p0qKH4RoI3ejr3tktf3rx2Zart_9KeUmJd5iiqXW8,1079
69
69
  deprecation-package/aws_cis_assessment_deprecated/__init__.py,sha256=WOaufqanKNhvWQ3frj8e627tS_kZnyk2R2hwqPFqydw,1892
70
70
  docs/README.md,sha256=MXnfbPRmxir-7ihG2lNmLI9TJG0Pp0QWqoDZtXiH_Mk,4912
71
71
  docs/adding-aws-backup-controls.md,sha256=l_H0H8W71n-6NbeplNujC_li2NiaQcYPr0hQMhEPbrc,21081
@@ -80,8 +80,8 @@ docs/scoring-comparison-aws-config.md,sha256=8BBe1tQsaAT0BAE3OdGIRFjuT1VJcOlM1qB
80
80
  docs/scoring-methodology.md,sha256=C86FisBxKt6pyr-Kp6rAVPz45yPZpgsGibjgq8obIsg,9404
81
81
  docs/troubleshooting.md,sha256=mGmWgrc3A1dn-Uk_XxWFh04OQxjmqkeax8vQX7takg0,18220
82
82
  docs/user-guide.md,sha256=lBDgU40tIPstOdNx4YqVkPTIDntn4o2y2tr2CPQt7b8,11942
83
- aws_cis_controls_assessment-1.1.1.dist-info/METADATA,sha256=gXpUI7yboznt4qn6KmVKtKSZVVO9In29KhHqrokOqZo,21383
84
- aws_cis_controls_assessment-1.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
85
- aws_cis_controls_assessment-1.1.1.dist-info/entry_points.txt,sha256=-AxPn5Y7yau0pQh33F5_uyWfvcnm2Kg1_nMQuLrZ7SY,68
86
- aws_cis_controls_assessment-1.1.1.dist-info/top_level.txt,sha256=4OHmV6RAEWkz-Se50kfmuGCd-mUSotDZz3iLGF9CmkI,44
87
- aws_cis_controls_assessment-1.1.1.dist-info/RECORD,,
83
+ aws_cis_controls_assessment-1.1.3.dist-info/METADATA,sha256=NllvhMBOmpsLo01qt7FQxXcHWAd4rJWkgP6QTQYZMog,21383
84
+ aws_cis_controls_assessment-1.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
85
+ aws_cis_controls_assessment-1.1.3.dist-info/entry_points.txt,sha256=-AxPn5Y7yau0pQh33F5_uyWfvcnm2Kg1_nMQuLrZ7SY,68
86
+ aws_cis_controls_assessment-1.1.3.dist-info/top_level.txt,sha256=4OHmV6RAEWkz-Se50kfmuGCd-mUSotDZz3iLGF9CmkI,44
87
+ aws_cis_controls_assessment-1.1.3.dist-info/RECORD,,