complio 0.1.1__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.
- CHANGELOG.md +208 -0
- README.md +343 -0
- complio/__init__.py +48 -0
- complio/cli/__init__.py +0 -0
- complio/cli/banner.py +87 -0
- complio/cli/commands/__init__.py +0 -0
- complio/cli/commands/history.py +439 -0
- complio/cli/commands/scan.py +700 -0
- complio/cli/main.py +115 -0
- complio/cli/output.py +338 -0
- complio/config/__init__.py +17 -0
- complio/config/settings.py +333 -0
- complio/connectors/__init__.py +9 -0
- complio/connectors/aws/__init__.py +0 -0
- complio/connectors/aws/client.py +342 -0
- complio/connectors/base.py +135 -0
- complio/core/__init__.py +10 -0
- complio/core/registry.py +228 -0
- complio/core/runner.py +351 -0
- complio/py.typed +0 -0
- complio/reporters/__init__.py +7 -0
- complio/reporters/generator.py +417 -0
- complio/tests_library/__init__.py +0 -0
- complio/tests_library/base.py +492 -0
- complio/tests_library/identity/__init__.py +0 -0
- complio/tests_library/identity/access_key_rotation.py +302 -0
- complio/tests_library/identity/mfa_enforcement.py +327 -0
- complio/tests_library/identity/root_account_protection.py +470 -0
- complio/tests_library/infrastructure/__init__.py +0 -0
- complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
- complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
- complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
- complio/tests_library/infrastructure/ebs_encryption.py +244 -0
- complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
- complio/tests_library/infrastructure/iam_password_policy.py +460 -0
- complio/tests_library/infrastructure/nacl_security.py +356 -0
- complio/tests_library/infrastructure/rds_encryption.py +252 -0
- complio/tests_library/infrastructure/s3_encryption.py +301 -0
- complio/tests_library/infrastructure/s3_public_access.py +369 -0
- complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
- complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
- complio/tests_library/logging/__init__.py +0 -0
- complio/tests_library/logging/cloudwatch_alarms.py +354 -0
- complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
- complio/tests_library/logging/cloudwatch_retention.py +252 -0
- complio/tests_library/logging/config_enabled.py +393 -0
- complio/tests_library/logging/eventbridge_rules.py +460 -0
- complio/tests_library/logging/guardduty_enabled.py +436 -0
- complio/tests_library/logging/security_hub_enabled.py +416 -0
- complio/tests_library/logging/sns_encryption.py +273 -0
- complio/tests_library/network/__init__.py +0 -0
- complio/tests_library/network/alb_nlb_security.py +421 -0
- complio/tests_library/network/api_gateway_security.py +452 -0
- complio/tests_library/network/cloudfront_https.py +332 -0
- complio/tests_library/network/direct_connect_security.py +343 -0
- complio/tests_library/network/nacl_configuration.py +367 -0
- complio/tests_library/network/network_firewall.py +355 -0
- complio/tests_library/network/transit_gateway_security.py +318 -0
- complio/tests_library/network/vpc_endpoints_security.py +339 -0
- complio/tests_library/network/vpn_security.py +333 -0
- complio/tests_library/network/waf_configuration.py +428 -0
- complio/tests_library/security/__init__.py +0 -0
- complio/tests_library/security/kms_key_rotation.py +314 -0
- complio/tests_library/storage/__init__.py +0 -0
- complio/tests_library/storage/backup_encryption.py +288 -0
- complio/tests_library/storage/dynamodb_encryption.py +280 -0
- complio/tests_library/storage/efs_encryption.py +257 -0
- complio/tests_library/storage/elasticache_encryption.py +370 -0
- complio/tests_library/storage/redshift_encryption.py +252 -0
- complio/tests_library/storage/s3_versioning.py +264 -0
- complio/utils/__init__.py +26 -0
- complio/utils/errors.py +179 -0
- complio/utils/exceptions.py +151 -0
- complio/utils/history.py +243 -0
- complio/utils/logger.py +391 -0
- complio-0.1.1.dist-info/METADATA +385 -0
- complio-0.1.1.dist-info/RECORD +79 -0
- complio-0.1.1.dist-info/WHEEL +4 -0
- complio-0.1.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"""
|
|
2
|
+
History command for viewing and comparing past scans.
|
|
3
|
+
|
|
4
|
+
This module implements CLI commands for managing scan history:
|
|
5
|
+
- complio history: View recent scans
|
|
6
|
+
- complio compare: Compare two scans
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
$ complio history
|
|
10
|
+
$ complio history --limit 20
|
|
11
|
+
$ complio compare scan_20260107_162335_abc123 scan_20260106_143022_xyz789
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from complio.utils.history import (
|
|
21
|
+
clear_old_history,
|
|
22
|
+
compare_scans,
|
|
23
|
+
get_scan_by_id,
|
|
24
|
+
get_scan_history,
|
|
25
|
+
)
|
|
26
|
+
from complio.utils.logger import get_logger
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@click.command()
|
|
30
|
+
@click.option(
|
|
31
|
+
"--limit",
|
|
32
|
+
"-n",
|
|
33
|
+
default=10,
|
|
34
|
+
type=int,
|
|
35
|
+
help="Maximum number of scans to display",
|
|
36
|
+
show_default=True,
|
|
37
|
+
)
|
|
38
|
+
@click.option(
|
|
39
|
+
"--details",
|
|
40
|
+
is_flag=True,
|
|
41
|
+
default=False,
|
|
42
|
+
help="Show detailed information for each scan",
|
|
43
|
+
)
|
|
44
|
+
def history(limit: int, details: bool) -> None:
|
|
45
|
+
"""View scan history.
|
|
46
|
+
|
|
47
|
+
Displays a list of recent compliance scans with summary information.
|
|
48
|
+
Use this to track compliance trends over time and reference past results.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
|
|
52
|
+
# View last 10 scans
|
|
53
|
+
$ complio history
|
|
54
|
+
|
|
55
|
+
# View last 20 scans
|
|
56
|
+
$ complio history --limit 20
|
|
57
|
+
|
|
58
|
+
# View with detailed test results
|
|
59
|
+
$ complio history --details
|
|
60
|
+
|
|
61
|
+
Scan results are stored locally in ~/.complio/history/
|
|
62
|
+
"""
|
|
63
|
+
console = Console()
|
|
64
|
+
logger = get_logger(__name__)
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
# Get scan history
|
|
68
|
+
scans = get_scan_history(limit=limit)
|
|
69
|
+
|
|
70
|
+
if not scans:
|
|
71
|
+
console.print("\n[yellow]No scan history found[/yellow]")
|
|
72
|
+
console.print("\n[dim]Run 'complio scan' to create your first scan[/dim]\n")
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Display scans in table
|
|
76
|
+
table = Table(
|
|
77
|
+
title=f"Scan History (Last {len(scans)} scans)",
|
|
78
|
+
show_header=True,
|
|
79
|
+
header_style="bold cyan",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
table.add_column("Scan ID", style="cyan", no_wrap=True)
|
|
83
|
+
table.add_column("Timestamp", style="green")
|
|
84
|
+
table.add_column("Region", style="yellow")
|
|
85
|
+
table.add_column("Score", style="magenta", justify="right")
|
|
86
|
+
table.add_column("Status", style="bold")
|
|
87
|
+
table.add_column("Tests", justify="right")
|
|
88
|
+
|
|
89
|
+
for scan in scans:
|
|
90
|
+
scan_id = scan["scan_id"]
|
|
91
|
+
timestamp = scan["timestamp"][:19] # Remove microseconds
|
|
92
|
+
region = scan["region"]
|
|
93
|
+
score = scan["summary"]["overall_score"]
|
|
94
|
+
status = scan["summary"]["compliance_status"]
|
|
95
|
+
total_tests = scan["summary"]["total_tests"]
|
|
96
|
+
passed = scan["summary"]["passed_tests"]
|
|
97
|
+
failed = scan["summary"]["failed_tests"]
|
|
98
|
+
|
|
99
|
+
# Color code status
|
|
100
|
+
if status == "COMPLIANT":
|
|
101
|
+
status_display = f"[green]✅ {status}[/green]"
|
|
102
|
+
else:
|
|
103
|
+
status_display = f"[red]❌ {status}[/red]"
|
|
104
|
+
|
|
105
|
+
# Color code score
|
|
106
|
+
if score >= 90:
|
|
107
|
+
score_display = f"[green]{score}%[/green]"
|
|
108
|
+
elif score >= 70:
|
|
109
|
+
score_display = f"[yellow]{score}%[/yellow]"
|
|
110
|
+
else:
|
|
111
|
+
score_display = f"[red]{score}%[/red]"
|
|
112
|
+
|
|
113
|
+
tests_display = f"{passed}/{total_tests}"
|
|
114
|
+
if failed > 0:
|
|
115
|
+
tests_display += f" ([red]{failed} failed[/red])"
|
|
116
|
+
|
|
117
|
+
table.add_row(
|
|
118
|
+
scan_id,
|
|
119
|
+
timestamp,
|
|
120
|
+
region,
|
|
121
|
+
score_display,
|
|
122
|
+
status_display,
|
|
123
|
+
tests_display,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
console.print()
|
|
127
|
+
console.print(table)
|
|
128
|
+
console.print()
|
|
129
|
+
|
|
130
|
+
# Show detailed results if requested
|
|
131
|
+
if details:
|
|
132
|
+
console.print("[bold cyan]Detailed Test Results:[/bold cyan]\n")
|
|
133
|
+
for scan in scans:
|
|
134
|
+
console.print(f"[bold]{scan['scan_id']}[/bold]")
|
|
135
|
+
console.print(f" Timestamp: {scan['timestamp']}")
|
|
136
|
+
console.print(f" Region: {scan['region']}")
|
|
137
|
+
console.print(f" Score: {scan['summary']['overall_score']}%")
|
|
138
|
+
console.print(" Test Results:")
|
|
139
|
+
|
|
140
|
+
for test_result in scan.get("test_results", []):
|
|
141
|
+
status_emoji = {
|
|
142
|
+
"passed": "✅",
|
|
143
|
+
"warning": "⚠️",
|
|
144
|
+
"failed": "❌",
|
|
145
|
+
"error": "🚫",
|
|
146
|
+
}.get(test_result["status"], "❓")
|
|
147
|
+
|
|
148
|
+
console.print(
|
|
149
|
+
f" {status_emoji} {test_result['test_name']}: "
|
|
150
|
+
f"{test_result['score']}% "
|
|
151
|
+
f"({test_result['findings_count']} findings)"
|
|
152
|
+
)
|
|
153
|
+
console.print()
|
|
154
|
+
|
|
155
|
+
# Show helpful tips
|
|
156
|
+
console.print("[dim]💡 Tip: Use 'complio compare <scan_id1> <scan_id2>' to compare two scans[/dim]\n")
|
|
157
|
+
|
|
158
|
+
logger.info("history_viewed", scan_count=len(scans))
|
|
159
|
+
|
|
160
|
+
except Exception as e:
|
|
161
|
+
logger.error("history_command_failed", error=str(e))
|
|
162
|
+
console.print(f"\n[red]❌ Failed to retrieve scan history: {str(e)}[/red]\n")
|
|
163
|
+
raise click.Abort()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@click.command()
|
|
167
|
+
@click.argument("scan_id_1")
|
|
168
|
+
@click.argument("scan_id_2")
|
|
169
|
+
@click.option(
|
|
170
|
+
"--detailed",
|
|
171
|
+
is_flag=True,
|
|
172
|
+
default=False,
|
|
173
|
+
help="Show test-by-test comparison",
|
|
174
|
+
)
|
|
175
|
+
def compare(scan_id_1: str, scan_id_2: str, detailed: bool) -> None:
|
|
176
|
+
"""Compare two scans to track compliance changes.
|
|
177
|
+
|
|
178
|
+
Compares two compliance scans and shows the differences in scores,
|
|
179
|
+
test results, and overall compliance status. Use this to track
|
|
180
|
+
improvement or identify regressions.
|
|
181
|
+
|
|
182
|
+
Arguments:
|
|
183
|
+
|
|
184
|
+
SCAN_ID_1 First scan ID (newer scan)
|
|
185
|
+
|
|
186
|
+
SCAN_ID_2 Second scan ID (older scan for comparison)
|
|
187
|
+
|
|
188
|
+
Examples:
|
|
189
|
+
|
|
190
|
+
# Compare two scans
|
|
191
|
+
$ complio compare scan_20260107_162335_abc123 scan_20260106_143022_xyz789
|
|
192
|
+
|
|
193
|
+
# Compare with detailed test breakdown
|
|
194
|
+
$ complio compare scan_20260107_... scan_20260106_... --detailed
|
|
195
|
+
|
|
196
|
+
Note: Scan IDs can be found using 'complio history'
|
|
197
|
+
"""
|
|
198
|
+
console = Console()
|
|
199
|
+
logger = get_logger(__name__)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
# Get comparison data
|
|
203
|
+
comparison = compare_scans(scan_id_1, scan_id_2)
|
|
204
|
+
|
|
205
|
+
scan1 = comparison["scan1"]
|
|
206
|
+
scan2 = comparison["scan2"]
|
|
207
|
+
diff = comparison["differences"]
|
|
208
|
+
|
|
209
|
+
# Display comparison header
|
|
210
|
+
console.print("\n[bold cyan]Scan Comparison[/bold cyan]\n")
|
|
211
|
+
|
|
212
|
+
# Comparison table
|
|
213
|
+
table = Table(show_header=True, header_style="bold")
|
|
214
|
+
table.add_column("Metric", style="cyan")
|
|
215
|
+
table.add_column("Scan 1 (Newer)", style="green", justify="right")
|
|
216
|
+
table.add_column("Scan 2 (Older)", style="yellow", justify="right")
|
|
217
|
+
table.add_column("Change", style="magenta", justify="right")
|
|
218
|
+
|
|
219
|
+
# Scan IDs
|
|
220
|
+
table.add_row(
|
|
221
|
+
"Scan ID",
|
|
222
|
+
scan1["scan_id"][:25] + "...",
|
|
223
|
+
scan2["scan_id"][:25] + "...",
|
|
224
|
+
"-",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
# Timestamps
|
|
228
|
+
table.add_row(
|
|
229
|
+
"Timestamp",
|
|
230
|
+
scan1["timestamp"][:19],
|
|
231
|
+
scan2["timestamp"][:19],
|
|
232
|
+
"-",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Overall Score
|
|
236
|
+
score_change = diff["score_change"]
|
|
237
|
+
if score_change > 0:
|
|
238
|
+
score_display = f"[green]+{score_change}%[/green]"
|
|
239
|
+
score_emoji = "📈"
|
|
240
|
+
elif score_change < 0:
|
|
241
|
+
score_display = f"[red]{score_change}%[/red]"
|
|
242
|
+
score_emoji = "📉"
|
|
243
|
+
else:
|
|
244
|
+
score_display = "[dim]No change[/dim]"
|
|
245
|
+
score_emoji = "➡️"
|
|
246
|
+
|
|
247
|
+
table.add_row(
|
|
248
|
+
"Overall Score",
|
|
249
|
+
f"{scan1['score']}%",
|
|
250
|
+
f"{scan2['score']}%",
|
|
251
|
+
f"{score_emoji} {score_display}",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Passed Tests
|
|
255
|
+
passed_change = diff["passed_change"]
|
|
256
|
+
if passed_change > 0:
|
|
257
|
+
passed_display = f"[green]+{passed_change}[/green]"
|
|
258
|
+
elif passed_change < 0:
|
|
259
|
+
passed_display = f"[red]{passed_change}[/red]"
|
|
260
|
+
else:
|
|
261
|
+
passed_display = "[dim]No change[/dim]"
|
|
262
|
+
|
|
263
|
+
table.add_row(
|
|
264
|
+
"Passed Tests",
|
|
265
|
+
str(scan1["passed"]),
|
|
266
|
+
str(scan2["passed"]),
|
|
267
|
+
passed_display,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Failed Tests
|
|
271
|
+
failed_change = diff["failed_change"]
|
|
272
|
+
if failed_change < 0:
|
|
273
|
+
failed_display = f"[green]{failed_change}[/green]"
|
|
274
|
+
elif failed_change > 0:
|
|
275
|
+
failed_display = f"[red]+{failed_change}[/red]"
|
|
276
|
+
else:
|
|
277
|
+
failed_display = "[dim]No change[/dim]"
|
|
278
|
+
|
|
279
|
+
table.add_row(
|
|
280
|
+
"Failed Tests",
|
|
281
|
+
str(scan1["failed"]),
|
|
282
|
+
str(scan2["failed"]),
|
|
283
|
+
failed_display,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
console.print(table)
|
|
287
|
+
console.print()
|
|
288
|
+
|
|
289
|
+
# Summary message
|
|
290
|
+
direction = diff["score_change_direction"]
|
|
291
|
+
if direction == "improved":
|
|
292
|
+
console.print(
|
|
293
|
+
f"[bold green]✅ Compliance has improved by {abs(score_change)}%[/bold green]\n"
|
|
294
|
+
)
|
|
295
|
+
elif direction == "declined":
|
|
296
|
+
console.print(
|
|
297
|
+
f"[bold red]⚠️ Compliance has declined by {abs(score_change)}%[/bold red]\n"
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
console.print("[bold]➡️ No change in compliance score[/bold]\n")
|
|
301
|
+
|
|
302
|
+
# Detailed comparison if requested
|
|
303
|
+
if detailed:
|
|
304
|
+
console.print("[bold cyan]Test-by-Test Comparison:[/bold cyan]\n")
|
|
305
|
+
console.print("[dim]Note: Detailed test comparison requires scan details[/dim]\n")
|
|
306
|
+
|
|
307
|
+
# Get full scan details
|
|
308
|
+
scan1_full = get_scan_by_id(scan_id_1)
|
|
309
|
+
scan2_full = get_scan_by_id(scan_id_2)
|
|
310
|
+
|
|
311
|
+
if scan1_full and scan2_full:
|
|
312
|
+
# Build test result maps
|
|
313
|
+
test_map_1 = {
|
|
314
|
+
t["test_id"]: t for t in scan1_full.get("test_results", [])
|
|
315
|
+
}
|
|
316
|
+
test_map_2 = {
|
|
317
|
+
t["test_id"]: t for t in scan2_full.get("test_results", [])
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
all_tests = set(test_map_1.keys()) | set(test_map_2.keys())
|
|
321
|
+
|
|
322
|
+
for test_id in sorted(all_tests):
|
|
323
|
+
test1 = test_map_1.get(test_id)
|
|
324
|
+
test2 = test_map_2.get(test_id)
|
|
325
|
+
|
|
326
|
+
if test1 and test2:
|
|
327
|
+
score_diff = test1["score"] - test2["score"]
|
|
328
|
+
if score_diff != 0:
|
|
329
|
+
if score_diff > 0:
|
|
330
|
+
change = f"[green]+{score_diff}%[/green]"
|
|
331
|
+
else:
|
|
332
|
+
change = f"[red]{score_diff}%[/red]"
|
|
333
|
+
console.print(
|
|
334
|
+
f" • {test1['test_name']}: "
|
|
335
|
+
f"{test1['score']}% → {test2['score']}% ({change})"
|
|
336
|
+
)
|
|
337
|
+
elif test1:
|
|
338
|
+
console.print(
|
|
339
|
+
f" • [green]NEW:[/green] {test1['test_name']} ({test1['score']}%)"
|
|
340
|
+
)
|
|
341
|
+
elif test2:
|
|
342
|
+
console.print(
|
|
343
|
+
f" • [red]REMOVED:[/red] {test2['test_name']} ({test2['score']}%)"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
console.print()
|
|
347
|
+
|
|
348
|
+
logger.info(
|
|
349
|
+
"scans_compared",
|
|
350
|
+
scan_id_1=scan_id_1,
|
|
351
|
+
scan_id_2=scan_id_2,
|
|
352
|
+
score_change=score_change,
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
except ValueError as e:
|
|
356
|
+
logger.error("compare_failed", error=str(e))
|
|
357
|
+
console.print(f"\n[red]❌ {str(e)}[/red]")
|
|
358
|
+
console.print("\n[dim]💡 Tip: Use 'complio history' to see available scan IDs[/dim]\n")
|
|
359
|
+
raise click.Abort()
|
|
360
|
+
except Exception as e:
|
|
361
|
+
logger.error("compare_command_failed", error=str(e))
|
|
362
|
+
console.print(f"\n[red]❌ Failed to compare scans: {str(e)}[/red]\n")
|
|
363
|
+
raise click.Abort()
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@click.command(name="clear-history")
|
|
367
|
+
@click.option(
|
|
368
|
+
"--days",
|
|
369
|
+
"-d",
|
|
370
|
+
default=30,
|
|
371
|
+
type=int,
|
|
372
|
+
help="Keep scans from last N days (default: 30)",
|
|
373
|
+
show_default=True,
|
|
374
|
+
)
|
|
375
|
+
@click.option(
|
|
376
|
+
"--force",
|
|
377
|
+
"-f",
|
|
378
|
+
is_flag=True,
|
|
379
|
+
help="Skip confirmation prompt",
|
|
380
|
+
)
|
|
381
|
+
def clear_history_cmd(days: int, force: bool) -> None:
|
|
382
|
+
"""Clear old scan history to free up disk space.
|
|
383
|
+
|
|
384
|
+
Removes scan history files older than the specified number of days.
|
|
385
|
+
This helps manage disk space while retaining recent compliance data.
|
|
386
|
+
|
|
387
|
+
Examples:
|
|
388
|
+
|
|
389
|
+
# Clear scans older than 30 days
|
|
390
|
+
$ complio clear-history
|
|
391
|
+
|
|
392
|
+
# Keep only last 7 days of scans
|
|
393
|
+
$ complio clear-history --days 7
|
|
394
|
+
|
|
395
|
+
# Clear without confirmation
|
|
396
|
+
$ complio clear-history --days 30 --force
|
|
397
|
+
|
|
398
|
+
Note: This action cannot be undone.
|
|
399
|
+
"""
|
|
400
|
+
console = Console()
|
|
401
|
+
logger = get_logger(__name__)
|
|
402
|
+
|
|
403
|
+
try:
|
|
404
|
+
# Get current scan count
|
|
405
|
+
scans = get_scan_history(limit=1000)
|
|
406
|
+
total_scans = len(scans)
|
|
407
|
+
|
|
408
|
+
if total_scans == 0:
|
|
409
|
+
console.print("\n[yellow]No scan history to clear[/yellow]\n")
|
|
410
|
+
return
|
|
411
|
+
|
|
412
|
+
# Confirmation unless --force
|
|
413
|
+
if not force:
|
|
414
|
+
console.print(f"\n[yellow]⚠️ This will delete scans older than {days} days[/yellow]")
|
|
415
|
+
console.print(f"Current total scans: {total_scans}\n")
|
|
416
|
+
|
|
417
|
+
if not click.confirm("Are you sure?", default=False):
|
|
418
|
+
console.print("\n[dim]Operation cancelled[/dim]\n")
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
# Clear old history
|
|
422
|
+
deleted_count = clear_old_history(keep_days=days)
|
|
423
|
+
|
|
424
|
+
if deleted_count > 0:
|
|
425
|
+
console.print(
|
|
426
|
+
f"\n[green]✅ Deleted {deleted_count} old scan(s)[/green]"
|
|
427
|
+
)
|
|
428
|
+
remaining = total_scans - deleted_count
|
|
429
|
+
console.print(f"[dim]Remaining scans: {remaining}[/dim]\n")
|
|
430
|
+
logger.info("history_cleared", deleted_count=deleted_count, days=days)
|
|
431
|
+
else:
|
|
432
|
+
console.print(
|
|
433
|
+
f"\n[dim]No scans older than {days} days found[/dim]\n"
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
except Exception as e:
|
|
437
|
+
logger.error("clear_history_failed", error=str(e))
|
|
438
|
+
console.print(f"\n[red]❌ Failed to clear history: {str(e)}[/red]\n")
|
|
439
|
+
raise click.Abort()
|