OSV-Scanner on Ubuntu 24.04 on Azure User Guide
Overview
OSV-Scanner is Google's open-source scanner for the OSV.dev vulnerability database. It reads the dependency manifests and lockfiles already in your projects - requirements.txt, package-lock.json, go.mod, pom.xml, Cargo.lock, Gemfile.lock, packages.lock.json, composer.lock and many more - and reports every known vulnerability affecting the exact versions you have pinned, with the fixed version for each.
The cloudimg image is a ready-to-run dependency-scanning host, not just the binary. The OSV database is baked into the image, so your very first scan is instant and runs with no network egress at all - which means this VM can sit in a locked-down subnet with no outbound internet and still scan. A daily timer refreshes the database and a second daily timer scans your projects, writing machine-readable JSON and SARIF reports for CI pickup. Backed by 24/7 cloudimg support.
What is included:
- OSV-Scanner 2.4.0 (
/usr/local/bin/osv-scanner), installed from the official Google release binary and verified against the upstream SHA256 checksum - A baked offline OSV database (~338 MB) at
/var/lib/osv-scanner/dbcovering 10 ecosystems: PyPI, npm, Go, Maven, crates.io, RubyGems, NuGet, Packagist, Alpine and Debian OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORYexported for every login shell, soosv-scanneruses the offline database automaticallyosv-db-refresh+osv-db-refresh.timer- daily database refresh, ecosystems configurableosv-scan-run+osv-scan.timer- daily scan of your projects, writing JSON + SARIF withlatest.*symlinks- Read-only self-test fixtures under
/usr/share/osv-scanner/fixturesso you can prove the scanner works on your own VM - 24/7 cloudimg support
This is a command-line product: no web UI, no listening service, no admin account and no password. SSH on port 22 is the only open port, and the SSH key you choose at launch is the only way in.
Prerequisites
An active Azure subscription, an SSH key pair, and a VNet + subnet in the target region. Standard_B2s (2 vCPU / 4 GiB RAM) is a good starting point; the offline database needs roughly 1 GB of free disk on top of the base OS. NSG inbound: allow 22/tcp from your management network. No inbound application ports are needed, and no outbound internet is needed for scanning.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for OSV-Scanner by cloudimg, and select Create. On Basics pick your subscription, resource group, region and size; under Administrator account choose SSH public key and paste your key; under Inbound port rules allow SSH (22). Then Review + create -> Create.
Step 2 - Deploy from the Azure CLI
az vm create \
--resource-group <your-rg> \
--name osv-scanner \
--image <marketplace-image-urn> \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>
There is no password and no console to log in to - this product has no credentials at all. /etc/osv-scanner/credentials on the VM is an informational note that says exactly that, and points at the paths below.
Step 4 - Confirm OSV-Scanner is installed
osv-scanner --version
osv-scanner version: 2.4.0
osv-scalibr version: 0.4.5
commit: b56b5191101d5f27d4787d5583d8d01e9518a7af
built at: 2026-06-18T12:55:27Z

