vibeguard-cli 1.0.0__py3-none-any.whl → 1.0.5__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 +1 -1
- vibeguard/cli/apply.py +11 -1
- vibeguard/cli/auth_cmd.py +0 -2
- vibeguard/cli/banners.py +98 -0
- vibeguard/cli/config_cmd.py +7 -7
- vibeguard/cli/fix.py +37 -8
- vibeguard/cli/main.py +1 -0
- vibeguard/cli/patch.py +60 -8
- vibeguard/cli/scan.py +7 -0
- vibeguard/core/bootstrap.py +16 -2
- vibeguard/core/bundles.py +303 -0
- vibeguard/core/downloader.py +11 -1
- vibeguard/core/license.py +79 -1
- vibeguard/core/telemetry.py +73 -0
- vibeguard/scanners/__init__.py +4 -0
- vibeguard/scanners/manifests/trivy.toml +14 -4
- vibeguard/scanners/runners/local.py +26 -1
- {vibeguard_cli-1.0.0.dist-info → vibeguard_cli-1.0.5.dist-info}/METADATA +1 -1
- {vibeguard_cli-1.0.0.dist-info → vibeguard_cli-1.0.5.dist-info}/RECORD +22 -19
- {vibeguard_cli-1.0.0.dist-info → vibeguard_cli-1.0.5.dist-info}/WHEEL +0 -0
- {vibeguard_cli-1.0.0.dist-info → vibeguard_cli-1.0.5.dist-info}/entry_points.txt +0 -0
- {vibeguard_cli-1.0.0.dist-info → vibeguard_cli-1.0.5.dist-info}/licenses/LICENSE +0 -0
vibeguard/__init__.py
CHANGED
vibeguard/cli/apply.py
CHANGED
|
@@ -11,9 +11,14 @@ import typer
|
|
|
11
11
|
from rich.panel import Panel
|
|
12
12
|
from rich.syntax import Syntax
|
|
13
13
|
|
|
14
|
+
from vibeguard.cli.banners import show_expiry_banner
|
|
14
15
|
from vibeguard.cli.display import get_console
|
|
15
16
|
from vibeguard.core.exit_codes import ExitCode
|
|
16
|
-
from vibeguard.core.license import
|
|
17
|
+
from vibeguard.core.license import (
|
|
18
|
+
ProFeatureError,
|
|
19
|
+
get_license_status_with_grace,
|
|
20
|
+
require_pro_license,
|
|
21
|
+
)
|
|
17
22
|
from vibeguard.models.patch import validate_unified_diff
|
|
18
23
|
|
|
19
24
|
console = get_console()
|
|
@@ -90,6 +95,11 @@ def apply(
|
|
|
90
95
|
console.print(f"[red]Error:[/red] {e}")
|
|
91
96
|
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
92
97
|
|
|
98
|
+
# Show expiry/grace period banner if license is expiring soon
|
|
99
|
+
license_status = get_license_status_with_grace()
|
|
100
|
+
if license_status.get("valid"):
|
|
101
|
+
show_expiry_banner(license_status)
|
|
102
|
+
|
|
93
103
|
# Verify patch file exists and is readable
|
|
94
104
|
patch_content = _read_patch_file(patch_file)
|
|
95
105
|
if patch_content is None:
|
vibeguard/cli/auth_cmd.py
CHANGED
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import threading
|
|
7
|
-
from datetime import UTC, datetime
|
|
8
7
|
|
|
9
8
|
import typer
|
|
10
9
|
from rich.live import Live
|
|
@@ -15,7 +14,6 @@ from rich.text import Text
|
|
|
15
14
|
|
|
16
15
|
from vibeguard.cli.display import BRAND_COLOR, VIBEGUARD_SPINNER_NAME, get_console
|
|
17
16
|
from vibeguard.core.auth import (
|
|
18
|
-
AuthError,
|
|
19
17
|
LicenseError,
|
|
20
18
|
NetworkError,
|
|
21
19
|
activate_license,
|
vibeguard/cli/banners.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Expiry and grace period banners for CLI commands.
|
|
2
|
+
|
|
3
|
+
Displays warning banners based on license/token status to alert users
|
|
4
|
+
about upcoming expiration or grace period status.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
from vibeguard.cli.display import get_console
|
|
14
|
+
|
|
15
|
+
console = get_console()
|
|
16
|
+
|
|
17
|
+
# Banner thresholds
|
|
18
|
+
CRITICAL_HOURS = 24 # Red banner when < 24 hours
|
|
19
|
+
APPROACHING_DAYS = 7 # Yellow banner when < 7 days
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def show_expiry_banner(license_status: dict[str, Any]) -> None:
|
|
23
|
+
"""Show warning banner based on license/token status.
|
|
24
|
+
|
|
25
|
+
Banner zones (in priority order):
|
|
26
|
+
1. Grace period (license expired, in 48h grace): Yellow urgent
|
|
27
|
+
2. Critical (< 24 hours until expiry): Red
|
|
28
|
+
3. Approaching (< 7 days until expiry): Yellow notice
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
license_status: Dictionary from get_license_status_with_grace() with:
|
|
32
|
+
- valid: bool - whether license is currently valid
|
|
33
|
+
- in_grace: bool - whether in grace period
|
|
34
|
+
- hours_left: int - hours remaining (if in grace)
|
|
35
|
+
- days_left: int - days remaining (if not in grace)
|
|
36
|
+
"""
|
|
37
|
+
if not license_status.get("valid"):
|
|
38
|
+
# Don't show banner if fully expired (auth will fail anyway)
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
in_grace = license_status.get("in_grace", False)
|
|
42
|
+
hours_left = license_status.get("hours_left", 0)
|
|
43
|
+
days_left = license_status.get("days_left", 999)
|
|
44
|
+
|
|
45
|
+
if in_grace:
|
|
46
|
+
# Grace period - yellow/urgent messaging
|
|
47
|
+
_show_grace_period_banner(hours_left)
|
|
48
|
+
elif days_left * 24 + hours_left <= CRITICAL_HOURS:
|
|
49
|
+
# Critical - less than 24 hours
|
|
50
|
+
total_hours = days_left * 24 + hours_left
|
|
51
|
+
_show_critical_banner(total_hours)
|
|
52
|
+
elif days_left <= APPROACHING_DAYS:
|
|
53
|
+
# Approaching - 1-7 days remaining
|
|
54
|
+
_show_approaching_banner(days_left)
|
|
55
|
+
# else: No banner for healthy licenses (> 7 days remaining)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _show_grace_period_banner(hours_left: int) -> None:
|
|
59
|
+
"""Show grace period warning banner (yellow, urgent)."""
|
|
60
|
+
console.print(
|
|
61
|
+
Panel(
|
|
62
|
+
f"[yellow bold]Your license expired. "
|
|
63
|
+
f"Grace period: {hours_left} hours remaining.[/yellow bold]\n"
|
|
64
|
+
"Renew now to avoid service interruption.\n"
|
|
65
|
+
"[dim]https://app.vibeguard.co/billing[/dim]",
|
|
66
|
+
title="[yellow]Grace Period Active[/yellow]",
|
|
67
|
+
border_style="yellow",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
console.print() # Add spacing after banner
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _show_critical_banner(hours_left: int) -> None:
|
|
74
|
+
"""Show critical expiry warning banner (red)."""
|
|
75
|
+
time_str = f"{hours_left} hour{'s' if hours_left != 1 else ''}"
|
|
76
|
+
console.print(
|
|
77
|
+
Panel(
|
|
78
|
+
f"[red bold]Your license expires in {time_str}![/red bold]\n"
|
|
79
|
+
"Renew at: [dim]https://app.vibeguard.co/billing[/dim]",
|
|
80
|
+
title="[red]License Expiring Soon[/red]",
|
|
81
|
+
border_style="red",
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
console.print()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _show_approaching_banner(days_left: int) -> None:
|
|
88
|
+
"""Show approaching expiry notice banner (yellow, informational)."""
|
|
89
|
+
day_str = f"{days_left} day{'s' if days_left != 1 else ''}"
|
|
90
|
+
console.print(
|
|
91
|
+
Panel(
|
|
92
|
+
f"[yellow]Your license expires in {day_str}.[/yellow]\n"
|
|
93
|
+
"Renew at: [dim]https://app.vibeguard.co/billing[/dim]",
|
|
94
|
+
title="Renewal Reminder",
|
|
95
|
+
border_style="yellow",
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
console.print()
|
vibeguard/cli/config_cmd.py
CHANGED
|
@@ -10,7 +10,7 @@ from questionary import Style
|
|
|
10
10
|
from rich.table import Table
|
|
11
11
|
|
|
12
12
|
from vibeguard.cli.display import get_console
|
|
13
|
-
from vibeguard.core.config import
|
|
13
|
+
from vibeguard.core.config import find_config_file, load_config, save_config
|
|
14
14
|
|
|
15
15
|
app = typer.Typer(
|
|
16
16
|
name="config",
|
|
@@ -114,7 +114,7 @@ def set_config(
|
|
|
114
114
|
# Parse the key path
|
|
115
115
|
parts = key.split(".")
|
|
116
116
|
if len(parts) != 2:
|
|
117
|
-
console.print(
|
|
117
|
+
console.print("[red]Error:[/red] Invalid key format. Use section.setting (e.g., report.format)")
|
|
118
118
|
raise typer.Exit(1)
|
|
119
119
|
|
|
120
120
|
section, setting = parts
|
|
@@ -126,7 +126,7 @@ def set_config(
|
|
|
126
126
|
config.report.auto_generate = value.lower() in ("true", "yes", "1", "on")
|
|
127
127
|
elif setting == "format":
|
|
128
128
|
if value not in ("html", "json", "sarif"):
|
|
129
|
-
console.print(
|
|
129
|
+
console.print("[red]Error:[/red] Invalid format. Choose: html, json, sarif")
|
|
130
130
|
raise typer.Exit(1)
|
|
131
131
|
config.report.format = value # type: ignore
|
|
132
132
|
elif setting == "output_dir":
|
|
@@ -139,14 +139,14 @@ def set_config(
|
|
|
139
139
|
elif section == "scan":
|
|
140
140
|
if setting == "pack":
|
|
141
141
|
if value not in ("core", "ecosystem", "full"):
|
|
142
|
-
console.print(
|
|
142
|
+
console.print("[red]Error:[/red] Invalid pack. Choose: core, ecosystem, full")
|
|
143
143
|
raise typer.Exit(1)
|
|
144
144
|
config.scan.pack = value # type: ignore
|
|
145
145
|
elif setting == "timeout":
|
|
146
146
|
config.scan.timeout = int(value)
|
|
147
147
|
elif setting == "min_severity":
|
|
148
148
|
if value not in ("critical", "high", "medium", "low", "info"):
|
|
149
|
-
console.print(
|
|
149
|
+
console.print("[red]Error:[/red] Invalid severity. Choose: critical, high, medium, low, info")
|
|
150
150
|
raise typer.Exit(1)
|
|
151
151
|
config.scan.min_severity = value # type: ignore
|
|
152
152
|
else:
|
|
@@ -155,7 +155,7 @@ def set_config(
|
|
|
155
155
|
elif section == "output":
|
|
156
156
|
if setting == "format":
|
|
157
157
|
if value not in ("terminal", "json", "sarif", "html"):
|
|
158
|
-
console.print(
|
|
158
|
+
console.print("[red]Error:[/red] Invalid format. Choose: terminal, json, sarif, html")
|
|
159
159
|
raise typer.Exit(1)
|
|
160
160
|
config.output.format = value # type: ignore
|
|
161
161
|
else:
|
|
@@ -248,5 +248,5 @@ def edit_config_interactive(
|
|
|
248
248
|
|
|
249
249
|
# Save
|
|
250
250
|
save_config(config, config_path)
|
|
251
|
-
console.print(
|
|
251
|
+
console.print("\n[green]Settings saved![/green]")
|
|
252
252
|
console.print(f"[dim]Config file: {config_path}[/dim]")
|
vibeguard/cli/fix.py
CHANGED
|
@@ -15,6 +15,7 @@ from rich.table import Table
|
|
|
15
15
|
from vibeguard.cli.display import get_console
|
|
16
16
|
from vibeguard.core.cache import load_latest_scan
|
|
17
17
|
from vibeguard.core.exit_codes import ExitCode
|
|
18
|
+
from vibeguard.models.auth import Bundle
|
|
18
19
|
from vibeguard.models.finding import Finding, Severity
|
|
19
20
|
|
|
20
21
|
console = get_console()
|
|
@@ -38,8 +39,8 @@ SEVERITY_COLORS = {
|
|
|
38
39
|
Severity.INFO: "dim",
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
# Prompt template for security fixes
|
|
42
|
-
|
|
42
|
+
# Prompt template for security fixes (hardcoded fallback)
|
|
43
|
+
_DEFAULT_FIX_PROMPT = """\
|
|
43
44
|
You are a security expert helping fix a vulnerability in code.
|
|
44
45
|
|
|
45
46
|
## Finding Details
|
|
@@ -133,6 +134,11 @@ def fix(
|
|
|
133
134
|
"""
|
|
134
135
|
target = path.resolve()
|
|
135
136
|
|
|
137
|
+
# Load cached bundle for prompt templates (no fetch - fix is FREE tier)
|
|
138
|
+
from vibeguard.core.bundles import load_cached_bundle
|
|
139
|
+
|
|
140
|
+
bundle = load_cached_bundle()
|
|
141
|
+
|
|
136
142
|
# Load latest scan
|
|
137
143
|
scan_result = load_latest_scan(target)
|
|
138
144
|
|
|
@@ -172,7 +178,7 @@ def fix(
|
|
|
172
178
|
|
|
173
179
|
if use_interactive:
|
|
174
180
|
# Interactive mode - main loop
|
|
175
|
-
_interactive_fix_loop(findings, target, bulk)
|
|
181
|
+
_interactive_fix_loop(findings, target, bulk, bundle=bundle)
|
|
176
182
|
raise typer.Exit(ExitCode.SUCCESS)
|
|
177
183
|
|
|
178
184
|
# Non-interactive mode - use provided finding_id
|
|
@@ -199,7 +205,7 @@ def fix(
|
|
|
199
205
|
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
200
206
|
|
|
201
207
|
# Generate prompt for the single finding
|
|
202
|
-
prompt = build_fix_prompt(finding, target)
|
|
208
|
+
prompt = build_fix_prompt(finding, target, bundle=bundle)
|
|
203
209
|
|
|
204
210
|
# In non-interactive mode (finding_id provided), just display and exit
|
|
205
211
|
# Check if we're in a TTY for action menu
|
|
@@ -218,13 +224,20 @@ def fix(
|
|
|
218
224
|
raise typer.Exit(ExitCode.SUCCESS)
|
|
219
225
|
|
|
220
226
|
|
|
221
|
-
def _interactive_fix_loop(
|
|
227
|
+
def _interactive_fix_loop(
|
|
228
|
+
findings: list[Finding],
|
|
229
|
+
target: Path,
|
|
230
|
+
bulk: bool,
|
|
231
|
+
*,
|
|
232
|
+
bundle: Bundle | None = None,
|
|
233
|
+
) -> None:
|
|
222
234
|
"""Main interactive loop for fix command.
|
|
223
235
|
|
|
224
236
|
Args:
|
|
225
237
|
findings: List of findings to choose from
|
|
226
238
|
target: Repository root path
|
|
227
239
|
bulk: Whether bulk mode is enabled
|
|
240
|
+
bundle: Optional policy bundle for prompt templates
|
|
228
241
|
"""
|
|
229
242
|
while True:
|
|
230
243
|
console.print()
|
|
@@ -243,7 +256,7 @@ def _interactive_fix_loop(findings: list[Finding], target: Path, bulk: bool) ->
|
|
|
243
256
|
|
|
244
257
|
# Generate and display prompts
|
|
245
258
|
prompts = [
|
|
246
|
-
(f, build_fix_prompt(f, target))
|
|
259
|
+
(f, build_fix_prompt(f, target, bundle=bundle))
|
|
247
260
|
for f in selected_findings
|
|
248
261
|
]
|
|
249
262
|
_display_prompts(prompts)
|
|
@@ -904,16 +917,32 @@ def _find_finding(findings: list[Finding], finding_id: str) -> Finding | None:
|
|
|
904
917
|
return None
|
|
905
918
|
|
|
906
919
|
|
|
907
|
-
def build_fix_prompt(
|
|
920
|
+
def build_fix_prompt(
|
|
921
|
+
finding: Finding,
|
|
922
|
+
repo_root: Path,
|
|
923
|
+
*,
|
|
924
|
+
bundle: Bundle | None = None,
|
|
925
|
+
) -> str:
|
|
908
926
|
"""Build the fix prompt from a finding.
|
|
909
927
|
|
|
928
|
+
Uses bundle-sourced prompt template if available,
|
|
929
|
+
falling back to hardcoded template.
|
|
930
|
+
|
|
910
931
|
Args:
|
|
911
932
|
finding: The finding to generate a prompt for
|
|
912
933
|
repo_root: Repository root path
|
|
934
|
+
bundle: Optional policy bundle for prompt templates
|
|
913
935
|
|
|
914
936
|
Returns:
|
|
915
937
|
Complete prompt string
|
|
916
938
|
"""
|
|
939
|
+
from vibeguard.core.bundles import get_prompt
|
|
940
|
+
|
|
941
|
+
# Select template: bundle-sourced or hardcoded fallback
|
|
942
|
+
template = _DEFAULT_FIX_PROMPT
|
|
943
|
+
if bundle is not None:
|
|
944
|
+
template = get_prompt(bundle, "fix_prompt", _DEFAULT_FIX_PROMPT)
|
|
945
|
+
|
|
917
946
|
# Build optional sections
|
|
918
947
|
has_range = finding.line_end and finding.line_end != finding.line_start
|
|
919
948
|
line_end_str = f"-{finding.line_end}" if has_range else ""
|
|
@@ -933,7 +962,7 @@ def build_fix_prompt(finding: Finding, repo_root: Path) -> str:
|
|
|
933
962
|
if not code_snippet:
|
|
934
963
|
code_snippet = "(Code snippet not available)"
|
|
935
964
|
|
|
936
|
-
return
|
|
965
|
+
return template.format(
|
|
937
966
|
scanner=finding.scanner,
|
|
938
967
|
rule_id=finding.rule_id,
|
|
939
968
|
severity=finding.severity.value.upper(),
|
vibeguard/cli/main.py
CHANGED
|
@@ -309,6 +309,7 @@ def show_baseline_submenu() -> list[str]:
|
|
|
309
309
|
if selected in ("show", "delete"):
|
|
310
310
|
# List available baselines for selection
|
|
311
311
|
from pathlib import Path
|
|
312
|
+
|
|
312
313
|
from vibeguard.core.baseline import list_baselines
|
|
313
314
|
|
|
314
315
|
baselines = list_baselines(Path("."))
|
vibeguard/cli/patch.py
CHANGED
|
@@ -14,6 +14,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
|
14
14
|
from rich.syntax import Syntax
|
|
15
15
|
from rich.table import Table
|
|
16
16
|
|
|
17
|
+
from vibeguard.cli.banners import show_expiry_banner
|
|
17
18
|
from vibeguard.cli.display import (
|
|
18
19
|
BRAND_COLOR,
|
|
19
20
|
VIBEGUARD_SPINNER_NAME,
|
|
@@ -23,8 +24,13 @@ from vibeguard.cli.display import (
|
|
|
23
24
|
from vibeguard.cli.fix import _find_finding, build_fix_prompt
|
|
24
25
|
from vibeguard.core.cache import load_latest_scan
|
|
25
26
|
from vibeguard.core.exit_codes import ExitCode
|
|
26
|
-
from vibeguard.core.license import
|
|
27
|
+
from vibeguard.core.license import (
|
|
28
|
+
ProFeatureError,
|
|
29
|
+
get_license_status_with_grace,
|
|
30
|
+
require_patch_capability,
|
|
31
|
+
)
|
|
27
32
|
from vibeguard.core.llm import LLMError, LLMResponse, generate
|
|
33
|
+
from vibeguard.models.auth import Bundle
|
|
28
34
|
from vibeguard.models.finding import Finding, Severity
|
|
29
35
|
from vibeguard.models.patch import (
|
|
30
36
|
PatchArtifact,
|
|
@@ -135,6 +141,14 @@ def patch(
|
|
|
135
141
|
console.print(f"[red]Error:[/red] {e}")
|
|
136
142
|
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
137
143
|
|
|
144
|
+
# Show expiry/grace period banner if license is expiring soon
|
|
145
|
+
license_status = get_license_status_with_grace()
|
|
146
|
+
if license_status.get("valid"):
|
|
147
|
+
show_expiry_banner(license_status)
|
|
148
|
+
|
|
149
|
+
# Load bundle for prompt templates (Pro users can fetch from server)
|
|
150
|
+
bundle = _load_bundle_for_patch()
|
|
151
|
+
|
|
138
152
|
# Load latest scan
|
|
139
153
|
scan_result = load_latest_scan(target)
|
|
140
154
|
if scan_result is None:
|
|
@@ -196,7 +210,8 @@ def patch(
|
|
|
196
210
|
console.print(f"[dim]Patch {i + 1} of {total}[/dim]")
|
|
197
211
|
|
|
198
212
|
result = _generate_and_save_patch(
|
|
199
|
-
selected_finding, target, provider, model, output, dry_run
|
|
213
|
+
selected_finding, target, provider, model, output, dry_run,
|
|
214
|
+
bundle=bundle,
|
|
200
215
|
)
|
|
201
216
|
if result:
|
|
202
217
|
success_count += 1
|
|
@@ -249,10 +264,35 @@ def patch(
|
|
|
249
264
|
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
250
265
|
|
|
251
266
|
# Generate patch for the single finding
|
|
252
|
-
result = _generate_and_save_patch(
|
|
267
|
+
result = _generate_and_save_patch(
|
|
268
|
+
finding, target, provider, model, output, dry_run, bundle=bundle,
|
|
269
|
+
)
|
|
253
270
|
raise typer.Exit(ExitCode.SUCCESS if result else ExitCode.SCAN_ERROR)
|
|
254
271
|
|
|
255
272
|
|
|
273
|
+
def _load_bundle_for_patch() -> Bundle | None:
|
|
274
|
+
"""Load bundle for patch command, fetching if Pro licensed.
|
|
275
|
+
|
|
276
|
+
Pro users can fetch from the server; falls back to cache or hardcoded.
|
|
277
|
+
Never raises -- bundle failure should never block patching.
|
|
278
|
+
"""
|
|
279
|
+
from vibeguard.core.auth import get_cached_token
|
|
280
|
+
from vibeguard.core.bundles import (
|
|
281
|
+
ensure_bundle,
|
|
282
|
+
get_hardcoded_fallback,
|
|
283
|
+
load_cached_bundle,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
token = get_cached_token()
|
|
287
|
+
token_str = token.token if token else None
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
return asyncio.run(ensure_bundle(token_str))
|
|
291
|
+
except Exception:
|
|
292
|
+
# Bundle fetch failure should never block patching
|
|
293
|
+
return load_cached_bundle() or get_hardcoded_fallback()
|
|
294
|
+
|
|
295
|
+
|
|
256
296
|
def _parse_severity(severity_str: str) -> Severity | None:
|
|
257
297
|
"""Parse severity string to Severity enum."""
|
|
258
298
|
severity_map = {
|
|
@@ -553,13 +593,15 @@ def _generate_and_save_patch(
|
|
|
553
593
|
model: str | None,
|
|
554
594
|
output: Path | None,
|
|
555
595
|
dry_run: bool,
|
|
596
|
+
*,
|
|
597
|
+
bundle: Bundle | None = None,
|
|
556
598
|
) -> bool:
|
|
557
599
|
"""Generate and save a patch for a single finding.
|
|
558
600
|
|
|
559
601
|
Returns True on success, False on failure.
|
|
560
602
|
"""
|
|
561
|
-
# Build prompt
|
|
562
|
-
prompt = build_fix_prompt(finding, target)
|
|
603
|
+
# Build prompt (uses bundle template if available)
|
|
604
|
+
prompt = build_fix_prompt(finding, target, bundle=bundle)
|
|
563
605
|
|
|
564
606
|
# Generate patch using LLM
|
|
565
607
|
console.print(
|
|
@@ -579,7 +621,9 @@ def _generate_and_save_patch(
|
|
|
579
621
|
msg = get_patching_message()
|
|
580
622
|
task = progress.add_task(f"{msg}...", total=None)
|
|
581
623
|
|
|
582
|
-
response = asyncio.run(
|
|
624
|
+
response = asyncio.run(
|
|
625
|
+
_generate_patch_async(prompt, provider, model, bundle=bundle)
|
|
626
|
+
)
|
|
583
627
|
|
|
584
628
|
progress.remove_task(task)
|
|
585
629
|
|
|
@@ -785,6 +829,8 @@ async def _generate_patch_async(
|
|
|
785
829
|
prompt: str,
|
|
786
830
|
provider: str | None,
|
|
787
831
|
model: str | None,
|
|
832
|
+
*,
|
|
833
|
+
bundle: Bundle | None = None,
|
|
788
834
|
) -> LLMResponse:
|
|
789
835
|
"""Generate patch using LLM.
|
|
790
836
|
|
|
@@ -792,14 +838,20 @@ async def _generate_patch_async(
|
|
|
792
838
|
prompt: The prompt to send
|
|
793
839
|
provider: Optional provider override
|
|
794
840
|
model: Optional model override
|
|
841
|
+
bundle: Optional policy bundle for LLM configuration
|
|
795
842
|
|
|
796
843
|
Returns:
|
|
797
844
|
LLMResponse with generated content
|
|
798
845
|
"""
|
|
846
|
+
from vibeguard.core.bundles import get_patch_rule
|
|
847
|
+
|
|
848
|
+
max_tokens = get_patch_rule(bundle, "max_tokens", 4096) if bundle else 4096
|
|
849
|
+
temperature = get_patch_rule(bundle, "temperature", 0.2) if bundle else 0.2
|
|
850
|
+
|
|
799
851
|
return await generate(
|
|
800
852
|
prompt,
|
|
801
853
|
provider=provider,
|
|
802
854
|
model=model,
|
|
803
|
-
max_tokens=
|
|
804
|
-
temperature=
|
|
855
|
+
max_tokens=max_tokens,
|
|
856
|
+
temperature=temperature,
|
|
805
857
|
)
|
vibeguard/cli/scan.py
CHANGED
|
@@ -862,6 +862,13 @@ async def _run_scan(
|
|
|
862
862
|
if not quiet:
|
|
863
863
|
console.print(f"[yellow]Warning:[/yellow] Failed to cache results: {e}")
|
|
864
864
|
|
|
865
|
+
# Fire-and-forget scan submission for Pro users
|
|
866
|
+
try:
|
|
867
|
+
from vibeguard.core.telemetry import submit_scan
|
|
868
|
+
submit_scan(result)
|
|
869
|
+
except Exception:
|
|
870
|
+
pass # Never block on telemetry
|
|
871
|
+
|
|
865
872
|
return result
|
|
866
873
|
|
|
867
874
|
|
vibeguard/core/bootstrap.py
CHANGED
|
@@ -6,6 +6,7 @@ import subprocess # nosec B404 # noqa: S404 # needed for pip install
|
|
|
6
6
|
import sys
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from enum import Enum
|
|
9
|
+
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
from vibeguard.core.downloader import DownloadConfig, download_binary, get_cached_binary
|
|
11
12
|
from vibeguard.scanners import ScannerManifest, load_manifest
|
|
@@ -68,8 +69,19 @@ class BootstrapSummary:
|
|
|
68
69
|
|
|
69
70
|
|
|
70
71
|
def _is_binary_available(binary_name: str) -> bool:
|
|
71
|
-
"""Check if a binary is available in PATH."""
|
|
72
|
-
|
|
72
|
+
"""Check if a binary is available in PATH or the current venv's bin directory."""
|
|
73
|
+
if shutil.which(binary_name) is not None:
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
# Check current Python environment's bin directory (handles pipx installs)
|
|
77
|
+
prefix = Path(sys.prefix)
|
|
78
|
+
bin_dir = prefix / ("Scripts" if sys.platform == "win32" else "bin")
|
|
79
|
+
if bin_dir.is_dir():
|
|
80
|
+
for ext in ("", ".exe"):
|
|
81
|
+
if (bin_dir / f"{binary_name}{ext}").is_file():
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
return False
|
|
73
85
|
|
|
74
86
|
|
|
75
87
|
def _is_docker_available() -> bool:
|
|
@@ -102,6 +114,8 @@ async def _try_download(manifest: ScannerManifest) -> bool:
|
|
|
102
114
|
archive_type=manifest.download_config.archive_type,
|
|
103
115
|
windows_archive_type=manifest.download_config.windows_archive_type,
|
|
104
116
|
windows_arch=manifest.download_config.windows_arch,
|
|
117
|
+
os_map=manifest.download_config.os_map,
|
|
118
|
+
arch_map=manifest.download_config.arch_map,
|
|
105
119
|
)
|
|
106
120
|
|
|
107
121
|
binary_path = await download_binary(dl_config)
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""Policy bundle fetching, caching, and loading.
|
|
2
|
+
|
|
3
|
+
Bundles contain server-managed prompts, patch rules, and defaults
|
|
4
|
+
that can be updated without releasing a new CLI version.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
import httpx
|
|
16
|
+
|
|
17
|
+
from vibeguard.core.auth import API_BASE_URL, API_TIMEOUT
|
|
18
|
+
from vibeguard.models.auth import Bundle, BundleMetadata
|
|
19
|
+
|
|
20
|
+
# Storage locations
|
|
21
|
+
BUNDLES_DIR = Path.home() / ".vibeguard" / "bundles"
|
|
22
|
+
BUNDLE_FILE = BUNDLES_DIR / "current.json"
|
|
23
|
+
BUNDLE_META_FILE = BUNDLES_DIR / "meta.json"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BundleError(Exception):
|
|
27
|
+
"""Raised when bundle operations fail."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---------------------------------------------------------------------------
|
|
31
|
+
# Hardcoded fallback (mirrors fix.py FIX_PROMPT_TEMPLATE)
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
_DEFAULT_FIX_PROMPT = """\
|
|
35
|
+
You are a security expert helping fix a vulnerability in code.
|
|
36
|
+
|
|
37
|
+
## Finding Details
|
|
38
|
+
- **Scanner**: {scanner}
|
|
39
|
+
- **Rule**: {rule_id}
|
|
40
|
+
- **Severity**: {severity}
|
|
41
|
+
- **File**: {file_path}
|
|
42
|
+
- **Line**: {line_start}{line_end_str}
|
|
43
|
+
{cwe_section}
|
|
44
|
+
## Issue Description
|
|
45
|
+
{message}
|
|
46
|
+
|
|
47
|
+
## Affected Code
|
|
48
|
+
```
|
|
49
|
+
{code_snippet}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Your Task
|
|
53
|
+
Generate a minimal, safe fix for this security issue. Follow these rules:
|
|
54
|
+
|
|
55
|
+
### Patch Safety Rules
|
|
56
|
+
1. Make minimal changes only - fix the vulnerability, nothing else
|
|
57
|
+
2. Do not add new dependencies unless absolutely required
|
|
58
|
+
3. Do not include any secrets, tokens, or credentials in your response
|
|
59
|
+
4. Preserve the existing code style and formatting
|
|
60
|
+
5. Output ONLY a valid unified diff (starting with --- and +++)
|
|
61
|
+
6. If you are uncertain about the fix, include a comment: # MANUAL_REVIEW_REQUIRED
|
|
62
|
+
|
|
63
|
+
### Expected Output Format
|
|
64
|
+
```diff
|
|
65
|
+
--- a/{file_path}
|
|
66
|
+
+++ b/{file_path}
|
|
67
|
+
@@ -line,count +line,count @@
|
|
68
|
+
context line
|
|
69
|
+
-removed line
|
|
70
|
+
+added line
|
|
71
|
+
context line
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Generate the patch now:
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _ensure_bundles_dir() -> None:
|
|
79
|
+
"""Ensure the bundles directory exists."""
|
|
80
|
+
BUNDLES_DIR.mkdir(parents=True, exist_ok=True)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
# Load / Save
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def load_cached_bundle() -> Bundle | None:
|
|
89
|
+
"""Load bundle from local cache (~/.vibeguard/bundles/current.json).
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Bundle if cached and valid, None otherwise.
|
|
93
|
+
"""
|
|
94
|
+
if not BUNDLE_FILE.exists():
|
|
95
|
+
return None
|
|
96
|
+
try:
|
|
97
|
+
data = json.loads(BUNDLE_FILE.read_text(encoding="utf-8"))
|
|
98
|
+
return Bundle(**data)
|
|
99
|
+
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_bundle_metadata() -> BundleMetadata | None:
|
|
104
|
+
"""Load bundle metadata from local cache.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
BundleMetadata if exists, None otherwise.
|
|
108
|
+
"""
|
|
109
|
+
if not BUNDLE_META_FILE.exists():
|
|
110
|
+
return None
|
|
111
|
+
try:
|
|
112
|
+
data = json.loads(BUNDLE_META_FILE.read_text(encoding="utf-8"))
|
|
113
|
+
return BundleMetadata(**data)
|
|
114
|
+
except (json.JSONDecodeError, KeyError, TypeError, ValueError):
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def save_bundle(bundle: Bundle, sha256: str | None = None) -> None:
|
|
119
|
+
"""Save bundle and metadata to local cache.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
bundle: The Bundle object to cache.
|
|
123
|
+
sha256: Optional SHA-256 hash for integrity verification.
|
|
124
|
+
"""
|
|
125
|
+
_ensure_bundles_dir()
|
|
126
|
+
|
|
127
|
+
# Write bundle content
|
|
128
|
+
content = bundle.model_dump_json(indent=2)
|
|
129
|
+
BUNDLE_FILE.write_text(content, encoding="utf-8")
|
|
130
|
+
|
|
131
|
+
# Write metadata
|
|
132
|
+
meta = BundleMetadata(
|
|
133
|
+
version=bundle.version,
|
|
134
|
+
downloaded_at=datetime.now(UTC),
|
|
135
|
+
sha256=sha256,
|
|
136
|
+
is_current=True,
|
|
137
|
+
)
|
|
138
|
+
BUNDLE_META_FILE.write_text(meta.model_dump_json(indent=2), encoding="utf-8")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def get_cached_version() -> str | None:
|
|
142
|
+
"""Get version of the currently cached bundle.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Version string or None if no bundle cached.
|
|
146
|
+
"""
|
|
147
|
+
meta = load_bundle_metadata()
|
|
148
|
+
return meta.version if meta else None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# Fetch
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def fetch_bundle(token: str) -> Bundle | None:
|
|
157
|
+
"""Fetch latest bundle from API server.
|
|
158
|
+
|
|
159
|
+
Uses conditional fetching: sends cached version as
|
|
160
|
+
If-None-Match header. If server returns 304 (Not Modified),
|
|
161
|
+
returns None (no update needed).
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
token: Auth token for the API.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
New Bundle if updated, None if already current.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
BundleError: If fetch fails (caller should fall back to cache).
|
|
171
|
+
"""
|
|
172
|
+
headers: dict[str, str] = {"Authorization": f"Bearer {token}"}
|
|
173
|
+
|
|
174
|
+
cached_version = get_cached_version()
|
|
175
|
+
if cached_version:
|
|
176
|
+
headers["If-None-Match"] = cached_version
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
async with httpx.AsyncClient(timeout=API_TIMEOUT) as client:
|
|
180
|
+
resp = await client.get(
|
|
181
|
+
f"{API_BASE_URL}/v1/bundles/latest",
|
|
182
|
+
headers=headers,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
if resp.status_code == 304:
|
|
186
|
+
return None # Already up to date
|
|
187
|
+
|
|
188
|
+
if resp.status_code == 401:
|
|
189
|
+
raise BundleError("Unauthorized: invalid or expired token")
|
|
190
|
+
|
|
191
|
+
if resp.status_code != 200:
|
|
192
|
+
raise BundleError(f"Unexpected status {resp.status_code}")
|
|
193
|
+
|
|
194
|
+
data = resp.json()
|
|
195
|
+
bundle = Bundle(**data)
|
|
196
|
+
|
|
197
|
+
# Compute SHA-256 of the raw response body
|
|
198
|
+
sha256 = hashlib.sha256(resp.content).hexdigest()
|
|
199
|
+
|
|
200
|
+
# Cache the new bundle
|
|
201
|
+
save_bundle(bundle, sha256=sha256)
|
|
202
|
+
|
|
203
|
+
return bundle
|
|
204
|
+
|
|
205
|
+
except httpx.HTTPError as exc:
|
|
206
|
+
raise BundleError(f"Network error fetching bundle: {exc}") from exc
|
|
207
|
+
except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc:
|
|
208
|
+
raise BundleError(f"Invalid bundle response: {exc}") from exc
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Main entry point
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def ensure_bundle(token: str | None = None) -> Bundle:
|
|
217
|
+
"""Get the current bundle, fetching if needed.
|
|
218
|
+
|
|
219
|
+
Priority order:
|
|
220
|
+
1. If online + token: Try fetch (with version check)
|
|
221
|
+
2. If cached bundle exists: Use it
|
|
222
|
+
3. Fall back to hardcoded defaults
|
|
223
|
+
|
|
224
|
+
This is the main entry point that other modules should call.
|
|
225
|
+
Never raises -- always returns a usable Bundle.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
token: Optional auth token (skips fetch if None).
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Bundle (from server, cache, or hardcoded fallback).
|
|
232
|
+
"""
|
|
233
|
+
# Try fetching from server if we have a token
|
|
234
|
+
if token:
|
|
235
|
+
try:
|
|
236
|
+
fetched = await fetch_bundle(token)
|
|
237
|
+
if fetched is not None:
|
|
238
|
+
return fetched
|
|
239
|
+
except BundleError:
|
|
240
|
+
pass # Fall through to cache
|
|
241
|
+
|
|
242
|
+
# Try cached bundle
|
|
243
|
+
cached = load_cached_bundle()
|
|
244
|
+
if cached is not None:
|
|
245
|
+
return cached
|
|
246
|
+
|
|
247
|
+
# Final fallback: hardcoded defaults
|
|
248
|
+
return get_hardcoded_fallback()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# Fallback & helpers
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def get_hardcoded_fallback() -> Bundle:
|
|
257
|
+
"""Get the hardcoded fallback bundle.
|
|
258
|
+
|
|
259
|
+
This contains the same prompts/rules that are currently
|
|
260
|
+
hardcoded in fix.py, ensuring backwards compatibility.
|
|
261
|
+
|
|
262
|
+
Returns:
|
|
263
|
+
Bundle with hardcoded defaults.
|
|
264
|
+
"""
|
|
265
|
+
return Bundle(
|
|
266
|
+
version="0.0.0-builtin",
|
|
267
|
+
prompts={
|
|
268
|
+
"fix_prompt": _DEFAULT_FIX_PROMPT,
|
|
269
|
+
},
|
|
270
|
+
patch_rules={
|
|
271
|
+
"max_tokens": 4096,
|
|
272
|
+
"temperature": 0.2,
|
|
273
|
+
},
|
|
274
|
+
defaults={},
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def get_prompt(bundle: Bundle, key: str, fallback: str) -> str:
|
|
279
|
+
"""Get a prompt template from bundle with fallback.
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
bundle: The bundle to look up.
|
|
283
|
+
key: Prompt key (e.g., "fix_prompt", "patch_system").
|
|
284
|
+
fallback: Default template if key not in bundle.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
Prompt template string.
|
|
288
|
+
"""
|
|
289
|
+
return bundle.prompts.get(key, fallback)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def get_patch_rule(bundle: Bundle, key: str, fallback: Any = None) -> Any:
|
|
293
|
+
"""Get a patch rule from bundle with fallback.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
bundle: The bundle to look up.
|
|
297
|
+
key: Rule key (e.g., "max_tokens", "temperature").
|
|
298
|
+
fallback: Default value if key not in bundle.
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
Rule value.
|
|
302
|
+
"""
|
|
303
|
+
return bundle.patch_rules.get(key, fallback)
|
vibeguard/core/downloader.py
CHANGED
|
@@ -82,6 +82,8 @@ class DownloadConfig(BaseModel):
|
|
|
82
82
|
archive_type: str = "tar.gz"
|
|
83
83
|
windows_archive_type: str | None = None # Override for Windows
|
|
84
84
|
windows_arch: str | None = None # Override arch for Windows (e.g., "x64" instead of "amd64")
|
|
85
|
+
os_map: dict[str, str] | None = None # Custom OS name mapping (e.g., {"darwin": "macOS"})
|
|
86
|
+
arch_map: dict[str, str] | None = None # Custom arch name mapping (e.g., {"amd64": "64bit"})
|
|
85
87
|
|
|
86
88
|
|
|
87
89
|
class PlatformInfo(NamedTuple):
|
|
@@ -148,6 +150,14 @@ async def download_binary(config: DownloadConfig) -> Path | None:
|
|
|
148
150
|
# Determine archive type and arch (Windows may use different values)
|
|
149
151
|
archive_type = config.archive_type
|
|
150
152
|
arch = platform_info.arch
|
|
153
|
+
os_name = platform_info.os
|
|
154
|
+
|
|
155
|
+
# Apply custom OS/arch mappings if provided (e.g., Trivy uses "macOS" not "darwin")
|
|
156
|
+
if config.os_map:
|
|
157
|
+
os_name = config.os_map.get(os_name, os_name)
|
|
158
|
+
if config.arch_map:
|
|
159
|
+
arch = config.arch_map.get(arch, arch)
|
|
160
|
+
|
|
151
161
|
if platform_info.os == "windows":
|
|
152
162
|
if config.windows_archive_type:
|
|
153
163
|
archive_type = config.windows_archive_type
|
|
@@ -157,7 +167,7 @@ async def download_binary(config: DownloadConfig) -> Path | None:
|
|
|
157
167
|
# Build download URL
|
|
158
168
|
url = config.url_template.format(
|
|
159
169
|
version=config.version,
|
|
160
|
-
os=
|
|
170
|
+
os=os_name,
|
|
161
171
|
arch=arch,
|
|
162
172
|
)
|
|
163
173
|
# Handle Windows archive type in URL if different from default
|
vibeguard/core/license.py
CHANGED
|
@@ -6,7 +6,6 @@ BYOK LLM keys are separately required for patch generation.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
-
from datetime import UTC, datetime
|
|
10
9
|
from typing import Any
|
|
11
10
|
|
|
12
11
|
from vibeguard.core.keyring import get_configured_providers
|
|
@@ -164,3 +163,82 @@ def get_token_expiry_message() -> str | None:
|
|
|
164
163
|
else:
|
|
165
164
|
minutes = remaining.seconds // 60
|
|
166
165
|
return f"Token expires in {minutes} minutes"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def get_license_status_with_grace() -> dict[str, Any]:
|
|
169
|
+
"""Get detailed license status including grace period information.
|
|
170
|
+
|
|
171
|
+
This function calculates the license status for displaying expiry
|
|
172
|
+
banners in CLI commands. It returns information about:
|
|
173
|
+
- Whether the license is valid (including grace period)
|
|
174
|
+
- Whether currently in grace period
|
|
175
|
+
- Time remaining until expiry
|
|
176
|
+
|
|
177
|
+
The grace period is 48 hours after license expiration. During grace,
|
|
178
|
+
the license is still valid but users should be warned to renew.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Dictionary with:
|
|
182
|
+
- valid: bool - True if license is currently usable
|
|
183
|
+
- in_grace: bool - True if in grace period (expired but within 48h)
|
|
184
|
+
- hours_left: int - Hours remaining (in grace period)
|
|
185
|
+
- days_left: int - Days remaining until expiry (when not in grace)
|
|
186
|
+
- license_expires_at: datetime | None - When license expires
|
|
187
|
+
- grace_end: datetime | None - When grace period ends
|
|
188
|
+
"""
|
|
189
|
+
from vibeguard.core.auth import get_cached_token, get_token_time_remaining
|
|
190
|
+
|
|
191
|
+
token = get_cached_token()
|
|
192
|
+
if token is None:
|
|
193
|
+
return {
|
|
194
|
+
"valid": False,
|
|
195
|
+
"in_grace": False,
|
|
196
|
+
"hours_left": 0,
|
|
197
|
+
"days_left": 0,
|
|
198
|
+
"license_expires_at": None,
|
|
199
|
+
"grace_end": None,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
remaining = get_token_time_remaining(token)
|
|
203
|
+
total_seconds = remaining.total_seconds()
|
|
204
|
+
|
|
205
|
+
if total_seconds <= 0:
|
|
206
|
+
# Token has fully expired (past grace period)
|
|
207
|
+
return {
|
|
208
|
+
"valid": False,
|
|
209
|
+
"in_grace": False,
|
|
210
|
+
"hours_left": 0,
|
|
211
|
+
"days_left": 0,
|
|
212
|
+
"license_expires_at": token.expires_at,
|
|
213
|
+
"grace_end": token.expires_at,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Token is still valid
|
|
217
|
+
# Calculate days and hours
|
|
218
|
+
days_left = remaining.days
|
|
219
|
+
hours_left = remaining.seconds // 3600
|
|
220
|
+
|
|
221
|
+
# Check if we're likely in grace period
|
|
222
|
+
# Grace period detection heuristic: If token has < 48 hours AND
|
|
223
|
+
# the token has specific grace indicators (will be enhanced when
|
|
224
|
+
# backend adds explicit fields)
|
|
225
|
+
#
|
|
226
|
+
# For now, we detect grace by checking if the entitlements include
|
|
227
|
+
# a grace marker OR if we're in the last 48 hours (conservative approach)
|
|
228
|
+
#
|
|
229
|
+
# TODO: When backend adds explicit in_grace field, use that instead
|
|
230
|
+
in_grace = False
|
|
231
|
+
|
|
232
|
+
# Check for explicit grace indicator in entitlements
|
|
233
|
+
# Backend can add "grace.active" entitlement during grace period
|
|
234
|
+
if token.entitlements and "grace.active" in token.entitlements:
|
|
235
|
+
in_grace = True
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
"valid": True,
|
|
239
|
+
"in_grace": in_grace,
|
|
240
|
+
"hours_left": hours_left if in_grace else 0,
|
|
241
|
+
"days_left": days_left,
|
|
242
|
+
"license_expires_at": token.expires_at,
|
|
243
|
+
"grace_end": token.expires_at, # Token expiry = grace end when bounded
|
|
244
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Scan telemetry submission.
|
|
2
|
+
|
|
3
|
+
Fire-and-forget scan metadata submission to the API for Pro users.
|
|
4
|
+
Only sends aggregated counts, never individual findings.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from typing import TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
|
|
15
|
+
from vibeguard.core.auth import API_BASE_URL, API_TIMEOUT
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from vibeguard.models.scan_result import ScanResult
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def submit_scan(result: ScanResult, repo_name: str | None = None) -> None:
|
|
24
|
+
"""Fire-and-forget scan submission. Never raises, never blocks the user.
|
|
25
|
+
|
|
26
|
+
Only submits if the user has a valid Pro token cached.
|
|
27
|
+
Uses synchronous httpx to avoid event loop conflicts.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
result: The completed scan result.
|
|
31
|
+
repo_name: Optional repo name (only if user opted in via settings).
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
from vibeguard.core.auth import get_cached_token
|
|
35
|
+
|
|
36
|
+
token_obj = get_cached_token()
|
|
37
|
+
if not token_obj:
|
|
38
|
+
return # No Pro license, skip
|
|
39
|
+
|
|
40
|
+
token_str = token_obj.token
|
|
41
|
+
|
|
42
|
+
# Calculate duration in milliseconds
|
|
43
|
+
duration_ms = None
|
|
44
|
+
if result.started_at and result.finished_at:
|
|
45
|
+
delta = result.finished_at - result.started_at
|
|
46
|
+
duration_ms = int(delta.total_seconds() * 1000)
|
|
47
|
+
|
|
48
|
+
payload = {
|
|
49
|
+
"score": result.score,
|
|
50
|
+
"grade": result.grade,
|
|
51
|
+
"critical_count": result.counts.get("critical", 0),
|
|
52
|
+
"high_count": result.counts.get("high", 0),
|
|
53
|
+
"medium_count": result.counts.get("medium", 0),
|
|
54
|
+
"low_count": result.counts.get("low", 0),
|
|
55
|
+
"total_findings": len(result.findings),
|
|
56
|
+
"scanners_run": result.scanners_run,
|
|
57
|
+
"scan_duration_ms": duration_ms,
|
|
58
|
+
"partial": result.partial,
|
|
59
|
+
"repo_name": repo_name,
|
|
60
|
+
"scanned_at": (result.finished_at or datetime.now(UTC)).isoformat(),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Use sync client to avoid event loop conflicts in test/async contexts
|
|
64
|
+
with httpx.Client(timeout=API_TIMEOUT) as client:
|
|
65
|
+
resp = client.post(
|
|
66
|
+
f"{API_BASE_URL}/v1/scans",
|
|
67
|
+
json=payload,
|
|
68
|
+
headers={"Authorization": f"Bearer {token_str}"},
|
|
69
|
+
)
|
|
70
|
+
resp.raise_for_status()
|
|
71
|
+
except Exception:
|
|
72
|
+
# Never block user flow on telemetry failure
|
|
73
|
+
logger.debug("Scan submission failed (ignored)", exc_info=True)
|
vibeguard/scanners/__init__.py
CHANGED
|
@@ -24,6 +24,8 @@ class DownloadConfig(BaseModel):
|
|
|
24
24
|
archive_type: str = "tar.gz"
|
|
25
25
|
windows_archive_type: str | None = None # Override for Windows
|
|
26
26
|
windows_arch: str | None = None # Override arch for Windows (e.g., "x64")
|
|
27
|
+
os_map: dict[str, str] | None = None # Custom OS name mapping (e.g., darwin → macOS)
|
|
28
|
+
arch_map: dict[str, str] | None = None # Custom arch name mapping (e.g., amd64 → 64bit)
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class PipConfig(BaseModel):
|
|
@@ -89,6 +91,8 @@ def load_manifest(name: str) -> ScannerManifest:
|
|
|
89
91
|
archive_type=download_data.get("archive_type", "tar.gz"),
|
|
90
92
|
windows_archive_type=download_data.get("windows_archive_type"),
|
|
91
93
|
windows_arch=download_data.get("windows_arch"),
|
|
94
|
+
os_map=download_data.get("os_map"),
|
|
95
|
+
arch_map=download_data.get("arch_map"),
|
|
92
96
|
)
|
|
93
97
|
|
|
94
98
|
# Parse pip config if present
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
[scanner]
|
|
5
5
|
name = "trivy"
|
|
6
6
|
display_name = "Trivy"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.69.1"
|
|
8
8
|
tier = "core"
|
|
9
9
|
categories = ["vulnerability", "sca"]
|
|
10
10
|
languages = ["*"]
|
|
@@ -20,13 +20,23 @@ binary_name = "trivy"
|
|
|
20
20
|
|
|
21
21
|
[install.download]
|
|
22
22
|
# Auto-download binary from GitHub releases
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
url_template = "https://github.com/aquasecurity/trivy/releases/download/v{version}/trivy_{version}_{os}-
|
|
23
|
+
# Trivy naming: trivy_{version}_{os}-{arch}.tar.gz
|
|
24
|
+
# e.g., trivy_0.69.1_macOS-ARM64.tar.gz, trivy_0.69.1_Linux-64bit.tar.gz
|
|
25
|
+
url_template = "https://github.com/aquasecurity/trivy/releases/download/v{version}/trivy_{version}_{os}-{arch}.tar.gz"
|
|
26
26
|
binary_name = "trivy"
|
|
27
27
|
archive_type = "tar.gz"
|
|
28
28
|
windows_archive_type = "zip"
|
|
29
29
|
|
|
30
|
+
# Trivy uses non-standard platform names
|
|
31
|
+
[install.download.os_map]
|
|
32
|
+
linux = "Linux"
|
|
33
|
+
darwin = "macOS"
|
|
34
|
+
windows = "windows"
|
|
35
|
+
|
|
36
|
+
[install.download.arch_map]
|
|
37
|
+
amd64 = "64bit"
|
|
38
|
+
arm64 = "ARM64"
|
|
39
|
+
|
|
30
40
|
[install.docker]
|
|
31
41
|
image = "aquasec/trivy:latest"
|
|
32
42
|
mount_mode = "ro"
|
|
@@ -10,6 +10,22 @@ from vibeguard.core.downloader import VIBEGUARD_BIN_DIR
|
|
|
10
10
|
from vibeguard.scanners.runners.base import BaseRunner, RunResult
|
|
11
11
|
|
|
12
12
|
|
|
13
|
+
def _get_venv_bin_dir() -> Path | None:
|
|
14
|
+
"""Get the bin/Scripts directory of the current Python environment.
|
|
15
|
+
|
|
16
|
+
This handles pipx installs where pip-installed tools end up in the
|
|
17
|
+
isolated venv's bin directory, not on the system PATH.
|
|
18
|
+
"""
|
|
19
|
+
prefix = Path(sys.prefix)
|
|
20
|
+
if sys.platform == "win32":
|
|
21
|
+
bin_dir = prefix / "Scripts"
|
|
22
|
+
else:
|
|
23
|
+
bin_dir = prefix / "bin"
|
|
24
|
+
if bin_dir.is_dir():
|
|
25
|
+
return bin_dir
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
13
29
|
class LocalRunner(BaseRunner):
|
|
14
30
|
"""Run scanners using locally installed binaries."""
|
|
15
31
|
|
|
@@ -29,12 +45,21 @@ class LocalRunner(BaseRunner):
|
|
|
29
45
|
return "local"
|
|
30
46
|
|
|
31
47
|
def is_available(self) -> bool:
|
|
32
|
-
"""Check if binary is available in PATH or downloaded cache."""
|
|
48
|
+
"""Check if binary is available in PATH, venv bin, or downloaded cache."""
|
|
33
49
|
# First check system PATH
|
|
34
50
|
self._binary_path = shutil.which(self.binary_name)
|
|
35
51
|
if self._binary_path:
|
|
36
52
|
return True
|
|
37
53
|
|
|
54
|
+
# Check current Python environment's bin directory (handles pipx installs)
|
|
55
|
+
venv_bin = _get_venv_bin_dir()
|
|
56
|
+
if venv_bin:
|
|
57
|
+
for ext in ("", ".exe"):
|
|
58
|
+
candidate = venv_bin / f"{self.binary_name}{ext}"
|
|
59
|
+
if candidate.is_file():
|
|
60
|
+
self._binary_path = str(candidate)
|
|
61
|
+
return True
|
|
62
|
+
|
|
38
63
|
# Check downloaded binaries in ~/.vibeguard/bin/
|
|
39
64
|
if self.version:
|
|
40
65
|
downloaded = self._find_downloaded_binary()
|
|
@@ -1,37 +1,40 @@
|
|
|
1
|
-
vibeguard/__init__.py,sha256=
|
|
1
|
+
vibeguard/__init__.py,sha256=U2ZQxvmERYmQeVNYZCIq_nk5sXB4e1HB0YKIbfKgnKY,84
|
|
2
2
|
vibeguard/cli/__init__.py,sha256=Q7Z9SD1h5tJ_09RzUml6y8D3CKTYVEq_eOympr1F6tg,34
|
|
3
|
-
vibeguard/cli/apply.py,sha256=
|
|
4
|
-
vibeguard/cli/auth_cmd.py,sha256=
|
|
3
|
+
vibeguard/cli/apply.py,sha256=9Nz5qrHZX9l9Vjbgzt1N9o1nzmdSkchwn7AMTo-mJOY,13837
|
|
4
|
+
vibeguard/cli/auth_cmd.py,sha256=IViIeWFF5Qs5yDIxs93UwiRz4BEjre0xYLoloELVjZA,10714
|
|
5
|
+
vibeguard/cli/banners.py,sha256=HaKPD9YTIKn6voH5VNF9OesasuyGigY5wekOSuW67F4,3416
|
|
5
6
|
vibeguard/cli/baseline_cmd.py,sha256=zPDkWQbYTlglVtloHjod2z2CsJ5E5yEXctl6QuCCaOM,8978
|
|
6
|
-
vibeguard/cli/config_cmd.py,sha256=
|
|
7
|
+
vibeguard/cli/config_cmd.py,sha256=JTRB8oINLr9CnN-KovHbUS_QO4_xueaxIqFJf_Qr_Nw,8190
|
|
7
8
|
vibeguard/cli/display.py,sha256=dzaubid56iJXHknwHsXhPRrbgjPnOA1s_IX2O-aYeUQ,11575
|
|
8
9
|
vibeguard/cli/doctor.py,sha256=sTAZS7FrQ3_D5ghf7-FgJKpwNW-WKXVbHHn80kshCA0,7456
|
|
9
|
-
vibeguard/cli/fix.py,sha256=
|
|
10
|
+
vibeguard/cli/fix.py,sha256=SffSjheEONeJnHz-EbwkfNAMAVzyQkG0rtOBY4ygYxw,30243
|
|
10
11
|
vibeguard/cli/import_cmd.py,sha256=JBlC8Hl3SqZhuIRzFM2YoJ6QY3LNTmD0eewnazB0n9M,5869
|
|
11
12
|
vibeguard/cli/init_cmd.py,sha256=cbcFcvvkQn6C8Brz6755iBhiMHGXcFGqcE1iieXDYkU,2653
|
|
12
13
|
vibeguard/cli/keys.py,sha256=dgPa4-1hQUEiCP3eK5-M0StnFafrJAjzbHLoMwYeFkk,6153
|
|
13
14
|
vibeguard/cli/live_cmd.py,sha256=ItY1YgASC1bY2tw_UF6AgSo85exoOkDrXjHgRmAmALo,17733
|
|
14
|
-
vibeguard/cli/main.py,sha256=
|
|
15
|
-
vibeguard/cli/patch.py,sha256=
|
|
15
|
+
vibeguard/cli/main.py,sha256=_tzU_FbY_9vxPdvDm9DTmQouQUTq5r1fD7Kju8pz-u8,21122
|
|
16
|
+
vibeguard/cli/patch.py,sha256=UraKIHdMH5VurNq-kVh4HNk2EtPrPZ9km4_uFJn_6dE,27108
|
|
16
17
|
vibeguard/cli/report.py,sha256=6yWfXeQ2AP_LfcjxnqzIT-iYcS-dPQEIdl1qbnn6mus,3183
|
|
17
|
-
vibeguard/cli/scan.py,sha256
|
|
18
|
+
vibeguard/cli/scan.py,sha256=-jbZ9okgG41xPC2zjtYfcu4iJ6IgLcT2hdbKF0BpppM,45085
|
|
18
19
|
vibeguard/core/__init__.py,sha256=SsrlJh9tgUlZaUKdOt0UYAie6S05YTcVEc3Txc-gvWM,36
|
|
19
20
|
vibeguard/core/auth.py,sha256=0e9HwI78df4sbLI5WWFGI1k5fllkdlN-e94pCAtdT9c,11199
|
|
20
21
|
vibeguard/core/baseline.py,sha256=Cf9mieZlQJ7cRUU6GvHomK4cxMJLN-cv5_TwmQageQM,6028
|
|
21
|
-
vibeguard/core/bootstrap.py,sha256=
|
|
22
|
+
vibeguard/core/bootstrap.py,sha256=drtTmQsec77yd1tW6Iy_L-owivzszb4CJKd2sKwvxvc,10026
|
|
23
|
+
vibeguard/core/bundles.py,sha256=ylEYCvJa4EcfMXpBZwBn23WB4nxrP2f7koXj7cgNZbQ,8518
|
|
22
24
|
vibeguard/core/cache.py,sha256=Ot5lwbpQsKyqx2fxEfGhKb58KdqBG8BCcEwhYmTYhCY,2131
|
|
23
25
|
vibeguard/core/config.py,sha256=wFmT_7vMIqqpQiph-_pusNUBU84CHT543spS30A2NzA,2816
|
|
24
26
|
vibeguard/core/dedup.py,sha256=ogykWvMEbjO7w90eXfy-DMVubLhIXlabxswde9pMqNk,5315
|
|
25
|
-
vibeguard/core/downloader.py,sha256=
|
|
27
|
+
vibeguard/core/downloader.py,sha256=aAhIOqBZT7qag178Ld0HQVttIUre8zh2AmN5GoFudTI,7324
|
|
26
28
|
vibeguard/core/example_detector.py,sha256=rF2uytuFwvTHvmCBleyvAi6zzoOfiRCLSeEWaidZ62g,5655
|
|
27
29
|
vibeguard/core/exit_codes.py,sha256=usThcg44B_fSeg2ioLqwXmpwKswzJjj6_QIPHHYzFxs,603
|
|
28
30
|
vibeguard/core/ignore.py,sha256=YsYGzY8LPFMz9USGJqPQW_llYZ8F9Ma7wfRnDnvNe_Q,6227
|
|
29
31
|
vibeguard/core/keyring.py,sha256=YnvgP9AteOyo3wtagAfklx4iC6L7WNErdmHEk05C94o,4586
|
|
30
|
-
vibeguard/core/license.py,sha256=
|
|
32
|
+
vibeguard/core/license.py,sha256=iLuBvtb1-b4MXH7NQocNxmSnq3m4nR1S40fLBRAH-5s,7708
|
|
31
33
|
vibeguard/core/llm.py,sha256=hJBCgavfH-_ibQ7p54dP7vf9bz8XgWYEE_rViHgRT9o,5748
|
|
32
34
|
vibeguard/core/path_classifier.py,sha256=oHpZVYfxEtnUDyeCUGoC_900Xtc6vOUW9f3JwOqaAxI,6918
|
|
33
35
|
vibeguard/core/repo_detector.py,sha256=osz0cfJaRiT9msf9n1aa5zTG2EduCbVfWJJFqFx5ySc,4879
|
|
34
36
|
vibeguard/core/sarif_import.py,sha256=VCunb98gYnrg0jvIsb0SEW3wWYpY093TQdZU2RmAYTE,10734
|
|
37
|
+
vibeguard/core/telemetry.py,sha256=QELZuJtOtAFztVFIeTpDh3wTTV-isctOfutMZTmTg0U,2476
|
|
35
38
|
vibeguard/core/triage.py,sha256=nprMedHXtd_h3WDKAFX2bPvHrCNu3aLC4_WBGYqo-QY,7371
|
|
36
39
|
vibeguard/core/url_validator.py,sha256=NTu3G8viY_da225IXXDI24mj-XhY_zO-LR_6gghFFkY,6996
|
|
37
40
|
vibeguard/core/validate.py,sha256=AZ7i8170Mho4OZM3H0mFI9PV-dlu3rw4TQcewTijmqA,5503
|
|
@@ -46,7 +49,7 @@ vibeguard/reporters/__init__.py,sha256=hc4Biet907VqQK9uywFzTzWYvHEd2IrnYbjo6jACB
|
|
|
46
49
|
vibeguard/reporters/badge.py,sha256=N8bzgZLRyl85rSzEjO6PpLnaGWo9AxO--HZbMyYsVR0,3433
|
|
47
50
|
vibeguard/reporters/html.py,sha256=y6EHP3WHT2wx3PAGsXM-A6nvEOqrKq33SxVqKeFFXJU,25476
|
|
48
51
|
vibeguard/reporters/sarif.py,sha256=eMU79cDusbrw1kfG5ZMDbnqBTN-Sx4slErFA_4KRmy0,5862
|
|
49
|
-
vibeguard/scanners/__init__.py,sha256=
|
|
52
|
+
vibeguard/scanners/__init__.py,sha256=RDuoMtspPEj8Inf7ZCvMM7VRSwd6NHezcz5ckafSj8g,4486
|
|
50
53
|
vibeguard/scanners/manifests/bandit.toml,sha256=FWQtr8Yngp5688YdHmGUyPV327pT6o_YikSreJb57Do,918
|
|
51
54
|
vibeguard/scanners/manifests/cargo_audit.toml,sha256=yhBIIUhSulhmVygDzDPqZO6WK1KcrEH2Egvd1pPf7tI,748
|
|
52
55
|
vibeguard/scanners/manifests/checkov.toml,sha256=ofQqcb31dqhE2k6pP3UAktO2FgbKBOSNRJ78hIlNJrA,1010
|
|
@@ -56,7 +59,7 @@ vibeguard/scanners/manifests/npm_audit.toml,sha256=BKYbSkjOPdFwLPNBSnvwAp9aDcZSq
|
|
|
56
59
|
vibeguard/scanners/manifests/nuclei.toml,sha256=RODBwsPF0vS9KT7iq1jIIHTXHrtOPbN6422xu6iUlOA,1864
|
|
57
60
|
vibeguard/scanners/manifests/pip_audit.toml,sha256=My250Nw9KxaZivQCOnsQPVa1mDKpMN0dok51wUy_wlY,897
|
|
58
61
|
vibeguard/scanners/manifests/semgrep.toml,sha256=ESfthvQRG0-ZFpsZOPTfJUBjIw1Uhubf3iQ8BrDibcY,1087
|
|
59
|
-
vibeguard/scanners/manifests/trivy.toml,sha256=
|
|
62
|
+
vibeguard/scanners/manifests/trivy.toml,sha256=4rfoRCIacIH-8yMkrQcssPIW0gd6orT5-8Qr3drRwTY,1505
|
|
60
63
|
vibeguard/scanners/manifests/trufflehog.toml,sha256=W2XjiCR6x8tTRuXlGFJHkSH17bpGLhFmkpWorLH7gGQ,1303
|
|
61
64
|
vibeguard/scanners/parsers/__init__.py,sha256=1I6ViSBxgmtJnEbLiVLYilU8YjuiTdzKqrzIb8nnRn4,44
|
|
62
65
|
vibeguard/scanners/parsers/bandit.py,sha256=25auwnWoItnhQ8Cp_IsCm8oj4UU7aEPT1NPITB-q1-s,3071
|
|
@@ -73,9 +76,9 @@ vibeguard/scanners/parsers/trufflehog.py,sha256=lBhPuEzanvCG7HzKfnB1Xg_6Zmdkvsic
|
|
|
73
76
|
vibeguard/scanners/runners/__init__.py,sha256=649hAu1aQ9qg0Sz7O-k6INGnZZSpVPFmIcgczw0Gzco,290
|
|
74
77
|
vibeguard/scanners/runners/base.py,sha256=Vz1LXQO2iMSugYUdL3lShfZ1MLF7-O7PKRsaAmylZ3s,827
|
|
75
78
|
vibeguard/scanners/runners/docker.py,sha256=fOpdECU06msT3sTXy6JYyFv2a9KRBEFOxTR2TiA0iNo,2287
|
|
76
|
-
vibeguard/scanners/runners/local.py,sha256=
|
|
77
|
-
vibeguard_cli-1.0.
|
|
78
|
-
vibeguard_cli-1.0.
|
|
79
|
-
vibeguard_cli-1.0.
|
|
80
|
-
vibeguard_cli-1.0.
|
|
81
|
-
vibeguard_cli-1.0.
|
|
79
|
+
vibeguard/scanners/runners/local.py,sha256=Gr0w5C7gkqYINOb5fqdpMDVJb_UK-alhjJIb2JJJe9U,5452
|
|
80
|
+
vibeguard_cli-1.0.5.dist-info/METADATA,sha256=rGnSLwT_pUv_fbJmjr5A8le5EoL2RvM6KoHOmMAHH9c,6082
|
|
81
|
+
vibeguard_cli-1.0.5.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
82
|
+
vibeguard_cli-1.0.5.dist-info/entry_points.txt,sha256=BdJMlHcuuC-RbYLBsHevmOAxNw6t7wBH64KsWOOacAk,53
|
|
83
|
+
vibeguard_cli-1.0.5.dist-info/licenses/LICENSE,sha256=oh_spsv2IyABL-cLsv0tOLGHf0qU1Sx6EHJzJMCpXrU,1071
|
|
84
|
+
vibeguard_cli-1.0.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|