Check a Go or npm project for vulnerable dependencies and denied licenses, against a policy you keep in the repository. Run it from the command line, in CI, from a local dashboard, or on every pull request.
$ depscan -path ./shop-api -policy policy.yaml
Dependencies: 22
Findings: 9
Violations: 7
Warnings: 3
❌ 7 policy violation(s) — scan failed:
[BLOCK] [vulnerability] golang.org/x/crypto@0.14.0
GHSA-v778-237x-gjrc (CRITICAL) — … — upgrade to 0.31.0
- Reads the lockfiles. Every
go.modandpackage-lock.json(npm 7 or newer) in the folder and its subfolders..git,vendorandnode_modulesare skipped. depscan never runsgoornpm. - Looks up licenses for every package version on deps.dev.
- Checks advisories for every package version on OSV.dev, merging records that describe the same flaw under different IDs.
- Applies your policy (
policy.yaml): what blocks, what warns, which licenses are denied, and which advisories you accepted on purpose.
If OSV.dev cannot be reached after retries, the scan fails. It is never reported as a pass with missing data.
Open https://depscan-server.thankfulbeach-61661ef7.malaysiawest.azurecontainerapps.io
and choose a project folder. Only its go.mod and package-lock.json files
are sent, and a scan's link keeps working for 30 days. The site sleeps when
nobody uses it, so the first visit can take a few seconds.
Download the binary for your system from
Releases; each archive comes
with the default policy.yaml. Or install it with Go 1.27 or newer and take
policy.yaml from this repo:
go install github.com/depscan-go/depscan/cmd/depscan@latest
depscan -path ./my-project -policy policy.yamlFrom a clone, go run ./cmd/depscan works the same way.
| Exit status | Meaning |
|---|---|
| 0 | No violations (warnings are allowed) |
| 1 | At least one violation: fail the build |
| 2 | The scan could not run (bad policy, unreadable folder, OSV.dev down) |
From a clone, with Go 1.27 or newer:
make run-server # go run ./cmd/server --policy=./policy.yamlOpen http://127.0.0.1:8080 and choose a project folder, or drop one on the page. The browser finds the lockfiles and sends only those to the server on your computer. Results, history and license reviews stay in your browser.
Scanning by typed path only works inside the server's scan root
(-scan-root, your home folder by default); choosing a folder works for any
folder on any drive.
policy.yaml is commented; the short version:
| Key | Meaning |
|---|---|
block_severities |
Advisory severities that fail the scan, e.g. [CRITICAL, HIGH] |
warn_severities |
Severities that are shown but do not fail it |
block_unknown_severity |
Fail on advisories that have no severity |
check_transitive |
Hold packages you did not add yourself to the same rules |
denied_licenses |
SPDX IDs you cannot ship. MIT OR GPL-3.0-only passes; MIT AND GPL-3.0-only does not |
block_unknown_license |
Fail, instead of warn, when deps.dev has no license |
skip_license_check |
No license lookups (offline runs) |
exceptions |
Advisories accepted on purpose: id, a required reason, an optional expires date |
fail_fast, max_violations |
Stop early |
Commit the same file your CI uses, so a laptop and CI give the same answer.
depscan -path . -report report.cdx.jsonwrites a deterministic CycloneDX 1.5 SBOM and, next
to it, report.cdx.json.sha256 in the format sha256sum -c reads.
A checksum file next to the report does not stop someone from changing both. Anchoring writes the report's SHA-256 into the memo of a Solana transaction, which the ledger timestamps and nobody can edit later:
depscan keygen # once: creates ~/.config/solana/id.json
# fund the address it prints with free devnet SOL: https://faucet.solana.com
depscan -path . -report report.cdx.json -anchor solanaThis saves report.cdx.json.anchor.json with the network, transaction ID,
signer and time. Only the 64-character fingerprint is published, as the memo
depscan:v1:sha256:<hex>; your report and package list are not. One anchor
costs 0.000005 SOL.
| Variable | Default |
|---|---|
DEPSCAN_SOLANA_RPC |
devnet; also testnet, mainnet-beta, or any RPC URL |
DEPSCAN_SOLANA_KEYPAIR |
~/.config/solana/id.json (the Solana CLI's keypair works too) |
DEPSCAN_SOLANA_KEY |
The secret key itself, for servers: base58 (as Phantom exports it) or the JSON array solana-keygen writes. Use this or DEPSCAN_SOLANA_KEYPAIR, not both |
depscan verify report.cdx.jsonre-hashes the report and checks it against report.cdx.json.sha256 and, if
it was anchored, against the transaction on the ledger. Verifying needs no
wallet. Add -json for CI, or -offline to check only the local files.
Lost the receipt? The ledger still has the anchor:
depscan verify -signer <address> -save report.cdx.json searches that
address's transactions for the report's fingerprint and writes the receipt
back.
| Exit status | Meaning |
|---|---|
| 0 | Verified |
| 1 | The report was changed, or the ledger does not hold its fingerprint |
| 2 | Could not check: no checksum or receipt, or the ledger was unreachable |
A match proves the report has not changed since it was anchored. It does not prove the project is still safe: advisories are published every day, so scan again for a current answer.
The server can scan every pull request at its latest commit and report back: a commit status (pending, then success, failure or error) and one comment listing what blocks the merge and the version that fixes it. A later push edits that comment instead of adding another.
-
Start the server where GitHub can reach it, with:
Variable Purpose DEPSCAN_WEBHOOK_SECRETTurns webhooks on; GitHub signs every delivery with it GITHUB_TOKENReads lockfiles of private repositories and posts results DEPSCAN_PR_FEEDBACKstatusfor the status only,offfor neither (default: both)DEPSCAN_PUBLIC_URLThe dashboard's public address, for the status's Details link A fine-grained token needs Contents: read, Commit statuses: read and write and Pull requests: read and write on the repository.
-
In the repository's Settings → Webhooks, add
https://<your server>/webhooks/github, content typeapplication/json, the same secret, and the Pull requests event. -
Optional: in branch protection, require the
depscanstatus to pass.
Only the lockfiles at the pull request's head commit are downloaded.
Set DEPSCAN_ANCHOR=solana and DEPSCAN_SOLANA_KEY (devnet by default) and
the server also builds each pull request scan's CycloneDX report, anchors its
SHA-256, and puts the transaction in the pull request comment. The report is
kept at GET /scans/{id}/report (see "Saving scans"). The server logs the
wallet's address and balance at startup.
The proof doesn't depend on the server: GitHub keeps the comment, the ledger
keeps the anchor, and depscan verify -signer <address> finds the anchor of
any copy of the report. Anchoring never changes a scan's result; if it fails,
the comment says why.
Set DATABASE_URL to a Postgres connection string and the server keeps every
scan there, so its link keeps working after a restart:
- dashboard uploads and folder scans for 30 days
- pull request scans, with their reports, until the saved scans pass 700 MB; then the oldest go first, uploads before pull request scans
Without it, scans are kept in memory until the server restarts. If the
database can't be reached, scans still run and return their result; only
their links won't work, and the log says store: … failed.
GET /scans lists the newest 300 scans only when the server listens on
127.0.0.1, ::1 or localhost. Anywhere else, including docker compose
(which listens on :8080 inside its container), a scan opens only through
its own link.
| Method and path | |
|---|---|
GET / |
The dashboard |
GET /healthz |
Liveness |
POST /scans |
Scan a folder inside the scan root: {"path": "..."} |
POST /scans/upload |
Scan lockfiles sent by the dashboard (gzip JSON, 1 MB limit) |
GET /scans |
The newest 300 scans, on a local server only (see "Saving scans") |
GET /scans/{id} |
One scan's result |
GET /scans/{id}/report |
The CycloneDX report of an anchored pull request scan |
GET /policy |
The loaded policy, read-only |
POST /webhooks/github |
GitHub webhook deliveries, when enabled |
A scan answers 200 when it passes and 422 when the policy is violated, with the same body either way.
- Package names and versions go to OSV.dev and deps.dev.
- With the dashboard, only
go.modandpackage-lock.jsonfiles go from the browser to the depscan server you run. - With anchoring, only the report's SHA-256 goes to the Solana network.
- The depscan server keeps each scan's result: with
DATABASE_URL, in its database for 30 days (pull request scans until space runs short); otherwise in memory until it restarts.
make test # go test -race ./...
make lint # golangci-lint run ./...The store's Postgres tests run when DEPSCAN_TEST_DATABASE_URL points at a
throwaway database (CI starts one):
docker run -d --rm --name depscan-test-pg -e POSTGRES_PASSWORD=depscan -p 55432:5432 postgres:18-alpine
DEPSCAN_TEST_DATABASE_URL='postgres://postgres:depscan@127.0.0.1:55432/postgres?sslmode=disable' make test| Package | |
|---|---|
internal/parser |
go.mod and package-lock.json parsing |
internal/license, internal/osv |
deps.dev and OSV.dev clients |
internal/policy |
Loading and applying policy.yaml |
internal/scan |
The scan engine that ties them together |
internal/report |
CycloneDX reports and their SHA-256 |
internal/anchor |
Anchoring backends (solana, noop) and receipts |
internal/github |
Webhooks, lockfile fetching, status and comment |
internal/store |
Where scans are kept: memory, or Postgres with DATABASE_URL |
internal/api |
HTTP handlers and the embedded dashboard |