aws-cis-controls-assessment 1.1.4__py3-none-any.whl → 1.2.2__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 (45) hide show
  1. aws_cis_assessment/__init__.py +4 -4
  2. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +365 -2
  3. aws_cis_assessment/controls/base_control.py +106 -24
  4. aws_cis_assessment/controls/ig1/__init__.py +144 -15
  5. aws_cis_assessment/controls/ig1/control_4_1.py +4 -4
  6. aws_cis_assessment/controls/ig1/control_access_analyzer.py +198 -0
  7. aws_cis_assessment/controls/ig1/control_access_asset_mgmt.py +360 -0
  8. aws_cis_assessment/controls/ig1/control_access_control.py +323 -0
  9. aws_cis_assessment/controls/ig1/control_backup_security.py +579 -0
  10. aws_cis_assessment/controls/ig1/control_cloudfront_logging.py +215 -0
  11. aws_cis_assessment/controls/ig1/control_configuration_mgmt.py +407 -0
  12. aws_cis_assessment/controls/ig1/control_data_classification.py +255 -0
  13. aws_cis_assessment/controls/ig1/control_dynamodb_encryption.py +279 -0
  14. aws_cis_assessment/controls/ig1/control_ebs_encryption.py +177 -0
  15. aws_cis_assessment/controls/ig1/control_efs_encryption.py +243 -0
  16. aws_cis_assessment/controls/ig1/control_elb_logging.py +195 -0
  17. aws_cis_assessment/controls/ig1/control_guardduty.py +156 -0
  18. aws_cis_assessment/controls/ig1/control_inspector.py +184 -0
  19. aws_cis_assessment/controls/ig1/control_inventory.py +511 -0
  20. aws_cis_assessment/controls/ig1/control_macie.py +165 -0
  21. aws_cis_assessment/controls/ig1/control_messaging_encryption.py +419 -0
  22. aws_cis_assessment/controls/ig1/control_mfa.py +485 -0
  23. aws_cis_assessment/controls/ig1/control_network_security.py +194 -619
  24. aws_cis_assessment/controls/ig1/control_patch_management.py +626 -0
  25. aws_cis_assessment/controls/ig1/control_rds_encryption.py +228 -0
  26. aws_cis_assessment/controls/ig1/control_s3_encryption.py +383 -0
  27. aws_cis_assessment/controls/ig1/control_tls_ssl.py +556 -0
  28. aws_cis_assessment/controls/ig1/control_version_mgmt.py +337 -0
  29. aws_cis_assessment/controls/ig1/control_vpc_flow_logs.py +205 -0
  30. aws_cis_assessment/controls/ig1/control_waf_logging.py +226 -0
  31. aws_cis_assessment/core/assessment_engine.py +160 -11
  32. aws_cis_assessment/core/aws_client_factory.py +17 -5
  33. aws_cis_assessment/core/models.py +20 -1
  34. aws_cis_assessment/core/scoring_engine.py +102 -1
  35. aws_cis_assessment/reporters/base_reporter.py +58 -13
  36. aws_cis_assessment/reporters/html_reporter.py +186 -9
  37. aws_cis_controls_assessment-1.2.2.dist-info/METADATA +320 -0
  38. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/RECORD +44 -20
  39. docs/developer-guide.md +204 -5
  40. docs/user-guide.md +137 -4
  41. aws_cis_controls_assessment-1.1.4.dist-info/METADATA +0 -404
  42. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/WHEEL +0 -0
  43. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/entry_points.txt +0 -0
  44. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/licenses/LICENSE +0 -0
  45. {aws_cis_controls_assessment-1.1.4.dist-info → aws_cis_controls_assessment-1.2.2.dist-info}/top_level.txt +0 -0
@@ -80,21 +80,36 @@ class ReportGenerator(ABC):
80
80
  Returns:
81
81
  Dictionary containing standardized report data