Step 5 - Confirm the offline database is baked in
The database ships inside the image, so there is nothing to download before your first scan:
du -sh /var/lib/osv-scanner/db
ls /var/lib/osv-scanner/db/osv-scanner
338M /var/lib/osv-scanner/db
Alpine
Debian
Go
Maven
NuGet
Packagist
PyPI
RubyGems
crates.io
npm
The scanner finds this database automatically, in every execution context - an interactive shell, sudo, a cron job, a systemd unit or a CI runner - with no environment setup on your part. /etc/profile.d/osv-scanner.sh exports the cache directory for login shells:
grep OSV_SCANNER /etc/profile.d/osv-scanner.sh
export OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY="/var/lib/osv-scanner/db"
...and because a profile.d export only reaches login shells, /usr/local/bin/osv-scanner is a small cloudimg wrapper that resolves the same path unconditionally before handing off to the upstream binary at /usr/local/lib/osv-scanner/osv-scanner. That is what makes non-interactive use work out of the box:
env -i /usr/bin/env PATH=/usr/local/bin:/usr/bin:/bin \
osv-scanner scan source --offline-vulnerabilities \
--lockfile requirements.txt:/usr/share/osv-scanner/fixtures/clean/requirements.txt \
>/dev/null 2>&1; echo "scrubbed-environment exit: $?"
scrubbed-environment exit: 0
Setting OSV_SCANNER_LOCAL_DB_CACHE_DIRECTORY yourself always overrides the wrapper, so you can point at a different database if you need to.
Step 6 - Run the built-in self-test
The image ships two read-only fixtures so you can prove the scanner works before you point it at anything of your own. The first pins a deliberately vulnerable urllib3==1.26.4:
osv-scanner scan source --offline-vulnerabilities \
--lockfile requirements.txt:/usr/share/osv-scanner/fixtures/vulnerable/requirements.txt
echo "exit: $? (1 = vulnerabilities found, which is the expected result here)"
Real output from this image - note Loaded PyPI local db from ..., which is the proof the scan ran entirely offline:
Scanned /usr/share/osv-scanner/fixtures/vulnerable/requirements.txt file and found 1 package
Loaded PyPI local db from /var/lib/osv-scanner/db/osv-scanner/PyPI/all.zip
Total 1 package affected by 9 known vulnerabilities (0 Critical, 6 High, 3 Medium, 0 Low, 0 Unknown) from 1 ecosystem.
9 vulnerabilities can be fixed.
+-------------------------------------+------+-----------+---------+---------+---------------+
| OSV URL | CVSS | ECOSYSTEM | PACKAGE | VERSION | FIXED VERSION |
+-------------------------------------+------+-----------+---------+---------+---------------+
| https://osv.dev/PYSEC-2021-108 | 8.7 | PyPI | urllib3 | 1.26.4 | 1.26.5 |
| https://osv.dev/GHSA-q2q7-5pp4-w6pg | | | | | |
| https://osv.dev/PYSEC-2023-192 | 8.1 | PyPI | urllib3 | 1.26.4 | 1.26.17 |
| https://osv.dev/GHSA-v845-jxx5-vc9f | | | | | |
| https://osv.dev/PYSEC-2026-1995 | 4.4 | PyPI | urllib3 | 1.26.4 | 1.26.19 |
| https://osv.dev/GHSA-34jh-p97f-mpxf | | | | | |
+-------------------------------------+------+-----------+---------+---------+---------------+

The second fixture pins a clean six==1.17.0 and must report nothing:
osv-scanner scan source --offline-vulnerabilities \
--lockfile requirements.txt:/usr/share/osv-scanner/fixtures/clean/requirements.txt
Scanned /usr/share/osv-scanner/fixtures/clean/requirements.txt file and found 1 package
Loaded PyPI local db from /var/lib/osv-scanner/db/osv-scanner/PyPI/all.zip
No issues found
Step 7 - Scan your own projects
Put your checkouts under /srv/projects (or point the configuration at wherever they already are) and scan the tree:
osv-scanner scan source --offline-vulnerabilities -r /srv/projects
-r walks the directory recursively and picks up every manifest and lockfile it recognises. To scan a single lockfile, use --lockfile <type>:<path> as in Step 6.
Step 8 - Scheduled scans and machine-readable reports
osv-scan-run is the scheduled entry point. It reads /etc/osv-scanner/scan.conf, scans SCAN_TARGET offline, and writes both a JSON and a SARIF report with latest.* symlinks.
Your VM ships with /srv/projects empty, so a scan run right now would correctly report 128 (nothing to scan). Drop a project in and run it - here we use the shipped vulnerable fixture as a stand-in for one of your own checkouts:
sudo install -d /srv/projects/demo
sudo cp /usr/share/osv-scanner/fixtures/vulnerable/requirements.txt /srv/projects/demo/
sudo osv-scan-run
echo "exit: $? (1 = vulnerabilities found)"
[2026-07-19T11:57:58+0000] osv-scan-run: scanning /srv/projects offline against /var/lib/osv-scanner/db
Scanned /srv/projects/demo/requirements.txt file and found 1 package
Loaded PyPI local db from /var/lib/osv-scanner/db/osv-scanner/PyPI/all.zip
[2026-07-19T11:58:01+0000] osv-scan-run: VULNERABILITIES FOUND - see /var/lib/osv-scanner/reports/osv-scan-20260719T115758Z.json
[2026-07-19T11:58:01+0000] osv-scan-run: reports: ... (latest.json / latest.sarif updated)
exit: 1 (1 = vulnerabilities found)
The reports directory keeps dated files plus stable symlinks your CI can always read:
ls -l /var/lib/osv-scanner/reports/latest.json /var/lib/osv-scanner/reports/latest.sarif
SARIF is the format GitHub code scanning, Azure DevOps and most IDEs consume directly, so latest.sarif can be uploaded as-is.
Both timers are enabled and armed out of the box:
systemctl list-timers osv-db-refresh.timer osv-scan.timer --no-pager
NEXT LEFT UNIT ACTIVATES
Mon 2026-07-20 00:07:28 UTC 14h osv-db-refresh.timer osv-db-refresh.service
Mon 2026-07-20 00:27:19 UTC 14h osv-scan.timer osv-scan.service

