OpenVet 0.6.0 is out. I managed to sneak some breaking changes into the
data structures again. Two important commands were included in this release:
openvet info allows you to query audit data about the dependencies in your
project, and openvet guard is my first attempt at integrating the OpenVet
checks into existing developer flows.
I also took the time to audit every single dependency of OpenVet with OpenVet itself, some dogfooding that allows me to test how this all fits together.
The rest of this post walks through the changes that matter from a user’s
perspective: the guard wrapper, the dependency lookup, the new per-dependency
narrative on audits, the new --project flag for root auto-discovery, and the
wire-format breaks to be aware of if you have audits authored against 0.5.0.
All direct dependencies of OpenVet are audited #
The 0.5.0 release post ended with 111 audits in the OpenVet log. In 0.6.0 that
grew to 572, covering every direct dependency the OpenVet workspace pulls in,
with the project’s own openvet.toml requirements satisfied end-to-end.
Running openvet check against the workspace now passes cleanly:
$ openvet check
[..]
Summary: 572 passed, 0 failed, 0 unaudited, 14 skipped
This is the first project that has a full audited-dependencies workflow. All audits are hosted in my log at openvet.org/xfbs. You can take a look at them if you are curious. Note that most of them are generated with the help of an LLM: My goal here was not to produce the most perfect audits, but to exercise every part of the data structures and to test the flow end-to-end. For some heavy dependencies, I only did a light audit, which did not evaluate all claims. And I have had to add some overrides. This models a reasonable real-world usage of OpenVet.
Notably, many of the design decisions (content-addressing, caching, compression) seem to be working well. Once you’ve populated the cache on the first run, checking takes only a couple hundred milliseconds:
$ time openvet check
[..]
openvet check 0.10s user 0.02s system 84% cpu 0.139 total
Running it with an empty cache is reasonable. This will do about ~600 network requests. It is fast because the OpenVet Registry has a CDN, will transparently serve pre-compressed objects, and network requests are issued in parallel.
$ rm ~/.cache/openvet/cache.db
$ openvet check
[..]
openvet check 0.44s user 0.34s system 20% cpu 3.752 total
The cache size is reasonable:
$ ls -lah ~/.cache/openvet/cache.db
-rw-r--r-- 1 patrick patrick 6.6M May 31 14:49 /home/patrick/.cache/openvet/cache.db
There is currently no garbage collection for the cache. If the cache grows too large, you would need to just delete it. That is something that is on my roadmap. But given that the size of it is in the megabytes, I am not too worried about that yet.
openvet guard — block a build until policy passes
#
OpenVet is not useful if it’s only advisory: the way that OpenVet becomes
useful is by integrating it into developer flows, so that it blocks you from
executing code that does not match your requirements. Doing that is not
trivial, and there are different ways that I am exploring. What I built into
this release is openvet guard, which is intended to be used as a kind of
proxy for running commands. It takes a command that you want to run, runs
openvet check quietly, only executing the command if the check passes. The
intended use is a shell alias:
alias cargo='openvet guard cargo'
With that in place, every cargo build, cargo test, cargo run in the
project first checks that all dependencies match the project’s openvet.toml
requirements, and only proceeds when they do. Forget to audit a new dependency
you just added? cargo build won’t run. Add a dependency whose existing audit
contradicts a requirement? Same.
On block, you get the same per-subject failure trees that openvet check prints, followed by an error summary so the reason is
unmistakable:
$ cargo build
✘ cargo:some-new-crate@0.3.0
└─ requires: is-benign
└─ no audit asserts is-benign
error: 1 subject(s) did not pass policy (run `openvet check` for details)
On pass, guard is silent, so the wrapped command’s output starts immediately.
The exec is in-place, so signals and exit codes flow through unchanged.
Hyphenated arguments after the command are forwarded verbatim (openvet guard cargo build --release --features foo works as expected).
Outside an OpenVet project, guard’s behaviour is configurable via a new
[guard] section in the user config. The default is strict = false, which
runs the wrapped command transparently — so aliasing cargo globally doesn’t
break anything in scratch directories. Flip to strict = true and guard
refuses to run the wrapped command anywhere that isn’t an OpenVet project,
except for paths you’ve explicitly excluded:
openvet config guard.strict --set true
openvet config guard.exclude --set "~/Projects/alacritty,~/scratch"
Unix-only for now.
openvet info — a man-page for a project dependency
#
openvet info <registry>:<package>[@<version>] finds every matching
subject in the project’s lockfiles and renders each matching audit as
a flat, reflowed document. The default view is short — claims and
summary — with longer sections opt-in via flags:
-r,--reportlong-form report-f,--findingsfindings (with--severity/--classto triage)-n,--annotationssource annotations-d,--depsdeclared direct dependencies (with the new per-dependency narratives, see below)-u,--usersreverse dependencies (which of your audited packages declare this one as a dep)-a,--alleverything at once
Short examples:
openvet info cargo:serde # claims + summary, paged
openvet info cargo:serde@1.0.210 # pin a version
openvet info -rfd cargo:serde # report, findings, deps
openvet info -a cargo:serde # the whole picture
openvet info cargo:serde --severity high # only high-severity findings
openvet info -u cargo:serde # who in my project uses it
The reverse-dependency mode (-u) is probably the most interesting feature.
If you have a dependency that has a soundness or safety issue, or some security
findings, you need to make a decision: is this dependency okay to use? There
are cases where dependencies have an issue that only surfaces if you use a
specific API, or if you use them on a specific platform.
Let’s say you want to check package frobnication. With the reverse-dependency mode, OpenVet will scan all of the other dependencies in your project to see if any of those use frobnication, and surface that to you, with a description of how the frobnication package is used. This should give you some data to help you decide if it is safe to use frobnication, even if it has a known issue.
Here’s an example of what the output looks like, this is the output of openvet info -u cargo:sha1:
cargo:sha1@0.11.0#sha256:aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214
Audit 57cbe92225e8 (from xfbs)
crypto-impl-correct=true, crypto-impl-safe=true, crypto-impl-tested=true, has-binaries=false, has-build-exec=false, has-fuzz-tests=false, has-install-exec=false, has-integration-tests=true, has-property-tests=false, has-unit-tests=false, impl-algorithm=false, impl-concurrency=false, impl-crypto=true, impl-datastructure=false, impl-interpreter=false, impl-jit=false, impl-parser=false, impl-protocol=false, is-benign=true, unsafe-documented=true, unsafe-minimal=true, unsafe-safe=true, unsafe-tested=true, uses-concurrency=false, uses-crypto=false, uses-environment=false, uses-exec=false, uses-filesystem=false, uses-interpreter=false, uses-jit=false, uses-network=false, uses-unsafe=true
sha1 0.11.0 is the RustCrypto SHA-1 (FIPS 180-4) implementation with four backends (portable soft, x86 SHA-NI, ARMv8 SHA1, LoongArch64 asm); the source matches upstream, unsafe is minimal and properly gated, and KAT vectors plus a 16 MiB feed test exercise all backends. One high-severity finding: SHA-1 itself is cryptographically broken — unsuitable for any new collision-sensitive use; the crate exists for legacy interop.
Reverse Dependencies
cargo:axum@0.8.9 -> cargo:sha1@0.10
Optional (ws feature). SHA-1 digest used to compute the Sec-WebSocket-Accept handshake header value as specified by RFC 6455. Not used for security, only for protocol compliance.
cargo:rsa@0.9.10 -> cargo:sha1@0.10.5
Optional dependency, used as a hash function for PKCS#1 v1.5 and OAEP operations in examples and tests. Not included in the default feature set.
cargo:ssh-key@0.6.7 -> cargo:sha1@0.10
Optional dependency, gated behind the sha1 feature. Provides SHA-1 used by the legacy Md5/Sha1 fingerprint formats and by the known_hosts HMAC-SHA1 host-name hashing scheme.
cargo:zip@8.6.0 -> cargo:sha1@0.11
Optional dependency behind the aes-crypto feature. Provides the SHA-1 hash function as the primitive for PBKDF2 and HMAC in the WinZip AES key derivation and authentication scheme.
Output paginates through a git-style pager (OPENVET_PAGER, then $PAGER,
then less -FRX) when stdout is a terminal. --no-pager turns it off.
Because the reverse-dependencies mode needs to walk all audits for all dependencies in your project, there is a slight delay (~100ms) when using that mode. I am considering using caching to make this faster, but for now I am okay with the slight inefficiency.
Per-direct-dependency narratives on audits #
Closely tied to info -u above. Audits now carry an Audit.dependencies field
with the auditor’s prose answer to why does this package need this dep and
how does it use it for each declared direct dependency. The audit body is
what reaches the consumer alongside the claims and findings. So a downstream
consumer triaging a disclosure against dep X can read the entries on his
audited packages that depend on X to judge whether the weakness is reachable
in his composition.
The authoring side is automated where it can be:
-
openvet audit newauto-seeds the list from a newopenvet-packageextractor — one entry per resolved dependency with an empty description for the auditor to fill in. The full extractor output (including the entries it skipped, like git URLs and path deps) is written todependencies.jsonnext toaudit.pbas an auditor-local sidecar, never signed, never published. -
A new
openvet audit dependencysubcommand manages the list at authoring time, letting you list, show, edit dependency metadata in audits.
The linter enforces a non-empty description on every entry, and warns inside a workspace when the extractor sees a dep the audit doesn’t carry. The server’s publish handler runs the same description check at publish time, so an audit with empty descriptions cannot enter a log even by bypassing the CLI.
The extractor itself is a meaningful addition on its own: every shipping
registry adapter (cargo, npm, pypi, rubygems, go) now parses its native
manifest and surfaces direct dependencies with their registry, package,
requirement, scope (runtime vs build), and an optional flag.
VCS provenance on audits #
Sibling to the dependency narratives, audits also gained an optional
Audit.vcs field that records where the audited package’s source is hosted in
version control: clone URL, optional commit hash, optional subdirectory for
monorepo packages. Registry-published VCS metadata is sometimes missing,
sometimes wrong, and sometimes unreachable; this field captures what actually
worked so a downstream consumer can navigate to the same source the auditor
reviewed.
openvet audit new records what cloned successfully. It has some retry and
fixup logic, rewriting some git:// and ssh:// URLs to https://. Many
registries don’t validate the git URLs, and some of them are broken. We fix
them on a best-effort basis, but sometimes it needs manual work to discover the
proper VCS URL.
A new openvet audit vcs subcommand inspects, sets, or clears the field.
--commit accepts only 40-character SHA-1 or 64-character SHA-256 hex strings:
tag names and branch names are rejected, because a tag-pinned audit can
silently start covering different source if someone moves the tag.
openvet audit check warns when a recorded provenance has no commit. A Git URL
without a pinned revision means the repo HEAD could move under the audit.
Project root auto-discovery #
OpenVet now discovers the project root by walking up the filesystem and
locating the openvet.toml file, similar to how git and cargo operate.
Previously, commands only worked in the root folder of projects, and I had
separate --config and --lockfile flags. Not because that is the right way
to build it, but I needed a way to override these for unit tests. I’ve now
done it properly, with either the parent-traversal for automatic discovery, or
a --project flag that lets you point at the project root. The
OPENVET_PROJECT environment variable can also be used to override this.
Other things worth a quick mention #
-
openvet checkruns ~3.5× faster on a cold cache. Per-subject audit lookups and the up-front head prefetch now fan out across logs and subjects with bounded 16-way concurrency. Output order is preserved, so the listing stays diff-stable across runs. -
openvet checkexplains not-asserted requirements. When a requirement neither passes nor is contradicted because the claims it rests on were never asserted,checknow prints the same per-audit expression tree it already shows for contradictions. The unevaluated claim is visible in the tree rather than collapsed to a barenot assertedline. -
openvet update <log>accepts a single log name. Refreshes only the named log’s pin and leaves every other entry inopenvet.lockuntouched. The bareopenvet updatestill refreshes every configured log. -
File annotations carry a per-file content hash. A new required
FileTarget.content_hashfield (32-byte BLAKE2b-256) binds each file annotation’s body and per-line ranges to the exact byte sequence the auditor reviewed at that path. The audit’ssubject.hashalready commits to the archive as a whole, but per-annotation hashes let a consumer detect (andopenvet audit checkflag) annotations whose target file has drifted under them without re-deriving the manifest. -
Audit subject binding is now enforced at publish. A new spec invariant requires that
InsertAuditandUpdateAuditoperations carry the samesubjectas the embedded audit’saudit.subject. This closes a hole where a hostile client could put an audit for one subject under a different tree key, misleading any consumer querying for that key. -
Commit timestamp monotonicity and admission window. Every published commit’s
timestampmust be greater than or equal to its predecessor’s, and the server rejects commits whosetimestampis more than 15 minutes away from its wall clock. Publishers whose system clock is badly off will see a400 Bad Request, fix by running NTP. This makes KeyState revocation more easily verifiable: monotonic time means commits cannot be backdated, which allows key expiry to work correctly.
Wire-format breaks #
0.6.0 is another breaking pre-1.0 release. If you have audits authored against 0.5.0, the things to know:
-
FileTarget.content_hashis now required. Audits produced by earlier releases (where file annotations had nocontent_hash) fail to decode. The path forward is to re-runopenvet audit annotate <path>over each file annotation, which reads the manifest, validates against the on-disk contents, and stamps the content hash onto the annotation. -
New optional fields (
Audit.dependencies,Audit.vcs) decode as empty /Noneon older blobs and are ignored by older readers, so they don’t break wire compatibility. -
A handful of source-level renames in the protocol crates (
CommitSigned→CommitEnvelope,AuditSigned→AuditEnvelope,Publish→Bundle,apply→build_commit, etc.). Wire bytes are unchanged for these; only Rust crate consumers see the rename. If you depend onopenvet-protooropenvet-logdirectly, your imports need updating.
The full per-bullet migration notes live in CHANGELOG.md in the
CLI repo.
What I’m working on next #
Some of the ideas that I listed in the 0.5.0 release I still haven’t gotten to, but they are still on the roadmap. There is a long list of features I still need to design, implement and/or test.
One feedback that I got from talking to @muji (thanks for taking the time!) was that the per-project config that OpenVet uses may not work well for all users. He suggested that it should support a way to use OpenVet as a global proxy for package registry operations. This is something that I need to think through, design and test. There are different ways this could be implemented: a proxy running on your local machine, or a hosted proxy that organizations can use to configure an organization-wide policy and overrides.
I see different mechanisms with different goals here:
- An organization-wide or system-wide proxy allows you to set a policy that applies to all projects. You can use this to enforce an organization-wide policy, like a baseline that all projects need to use, or for individual users it could be used to set a system-wide policy with a minimum set of requirements.
- A project configuration can be used to set more specific requirements. You may not need this at all. Or this can be used to set more granular requirements, depending on the needs of a project.
The over-arching goal of OpenVet is to make it as easy as possible for both individuals and organizations to adopt it. So getting this right matters a lot, and it requires me to do a lot of prototyping, testing, and talking to people to get some outside perspectives.
There are some alternative implementation approaches that I want to test out for the OpenVet guard mode that don’t involve an alias, so that tooling (IDEs) can use them too. I know roughly what those would look like, but I haven’t implemented them yet.
I put together some thoughts on the Licensing of audits hosted on the OpenVet registry, which I have not implemented in this version yet, but will land in the next version.
I’ll keep posting here as these land.
Find it / report it #
- Install:
cargo install openvet - Source: gitlab.com/openvet-org/openvet
- Docs: docs.openvet.org
- Changelog:
CHANGELOG.md - Issues: the GitLab tracker on the CLI repo. Issues and MRs are very much welcome — this is still a one-person project and any extra eyes help.
If you’d rather reach me directly, my GitHub profile has the contact info.