82
82
  """
83
- # Calculate additional metrics
84
- total_resources = sum(
85
- sum(len(control.findings) for control in ig.control_scores.values())
86
- for ig in assessment_result.ig_scores.values()
87
- )
88
-
83
+ # Calculate additional metrics - deduplicate resources across IGs
84
+ # Since IG2 includes IG1 and IG3 includes IG1+IG2, we need to count unique resources
85
+ # Use IG1 as the source of truth since it contains all unique evaluations
86
+ # (controls in higher IGs are the same controls, just evaluated again)
87
+
88
+ # Collect unique resources by (resource_id, resource_type, region, config_rule_name)
89
+ unique_resources = {}
90
+
91
+ for ig_name, ig_score in assessment_result.ig_scores.items():
92
+ for control_id, control in ig_score.control_scores.items():
93
+ for finding in control.findings:
94
+ # Create unique key for each resource evaluation
95
+ resource_key = (
96
+ finding.resource_id,
97
+ finding.resource_type,
98
+ finding.region,
99
+ finding.config_rule_name
100
+ )
101
+ # Store the finding (last one wins, but they should be identical)
102
+ unique_resources[resource_key] = finding
103
+
104
+ # Now count from unique resources
105
+ total_resources = len(unique_resources)
89
106
  total_compliant = sum(
90
- sum(control.compliant_resources for control in ig.control_scores.values())
91
- for ig in assessment_result.ig_scores.values()
107
+ 1 for finding in unique_resources.values()
108
+ if finding.compliance_status.value == 'COMPLIANT'
92
109
  )
93
-
94
110
  total_non_compliant = sum(
95
- sum(len([f for f in control.findings if f.compliance_status.value == 'NON_COMPLIANT'])
96
- for control in ig.control_scores.values())
97
- for ig in assessment_result.ig_scores.values()
111
+ 1 for finding in unique_resources.values()
112
+ if finding.compliance_status.value == 'NON_COMPLIANT'
98
113
  )
99
114
 
100
115
  # Prepare standardized data structure
@@ -119,7 +134,8 @@ class ReportGenerator(ABC):
119
134
  'compliant_resources': total_compliant,
120
135
  'non_compliant_resources': total_non_compliant,
121
136
  'top_risk_areas': compliance_summary.top_risk_areas,
122
- 'compliance_trend': compliance_summary.compliance_trend
137
+ 'compliance_trend': compliance_summary.compliance_trend,
138
+ 'coverage_metrics': self._prepare_coverage_metrics_data(compliance_summary.coverage_metrics)
123
139
  },
124
140
  'implementation_groups': self._prepare_ig_data(assessment_result.ig_scores),
125
141
  'remediation_priorities': self._prepare_remediation_data(compliance_summary.remediation_priorities),
@@ -228,6 +244,35 @@ class ReportGenerator(ABC):
228
244
 
229
245
  return remediation_data
230
246
 
247
+ def _prepare_coverage_metrics_data(self, coverage_metrics: Dict[str, Any]) -> Dict[str, Any]:
248
+ """Prepare coverage metrics data for reporting.
249
+
250
+ Args:
251
+ coverage_metrics: Dictionary of CoverageMetrics objects by IG
252
+
253
+ Returns:
254
+ Dictionary containing coverage metrics structured for reporting
255
+ """
256
+ coverage_data = {}
257
+
258
+ for ig_name, metrics in coverage_metrics.items():
259
+ # Handle both CoverageMetrics objects and dictionaries
260
+ if hasattr(metrics, '__dict__'):
261
+ metrics_dict = metrics.__dict__
262
+ else:
263
+ metrics_dict = metrics
264
+
265
+ coverage_data[ig_name] = {
266
+ 'implementation_group': metrics_dict.get('implementation_group', ig_name),
267
+ 'total_safeguards': metrics_dict.get('total_safeguards', 0),
268
+ 'covered_safeguards': metrics_dict.get('covered_safeguards', 0),
269
+ 'coverage_percentage': metrics_dict.get('coverage_percentage', 0.0),
270
+ 'implemented_rules': metrics_dict.get('implemented_rules', 0),
271
+ 'safeguard_details': metrics_dict.get('safeguard_details', {})
272
+ }
273
+
274
+ return coverage_data
275
+
231
276
  def _prepare_findings_data(self, ig_scores: Dict[str, IGScore]) -> Dict[str, Any]:
232
277
  """Prepare detailed findings data for reporting.
