vibeguard-cli 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vibeguard/__init__.py +3 -0
- vibeguard/cli/__init__.py +1 -0
- vibeguard/cli/apply.py +413 -0
- vibeguard/cli/auth_cmd.py +318 -0
- vibeguard/cli/baseline_cmd.py +286 -0
- vibeguard/cli/config_cmd.py +252 -0
- vibeguard/cli/display.py +356 -0
- vibeguard/cli/doctor.py +228 -0
- vibeguard/cli/fix.py +977 -0
- vibeguard/cli/import_cmd.py +180 -0
- vibeguard/cli/init_cmd.py +113 -0
- vibeguard/cli/keys.py +193 -0
- vibeguard/cli/live_cmd.py +564 -0
- vibeguard/cli/main.py +667 -0
- vibeguard/cli/patch.py +805 -0
- vibeguard/cli/report.py +106 -0
- vibeguard/cli/scan.py +1227 -0
- vibeguard/core/__init__.py +1 -0
- vibeguard/core/auth.py +402 -0
- vibeguard/core/baseline.py +212 -0
- vibeguard/core/bootstrap.py +303 -0
- vibeguard/core/cache.py +77 -0
- vibeguard/core/config.py +99 -0
- vibeguard/core/dedup.py +168 -0
- vibeguard/core/downloader.py +222 -0
- vibeguard/core/example_detector.py +159 -0
- vibeguard/core/exit_codes.py +19 -0
- vibeguard/core/ignore.py +243 -0
- vibeguard/core/keyring.py +188 -0
- vibeguard/core/license.py +166 -0
- vibeguard/core/llm.py +206 -0
- vibeguard/core/path_classifier.py +152 -0
- vibeguard/core/repo_detector.py +143 -0
- vibeguard/core/sarif_import.py +342 -0
- vibeguard/core/triage.py +205 -0
- vibeguard/core/url_validator.py +259 -0
- vibeguard/core/validate.py +174 -0
- vibeguard/models/__init__.py +24 -0
- vibeguard/models/auth.py +92 -0
- vibeguard/models/baseline.py +105 -0
- vibeguard/models/finding.py +78 -0
- vibeguard/models/patch.py +164 -0
- vibeguard/models/scan_result.py +190 -0
- vibeguard/models/triage.py +53 -0
- vibeguard/reporters/__init__.py +7 -0
- vibeguard/reporters/badge.py +103 -0
- vibeguard/reporters/html.py +920 -0
- vibeguard/reporters/sarif.py +175 -0
- vibeguard/scanners/__init__.py +130 -0
- vibeguard/scanners/manifests/bandit.toml +39 -0
- vibeguard/scanners/manifests/cargo_audit.toml +31 -0
- vibeguard/scanners/manifests/checkov.toml +37 -0
- vibeguard/scanners/manifests/dockle.toml +46 -0
- vibeguard/scanners/manifests/gitleaks.toml +48 -0
- vibeguard/scanners/manifests/npm_audit.toml +31 -0
- vibeguard/scanners/manifests/nuclei.toml +58 -0
- vibeguard/scanners/manifests/pip_audit.toml +36 -0
- vibeguard/scanners/manifests/semgrep.toml +43 -0
- vibeguard/scanners/manifests/trivy.toml +50 -0
- vibeguard/scanners/manifests/trufflehog.toml +48 -0
- vibeguard/scanners/parsers/__init__.py +1 -0
- vibeguard/scanners/parsers/bandit.py +94 -0
- vibeguard/scanners/parsers/cargo_audit.py +185 -0
- vibeguard/scanners/parsers/checkov.py +179 -0
- vibeguard/scanners/parsers/dockle.py +185 -0
- vibeguard/scanners/parsers/gitleaks.py +95 -0
- vibeguard/scanners/parsers/npm_audit.py +219 -0
- vibeguard/scanners/parsers/nuclei.py +247 -0
- vibeguard/scanners/parsers/pip_audit.py +166 -0
- vibeguard/scanners/parsers/semgrep.py +110 -0
- vibeguard/scanners/parsers/trivy.py +86 -0
- vibeguard/scanners/parsers/trufflehog.py +150 -0
- vibeguard/scanners/runners/__init__.py +7 -0
- vibeguard/scanners/runners/base.py +41 -0
- vibeguard/scanners/runners/docker.py +86 -0
- vibeguard/scanners/runners/local.py +144 -0
- vibeguard_cli-1.0.0.dist-info/METADATA +223 -0
- vibeguard_cli-1.0.0.dist-info/RECORD +81 -0
- vibeguard_cli-1.0.0.dist-info/WHEEL +4 -0
- vibeguard_cli-1.0.0.dist-info/entry_points.txt +2 -0
- vibeguard_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
vibeguard/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands for VibeGuard."""
|
vibeguard/cli/apply.py
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
"""Apply command - safely apply patches with git checks (PRO tier)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess # nosec B404 # noqa: S404 # needed for git operations
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.syntax import Syntax
|
|
13
|
+
|
|
14
|
+
from vibeguard.cli.display import get_console
|
|
15
|
+
from vibeguard.core.exit_codes import ExitCode
|
|
16
|
+
from vibeguard.core.license import ProFeatureError, require_pro_license
|
|
17
|
+
from vibeguard.models.patch import validate_unified_diff
|
|
18
|
+
|
|
19
|
+
console = get_console()
|
|
20
|
+
|
|
21
|
+
# Default patches directory
|
|
22
|
+
PATCHES_DIR = ".vibeguard/patches"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def apply(
|
|
26
|
+
patch_file: Path | None = typer.Argument(
|
|
27
|
+
None,
|
|
28
|
+
help="Path to the .patch file to apply",
|
|
29
|
+
exists=True,
|
|
30
|
+
file_okay=True,
|
|
31
|
+
dir_okay=False,
|
|
32
|
+
resolve_path=True,
|
|
33
|
+
),
|
|
34
|
+
path: Path = typer.Option(
|
|
35
|
+
Path("."),
|
|
36
|
+
"--path",
|
|
37
|
+
"-p",
|
|
38
|
+
help="Path to the git repository",
|
|
39
|
+
exists=True,
|
|
40
|
+
file_okay=False,
|
|
41
|
+
dir_okay=True,
|
|
42
|
+
resolve_path=True,
|
|
43
|
+
),
|
|
44
|
+
dry_run: bool = typer.Option(
|
|
45
|
+
False,
|
|
46
|
+
"--dry-run",
|
|
47
|
+
help="Check if patch applies cleanly without actually applying",
|
|
48
|
+
),
|
|
49
|
+
force: bool = typer.Option(
|
|
50
|
+
False,
|
|
51
|
+
"--force",
|
|
52
|
+
"-f",
|
|
53
|
+
help="Apply even if working directory has uncommitted changes",
|
|
54
|
+
),
|
|
55
|
+
reverse: bool = typer.Option(
|
|
56
|
+
False,
|
|
57
|
+
"--reverse",
|
|
58
|
+
"-R",
|
|
59
|
+
help="Reverse/unapply the patch",
|
|
60
|
+
),
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Apply a generated patch to fix a security finding.
|
|
63
|
+
|
|
64
|
+
This is a PRO feature that requires an active Pro license.
|
|
65
|
+
Run 'vibeguard auth login <license-key>' to activate.
|
|
66
|
+
|
|
67
|
+
Safety checks performed:
|
|
68
|
+
- Verifies patch file is valid unified diff
|
|
69
|
+
- Checks repository is a git repo
|
|
70
|
+
- Warns if working directory has uncommitted changes
|
|
71
|
+
- Uses 'git apply --check' to verify patch applies cleanly
|
|
72
|
+
- Creates backup information before applying
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
vibeguard apply .vibeguard/patches/abc123.patch
|
|
76
|
+
vibeguard apply my-fix.patch --dry-run
|
|
77
|
+
vibeguard apply .vibeguard/patches/abc123.patch --reverse
|
|
78
|
+
"""
|
|
79
|
+
target = path.resolve()
|
|
80
|
+
|
|
81
|
+
# Handle missing patch_file with helpful message
|
|
82
|
+
if patch_file is None:
|
|
83
|
+
_show_missing_patch_help(target)
|
|
84
|
+
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
85
|
+
|
|
86
|
+
# Check Pro license
|
|
87
|
+
try:
|
|
88
|
+
require_pro_license("Patch application")
|
|
89
|
+
except ProFeatureError as e:
|
|
90
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
91
|
+
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
92
|
+
|
|
93
|
+
# Verify patch file exists and is readable
|
|
94
|
+
patch_content = _read_patch_file(patch_file)
|
|
95
|
+
if patch_content is None:
|
|
96
|
+
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
97
|
+
|
|
98
|
+
# Validate patch format
|
|
99
|
+
is_valid, error = validate_unified_diff(patch_content)
|
|
100
|
+
if not is_valid:
|
|
101
|
+
console.print(f"[red]Error:[/red] Invalid patch file: {error}")
|
|
102
|
+
console.print(f"\n[dim]File: {patch_file}[/dim]")
|
|
103
|
+
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
104
|
+
|
|
105
|
+
# Check if target is a git repository
|
|
106
|
+
if not _is_git_repo(target):
|
|
107
|
+
console.print(f"[red]Error:[/red] Not a git repository: {target}")
|
|
108
|
+
console.print("\nVibeGuard apply requires a git repository for safety checks.")
|
|
109
|
+
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
110
|
+
|
|
111
|
+
# Load patch metadata if available
|
|
112
|
+
metadata = _load_patch_metadata(patch_file)
|
|
113
|
+
|
|
114
|
+
# Show patch info
|
|
115
|
+
_show_patch_info(patch_file, patch_content, metadata, reverse)
|
|
116
|
+
|
|
117
|
+
# Check working directory status
|
|
118
|
+
if not force:
|
|
119
|
+
has_changes, status_output = _check_working_directory(target)
|
|
120
|
+
if has_changes:
|
|
121
|
+
console.print("[yellow]Warning:[/yellow] Working directory has uncommitted changes.\n")
|
|
122
|
+
console.print("[dim]Uncommitted files:[/dim]")
|
|
123
|
+
console.print(status_output[:500])
|
|
124
|
+
console.print()
|
|
125
|
+
console.print(
|
|
126
|
+
"Use [cyan]--force[/cyan] to apply anyway, or commit/stash changes first."
|
|
127
|
+
)
|
|
128
|
+
raise typer.Exit(ExitCode.CONFIG_ERROR)
|
|
129
|
+
|
|
130
|
+
# Verify patch applies cleanly with git apply --check
|
|
131
|
+
console.print("[dim]Checking if patch applies cleanly...[/dim]")
|
|
132
|
+
can_apply, check_error = _git_apply_check(target, patch_file, reverse)
|
|
133
|
+
|
|
134
|
+
if not can_apply:
|
|
135
|
+
console.print("[red]Error:[/red] Patch cannot be applied cleanly.\n")
|
|
136
|
+
console.print("[bold]Git apply check output:[/bold]")
|
|
137
|
+
console.print(check_error)
|
|
138
|
+
console.print()
|
|
139
|
+
console.print("[dim]Tips:[/dim]")
|
|
140
|
+
console.print(" - The file may have been modified since the patch was generated")
|
|
141
|
+
console.print(
|
|
142
|
+
" - Try running [cyan]vibeguard scan[/cyan] and [cyan]vibeguard patch[/cyan] again"
|
|
143
|
+
)
|
|
144
|
+
console.print(" - Check if the patch was already applied")
|
|
145
|
+
raise typer.Exit(ExitCode.SCAN_ERROR)
|
|
146
|
+
|
|
147
|
+
console.print("[green]Patch applies cleanly![/green]\n")
|
|
148
|
+
|
|
149
|
+
# Dry run - stop here
|
|
150
|
+
if dry_run:
|
|
151
|
+
console.print("[dim]Dry run - patch not applied[/dim]")
|
|
152
|
+
console.print(f"\nTo apply: [cyan]vibeguard apply {patch_file}[/cyan]")
|
|
153
|
+
raise typer.Exit(ExitCode.SUCCESS)
|
|
154
|
+
|
|
155
|
+
# Get current HEAD for reference
|
|
156
|
+
head_before = _get_git_head(target)
|
|
157
|
+
|
|
158
|
+
# Apply the patch
|
|
159
|
+
action = "Reversing" if reverse else "Applying"
|
|
160
|
+
console.print(f"[dim]{action} patch...[/dim]")
|
|
161
|
+
|
|
162
|
+
success, apply_output = _git_apply(target, patch_file, reverse)
|
|
163
|
+
|
|
164
|
+
if not success:
|
|
165
|
+
console.print("[red]Error:[/red] Failed to apply patch.\n")
|
|
166
|
+
console.print(apply_output)
|
|
167
|
+
raise typer.Exit(ExitCode.SCAN_ERROR)
|
|
168
|
+
|
|
169
|
+
# Success!
|
|
170
|
+
action_past = "reversed" if reverse else "applied"
|
|
171
|
+
console.print(f"[green]Patch {action_past} successfully![/green]\n")
|
|
172
|
+
|
|
173
|
+
# Show what changed
|
|
174
|
+
console.print("[dim]Modified files:[/dim]")
|
|
175
|
+
modified = _get_modified_files_from_patch(patch_content)
|
|
176
|
+
for f in modified:
|
|
177
|
+
console.print(f" {f}")
|
|
178
|
+
|
|
179
|
+
console.print()
|
|
180
|
+
console.print("[dim]Next steps:[/dim]")
|
|
181
|
+
console.print(" 1. Review the changes: [cyan]git diff[/cyan]")
|
|
182
|
+
console.print(" 2. Run tests to verify the fix")
|
|
183
|
+
console.print(" 3. Commit when satisfied: [cyan]git commit -am 'Fix security finding'[/cyan]")
|
|
184
|
+
|
|
185
|
+
if head_before:
|
|
186
|
+
console.print(f"\n[dim]To undo: git checkout {head_before[:8]} -- <files>[/dim]")
|
|
187
|
+
|
|
188
|
+
raise typer.Exit(ExitCode.SUCCESS)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _show_missing_patch_help(target: Path) -> None:
|
|
192
|
+
"""Show helpful error when patch file is missing."""
|
|
193
|
+
console.print("[red]Error:[/red] Missing patch file.\n")
|
|
194
|
+
console.print("[bold]Usage:[/bold] vibeguard apply <patch-file>\n")
|
|
195
|
+
|
|
196
|
+
# Check for available patches
|
|
197
|
+
patches_dir = target / PATCHES_DIR
|
|
198
|
+
if patches_dir.exists():
|
|
199
|
+
patches = list(patches_dir.glob("*.patch"))
|
|
200
|
+
if patches:
|
|
201
|
+
console.print("[bold]Available patches:[/bold]")
|
|
202
|
+
for p in patches[:10]:
|
|
203
|
+
# Try to load metadata for context
|
|
204
|
+
meta = _load_patch_metadata(p)
|
|
205
|
+
if meta:
|
|
206
|
+
console.print(
|
|
207
|
+
f" [green]{p.name}[/green] {meta.get('file_path', '')[:40]}"
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
console.print(f" [green]{p.name}[/green]")
|
|
211
|
+
if len(patches) > 10:
|
|
212
|
+
console.print(f" [dim]... and {len(patches) - 10} more[/dim]")
|
|
213
|
+
console.print(f"\n[bold]Example:[/bold] vibeguard apply {patches[0]}")
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
console.print("Generate a patch first using:")
|
|
217
|
+
console.print(" 1. [cyan]vibeguard scan[/cyan] (find security issues)")
|
|
218
|
+
console.print(" 2. [cyan]vibeguard patch <finding-id>[/cyan] (generate fix)")
|
|
219
|
+
console.print(" 3. [cyan]vibeguard apply <patch-file>[/cyan] (apply fix)")
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _read_patch_file(patch_file: Path) -> str | None:
|
|
223
|
+
"""Read patch file content."""
|
|
224
|
+
try:
|
|
225
|
+
return patch_file.read_text(encoding="utf-8")
|
|
226
|
+
except OSError as e:
|
|
227
|
+
console.print(f"[red]Error:[/red] Cannot read patch file: {e}")
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _is_git_repo(path: Path) -> bool:
|
|
232
|
+
"""Check if path is inside a git repository."""
|
|
233
|
+
git_dir = path / ".git"
|
|
234
|
+
if git_dir.exists():
|
|
235
|
+
return True
|
|
236
|
+
|
|
237
|
+
# Check parent directories
|
|
238
|
+
try:
|
|
239
|
+
result = subprocess.run( # nosec B603 B607
|
|
240
|
+
["git", "rev-parse", "--git-dir"],
|
|
241
|
+
cwd=path,
|
|
242
|
+
capture_output=True,
|
|
243
|
+
text=True,
|
|
244
|
+
timeout=10,
|
|
245
|
+
)
|
|
246
|
+
return result.returncode == 0
|
|
247
|
+
except (subprocess.SubprocessError, OSError):
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _check_working_directory(path: Path) -> tuple[bool, str]:
|
|
252
|
+
"""Check if working directory has uncommitted changes.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Tuple of (has_changes, status_output)
|
|
256
|
+
"""
|
|
257
|
+
try:
|
|
258
|
+
result = subprocess.run( # nosec B603 B607
|
|
259
|
+
["git", "status", "--porcelain"],
|
|
260
|
+
cwd=path,
|
|
261
|
+
capture_output=True,
|
|
262
|
+
text=True,
|
|
263
|
+
timeout=30,
|
|
264
|
+
)
|
|
265
|
+
output = result.stdout.strip()
|
|
266
|
+
return bool(output), output
|
|
267
|
+
except (subprocess.SubprocessError, OSError):
|
|
268
|
+
return False, ""
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _git_apply_check(
|
|
272
|
+
path: Path, patch_file: Path, reverse: bool = False
|
|
273
|
+
) -> tuple[bool, str]:
|
|
274
|
+
"""Run git apply --check to verify patch.
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Tuple of (can_apply, error_output)
|
|
278
|
+
"""
|
|
279
|
+
cmd = ["git", "apply", "--check"]
|
|
280
|
+
if reverse:
|
|
281
|
+
cmd.append("--reverse")
|
|
282
|
+
cmd.append(str(patch_file))
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
result = subprocess.run( # nosec B603
|
|
286
|
+
cmd,
|
|
287
|
+
cwd=path,
|
|
288
|
+
capture_output=True,
|
|
289
|
+
text=True,
|
|
290
|
+
timeout=60,
|
|
291
|
+
)
|
|
292
|
+
if result.returncode == 0:
|
|
293
|
+
return True, ""
|
|
294
|
+
return False, result.stderr or result.stdout
|
|
295
|
+
except subprocess.TimeoutExpired:
|
|
296
|
+
return False, "Git apply check timed out"
|
|
297
|
+
except (subprocess.SubprocessError, OSError) as e:
|
|
298
|
+
return False, str(e)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _git_apply(
|
|
302
|
+
path: Path, patch_file: Path, reverse: bool = False
|
|
303
|
+
) -> tuple[bool, str]:
|
|
304
|
+
"""Apply patch using git apply.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Tuple of (success, output)
|
|
308
|
+
"""
|
|
309
|
+
cmd = ["git", "apply"]
|
|
310
|
+
if reverse:
|
|
311
|
+
cmd.append("--reverse")
|
|
312
|
+
cmd.append(str(patch_file))
|
|
313
|
+
|
|
314
|
+
try:
|
|
315
|
+
result = subprocess.run( # nosec B603
|
|
316
|
+
cmd,
|
|
317
|
+
cwd=path,
|
|
318
|
+
capture_output=True,
|
|
319
|
+
text=True,
|
|
320
|
+
timeout=60,
|
|
321
|
+
)
|
|
322
|
+
if result.returncode == 0:
|
|
323
|
+
return True, result.stdout
|
|
324
|
+
return False, result.stderr or result.stdout
|
|
325
|
+
except subprocess.TimeoutExpired:
|
|
326
|
+
return False, "Git apply timed out"
|
|
327
|
+
except (subprocess.SubprocessError, OSError) as e:
|
|
328
|
+
return False, str(e)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _get_git_head(path: Path) -> str | None:
|
|
332
|
+
"""Get current HEAD commit hash."""
|
|
333
|
+
try:
|
|
334
|
+
result = subprocess.run( # nosec B603 B607
|
|
335
|
+
["git", "rev-parse", "HEAD"],
|
|
336
|
+
cwd=path,
|
|
337
|
+
capture_output=True,
|
|
338
|
+
text=True,
|
|
339
|
+
timeout=10,
|
|
340
|
+
)
|
|
341
|
+
if result.returncode == 0:
|
|
342
|
+
return result.stdout.strip()
|
|
343
|
+
except (subprocess.SubprocessError, OSError):
|
|
344
|
+
pass
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _load_patch_metadata(patch_file: Path) -> dict[str, Any] | None:
|
|
349
|
+
"""Load patch metadata JSON if available."""
|
|
350
|
+
meta_path = patch_file.with_suffix(".json")
|
|
351
|
+
if not meta_path.exists():
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
try:
|
|
355
|
+
content = meta_path.read_text(encoding="utf-8")
|
|
356
|
+
data: dict[str, Any] = json.loads(content)
|
|
357
|
+
return data
|
|
358
|
+
except (OSError, json.JSONDecodeError):
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _show_patch_info(
|
|
363
|
+
patch_file: Path,
|
|
364
|
+
patch_content: str,
|
|
365
|
+
metadata: dict[str, Any] | None,
|
|
366
|
+
reverse: bool,
|
|
367
|
+
) -> None:
|
|
368
|
+
"""Display patch information."""
|
|
369
|
+
action = "Reversing" if reverse else "Applying"
|
|
370
|
+
title = f"{action} Patch"
|
|
371
|
+
|
|
372
|
+
info_lines = [f"[bold]File:[/bold] {patch_file.name}"]
|
|
373
|
+
|
|
374
|
+
if metadata:
|
|
375
|
+
if "file_path" in metadata:
|
|
376
|
+
info_lines.append(f"[bold]Target:[/bold] {metadata['file_path']}")
|
|
377
|
+
if "finding_id" in metadata:
|
|
378
|
+
info_lines.append(f"[bold]Finding:[/bold] {metadata['finding_id'][:12]}")
|
|
379
|
+
if "provider" in metadata and "model" in metadata:
|
|
380
|
+
info_lines.append(
|
|
381
|
+
f"[bold]Generated by:[/bold] {metadata['provider']}/{metadata['model']}"
|
|
382
|
+
)
|
|
383
|
+
if metadata.get("manual_review_required"):
|
|
384
|
+
info_lines.append("[yellow]Note: Manual review markers present[/yellow]")
|
|
385
|
+
|
|
386
|
+
console.print(Panel("\n".join(info_lines), title=title))
|
|
387
|
+
console.print()
|
|
388
|
+
|
|
389
|
+
# Show diff preview (first 30 lines)
|
|
390
|
+
lines = patch_content.split("\n")
|
|
391
|
+
preview = "\n".join(lines[:30])
|
|
392
|
+
if len(lines) > 30:
|
|
393
|
+
preview += f"\n... ({len(lines) - 30} more lines)"
|
|
394
|
+
|
|
395
|
+
syntax = Syntax(preview, "diff", theme="monokai", line_numbers=False)
|
|
396
|
+
console.print(Panel(syntax, title="Patch Preview"))
|
|
397
|
+
console.print()
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _get_modified_files_from_patch(patch_content: str) -> list[str]:
|
|
401
|
+
"""Extract list of files modified by the patch."""
|
|
402
|
+
files = []
|
|
403
|
+
for line in patch_content.split("\n"):
|
|
404
|
+
if line.startswith("+++ "):
|
|
405
|
+
# Extract file path, removing a/ or b/ prefix
|
|
406
|
+
file_path = line[4:].strip()
|
|
407
|
+
if file_path.startswith("b/"):
|
|
408
|
+
file_path = file_path[2:]
|
|
409
|
+
elif file_path.startswith("a/"):
|
|
410
|
+
file_path = file_path[2:]
|
|
411
|
+
if file_path and file_path != "/dev/null":
|
|
412
|
+
files.append(file_path)
|
|
413
|
+
return files
|