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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """VibeGuard CLI - Unified security scanner orchestrator."""
2
2
 
3
- __version__ = "1.0.0"
3
+ __version__ = "1.0.5"
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 ProFeatureError, require_pro_license
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,
@@ -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()
@@ -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 VibeGuardConfig, find_config_file, load_config, save_config
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(f"[red]Error:[/red] Invalid key format. Use section.setting (e.g., report.format)")
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(f"[red]Error:[/red] Invalid format. Choose: html, json, sarif")
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(f"[red]Error:[/red] Invalid pack. Choose: core, ecosystem, full")
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(f"[red]Error:[/red] Invalid severity. Choose: critical, high, medium, low, info")
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(f"[red]Error:[/red] Invalid format. Choose: terminal, json, sarif, html")
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(f"\n[green]Settings saved![/green]")
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
- FIX_PROMPT_TEMPLATE = """\
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(findings: list[Finding], target: Path, bulk: bool) -> None:
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(finding: Finding, repo_root: Path) -> str:
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 FIX_PROMPT_TEMPLATE.format(
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 ProFeatureError, require_patch_capability
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(finding, target, provider, model, output, dry_run)
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(_generate_patch_async(prompt, provider, model))
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=4096,
804
- temperature=0.2, # Low temperature for deterministic output
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
 
@@ -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
- return shutil.which(binary_name) is not None
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)
@@ -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=platform_info.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)
@@ -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.50.4"
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
- # Note: Trivy uses non-standard naming (Linux-64bit, windows-64bit)
24
- # TODO: Add per-scanner OS/arch mapping for full platform support
25
- url_template = "https://github.com/aquasecurity/trivy/releases/download/v{version}/trivy_{version}_{os}-64bit.tar.gz"
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vibeguard-cli
3
- Version: 1.0.0
3
+ Version: 1.0.5
4
4
  Summary: Unified security scanner orchestrator for local repos
5
5
  Author: VibeGuard Team
6
6
  License: MIT
@@ -1,37 +1,40 @@
1
- vibeguard/__init__.py,sha256=4CX7CNi5oeMgILXscJgjJ9vOhEdg7OTFb8wR5YgC1Vo,84
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=OCLOZXvpVr7PnI_tWVM-yhMICo9IBZ46V_4F-4dCWRg,13537
4
- vibeguard/cli/auth_cmd.py,sha256=AXyN1vQNEJOoR-FtcDMqBYn6YinzuKfcEZeI-XyA5jA,10764
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=9Ua2FXolcGVH17B7YpYwhMv8Wu5BSOk4c5vhEcFXcd8,8213
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=gaVqN_n_F7Nb-C7WG2hEdDP6X6WwVsFoTHhRAhp4tOA,29407
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=p6pzPd2ErrrQ7Kl-eR59X-KvcPYteFh-hskReaopQto,21121
15
- vibeguard/cli/patch.py,sha256=cIJqdMkPbOTo7PgU_OSJqJ0y5HRgsRXN5Nyl9e79Q3Y,25437
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=pw3hGN-VMmzKwmp0kou3XS_xggcg3sWjG1rW6PgbOP8,44875
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=4d2aFL6SYzyfIjA-8Jow4hU1jQDEGRiU1zDtXPtEsdo,9501
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=sQAnVLpVAfl2c5iMUOEz-5FgtFqRQl_pGTt27gMiVz4,6872
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=JBFx_C_dMIXocKzBMw2WRUR5o-A8xLLLrWkhH_50TWY,4886
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=eqy_uVWvlIK0JBvM-sO9j8mzcZXtg_R5S83YqAGCFYA,4199
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=j3pEOnDP6iLhCZYTeEXLWrozC1gR15NQEixv6X5FT8A,1332
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=5Lu0yJ1H4fjRDY2aB-AxMihIDlNyHAgpKa01v3qQa3I,4608
77
- vibeguard_cli-1.0.0.dist-info/METADATA,sha256=HKBJUmIsuE3N1uHh_tlyS7ZpVeMjU76wzcRDHMlcipE,6082
78
- vibeguard_cli-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
79
- vibeguard_cli-1.0.0.dist-info/entry_points.txt,sha256=BdJMlHcuuC-RbYLBsHevmOAxNw6t7wBH64KsWOOacAk,53
80
- vibeguard_cli-1.0.0.dist-info/licenses/LICENSE,sha256=oh_spsv2IyABL-cLsv0tOLGHf0qU1Sx6EHJzJMCpXrU,1071
81
- vibeguard_cli-1.0.0.dist-info/RECORD,,
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,,