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.
Files changed (79) hide show
  1. CHANGELOG.md +208 -0
  2. README.md +343 -0
  3. complio/__init__.py +48 -0
  4. complio/cli/__init__.py +0 -0
  5. complio/cli/banner.py +87 -0
  6. complio/cli/commands/__init__.py +0 -0
  7. complio/cli/commands/history.py +439 -0
  8. complio/cli/commands/scan.py +700 -0
  9. complio/cli/main.py +115 -0
  10. complio/cli/output.py +338 -0
  11. complio/config/__init__.py +17 -0
  12. complio/config/settings.py +333 -0
  13. complio/connectors/__init__.py +9 -0
  14. complio/connectors/aws/__init__.py +0 -0
  15. complio/connectors/aws/client.py +342 -0
  16. complio/connectors/base.py +135 -0
  17. complio/core/__init__.py +10 -0
  18. complio/core/registry.py +228 -0
  19. complio/core/runner.py +351 -0
  20. complio/py.typed +0 -0
  21. complio/reporters/__init__.py +7 -0
  22. complio/reporters/generator.py +417 -0
  23. complio/tests_library/__init__.py +0 -0
  24. complio/tests_library/base.py +492 -0
  25. complio/tests_library/identity/__init__.py +0 -0
  26. complio/tests_library/identity/access_key_rotation.py +302 -0
  27. complio/tests_library/identity/mfa_enforcement.py +327 -0
  28. complio/tests_library/identity/root_account_protection.py +470 -0
  29. complio/tests_library/infrastructure/__init__.py +0 -0
  30. complio/tests_library/infrastructure/cloudtrail_encryption.py +286 -0
  31. complio/tests_library/infrastructure/cloudtrail_log_validation.py +274 -0
  32. complio/tests_library/infrastructure/cloudtrail_logging.py +400 -0
  33. complio/tests_library/infrastructure/ebs_encryption.py +244 -0
  34. complio/tests_library/infrastructure/ec2_security_groups.py +321 -0
  35. complio/tests_library/infrastructure/iam_password_policy.py +460 -0
  36. complio/tests_library/infrastructure/nacl_security.py +356 -0
  37. complio/tests_library/infrastructure/rds_encryption.py +252 -0
  38. complio/tests_library/infrastructure/s3_encryption.py +301 -0
  39. complio/tests_library/infrastructure/s3_public_access.py +369 -0
  40. complio/tests_library/infrastructure/secrets_manager_encryption.py +248 -0
  41. complio/tests_library/infrastructure/vpc_flow_logs.py +287 -0
  42. complio/tests_library/logging/__init__.py +0 -0
  43. complio/tests_library/logging/cloudwatch_alarms.py +354 -0
  44. complio/tests_library/logging/cloudwatch_logs_encryption.py +281 -0
  45. complio/tests_library/logging/cloudwatch_retention.py +252 -0
  46. complio/tests_library/logging/config_enabled.py +393 -0
  47. complio/tests_library/logging/eventbridge_rules.py +460 -0
  48. complio/tests_library/logging/guardduty_enabled.py +436 -0
  49. complio/tests_library/logging/security_hub_enabled.py +416 -0
  50. complio/tests_library/logging/sns_encryption.py +273 -0
  51. complio/tests_library/network/__init__.py +0 -0
  52. complio/tests_library/network/alb_nlb_security.py +421 -0
  53. complio/tests_library/network/api_gateway_security.py +452 -0
  54. complio/tests_library/network/cloudfront_https.py +332 -0
  55. complio/tests_library/network/direct_connect_security.py +343 -0
  56. complio/tests_library/network/nacl_configuration.py +367 -0
  57. complio/tests_library/network/network_firewall.py +355 -0
  58. complio/tests_library/network/transit_gateway_security.py +318 -0
  59. complio/tests_library/network/vpc_endpoints_security.py +339 -0
  60. complio/tests_library/network/vpn_security.py +333 -0
  61. complio/tests_library/network/waf_configuration.py +428 -0
  62. complio/tests_library/security/__init__.py +0 -0
  63. complio/tests_library/security/kms_key_rotation.py +314 -0
  64. complio/tests_library/storage/__init__.py +0 -0
  65. complio/tests_library/storage/backup_encryption.py +288 -0
  66. complio/tests_library/storage/dynamodb_encryption.py +280 -0
  67. complio/tests_library/storage/efs_encryption.py +257 -0
  68. complio/tests_library/storage/elasticache_encryption.py +370 -0
  69. complio/tests_library/storage/redshift_encryption.py +252 -0
  70. complio/tests_library/storage/s3_versioning.py +264 -0
  71. complio/utils/__init__.py +26 -0
  72. complio/utils/errors.py +179 -0
  73. complio/utils/exceptions.py +151 -0
  74. complio/utils/history.py +243 -0
  75. complio/utils/logger.py +391 -0
  76. complio-0.1.1.dist-info/METADATA +385 -0
  77. complio-0.1.1.dist-info/RECORD +79 -0
  78. complio-0.1.1.dist-info/WHEEL +4 -0
  79. 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()