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.
Files changed (26) hide show
  1. aws_cis_assessment/__init__.py +2 -2
  2. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +94 -1
  3. aws_cis_assessment/config/rules/cis_controls_ig2.yaml +680 -1
  4. aws_cis_assessment/controls/ig1/__init__.py +17 -0
  5. aws_cis_assessment/controls/ig1/control_aws_backup_service.py +1276 -0
  6. aws_cis_assessment/controls/ig2/__init__.py +74 -1
  7. aws_cis_assessment/controls/ig2/control_4_5_6_access_configuration.py +2638 -0
  8. aws_cis_assessment/controls/ig2/control_8_audit_logging.py +984 -0
  9. aws_cis_assessment/controls/ig2/control_aws_backup_ig2.py +23 -0
  10. aws_cis_assessment/core/assessment_engine.py +74 -0
  11. aws_cis_assessment/reporters/html_reporter.py +197 -35
  12. {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/METADATA +163 -12
  13. {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/RECORD +26 -21
  14. docs/README.md +14 -3
  15. docs/adding-aws-backup-controls.md +562 -0
  16. docs/assessment-logic.md +291 -3
  17. docs/cli-reference.md +1 -1
  18. docs/config-rule-mappings.md +465 -7
  19. docs/developer-guide.md +312 -3
  20. docs/installation.md +2 -2
  21. docs/troubleshooting.md +211 -2
  22. docs/user-guide.md +47 -2
  23. {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/WHEEL +0 -0
  24. {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/entry_points.txt +0 -0
  25. {aws_cis_controls_assessment-1.0.9.dist-info → aws_cis_controls_assessment-1.1.0.dist-info}/licenses/LICENSE +0 -0
  26. {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 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';
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
- <h4>{remediation['control_id']} - {remediation['config_rule_name']}</h4>
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']}">{remediation['priority']}</span>
1860
- <span class="badge {remediation['effort_badge']}">{remediation['estimated_effort']}</span>
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
- <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>
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, rule name, and optional title.
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 AWS Config rule name, making it easier for users to understand what
2623
- each control checks without looking up documentation.
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} ({config_rule_name})"
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.5", "root-account-hardware-mfa-enabled")
2638
- "1.5: root-account-hardware-mfa-enabled"
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("2.1", "iam-password-policy", "IAM Password Policy")
2641
- "2.1: IAM Password Policy (iam-password-policy)"
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
- - Gracefully handles missing config_rule_name by falling back to control_id only
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 config_rule_name:
2652
- # Fallback to control_id only if config_rule_name is missing
2653
- return control_id
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} ({config_rule_name})"
2657
- else:
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.