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,1259 @@
1
+ """Main CLI entry point for AWS CIS Controls compliance assessment tool."""
2
+
3
+ import os
4
+ import sys
5
+ import json
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Optional, List, Dict, Any
9
+ from datetime import datetime
10
+
11
+ import click
12
+ from tabulate import tabulate
13
+
14
+ from aws_cis_assessment.core.assessment_engine import AssessmentEngine, AssessmentProgress
15
+ from aws_cis_assessment.core.scoring_engine import ScoringEngine
16
+ from aws_cis_assessment.config.config_loader import ConfigRuleLoader
17
+ from aws_cis_assessment.reporters.json_reporter import JSONReporter
18
+ from aws_cis_assessment.reporters.html_reporter import HTMLReporter
19
+ from aws_cis_assessment.reporters.csv_reporter import CSVReporter
20
+ from aws_cis_assessment.core.models import ImplementationGroup
21
+ from aws_cis_assessment.cli.utils import (
22
+ get_default_regions, validate_output_format, format_duration,
23
+ colorize_compliance_status, is_tty
24
+ )
25
+ from aws_cis_assessment.cli.examples import (
26
+ get_all_examples, get_troubleshooting_guide, get_best_practices
27
+ )
28
+ from aws_cis_assessment import __version__
29
+
30
+
31
+ def _parse_output_formats(ctx, param, value):
32
+ """Parse output formats, handling both multiple flags and comma-separated values."""
33
+ if not value:
34
+ return ['json'] # default
35
+
36
+ valid_formats = ['json', 'html', 'csv']
37
+ formats = []
38
+
39
+ # Handle comma-separated values
40
+ if isinstance(value, str):
41
+ if ',' in value:
42
+ formats = [fmt.strip().lower() for fmt in value.split(',')]
43
+ else:
44
+ formats = [value.lower()]
45
+ else:
46
+ formats = [value.lower()]
47
+
48
+ # Validate formats
49
+ invalid_formats = [fmt for fmt in formats if fmt not in valid_formats]
50
+ if invalid_formats:
51
+ raise click.BadParameter(f"Invalid format(s): {', '.join(invalid_formats)}. Valid formats: {', '.join(valid_formats)}")
52
+
53
+ # Remove duplicates while preserving order
54
+ seen = set()
55
+ unique_formats = []
56
+ for fmt in formats:
57
+ if fmt not in seen:
58
+ seen.add(fmt)
59
+ unique_formats.append(fmt)
60
+
61
+ return unique_formats
62
+
63
+
64
+ # Configure logging
65
+ logging.basicConfig(
66
+ level=logging.INFO,
67
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
68
+ )
69
+ logger = logging.getLogger(__name__)
70
+
71
+
72
+ class ProgressDisplay:
73
+ """Display progress updates during assessment."""
74
+
75
+ def __init__(self, verbose: bool = False):
76
+ self.verbose = verbose
77
+ self.last_update = None
78
+
79
+ def update_progress(self, progress: AssessmentProgress):
80
+ """Update progress display."""
81
+ if not self.verbose and progress.progress_percentage == self.last_update:
82
+ return
83
+
84
+ # Clear previous line if not verbose
85
+ if not self.verbose:
86
+ click.echo('\r', nl=False)
87
+
88
+ # Format progress message
89
+ if progress.current_control and progress.current_region:
90
+ status_msg = f"Assessing {progress.current_control} in {progress.current_region}"
91
+ else:
92
+ status_msg = "Initializing assessment..."
93
+
94
+ progress_bar = self._create_progress_bar(progress.progress_percentage)
95
+
96
+ if self.verbose:
97
+ click.echo(f"[{progress_bar}] {progress.progress_percentage:.1f}% - {status_msg}")
98
+ else:
99
+ click.echo(f"[{progress_bar}] {progress.progress_percentage:.1f}% - {status_msg}", nl=False)
100
+
101
+ self.last_update = progress.progress_percentage
102
+
103
+ # Show errors if any
104
+ if progress.errors and self.verbose:
105
+ for error in progress.errors[-3:]: # Show last 3 errors
106
+ click.echo(f" ⚠️ {error}", err=True)
107
+
108
+ def _create_progress_bar(self, percentage: float, width: int = 20) -> str:
109
+ """Create a text-based progress bar."""
110
+ filled = int(width * percentage / 100)
111
+ bar = '█' * filled + '░' * (width - filled)
112
+ return bar
113
+
114
+ def finish(self):
115
+ """Finish progress display."""
116
+ if not self.verbose:
117
+ click.echo() # New line after progress
118
+
119
+
120
+ @click.group()
121
+ @click.version_option(version=__version__, prog_name="aws-cis-assess")
122
+ @click.option('--verbose', '-v', is_flag=True, help='Enable verbose output')
123
+ @click.option('--debug', is_flag=True, help='Enable debug logging')
124
+ @click.pass_context
125
+ def cli(ctx, verbose, debug):
126
+ """AWS CIS Controls Compliance Assessment Tool
127
+
128
+ Evaluate AWS account security posture against CIS Controls Implementation Groups
129
+ (IG1, IG2, IG3) and generate comprehensive compliance reports.
130
+
131
+ Commands:
132
+
133
+ assess Run compliance assessment
134
+ list-controls List available CIS Controls
135
+ list-regions List available AWS regions
136
+ show-stats Show assessment statistics
137
+ validate-config Validate configuration files
138
+ validate-credentials Test AWS credentials
139
+ help-guide Show detailed help and examples
140
+
141
+ Examples:
142
+
143
+ # Run full assessment with HTML report
144
+ aws-cis-assess assess --output-format html --output-file report.html
145
+
146
+ # Assess only IG1 controls in specific regions
147
+ aws-cis-assess assess --implementation-groups IG1 --regions us-east-1,us-west-2
148
+
149
+ # Assess specific controls with detailed logging
150
+ aws-cis-assess assess --controls 1.1,3.3,4.1 --log-level DEBUG
151
+
152
+ # Generate multiple output formats in custom directory
153
+ aws-cis-assess assess --output-format json,html,csv --output-dir ./reports/
154
+
155
+ # Use custom AWS profile and configuration
156
+ aws-cis-assess assess --aws-profile prod --config-path ./custom-config/
157
+ """
158
+ ctx.ensure_object(dict)
159
+ ctx.obj['verbose'] = verbose
160
+
161
+ if debug:
162
+ logging.getLogger().setLevel(logging.DEBUG)
163
+ logging.getLogger('aws_cis_assessment').setLevel(logging.DEBUG)
164
+ elif verbose:
165
+ logging.getLogger().setLevel(logging.INFO)
166
+ else:
167
+ logging.getLogger().setLevel(logging.WARNING)
168
+
169
+
170
+ @cli.command()
171
+ @click.option('--implementation-groups', '-ig',
172
+ type=click.Choice(['IG1', 'IG2', 'IG3'], case_sensitive=False),
173
+ multiple=True,
174
+ help='Implementation Groups to assess (can specify multiple)')
175
+ @click.option('--controls', '-ctrl',
176
+ help='Comma-separated list of specific CIS Control IDs to assess (e.g., 1.1,3.3,4.1)')
177
+ @click.option('--exclude-controls',
178
+ help='Comma-separated list of CIS Control IDs to exclude from assessment')
179
+ @click.option('--regions', '-r',
180
+ help='Comma-separated list of AWS regions (default: us-east-1)')
181
+ @click.option('--exclude-regions',
182
+ help='Comma-separated list of AWS regions to exclude from assessment')
183
+ @click.option('--aws-profile', '-p',
184
+ help='AWS profile to use for credentials')
185
+ @click.option('--aws-access-key-id',
186
+ help='AWS Access Key ID (alternative to profile)')
187
+ @click.option('--aws-secret-access-key',
188
+ help='AWS Secret Access Key (alternative to profile)')
189
+ @click.option('--aws-session-token',
190
+ help='AWS Session Token (for temporary credentials)')
191
+ @click.option('--config-path', '-c',
192
+ type=click.Path(exists=True, file_okay=False, dir_okay=True),
193
+ help='Path to CIS Controls configuration directory')
194
+ @click.option('--output-format', '-f',
195
+ default='json',
196
+ callback=_parse_output_formats,
197
+ help='Output format(s) for the report (comma-separated or multiple flags: json,html,csv or -f json -f html)')
198
+ @click.option('--output-file', '-o',
199
+ help='Output file path (extension added based on format)')
200
+ @click.option('--output-dir',
201
+ type=click.Path(file_okay=False, dir_okay=True),
202
+ help='Output directory for generated reports')
203
+ @click.option('--max-workers', '-w',
204
+ type=int,
205
+ default=4,
206
+ help='Maximum number of parallel workers for assessment')
207
+ @click.option('--timeout',
208
+ type=int,
209
+ default=3600,
210
+ help='Assessment timeout in seconds (default: 3600)')
211
+ @click.option('--enable-error-recovery/--disable-error-recovery',
212
+ default=True,
213
+ help='Enable/disable error recovery mechanisms')
214
+ @click.option('--enable-audit-trail/--disable-audit-trail',
215
+ default=True,
216
+ help='Enable/disable audit trail logging')
217
+ @click.option('--dry-run',
218
+ is_flag=True,
219
+ help='Validate configuration and credentials without running assessment')
220
+ @click.option('--quiet', '-q',
221
+ is_flag=True,
222
+ help='Suppress progress output (only show final results)')
223
+ @click.option('--log-level',
224
+ type=click.Choice(['DEBUG', 'INFO', 'WARNING', 'ERROR'], case_sensitive=False),
225
+ help='Set logging level (overrides --verbose and --debug)')
226
+ @click.option('--log-file',
227
+ type=click.Path(),
228
+ help='Write logs to specified file in addition to console')
229
+ @click.pass_context
230
+ def assess(ctx, implementation_groups, controls, exclude_controls, regions, exclude_regions,
231
+ aws_profile, aws_access_key_id, aws_secret_access_key, aws_session_token,
232
+ config_path, output_format, output_file, output_dir, max_workers, timeout,
233
+ enable_error_recovery, enable_audit_trail, dry_run, quiet, log_level, log_file):
234
+ """Run CIS Controls compliance assessment.
235
+
236
+ This command evaluates your AWS account against CIS Controls Implementation Groups
237
+ and generates comprehensive compliance reports.
238
+
239
+ Examples:
240
+
241
+ # Full assessment with default settings
242
+ aws-cis-assess assess
243
+
244
+ # Assess only essential controls (IG1) in specific regions
245
+ aws-cis-assess assess -ig IG1 -r us-east-1,us-west-2
246
+
247
+ # Assess specific controls across all regions
248
+ aws-cis-assess assess --controls 1.1,3.3,4.1
249
+
250
+ # Exclude certain regions from assessment
251
+ aws-cis-assess assess --exclude-regions us-gov-east-1,us-gov-west-1
252
+
253
+ # Generate HTML report with custom output directory
254
+ aws-cis-assess assess -f html --output-dir ./reports/
255
+
256
+ # Use specific AWS profile with custom configuration and logging
257
+ aws-cis-assess assess -p production -c ./config/ --log-level DEBUG --log-file assessment.log
258
+ """
259
+ verbose = ctx.obj.get('verbose', False)
260
+
261
+ try:
262
+ # Configure logging based on options
263
+ _configure_logging(verbose, ctx.obj.get('debug', False), log_level, log_file)
264
+
265
+ # Parse regions and handle exclusions
266
+ region_list = _parse_regions(regions, exclude_regions)
267
+
268
+ # Parse implementation groups
269
+ ig_list = list(implementation_groups) if implementation_groups else None
270
+
271
+ # Parse controls and exclusions
272
+ controls_list = _parse_controls(controls)
273
+ exclude_controls_list = _parse_controls(exclude_controls)
274
+
275
+ # Validate control selections
276
+ if controls_list and ig_list:
277
+ click.echo("⚠️ Warning: Both --controls and --implementation-groups specified. Controls will take precedence.", err=True)
278
+
279
+ # Prepare AWS credentials
280
+ aws_credentials = _prepare_aws_credentials(
281
+ aws_profile, aws_access_key_id, aws_secret_access_key, aws_session_token
282
+ )
283
+
284
+ # Set up output directory
285
+ if output_dir:
286
+ output_base_path = Path(output_dir)
287
+ output_base_path.mkdir(parents=True, exist_ok=True)
288
+ if not output_file:
289
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
290
+ output_file = str(output_base_path / f"cis_assessment_{timestamp}")
291
+
292
+ # Initialize progress display (suppress if quiet mode)
293
+ progress_display = ProgressDisplay(verbose=verbose and not quiet)
294
+
295
+ # Initialize assessment engine
296
+ if not quiet:
297
+ click.echo("🔧 Initializing assessment engine...")
298
+
299
+ engine = AssessmentEngine(
300
+ aws_credentials=aws_credentials,
301
+ regions=region_list,
302
+ config_path=config_path,
303
+ max_workers=max_workers,
304
+ progress_callback=progress_display.update_progress if not quiet else None,
305
+ enable_error_recovery=enable_error_recovery,
306
+ enable_audit_trail=enable_audit_trail,
307
+ timeout=timeout
308
+ )
309
+
310
+ # Validate configuration
311
+ if not quiet:
312
+ click.echo("✅ Validating configuration...")
313
+ validation_errors = engine.validate_configuration()
314
+ if validation_errors:
315
+ click.echo("❌ Configuration validation failed:", err=True)
316
+ for error in validation_errors:
317
+ click.echo(f" • {error}", err=True)
318
+ sys.exit(1)
319
+
320
+ # Show assessment summary
321
+ summary = engine.get_assessment_summary(
322
+ implementation_groups=ig_list,
323
+ controls=controls_list,
324
+ exclude_controls=exclude_controls_list
325
+ )
326
+ if not quiet:
327
+ _display_assessment_summary(summary, verbose)
328
+
329
+ if dry_run:
330
+ click.echo("✅ Dry run completed successfully. Configuration is valid.")
331
+ return
332
+
333
+ # Run assessment
334
+ if not quiet:
335
+ click.echo("🚀 Starting compliance assessment...")
336
+ click.echo(f" Implementation Groups: {ig_list or summary.get('implementation_groups', ['IG1', 'IG2', 'IG3'])}")
337
+ if controls_list:
338
+ click.echo(f" Specific Controls: {controls_list}")
339
+ if exclude_controls_list:
340
+ click.echo(f" Excluded Controls: {exclude_controls_list}")
341
+ click.echo(f" Regions: {summary['regions']}")
342
+ click.echo(f" Total Assessments: {summary['total_assessments']}")
343
+ click.echo()
344
+
345
+ assessment_result = engine.run_assessment(
346
+ implementation_groups=ig_list,
347
+ controls=controls_list,
348
+ exclude_controls=exclude_controls_list
349
+ )
350
+
351
+ if not quiet:
352
+ progress_display.finish()
353
+
354
+ # Generate compliance summary
355
+ scoring_engine = engine.get_scoring_engine()
356
+ compliance_summary = scoring_engine.generate_compliance_summary(assessment_result)
357
+
358
+ # Display results summary
359
+ _display_results_summary(assessment_result, compliance_summary, verbose and not quiet)
360
+
361
+ # Generate reports
362
+ _generate_reports(assessment_result, compliance_summary, output_format, output_file, verbose and not quiet)
363
+
364
+ # Show error summary if available
365
+ error_summary = engine.get_error_summary()
366
+ if error_summary and verbose and not quiet:
367
+ _display_error_summary(error_summary)
368
+
369
+ if not quiet:
370
+ click.echo("✅ Assessment completed successfully!")
371
+
372
+ # Show final summary with color coding if terminal supports it
373
+ if is_tty() and not quiet:
374
+ overall_pct = compliance_summary.overall_compliance_percentage
375
+ colored_status = colorize_compliance_status(f"{overall_pct:.1f}%", overall_pct)
376
+ click.echo(f"\n🎯 Final Result: {colored_status} overall compliance")
377
+
378
+ except KeyboardInterrupt:
379
+ click.echo("\n⚠️ Assessment interrupted by user", err=True)
380
+ sys.exit(1)
381
+ except Exception as e:
382
+ click.echo(f"❌ Assessment failed: {str(e)}", err=True)
383
+ if verbose:
384
+ import traceback
385
+ click.echo(traceback.format_exc(), err=True)
386
+ sys.exit(1)
387
+
388
+
389
+ @cli.command()
390
+ @click.option('--aws-profile', '-p',
391
+ help='AWS profile to use for credentials')
392
+ @click.option('--output-format', '-f',
393
+ type=click.Choice(['table', 'json'], case_sensitive=False),
394
+ default='table',
395
+ help='Output format for the region list')
396
+ @click.pass_context
397
+ def list_regions(ctx, aws_profile, output_format):
398
+ """List available AWS regions.
399
+
400
+ This command displays all AWS regions that can be used for assessment,
401
+ showing which regions are enabled for your account.
402
+
403
+ Examples:
404
+
405
+ # List regions in table format
406
+ aws-cis-assess list-regions
407
+
408
+ # List regions in JSON format
409
+ aws-cis-assess list-regions -f json
410
+
411
+ # Use specific AWS profile
412
+ aws-cis-assess list-regions -p production
413
+ """
414
+ verbose = ctx.obj.get('verbose', False)
415
+
416
+ try:
417
+ from aws_cis_assessment.cli.utils import get_all_enabled_regions, get_default_regions
418
+
419
+ # Get regions
420
+ if aws_profile:
421
+ # Use specific profile to get enabled regions
422
+ aws_credentials = _prepare_aws_credentials(aws_profile, None, None, None)
423
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
424
+ aws_factory = AWSClientFactory(aws_credentials, None)
425
+ enabled_regions = aws_factory.get_enabled_regions()
426
+ else:
427
+ enabled_regions = get_all_enabled_regions()
428
+
429
+ default_regions = get_default_regions()
430
+
431
+ if output_format == 'json':
432
+ # JSON output
433
+ regions_data = {
434
+ 'enabled_regions': enabled_regions,
435
+ 'default_regions': default_regions,
436
+ 'total_enabled': len(enabled_regions),
437
+ 'total_default': len(default_regions)
438
+ }
439
+ click.echo(json.dumps(regions_data, indent=2))
440
+ else:
441
+ # Table output
442
+ click.echo("📍 Available AWS Regions")
443
+ click.echo("=" * 50)
444
+
445
+ table_data = []
446
+ for region in sorted(enabled_regions):
447
+ is_default = "✓" if region in default_regions else ""
448
+ table_data.append([region, is_default])
449
+
450
+ headers = ['Region', 'Default']
451
+ from tabulate import tabulate
452
+ click.echo(tabulate(table_data, headers=headers, tablefmt='grid'))
453
+
454
+ click.echo(f"\nTotal enabled regions: {len(enabled_regions)}")
455
+ click.echo(f"Default regions for assessment: {len(default_regions)}")
456
+
457
+ except Exception as e:
458
+ click.echo(f"❌ Failed to list regions: {str(e)}", err=True)
459
+ if verbose:
460
+ import traceback
461
+ click.echo(traceback.format_exc(), err=True)
462
+ sys.exit(1)
463
+
464
+
465
+ @cli.command()
466
+ @click.option('--config-path', '-c',
467
+ type=click.Path(exists=True, file_okay=False, dir_okay=True),
468
+ help='Path to CIS Controls configuration directory')
469
+ @click.option('--implementation-groups', '-ig',
470
+ type=click.Choice(['IG1', 'IG2', 'IG3'], case_sensitive=False),
471
+ multiple=True,
472
+ help='Implementation Groups to analyze (can specify multiple)')
473
+ @click.option('--controls',
474
+ help='Comma-separated list of specific CIS Control IDs to analyze')
475
+ @click.option('--regions', '-r',
476
+ help='Comma-separated list of AWS regions to analyze')
477
+ @click.option('--output-format', '-f',
478
+ type=click.Choice(['table', 'json'], case_sensitive=False),
479
+ default='table',
480
+ help='Output format for the statistics')
481
+ @click.pass_context
482
+ def show_stats(ctx, config_path, implementation_groups, controls, regions, output_format):
483
+ """Show assessment statistics and scope.
484
+
485
+ This command displays detailed statistics about what would be assessed
486
+ based on the specified criteria, without actually running the assessment.
487
+
488
+ Examples:
489
+
490
+ # Show statistics for all controls
491
+ aws-cis-assess show-stats
492
+
493
+ # Show statistics for specific Implementation Groups
494
+ aws-cis-assess show-stats -ig IG1,IG2
495
+
496
+ # Show statistics for specific controls
497
+ aws-cis-assess show-stats --controls 1.1,3.3,4.1
498
+
499
+ # Show statistics in JSON format
500
+ aws-cis-assess show-stats -f json
501
+ """
502
+ verbose = ctx.obj.get('verbose', False)
503
+
504
+ try:
505
+ # Initialize config loader
506
+ from aws_cis_assessment.config.config_loader import ConfigRuleLoader
507
+ config_loader = ConfigRuleLoader(config_path)
508
+
509
+ # Parse options
510
+ ig_list = list(implementation_groups) if implementation_groups else None
511
+ controls_list = _parse_controls(controls)
512
+ region_list = _parse_regions(regions, None)
513
+
514
+ # Get statistics
515
+ stats = config_loader.get_assessment_statistics(
516
+ implementation_groups=ig_list,
517
+ controls=controls_list,
518
+ regions=region_list
519
+ )
520
+
521
+ if output_format == 'json':
522
+ click.echo(json.dumps(stats, indent=2))
523
+ else:
524
+ _display_assessment_statistics(stats, verbose)
525
+
526
+ except Exception as e:
527
+ click.echo(f"❌ Failed to show statistics: {str(e)}", err=True)
528
+ if verbose:
529
+ import traceback
530
+ click.echo(traceback.format_exc(), err=True)
531
+ sys.exit(1)
532
+
533
+
534
+ def _display_assessment_statistics(stats: Dict[str, Any], verbose: bool):
535
+ """Display assessment statistics in table format."""
536
+ click.echo("📊 Assessment Statistics")
537
+ click.echo("=" * 50)
538
+
539
+ # Overall statistics
540
+ click.echo(f"Total Controls: {stats.get('total_controls', 0)}")
541
+ click.echo(f"Total Config Rules: {stats.get('total_config_rules', 0)}")
542
+ click.echo(f"Total Regions: {stats.get('total_regions', 0)}")
543
+ click.echo(f"Estimated Assessments: {stats.get('estimated_assessments', 0)}")
544
+
545
+ # By Implementation Group
546
+ if 'by_implementation_group' in stats:
547
+ click.echo("\nBy Implementation Group:")
548
+ for ig, ig_stats in stats['by_implementation_group'].items():
549
+ click.echo(f" {ig}: {ig_stats.get('controls', 0)} controls, {ig_stats.get('config_rules', 0)} rules")
550
+
551
+ # By service
552
+ if 'by_service' in stats and verbose:
553
+ click.echo("\nBy AWS Service:")
554
+ for service, count in sorted(stats['by_service'].items()):
555
+ click.echo(f" {service}: {count} assessments")
556
+
557
+ # Resource types
558
+ if 'resource_types' in stats and verbose:
559
+ click.echo(f"\nResource Types: {len(stats['resource_types'])}")
560
+ for resource_type in sorted(stats['resource_types'])[:10]: # Show first 10
561
+ click.echo(f" • {resource_type}")
562
+ if len(stats['resource_types']) > 10:
563
+ click.echo(f" ... and {len(stats['resource_types']) - 10} more")
564
+
565
+
566
+ @cli.command()
567
+ @click.option('--config-path', '-c',
568
+ type=click.Path(exists=True, file_okay=False, dir_okay=True),
569
+ help='Path to CIS Controls configuration directory')
570
+ @click.option('--output-format', '-f',
571
+ type=click.Choice(['table', 'json'], case_sensitive=False),
572
+ default='table',
573
+ help='Output format for the control list')
574
+ @click.pass_context
575
+ def list_controls(ctx, config_path, output_format):
576
+ """List available CIS Controls and their Config rules.
577
+
578
+ This command displays all available CIS Controls organized by Implementation Group,
579
+ showing the AWS Config rules that will be evaluated for each control.
580
+
581
+ Examples:
582
+
583
+ # List controls in table format
584
+ aws-cis-assess list-controls
585
+
586
+ # List controls in JSON format
587
+ aws-cis-assess list-controls -f json
588
+
589
+ # Use custom configuration path
590
+ aws-cis-assess list-controls -c ./custom-config/
591
+ """
592
+ verbose = ctx.obj.get('verbose', False)
593
+
594
+ try:
595
+ # Initialize config loader
596
+ config_loader = ConfigRuleLoader(config_path)
597
+
598
+ # Get all controls
599
+ all_controls = config_loader.get_all_controls()
600
+
601
+ if output_format == 'json':
602
+ # JSON output
603
+ controls_data = {}
604
+ for control_id, control in all_controls.items():
605
+ controls_data[control_id] = {
606
+ 'title': control.title,
607
+ 'implementation_group': control.implementation_group,
608
+ 'weight': control.weight,
609
+ 'config_rules': [
610
+ {
611
+ 'name': rule.name,
612
+ 'resource_types': rule.resource_types,
613
+ 'description': rule.description
614
+ }
615
+ for rule in control.config_rules
616
+ ]
617
+ }
618
+
619
+ click.echo(json.dumps(controls_data, indent=2))
620
+ else:
621
+ # Table output
622
+ _display_controls_table(all_controls, verbose)
623
+
624
+ except Exception as e:
625
+ click.echo(f"❌ Failed to list controls: {str(e)}", err=True)
626
+ if verbose:
627
+ import traceback
628
+ click.echo(traceback.format_exc(), err=True)
629
+ sys.exit(1)
630
+
631
+
632
+ @cli.command()
633
+ @click.option('--aws-profile', '-p',
634
+ help='AWS profile to use for credentials')
635
+ @click.option('--aws-access-key-id',
636
+ help='AWS Access Key ID (alternative to profile)')
637
+ @click.option('--aws-secret-access-key',
638
+ help='AWS Secret Access Key (alternative to profile)')
639
+ @click.option('--aws-session-token',
640
+ help='AWS Session Token (for temporary credentials)')
641
+ @click.option('--regions', '-r',
642
+ help='Comma-separated list of AWS regions to validate')
643
+ @click.pass_context
644
+ def validate_credentials(ctx, aws_profile, aws_access_key_id, aws_secret_access_key,
645
+ aws_session_token, regions):
646
+ """Validate AWS credentials and permissions.
647
+
648
+ This command validates your AWS credentials and checks if you have the necessary
649
+ permissions to run CIS Controls assessments.
650
+
651
+ Examples:
652
+
653
+ # Validate default credentials
654
+ aws-cis-assess validate-credentials
655
+
656
+ # Validate specific AWS profile
657
+ aws-cis-assess validate-credentials -p production
658
+
659
+ # Validate credentials for specific regions
660
+ aws-cis-assess validate-credentials -r us-east-1,us-west-2
661
+ """
662
+ verbose = ctx.obj.get('verbose', False)
663
+
664
+ try:
665
+ # Parse regions
666
+ region_list = None
667
+ if regions:
668
+ region_list = [r.strip() for r in regions.split(',')]
669
+
670
+ # Prepare AWS credentials
671
+ aws_credentials = _prepare_aws_credentials(
672
+ aws_profile, aws_access_key_id, aws_secret_access_key, aws_session_token
673
+ )
674
+
675
+ # Initialize assessment engine for credential validation
676
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
677
+
678
+ click.echo("🔧 Validating AWS credentials...")
679
+ aws_factory = AWSClientFactory(aws_credentials, region_list)
680
+
681
+ # Validate credentials
682
+ if aws_factory.validate_credentials():
683
+ click.echo("✅ AWS credentials are valid")
684
+
685
+ # Get account information
686
+ account_info = aws_factory.get_account_info()
687
+ click.echo(f" Account ID: {account_info.get('account_id', 'Unknown')}")
688
+ click.echo(f" User/Role: {account_info.get('user_id', 'Unknown')}")
689
+ click.echo(f" Regions: {aws_factory.regions}")
690
+
691
+ if verbose:
692
+ # Show supported services
693
+ supported_services = aws_factory.get_supported_services()
694
+ click.echo(f" Supported Services: {', '.join(supported_services[:10])}...") # Show first 10
695
+ else:
696
+ click.echo("❌ AWS credential validation failed", err=True)
697
+ sys.exit(1)
698
+
699
+ except Exception as e:
700
+ click.echo(f"❌ Credential validation failed: {str(e)}", err=True)
701
+ if verbose:
702
+ import traceback
703
+ click.echo(traceback.format_exc(), err=True)
704
+ sys.exit(1)
705
+
706
+
707
+ @cli.command()
708
+ @click.option('--topic', '-t',
709
+ type=click.Choice(['examples', 'troubleshooting', 'best-practices'], case_sensitive=False),
710
+ help='Specific help topic to display')
711
+ @click.pass_context
712
+ def help_guide(ctx, topic):
713
+ """Show detailed help, examples, and troubleshooting guide.
714
+
715
+ This command provides comprehensive help including usage examples,
716
+ troubleshooting guidance, and best practices for using the CIS assessment tool.
717
+
718
+ Examples:
719
+
720
+ # Show all help topics
721
+ aws-cis-assess help-guide
722
+
723
+ # Show usage examples
724
+ aws-cis-assess help-guide --topic examples
725
+
726
+ # Show troubleshooting guide
727
+ aws-cis-assess help-guide --topic troubleshooting
728
+ """
729
+ verbose = ctx.obj.get('verbose', False)
730
+
731
+ if topic == 'examples' or topic is None:
732
+ _display_usage_examples()
733
+
734
+ if topic == 'troubleshooting' or topic is None:
735
+ _display_troubleshooting_guide()
736
+
737
+ if topic == 'best-practices' or topic is None:
738
+ _display_best_practices()
739
+
740
+
741
+ def _display_usage_examples():
742
+ """Display usage examples."""
743
+ click.echo("📚 Usage Examples")
744
+ click.echo("=" * 50)
745
+
746
+ examples = get_all_examples()
747
+ for example_name, example_data in examples.items():
748
+ click.echo(f"\n🔹 {example_data['title']}")
749
+ click.echo(f" {example_data['description']}")
750
+ click.echo(f" Command: {example_data['command']}")
751
+ click.echo(f" {example_data['explanation']}")
752
+
753
+
754
+ def _display_troubleshooting_guide():
755
+ """Display troubleshooting guide."""
756
+ click.echo("\n🔧 Troubleshooting Guide")
757
+ click.echo("=" * 50)
758
+
759
+ guide = get_troubleshooting_guide()
760
+ for category, info in guide.items():
761
+ click.echo(f"\n❗ {info['title']}")
762
+ click.echo(" Common Problems:")
763
+ for problem in info['problems']:
764
+ click.echo(f" • {problem}")
765
+ click.echo(" Solutions:")
766
+ for solution in info['solutions']:
767
+ click.echo(f" ✓ {solution}")
768
+
769
+
770
+ def _display_best_practices():
771
+ """Display best practices."""
772
+ click.echo("\n💡 Best Practices")
773
+ click.echo("=" * 50)
774
+
775
+ practices = get_best_practices()
776
+ for practice in practices:
777
+ click.echo(f"\n🎯 {practice['title']}")
778
+ click.echo(f" {practice['description']}")
779
+ click.echo(f" Example: {practice['command']}")
780
+
781
+
782
+ @cli.command()
783
+ @click.option('--config-path', '-c',
784
+ type=click.Path(exists=True, file_okay=False, dir_okay=True),
785
+ help='Path to CIS Controls configuration directory')
786
+ @click.pass_context
787
+ def validate_config(ctx, config_path):
788
+ """Validate CIS Controls configuration files.
789
+
790
+ This command validates the YAML configuration files that define the mapping
791
+ between CIS Controls and AWS Config rules.
792
+
793
+ Examples:
794
+
795
+ # Validate default configuration
796
+ aws-cis-assess validate-config
797
+
798
+ # Validate custom configuration
799
+ aws-cis-assess validate-config -c ./custom-config/
800
+ """
801
+ verbose = ctx.obj.get('verbose', False)
802
+
803
+ try:
804
+ # Initialize config loader
805
+ config_loader = ConfigRuleLoader(config_path)
806
+
807
+ click.echo("🔧 Validating CIS Controls configuration...")
808
+
809
+ # Validate configuration
810
+ validation_errors = config_loader.validate_configuration()
811
+
812
+ if validation_errors:
813
+ click.echo("❌ Configuration validation failed:", err=True)
814
+ for error in validation_errors:
815
+ click.echo(f" • {error}", err=True)
816
+ sys.exit(1)
817
+ else:
818
+ click.echo("✅ Configuration is valid")
819
+
820
+ # Show configuration summary
821
+ if verbose:
822
+ rules_count = config_loader.get_rules_count_by_ig()
823
+ click.echo("Configuration Summary:")
824
+ for ig, count in rules_count.items():
825
+ click.echo(f" {ig}: {count} Config rules")
826
+
827
+ except Exception as e:
828
+ click.echo(f"❌ Configuration validation failed: {str(e)}", err=True)
829
+ if verbose:
830
+ import traceback
831
+ click.echo(traceback.format_exc(), err=True)
832
+ sys.exit(1)
833
+
834
+
835
+ def _prepare_aws_credentials(aws_profile: Optional[str],
836
+ aws_access_key_id: Optional[str],
837
+ aws_secret_access_key: Optional[str],
838
+ aws_session_token: Optional[str]) -> Optional[Dict[str, str]]:
839
+ """Prepare AWS credentials dictionary from CLI options."""
840
+ credentials = {}
841
+
842
+ if aws_profile:
843
+ credentials['profile_name'] = aws_profile
844
+
845
+ if aws_access_key_id and aws_secret_access_key:
846
+ credentials['aws_access_key_id'] = aws_access_key_id
847
+ credentials['aws_secret_access_key'] = aws_secret_access_key
848
+ if aws_session_token:
849
+ credentials['aws_session_token'] = aws_session_token
850
+
851
+ return credentials if credentials else None
852
+
853
+
854
+ def _parse_regions(regions: Optional[str], exclude_regions: Optional[str]) -> Optional[List[str]]:
855
+ """Parse regions and handle exclusions."""
856
+ region_list = None
857
+
858
+ if regions:
859
+ region_list = [r.strip() for r in regions.split(',')]
860
+
861
+ if exclude_regions:
862
+ exclude_list = [r.strip() for r in exclude_regions.split(',')]
863
+ if region_list:
864
+ # Remove excluded regions from specified regions
865
+ region_list = [r for r in region_list if r not in exclude_list]
866
+ else:
867
+ # Get all regions and exclude specified ones
868
+ from aws_cis_assessment.cli.utils import get_all_enabled_regions
869
+ all_regions = get_all_enabled_regions()
870
+ region_list = [r for r in all_regions if r not in exclude_list]
871
+
872
+ return region_list
873
+
874
+
875
+ def _parse_controls(controls: Optional[str]) -> Optional[List[str]]:
876
+ """Parse comma-separated control IDs."""
877
+ if not controls:
878
+ return None
879
+
880
+ return [c.strip() for c in controls.split(',')]
881
+
882
+
883
+ def _configure_logging(verbose: bool, debug: bool, log_level: Optional[str], log_file: Optional[str]):
884
+ """Configure logging based on CLI options."""
885
+ # Determine log level
886
+ if log_level:
887
+ level = getattr(logging, log_level.upper())
888
+ elif debug:
889
+ level = logging.DEBUG
890
+ elif verbose:
891
+ level = logging.INFO
892
+ else:
893
+ level = logging.WARNING
894
+
895
+ # Configure root logger
896
+ root_logger = logging.getLogger()
897
+ root_logger.setLevel(level)
898
+
899
+ # Clear existing handlers
900
+ for handler in root_logger.handlers[:]:
901
+ root_logger.removeHandler(handler)
902
+
903
+ # Create formatter
904
+ formatter = logging.Formatter(
905
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
906
+ )
907
+
908
+ # Console handler
909
+ console_handler = logging.StreamHandler(sys.stderr)
910
+ console_handler.setLevel(level)
911
+ console_handler.setFormatter(formatter)
912
+ root_logger.addHandler(console_handler)
913
+
914
+ # File handler if specified
915
+ if log_file:
916
+ try:
917
+ file_handler = logging.FileHandler(log_file)
918
+ file_handler.setLevel(level)
919
+ file_handler.setFormatter(formatter)
920
+ root_logger.addHandler(file_handler)
921
+ except Exception as e:
922
+ click.echo(f"⚠️ Warning: Could not create log file {log_file}: {e}", err=True)
923
+
924
+
925
+ def _display_assessment_summary(summary: Dict[str, Any], verbose: bool):
926
+ """Display assessment summary information."""
927
+ click.echo("📊 Assessment Summary:")
928
+ click.echo(f" Implementation Groups: {', '.join(summary['implementation_groups'])}")
929
+ click.echo(f" Total Assessments: {summary['total_assessments']}")
930
+ click.echo(f" Regions: {', '.join(summary['regions'])}")
931
+
932
+ if verbose:
933
+ click.echo(" Assessments by IG:")
934
+ for ig, data in summary['assessments_by_ig'].items():
935
+ click.echo(f" {ig}: {data['count']} assessments")
936
+
937
+
938
+ def _display_results_summary(assessment_result, compliance_summary, verbose: bool):
939
+ """Display assessment results summary."""
940
+ click.echo()
941
+ click.echo("📈 Assessment Results:")
942
+
943
+ # Format duration properly
944
+ duration_str = format_duration(assessment_result.assessment_duration)
945
+
946
+ click.echo(f" Overall Compliance: {compliance_summary.overall_compliance_percentage:.1f}%")
947
+ click.echo(f" IG1 Compliance: {compliance_summary.ig1_compliance_percentage:.1f}%")
948
+ click.echo(f" IG2 Compliance: {compliance_summary.ig2_compliance_percentage:.1f}%")
949
+ click.echo(f" IG3 Compliance: {compliance_summary.ig3_compliance_percentage:.1f}%")
950
+ click.echo(f" Total Resources: {assessment_result.total_resources_evaluated}")
951
+ click.echo(f" Assessment Duration: {duration_str}")
952
+
953
+ if verbose and compliance_summary.top_risk_areas:
954
+ click.echo(" Top Risk Areas:")
955
+ for risk_area in compliance_summary.top_risk_areas[:5]:
956
+ click.echo(f" • {risk_area}")
957
+
958
+
959
+ def _display_controls_table(all_controls: Dict[str, Any], verbose: bool):
960
+ """Display controls in table format."""
961
+ # Group controls by Implementation Group
962
+ controls_by_ig = {}
963
+ for control_id, control in all_controls.items():
964
+ ig = control.implementation_group
965
+ if ig not in controls_by_ig:
966
+ controls_by_ig[ig] = []
967
+ controls_by_ig[ig].append(control)
968
+
969
+ for ig in ['IG1', 'IG2', 'IG3']:
970
+ if ig not in controls_by_ig:
971
+ continue
972
+
973
+ click.echo(f"\n{ig} - {_get_ig_description(ig)}")
974
+ click.echo("=" * 80)
975
+
976
+ table_data = []
977
+ for control in sorted(controls_by_ig[ig], key=lambda c: c.control_id):
978
+ rule_count = len(control.config_rules)
979
+ if verbose:
980
+ rule_names = ', '.join([rule.name for rule in control.config_rules[:3]])
981
+ if rule_count > 3:
982
+ rule_names += f" (+{rule_count - 3} more)"
983
+ else:
984
+ rule_names = f"{rule_count} Config rules"
985
+
986
+ table_data.append([
987
+ control.control_id,
988
+ control.title[:50] + ('...' if len(control.title) > 50 else ''),
989
+ rule_names
990
+ ])
991
+
992
+ headers = ['Control ID', 'Title', 'Config Rules']
993
+ click.echo(tabulate(table_data, headers=headers, tablefmt='grid'))
994
+
995
+
996
+ def _get_ig_description(ig: str) -> str:
997
+ """Get description for Implementation Group."""
998
+ descriptions = {
999
+ 'IG1': 'Essential Cyber Hygiene',
1000
+ 'IG2': 'Enhanced Security',
1001
+ 'IG3': 'Advanced Security'
1002
+ }
1003
+ return descriptions.get(ig, '')
1004
+
1005
+
1006
+ def _generate_reports(assessment_result, compliance_summary, output_formats, output_file, verbose: bool):
1007
+ """Generate reports in specified formats."""
1008
+ click.echo()
1009
+ click.echo("📄 Generating reports...")
1010
+
1011
+ # Default output file if not specified
1012
+ if not output_file:
1013
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
1014
+ output_file = f"cis_assessment_{timestamp}"
1015
+
1016
+ # Remove extension from output_file if present
1017
+ output_base = Path(output_file).stem
1018
+ output_dir = Path(output_file).parent
1019
+
1020
+ for format_type in output_formats:
1021
+ try:
1022
+ if format_type.lower() == 'json':
1023
+ reporter = JSONReporter()
1024
+ output_path = output_dir / f"{output_base}.json"
1025
+ elif format_type.lower() == 'html':
1026
+ reporter = HTMLReporter()
1027
+ output_path = output_dir / f"{output_base}.html"
1028
+ elif format_type.lower() == 'csv':
1029
+ reporter = CSVReporter()
1030
+ output_path = output_dir / f"{output_base}.csv"
1031
+ else:
1032
+ click.echo(f"⚠️ Unsupported format: {format_type}", err=True)
1033
+ continue
1034
+
1035
+ # Generate report
1036
+ report_content = reporter.generate_report(assessment_result, compliance_summary, str(output_path))
1037
+
1038
+ click.echo(f" ✅ {format_type.upper()} report: {output_path}")
1039
+
1040
+ except Exception as e:
1041
+ click.echo(f" ❌ Failed to generate {format_type.upper()} report: {str(e)}", err=True)
1042
+ if verbose:
1043
+ import traceback
1044
+ click.echo(traceback.format_exc(), err=True)
1045
+
1046
+
1047
+ def _display_error_summary(error_summary: Dict[str, Any]):
1048
+ """Display error summary if available."""
1049
+ if not error_summary or not error_summary.get('errors'):
1050
+ return
1051
+
1052
+ click.echo()
1053
+ click.echo("⚠️ Error Summary:")
1054
+
1055
+ total_errors = error_summary.get('total_errors', 0)
1056
+ click.echo(f" Total Errors: {total_errors}")
1057
+
1058
+ if 'errors_by_category' in error_summary:
1059
+ for category, count in error_summary['errors_by_category'].items():
1060
+ click.echo(f" {category}: {count}")
1061
+
1062
+ # Show recent errors
1063
+ recent_errors = error_summary.get('recent_errors', [])
1064
+ if recent_errors:
1065
+ click.echo(" Recent Errors:")
1066
+ for error in recent_errors[-5:]: # Show last 5 errors
1067
+ click.echo(f" • {error}")
1068
+
1069
+
1070
+ @cli.command('validate-accuracy')
1071
+ @click.option('--aws-profile', '-p',
1072
+ help='AWS profile to use for credentials')
1073
+ @click.option('--aws-access-key-id',
1074
+ envvar='AWS_ACCESS_KEY_ID',
1075
+ help='AWS access key ID')
1076
+ @click.option('--aws-secret-access-key',
1077
+ envvar='AWS_SECRET_ACCESS_KEY',
1078
+ help='AWS secret access key')
1079
+ @click.option('--aws-session-token',
1080
+ envvar='AWS_SESSION_TOKEN',
1081
+ help='AWS session token')
1082
+ @click.option('--regions', '-r',
1083
+ help='Comma-separated list of AWS regions to validate')
1084
+ @click.option('--config-rules',
1085
+ help='Comma-separated list of specific Config rules to validate')
1086
+ @click.option('--output-file', '-o',
1087
+ type=click.Path(),
1088
+ help='Output file for validation report')
1089
+ @click.option('--check-config-availability', is_flag=True,
1090
+ help='Check AWS Config service availability in regions')
1091
+ @click.option('--verbose', '-v', is_flag=True, help='Enable verbose output')
1092
+ @click.pass_context
1093
+ def validate_accuracy(ctx, aws_profile, aws_access_key_id, aws_secret_access_key,
1094
+ aws_session_token, regions, config_rules, output_file,
1095
+ check_config_availability, verbose):
1096
+ """Validate assessment accuracy against AWS Config rule evaluations.
1097
+
1098
+ This command compares our assessment results with AWS Config rule evaluations
1099
+ to validate accuracy. Requires AWS Config to be enabled in target regions.
1100
+
1101
+ Examples:
1102
+ aws-cis-assess validate-accuracy
1103
+ aws-cis-assess validate-accuracy --regions us-east-1,us-west-2
1104
+ aws-cis-assess validate-accuracy --config-rules eip-attached,iam-password-policy
1105
+ aws-cis-assess validate-accuracy --check-config-availability
1106
+ """
1107
+ from aws_cis_assessment.core.accuracy_validator import AccuracyValidator
1108
+ from aws_cis_assessment.core.aws_client_factory import AWSClientFactory
1109
+
1110
+ try:
1111
+ # Configure logging
1112
+ _configure_logging(verbose, ctx.parent.params.get('debug', False), None, None)
1113
+
1114
+ # Prepare AWS credentials
1115
+ credentials = _prepare_aws_credentials(
1116
+ aws_profile, aws_access_key_id, aws_secret_access_key, aws_session_token
1117
+ )
1118
+
1119
+ # Parse regions
1120
+ region_list = _parse_regions(regions, None)
1121
+
1122
+ click.echo("🔍 Starting assessment accuracy validation...")
1123
+
1124
+ # Create AWS client factory
1125
+ aws_factory = AWSClientFactory(credentials, region_list)
1126
+
1127
+ # Validate credentials
1128
+ if not aws_factory.validate_credentials():
1129
+ click.echo("❌ AWS credential validation failed", err=True)
1130
+ sys.exit(1)
1131
+
1132
+ account_info = aws_factory.get_account_info()
1133
+ click.echo(f" Account: {account_info['account_id']}")
1134
+ click.echo(f" Regions: {', '.join(aws_factory.regions)}")
1135
+
1136
+ # Create accuracy validator
1137
+ validator = AccuracyValidator(aws_factory)
1138
+
1139
+ # Check Config service availability if requested
1140
+ if check_config_availability:
1141
+ click.echo()
1142
+ click.echo("🔧 Checking AWS Config service availability...")
1143
+
1144
+ availability = validator.check_config_service_availability()
1145
+
1146
+ available_regions = [region for region, available in availability.items() if available]
1147
+ unavailable_regions = [region for region, available in availability.items() if not available]
1148
+
1149
+ if available_regions:
1150
+ click.echo(f" ✅ Config available: {', '.join(available_regions)}")
1151
+ if unavailable_regions:
1152
+ click.echo(f" ❌ Config unavailable: {', '.join(unavailable_regions)}")
1153
+
1154
+ if not available_regions:
1155
+ click.echo("❌ AWS Config is not available in any target regions", err=True)
1156
+ click.echo(" Enable AWS Config to use accuracy validation", err=True)
1157
+ sys.exit(1)
1158
+
1159
+ # Update regions to only include available ones
1160
+ aws_factory.regions = available_regions
1161
+
1162
+ # Run a sample assessment to get results for validation
1163
+ click.echo()
1164
+ click.echo("🏃 Running sample assessment for validation...")
1165
+
1166
+ with AssessmentEngine(
1167
+ aws_credentials=credentials,
1168
+ regions=aws_factory.regions,
1169
+ max_workers=4,
1170
+ enable_resource_monitoring=False, # Disable for validation
1171
+ enable_audit_trail=False
1172
+ ) as engine:
1173
+
1174
+ # Run assessment for IG1 only (faster for validation)
1175
+ assessment_result = engine.run_assessment(['IG1'])
1176
+
1177
+ click.echo(f" Assessed {assessment_result.total_resources_evaluated} resources")
1178
+
1179
+ # Extract compliance results for validation
1180
+ all_compliance_results = []
1181
+ for ig_score in assessment_result.ig_scores.values():
1182
+ for control_score in ig_score.control_scores.values():
1183
+ all_compliance_results.extend(control_score.findings)
1184
+
1185
+ # Parse config rules filter
1186
+ config_rule_names = None
1187
+ if config_rules:
1188
+ config_rule_names = [rule.strip() for rule in config_rules.split(',')]
1189
+
1190
+ # Validate accuracy
1191
+ click.echo()
1192
+ click.echo("🎯 Validating assessment accuracy...")
1193
+
1194
+ validation_summary = validator.validate_assessment_accuracy(
1195
+ all_compliance_results,
1196
+ config_rule_names
1197
+ )
1198
+
1199
+ # Display results
1200
+ click.echo()
1201
+ click.echo("📊 Validation Results:")
1202
+ click.echo(f" Total rules validated: {validation_summary.total_rules_validated}")
1203
+ click.echo(f" Accurate rules: {validation_summary.accurate_rules}")
1204
+ click.echo(f" Overall accuracy: {validation_summary.overall_accuracy:.1f}%")
1205
+
1206
+ if verbose:
1207
+ click.echo()
1208
+ click.echo("📋 Individual Rule Results:")
1209
+ for result in validation_summary.validation_results:
1210
+ status = "✅" if result.is_accurate else "❌"
1211
+ click.echo(f" {status} {result.config_rule_name}: {result.accuracy_percentage:.1f}% "
1212
+ f"({result.matching_results}/{result.total_resources})")
1213
+
1214
+ if result.discrepancies and verbose:
1215
+ for discrepancy in result.discrepancies[:3]: # Show first 3
1216
+ if 'issue' in discrepancy:
1217
+ click.echo(f" • {discrepancy['resource_id']}: {discrepancy['issue']}")
1218
+ else:
1219
+ click.echo(f" • {discrepancy['resource_id']}: "
1220
+ f"Our={discrepancy['our_status']}, "
1221
+ f"Config={discrepancy['config_status']}")
1222
+
1223
+ # Generate validation report
1224
+ if output_file or verbose:
1225
+ report = validator.generate_validation_report(validation_summary)
1226
+
1227
+ if output_file:
1228
+ with open(output_file, 'w') as f:
1229
+ f.write(report)
1230
+ click.echo(f" 📄 Validation report saved: {output_file}")
1231
+ elif verbose:
1232
+ click.echo()
1233
+ click.echo("📄 Validation Report:")
1234
+ click.echo(report)
1235
+
1236
+ # Exit with appropriate code
1237
+ if validation_summary.overall_accuracy >= 95.0:
1238
+ click.echo()
1239
+ click.echo("✅ Validation completed successfully! High accuracy achieved.")
1240
+ else:
1241
+ click.echo()
1242
+ click.echo("⚠️ Validation completed with accuracy concerns. Review discrepancies.")
1243
+ sys.exit(1)
1244
+
1245
+ except Exception as e:
1246
+ click.echo(f"❌ Validation failed: {str(e)}", err=True)
1247
+ if verbose:
1248
+ import traceback
1249
+ click.echo(traceback.format_exc(), err=True)
1250
+ sys.exit(1)
1251
+
1252
+
1253
+ def main():
1254
+ """Main entry point for the CLI."""
1255
+ cli()
1256
+
1257
+
1258
+ if __name__ == '__main__':
1259
+ main()