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.
- aws_cis_assessment/__init__.py +1 -1
- aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +42 -6
- aws_cis_assessment/reporters/html_reporter.py +129 -185
- {aws_cis_controls_assessment-1.1.1.dist-info → aws_cis_controls_assessment-1.1.3.dist-info}/METADATA +1 -1
- {aws_cis_controls_assessment-1.1.1.dist-info → aws_cis_controls_assessment-1.1.3.dist-info}/RECORD +9 -9
- {aws_cis_controls_assessment-1.1.1.dist-info → aws_cis_controls_assessment-1.1.3.dist-info}/WHEEL +0 -0
- {aws_cis_controls_assessment-1.1.1.dist-info → aws_cis_controls_assessment-1.1.3.dist-info}/entry_points.txt +0 -0
- {aws_cis_controls_assessment-1.1.1.dist-info → aws_cis_controls_assessment-1.1.3.dist-info}/licenses/LICENSE +0 -0
- {aws_cis_controls_assessment-1.1.1.dist-info → aws_cis_controls_assessment-1.1.3.dist-info}/top_level.txt +0 -0
aws_cis_assessment/__init__.py
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
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
|
-
|
|
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>©
|
|
1959
|
+
<p>© {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": "
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
|
2698
|
-
<th onclick="sortResourceTable(1)">Resource Type
|
|
2699
|
-
<th onclick="sortResourceTable(2)">Region
|
|
2700
|
-
<th onclick="sortResourceTable(3)">Status
|
|
2701
|
-
<th onclick="sortResourceTable(4)">Control
|
|
2702
|
-
<th onclick="sortResourceTable(5)">Config Rule
|
|
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
|
|
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': '
|
|
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
|
-
|
|
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
|
{aws_cis_controls_assessment-1.1.1.dist-info → aws_cis_controls_assessment-1.1.3.dist-info}/METADATA
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-cis-controls-assessment
|
|
3
|
-
Version: 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>
|
{aws_cis_controls_assessment-1.1.1.dist-info → aws_cis_controls_assessment-1.1.3.dist-info}/RECORD
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
aws_cis_assessment/__init__.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
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.
|
|
84
|
-
aws_cis_controls_assessment-1.1.
|
|
85
|
-
aws_cis_controls_assessment-1.1.
|
|
86
|
-
aws_cis_controls_assessment-1.1.
|
|
87
|
-
aws_cis_controls_assessment-1.1.
|
|
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,,
|
{aws_cis_controls_assessment-1.1.1.dist-info → aws_cis_controls_assessment-1.1.3.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|