aws-cis-controls-assessment 1.0.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.
Files changed (77) hide show
  1. aws_cis_assessment/__init__.py +11 -0
  2. aws_cis_assessment/cli/__init__.py +3 -0
  3. aws_cis_assessment/cli/examples.py +274 -0
  4. aws_cis_assessment/cli/main.py +1259 -0
  5. aws_cis_assessment/cli/utils.py +356 -0
  6. aws_cis_assessment/config/__init__.py +1 -0
  7. aws_cis_assessment/config/config_loader.py +328 -0
  8. aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
  9. aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
  10. aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
  11. aws_cis_assessment/controls/__init__.py +1 -0
  12. aws_cis_assessment/controls/base_control.py +400 -0
  13. aws_cis_assessment/controls/ig1/__init__.py +239 -0
  14. aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
  15. aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
  16. aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
  17. aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
  18. aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
  19. aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
  20. aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
  21. aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
  22. aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
  23. aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
  24. aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
  25. aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
  26. aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
  27. aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
  28. aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
  29. aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
  30. aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
  31. aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
  32. aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
  33. aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
  34. aws_cis_assessment/controls/ig2/__init__.py +172 -0
  35. aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
  36. aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
  37. aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
  38. aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
  39. aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
  40. aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
  41. aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
  42. aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
  43. aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
  44. aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
  45. aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
  46. aws_cis_assessment/controls/ig3/__init__.py +49 -0
  47. aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
  48. aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
  49. aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
  50. aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
  51. aws_cis_assessment/core/__init__.py +1 -0
  52. aws_cis_assessment/core/accuracy_validator.py +425 -0
  53. aws_cis_assessment/core/assessment_engine.py +1266 -0
  54. aws_cis_assessment/core/audit_trail.py +491 -0
  55. aws_cis_assessment/core/aws_client_factory.py +313 -0
  56. aws_cis_assessment/core/error_handler.py +607 -0
  57. aws_cis_assessment/core/models.py +166 -0
  58. aws_cis_assessment/core/scoring_engine.py +459 -0
  59. aws_cis_assessment/reporters/__init__.py +8 -0
  60. aws_cis_assessment/reporters/base_reporter.py +454 -0
  61. aws_cis_assessment/reporters/csv_reporter.py +835 -0
  62. aws_cis_assessment/reporters/html_reporter.py +2162 -0
  63. aws_cis_assessment/reporters/json_reporter.py +561 -0
  64. aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
  65. aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
  66. aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
  67. aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
  68. aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
  69. aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
  70. docs/README.md +94 -0
  71. docs/assessment-logic.md +766 -0
  72. docs/cli-reference.md +698 -0
  73. docs/config-rule-mappings.md +393 -0
  74. docs/developer-guide.md +858 -0
  75. docs/installation.md +299 -0
  76. docs/troubleshooting.md +634 -0
  77. docs/user-guide.md +487 -0
