vibeguard-cli 1.0.0__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.
- vibeguard/__init__.py +3 -0
- vibeguard/cli/__init__.py +1 -0
- vibeguard/cli/apply.py +413 -0
- vibeguard/cli/auth_cmd.py +318 -0
- vibeguard/cli/baseline_cmd.py +286 -0
- vibeguard/cli/config_cmd.py +252 -0
- vibeguard/cli/display.py +356 -0
- vibeguard/cli/doctor.py +228 -0
- vibeguard/cli/fix.py +977 -0
- vibeguard/cli/import_cmd.py +180 -0
- vibeguard/cli/init_cmd.py +113 -0
- vibeguard/cli/keys.py +193 -0
- vibeguard/cli/live_cmd.py +564 -0
- vibeguard/cli/main.py +667 -0
- vibeguard/cli/patch.py +805 -0
- vibeguard/cli/report.py +106 -0
- vibeguard/cli/scan.py +1227 -0
- vibeguard/core/__init__.py +1 -0
- vibeguard/core/auth.py +402 -0
- vibeguard/core/baseline.py +212 -0
- vibeguard/core/bootstrap.py +303 -0
- vibeguard/core/cache.py +77 -0
- vibeguard/core/config.py +99 -0
- vibeguard/core/dedup.py +168 -0
- vibeguard/core/downloader.py +222 -0
- vibeguard/core/example_detector.py +159 -0
- vibeguard/core/exit_codes.py +19 -0
- vibeguard/core/ignore.py +243 -0
- vibeguard/core/keyring.py +188 -0
- vibeguard/core/license.py +166 -0
- vibeguard/core/llm.py +206 -0
- vibeguard/core/path_classifier.py +152 -0
- vibeguard/core/repo_detector.py +143 -0
- vibeguard/core/sarif_import.py +342 -0
- vibeguard/core/triage.py +205 -0
- vibeguard/core/url_validator.py +259 -0
- vibeguard/core/validate.py +174 -0
- vibeguard/models/__init__.py +24 -0
- vibeguard/models/auth.py +92 -0
- vibeguard/models/baseline.py +105 -0
- vibeguard/models/finding.py +78 -0
- vibeguard/models/patch.py +164 -0
- vibeguard/models/scan_result.py +190 -0
- vibeguard/models/triage.py +53 -0
- vibeguard/reporters/__init__.py +7 -0
- vibeguard/reporters/badge.py +103 -0
- vibeguard/reporters/html.py +920 -0
- vibeguard/reporters/sarif.py +175 -0
- vibeguard/scanners/__init__.py +130 -0
- vibeguard/scanners/manifests/bandit.toml +39 -0
- vibeguard/scanners/manifests/cargo_audit.toml +31 -0
- vibeguard/scanners/manifests/checkov.toml +37 -0
- vibeguard/scanners/manifests/dockle.toml +46 -0
- vibeguard/scanners/manifests/gitleaks.toml +48 -0
- vibeguard/scanners/manifests/npm_audit.toml +31 -0
- vibeguard/scanners/manifests/nuclei.toml +58 -0
- vibeguard/scanners/manifests/pip_audit.toml +36 -0
- vibeguard/scanners/manifests/semgrep.toml +43 -0
- vibeguard/scanners/manifests/trivy.toml +50 -0
- vibeguard/scanners/manifests/trufflehog.toml +48 -0
- vibeguard/scanners/parsers/__init__.py +1 -0
- vibeguard/scanners/parsers/bandit.py +94 -0
- vibeguard/scanners/parsers/cargo_audit.py +185 -0
- vibeguard/scanners/parsers/checkov.py +179 -0
- vibeguard/scanners/parsers/dockle.py +185 -0
- vibeguard/scanners/parsers/gitleaks.py +95 -0
- vibeguard/scanners/parsers/npm_audit.py +219 -0
- vibeguard/scanners/parsers/nuclei.py +247 -0
- vibeguard/scanners/parsers/pip_audit.py +166 -0
- vibeguard/scanners/parsers/semgrep.py +110 -0
- vibeguard/scanners/parsers/trivy.py +86 -0
- vibeguard/scanners/parsers/trufflehog.py +150 -0
- vibeguard/scanners/runners/__init__.py +7 -0
- vibeguard/scanners/runners/base.py +41 -0
- vibeguard/scanners/runners/docker.py +86 -0
- vibeguard/scanners/runners/local.py +144 -0
- vibeguard_cli-1.0.0.dist-info/METADATA +223 -0
- vibeguard_cli-1.0.0.dist-info/RECORD +81 -0
- vibeguard_cli-1.0.0.dist-info/WHEEL +4 -0
- vibeguard_cli-1.0.0.dist-info/entry_points.txt +2 -0
- vibeguard_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Auth command - manage Pro license authentication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import threading
|
|
7
|
+
from datetime import UTC, datetime
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.live import Live
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.spinner import Spinner
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from vibeguard.cli.display import BRAND_COLOR, VIBEGUARD_SPINNER_NAME, get_console
|
|
17
|
+
from vibeguard.core.auth import (
|
|
18
|
+
AuthError,
|
|
19
|
+
LicenseError,
|
|
20
|
+
NetworkError,
|
|
21
|
+
activate_license,
|
|
22
|
+
clear_auth_cache,
|
|
23
|
+
get_cached_token,
|
|
24
|
+
get_or_create_machine_id,
|
|
25
|
+
get_token_time_remaining,
|
|
26
|
+
mask_license_key,
|
|
27
|
+
save_token_to_cache,
|
|
28
|
+
should_refresh_token,
|
|
29
|
+
)
|
|
30
|
+
from vibeguard.core.exit_codes import ExitCode
|
|
31
|
+
from vibeguard.core.keyring import get_configured_providers
|
|
32
|
+
|
|
33
|
+
app = typer.Typer(
|
|
34
|
+
name="auth",
|
|
35
|
+
help="Manage Pro license authentication.",
|
|
36
|
+
invoke_without_command=True,
|
|
37
|
+
no_args_is_help=True,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
console = get_console()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.callback(invoke_without_command=True)
|
|
44
|
+
def auth_callback(ctx: typer.Context) -> None:
|
|
45
|
+
"""Manage Pro license authentication.
|
|
46
|
+
|
|
47
|
+
Activate your Pro license to unlock premium features like
|
|
48
|
+
automated patch generation and application.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
vibeguard auth login VGPRO-XXXX-XXXX-XXXX # Activate license
|
|
52
|
+
vibeguard auth status # Check license status
|
|
53
|
+
vibeguard auth logout # Deactivate this machine
|
|
54
|
+
"""
|
|
55
|
+
if ctx.invoked_subcommand is None:
|
|
56
|
+
console.print(ctx.get_help())
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _activate_with_spinner(license_key: str) -> tuple[bool, str, object | None]:
|
|
60
|
+
"""Run activation in background thread with spinner.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Tuple of (success, message, response_or_error)
|
|
64
|
+
"""
|
|
65
|
+
result: tuple[bool, str, object | None] = (False, "Activation failed", None)
|
|
66
|
+
done = threading.Event()
|
|
67
|
+
|
|
68
|
+
def do_activate() -> None:
|
|
69
|
+
nonlocal result
|
|
70
|
+
try:
|
|
71
|
+
response = asyncio.run(activate_license(license_key))
|
|
72
|
+
result = (True, "License activated successfully!", response)
|
|
73
|
+
except LicenseError as e:
|
|
74
|
+
result = (False, str(e), e)
|
|
75
|
+
except NetworkError as e:
|
|
76
|
+
result = (False, str(e), e)
|
|
77
|
+
except Exception as e:
|
|
78
|
+
result = (False, f"Unexpected error: {e}", e)
|
|
79
|
+
finally:
|
|
80
|
+
done.set()
|
|
81
|
+
|
|
82
|
+
thread = threading.Thread(target=do_activate, daemon=True)
|
|
83
|
+
thread.start()
|
|
84
|
+
|
|
85
|
+
spinner = Spinner(VIBEGUARD_SPINNER_NAME, text=Text(" Activating license...", style=BRAND_COLOR))
|
|
86
|
+
with Live(spinner, console=console, transient=True):
|
|
87
|
+
done.wait(timeout=45)
|
|
88
|
+
|
|
89
|
+
if not done.is_set():
|
|
90
|
+
return (False, "Activation timed out. Please try again.", None)
|
|
91
|
+
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app.command("login")
|
|
96
|
+
def login(
|
|
97
|
+
license_key: str | None = typer.Argument(
|
|
98
|
+
None,
|
|
99
|
+
help="Your VibeGuard Pro license key (e.g., VGPRO-XXXX-XXXX-XXXX)",
|
|
100
|
+
),
|
|
101
|
+
) -> None:
|
|
102
|
+
"""Activate Pro license for this machine.
|
|
103
|
+
|
|
104
|
+
Your license key can be found in your account at https://vibeguard.co/account
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
vibeguard auth login VGPRO-XXXX-XXXX-XXXX
|
|
108
|
+
"""
|
|
109
|
+
# Handle missing argument
|
|
110
|
+
if license_key is None:
|
|
111
|
+
console.print("[red]Error:[/red] Missing license key.\n")
|
|
112
|
+
console.print("[bold]Usage:[/bold] vibeguard auth login <license-key>\n")
|
|
113
|
+
console.print("[bold]Example:[/bold] vibeguard auth login VGPRO-XXXX-XXXX-XXXX\n")
|
|
114
|
+
console.print("Get your license key at: [link=https://vibeguard.co/pricing]https://vibeguard.co/pricing[/link]")
|
|
115
|
+
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
116
|
+
|
|
117
|
+
# Check if already logged in
|
|
118
|
+
existing_token = get_cached_token()
|
|
119
|
+
if existing_token is not None:
|
|
120
|
+
console.print("[yellow]You are already logged in.[/yellow]")
|
|
121
|
+
console.print(f"Current plan: [bold]{existing_token.plan or 'Pro'}[/bold]")
|
|
122
|
+
remaining = get_token_time_remaining(existing_token)
|
|
123
|
+
if remaining.total_seconds() > 0:
|
|
124
|
+
days = remaining.days
|
|
125
|
+
hours = remaining.seconds // 3600
|
|
126
|
+
console.print(f"Token expires in: {days} days, {hours} hours")
|
|
127
|
+
console.print("\nTo switch accounts, run [bold]vibeguard auth logout[/bold] first.")
|
|
128
|
+
raise typer.Exit(ExitCode.SUCCESS)
|
|
129
|
+
|
|
130
|
+
# Activate
|
|
131
|
+
console.print(f"Activating license: [dim]{mask_license_key(license_key)}[/dim]\n")
|
|
132
|
+
|
|
133
|
+
success, message, response = _activate_with_spinner(license_key)
|
|
134
|
+
|
|
135
|
+
if success and response is not None:
|
|
136
|
+
# Save token to cache
|
|
137
|
+
token = save_token_to_cache(response)
|
|
138
|
+
|
|
139
|
+
console.print(f"[green]{message}[/green]\n")
|
|
140
|
+
|
|
141
|
+
# Show details
|
|
142
|
+
table = Table(show_header=False, box=None)
|
|
143
|
+
table.add_column("Field", style="dim")
|
|
144
|
+
table.add_column("Value")
|
|
145
|
+
|
|
146
|
+
table.add_row("Plan", token.plan or "Pro")
|
|
147
|
+
table.add_row("Machine ID", get_or_create_machine_id()[:8] + "...")
|
|
148
|
+
|
|
149
|
+
remaining = get_token_time_remaining(token)
|
|
150
|
+
days = remaining.days
|
|
151
|
+
hours = remaining.seconds // 3600
|
|
152
|
+
table.add_row("Token expires", f"in {days} days, {hours} hours")
|
|
153
|
+
|
|
154
|
+
if token.entitlements:
|
|
155
|
+
table.add_row("Entitlements", ", ".join(token.entitlements))
|
|
156
|
+
|
|
157
|
+
console.print(table)
|
|
158
|
+
console.print()
|
|
159
|
+
|
|
160
|
+
# Check if LLM key is configured
|
|
161
|
+
providers = get_configured_providers()
|
|
162
|
+
if not providers:
|
|
163
|
+
console.print(
|
|
164
|
+
Panel(
|
|
165
|
+
"[yellow]Pro license activated![/yellow]\n\n"
|
|
166
|
+
"To use patch generation, configure an LLM API key:\n"
|
|
167
|
+
" [bold]vibeguard keys set openai <your-api-key>[/bold]\n"
|
|
168
|
+
" [bold]vibeguard keys set anthropic <your-api-key>[/bold]",
|
|
169
|
+
title="Next Step",
|
|
170
|
+
border_style="yellow",
|
|
171
|
+
)
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
console.print("[green]You're all set![/green] Try [bold]vibeguard patch[/bold] to generate fixes.")
|
|
175
|
+
|
|
176
|
+
else:
|
|
177
|
+
console.print(f"[red]Activation failed:[/red] {message}")
|
|
178
|
+
|
|
179
|
+
# Provide helpful suggestions
|
|
180
|
+
if isinstance(response, NetworkError):
|
|
181
|
+
console.print("\n[dim]Tips:[/dim]")
|
|
182
|
+
console.print(" - Check your internet connection")
|
|
183
|
+
console.print(" - Try again in a few moments")
|
|
184
|
+
console.print(" - Contact support@vibeguard.co if the issue persists")
|
|
185
|
+
elif isinstance(response, LicenseError):
|
|
186
|
+
console.print("\n[dim]Tips:[/dim]")
|
|
187
|
+
console.print(" - Verify your license key is correct")
|
|
188
|
+
console.print(" - Check your account at https://vibeguard.co/account")
|
|
189
|
+
console.print(" - Contact support@vibeguard.co for help")
|
|
190
|
+
|
|
191
|
+
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@app.command("status")
|
|
195
|
+
def status() -> None:
|
|
196
|
+
"""Show current license status.
|
|
197
|
+
|
|
198
|
+
Displays:
|
|
199
|
+
- License activation status
|
|
200
|
+
- Token expiry
|
|
201
|
+
- Current plan
|
|
202
|
+
- Available entitlements
|
|
203
|
+
- Configured LLM providers
|
|
204
|
+
|
|
205
|
+
Example:
|
|
206
|
+
vibeguard auth status
|
|
207
|
+
"""
|
|
208
|
+
token = get_cached_token()
|
|
209
|
+
providers = get_configured_providers()
|
|
210
|
+
machine_id = get_or_create_machine_id()
|
|
211
|
+
|
|
212
|
+
# Build status table
|
|
213
|
+
table = Table(title="VibeGuard License Status", show_header=False)
|
|
214
|
+
table.add_column("Field", style="cyan")
|
|
215
|
+
table.add_column("Value")
|
|
216
|
+
|
|
217
|
+
if token is not None:
|
|
218
|
+
table.add_row("Status", "[green]Licensed (Pro)[/green]")
|
|
219
|
+
table.add_row("Plan", token.plan or "Pro")
|
|
220
|
+
|
|
221
|
+
# Token expiry
|
|
222
|
+
remaining = get_token_time_remaining(token)
|
|
223
|
+
if remaining.total_seconds() > 0:
|
|
224
|
+
days = remaining.days
|
|
225
|
+
hours = remaining.seconds // 3600
|
|
226
|
+
if days > 1:
|
|
227
|
+
expiry_str = f"in {days} days, {hours} hours"
|
|
228
|
+
elif days == 1:
|
|
229
|
+
expiry_str = f"in 1 day, {hours} hours"
|
|
230
|
+
else:
|
|
231
|
+
expiry_str = f"in {hours} hours"
|
|
232
|
+
|
|
233
|
+
if should_refresh_token(token):
|
|
234
|
+
expiry_str += " [yellow](refresh pending)[/yellow]"
|
|
235
|
+
|
|
236
|
+
table.add_row("Token expires", expiry_str)
|
|
237
|
+
else:
|
|
238
|
+
table.add_row("Token expires", "[red]Expired[/red]")
|
|
239
|
+
|
|
240
|
+
# Entitlements
|
|
241
|
+
if token.entitlements:
|
|
242
|
+
table.add_row("Entitlements", ", ".join(token.entitlements))
|
|
243
|
+
else:
|
|
244
|
+
table.add_row("Entitlements", "[dim]None[/dim]")
|
|
245
|
+
|
|
246
|
+
# Last refresh
|
|
247
|
+
if token.last_refresh:
|
|
248
|
+
refresh_str = token.last_refresh.strftime("%Y-%m-%d %H:%M UTC")
|
|
249
|
+
table.add_row("Last refresh", refresh_str)
|
|
250
|
+
|
|
251
|
+
else:
|
|
252
|
+
table.add_row("Status", "[yellow]Not licensed (Free tier)[/yellow]")
|
|
253
|
+
table.add_row("Plan", "Free")
|
|
254
|
+
|
|
255
|
+
# Machine ID
|
|
256
|
+
table.add_row("Machine ID", machine_id[:12] + "...")
|
|
257
|
+
|
|
258
|
+
# LLM providers
|
|
259
|
+
if providers:
|
|
260
|
+
table.add_row("LLM providers", ", ".join(providers))
|
|
261
|
+
else:
|
|
262
|
+
table.add_row("LLM providers", "[dim]None configured[/dim]")
|
|
263
|
+
|
|
264
|
+
console.print(table)
|
|
265
|
+
console.print()
|
|
266
|
+
|
|
267
|
+
# Actionable guidance
|
|
268
|
+
if token is None:
|
|
269
|
+
console.print(
|
|
270
|
+
Panel(
|
|
271
|
+
"Activate your Pro license to unlock patch generation:\n"
|
|
272
|
+
" [bold]vibeguard auth login <your-license-key>[/bold]\n\n"
|
|
273
|
+
"Get a license at: [link=https://vibeguard.co/pricing]https://vibeguard.co/pricing[/link]",
|
|
274
|
+
title="Upgrade to Pro",
|
|
275
|
+
border_style="yellow",
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
elif not providers:
|
|
279
|
+
console.print(
|
|
280
|
+
Panel(
|
|
281
|
+
"Configure an LLM API key to use patch generation:\n"
|
|
282
|
+
" [bold]vibeguard keys set openai <your-api-key>[/bold]\n"
|
|
283
|
+
" [bold]vibeguard keys set anthropic <your-api-key>[/bold]",
|
|
284
|
+
title="Configure LLM",
|
|
285
|
+
border_style="yellow",
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
else:
|
|
289
|
+
console.print("[green]Ready to use Pro features![/green] Try [bold]vibeguard patch[/bold]")
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@app.command("logout")
|
|
293
|
+
def logout() -> None:
|
|
294
|
+
"""Deactivate license and clear cached token.
|
|
295
|
+
|
|
296
|
+
Your license key remains valid and can be activated
|
|
297
|
+
on another machine.
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
vibeguard auth logout
|
|
301
|
+
"""
|
|
302
|
+
token = get_cached_token()
|
|
303
|
+
|
|
304
|
+
if token is None:
|
|
305
|
+
console.print("[yellow]You are not currently logged in.[/yellow]")
|
|
306
|
+
raise typer.Exit(ExitCode.SUCCESS)
|
|
307
|
+
|
|
308
|
+
# Confirm logout
|
|
309
|
+
console.print(f"Current plan: [bold]{token.plan or 'Pro'}[/bold]")
|
|
310
|
+
console.print()
|
|
311
|
+
|
|
312
|
+
if clear_auth_cache():
|
|
313
|
+
console.print("[green]Logged out successfully.[/green]")
|
|
314
|
+
console.print()
|
|
315
|
+
console.print("[dim]Your license key is still valid.[/dim]")
|
|
316
|
+
console.print("[dim]You can reactivate on this or another machine.[/dim]")
|
|
317
|
+
else:
|
|
318
|
+
console.print("[yellow]No active session to clear.[/yellow]")
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Baseline command - manage security baselines for regression checking."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.table import Table
|
|
9
|
+
|
|
10
|
+
from vibeguard.cli.display import get_console
|
|
11
|
+
from vibeguard.core import cache
|
|
12
|
+
from vibeguard.core.baseline import (
|
|
13
|
+
delete_baseline,
|
|
14
|
+
list_baselines,
|
|
15
|
+
load_baseline,
|
|
16
|
+
save_baseline,
|
|
17
|
+
)
|
|
18
|
+
from vibeguard.core.exit_codes import ExitCode
|
|
19
|
+
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="baseline",
|
|
22
|
+
help="Manage security baselines for regression checking.",
|
|
23
|
+
invoke_without_command=True,
|
|
24
|
+
no_args_is_help=True,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
console = get_console()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@app.callback(invoke_without_command=True)
|
|
31
|
+
def baseline_callback(ctx: typer.Context) -> None:
|
|
32
|
+
"""Manage security baselines for regression checking.
|
|
33
|
+
|
|
34
|
+
Baselines capture your security findings at a point in time,
|
|
35
|
+
allowing you to detect regressions (new issues) in future scans.
|
|
36
|
+
|
|
37
|
+
Examples:
|
|
38
|
+
vibeguard baseline save # Save as "default"
|
|
39
|
+
vibeguard baseline save release-1.0 # Save with custom name
|
|
40
|
+
vibeguard baseline list # List all baselines
|
|
41
|
+
vibeguard baseline show default # Show baseline details
|
|
42
|
+
vibeguard baseline delete old-baseline # Delete a baseline
|
|
43
|
+
|
|
44
|
+
To scan against a baseline:
|
|
45
|
+
vibeguard scan . --baseline default
|
|
46
|
+
"""
|
|
47
|
+
if ctx.invoked_subcommand is None:
|
|
48
|
+
# Show help when no subcommand given
|
|
49
|
+
console.print(ctx.get_help())
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.command("save")
|
|
53
|
+
def save_cmd(
|
|
54
|
+
name: str = typer.Argument(
|
|
55
|
+
"default",
|
|
56
|
+
help="Name for the baseline (default: 'default')",
|
|
57
|
+
),
|
|
58
|
+
path: Path = typer.Option(
|
|
59
|
+
Path("."),
|
|
60
|
+
"--path",
|
|
61
|
+
"-p",
|
|
62
|
+
help="Repository path (default: current directory)",
|
|
63
|
+
),
|
|
64
|
+
force: bool = typer.Option(
|
|
65
|
+
False,
|
|
66
|
+
"--force",
|
|
67
|
+
"-f",
|
|
68
|
+
help="Overwrite existing baseline without confirmation",
|
|
69
|
+
),
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Save current scan as a baseline.
|
|
72
|
+
|
|
73
|
+
Saves the most recent scan results as a named baseline.
|
|
74
|
+
Future scans can compare against this baseline to detect regressions.
|
|
75
|
+
|
|
76
|
+
Examples:
|
|
77
|
+
vibeguard baseline save
|
|
78
|
+
vibeguard baseline save release-1.0
|
|
79
|
+
vibeguard baseline save --path /path/to/repo
|
|
80
|
+
vibeguard baseline save release-1.0 --force
|
|
81
|
+
"""
|
|
82
|
+
target = path.resolve()
|
|
83
|
+
|
|
84
|
+
# Load latest scan
|
|
85
|
+
result = cache.load_latest_scan(target)
|
|
86
|
+
if result is None:
|
|
87
|
+
console.print("[red]Error:[/red] No cached scan found.")
|
|
88
|
+
console.print("[dim]Run 'vibeguard scan .' first.[/dim]")
|
|
89
|
+
raise typer.Exit(ExitCode.NO_CACHE)
|
|
90
|
+
|
|
91
|
+
# Check if baseline exists
|
|
92
|
+
existing = load_baseline(target, name)
|
|
93
|
+
if existing and not force:
|
|
94
|
+
console.print(f"[yellow]Warning:[/yellow] Baseline '{name}' already exists.")
|
|
95
|
+
console.print(f" Created: {existing.created_at.strftime('%Y-%m-%d %H:%M')}")
|
|
96
|
+
console.print(f" Findings: {existing.actionable_count}")
|
|
97
|
+
overwrite = typer.confirm("Overwrite?", default=False)
|
|
98
|
+
if not overwrite:
|
|
99
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
100
|
+
raise typer.Exit(ExitCode.SUCCESS)
|
|
101
|
+
|
|
102
|
+
# Save baseline
|
|
103
|
+
baseline_path = save_baseline(result, target, name)
|
|
104
|
+
|
|
105
|
+
actionable_count = len(result.actionable_findings)
|
|
106
|
+
console.print(f"[green]Baseline saved:[/green] {name}")
|
|
107
|
+
console.print(f" Findings: {actionable_count} actionable")
|
|
108
|
+
console.print(f" Scanners: {', '.join(result.scanners_run)}")
|
|
109
|
+
console.print(f" Path: {baseline_path}")
|
|
110
|
+
console.print()
|
|
111
|
+
console.print("[dim]Compare future scans with:[/dim]")
|
|
112
|
+
console.print(f"[dim] vibeguard scan . --baseline {name}[/dim]")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.command("list")
|
|
116
|
+
def list_cmd(
|
|
117
|
+
path: Path = typer.Option(
|
|
118
|
+
Path("."),
|
|
119
|
+
"--path",
|
|
120
|
+
"-p",
|
|
121
|
+
help="Repository path",
|
|
122
|
+
),
|
|
123
|
+
) -> None:
|
|
124
|
+
"""List all saved baselines.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
vibeguard baseline list
|
|
128
|
+
"""
|
|
129
|
+
target = path.resolve()
|
|
130
|
+
baselines = list_baselines(target)
|
|
131
|
+
|
|
132
|
+
if not baselines:
|
|
133
|
+
console.print("[yellow]No baselines found.[/yellow]")
|
|
134
|
+
console.print("[dim]Create one with: vibeguard baseline save[/dim]")
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
table = Table(title="Saved Baselines")
|
|
138
|
+
table.add_column("Name", style="cyan")
|
|
139
|
+
table.add_column("Created", style="dim")
|
|
140
|
+
table.add_column("Findings", justify="right")
|
|
141
|
+
table.add_column("Scanners")
|
|
142
|
+
|
|
143
|
+
for baseline in baselines:
|
|
144
|
+
created = baseline.created_at.strftime("%Y-%m-%d %H:%M")
|
|
145
|
+
scanners = ", ".join(baseline.scanners_used[:3])
|
|
146
|
+
if len(baseline.scanners_used) > 3:
|
|
147
|
+
scanners += "..."
|
|
148
|
+
|
|
149
|
+
table.add_row(
|
|
150
|
+
baseline.name,
|
|
151
|
+
created,
|
|
152
|
+
str(baseline.actionable_count),
|
|
153
|
+
scanners,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
console.print(table)
|
|
157
|
+
console.print()
|
|
158
|
+
console.print("[dim]Compare scans with: vibeguard scan . --baseline <name>[/dim]")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command("show")
|
|
162
|
+
def show_cmd(
|
|
163
|
+
name: str = typer.Argument(
|
|
164
|
+
...,
|
|
165
|
+
help="Baseline name to show",
|
|
166
|
+
),
|
|
167
|
+
path: Path = typer.Option(
|
|
168
|
+
Path("."),
|
|
169
|
+
"--path",
|
|
170
|
+
"-p",
|
|
171
|
+
help="Repository path",
|
|
172
|
+
),
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Show details of a baseline.
|
|
175
|
+
|
|
176
|
+
Example:
|
|
177
|
+
vibeguard baseline show default
|
|
178
|
+
vibeguard baseline show release-1.0
|
|
179
|
+
"""
|
|
180
|
+
target = path.resolve()
|
|
181
|
+
baseline = load_baseline(target, name)
|
|
182
|
+
|
|
183
|
+
if baseline is None:
|
|
184
|
+
console.print(f"[red]Error:[/red] Baseline '{name}' not found.")
|
|
185
|
+
|
|
186
|
+
# Show available baselines
|
|
187
|
+
available = list_baselines(target)
|
|
188
|
+
if available:
|
|
189
|
+
console.print("\n[dim]Available baselines:[/dim]")
|
|
190
|
+
for b in available:
|
|
191
|
+
console.print(f" - {b.name}")
|
|
192
|
+
else:
|
|
193
|
+
console.print("\n[dim]No baselines saved. Create one with:[/dim]")
|
|
194
|
+
console.print("[dim] vibeguard baseline save[/dim]")
|
|
195
|
+
|
|
196
|
+
raise typer.Exit(ExitCode.NO_CACHE)
|
|
197
|
+
|
|
198
|
+
console.print(f"[bold]Baseline: {baseline.name}[/bold]")
|
|
199
|
+
console.print(f" Created: {baseline.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
200
|
+
console.print(f" VibeGuard Version: {baseline.vibeguard_version}")
|
|
201
|
+
console.print(f" Total Findings: {baseline.total_count}")
|
|
202
|
+
console.print(f" Actionable: {baseline.actionable_count}")
|
|
203
|
+
console.print(f" Scanners: {', '.join(baseline.scanners_used)}")
|
|
204
|
+
|
|
205
|
+
if baseline.findings:
|
|
206
|
+
console.print()
|
|
207
|
+
|
|
208
|
+
# Show severity breakdown
|
|
209
|
+
severity_counts: dict[str, int] = {}
|
|
210
|
+
for f in baseline.findings:
|
|
211
|
+
sev = f.severity.value.upper()
|
|
212
|
+
severity_counts[sev] = severity_counts.get(sev, 0) + 1
|
|
213
|
+
|
|
214
|
+
console.print("[bold]Severity Breakdown:[/bold]")
|
|
215
|
+
for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
|
|
216
|
+
if sev in severity_counts:
|
|
217
|
+
color = {
|
|
218
|
+
"CRITICAL": "red bold",
|
|
219
|
+
"HIGH": "red",
|
|
220
|
+
"MEDIUM": "yellow",
|
|
221
|
+
"LOW": "blue",
|
|
222
|
+
"INFO": "dim",
|
|
223
|
+
}.get(sev, "white")
|
|
224
|
+
console.print(f" [{color}]{sev}:[/{color}] {severity_counts[sev]}")
|
|
225
|
+
|
|
226
|
+
console.print()
|
|
227
|
+
console.print("[dim]Compare against this baseline with:[/dim]")
|
|
228
|
+
console.print(f"[dim] vibeguard scan . --baseline {baseline.name}[/dim]")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@app.command("delete")
|
|
232
|
+
def delete_cmd(
|
|
233
|
+
name: str = typer.Argument(
|
|
234
|
+
...,
|
|
235
|
+
help="Baseline name to delete",
|
|
236
|
+
),
|
|
237
|
+
path: Path = typer.Option(
|
|
238
|
+
Path("."),
|
|
239
|
+
"--path",
|
|
240
|
+
"-p",
|
|
241
|
+
help="Repository path",
|
|
242
|
+
),
|
|
243
|
+
force: bool = typer.Option(
|
|
244
|
+
False,
|
|
245
|
+
"--force",
|
|
246
|
+
"-f",
|
|
247
|
+
help="Delete without confirmation",
|
|
248
|
+
),
|
|
249
|
+
) -> None:
|
|
250
|
+
"""Delete a baseline.
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
vibeguard baseline delete old-baseline
|
|
254
|
+
vibeguard baseline delete old-baseline --force
|
|
255
|
+
"""
|
|
256
|
+
target = path.resolve()
|
|
257
|
+
|
|
258
|
+
# Check if baseline exists
|
|
259
|
+
baseline = load_baseline(target, name)
|
|
260
|
+
if baseline is None:
|
|
261
|
+
console.print(f"[red]Error:[/red] Baseline '{name}' not found.")
|
|
262
|
+
|
|
263
|
+
# Show available baselines
|
|
264
|
+
available = list_baselines(target)
|
|
265
|
+
if available:
|
|
266
|
+
console.print("\n[dim]Available baselines:[/dim]")
|
|
267
|
+
for b in available:
|
|
268
|
+
console.print(f" - {b.name}")
|
|
269
|
+
|
|
270
|
+
raise typer.Exit(ExitCode.NO_CACHE)
|
|
271
|
+
|
|
272
|
+
if not force:
|
|
273
|
+
console.print(f"[yellow]About to delete baseline:[/yellow] {name}")
|
|
274
|
+
console.print(f" Created: {baseline.created_at.strftime('%Y-%m-%d %H:%M')}")
|
|
275
|
+
console.print(f" Findings: {baseline.actionable_count}")
|
|
276
|
+
|
|
277
|
+
confirm = typer.confirm("Delete this baseline?", default=False)
|
|
278
|
+
if not confirm:
|
|
279
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
280
|
+
raise typer.Exit(ExitCode.SUCCESS)
|
|
281
|
+
|
|
282
|
+
if delete_baseline(target, name):
|
|
283
|
+
console.print(f"[green]Deleted baseline:[/green] {name}")
|
|
284
|
+
else:
|
|
285
|
+
console.print("[red]Error:[/red] Failed to delete baseline.")
|
|
286
|
+
raise typer.Exit(ExitCode.CONFIG_ERROR)
|