vesk-cli 0.1.4__tar.gz → 0.1.5__tar.gz
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.
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/PKG-INFO +3 -1
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/README.md +2 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/__init__.py +1 -1
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/cli.py +1 -1
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/runner.py +39 -3
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/tests/integration/test_scan_cli.py +12 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/.gitignore +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/LICENSE +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/NOTICE +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/docs/.gitkeep +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/examples/.gitkeep +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/pyproject.toml +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/client.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/config.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/data/.gitkeep +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/probes/__init__.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/probes/base.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/probes/data/.gitkeep +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/probes/data/jailbreak.yaml +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/probes/data/pii_leakage.yaml +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/probes/data/prompt_injection.yaml +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/probes/jailbreak.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/probes/pii_leakage.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/probes/prompt_injection.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/report/__init__.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/report/model.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/report/render.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/report/templates/report.html.j2 +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/report/templates/report.md.j2 +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/src/vesk/scaffold.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/tests/unit/test_cli.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/tests/unit/test_init.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/tests/unit/test_report.py +0 -0
- {vesk_cli-0.1.4 → vesk_cli-0.1.5}/tests/unit/test_scan.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vesk-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5
|
|
4
4
|
Summary: Vesk Verify: test LLM endpoints and produce compliance-mapped reports.
|
|
5
5
|
Author: Vesk
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -73,6 +73,8 @@ Runs the configured probes against the target and writes a results JSON file.
|
|
|
73
73
|
- `--out PATH`: results file (default `results.json`).
|
|
74
74
|
- `--env-file PATH`: optional dotenv-style file (`KEY=VALUE` lines) to read the API key from. Not read unless given; real environment variables win over the file.
|
|
75
75
|
|
|
76
|
+
While running, `vesk scan` prints progress to stderr: target host and model, each probe's start and status, and request counts. It never prints prompts, responses, API keys, URL paths or credentials; those stay in the results file only (with PII masked).
|
|
77
|
+
|
|
76
78
|
Exit codes:
|
|
77
79
|
|
|
78
80
|
| Code | Meaning |
|
|
@@ -42,6 +42,8 @@ Runs the configured probes against the target and writes a results JSON file.
|
|
|
42
42
|
- `--out PATH`: results file (default `results.json`).
|
|
43
43
|
- `--env-file PATH`: optional dotenv-style file (`KEY=VALUE` lines) to read the API key from. Not read unless given; real environment variables win over the file.
|
|
44
44
|
|
|
45
|
+
While running, `vesk scan` prints progress to stderr: target host and model, each probe's start and status, and request counts. It never prints prompts, responses, API keys, URL paths or credentials; those stay in the results file only (with PII masked).
|
|
46
|
+
|
|
45
47
|
Exit codes:
|
|
46
48
|
|
|
47
49
|
| Code | Meaning |
|
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import asyncio
|
|
6
6
|
from datetime import datetime, timezone
|
|
7
7
|
from pathlib import Path
|
|
8
|
+
from typing import Callable
|
|
9
|
+
from urllib.parse import urlsplit
|
|
8
10
|
|
|
9
11
|
from pydantic import BaseModel, Field
|
|
10
12
|
|
|
@@ -78,17 +80,35 @@ def _excerpt(text: str) -> str:
|
|
|
78
80
|
return text if len(text) <= RESPONSE_LIMIT else text[:RESPONSE_LIMIT] + "…"
|
|
79
81
|
|
|
80
82
|
|
|
81
|
-
|
|
83
|
+
def display_host(base_url: str) -> str:
|
|
84
|
+
"""Host[:port] only: drops credentials, path and query so nothing sensitive is echoed."""
|
|
85
|
+
return urlsplit(base_url).netloc.rpartition("@")[2] or "target"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def _run_probe(
|
|
89
|
+
probe: Probe,
|
|
90
|
+
cfg: Config,
|
|
91
|
+
client: ChatClient,
|
|
92
|
+
sem: asyncio.Semaphore,
|
|
93
|
+
progress: Progress | None = None,
|
|
94
|
+
):
|
|
82
95
|
cases = probe.cases(cfg.target.system_prompt, cfg.scan.samples_per_probe, cfg.scan.seed)
|
|
83
96
|
errors: list[str] = []
|
|
97
|
+
done = 0
|
|
98
|
+
step = max(1, len(cases) // 4)
|
|
84
99
|
|
|
85
100
|
async def one(case) -> CaseResult | None:
|
|
101
|
+
nonlocal done
|
|
86
102
|
async with sem:
|
|
87
103
|
try:
|
|
88
104
|
response = await client.complete(case.messages)
|
|
89
105
|
except EndpointError as e:
|
|
90
106
|
errors.append(str(e))
|
|
91
107
|
return None
|
|
108
|
+
finally:
|
|
109
|
+
done += 1
|
|
110
|
+
if progress and (done == len(cases) or done % step == 0):
|
|
111
|
+
progress(f" {probe.name}: {done}/{len(cases)} requests")
|
|
92
112
|
verdict = probe.evaluate(case, response)
|
|
93
113
|
return CaseResult(
|
|
94
114
|
id=case.id,
|
|
@@ -118,12 +138,28 @@ async def _run_probe(probe: Probe, cfg: Config, client: ChatClient, sem: asyncio
|
|
|
118
138
|
)
|
|
119
139
|
|
|
120
140
|
|
|
141
|
+
Progress = Callable[[str], None]
|
|
142
|
+
|
|
143
|
+
|
|
121
144
|
async def run_scan(
|
|
122
|
-
cfg: Config, probes: list[Probe], client: ChatClient
|
|
145
|
+
cfg: Config, probes: list[Probe], client: ChatClient, progress: Progress | None = None
|
|
123
146
|
) -> ScanResults:
|
|
147
|
+
"""`progress` receives short status lines: probe names and counts only, never content."""
|
|
124
148
|
started = _now()
|
|
125
149
|
sem = asyncio.Semaphore(cfg.scan.max_concurrency)
|
|
126
|
-
|
|
150
|
+
if progress:
|
|
151
|
+
progress(
|
|
152
|
+
f"Scanning {display_host(cfg.target.base_url)} (model {cfg.target.model}) "
|
|
153
|
+
f"with {len(probes)} probe(s)"
|
|
154
|
+
)
|
|
155
|
+
runs = []
|
|
156
|
+
for i, p in enumerate(probes, 1):
|
|
157
|
+
if progress:
|
|
158
|
+
progress(f"[{i}/{len(probes)}] {p.name}: running")
|
|
159
|
+
run = await _run_probe(p, cfg, client, sem, progress)
|
|
160
|
+
if progress:
|
|
161
|
+
progress(f"[{i}/{len(probes)}] {p.name}: {run.status}")
|
|
162
|
+
runs.append(run)
|
|
127
163
|
return ScanResults(
|
|
128
164
|
target=cfg.target.base_url,
|
|
129
165
|
model=cfg.target.model,
|
|
@@ -120,6 +120,18 @@ def test_endpoint_down_exits_3_and_still_writes_results(cfg, tmp_path):
|
|
|
120
120
|
assert all(p["status"] == "error" and p["error"] for p in data["probes"])
|
|
121
121
|
|
|
122
122
|
|
|
123
|
+
@respx.mock
|
|
124
|
+
def test_progress_shows_status_without_sensitive_data(cfg, tmp_path):
|
|
125
|
+
respx.post(URL).mock(side_effect=vulnerable)
|
|
126
|
+
r, out = scan(cfg, tmp_path)
|
|
127
|
+
assert "Scanning llm.test (model m) with 3 probe(s)" in r.output
|
|
128
|
+
assert "[1/3]" in r.output and "[3/3]" in r.output
|
|
129
|
+
assert "4/4 requests" in r.output
|
|
130
|
+
assert "secret-key" not in r.output
|
|
131
|
+
assert "/v1" not in r.output.split("Scanning", 1)[1].splitlines()[0]
|
|
132
|
+
assert not MARKER.search(r.output)
|
|
133
|
+
|
|
134
|
+
|
|
123
135
|
@respx.mock
|
|
124
136
|
def test_sends_bearer_key(cfg, tmp_path):
|
|
125
137
|
route = respx.post(URL).mock(side_effect=hardened)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|