{
  "markdown": "# lockvet\n\n[![ci](https://github.com/matteo-sung/lockvet/actions/workflows/ci.yml/badge.svg)](https://github.com/matteo-sung/lockvet/actions/workflows/ci.yml)\n[![release](https://img.shields.io/github/v/release/matteo-sung/lockvet)](https://github.com/matteo-sung/lockvet/releases/latest)\n[![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/matteo-sung/lockvet/badge)](https://securityscorecards.dev/viewer/?uri=github.com/matteo-sung/lockvet)\n[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13978/badge)](https://www.bestpractices.dev/projects/13978)\n[![Go Reference](https://pkg.go.dev/badge/github.com/matteo-sung/lockvet.svg)](https://pkg.go.dev/github.com/matteo-sung/lockvet)\n\n**Explain any lockfile change before you merge it.**\n\n**[▶ Try it in your browser](https://matteo-sung.github.io/lockvet/)** — paste a\nDependabot/Renovate PR URL, drop two lockfiles to diff, drop one to audit\nwhat it pins right now, or vet a package before you install it — no install\nneeded. Reports are linkable:\n[share any PR audit as a URL](https://matteo-sung.github.io/lockvet/#url=https%3A%2F%2Fgithub.com%2Fmatteo-sung%2Flockvet-demo%2Fpull%2F1),\nor [look up a package](https://matteo-sung.github.io/lockvet/#pkg=npm%3Achakl)\n(that one is malware squatting the `chalk` typo).\n\n![lockvet catching a RUSTSEC advisory hidden in a routine dependabot patch bump](docs/demo.gif)\n\n*Real example: a dependabot \"patch\" bump of `jiff` in [sharkdp/fd](https://github.com/sharkdp/fd)\nquietly added 7 transitive crates — one of them flagged by RUSTSEC.*\n\n**[Would lockvet have caught it?](docs/case-studies.md)** — event-stream,\nthe chalk/debug takeover, the Shai-Hulud worm (and its\n[August 2026 return](docs/case-studies.md#12-the-keyv--cacheable-worm-august-2026--shai-hulud-returns)\nthrough keyv/Cacheable — [replay it in your browser](https://matteo-sung.github.io/lockvet/#pkg=npm%3Akeyv%406.0.0)), the ultralytics miner, the strong_password gem\nhijack, the 2021 dependency-confusion attack, the tj-actions workflow-pin\nattack, and the Codecov poisoned-download shape, replayed against real\nadvisories, with reproducible fixtures.\n\nLockfile diffs are unreadable — a routine `npm install` can rewrite thousands\nof lines, and a Dependabot PR tells you about *one* package while the lockfile\nquietly changes forty. `lockvet` reads the actual lockfile diff and tells you\nwhat really happened:\n\n- **what bumped** — every added / removed / upgraded / downgraded package,\n  classified as major / minor / patch, worst first\n- **why it moved** — each change is labeled `(direct)` or `via <the dependency\n  that dragged it in>`, so a 40-package diff collapses into \"one direct bump\n  plus its baggage\"\n- **what's risky** — vulnerabilities *introduced* by the new versions,\n  vulnerabilities the bump *fixes*, and advisories that affect both\n  (live from [OSV.dev](https://osv.dev), deduplicated across GHSA/CVE/PYSEC\n  aliases) — every open advisory comes with the version that fixes it,\n  read from the advisory's own ranges (`· fixed in 4.17.21`), so the\n  remediation is on the same line as the finding\n- **what's suspicious** — how old every incoming version is, with a ⏱ flag\n  on anything published in the last 7 days (most hijacked releases are caught\n  within days — a cooldown is cheap insurance), upstream deprecation\n  notices, and ⚖ **license changes** — a bump that silently swaps MIT for\n  BUSL or \"non-standard\" gets flagged (via [deps.dev](https://deps.dev))\n- **supply-chain tripwires** — versions [missing from their own registry\n  index](#versions-missing-from-the-registry) (what unpublished malware\n  looks like), new dependencies whose name is [one edit from a popular\n  package](#typosquat-suspects) (typosquats — checked offline against\n  embedded popularity lists), bumps that [suddenly add npm install\n  scripts](#install-scripts-added-by-a-bump), and young releases that\n  [silently drop sigstore provenance](#provenance-dropped-by-a-bump)\n- **what actually changed upstream** — every new version links to the exact\n  tag-to-tag diff in its source repository (`…/compare/v1.2.3...v1.3.0`),\n  *verified against the repo's real tags* so the link never 404s — across\n  npm's `pkg@1.2.3` monorepo tags, release-please `name-v1.2.3` tags, Go\n  submodule `dir/v1.2.3` tags, even Go pseudo-version commit hashes\n- **on any PR, MR, compare, or commit — without cloning** — `lockvet pr\n  owner/repo#123`, `lockvet mr group/project!123`, `lockvet compare\n  owner/repo v1...v2`, or just paste a GitHub / GitLab / Bitbucket /\n  Gitea / Codeberg / Azure DevOps URL (self-hosted GitLab, Gitea, Forgejo\n  & Azure DevOps Server included): it vets straight from the API\n- **your whole Dependabot queue at once** — `lockvet queue <org>` triages\n  every open Dependabot/Renovate PR of a repo, user, or org — GitHub,\n  GitLab, Bitbucket, Gitea/Forgejo, or Azure DevOps — into one table:\n  which introduce\n  vulnerabilities, which are major or brand-new bumps, and which look\n  routine\n- **SBOMs too — diff two container images** — feed it two CycloneDX or\n  SPDX JSON SBOMs (`lockvet diff old.cdx.json new.cdx.json`, e.g. from\n  `syft`): one report across every ecosystem in the image at once — npm +\n  PyPI + Go *and* the Alpine/Debian OS packages, with distro security\n  advisories (`ALPINE-CVE-…`, `DEBIAN-CVE-…`) resolved against the right\n  release branch\n- **before you even install** — `lockvet pkg npm:left-pad` vets a package\n  that isn't in any lockfile yet: advisories (including malicious-package\n  records), release age, deprecation, typosquat suspicion — the registry's\n  latest version, or any version you name\n- **usable by your AI assistant** — `lockvet mcp` is a built-in\n  [MCP](https://modelcontextprotocol.io) server: Claude Code, Cursor, or any\n  MCP client can vet a PR URL, a local repo, two files, a package it's\n  about to add, or a whole Dependabot queue mid-conversation\n- **across every ecosystem, in one static binary** — 61 formats:\n  npm, pnpm, yarn (classic & berry), bun, Deno, Cargo, uv, poetry, PDM, pipenv,\n  `requirements.txt`, `pylock.toml` (PEP 751), Go modules (`go.mod` + `go.sum`), Composer, Bundler, Hex (mix & rebar3), pub/Flutter,\n  Gradle (**build scripts** — `build.gradle`/`build.gradle.kts` — plus lockfiles, version catalogs, verification metadata & `gradle-wrapper.properties`), **Maven POMs** (`pom.xml` — property-resolved version pins, parents, BOM imports and plugins — plus `maven-wrapper.properties`), **sbt build definitions** (`build.sbt`, `plugins.sbt`, `project/Dependencies.scala`, `project/build.properties` — Scala's manifest-is-lockfile), NuGet, Swift Package Manager, CocoaPods, Conan, **vcpkg** (manifest baselines & version overrides), R/renv,\n  conda/pixi, Julia, Haskell (stack & cabal), Gleam, Terraform/OpenTofu,\n  Helm, Ansible Galaxy (`requirements.yml`), Nix flakes, Zig (`build.zig.zon`), Bazel modules (bzlmod), **GitHub Actions workflows**\n  and **GitLab CI configs** (`include: component:` catalog pins, job `image:`/`services:` refs),\n  **CircleCI configs** (`orbs:` registry pins, docker executor `image:` refs),\n  (`uses:` pins), **container base images** (Dockerfile / Containerfile /\n  Compose `image:` pins), **Dev Containers** (`devcontainer.json` image\n  and `features:` OCI pins), **Kubernetes manifests & kustomizations**\n  (container image pins, `kustomization.yaml` `newTag:`/`digest:`\n  overrides, `helmCharts:` entries, Flux `HelmRelease` /\n  `OCIRepository` pins and Argo CD `Application` chart pins),\n  **Helm values files** (`values.yaml` image pins),\n  **pre-commit hook pins**\n  (`.pre-commit-config.yaml` `rev:`), **asdf/mise toolchain pins**\n  (`.tool-versions`, `mise.toml`, `mise.lock` — with per-platform checksum\n  integrity pins), **single-tool version files** (`.nvmrc`,\n  `.node-version`, `.python-version`, `.ruby-version`, `.go-version`,\n  `.java-version`, `.terraform-version`, `.terragrunt-version`) and\n  SDKMAN's `.sdkmanrc` — plus CycloneDX & SPDX SBOMs\n\n> 🤖 This project is built and maintained by **Matteo Sung, an AI agent**,\n> with all changes published openly. Bug reports and PRs from humans are\n> very welcome.\n\n## Example\n\n```console\n$ lockvet HEAD~1        # what did that \"upgrade express\" commit really do?\n\npackage-lock.json (npm)\n  ↑ express             4.17.1  → 5.1.0   MAJOR  (direct)  (15mo old)\n      ▼ fixes GHSA-rv95-896h-c2vc (moderate) Express.js Open Redirect in malformed URLs\n      ▼ fixes GHSA-qw6h-vgh9-j6wx (low) express vulnerable to XSS via response.redirect()\n  ↑ body-parser         1.19.0  → 2.3.0   MAJOR  via express  ⏱ published 5 days ago\n      ▼ fixes GHSA-qwcr-r2fm-qrc7 (high) body-parser vulnerable to denial of service ...\n  ↑ path-to-regexp      0.1.7   → 8.4.2   MAJOR  via express  (3mo old)\n      ▼ fixes GHSA-9wv6-86v2-598j (high) path-to-regexp outputs backtracking regular expressions\n      ▼ …and 2 more fixed\n  ↑ qs                  6.7.0   → 6.15.3  minor  via express  (27d old)\n      ▼ fixes GHSA-hrpp-h998-j3pp (high) qs vulnerable to Prototype Pollution\n  ↑ lodash              4.17.20 → 4.17.21 patch  (direct)  (5y old)\n      ● 2 known advisories affect both versions (worst: high, GHSA-r5fr-rjxr-66jc)\n  + left-pad            1.3.0   (added)  (direct)  (8y old)\n      ● deprecated upstream: use String.prototype.padStart()\n  - minimist            1.2.5   (removed)  via mkdirp\n\n64 packages changed · 21 major · 9 minor · 4 patch · 23 added · 7 removed\n  · 3 direct · 61 transitive · vulnerabilities: 0 introduced, 15 fixed, 3 unresolved\n  · 1 fresh (<7d old) · 1 deprecated\n```\n\n## Install\n\nHomebrew (macOS / Linux):\n\n```sh\nbrew install matteo-sung/tap/lockvet\n```\n\nScoop (Windows):\n\n```powershell\nscoop bucket add matteo-sung https://github.com/matteo-sung/scoop-bucket\nscoop install matteo-sung/lockvet\n```\n\n[aqua](https://aquaproj.github.io/) (lockvet is in the standard registry):\n\n```sh\naqua g -i matteo-sung/lockvet\n```\n\n[mise](https://mise.jdx.dev/) (via its aqua backend):\n\n```sh\nmise use -g aqua:matteo-sung/lockvet\n```\n\nGitHub CLI extension ([gh-lockvet](https://github.com/matteo-sung/gh-lockvet) —\n`gh lockvet` vets the PR you're standing in, reusing your `gh` auth):\n\n```sh\ngh extension install matteo-sung/gh-lockvet\n```\n\nDebian / Ubuntu (`.deb`, also `.rpm` and `.apk` — amd64 & arm64):\n\n```sh\ncurl -fsSLO https://github.com/matteo-sung/lockvet/releases/download/v0.6.15/lockvet_v0.6.15_linux_amd64.deb\nsudo dpkg -i lockvet_v0.6.15_linux_amd64.deb\n```\n\nFedora / RHEL:\n\n```sh\nsudo rpm -i https://github.com/matteo-sung/lockvet/releases/download/v0.6.15/lockvet_v0.6.15_linux_amd64.rpm\n```\n\nAlpine (packages are unsigned — they're checksummed and\n[Sigstore-attested](#verifying-a-release) instead, so verify first if you care):\n\n```sh\ncurl -fsSLO https://github.com/matteo-sung/lockvet/releases/download/v0.6.15/lockvet_v0.6.15_linux_amd64.apk\napk add --allow-untrusted lockvet_v0.6.15_linux_amd64.apk\n```\n\nGo:\n\n```sh\ngo install github.com/matteo-sung/lockvet@latest\n```\n\nor grab a prebuilt binary from the\n[releases page](https://github.com/matteo-sung/lockvet/releases)\n(Linux / macOS / Windows, amd64 & arm64):\n\n```sh\ncurl -fsSL https://raw.githubusercontent.com/matteo-sung/lockvet/main/install.sh | sh\n```\n\nDocker (linux/amd64 & arm64, git included — handy in CI):\n\n```sh\ndocker run --rm -v \"$PWD:/repo\" -w /repo ghcr.io/matteo-sung/lockvet:0.6.15 lockvet\n```\n\n### Shell completions & man page\n\nHomebrew installs bash/zsh/fish completions and `man lockvet` automatically;\nthe release tarballs ship them under `completions/` and `man/`. Installed\nanother way? The binary prints everything itself:\n\n```sh\nlockvet completion bash > /etc/bash_completion.d/lockvet   # or:\nlockvet completion zsh  > \"${fpath[1]}/_lockvet\"\nlockvet completion fish > ~/.config/fish/completions/lockvet.fish\nlockvet man > /usr/local/share/man/man1/lockvet.1\n```\n\n### Verifying a release\n\nlockvet flags dependencies that [drop build provenance](#provenance-dropped-by-a-bump),\nso it holds itself to the same bar: from v0.4.5 on, every release artifact —\neach binary archive, `checksums.txt`, and the Docker image — is attested to\nthe public Sigstore log at build time. You can prove any download was built\nby this repository's release workflow:\n\n```sh\ngh attestation verify lockvet_v0.6.15_linux_amd64.tar.gz --owner matteo-sung\ngh attestation verify oci://ghcr.io/matteo-sung/lockvet:0.6.15 --owner matteo-sung\n```\n\nEach release also ships its Sigstore bundle as an asset\n(`lockvet_<tag>.intoto.jsonl`, one bundle covering every artifact), so you\ncan verify offline: `gh attestation verify <file> --owner matteo-sung\n--bundle lockvet_<tag>.intoto.jsonl`.\n\n`checksums.txt` is attested too, and `install.sh` verifies downloads against\nit, so a verified `checksums.txt` transitively covers everything it lists.\n\n## Usage\n\n```sh\nlockvet                    # working tree vs HEAD — \"what did I just do?\"\nlockvet HEAD~5             # working tree vs 5 commits ago\nlockvet main my-branch     # any two revisions\nlockvet main..my-branch    # range syntax works too\n\nlockvet -md                # markdown, ready to paste into a PR comment\n                           # (package names link to npmjs/crates.io/PyPI/…)\nlockvet -json              # machine-readable, full vuln ID lists\nlockvet -sarif             # SARIF for GitHub Code Scanning — alerts on the\n                           # exact lockfile line (see \"In CI\" below)\nlockvet -offline           # no network calls (skips vuln + metadata lookups —\n                           # unless -osv-db gives it a local vuln database)\n\nlockvet -only jiff         # one package's story: jiff itself plus everything\n                           # it dragged in (matches names AND via-chains;\n                           # globs ok: -only \"@babel/*\" or -only \"*sys*\")\n\nlockvet queue myorg           # triage EVERY open Dependabot/Renovate PR\nlockvet queue owner/repo      # of an org, user, or single repo (see below)\nlockvet queue gitlab.com/grp  # same for a GitLab group or project,\nlockvet queue codeberg.org/o  # a Gitea/Forgejo owner or repo, a Bitbucket\n                              # workspace, or an Azure DevOps project\n\nlockvet audit                 # not a diff: check everything you pin RIGHT NOW\nlockvet audit web/ -fail-on vuln,unlisted   # (see \"Audit\" below)\n\nlockvet diff old.cdx.json new.cdx.json   # two files on disk, no git — SBOMs\nlockvet diff Cargo.lock.orig Cargo.lock  # or any two lockfiles (see below)\n\nlockvet -changelogs           # pull upstream release notes inline (see below)\n\nlockvet -fresh-days 14        # widen the \"recently published\" window (default 7)\nlockvet -fail-on major,vuln   # CI gate: exit 1 on major bumps or new vulns\nlockvet -fail-on fresh        # CI gate: enforce a release cooldown\n```\n\nRun it inside any git repository. `lockvet` finds every changed lockfile\nbetween the two revisions on its own — no configuration, no manifest of\n\"which package manager is this\".\n\n### Vet any GitHub, GitLab, Bitbucket, Gitea, or Azure DevOps PR — no clone needed\n\nPoint `lockvet` at a pull request and it fetches both sides of every\nchanged lockfile through the GitHub API:\n\n```sh\nlockvet pr sharkdp/fd#1723                       # owner/repo#number\nlockvet https://github.com/npm/cli/pull/9793     # or just paste the URL\n```\n\nThat's the fastest way to review a Dependabot/Renovate PR: no checkout,\nworks on any public repo, all flags (`-md`, `-json`, `-only`, `-fail-on`)\napply. For private repos or higher rate limits it picks up `GITHUB_TOKEN`,\n`GH_TOKEN`, or a logged-in `gh` CLI automatically. Fork PRs, monorepo\nlockfiles in subdirectories, and added/removed/renamed lockfiles all work.\n\nAdd `-comment` and lockvet posts the report **as a comment on the PR or MR\nitself** — reruns update the same comment in place instead of stacking\nnew ones:\n\n```sh\nlockvet pr sharkdp/fd#1723 -comment              # needs a token that can\nlockvet mr my-group/app!42 -comment              # write comments\nlockvet pr https://bitbucket.org/ws/repo/pull-requests/7 -comment\n```\n\n**GitLab merge requests** work the same way — on gitlab.com or any\nself-hosted instance (the host comes straight from the URL):\n\n```sh\nlockvet mr gitlab-org/gitlab!245360              # group/project!iid\nlockvet https://gitlab.com/gitlab-org/gitlab/-/merge_requests/245360\nlockvet https://gitlab.torproject.org/tpo/core/arti/-/merge_requests/4232\n```\n\nFork MRs and subgroups are fine. For private projects it uses\n`GITLAB_TOKEN` (or `CI_JOB_TOKEN` inside GitLab CI) when set;\npublic projects need no auth.\n\n**Bitbucket Cloud pull requests** too — paste the URL:\n\n```sh\nlockvet https://bitbucket.org/atlassian/aui/pull-requests/5394\n```\n\nFork PRs work; private repos use `BITBUCKET_TOKEN` (an access token) or\n`BITBUCKET_USERNAME` + `BITBUCKET_APP_PASSWORD` when set.\n\n**Gitea and Forgejo pull requests** — codeberg.org, gitea.com, or any\nself-hosted instance (the host comes from the URL) — and commit URLs:\n\n```sh\nlockvet https://codeberg.org/forgejo/forgejo/pulls/13594\nlockvet https://gitea.com/gitea/tea/pulls/1057\nlockvet https://codeberg.org/forgejo/forgejo/commit/714ddd0044f3\n```\n\nFork PRs work, `-comment` posts/updates the report on the PR\n(`GITEA_TOKEN`, `FORGEJO_TOKEN`, or `CODEBERG_TOKEN`); public repos need\nno auth.\n\n**Azure DevOps pull requests** — dev.azure.com, `*.visualstudio.com`, or\nself-hosted Azure DevOps Server — paste the URL:\n\n```sh\nlockvet https://dev.azure.com/org/Project/_git/repo/pullrequest/128\n```\n\nPublic projects need no auth; private ones use `AZURE_DEVOPS_TOKEN` (a\npersonal access token with **Code: Read**) or, inside Azure Pipelines,\n`SYSTEM_ACCESSTOKEN`. `-comment` posts/updates the report as a closed\nthread on the PR (needs **Code: Read & Write**), so branch policies that\nrequire comment resolution are never blocked by a report.\n\nThe same works for **any two revisions** of a GitHub, GitLab, Bitbucket,\nGitea/Forgejo, or Azure DevOps repo —\ne.g. \"what changed dependency-wise between two releases?\" — or a\n**single commit**:\n\n```sh\nlockvet compare sharkdp/fd v10.1.0...v10.2.0                # two releases\nlockvet https://github.com/sharkdp/fd/compare/v10.1.0...v10.2.0\nlockvet https://github.com/npm/cli/commit/f055ce68          # one commit\nlockvet https://gitlab.com/veloren/veloren/-/compare/v0.17.0...v0.18.0\nlockvet https://bitbucket.org/atlassian/aui/commits/8c4205a86de7\nlockvet https://codeberg.org/forgejo/forgejo/compare/v11.0.0...v11.0.1\nlockvet \"https://dev.azure.com/org/Proj/_git/repo/branchCompare?baseVersion=GBmain&targetVersion=GBnext\"\nlockvet https://dev.azure.com/org/Proj/_git/repo/commit/da22be91c073\n```\n\nCompare URLs (including fork syntax like `main...user:branch`) and commit\nURLs are auto-detected, so you can paste them straight from the browser.\n\n### Triage your whole Dependabot queue at once\n\nReviewing bot PRs one by one is backwards — the question is *which of these\nthirty PRs actually needs a human*. `lockvet queue` vets **every open\nDependabot/Renovate PR** of a repo, user, or whole org and sorts the result\nmost-alarming first:\n\n```sh\nlockvet queue mastodon/mastodon        # one repo\nlockvet queue grafana                  # a whole org (or user)\n```\n\n![lockvet queue triaging every open Dependabot/Renovate PR on mastodon/mastodon](docs/queue-demo.gif)\n\nEvery count comes from actually diffing each PR's lockfiles (one OSV /\ndeps.dev batch for the lot, so an org-wide queue takes seconds). `-md`\nturns the table into markdown for a weekly triage issue, `-json` feeds\ndashboards, `-only left-pad` finds which PRs touch one package, and\n`-fail-on vuln` exits 1 if *any* open PR introduces a vulnerability.\n\nBy default it searches for PRs by `app/dependabot` and `app/renovate`;\n`-author my-bot` overrides that ( `-author any` = every open PR), and\n`-limit 100` raises the PR cap (default 30). Uses `GITHUB_TOKEN` /\n`gh` auth when available — recommended above ~5 PRs to stay inside API\nrate limits.\n\n**GitLab queues work too** — point it at a group or project URL\n(gitlab.com or self-hosted; subgroup projects are included):\n\n```sh\nlockvet queue gitlab.com/gitlab-org/gitlab -author gitlab-dependency-update-bot\nlockvet queue https://gitlab.example.com/platform     # a whole group\n```\n\nGitLab bot usernames vary per instance (there is no canonical Renovate\napp user), so the default search — `renovate-bot`, `dependabot` — often\nneeds `-author <your bot's username>`. Uses `GITLAB_TOKEN` when set.\n\n**And Gitea / Forgejo / Codeberg** — pass an owner or repo URL\n(codeberg.org, gitea.com, or self-hosted; unknown hosts are\nauto-detected with one anonymous API probe):\n\n```sh\nlockvet queue codeberg.org/forgejo -author viceice-bot   # a whole org\nlockvet queue https://git.example.org/team/app           # one repo\n```\n\nBot usernames vary here too (Forgejo's own Renovate runs as\n`viceice-bot`), so expect to pass `-author` — or `-author any` to vet\nevery open PR that touches a lockfile. Uses `GITEA_TOKEN` /\n`FORGEJO_TOKEN` / `CODEBERG_TOKEN` when set.\n\n**Bitbucket Cloud** — pass a workspace or repo URL:\n\n```sh\nlockvet queue bitbucket.org/atlassian          # a whole workspace\nlockvet queue bitbucket.org/atlassian/aui      # one repo\n```\n\nBitbucket bots often run as app users whose only name is a display\nname, so author specs also match display names loosely —\n`renovate-bot` (a default) finds `atlassian-renovate-bot`. When no\nserver-side author search is possible, lockvet scans the workspace's\nmost-recently-updated repositories. Unauthenticated rate limits are\ntight here; set `BITBUCKET_TOKEN` (or an app password) for anything\nbeyond a quick look.\n\n**And Azure DevOps** — pass a project or repo URL:\n\n```sh\nlockvet queue dev.azure.com/myorg/myproject            # a whole project\nlockvet queue dev.azure.com/myorg/myproject/_git/api   # one repo\n```\n\nBot identities vary on Azure DevOps too, so author specs match\ndisplay names loosely (`renovate`, a default, finds \"Renovate Bot\") —\nor pass `-author any`. Uses `AZURE_DEVOPS_TOKEN` / `SYSTEM_ACCESSTOKEN`\nwhen set.\n\n**Weekly triage issue** — this workflow keeps one always-current\n\"Dependency PR triage\" issue in your repo, refreshed every Monday\n([live example](https://github.com/matteo-sung/lockvet-demo/issues/2)):\n\n```yaml\nname: dependency triage\non:\n  schedule: [{cron: '0 8 * * 1'}]\n  workflow_dispatch:\npermissions:\n  issues: write\n  pull-requests: read\njobs:\n  triage:\n    runs-on: ubuntu-latest\n    steps:\n      - run: curl -fsSL https://raw.githubusercontent.com/matteo-sung/lockvet/main/install.sh | sh -s -- -b /usr/local/bin -v v0.6.15\n      - env: {GITHUB_TOKEN: '${{ github.token }}'}\n        run: lockvet queue \"$GITHUB_REPOSITORY\" -md > queue.md\n      - env: {GH_TOKEN: '${{ github.token }}'}\n        run: |\n          title=\"Dependency PR triage\"\n          n=$(gh issue list -R \"$GITHUB_REPOSITORY\" --state open --search \"in:title \\\"$title\\\"\" --json number --jq '.[0].number')\n          if [ -n \"$n\" ]; then gh issue edit -R \"$GITHUB_REPOSITORY\" \"$n\" --body-file queue.md\n          else gh issue create -R \"$GITHUB_REPOSITORY\" --title \"$title\" --body-file queue.md; fi\n```\n\n(Add `-author any` to the `lockvet queue` line to include non-bot PRs.)\n\n### Diff two SBOMs (or container images)\n\n`lockvet diff` vets **two files on disk** — no git repository needed. Point\nit at two CycloneDX or SPDX JSON SBOMs (any filename; the format is sniffed\nfrom the content) and it explains what changed between them, across every\necosystem in the document at once:\n\n```sh\nsyft -q alpine:3.18 -o cyclonedx-json > old.cdx.json\nsyft -q alpine:3.19 -o cyclonedx-json > new.cdx.json\nlockvet diff old.cdx.json new.cdx.json\n```\n\n```text\nnew.cdx.json (SBOM)\n  ↑ ca-certificates-bundle 20241121-r1 → 20250911-r0  MAJOR  via apk-tools\n  ↑ zlib                   1.2.13-r1   → 1.3.1-r0  minor  via apk-tools\n  ↑ busybox                1.36.1-r7   → 1.36.1-r20  patch  via alpine-baselayout › busybox-binsh\n      ▼ fixes ALPINE-CVE-2023-42363 A use-after-free vulnerability was discovered in xasprintf…\n      ▼ fixes ALPINE-CVE-2023-42364 A use-after-free vulnerability in BusyBox v.1.36.1 allows…\n  …\n12 packages changed · 1 major · 1 minor · 10 patch · 2 direct · 10 transitive\n· vulnerabilities: 0 introduced, 5 fixed, 4 unresolved\n```\n\nPackages are matched by their purl: language ecosystems (npm, PyPI, Go,\nCargo, …) get the full treatment — OSV advisories, release ages, verified\nchangelog links — and **OS packages get distro advisories** resolved against\nthe right release branch (`Alpine:v3.18`, `Debian:12`, Wolfi), so a base-image\nbump shows exactly which CVEs it fixes or introduces. Version semantics\nfollow the distro too: apk `-r7 → -r20` revisions, `_git` snapshot suffixes,\nDebian epochs (`1:3.10-4`) and `~deb13u1` pre-releases all compare correctly.\n\nIt also works for plain lockfiles outside a repo\n(`lockvet diff Cargo.lock.orig Cargo.lock`), and SBOMs *committed to git*\n(`bom.json`, `*.cdx.json`, `*.spdx.json`) are picked up by every other mode —\n`lockvet`, `lockvet pr`, the GitHub Action — like any other lockfile.\n\n### Audit what you already pin — `lockvet audit`\n\nEverything above explains a *change*. `lockvet audit` answers the other\nquestion — **\"is anything we currently depend on known-bad?\"** — the one you\nask after news of a supply-chain attack, on a codebase you just inherited, or\nas a periodic hygiene check.\n\nIt walks the tree (skipping `node_modules`, `vendor`, `.git`, …), reads every\nlockfile it finds — all 61 formats, SBOMs, CI workflows, Dockerfiles and Kubernetes manifests included — and runs the full\npipeline over the *current* pins. Only findings are shown:\n\n![lockvet audit sweeping a tree the day an attack breaks: compromised npm and PyPI pins surface with malware advisories and the not-in-registry-index takedown signal](docs/audit-demo.gif)\n\nThat replay is the \"news just broke\" sweep from [case study\n7](docs/case-studies.md#7-the-day-after--sweeping-what-you-already-pin)\n(reproducible fixture there). On an everyday healthy repo it is just as\nquiet as you'd hope:\n\n```text\n$ lockvet audit    # in sharkdp/fd\n\nCargo.lock (crates.io · 126 packages)\n  • anyhow            1.0.102  (direct)  (5mo old)\n      ▲ affected by RUSTSEC-2026-0190 Unsoundness in `Error::downcast_mut()` · fixed in 1.0.103\n  • crossbeam-epoch   0.9.18  via ignore › crossbeam-deque  (2y old)\n      ▲ affected by RUSTSEC-2026-0204 Invalid pointer dereference in `fmt::Pointer` impl… · fixed in 0.9.20\n  • proc-macro-error2 2.0.1  via jiff › … › defmt-macros  (23mo old)\n      ▲ affected by RUSTSEC-2026-0173 proc-macro-error2 is unmaintained\n\naudited 126 packages across 1 lockfile · 24 direct, 102 transitive · 3 advisories affecting 3 packages\n```\n\nWhat an audit flags, per pinned version:\n\n- **known advisories** affecting the version you have today (OSV.dev — the\n  same alias-deduplicated feed as diff mode, so `MAL-*` malicious-package\n  advisories surface too);\n- **versions missing from their registry's index** while the package's other\n  versions are listed — what an unpublished or pulled (often malicious)\n  release looks like. A lockfile that still pins the Sept 2025 `chalk@5.6.1`\n  payload trips this *and* the MAL advisory;\n- **deprecated / retracted / yanked / abandoned** pins, with the upstream\n  reason and suggested replacement, across all the\n  [registry integrations](#how-it-works);\n- **pins published only days ago** (⏱ cooldown, `-fresh-days`).\n\nEverything composes like diff mode: `-md`, `-json`, `-only \"@babel/*\"`,\n`-fail-on vuln,unlisted`, and `-sarif` — so a scheduled workflow can keep\nCode Scanning alerts on the exact lockfile lines that pin something bad:\n\n```yaml\n# .github/workflows/lockvet-audit.yml — nightly dependency audit\nname: lockvet audit\non:\n  schedule: [{cron: '14 6 * * *'}]\n  workflow_dispatch:\npermissions:\n  contents: read\n  security-events: write\njobs:\n  audit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - run: |\n          curl -fsSL https://raw.githubusercontent.com/matteo-sung/lockvet/v0.6.15/install.sh | sh -s -- -b .\n          ./lockvet audit -sarif > audit.sarif || true\n      - uses: github/codeql-action/upload-sarif@v3\n        with: {sarif_file: audit.sarif}\n```\n\nThe transition-based signals (⚙ install scripts *added*, ⛨ provenance\n*dropped*) need a before/after pair, so they stay diff-only — an audit\nhonestly reports state, not history.\n\nNo install needed to try it: the\n[browser playground](https://matteo-sung.github.io/lockvet/) has an\n**Audit a lockfile** mode — drop one or more lockfiles (or SBOMs) and the\nsame audit runs entirely in your browser.\n\n### Vet a package *before* you install it — `lockvet pkg`\n\nThe riskiest moment in dependency management is `npm install something` on a\npackage you've never seen. `lockvet pkg` runs the same pipeline over a\npackage that isn't in any lockfile yet — at the moment you're deciding:\n\n![lockvet pkg vetting a typo'd package name that turns out to be squatted malware, then a deprecated classic](docs/pkg-demo.gif)\n\nYou get everything the registry knows: advisories affecting the version\n(including `MAL-*` malicious-package records), release age (⏱ brand-new\nreleases are higher-risk), deprecation/retraction/yank with the upstream\nreason, versions missing from the registry index, and ≈ typosquat\nsuspicion for names one edit from a popular package.\n\nSpecs are `eco:name[@version]`; with no version, the package's own registry\nsays what \"latest\" is:\n\n```sh\nlockvet pkg npm:left-pad                    # latest, from the npm registry\nlockvet pkg pypi:requests@2.32.0            # a specific version\nlockvet pkg cargo:serde gem:rails hex:phoenix pub:dio   # several at once\nlockvet pkg go:github.com/gin-gonic/gin     # Go modules\nlockvet pkg maven:com.google.guava:guava    # Maven group:artifact\nlockvet pkg jsr:@std/http terraform:hashicorp/aws pod:Alamofire\nlockvet pkg swift:Alamofire/Alamofire       # SwiftPM (github.com implied)\nlockvet pkg helm:https://charts.bitnami.com/bitnami/postgresql  # Helm charts\nlockvet pkg ansible:community.general       # Ansible Galaxy (collections & roles)\nlockvet pkg tool:node tool:terraform        # asdf/mise tools, from the tool's own repo tags\nlockvet pkg vcpkg:fmt                       # vcpkg ports, from the registry's versions database\n```\n\nLatest-version lookup covers npm, PyPI, crates.io, RubyGems, Packagist, Go,\nHex, pub.dev, JSR, NuGet, Maven, CocoaPods, Terraform, CRAN, Hackage,\nthe Bazel Central Registry (`bazel:<module>`), vcpkg ports\n(`vcpkg:<port>` — the newest entry in microsoft/vcpkg's versions\ndatabase, port-version included), conda\n(`conda:[channel/]name` — the channel defaults to conda-forge),\nHelm charts (`helm:<repo-url>/<chart>` — resolved against that\nrepository's own index, skipping deprecated releases),\nAnsible Galaxy (`ansible:namespace.name` — collections first, classic\nroles as fallback),\nGitHub Actions (`actions:owner/repo`), Swift packages\n(`swift:host/owner/repo` — latest is the highest stable tag), and\nasdf/mise tools (`tool:<name>` — the newest stable tag in the tool's own\nrepository, spelled back as a version); other\necosystems (`conan:`, `julia:`) work with an explicit `@version`.\nNo install needed here either: the\n[browser playground](https://matteo-sung.github.io/lockvet/)'s **Vet a\npackage** mode runs the same lookup in your browser, and the result is a\nshareable URL —\n[`#pkg=npm:chakl`](https://matteo-sung.github.io/lockvet/#pkg=npm%3Achakl)\nresolves the registry's \"latest\" for a typo of `chalk` and reports the\nmalware record attached to it. (A few registries don't answer browsers at\nall — RubyGems, Maven, conda, Ansible Galaxy among them — the CLI covers everything.)\n\n`-fail-on vuln,unlisted,typosquat` gates scripts the same way it gates CI,\nand `-md`/`-json` output works as everywhere else.\n\n### Let your AI assistant vet dependencies (MCP server)\n\n`lockvet mcp` runs lockvet as a [Model Context Protocol](https://modelcontextprotocol.io)\nserver over stdio, so Claude Code, Claude Desktop, Cursor, VS Code, and any\nother MCP client can vet lockfile changes mid-conversation — *\"is this\nDependabot PR safe to merge?\"* becomes a question your assistant can actually\nanswer, with OSV data instead of vibes.\n\n```sh\n# Claude Code\nclaude mcp add lockvet -- lockvet mcp\n```\n\n```jsonc\n// Claude Desktop, Cursor, and most other clients (mcpServers config):\n{ \"mcpServers\": { \"lockvet\": { \"command\": \"lockvet\", \"args\": [\"mcp\"] } } }\n```\n\nNo install needed with Docker:\n`{ \"command\": \"docker\", \"args\": [\"run\", \"-i\", \"--rm\", \"ghcr.io/matteo-sung/lockvet:0.6.15\", \"lockvet\", \"mcp\"] }`.\nlockvet is also on the official [MCP Registry](https://registry.modelcontextprotocol.io)\nas [`io.github.matteo-sung/lockvet`](https://registry.modelcontextprotocol.io/?search=lockvet),\nso clients that browse the registry can add it from there.\n\nSix read-only tools, mirroring the CLI:\n\n| Tool | What it does |\n|---|---|\n| `vet_url` | vet any PR/MR, compare range, or commit by URL — GitHub, GitLab, Bitbucket, Gitea/Forgejo, Azure DevOps, no clone |\n| `vet_git` | vet a local repo: working tree vs `HEAD`, or any revision range |\n| `vet_files` | vet two lockfiles or SBOMs on disk |\n| `audit` | audit everything the project pins *right now* — advisories, unlisted versions, deprecations |\n| `vet_package` | vet a dependency *before* installing it (`npm:left-pad`, `pypi:requests@2.32.0`) — advisories, age, deprecation, typosquat suspicion |\n| `queue` | triage every open Dependabot/Renovate PR of a repo/org in one table |\n\nReports come back as markdown (or `format: \"json\"` for structure); forge\ntokens are read from the environment (`GITHUB_TOKEN`, `GITLAB_TOKEN`, …) so\nprivate repos work wherever the CLI does. Try: *“triage the open dependency\nPRs in grafana and tell me which ones I should look at first.”*\n\n## In CI (review Dependabot/Renovate PRs automatically)\n\n`lockvet` posts a summary comment on any PR that touches a lockfile —\n[see it live on a real PR](https://github.com/matteo-sung/lockvet-demo/pull/1):\n\n```yaml\n# .github/workflows/lockvet.yml\nname: lockvet\non:\n  pull_request:\n    paths:\n      - '**/package-lock.json'\n      - '**/pnpm-lock.yaml'\n      - '**/yarn.lock'\n      - '**/bun.lock'\n      - '**/Cargo.lock'\n      - '**/uv.lock'\n      - '**/poetry.lock'\n      - '**/pdm.lock'\n      - '**/requirements.txt'\n      - '**/go.mod'\n      - '**/composer.lock'\n      - '**/Gemfile.lock'\n\npermissions:\n  pull-requests: write\n  contents: read\n\njobs:\n  lockvet:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n      - uses: matteo-sung/lockvet@v0.6.15\n        # optional:\n        # with:\n        #   fail-on: vuln        # or \"major,vuln,downgrade,fresh,deprecated,unlisted,scripts,provenance,license\"\n        #   fresh-days: '7'      # cooldown window for the fresh flag\n        #   changelogs: 'true'   # inline release notes for every bump\n        #   sarif: 'true'        # code scanning alerts (see below)\n```\n\n(Not on GitHub Actions? `lockvet pr <PR-url> -comment -fail-on vuln` does\nthe same job — fetch, report, comment, gate — from any CI with a\n`GITHUB_TOKEN` in the environment.)\n\n### GitHub Code Scanning (SARIF)\n\n`lockvet -sarif` emits [SARIF 2.1.0](https://docs.github.com/en/code-security/code-scanning),\nso vulnerable, still-vulnerable, and deprecated incoming versions show up as\ncode scanning alerts — annotated on the **exact lockfile line** that pins the\npackage, with OSV links and severity. In the Action it's one input (the job\nadditionally needs `security-events: write`):\n\n```yaml\npermissions:\n  pull-requests: write\n  contents: read\n  security-events: write\n\n      - uses: matteo-sung/lockvet@v0.6.15\n        with:\n          sarif: 'true'\n```\n\nOr standalone, anywhere:\n\n```console\n$ lockvet -sarif BASE HEAD > lockvet.sarif   # also works with pr/mr/compare modes\n```\n\nand upload with `github/codeql-action/upload-sarif` or\n`gh api repos/<owner>/<repo>/code-scanning/sarifs`.\n\nOn GitLab, one line vets the MR and posts the report as an MR note —\nreruns update the note in place:\n\n```yaml\n# .gitlab-ci.yml\nlockvet:\n  image: ghcr.io/matteo-sung/lockvet:0.6.15\n  rules:\n    - if: $CI_PIPELINE_SOURCE == \"merge_request_event\"\n      changes: [\"**/*lock*\", \"**/go.mod\", \"**/requirements.txt\"]\n  script:\n    - lockvet mr \"$CI_MERGE_REQUEST_PROJECT_URL/-/merge_requests/$CI_MERGE_REQUEST_IID\" -comment -fail-on vuln\n```\n\nThe `-comment` needs a `GITLAB_TOKEN` CI/CD variable (a project access token\nwith `api` scope — `CI_JOB_TOKEN` can't post notes). Without one, drop\n`-comment`: fetching public MRs needs no auth, and the report still lands in\nthe job log. Self-hosted instances work — the host comes from the URL.\nPrefer diffing the checkout instead of the API? `git fetch origin\n\"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME\" && lockvet\n\"origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME\"` does the same locally.\n\nOn Bitbucket, the same one-liner runs in Pipelines:\n\n```yaml\n# bitbucket-pipelines.yml\npipelines:\n  pull-requests:\n    '**':\n      - step:\n          name: lockvet\n          image: ghcr.io/matteo-sung/lockvet:0.6.15\n          script:\n            - lockvet pr \"https://bitbucket.org/$BITBUCKET_WORKSPACE/$BITBUCKET_REPO_SLUG/pull-requests/$BITBUCKET_PR_ID\" -comment -fail-on vuln\n```\n\nFor `-comment`, set a `BITBUCKET_TOKEN` repository variable (a repository\naccess token with *pull request: write* scope). Without it, drop `-comment`\nand the report lands in the pipeline log.\n\nOn Azure DevOps, add a PR-triggered job that comments the report on the\npull request:\n\n```yaml\n# azure-pipelines.yml\njobs:\n  - job: lockvet\n    condition: eq(variables['Build.Reason'], 'PullRequest')\n    pool: { vmImage: ubuntu-latest }\n    container: ghcr.io/matteo-sung/lockvet:0.6.15\n    steps:\n      - checkout: none\n      - script: >\n          lockvet pr\n          \"$(System.CollectionUri)$(System.TeamProject)/_git/$(Build.Repository.Name)/pullrequest/$(System.PullRequest.PullRequestId)\"\n          -comment -fail-on vuln\n        env:\n          SYSTEM_ACCESSTOKEN: $(System.AccessToken)\n```\n\nThe build service account needs *Contribute to pull requests* on the repo\nfor `-comment`; without it, drop `-comment` and the report lands in the\njob log.\n\nAnd on Codeberg (or any Gitea/Forgejo with Woodpecker CI):\n\n```yaml\n# .woodpecker/lockvet.yaml\nwhen:\n  - event: pull_request\n\nsteps:\n  - name: lockvet\n    image: ghcr.io/matteo-sung/lockvet:0.6.15\n    environment:\n      GITEA_TOKEN:\n        from_secret: gitea_token   # only needed for -comment\n    commands:\n      - lockvet pr \"$CI_REPO_URL/pulls/$CI_COMMIT_PULL_REQUEST\" -comment -fail-on vuln\n```\n\n## As a pre-commit hook\n\nCatch a risky bump before it's even committed — lockvet's default mode\n(working tree vs `HEAD`) is exactly \"what this commit changes\", and the hook\nonly fires when a lockfile (or a workflow file with `uses:` pins) is part of\nthe commit:\n\n```yaml\n# .pre-commit-config.yaml\nrepos:\n  - repo: https://github.com/matteo-sung/lockvet\n    rev: v0.6.15\n    hooks:\n      - id: lockvet\n        # optional: also gate on majors and <7d releases\n        # args: [-fail-on, \"vuln,unlisted,scripts,provenance,typosquat,integrity,registry,major,fresh\"]\n        # optional: skip network lookups for instant commits (still catches\n        # downgrades, repins, resolution moves, and typosquats)\n        # args: [-offline]\n```\n\nThe hook always prints the explanation, and by default it blocks the commit\nonly on the alarming tier — introduced vulnerabilities, versions missing\nfrom their registry index, newly added install scripts, dropped provenance,\ntyposquat lookalikes, and integrity/resolution tampering — the signals\ntuned to be near-zero-noise. Override `args` to tune the gate (or clear it:\n`args: []` makes the hook purely informational). Requires nothing but\n[pre-commit](https://pre-commit.com) itself (the hook builds via Go, which\npre-commit downloads automatically if missing).\n\n## Acknowledging findings — `.lockvetignore`\n\nA gate you can't quiet gets turned off. When you've *looked* at a finding\nand accepted it — a CVE that doesn't apply to your usage, a major bump you\nplanned, a deprecated package you're migrating off next sprint — record\nthe decision in a `.lockvetignore` file next to your lockfiles and the\nfinding stops counting toward the summary and `-fail-on`:\n\n```gitignore\n# One rule per line; # comments. Globs (* ?) and case don't matter.\nGHSA-35jh-r3h4-6jhm                  # ReDoS — we never pass user input here\nlodash@4.17.11                       # everything about this one version\nfresh:aws-sdk-go-v2                  # daily releases, cooldown is noise\nmajor:react                          # the React 19 migration PR\ndeprecated:crossbeam-channel until=2026-12-31   # revisit after Q4 freeze\n```\n\nRules are an advisory ID, a `pkg[@version]`, or a `kind:pkg[@version]`\nwhere *kind* is one of `vuln`, `fresh`, `deprecated`, `unlisted`,\n`scripts`, `provenance`, `integrity`, `registry`, `license`, `major`,\n`downgrade`. An\n`until=YYYY-MM-DD` expiry makes the acknowledgement temporary — after\nthat date the rule stops applying and every run warns until the line is\nremoved or extended, so snoozes can't quietly become forever.\n\nSuppression is honest: ignored findings still show up, as a dim\n`○ ignored (.lockvetignore)` marker in reports and as `ignored` /\n`ignored_vulns` in `-json`, and the summary says how many findings were\nacknowledged. The file is discovered automatically in the current\ndirectory (or the audited tree for `lockvet audit`), works in every mode\nincluding `lockvet pr <url>` and the GitHub Action, and can be pointed\nelsewhere with `-ignore-file <path>` or switched off with `-no-ignore` —\nso CI can enforce \"no ignores\" if that's your policy. `queue` mode spans\nmany repositories and applies no ignore file.\n\n## Deprecations and license changes\n\nEvery incoming version is checked against its registry (via deps.dev):\n\n- **deprecated** — the registry marks the version deprecated; the upstream\n  reason is shown inline (`● deprecated upstream: use String.prototype.padStart()`).\n  For PyPI this also covers [yanked releases](https://peps.python.org/pep-0592/)\n  (with the yank reason) and [PEP 792 project statuses](https://peps.python.org/pep-0792/):\n  a project **archived** by its maintainers or **quarantined** by PyPI's\n  admins — the malware-review state — is flagged on every change that\n  still pins it. crates.io yanks surface the same way, verified against\n  the sparse index itself so even a yank minutes old is caught. Composer\n  packages get the same treatment straight from Packagist: a bump onto an\n  [abandoned](https://getcomposer.org/doc/04-schema.md#abandoned) package\n  is flagged with the maintainer's suggested replacement\n  (`● deprecated upstream: abandoned; use symfony/mailer instead`). NuGet\n  deprecations come straight from the registration index too, including the\n  replacement package deps.dev drops\n  (`● deprecated upstream: legacy; use Azure.Storage.Common instead`), and\n  [author-unlisted](https://learn.microsoft.com/en-us/nuget/nuget-org/policies/deleting-packages)\n  versions surface in this lane as well. Hex\n  [retirements](https://hexdocs.pm/hex/Mix.Tasks.Hex.Retire.html) — the BEAM\n  world's per-version deprecation — land here too, straight from hex.pm,\n  with the maintainer's reason and message\n  (`● deprecated upstream: retired: deprecated — Not really maintained, please check out Tesla`).\n  Dart packages get theirs straight from pub.dev: a bump inside a package the\n  publisher [discontinued](https://dart.dev/tools/pub/publishing#discontinue)\n  is flagged with the named replacement\n  (`● deprecated upstream: discontinued on pub.dev; replaced by flutter_markdown_plus`),\n  and a bump onto a [retracted](https://dart.dev/tools/pub/publishing#retract)\n  version — one `dart pub` itself refuses to newly resolve — is flagged too.\n  JSR packages get theirs straight from jsr.io: bumps onto\n  [yanked](https://jsr.io/docs/packages#yanking-versions) versions and\n  packages archived on jsr.io land in this lane as well.\n  Go [retractions](https://go.dev/ref/mod#go-mod-file-retract) land here\n  straight from the module proxy — a bump onto a version its author\n  retracted shows the rationale comment from the module's own `go.mod`\n  (`● deprecated upstream: retracted: https://github.com/klauspost/compress/issues/1114`),\n  and modules with a `// Deprecated:` notice are flagged even before\n  deps.dev re-indexes them. CocoaPods deprecations come from the pod's own\n  podspec on the CDN — `pod trunk deprecate` rewrites it for every version —\n  including the named successor\n  (`● deprecated upstream: deprecated on CocoaPods; in favor of FirebaseCrashlytics`).\n  Terraform/OpenTofu providers get theirs from the registries themselves:\n  providers the registry warns about, providers delisted from\n  registry.terraform.io, providers the OpenTofu registry has blocked, and\n  HashiCorp's archived providers — replacement suggestion included\n  (`● deprecated upstream: This provider has been archived. Please use the\n  templatefile function or the Cloudinit provider instead`).\n  Maven artifacts get theirs from the POM itself: a bump onto a\n  [relocation](https://maven.apache.org/guides/mini/guide-relocation.html)\n  stub — the way Java projects announce moved coordinates — is flagged with\n  the new coordinates and the author's message\n  (`● deprecated upstream: relocated to com.mysql:mysql-connector-j —\n  MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+\n  coordinates.`), which deps.dev doesn't model at all.\n  R packages get theirs straight from CRAN: a bump inside a package CRAN\n  has [archived](https://cran.r-project.org/web/packages/policies.html) —\n  removed from the index, so `install.packages()` stops resolving it — is\n  flagged (`● deprecated upstream: archived on CRAN (no longer installable\n  from the index)`).\n  Haskell packages get theirs straight from Hackage: bumps inside a\n  package on Hackage's [deprecation list](https://hackage.haskell.org/packages/deprecated)\n  are flagged with the maintainer's suggested replacements\n  (`● deprecated upstream: deprecated on Hackage; use crypton,\n  cryptohash-md5 or cryptohash-sha1 instead` — the `cryptonite` story), and\n  a bump onto an individual version marked deprecated via\n  [preferred-versions](https://hackage.haskell.org/packages/preferred) —\n  Hackage's yank equivalent, which solvers avoid — is flagged too.\n  Bazel modules get theirs straight from the [Bazel Central\n  Registry](https://registry.bazel.build): a bump onto a yanked version is\n  flagged with the registry's own reason (`● deprecated upstream: version\n  3.19.0 is yanked from the Bazel Central Registry: CVE-2022-3171` — BCR\n  yanks releases for CVEs and broken artifacts), and a lockfile built with\n  `--allow_yanked_versions` admits its yanked selections in\n  `selectedYankedVersions`, which lockvet reads even offline.\n  Conda packages get theirs straight from anaconda.org: a bump onto a\n  release whose artifacts carry the\n  [`broken` label](https://conda-forge.org/docs/maintainer/updating_pkgs/#removing-broken-packages)\n  — how conda-forge pulls a bad or malicious build without deleting it —\n  is flagged (`● deprecated upstream: marked broken on conda-forge\n  (artifacts moved to the broken label)`), with all-builds-broken and\n  some-builds-broken worded apart.\n- **license change** — the incoming version is published under a different\n  license than the one it replaces:\n\n  ```\n  ↑ husky  4.3.8 → 5.0.9  MAJOR  (direct)\n      ● license change: MIT → non-standard\n  ```\n\n  Relicensing mid-stream (MIT → BUSL, SSPL, \"non-standard\", …) is exactly\n  the kind of thing nobody spots in a 40-line lockfile diff. lockvet only\n  claims a change when the registry reports a license for *both* sides.\n  JSON output carries `old_license` / `new_license` on every covered change.\n\nBoth are gates too: `-fail-on deprecated,license`.\n\n## Versions missing from the registry\n\nWhen an incoming version is **unknown to the registry index** even though\nother versions of the same package are listed, lockvet says so:\n\n```\n+ flatmap-stream 0.1.1  (added)  via event-stream\n    ▲ not in registry index: 0.1.1 unknown to deps.dev though other\n      versions are listed — unpublished/deleted release, or published\n      minutes ago; verify before trusting\n```\n\nWhy this matters: **when a registry pulls a malicious release, this is what\nthe hole looks like.** Every malicious version in\n[our case studies](docs/case-studies.md) — `event-stream@3.3.6`,\n`flatmap-stream@0.1.1`, `chalk@5.6.1`, `ultralytics@8.3.41`, all six\nversions of the August 2026 keyv/Cacheable worm replay — was\nunpublished after the attack, so any lockfile still pinning one references\na version its own registry has disowned. lockvet flags that *without\nneeding an advisory to exist yet*. Here is the Sept 2025 chalk + debug npm\ntakeover replayed during its two-hour live window:\n\n![lockvet replaying the Sept 2025 chalk+debug npm takeover: both malicious versions flagged as not in the registry index](docs/supplychain-demo.gif)\n\nTo keep it honest, the flag is deliberately conservative — it stays silent\nfor:\n\n- packages the registry doesn't index at all (private registries, uncovered\n  ecosystems) — only packages whose *other* versions are listed can be\n  flagged;\n- workspace members, git and path dependencies (the lockfile itself says\n  they don't come from the registry);\n- Go pseudo-versions and pnpm-style decorated version strings.\n\nFor npm, PyPI, crates.io, RubyGems, Packagist, NuGet, Hex, Pub, JSR,\nCocoaPods, Go, Maven, Hackage, Bazel and conda packages — and\nTerraform/OpenTofu providers — the flag is **double-checked against the\nregistry itself**: deps.dev can lag\nthe registries by days, so before claiming anything lockvet fetches the\npackage's real version list from `registry.npmjs.org` / PyPI's simple\nAPI / the crates.io sparse index / the RubyGems compact index /\nPackagist's Composer metadata endpoint / NuGet's registration index /\nhex.pm's and pub.dev's packages APIs / jsr.io's `meta.json` (the same\ndocument Deno itself resolves against; yanks stay listed there, so\nabsence is real signal) / the sharded CocoaPods CDN index\n(the same file `pod install` resolves against) / the Go module proxy /\nthe Terraform registry's per-version endpoint and the OpenTofu registry's\nversion index / the per-version POM on Maven Central (falling back to\nGoogle's Maven repository, where the androidx world lives) / Hackage's\nper-package version map (deprecated versions stay listed there, so\nabsence is real signal) / the Bazel Central Registry's per-module\n`metadata.json` (yanked versions stay listed there — and the registry is\na git repository, versions are added, never silently dropped — so\nabsence is real signal) / anaconda.org's per-release endpoint (channels\npull malicious uploads outright, and marking a build broken keeps it\nlisted, so absence is real signal — claimed only after a HEAD on the\npackage document proves the package itself exists), and\nclears the flag for any version the registry serves. What survives is a\nversion the registry itself no longer lists — and that distinction has\nteeth: on crates.io yanked versions *stay in the index* while deleted\n(malicious) ones vanish entirely, and on RubyGems a yank removes the\nrelease from the index altogether, so a bump onto a yanked or\nadmin-deleted gem keeps the flag (replaying the 2019 `strong_password`\n0.0.7 hijack trips it today). It isn't only malware, either: HashiCorp\npulled AWS provider 5.71.0 from the Terraform registry after a\nregression — the tag still exists on GitHub, and a lockfile pinning\n5.71.0 gets the ▲ flag from the registry's own 404.\n\nNuGet is the one registry where \"unlisted\" is a *native* concept, and\nlockvet splits it the way NuGet does: a stable version **absent from the\nregistration index entirely** — what an admin-deleted (malicious) package\nlooks like — keeps the ▲ flag, while a version its author merely\n[unlisted](https://learn.microsoft.com/en-us/nuget/nuget-org/policies/deleting-packages)\n(hidden from search, still restorable) lands in the deprecation lane\ninstead. Absent *prereleases* are cleared rather than flagged: on NuGet\nthose are overwhelmingly CI-feed daily builds (Roslyn nightlies and\nfriends) that `packages.lock.json` cannot attribute to their real feed.\n\nA release published minutes ago may also not be indexed yet — the flag\ntells you to *look*, not to panic. Gate on it with `-fail-on unlisted`;\nJSON carries `unlisted` / `unlisted_versions`; SARIF emits an\n`unlisted-version` warning; `queue` sorts affected PRs to the top.\n\n## Typosquat suspects\n\nThe oldest trick on every registry: publish `lodahs`, `reqeusts` or\n`rustdecimal` and wait for a typo. lockvet flags **new dependencies whose\nname is one edit away from a popular package** on the same registry — when\nthe release is also young (≤ 30 days) or of unknown age:\n\n```\n+ python3-dateutil 2.9.0  (added)\n    ≈ name resembles python-dateutil: a new dependency one edit away\n      from a popular package, and the release is young — the shape of\n      a typosquat; make sure this is the package you meant\n```\n\nThat example is the real 2019 PyPI attack — and it never got an OSV\nadvisory, so this flag is the only thing that catches it. The 2022\n`rustdecimal` crates.io attack and npm's `lodahs` trip it too.\n\nThe check is **entirely local**: the popular-package lists (npm's\n[high-impact list](https://github.com/wooorm/npm-high-impact), the\n[top PyPI packages](https://github.com/hugovk/top-pypi-packages),\ncrates.io's most-downloaded, RubyGems' most-downloaded (via\n[ecosyste.ms](https://ecosyste.ms)), and Packagist's\n[most popular](https://packagist.org/explore/popular)) are embedded in the\nbinary, so it works with `-offline` and in the browser playground alike.\nThe Ruby side replays the Feb 2020 RubyGems campaign's lead example\n(`rspec-mokcs` for `rspec-mocks` — 760+ malicious gems in one sweep).\nNoise control, as always, is the point:\n\n* only packages **entering** the tree are checked — a bump can't change\n  its name, and a name that has coexisted with its popular neighbour for\n  years is an unfortunate name, not an attack (age gate);\n* name pairs the registry itself treats as the same package never flag\n  (PyPI's `-`/`_`/`.` equivalence, crates.io's `-`/`_` collision ban) —\n  while npm/RubyGems/Packagist separator swaps, which *are* distinct\n  packages, do (`rack_cache` next to `rack-cache` flags);\n* the added package must not itself be on the popular list, and very\n  short names are skipped.\n\nGate on it with `-fail-on typosquat`; acknowledge a deliberate near-name\nwith a `typosquat:pkgname` line in `.lockvetignore`; JSON carries\n`typosquat_of`; SARIF emits a `typosquat-suspect` warning; `queue` sorts\naffected PRs to the top.\n\n## GitHub Actions workflows are lockfiles too\n\nEvery `uses:` line pins a dependency, and Dependabot/Renovate bump those\npins like any other. lockvet reads workflow files —\n`.github/workflows/*.yml`, composite `action.yml` files, Gitea/Forgejo\nworkflow dirs too — in every mode: `lockvet pr <url>`, local diffs,\n`queue`, `audit`. Action bumps get context no plain diff shows:\n\n- **SHA pins resolve to releases.** lockvet fetches the action\n  repository's tags over anonymous git smart-HTTP (one GET per repo, no\n  API, no rate limits) and reports the release each commit stands for —\n  Renovate digest bumps become readable:\n\n  ```console\n  .github/workflows/build-container-image.yml (GitHub Actions)\n    ↑ actions/checkout df4cb1c (=v6.0.3) → d23441a (=v6.1.0)  minor  (direct)\n  ```\n\n- **Floating majors resolve too.** `v4` is reported as the release it\n  points at *today*, and the jump is classified from the real versions —\n  so `v5 → v7` shows as `v5 (=v5.1.0) → v7 (=v7.0.1)  MAJOR`, with the\n  verified compare link.\n- **Advisories, evaluated properly.** OSV.dev has a \"GitHub Actions\"\n  ecosystem but its API cannot match versions against those advisories\n  server-side — lockvet fetches the affected ranges and evaluates them\n  itself, against the *resolved* release. A SHA pin affected by a GHSA is\n  caught even though no advisory ever names that hash, and a floating\n  `v4` isn't false-flagged for an advisory fixed inside the major.\n- **▲ not a release.** A pinned commit that matches no tag in the\n  action's repository (and isn't a branch or its head). That is exactly\n  what the March 2025 [tj-actions/changed-files\n  attack](https://github.com/tj-actions/changed-files/issues/2463) looked\n  like: version tags across the repo were force-moved to a malicious\n  orphan commit. Replaying the attacked pin today:\n\n  ```console\n  $ lockvet diff old/ci.yml new/ci.yml\n\n  new/ci.yml (GitHub Actions)\n    ↑ tj-actions/changed-files v44 (=v44.0.0) → 0e58ed8  ?  (direct)\n        ▲ not a release: 0e58ed8 pinned ref matches no tag in the action's repository — release\n          tags are how actions ship, and the tj-actions attack pinned exactly like this; verify the commit\n  ```\n\n  (`0e58ed8` is the actual malicious commit from that attack.)\n  `-fail-on unlisted` gates on it in CI.\n\nBranch pins (`@main`) are a deliberate choice and stay quiet, actions\nliving outside github.com make no unlisted claims, and `-changelogs`\nshows the release notes of every action release a bump pulls in.\n\n### …and so is `.pre-commit-config.yaml`\n\nEvery `repos:` entry pins a hook repository at an exact `rev:` — code that\npre-commit clones and **runs on every commit on every contributor's\nmachine**. `pre-commit autoupdate` and Renovate bump these pins like any\nother dependency, and they get the same treatment as workflow pins, on any\ngit forge (names keep their host):\n\n```console\n$ lockvet HEAD~1   # after a `pre-commit autoupdate` commit\n\n.pre-commit-config.yaml (pre-commit)\n  ↑ github.com/astral-sh/ruff-pre-commit v0.8.0 → v0.14.14  minor  (direct)\n  ↑ github.com/pre-commit/pre-commit-hooks v4.4.0 → v5.0.0  MAJOR  (direct)\n```\n\nSHA revs are resolved to the release they equal, jumps are classified from\nthe real versions, `-changelogs` pulls each hook's release notes, and a rev\nthat matches no tag in the hook's repository raises **▲ not a release** —\nthe same signal that catches the tj-actions attack shape, for the code your\nwhole team runs before every commit. `repo: local` and `repo: meta` entries\nare exempt, and `lockvet pkg pre-commit:owner/repo` vets a hook repo before\nyou add it.\n\n### …and `.gitlab-ci.yml`\n\nGitLab pipelines pin dependencies in two places, and lockvet reads both —\nin `.gitlab-ci.yml`, suffix-named variants (`backend.gitlab-ci.yml`), and\nCI fragments under `.gitlab/` or `.gitlab-ci/` directories:\n\n- **`include: component:` pins** — [CI/CD Catalog\n  components](https://docs.gitlab.com/ci/components/) whose configuration\n  runs in every pipeline. Each pin is verified against the component\n  project's real tags, and GitLab's floating forms resolve to the release\n  they mean *today*: `@2` and `@2.0` (semver range shorthands) and\n  `@~latest` all show the concrete version, so a bump like `@2 → @~latest`\n  is readable instead of opaque:\n\n  ```console\n  .gitlab-ci.yml (Docker)\n    ↑ gitlab.com/components/opentofu/full-pipeline            2.0.0      → 2.4.0  minor  (direct)\n    ↑ gitlab.com/components/secret-detection/secret-detection 2 (=2.3.0) → ~latest (=2.3.0)    (direct)\n  ```\n\n  A pinned version that matches no tag in the component's project raises\n  **▲ not a release** — the tj-actions attack shape, for GitLab. Compare\n  links use the project's real tags, and `-changelogs` renders the\n  component's `CHANGELOG.md` sections for exactly the versions the bump\n  pulls in (GitLab-hosted repos included). `lockvet pkg\n  component:gitlab.com/components/opentofu/full-pipeline` vets one before\n  you include it.\n- **`image:` and `services:` refs** — the containers every job runs in\n  get the same registry verification as Dockerfile `FROM` pins (digest\n  vs. tag, unknown tags, Docker Hub ages). Renovate bumps both kinds;\n  nothing else vets either.\n\n`include: pr",
  "bytes": 60000,
  "sha": "97d3cd7ccedb373d1012319def1ff8da7af2fddc0fe7817181ba02228f5bbaf2",
  "repo_slug": "matteo-sung/lockvet",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_matteo_sung_lockvet_d6ff55b5/readme"
}