Step 9 - The exit-code contract (CI integration)
osv-scanner and osv-scan-run both follow the documented contract, which is what makes this usable as a pipeline gate:
| Exit code | Meaning |
|---|---|
0 |
No vulnerabilities found |
1 |
Vulnerabilities found |
128 |
No packages found (nothing to scan, or an unsupported project layout) |
osv-scanner scan source --offline-vulnerabilities \
--lockfile requirements.txt:/usr/share/osv-scanner/fixtures/clean/requirements.txt \
>/dev/null 2>&1; echo "clean fixture exit: $?"
clean fixture exit: 0
Treat 128 explicitly in CI - it means the scan found nothing to look at, which is usually a path or layout problem rather than a clean result.

Configuration
Everything lives in /etc/osv-scanner/scan.conf:
grep -v '^#' /etc/osv-scanner/scan.conf | grep .
SCAN_TARGET="/srv/projects"
REPORT_DIR="/var/lib/osv-scanner/reports"
OSV_DB_DIR="/var/lib/osv-scanner/db"
OSV_ECOSYSTEMS="PyPI npm Go Maven crates.io RubyGems NuGet Packagist Alpine Debian"
REPORT_RETENTION_DAYS=30
SCAN_TARGET- the directory the daily scan walks. Point it at your checkouts.OSV_ECOSYSTEMS- which ecosystems are kept offline. See "Adding ecosystems" below.REPORT_RETENTION_DAYS- dated reports older than this are pruned.
After editing, restart the timer:
sudo systemctl restart osv-scan.timer
Adding ecosystems to the offline database
Ten ecosystems are baked in by default. The full list of ecosystems OSV publishes is at osv-vulnerabilities.storage.googleapis.com/ecosystems.txt. Ubuntu is deliberately not cached by default because it alone is roughly 535 MB - add it (or any other ecosystem) if you need it:
sudo sed -i 's/^OSV_ECOSYSTEMS=.*/OSV_ECOSYSTEMS="PyPI npm Go Maven crates.io RubyGems NuGet Packagist Alpine Debian Ubuntu"/' /etc/osv-scanner/scan.conf
sudo osv-db-refresh
osv-db-refresh downloads only the ecosystems listed in the configuration, so adding one does not re-download the rest.
Keeping the database current
The database is baked at build time so your first scan works immediately and offline. It is then refreshed at first boot and daily by osv-db-refresh.timer, so a VM launched months after the image was published is never scanning against a stale image-time database.
The first-boot refresh is deliberately network-tolerant: if your subnet has no outbound internet it logs the failure and moves on rather than failing the boot, because the baked database still makes the scanner fully useful. To refresh on demand:
sudo osv-db-refresh
Maintenance
- Database: refreshed daily by
osv-db-refresh.timer; force one withsudo osv-db-refresh. - Scans: run daily by
osv-scan.timer; run one on demand withsudo osv-scan-run. Inspect history withjournalctl -u osv-scan.service. - Upgrades: download a newer release binary from the OSV-Scanner releases, verify it against the published
SHA256SUMS, and replace/usr/local/bin/osv-scanner. - Security patches: unattended-upgrades remains enabled, so the OS continues to receive security updates automatically.
- Air-gapped use: nothing about scanning requires egress. Only
osv-db-refreshdoes, and it fails soft.
Support
cloudimg provides 24/7 expert support for this image. Contact support@cloudimg.co.uk.