Vozern Attest — AI development governance: inventory AI agents/MCP servers and unmanaged developer software, evaluate against an approved catalogue and advisories, emit signed audit-grade evidence
Project description
Attest, by Vozern — workstation unmanaged-software & AI tooling attestation
Vozern is the company (vozern.com); Attest is the product. The Python distribution is published as
vozern(pipx install vozern); the command and import package areattest.
Inventories software installed outside centrally-managed channels on a macOS workstation — Homebrew, global npm, pipx, VS Code extensions, AI tools and MCP servers — matches it against known advisories via OSV.dev, evaluates it against an approved AI-tool catalogue, and produces an audit-grade attestation report mapped to selectable control frameworks (NIST SP 800-53, CIS Controls v8, organization policy, or your own internal control set).
The report is the product. The scanner exists to generate a credible one.
No telemetry, ever. attest makes exactly two kinds of network calls, both
explicit and both optional: the OSV.dev advisory lookup (--no-osv disables)
and the VS Code Marketplace publisher-trust lookup (--no-trust disables).
Nothing about your machine leaves it; reports are written to local files only.
Run it fully offline and it still produces a valid inventory attestation.
(Connecting to the Attest control plane — see PLATFORM.md — is explicit,
opt-in, and sends signed control evidence only: outcomes, counts, and hashes,
never the inventory itself.)
Run it
Zero-install (bare Python 3.10+, nothing to pip install):
python3 attest.py # scan this machine + match advisories
python3 attest.py --open # ...and open the report
python3 attest.py --demo # render a sample report (illustrative data)
python3 attest.py --no-osv # inventory only, fully offline
Or install it:
pipx install . # console command: attest
attest --demo
attest --version
Output formats
attest --format html # default; print-clean, self-contained
attest --format json --format sarif # repeatable
attest --format pdf # true A4 PDF (optional extra, below)
attest -o quarterly-scan --format html --format sarif
# -> quarterly-scan.html, quarterly-scan.sarif
- HTML — the human artefact: self-contained, no external assets, prints to a filable A4 document.
- JSON — documented shape below, for arbitrary downstream tooling.
- SARIF 2.1.0 — validates against the official schema; loads into GitHub
code scanning and standard SARIF viewers. Accepted risks map to native
suppressions. - PDF — a real A4 PDF rendered from the same HTML:
pip install 'attest[pdf]'(needsbrew install pango; if the libraries aren't found at run time, prefix withDYLD_FALLBACK_LIBRARY_PATH="$(brew --prefix)/lib").
Control frameworks (--framework, repeatable)
attest --framework nist --framework cis # the default pair
attest --framework internal # generic internal-control template
Frameworks are data, not code: each is a YAML file
(attest/frameworks/*.yaml) defining controls — id, name, assertion, an
evidence template over a fixed metric namespace, and declarative status rules
(first match wins). Copy internal.yaml, rename the ids to your control
register, and the report speaks your auditor's language. Files load with
PyYAML when installed, otherwise through a vendored strict-subset parser, so
the zero-install path keeps working.
Accepted risk (exceptions.yaml)
A finding you have consciously accepted moves out of "attention" and into a
documented accepted-risk section — visible justification, owner, optional
expiry. Place exceptions.yaml next to where you run attest (or pass
--exceptions PATH):
exceptions:
- finding: "npm-global:lodash:GHSA-jf85-cpcp-j695" # channel:package:advisory
justification: "Dev-only tool; not exposed to untrusted input."
owner: "name@example.com"
expires: "2026-12-31" # optional; past expiry it reverts to open
Expired acceptances reopen with a visible ACCEPTANCE EXPIRED tag. In SARIF,
accepted findings carry a suppression with the justification.
What it checks — and how honestly
| Channel | Inventoried | Advisory matching | Confidence label |
|---|---|---|---|
| global npm | yes | OSV npm, native |
exact |
| pipx | yes | OSV PyPI, native |
exact |
| Homebrew formulae | yes | curated upstream map, else name-based heuristic | high / low |
| Homebrew casks | yes | never matched (names too generic to be honest) | — |
| VS Code extensions | yes | no feed exists; trust signal instead | — |
| AI tools (CLIs + apps) | yes | no feed exists; approval catalogue via --policy |
— |
| MCP servers | yes | no feed exists; approval catalogue via --policy |
— |
AI tools & MCP servers are discovered from the client configurations that
Claude Code/Desktop, Cursor, Windsurf, VS Code, Codex, and Gemini CLI actually
read, plus known AI binaries and macOS app bundles. With
--policy policy.yaml (see policy.example.yaml), anything outside the
organization's approved catalogue becomes an ATTEST-UNAPPROVED finding that
flows through findings, SARIF, exceptions, and --fail-on like any advisory.
Homebrew has no OSV ecosystem, so matching is best-effort by design: a
curated map (attest/data/brew-map.yaml) ties well-known formulae to advisory
sources that track upstream version numbers (Bitnami, Go modules, PyPI,
crates.io, npm) at high confidence; unmapped formulae fall back to a
name-only lookup filtered to those same upstream-semantics ecosystems at
low confidence, labelled "verify before acting" in the report. Distro
feeds (Ubuntu/Alpine/Debian/...) are never accepted — their version ranges
describe distro builds, not your Homebrew install. Formulae with no upstream
feed (openssl, curl, ...) are shown as unmatched, not as clean. Control it
with --brew-match off|curated|full (default full; the heuristic pass
queries OSV once per unmapped formula, so curated is much faster on
formula-heavy machines).
VS Code extensions carry a trust signal: marketplace publisher verification, staleness (>2 years without an update), and a curated offline denylist of publicly documented malicious extensions — a denylist hit is a CRITICAL finding.
CI gating
attest --fail-on critical # exit 2 if any OPEN critical advisory exists
attest --fail-on high # critical or high
Off by default; accepted risks never trip the gate.
JSON shape (attest-report/1)
{
"schema": "attest-report/1",
"tool": {"name": "attest", "version": "1.0.0"},
"demo": false,
"context": {"hostname", "user", "platform", "scanned_at"},
"match_meta": {"osv_available", "queryable", "unqueryable", "error",
"pages_fetched", "truncated", "cache": {"hits", "misses"}},
"trust_meta": {"checked", "available", "error"},
"summary": { /* the full metric namespace: components, channels,
open_advisories, accepted_advisories, critical_open, ... */ },
"controls": [{"id", "framework", "name", "status", "assertion", "evidence"}],
"records": [{
"channel": "npm-global", "ecosystem": "npm",
"name": "lodash", "version": "4.17.4",
"coverage": "native", // native|curated|heuristic|none
"vulns": [{
"id": "GHSA-...", "cve": "CVE-...", "severity": "HIGH",
"summary": "...", "url": "...",
"confidence": "exact", // exact|high|low
"accepted": {"justification", "owner", "expires", "expired"} // optional
}],
"trust": { /* extensions only: available, verified, last_updated,
stale, denylisted, denylist_reason */ }
}]
}
Additive changes keep /1; breaking changes bump it.
Publish to a control plane (opt-in)
The platform layer (see PLATFORM.md). A pilot control plane ships in the
package — stdlib only, no extra dependencies:
python3 -m attest.server --enroll-token SECRET # admin box; dashboard on :8788
attest --enroll http://plane.example:8788 --token SECRET # once per device
attest --publish --disclosure outcomes # after any scan
What publishes is a signed attest-attestation/1 envelope — control
outcomes, an evidence hash, and (at higher disclosure tiers) counts or
failing items. Never the inventory. Ed25519 signing needs the optional
extra: pip install 'attest[publish]'; without it envelopes publish
unsigned and the server marks them unverified. The dashboard shows fleet
control posture, device freshness, the unapproved-AI-tooling approval queue,
the accepted-risk register with expiry alerts, and a downloadable evidence
pack. Enrolled devices fetch their group's approved-tool catalogue from the
plane automatically at scan time. Enterprise rollout (SSO proxy, MDM,
scheduling): see DEPLOY.md.
Other flags
--input inv.json (render from a saved inventory) · --save-inventory PATH ·
--no-cache / --refresh (the OSV hydration cache lives in
~/.cache/attest/, TTL 7 days) · --open (macOS).
Everything degrades gracefully offline: network failure annotates the report, it never crashes a run.
Development
pip install -e '.[dev]'
pytest # the suite is fully offline
ATTEST_UPDATE_GOLDEN=1 pytest tests/test_export.py # regenerate HTML snapshot
Deliberately out of scope: fleet/multi-machine, agents/daemons, dashboards, auto-remediation, policy enforcement, auth, Windows/Linux.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vozern-1.0.0.tar.gz.
File metadata
- Download URL: vozern-1.0.0.tar.gz
- Upload date:
- Size: 81.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
370fd49fa53bb1b3b8dc3990bfcf8f6554cd5e62f54b73b1a27f905f48717933
|
|
| MD5 |
48918094c824a4acdf2a4959b7608bc0
|
|
| BLAKE2b-256 |
32b3d8724834225f03574b34cc533b0d0c5eccddefa7717026e4a8cb90400b9d
|
File details
Details for the file vozern-1.0.0-py3-none-any.whl.
File metadata
- Download URL: vozern-1.0.0-py3-none-any.whl
- Upload date:
- Size: 69.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a531aa057378b5dcd332e49974496b0c19f18f75573f3eb746cad14ff8ce44f0
|
|
| MD5 |
bc634fe0cb214eaf10c2e284b14bdf9e
|
|
| BLAKE2b-256 |
d6fc814382470ff8a9fdb2ab65689cec908398d4909794c036ecf288efa045e8
|