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.
- aws_cis_assessment/__init__.py +11 -0
- aws_cis_assessment/cli/__init__.py +3 -0
- aws_cis_assessment/cli/examples.py +274 -0
- aws_cis_assessment/cli/main.py +1259 -0
- aws_cis_assessment/cli/utils.py +356 -0
- aws_cis_assessment/config/__init__.py +1 -0
- aws_cis_assessment/config/config_loader.py +328 -0
- aws_cis_assessment/config/rules/cis_controls_ig1.yaml +590 -0
- aws_cis_assessment/config/rules/cis_controls_ig2.yaml +412 -0
- aws_cis_assessment/config/rules/cis_controls_ig3.yaml +100 -0
- aws_cis_assessment/controls/__init__.py +1 -0
- aws_cis_assessment/controls/base_control.py +400 -0
- aws_cis_assessment/controls/ig1/__init__.py +239 -0
- aws_cis_assessment/controls/ig1/control_1_1.py +586 -0
- aws_cis_assessment/controls/ig1/control_2_2.py +231 -0
- aws_cis_assessment/controls/ig1/control_3_3.py +718 -0
- aws_cis_assessment/controls/ig1/control_3_4.py +235 -0
- aws_cis_assessment/controls/ig1/control_4_1.py +461 -0
- aws_cis_assessment/controls/ig1/control_access_keys.py +310 -0
- aws_cis_assessment/controls/ig1/control_advanced_security.py +512 -0
- aws_cis_assessment/controls/ig1/control_backup_recovery.py +510 -0
- aws_cis_assessment/controls/ig1/control_cloudtrail_logging.py +197 -0
- aws_cis_assessment/controls/ig1/control_critical_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_data_protection.py +898 -0
- aws_cis_assessment/controls/ig1/control_iam_advanced.py +573 -0
- aws_cis_assessment/controls/ig1/control_iam_governance.py +493 -0
- aws_cis_assessment/controls/ig1/control_iam_policies.py +383 -0
- aws_cis_assessment/controls/ig1/control_instance_optimization.py +100 -0
- aws_cis_assessment/controls/ig1/control_network_enhancements.py +203 -0
- aws_cis_assessment/controls/ig1/control_network_security.py +672 -0
- aws_cis_assessment/controls/ig1/control_s3_enhancements.py +173 -0
- aws_cis_assessment/controls/ig1/control_s3_security.py +422 -0
- aws_cis_assessment/controls/ig1/control_vpc_security.py +235 -0
- aws_cis_assessment/controls/ig2/__init__.py +172 -0
- aws_cis_assessment/controls/ig2/control_3_10.py +698 -0
- aws_cis_assessment/controls/ig2/control_3_11.py +1330 -0
- aws_cis_assessment/controls/ig2/control_5_2.py +393 -0
- aws_cis_assessment/controls/ig2/control_advanced_encryption.py +355 -0
- aws_cis_assessment/controls/ig2/control_codebuild_security.py +263 -0
- aws_cis_assessment/controls/ig2/control_encryption_rest.py +382 -0
- aws_cis_assessment/controls/ig2/control_encryption_transit.py +382 -0
- aws_cis_assessment/controls/ig2/control_network_ha.py +467 -0
- aws_cis_assessment/controls/ig2/control_remaining_encryption.py +426 -0
- aws_cis_assessment/controls/ig2/control_remaining_rules.py +363 -0
- aws_cis_assessment/controls/ig2/control_service_logging.py +402 -0
- aws_cis_assessment/controls/ig3/__init__.py +49 -0
- aws_cis_assessment/controls/ig3/control_12_8.py +395 -0
- aws_cis_assessment/controls/ig3/control_13_1.py +467 -0
- aws_cis_assessment/controls/ig3/control_3_14.py +523 -0
- aws_cis_assessment/controls/ig3/control_7_1.py +359 -0
- aws_cis_assessment/core/__init__.py +1 -0
- aws_cis_assessment/core/accuracy_validator.py +425 -0
- aws_cis_assessment/core/assessment_engine.py +1266 -0
- aws_cis_assessment/core/audit_trail.py +491 -0
- aws_cis_assessment/core/aws_client_factory.py +313 -0
- aws_cis_assessment/core/error_handler.py +607 -0
- aws_cis_assessment/core/models.py +166 -0
- aws_cis_assessment/core/scoring_engine.py +459 -0
- aws_cis_assessment/reporters/__init__.py +8 -0
- aws_cis_assessment/reporters/base_reporter.py +454 -0
- aws_cis_assessment/reporters/csv_reporter.py +835 -0
- aws_cis_assessment/reporters/html_reporter.py +2162 -0
- aws_cis_assessment/reporters/json_reporter.py +561 -0
- aws_cis_controls_assessment-1.0.3.dist-info/METADATA +248 -0
- aws_cis_controls_assessment-1.0.3.dist-info/RECORD +77 -0
- aws_cis_controls_assessment-1.0.3.dist-info/WHEEL +5 -0
- aws_cis_controls_assessment-1.0.3.dist-info/entry_points.txt +2 -0
- aws_cis_controls_assessment-1.0.3.dist-info/licenses/LICENSE +21 -0
- aws_cis_controls_assessment-1.0.3.dist-info/top_level.txt +2 -0
- docs/README.md +94 -0
- docs/assessment-logic.md +766 -0
- docs/cli-reference.md +698 -0
- docs/config-rule-mappings.md +393 -0
- docs/developer-guide.md +858 -0
- docs/installation.md +299 -0
- docs/troubleshooting.md +634 -0
- 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()
|