@@ -0,0 +1,454 @@
1
+ """Base Report Generator for CIS Controls compliance assessment reports."""
2
+
3
+ import logging
4
+ from abc import ABC, abstractmethod
5
+ from typing import Dict, Any, List, Optional
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ from aws_cis_assessment.core.models import (
10
+ AssessmentResult, ComplianceSummary, RemediationGuidance,
11
+ IGScore, ControlScore, ComplianceResult
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ReportGenerator(ABC):
18
+ """Abstract base class for generating compliance assessment reports."""
19
+
20
+ def __init__(self, template_dir: Optional[str] = None):
21
+ """Initialize report generator with optional template directory.
22
+
23
+ Args:
24
+ template_dir: Optional path to custom report templates
25
+ """
26
+ self.template_dir = Path(template_dir) if template_dir else None
27
+ self.report_metadata = {}
28
+ logger.info(f"Initialized {self.__class__.__name__} with template_dir: {template_dir}")
29
+
30
+ @abstractmethod
31
+ def generate_report(self, assessment_result: AssessmentResult,
32
+ compliance_summary: ComplianceSummary,
33
+ output_path: Optional[str] = None) -> str:
34
+ """Generate compliance assessment report.
35
+
36
+ Args:
37
+ assessment_result: Complete assessment result data
38
+ compliance_summary: Executive summary of compliance status
39
+ output_path: Optional path to save the report
40
+
41
+ Returns:
42
+ Generated report content as string
43
+ """
44
+ pass
45
+
46
+ @abstractmethod
47
+ def get_supported_formats(self) -> List[str]:
48
+ """Get list of supported output formats.
49
+
50
+ Returns:
51
+ List of supported format strings (e.g., ['json', 'html', 'csv'])
52
+ """
53
+ pass
54
+
55
+ def set_report_metadata(self, metadata: Dict[str, Any]):
56
+ """Set additional metadata for the report.
57
+
58
+ Args:
59
+ metadata: Dictionary containing report metadata
60
+ """
61
+ self.report_metadata.update(metadata)
62
+ logger.debug(f"Updated report metadata: {metadata}")
63
+
64
+ def get_report_metadata(self) -> Dict[str, Any]:
65
+ """Get current report metadata.
66
+
67
+ Returns:
68
+ Dictionary containing current report metadata
69
+ """
70
+ return self.report_metadata.copy()
71
+
72
+ def _prepare_report_data(self, assessment_result: AssessmentResult,
73
+ compliance_summary: ComplianceSummary) -> Dict[str, Any]:
74
+ """Prepare standardized data structure for report generation.
75
+
76
+ Args:
77
+ assessment_result: Complete assessment result data
78
+ compliance_summary: Executive summary of compliance status
79
+
80
+ Returns:
81
+ Dictionary containing standardized report data
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
+
89
+ total_compliant = sum(
90
+ sum(control.compliant_resources for control in ig.control_scores.values())
91
+ for ig in assessment_result.ig_scores.values()
92
+ )
93
+
94
+ 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()
98
+ )
99
+
100
+ # Prepare standardized data structure
101
+ report_data = {
102
+ 'metadata': {
103
+ 'report_generated_at': datetime.now().isoformat(),
104
+ 'assessment_timestamp': assessment_result.timestamp.isoformat(),
105
+ 'account_id': assessment_result.account_id,
106
+ 'regions_assessed': assessment_result.regions_assessed,
107
+ 'assessment_duration': str(assessment_result.assessment_duration) if assessment_result.assessment_duration else None,
108
+ 'total_resources_evaluated': assessment_result.total_resources_evaluated,
109
+ **self.report_metadata
110
+ },
111
+ 'executive_summary': {
112
+ 'overall_compliance_percentage': compliance_summary.overall_compliance_percentage,
113
+ 'ig1_compliance_percentage': compliance_summary.ig1_compliance_percentage,
114
+ 'ig2_compliance_percentage': compliance_summary.ig2_compliance_percentage,
115
+ 'ig3_compliance_percentage': compliance_summary.ig3_compliance_percentage,
116
+ 'total_resources': total_resources,
117
+ 'compliant_resources': total_compliant,
118
+ 'non_compliant_resources': total_non_compliant,
119
+ 'top_risk_areas': compliance_summary.top_risk_areas,
120
+ 'compliance_trend': compliance_summary.compliance_trend
121
+ },
122
+ 'implementation_groups': self._prepare_ig_data(assessment_result.ig_scores),
123
+ 'remediation_priorities': self._prepare_remediation_data(compliance_summary.remediation_priorities),
124
+ 'detailed_findings': self._prepare_findings_data(assessment_result.ig_scores)
125
+ }
126
+
127
+ return report_data
128
+
129
+ def _prepare_ig_data(self, ig_scores: Dict[str, IGScore]) -> Dict[str, Any]:
130
+ """Prepare Implementation Group data for reporting.
131
+
132
+ Args:
133
+ ig_scores: Dictionary of IG scores
134
+
135
+ Returns:
136
+ Dictionary containing IG data structured for reporting
137
+ """
138
+ ig_data = {}
139
+
140
+ for ig_name, ig_score in ig_scores.items():
141
+ ig_data[ig_name] = {
142
+ 'implementation_group': ig_score.implementation_group,
143
+ 'total_controls': ig_score.total_controls,
144
+ 'compliant_controls': ig_score.compliant_controls,
145
+ 'compliance_percentage': ig_score.compliance_percentage,
146
+ 'controls': self._prepare_control_data(ig_score.control_scores)
147
+ }
148
+
149
+ return ig_data
150
+
151
+ def _prepare_control_data(self, control_scores: Dict[str, ControlScore]) -> Dict[str, Any]:
152
+ """Prepare Control data for reporting.
153
+
154
+ Args:
155
+ control_scores: Dictionary of control scores
156
+
157
+ Returns:
158
+ Dictionary containing control data structured for reporting
159
+ """
160
+ control_data = {}
161
+
162
+ for control_id, control_score in control_scores.items():
163
+ control_data[control_id] = {
164
+ 'control_id': control_score.control_id,
165
+ 'title': control_score.title,
166
+ 'implementation_group': control_score.implementation_group,
167
+ 'total_resources': control_score.total_resources,
168
+ 'compliant_resources': control_score.compliant_resources,
169
+ 'compliance_percentage': control_score.compliance_percentage,
170
+ 'config_rules_evaluated': control_score.config_rules_evaluated,
171
+ 'findings_count': len(control_score.findings),
172
+ 'non_compliant_findings': [
173
+ self._prepare_finding_data(finding)
174
+ for finding in control_score.findings
175
+ if finding.compliance_status.value == 'NON_COMPLIANT'
176
+ ],
177
+ 'compliant_findings': [
178
+ self._prepare_finding_data(finding)
179
+ for finding in control_score.findings
180
+ if finding.compliance_status.value == 'COMPLIANT'
181
+ ]
182
+ }
183
+
184
+ return control_data
185
+
186
+ def _prepare_finding_data(self, finding: ComplianceResult) -> Dict[str, Any]:
187
+ """Prepare individual finding data for reporting.
188
+
189
+ Args:
190
+ finding: ComplianceResult object
191
+
192
+ Returns:
193
+ Dictionary containing finding data structured for reporting
194
+ """
195
+ return {
196
+ 'resource_id': finding.resource_id,
197
+ 'resource_type': finding.resource_type,
198
+ 'compliance_status': finding.compliance_status.value,
199
+ 'evaluation_reason': finding.evaluation_reason,
200
+ 'config_rule_name': finding.config_rule_name,
201
+ 'region': finding.region,
202
+ 'timestamp': finding.timestamp.isoformat(),
203
+ 'remediation_guidance': finding.remediation_guidance
204
+ }
205
+
206
+ def _prepare_remediation_data(self, remediation_priorities: List[RemediationGuidance]) -> List[Dict[str, Any]]:
207
+ """Prepare remediation guidance data for reporting.
208
+
209
+ Args:
210
+ remediation_priorities: List of RemediationGuidance objects
211
+
212
+ Returns:
213
+ List of dictionaries containing remediation data structured for reporting
214
+ """
215
+ remediation_data = []
216
+
217
+ for guidance in remediation_priorities:
218
+ remediation_data.append({
219
+ 'config_rule_name': guidance.config_rule_name,
220
+ 'control_id': guidance.control_id,
221
+ 'priority': guidance.priority,
222
+ 'estimated_effort': guidance.estimated_effort,
223
+ 'remediation_steps': guidance.remediation_steps,
224
+ 'aws_documentation_link': guidance.aws_documentation_link
225
+ })
226
+
227
+ return remediation_data
228
+
229
+ def _prepare_findings_data(self, ig_scores: Dict[str, IGScore]) -> Dict[str, Any]:
230
+ """Prepare detailed findings data for reporting.
231
+
232
+ Args:
233
+ ig_scores: Dictionary of IG scores
234
+
235
+ Returns:
236
+ Dictionary containing findings data structured for reporting
237
+ """
238
+ findings_data = {}
239
+
240
+ for ig_name, ig_score in ig_scores.items():
241
+ ig_findings = {}
242
+ for control_id, control_score in ig_score.control_scores.items():
243
+ control_findings = []
244
+ for finding in control_score.findings:
245
+ control_findings.append(self._prepare_finding_data(finding))
246
+ ig_findings[control_id] = control_findings
247
+ findings_data[ig_name] = ig_findings
248
+
249
+ return findings_data
250
+
251
+ def _validate_report_data(self, report_data: Dict[str, Any]) -> bool:
252
+ """Validate report data structure for consistency.
253
+
254
+ Args:
255
+ report_data: Prepared report data dictionary
256
+
257
+ Returns:
258
+ True if data is valid, False otherwise
259
+ """
260
+ required_sections = ['metadata', 'executive_summary', 'implementation_groups',
261
+ 'remediation_priorities', 'detailed_findings']
262
+
263
+ for section in required_sections:
264
+ if section not in report_data:
265
+ logger.error(f"Missing required section in report data: {section}")
266
+ return False
267
+
268
+ # Validate metadata section
269
+ metadata = report_data['metadata']
270
+ required_metadata = ['report_generated_at', 'assessment_timestamp', 'account_id']
271
+ for field in required_metadata:
272
+ if field not in metadata:
273
+ logger.error(f"Missing required metadata field: {field}")
274
+ return False
275
+
276
+ # Validate executive summary
277
+ summary = report_data['executive_summary']
278
+ required_summary_fields = ['overall_compliance_percentage', 'total_resources']
279
+ for field in required_summary_fields:
280
+ if field not in summary:
281
+ logger.error(f"Missing required executive summary field: {field}")
282
+ return False
283
+
284
+ logger.debug("Report data validation passed")
285
+ return True
286
+
287
+ def _save_report_to_file(self, content: str, output_path: str) -> bool:
288
+ """Save report content to file.
289
+
290
+ Args:
291
+ content: Report content to save
292
+ output_path: Path where to save the report
293
+
294
+ Returns:
295
+ True if saved successfully, False otherwise
296
+ """
297
+ try:
298
+ output_file = Path(output_path)
299
+ output_file.parent.mkdir(parents=True, exist_ok=True)
300
+
301
+ with open(output_file, 'w', encoding='utf-8') as f:
302
+ f.write(content)
303
+
304
+ logger.info(f"Report saved to: {output_path}")
305
+ return True
306
+
307
+ except Exception as e:
308
+ logger.error(f"Failed to save report to {output_path}: {e}")
309
+ return False
310
+
311
+ def validate_assessment_data(self, assessment_result: AssessmentResult,
312
+ compliance_summary: ComplianceSummary) -> bool:
313
+ """Validate input assessment data before report generation.
314
+
315
+ Args:
316
+ assessment_result: Assessment result to validate
317
+ compliance_summary: Compliance summary to validate
318
+
319
+ Returns:
320
+ True if data is valid, False otherwise
321
+ """
322
+ if not assessment_result.account_id:
323
+ logger.error("Assessment result missing account_id")
324
+ return False
325
+
326
+ if not assessment_result.regions_assessed:
327
+ logger.error("Assessment result missing regions_assessed")
328
+ return False
329
+
330
+ if not assessment_result.ig_scores:
331
+ logger.error("Assessment result missing ig_scores")
332
+ return False
333
+
334
+ # Validate compliance summary
335
+ if compliance_summary.overall_compliance_percentage < 0 or compliance_summary.overall_compliance_percentage > 100:
336
+ logger.error(f"Invalid overall compliance percentage: {compliance_summary.overall_compliance_percentage}")
337
+ return False
338
+
339
+ logger.debug("Assessment data validation passed")
340
+ return True
341
+
342
+
343
+ class ReportTemplateEngine:
344
+ """Template engine for generating formatted reports."""
345
+
346
+ def __init__(self, template_dir: Optional[str] = None):
347
+ """Initialize template engine.
348
+
349
+ Args:
350
+ template_dir: Optional path to custom templates
351
+ """
352
+ self.template_dir = Path(template_dir) if template_dir else None
353
+ self.templates = {}
354
+ logger.info(f"Initialized ReportTemplateEngine with template_dir: {template_dir}")
355
+
356
+ def load_template(self, template_name: str) -> str:
357
+ """Load template content from file or built-in templates.
358
+
359
+ Args:
360
+ template_name: Name of the template to load
361
+
362
+ Returns:
363
+ Template content as string
364
+ """
365
+ # Check for custom template first
366
+ if self.template_dir:
367
+ custom_template_path = self.template_dir / f"{template_name}.template"
368
+ if custom_template_path.exists():
369
+ with open(custom_template_path, 'r', encoding='utf-8') as f:
370
+ template_content = f.read()
371
+ logger.debug(f"Loaded custom template: {template_name}")
372
+ return template_content
373
+
374
+ # Fall back to built-in templates
375
+ built_in_template = self._get_builtin_template(template_name)
376
+ if built_in_template:
377
+ logger.debug(f"Using built-in template: {template_name}")
378
+ return built_in_template
379
+
380
+ logger.warning(f"Template not found: {template_name}")
381
+ return ""
382
+
383
+ def _get_builtin_template(self, template_name: str) -> Optional[str]:
384
+ """Get built-in template content.
385
+
386
+ Args:
387
+ template_name: Name of the built-in template
388
+
389
+ Returns:
390
+ Template content or None if not found
391
+ """
392
+ builtin_templates = {
393
+ 'executive_summary': """
394
+ # Executive Summary
395
+
396
+ **Overall Compliance:** {overall_compliance_percentage:.1f}%
397
+ **Assessment Date:** {assessment_timestamp}
398
+ **AWS Account:** {account_id}
399
+
400
+ ## Implementation Group Compliance
401
+ - **IG1 (Essential Cyber Hygiene):** {ig1_compliance_percentage:.1f}%
402
+ - **IG2 (Enhanced Security):** {ig2_compliance_percentage:.1f}%
403
+ - **IG3 (Advanced Security):** {ig3_compliance_percentage:.1f}%
404
+
405
+ ## Resource Summary
406
+ - **Total Resources Evaluated:** {total_resources}
407
+ - **Compliant Resources:** {compliant_resources}
408
+ - **Non-Compliant Resources:** {non_compliant_resources}
409
+
410
+ ## Top Risk Areas
411
+ {top_risk_areas}
412
+ """,
413
+ 'control_detail': """
414
+ ## Control {control_id}: {title}
415
+
416
+ **Implementation Group:** {implementation_group}
417
+ **Compliance:** {compliant_resources}/{total_resources} ({compliance_percentage:.1f}%)
418
+ **Config Rules:** {config_rules_evaluated}
419
+
420
+ ### Non-Compliant Resources
421
+ {non_compliant_findings}
422
+ """,
423
+ 'remediation_guidance': """
424
+ # Remediation Priorities
425
+
426
+ {remediation_items}
427
+ """
428
+ }
429
+
430
+ return builtin_templates.get(template_name)
431
+
432
+ def render_template(self, template_content: str, data: Dict[str, Any]) -> str:
433
+ """Render template with provided data.
434
+
435
+ Args:
436
+ template_content: Template content with placeholders
437
+ data: Data dictionary for template substitution
438
+
439
+ Returns:
440
+ Rendered template content
441
+ """
442
+ try:
443
+ # Simple template rendering using string formatting
444
+ # For more complex templating, could integrate Jinja2 or similar
445
+ rendered_content = template_content.format(**data)
446
+ logger.debug("Template rendered successfully")
447
+ return rendered_content
448
+
449
+ except KeyError as e:
450
+ logger.error(f"Template rendering failed - missing key: {e}")
451
+ return template_content
452
+ except Exception as e:
453
+ logger.error(f"Template rendering failed: {e}")
454
+ return template_content