/ announcements / OpenVet 0.6.0 Release
cli release

OpenVet 0.6.0 Release

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, --report long-form report
  • -f, --findings findings (with --severity / --class to triage)
  • -n, --annotations source annotations
  • -d, --deps declared direct dependencies (with the new per-dependency narratives, see below)
  • -u, --users reverse dependencies (which of your audited packages declare this one as a dep)
  • -a, --all everything 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 new auto-seeds the list from a new openvet-package extractor — 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 to dependencies.json next to audit.pb as an auditor-local sidecar, never signed, never published.

  • A new openvet audit dependency subcommand 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 check runs ~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 check explains not-asserted requirements. When a requirement neither passes nor is contradicted because the claims it rests on were never asserted, check now 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 bare not asserted line.

  • openvet update <log> accepts a single log name. Refreshes only the named log’s pin and leaves every other entry in openvet.lock untouched. The bare openvet update still refreshes every configured log.

  • File annotations carry a per-file content hash. A new required FileTarget.content_hash field (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’s subject.hash already commits to the archive as a whole, but per-annotation hashes let a consumer detect (and openvet audit check flag) 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 InsertAudit and UpdateAudit operations carry the same subject as the embedded audit’s audit.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 timestamp must be greater than or equal to its predecessor’s, and the server rejects commits whose timestamp is more than 15 minutes away from its wall clock. Publishers whose system clock is badly off will see a 400 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_hash is now required. Audits produced by earlier releases (where file annotations had no content_hash) fail to decode. The path forward is to re-run openvet 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 / None on 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 (CommitSignedCommitEnvelope, AuditSignedAuditEnvelope, PublishBundle, applybuild_commit, etc.). Wire bytes are unchanged for these; only Rust crate consumers see the rename. If you depend on openvet-proto or openvet-log directly, 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 #

If you’d rather reach me directly, my GitHub profile has the contact info.