vesk-cli 0.1.2__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.
- vesk/__init__.py +3 -0
- vesk/cli.py +121 -0
- vesk/client.py +80 -0
- vesk/config.py +78 -0
- vesk/data/.gitkeep +0 -0
- vesk/probes/__init__.py +12 -0
- vesk/probes/base.py +68 -0
- vesk/probes/data/.gitkeep +0 -0
- vesk/probes/data/jailbreak.yaml +27 -0
- vesk/probes/data/pii_leakage.yaml +26 -0
- vesk/probes/data/prompt_injection.yaml +26 -0
- vesk/probes/jailbreak.py +52 -0
- vesk/probes/pii_leakage.py +91 -0
- vesk/probes/prompt_injection.py +48 -0
- vesk/report/__init__.py +1 -0
- vesk/report/model.py +43 -0
- vesk/report/render.py +52 -0
- vesk/report/templates/report.html.j2 +48 -0
- vesk/report/templates/report.md.j2 +48 -0
- vesk/runner.py +134 -0
- vesk/scaffold.py +48 -0
- vesk_cli-0.1.2.dist-info/METADATA +148 -0
- vesk_cli-0.1.2.dist-info/RECORD +27 -0
- vesk_cli-0.1.2.dist-info/WHEEL +4 -0
- vesk_cli-0.1.2.dist-info/entry_points.txt +2 -0
- vesk_cli-0.1.2.dist-info/licenses/LICENSE +201 -0
- vesk_cli-0.1.2.dist-info/licenses/NOTICE +5 -0
vesk/report/model.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Schema of the results JSON consumed by `vesk report`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
Status = Literal["pass", "fail", "error"]
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Case(BaseModel):
|
|
13
|
+
id: str
|
|
14
|
+
passed: bool
|
|
15
|
+
prompt: str = ""
|
|
16
|
+
response: str = ""
|
|
17
|
+
evidence: str = ""
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProbeResult(BaseModel):
|
|
21
|
+
id: str
|
|
22
|
+
name: str = ""
|
|
23
|
+
category: str = ""
|
|
24
|
+
status: Status
|
|
25
|
+
description: str = ""
|
|
26
|
+
error: str = ""
|
|
27
|
+
cases: list[Case] = Field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def failed_cases(self) -> list[Case]:
|
|
31
|
+
return [c for c in self.cases if not c.passed]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Results(BaseModel):
|
|
35
|
+
vesk_version: str = ""
|
|
36
|
+
target: str = ""
|
|
37
|
+
model: str = ""
|
|
38
|
+
started_at: str = ""
|
|
39
|
+
finished_at: str = ""
|
|
40
|
+
probes: list[ProbeResult] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
def count(self, status: Status) -> int:
|
|
43
|
+
return sum(1 for p in self.probes if p.status == status)
|
vesk/report/render.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Render a Results object to Markdown or HTML."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from jinja2 import Environment, FileSystemLoader
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from vesk.report.model import Results
|
|
12
|
+
|
|
13
|
+
DISCLAIMER = (
|
|
14
|
+
"This report is generated automatically from automated test results. It is not legal "
|
|
15
|
+
"or compliance advice, and it does not certify compliance with any law, regulation or "
|
|
16
|
+
"standard. Consult qualified professionals before relying on it."
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
FORMATS = ("md", "html")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ReportError(Exception):
|
|
23
|
+
"""Results file could not be read or rendered."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def load_results(path: Path) -> Results:
|
|
27
|
+
try:
|
|
28
|
+
raw = path.read_text(encoding="utf-8")
|
|
29
|
+
except OSError as e:
|
|
30
|
+
raise ReportError(f"cannot read {path}: {e.strerror or e}") from e
|
|
31
|
+
try:
|
|
32
|
+
return Results.model_validate(json.loads(raw))
|
|
33
|
+
except json.JSONDecodeError as e:
|
|
34
|
+
raise ReportError(f"{path} is not valid JSON: {e}") from e
|
|
35
|
+
except ValidationError as e:
|
|
36
|
+
raise ReportError(f"{path} is not a valid results file:\n{e}") from e
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _env(fmt: str) -> Environment:
|
|
40
|
+
return Environment(
|
|
41
|
+
loader=FileSystemLoader(Path(__file__).parent / "templates"),
|
|
42
|
+
autoescape=fmt == "html",
|
|
43
|
+
trim_blocks=True,
|
|
44
|
+
lstrip_blocks=True,
|
|
45
|
+
keep_trailing_newline=True,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def render(results: Results, fmt: str = "md") -> str:
|
|
50
|
+
if fmt not in FORMATS:
|
|
51
|
+
raise ReportError(f"unknown format {fmt!r}; choose one of: {', '.join(FORMATS)}")
|
|
52
|
+
return _env(fmt).get_template(f"report.{fmt}.j2").render(r=results, disclaimer=DISCLAIMER)
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Vesk Verify report</title>
|
|
6
|
+
<style>
|
|
7
|
+
body{font-family:sans-serif;max-width:60rem;margin:2rem auto;padding:0 1rem}
|
|
8
|
+
table{border-collapse:collapse;width:100%}td,th{border:1px solid #ccc;padding:.3rem .5rem;text-align:left}
|
|
9
|
+
.pass{color:#0a6b2d}.fail,.error{color:#b00020}
|
|
10
|
+
.disclaimer{border:1px solid #c90;background:#fff8e1;padding:.6rem}
|
|
11
|
+
pre{white-space:pre-wrap}
|
|
12
|
+
</style>
|
|
13
|
+
</head>
|
|
14
|
+
<body>
|
|
15
|
+
<h1>Vesk Verify report</h1>
|
|
16
|
+
<p class="disclaimer"><strong>Disclaimer:</strong> {{ disclaimer }}</p>
|
|
17
|
+
<ul>
|
|
18
|
+
<li>Target: {{ r.target or "n/a" }}</li>
|
|
19
|
+
<li>Model: {{ r.model or "n/a" }}</li>
|
|
20
|
+
<li>Run: {{ r.started_at or "n/a" }} to {{ r.finished_at or "n/a" }}</li>
|
|
21
|
+
<li>Vesk version: {{ r.vesk_version or "n/a" }}</li>
|
|
22
|
+
</ul>
|
|
23
|
+
<h2>Summary</h2>
|
|
24
|
+
<p>{{ r.count("pass") }} passed, {{ r.count("fail") }} failed, {{ r.count("error") }} errored ({{ r.probes|length }} probes).</p>
|
|
25
|
+
<table>
|
|
26
|
+
<tr><th>Probe</th><th>Category</th><th>Status</th><th>Failed cases</th></tr>
|
|
27
|
+
{% for p in r.probes %}
|
|
28
|
+
<tr><td>{{ p.name or p.id }}</td><td>{{ p.category }}</td><td class="{{ p.status }}">{{ p.status|upper }}</td><td>{{ p.failed_cases|length }}/{{ p.cases|length }}</td></tr>
|
|
29
|
+
{% endfor %}
|
|
30
|
+
</table>
|
|
31
|
+
<h2>Details</h2>
|
|
32
|
+
{% for p in r.probes if p.status != "pass" %}
|
|
33
|
+
<h3>{{ p.name or p.id }} (<span class="{{ p.status }}">{{ p.status|upper }}</span>)</h3>
|
|
34
|
+
{% if p.description %}<p>{{ p.description }}</p>{% endif %}
|
|
35
|
+
{% if p.error %}<p>Error: {{ p.error }}</p>{% endif %}
|
|
36
|
+
{% for c in p.failed_cases %}
|
|
37
|
+
<h4>Case {{ c.id }}</h4>
|
|
38
|
+
<p>Prompt:</p><pre>{{ c.prompt }}</pre>
|
|
39
|
+
<p>Response:</p><pre>{{ c.response }}</pre>
|
|
40
|
+
{% if c.evidence %}<p>Evidence: {{ c.evidence }}</p>{% endif %}
|
|
41
|
+
{% endfor %}
|
|
42
|
+
{% else %}
|
|
43
|
+
<p>No failures or errors.</p>
|
|
44
|
+
{% endfor %}
|
|
45
|
+
<hr>
|
|
46
|
+
<p class="disclaimer">{{ disclaimer }}</p>
|
|
47
|
+
</body>
|
|
48
|
+
</html>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Vesk Verify report
|
|
2
|
+
|
|
3
|
+
> **Disclaimer:** {{ disclaimer }}
|
|
4
|
+
|
|
5
|
+
- Target: {{ r.target or "n/a" }}
|
|
6
|
+
- Model: {{ r.model or "n/a" }}
|
|
7
|
+
- Run: {{ r.started_at or "n/a" }} to {{ r.finished_at or "n/a" }}
|
|
8
|
+
- Vesk version: {{ r.vesk_version or "n/a" }}
|
|
9
|
+
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
{{ r.count("pass") }} passed, {{ r.count("fail") }} failed, {{ r.count("error") }} errored ({{ r.probes|length }} probes).
|
|
13
|
+
|
|
14
|
+
| Probe | Category | Status | Failed cases |
|
|
15
|
+
|---|---|---|---|
|
|
16
|
+
{% for p in r.probes %}
|
|
17
|
+
| {{ (p.name or p.id)|replace("|", "\\|") }} | {{ p.category|replace("|", "\\|") }} | {{ p.status|upper }} | {{ p.failed_cases|length }}/{{ p.cases|length }} |
|
|
18
|
+
{% endfor %}
|
|
19
|
+
|
|
20
|
+
## Details
|
|
21
|
+
{% for p in r.probes if p.status != "pass" %}
|
|
22
|
+
|
|
23
|
+
### {{ p.name or p.id }} ({{ p.status|upper }})
|
|
24
|
+
{% if p.description %}
|
|
25
|
+
|
|
26
|
+
{{ p.description }}
|
|
27
|
+
{% endif %}
|
|
28
|
+
{% if p.error %}
|
|
29
|
+
|
|
30
|
+
Error: {{ p.error }}
|
|
31
|
+
{% endif %}
|
|
32
|
+
{% for c in p.failed_cases %}
|
|
33
|
+
|
|
34
|
+
- Case `{{ c.id }}`
|
|
35
|
+
- Prompt: `{{ c.prompt|replace("`", "'")|replace("\n", " ") }}`
|
|
36
|
+
- Response: `{{ c.response|replace("`", "'")|replace("\n", " ") }}`
|
|
37
|
+
{% if c.evidence %}
|
|
38
|
+
- Evidence: {{ c.evidence|replace("\n", " ") }}
|
|
39
|
+
{% endif %}
|
|
40
|
+
{% endfor %}
|
|
41
|
+
{% else %}
|
|
42
|
+
|
|
43
|
+
No failures or errors.
|
|
44
|
+
{% endfor %}
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
{{ disclaimer }}
|
vesk/runner.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Runs probes against a chat client and builds the results consumed by `vesk report`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from vesk import __version__
|
|
12
|
+
from vesk.client import ChatClient, EndpointError
|
|
13
|
+
from vesk.config import SEVERITY_RANK, Config, ConfigError
|
|
14
|
+
from vesk.probes import REGISTRY, Probe
|
|
15
|
+
|
|
16
|
+
RESPONSE_LIMIT = 500
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CaseResult(BaseModel):
|
|
20
|
+
id: str
|
|
21
|
+
passed: bool
|
|
22
|
+
prompt: str = ""
|
|
23
|
+
response: str = ""
|
|
24
|
+
evidence: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProbeRun(BaseModel):
|
|
28
|
+
id: str
|
|
29
|
+
name: str
|
|
30
|
+
category: str
|
|
31
|
+
severity: str
|
|
32
|
+
status: str # pass | fail | error
|
|
33
|
+
description: str = ""
|
|
34
|
+
error: str = ""
|
|
35
|
+
cases: list[CaseResult] = Field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ScanResults(BaseModel):
|
|
39
|
+
schema_version: int = 1
|
|
40
|
+
vesk_version: str = __version__
|
|
41
|
+
target: str
|
|
42
|
+
model: str
|
|
43
|
+
system_name: str = ""
|
|
44
|
+
started_at: str
|
|
45
|
+
finished_at: str
|
|
46
|
+
probes: list[ProbeRun]
|
|
47
|
+
|
|
48
|
+
def exit_code(self, fail_on: str) -> int:
|
|
49
|
+
"""3 = incomplete scan (endpoint errors), 1 = findings at/above fail_on, else 0."""
|
|
50
|
+
if any(p.error for p in self.probes):
|
|
51
|
+
return 3
|
|
52
|
+
if fail_on == "none":
|
|
53
|
+
return 0
|
|
54
|
+
floor = SEVERITY_RANK[fail_on]
|
|
55
|
+
failing = (p for p in self.probes if p.status == "fail")
|
|
56
|
+
return 1 if any(SEVERITY_RANK[p.severity] >= floor for p in failing) else 0
|
|
57
|
+
|
|
58
|
+
def write(self, path: Path) -> None:
|
|
59
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
path.write_text(self.model_dump_json(indent=2) + "\n", encoding="utf-8")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def select_probes(cfg: Config, override: list[str] | None) -> list[Probe]:
|
|
64
|
+
names = override or cfg.scan.probes or list(REGISTRY)
|
|
65
|
+
unknown = [n for n in names if n not in REGISTRY]
|
|
66
|
+
if unknown:
|
|
67
|
+
raise ConfigError(
|
|
68
|
+
f"unknown probe(s): {', '.join(unknown)}. Available: {', '.join(REGISTRY)}"
|
|
69
|
+
)
|
|
70
|
+
return [REGISTRY[n]() for n in dict.fromkeys(names)]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _now() -> str:
|
|
74
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _excerpt(text: str) -> str:
|
|
78
|
+
return text if len(text) <= RESPONSE_LIMIT else text[:RESPONSE_LIMIT] + "…"
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _run_probe(probe: Probe, cfg: Config, client: ChatClient, sem: asyncio.Semaphore):
|
|
82
|
+
cases = probe.cases(cfg.target.system_prompt, cfg.scan.samples_per_probe, cfg.scan.seed)
|
|
83
|
+
errors: list[str] = []
|
|
84
|
+
|
|
85
|
+
async def one(case) -> CaseResult | None:
|
|
86
|
+
async with sem:
|
|
87
|
+
try:
|
|
88
|
+
response = await client.complete(case.messages)
|
|
89
|
+
except EndpointError as e:
|
|
90
|
+
errors.append(str(e))
|
|
91
|
+
return None
|
|
92
|
+
verdict = probe.evaluate(case, response)
|
|
93
|
+
return CaseResult(
|
|
94
|
+
id=case.id,
|
|
95
|
+
passed=not verdict.failed,
|
|
96
|
+
prompt=probe.redact(case, case.prompt),
|
|
97
|
+
response=_excerpt(probe.redact(case, response)),
|
|
98
|
+
evidence=verdict.evidence,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
results = [r for r in await asyncio.gather(*(one(c) for c in cases)) if r is not None]
|
|
102
|
+
if not results:
|
|
103
|
+
status = "error"
|
|
104
|
+
else:
|
|
105
|
+
status = "fail" if any(not r.passed for r in results) else "pass"
|
|
106
|
+
error = ""
|
|
107
|
+
if errors:
|
|
108
|
+
error = f"{len(errors)} of {len(cases)} requests failed: {errors[0]}"
|
|
109
|
+
return ProbeRun(
|
|
110
|
+
id=probe.name,
|
|
111
|
+
name=probe.name.replace("_", " ").title(),
|
|
112
|
+
category=probe.category,
|
|
113
|
+
severity=probe.severity,
|
|
114
|
+
status=status,
|
|
115
|
+
description=probe.description,
|
|
116
|
+
error=error,
|
|
117
|
+
cases=results,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def run_scan(
|
|
122
|
+
cfg: Config, probes: list[Probe], client: ChatClient
|
|
123
|
+
) -> ScanResults:
|
|
124
|
+
started = _now()
|
|
125
|
+
sem = asyncio.Semaphore(cfg.scan.max_concurrency)
|
|
126
|
+
runs = [await _run_probe(p, cfg, client, sem) for p in probes]
|
|
127
|
+
return ScanResults(
|
|
128
|
+
target=cfg.target.base_url,
|
|
129
|
+
model=cfg.target.model,
|
|
130
|
+
system_name=cfg.report.system_name,
|
|
131
|
+
started_at=started,
|
|
132
|
+
finished_at=_now(),
|
|
133
|
+
probes=runs,
|
|
134
|
+
)
|
vesk/scaffold.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Starter config written by `vesk init`."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
STARTER_CONFIG = """\
|
|
6
|
+
# Vesk Verify configuration. Docs: https://github.com/vesk-ai/vesk-cli
|
|
7
|
+
version: 1
|
|
8
|
+
|
|
9
|
+
target:
|
|
10
|
+
# Any OpenAI-compatible chat completions endpoint.
|
|
11
|
+
base_url: https://api.example.com/v1
|
|
12
|
+
model: my-model
|
|
13
|
+
# Name of the environment variable holding the API key.
|
|
14
|
+
# The key itself must never be written in this file.
|
|
15
|
+
api_key_env: VESK_API_KEY
|
|
16
|
+
# Optional system prompt your application uses (helps injection/leak probes).
|
|
17
|
+
# system_prompt: "You are a support bot."
|
|
18
|
+
timeout_s: 30
|
|
19
|
+
max_retries: 2
|
|
20
|
+
|
|
21
|
+
scan:
|
|
22
|
+
# Probes to run. Available: prompt_injection, jailbreak, pii_leakage.
|
|
23
|
+
probes: [prompt_injection, jailbreak, pii_leakage]
|
|
24
|
+
samples_per_probe: 20
|
|
25
|
+
max_concurrency: 4
|
|
26
|
+
seed: 0
|
|
27
|
+
# Exit with code 1 if any finding is at or above this severity: none|low|medium|high
|
|
28
|
+
fail_on: high
|
|
29
|
+
|
|
30
|
+
report:
|
|
31
|
+
frameworks: [eu_ai_act, dpdp, nist_ai_rmf]
|
|
32
|
+
system_name: "My AI system"
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ConfigExistsError(FileExistsError):
|
|
37
|
+
"""Raised when the target file exists and overwrite was not requested."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def write_starter_config(path: Path, force: bool = False) -> Path:
|
|
41
|
+
"""Write the starter config to `path`, creating parent directories."""
|
|
42
|
+
if path.exists() and not force:
|
|
43
|
+
raise ConfigExistsError(f"{path} already exists (use --force to overwrite)")
|
|
44
|
+
if path.is_dir():
|
|
45
|
+
raise IsADirectoryError(f"{path} is a directory")
|
|
46
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
path.write_text(STARTER_CONFIG, encoding="utf-8")
|
|
48
|
+
return path
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vesk-cli
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Vesk Verify: test LLM endpoints and produce compliance-mapped reports.
|
|
5
|
+
Author: Vesk
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
License-File: NOTICE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: httpx>=0.27
|
|
18
|
+
Requires-Dist: jinja2>=3.1
|
|
19
|
+
Requires-Dist: pydantic>=2
|
|
20
|
+
Requires-Dist: pyyaml>=6
|
|
21
|
+
Requires-Dist: typer>=0.12
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: build; extra == 'dev'
|
|
24
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
27
|
+
Requires-Dist: respx; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
29
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# vesk
|
|
33
|
+
|
|
34
|
+
**Vesk Verify** tests an OpenAI-compatible LLM endpoint with prompt-injection, jailbreak and
|
|
35
|
+
PII-leakage probes, then renders a report mapped to EU AI Act, DPDP and NIST AI RMF.
|
|
36
|
+
|
|
37
|
+
> Status: alpha (0.1.x). Interfaces may change between minor versions.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
pip install vesk-cli
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Requires Python 3.10 or newer.
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
vesk init # write a commented vesk.yaml
|
|
51
|
+
export VESK_API_KEY=... # the env var named by target.api_key_env
|
|
52
|
+
vesk scan # run probes, write results.json
|
|
53
|
+
vesk report results.json -o report.md
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Commands
|
|
57
|
+
|
|
58
|
+
### `vesk init`
|
|
59
|
+
|
|
60
|
+
Writes a starter `vesk.yaml`.
|
|
61
|
+
|
|
62
|
+
- `--path PATH`: where to write it (default `vesk.yaml`).
|
|
63
|
+
- `--force`: overwrite an existing file.
|
|
64
|
+
|
|
65
|
+
### `vesk scan`
|
|
66
|
+
|
|
67
|
+
Runs the configured probes against the target and writes a results JSON file.
|
|
68
|
+
|
|
69
|
+
- `-c, --config PATH`: config file (default `vesk.yaml`).
|
|
70
|
+
- `--probe NAME`: run only this probe; repeatable.
|
|
71
|
+
- `--out PATH`: results file (default `results.json`).
|
|
72
|
+
|
|
73
|
+
Exit codes:
|
|
74
|
+
|
|
75
|
+
| Code | Meaning |
|
|
76
|
+
|------|---------|
|
|
77
|
+
| 0 | Scan completed; no finding at or above `scan.fail_on`. |
|
|
78
|
+
| 1 | At least one failing probe at or above `scan.fail_on`. |
|
|
79
|
+
| 2 | Configuration or file error. |
|
|
80
|
+
| 3 | Incomplete scan: a probe hit endpoint errors. |
|
|
81
|
+
|
|
82
|
+
This makes `vesk scan` usable as a CI gate.
|
|
83
|
+
|
|
84
|
+
### `vesk report`
|
|
85
|
+
|
|
86
|
+
Renders a report from a results file.
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
vesk report results.json --format html -o report.html
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- `--format md|html`: output format (default `md`).
|
|
93
|
+
- `-o, --output PATH`: write to a file instead of stdout.
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
`vesk init` writes a fully commented file. In short:
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
version: 1
|
|
101
|
+
target:
|
|
102
|
+
base_url: https://api.example.com/v1 # OpenAI-compatible chat completions API
|
|
103
|
+
model: my-model
|
|
104
|
+
api_key_env: VESK_API_KEY # name of the env var; never put the key in the file
|
|
105
|
+
# system_prompt: "You are a support bot."
|
|
106
|
+
timeout_s: 30
|
|
107
|
+
max_retries: 2
|
|
108
|
+
scan:
|
|
109
|
+
probes: [prompt_injection, jailbreak, pii_leakage]
|
|
110
|
+
samples_per_probe: 20
|
|
111
|
+
max_concurrency: 4
|
|
112
|
+
seed: 0
|
|
113
|
+
fail_on: high # none | low | medium | high
|
|
114
|
+
report:
|
|
115
|
+
frameworks: [eu_ai_act, dpdp, nist_ai_rmf]
|
|
116
|
+
system_name: "My AI system"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Probes
|
|
120
|
+
|
|
121
|
+
| Probe | Checks |
|
|
122
|
+
|-------|--------|
|
|
123
|
+
| `prompt_injection` | Whether text in the user turn or in quoted data overrides the instructions. |
|
|
124
|
+
| `jailbreak` | Whether persona and role-play framings make the model drop its rules. Probes ask only for a harmless marker, never for harmful content. |
|
|
125
|
+
| `pii_leakage` | Whether the model repeats synthetic personal data planted in its own context. Training-data regurgitation is out of scope for 0.1. |
|
|
126
|
+
|
|
127
|
+
## Privacy
|
|
128
|
+
|
|
129
|
+
The API key is read from an environment variable and is not stored in the config. Probe
|
|
130
|
+
requests go only to the endpoint you configure. PII probes use synthetic values, masked in
|
|
131
|
+
stored results.
|
|
132
|
+
|
|
133
|
+
## Not legal advice
|
|
134
|
+
|
|
135
|
+
Reports are generated automatically from test results. They are not legal or compliance advice
|
|
136
|
+
and do not certify compliance with any law, regulation or standard.
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
pip install -e ".[dev]"
|
|
142
|
+
pytest
|
|
143
|
+
ruff check .
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## License
|
|
147
|
+
|
|
148
|
+
Apache-2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
vesk/__init__.py,sha256=YPFawP1xoc0x_YlmM9OEuRhMw9NeZQtmeZFNd5sIpJ8,95
|
|
2
|
+
vesk/cli.py,sha256=1fPQLOv94vDUB0vmhQolG-88JfcdozkMqEU0C-ieEWM,3757
|
|
3
|
+
vesk/client.py,sha256=bgc2uDGVcOr12Y4n7A-DCgmpWC3r1XMH3kXSHrEWHFY,2700
|
|
4
|
+
vesk/config.py,sha256=ZxOGFErGjFNO-2_ZIorZOPV7ekk0YrZcKIbSiTAkb-8,2544
|
|
5
|
+
vesk/runner.py,sha256=dTxsg_QKa8lZcJ1oU4jrfRMDoDYN8GxJ0aETxajN8rc,4077
|
|
6
|
+
vesk/scaffold.py,sha256=djeLAEY72fKwh3mFvQzErqtckdo9S_rgwrIVN696CO4,1559
|
|
7
|
+
vesk/data/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
vesk/probes/__init__.py,sha256=Kn5etDCHFh56k77SszloaWxcyOzP1aAuIKt9zb5SRHU,426
|
|
9
|
+
vesk/probes/base.py,sha256=0twz3F9Gt6eFLSwksqTz5dvRGPwBN2OeG-NOCUDNvDA,2063
|
|
10
|
+
vesk/probes/jailbreak.py,sha256=dOVA5H-nGWM51S41oL7fRUXLMRma9CuNelcwJgjCQow,1735
|
|
11
|
+
vesk/probes/pii_leakage.py,sha256=_7qDS7am-zcav8_4uA-CGZSqPJ3mO02xQtr7MaXq--U,3146
|
|
12
|
+
vesk/probes/prompt_injection.py,sha256=IbDRd2FNzp2vCAcp-PNVnvWwYuPzXHJHowdGyk0-K8Y,1605
|
|
13
|
+
vesk/probes/data/.gitkeep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
vesk/probes/data/jailbreak.yaml,sha256=1lu0CcWewfoN7qUtB-1W3Amyn_oB0rMA2CpVA6XJF6s,1793
|
|
15
|
+
vesk/probes/data/pii_leakage.yaml,sha256=jlAZv1Nx4PfOKi_77EkuywsSeMX83KekA1G2qdJUBSg,1205
|
|
16
|
+
vesk/probes/data/prompt_injection.yaml,sha256=NBKvaGDUnGhmz3ofJUEymQu8DKHwTvjuvUZNz88nYeI,1685
|
|
17
|
+
vesk/report/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
18
|
+
vesk/report/model.py,sha256=7LbZdPI2Dh55LBWxYotBbD0Zml4xXeUA5Xd1mxGjuW4,959
|
|
19
|
+
vesk/report/render.py,sha256=0tuhLPmIbr8tglsD4FsBlhELcR5J7APNnog3BN-9Po0,1642
|
|
20
|
+
vesk/report/templates/report.html.j2,sha256=H1bbbLg4QWDn0DBTaCkwWssQllvUvVt3ytfUFI7Gerw,1825
|
|
21
|
+
vesk/report/templates/report.md.j2,sha256=9dT-9pahBwDIqkk7Xk13GvJWguQE6i5o00n4B8UPhOo,1201
|
|
22
|
+
vesk_cli-0.1.2.dist-info/METADATA,sha256=izqBoazvJj2xBb3UzFUz-HCZEB-OUCjNTg2cDfVZucA,4233
|
|
23
|
+
vesk_cli-0.1.2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
24
|
+
vesk_cli-0.1.2.dist-info/entry_points.txt,sha256=0Tf_RxRyGYLVCZFhshQ8b1MV30xqSPFaE3f4EXjE-HE,38
|
|
25
|
+
vesk_cli-0.1.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
26
|
+
vesk_cli-0.1.2.dist-info/licenses/NOTICE,sha256=rfzyf7DuE9d7-OA76HB_sIxIFAPLr3D3DNE1rHLsoQU,138
|
|
27
|
+
vesk_cli-0.1.2.dist-info/RECORD,,
|