233
278
 
@@ -173,6 +173,12 @@ class HTMLReporter(ReportGenerator):
173
173
  # Add chart data for Implementation Groups
174
174
  html_data["chart_data"] = self._prepare_chart_data(html_data)
175
175
 
176
+ # Add coverage metrics if available
177
+ if "coverage_metrics" in report_data.get("executive_summary", {}):
178
+ html_data["coverage_metrics"] = self._prepare_coverage_metrics(
179
+ report_data["executive_summary"]["coverage_metrics"]
180
+ )
181
+
176
182
  # Enhance Implementation Group data with visual elements
177
183
  for ig_name, ig_data in html_data["implementation_groups"].items():
178
184
  ig_data["status_color"] = self._get_status_color(ig_data["compliance_percentage"])
@@ -483,6 +489,76 @@ class HTMLReporter(ReportGenerator):
483
489
  font-size: 0.8em;
484
490
  }
485
491
 
492
+ /* Coverage Metrics Section */
493
+ .coverage-metrics-section {
494
+ margin: 30px 0;
495
+ padding: 20px;
496
+ background: #f8f9fa;
497
+ border-radius: 10px;
498
+ }
499
+
500
+ .coverage-intro {
501
+ color: #666;
502
+ margin-bottom: 20px;
503
+ font-size: 0.95em;
504
+ }
505
+
506
+ .coverage-grid {
507
+ display: grid;
508
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
509
+ gap: 20px;
510
+ }
511
+
512
+ .coverage-card {
513
+ background: white;
514
+ border-radius: 10px;
515
+ padding: 20px;
516
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
517
+ border-left: 5px solid #3498db;
518
+ transition: transform 0.2s, box-shadow 0.2s;
519
+ }
520
+
521
+ .coverage-card:hover {
522
+ transform: translateY(-2px);
523
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
524
+ }
525
+
526
+ .coverage-card.excellent { border-left-color: #27ae60; }
527
+ .coverage-card.good { border-left-color: #2ecc71; }
528
+ .coverage-card.fair { border-left-color: #f39c12; }
529
+ .coverage-card.poor { border-left-color: #e67e22; }
530
+ .coverage-card.critical { border-left-color: #e74c3c; }
531
+
532
+ .coverage-card h4 {
533
+ margin: 0 0 15px 0;
534
+ color: #2c3e50;
535
+ font-size: 1.2em;
536
+ }
537
+
538
+ .coverage-value {
539
+ font-size: 2.5em;
540
+ font-weight: bold;
541
+ color: #2c3e50;
542
+ margin-bottom: 15px;
543
+ }
544
+
545
+ .coverage-details {
546
+ color: #666;
547
+ font-size: 0.9em;
548
+ }
549
+
550
+ .coverage-details p {
551
+ margin: 8px 0;
552
+ }
553
+
554
+ .coverage-description {
555
+ margin-top: 12px;
556
+ padding-top: 12px;
557
+ border-top: 1px solid #e0e0e0;
558
+ font-style: italic;
559
+ color: #555;
560
+ }
561
+
486
562
  /* Implementation Groups */
487
563
  .ig-section {
488
564
  margin-bottom: 40px;
@@ -1785,13 +1861,16 @@ class HTMLReporter(ReportGenerator):
1785
1861
  <span>{ig_value:.1f}%</span>
1786
1862
  </div>
1787
1863
  <div class="progress-container">
1788
- <div class="progress-bar {ig_status}" data-width="{ig_value}">
1864
+ <div class="progress-bar {ig_status}" data-width="{self._get_display_width(ig_value)}">
1789
1865
  <span class="progress-text">{ig_value:.1f}%</span>
1790
1866
  </div>
1791
1867
  </div>
1792
1868
  </div>
1793
1869
  """
1794
1870
 
1871
+ # Generate coverage metrics section
1872
+ coverage_section = self._generate_coverage_metrics_section(html_data.get("coverage_metrics", {}))
1873
+
1795
1874
  # Generate charts section
1796
1875
  charts_section = ""
1797
1876
  if self.include_charts:
@@ -1817,6 +1896,8 @@ class HTMLReporter(ReportGenerator):
1817
1896
  {ig_progress}
1818
1897
  </div>
1819
1898
 
1899
+ {coverage_section}
1900
+
1820
1901
  {charts_section}
1821
1902
  </section>
1822
1903
  """
@@ -1878,7 +1959,7 @@ class HTMLReporter(ReportGenerator):
1878
1959
  </div>
1879
1960
  {inheritance_indicator}
1880
1961
  <div class="progress-container">
1881
- <div class="progress-bar {status_class}" data-width="{control_data['compliance_percentage']}">
1962
+ <div class="progress-bar {status_class}" data-width="{self._get_display_width(control_data['compliance_percentage'])}">
1882
1963
  <span class="progress-text">{control_data['compliance_percentage']:.1f}%</span>
1883
1964
  </div>
1884
1965
  </div>
@@ -2025,6 +2106,34 @@ class HTMLReporter(ReportGenerator):
2025
2106
  "riskDistribution": risk_distribution
2026
2107
  }
2027
2108
 
2109
+ def _prepare_coverage_metrics(self, coverage_metrics: Dict[str, Any]) -> Dict[str, Any]:
2110
+ """Prepare coverage metrics for HTML display.
2111
+
2112
+ Args:
2113
+ coverage_metrics: Coverage metrics from ComplianceSummary
2114
+
2115
+ Returns:
2116
+ Formatted coverage metrics dictionary
2117
+ """
2118
+ formatted_metrics = {}
2119
+
2120
+ for ig_name, metrics in coverage_metrics.items():
2121
+ # Handle both CoverageMetrics objects and dictionaries
2122
+ if hasattr(metrics, '__dict__'):
2123
+ metrics_dict = metrics.__dict__
2124
+ else:
2125
+ metrics_dict = metrics
2126
+
2127
+ formatted_metrics[ig_name] = {
2128
+ 'coverage_percentage': metrics_dict.get('coverage_percentage', 0),
2129
+ 'total_safeguards': metrics_dict.get('total_safeguards', 0),
2130
+ 'covered_safeguards': metrics_dict.get('covered_safeguards', 0),
2131
+ 'implemented_rules': metrics_dict.get('implemented_rules', 0),
2132
+ 'safeguard_details': metrics_dict.get('safeguard_details', {})
2133
+ }
2134
+
2135
+ return formatted_metrics
2136
+
2028
2137
  def _build_navigation_structure(self, html_data: Dict[str, Any]) -> Dict[str, Any]:
2029
2138
  """Build navigation structure for the report.
2030
2139
 
@@ -2079,6 +2188,20 @@ class HTMLReporter(ReportGenerator):
2079
2188
  else:
2080
2189
  return "#e74c3c" # Red
2081
2190
 
2191
+ def _get_display_width(self, actual_percentage: float) -> float:
2192
+ """Calculate display width for progress bars.
2193
+
2194
+ Ensures minimum 5% width for visibility when actual percentage is 0%.
2195
+ This prevents progress bars from being invisible for non-compliant controls.
2196
+
2197
+ Args:
2198
+ actual_percentage: Actual compliance percentage (0-100)
2199
+
2200
+ Returns:
2201
+ Display width with minimum 5% for visibility
2202
+ """
2203
+ return max(5.0, actual_percentage)
2204
+
2082
2205
  def _get_status_class(self, compliance_percentage: float) -> str:
2083
2206
  """Get CSS status class based on compliance percentage."""
2084
2207
  if compliance_percentage >= 95.0:
@@ -2193,9 +2316,9 @@ class HTMLReporter(ReportGenerator):
2193
2316
  have higher impact on the overall score.
2194
2317
  </p>
2195
2318
  <ul class="comparison-features">
2196
- <li>✓ Prioritizes critical security controls</li>
2197
- <li>✓ Prevents resource count skew</li>
2198
- <li>✓ Guides remediation priorities</li>
2319
+ <li>Prioritizes critical security controls</li>
2320
+ <li>Prevents resource count skew</li>
2321
+ <li>Guides remediation priorities</li>
2199
2322
  </ul>
2200
2323
  </div>
2201
2324
 
@@ -2207,15 +2330,69 @@ class HTMLReporter(ReportGenerator):
2207
2330
  All rules treated equally regardless of security criticality.
2208
2331
  </p>
2209
2332
  <ul class="comparison-features">
2210
- <li>✓ Simple and straightforward</li>
2211
- <li>✓ Easy to audit</li>
2212
- <li>✓ Resource-level tracking</li>
2333
+ <li>Simple and straightforward</li>
2334
+ <li>Easy to audit</li>
2335
+ <li>Resource-level tracking</li>
2213
2336
  </ul>
2214
2337
  </div>
2215
2338
  </div>
2216
2339
  </div>
2217
2340
  """
2218
2341
 
2342
+ def _generate_coverage_metrics_section(self, coverage_metrics: Dict[str, Any]) -> str:
2343
+ """Generate CIS Controls coverage metrics section.
2344
+
2345
+ Args:
2346
+ coverage_metrics: Dictionary of coverage metrics by IG
2347
+
2348
+ Returns:
2349
+ HTML section displaying coverage metrics
2350
+ """
2351
+ if not coverage_metrics:
2352
+ return ""
2353
+
2354
+ coverage_cards = ""
2355
+ for ig_name in ['IG1', 'IG2', 'IG3']:
2356
+ metrics = coverage_metrics.get(ig_name)
2357
+ if not metrics:
2358
+ continue
2359
+
2360
+ coverage_pct = metrics.get('coverage_percentage', 0)
2361
+ total_safeguards = metrics.get('total_safeguards', 0)
2362
+ covered_safeguards = metrics.get('covered_safeguards', 0)
2363
+ implemented_rules = metrics.get('implemented_rules', 0)
2364
+ description = metrics.get('safeguard_details', {}).get('coverage_description', '')
2365
+
2366
+ status_class = self._get_status_class(coverage_pct)
2367
+
2368
+ coverage_cards += f"""
2369
+ <div class="coverage-card {status_class}">
2370
+ <h4>{ig_name} Coverage</h4>
2371
+ <div class="coverage-value">{coverage_pct:.1f}%</div>
2372
+ <div class="coverage-details">
2373
+ <p><strong>{covered_safeguards}</strong> of <strong>{total_safeguards}</strong> safeguards covered</p>
2374
+ <p><strong>{implemented_rules}</strong> rules implemented</p>
2375
+ <p class="coverage-description">{description}</p>
2376
+ </div>
2377
+ </div>
2378
+ """
2379
+
2380
+ if not coverage_cards:
2381
+ return ""
2382
+
2383
+ return f"""
2384
+ <div class="coverage-metrics-section">
2385
+ <h3>CIS Controls v8.1 Coverage</h3>
2386
+ <p class="coverage-intro">
2387
+ This assessment implements rules that cover CIS Controls v8.1 safeguards across Implementation Groups.
2388
+ Coverage indicates the percentage of safeguards that have at least one assessment rule.
2389
+ </p>
2390
+ <div class="coverage-grid">
2391
+ {coverage_cards}
2392
+ </div>
2393
+ </div>
2394
+ """
2395
+
2219
2396
  def set_chart_options(self, include_charts: bool = True) -> None:
2220
2397
  """Configure chart inclusion options.
2221
2398
 
@@ -2561,7 +2738,7 @@ class HTMLReporter(ReportGenerator):
2561
2738
  type_compliance = (stats["compliant"] / stats["total"] * 100) if stats["total"] > 0 else 0
2562
2739
  status_class = self._get_status_class(type_compliance)
2563
2740
  # Ensure minimum width of 5% for visibility when compliance is 0%
2564
- display_width = max(type_compliance, 5.0) if type_compliance < 5.0 else type_compliance
2741
+ display_width = self._get_display_width(type_compliance)
2565
2742
 
2566
2743
  resource_type_breakdown += f"""
2567
2744
  <div class="resource-type-stat">