Security audit
Offline-first vulnerability scanning with vis audit — sync the advisory cache, run audits, apply fixes, gate CI.
Security audit
vis audit is an offline-first vulnerability scanner. It walks the installed package set (resolved from the lockfile, no node_modules traversal) and matches every entry against an OSV advisory database held locally. When the local cache is present, no network access is required at scan time — making it suitable for hermetic CI, airgapped environments, and pre-release gates.
This guide covers:
- Populating the offline cache.
- Running an audit and choosing an output format.
- Apply loops — patching direct and transitive vulnerabilities.
- CI integration.
- Accepted-risk workflow.
1. Populate the offline cache
The cache lives at <workspace>/node_modules/.cache/vis/advisories/db.sqlite (resolved via @visulima/find-cache-dir). Run sync once at install time:
vis advisories syncThis downloads the OSV all.zip dump for the npm ecosystem (~5 MB compressed) and ingests it into a SQLite cache (~12 MB on disk). The next sync short-circuits via the upstream ETag header if the file hasn't changed.
# CI / scheduled refresh
vis advisories sync # ETag-aware; no work if unchanged
vis advisories sync --force # force re-download
# Verify freshness in CI
vis advisories status --format json | jq -e '.ecosystems[0].lastSyncIso'See vis advisories for full reference.
2. Run an audit
vis audit # human-readable table
vis audit --severity high # only show high/critical
vis audit --prod-only # skip devDependencies
vis audit --usage # reachability filter — only statically-imported packages
# For non-human consumers
vis audit --format json
vis audit --format sarif # GitHub / GitLab code-scanning uploads
vis audit --format csaf # CSAF 2.0 csaf_vex profile
vis audit --format cyclonedx-vex # CycloneDX 1.7 SBOM + VEX
vis audit --format gitlab # GitLab dependency-scanning report (Secure stage)
vis audit --format junit # JUnit XML (Surefire-compatible)
vis audit --report ./report.html # self-contained HTMLReachability
--usage (or security.policies.vulnerability.usage.enabled in config) filters findings to only those packages that are statically imported somewhere in your source tree. Dynamic require() calls behind variable names are conservatively treated as reachable.
This is a noise reducer — vulnerabilities in unused devDependencies stay out of your dashboard.
3. Fix loops
vis audit ships two fix loops. Both default to a dry-run preview and prompt before changing anything.
--fix — direct-dep upgrades
When the vulnerable package is declared in your package.json (or any workspace package.json), --fix upgrades it by calling the active package manager's update command.
vis audit --fix # interactive preview + prompt
vis audit --fix --yes # CI — skip prompt
vis audit --fix --allow-major # permit major-version bumpsThe planner classifies each fix as in-range (caret bump that satisfies the existing range) or major (the fix is outside the range — requires --allow-major). Findings that don't match any manifest are reported under "Transitive only" — those belong to the second loop.
--fix-transitive — PM-specific overrides
For transitive vulnerabilities (the vulnerable package isn't in your manifest — it's pulled in by something else), --fix-transitive writes a pinned override into the appropriate surface for your package manager:
| Package manager | Surface |
|---|---|
| pnpm v10+ | pnpm-workspace.yaml#overrides |
| pnpm v9 | package.json#pnpm.overrides |
| npm | package.json#overrides |
| Bun | package.json#overrides |
| Yarn | package.json#resolutions |
The writer is atomic (.tmp sibling + renameSync) and idempotent — re-running with the same plan is a no-op.
In CI, --fix-transitive is gated by two locks:
--yeson the CLI.security.audit.apply.transitive.enabled = trueinvis.config.ts.
This intentional friction prevents a misconfigured pipeline from rewriting your override surface every PR.
vis audit --fix-transitive # interactive preview
vis audit --fix-transitive --yes # CI — requires config flag too4. CI integration
A typical pipeline:
# .github/workflows/audit.yml
- run: pnpm install --frozen-lockfile
- run: pnpm exec vis advisories sync
- run: pnpm exec vis audit --offline --format sarif > audit.sarif
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: audit.sarif
# Hard gate the build on high+ findings
- run: pnpm exec vis audit --offline --fail-on highGitLab CI
The gitlab and junit formats land artifacts that GitLab's Secure stage and merge-request widgets pick up natively:
# .gitlab-ci.yml
audit:
image: node:24
script:
- pnpm install --frozen-lockfile
- pnpm exec vis advisories sync
- pnpm exec vis audit --offline --format gitlab > gl-dependency-scanning.json
- pnpm exec vis audit --offline --format junit > junit.xml
- pnpm exec vis audit --offline --fail-on high # hard gate
artifacts:
reports:
dependency_scanning: gl-dependency-scanning.json
junit: junit.xmlThe --severity flag is the equivalent of osv2gitlab's --threshold: vis audit --severity high --format gitlab keeps only high/critical findings in the GitLab report.
Exit-code semantics
| Flag | Exit behaviour |
|---|---|
| none | Always exit 0. Audit is informational by default. |
--exit-code | Exit 1 if any actionable finding is present or the policy engine emits a block decision. |
--fail-on <level> | Exit 1 when a vulnerability meets <level> or higher, or when a block policy fires. |
Acknowledged risks are excluded from the gate unless --show-accepted is set.
5. Supply-chain policy engine
Beyond vulnerability findings, vis audit runs a configurable supply-chain
policy engine inspired by Socket.dev. Each policy maps
to a security.policies.<name> field in vis.config.ts; leaving a field
unset disables the policy entirely.
This release ships four offline-clean policies:
license— SPDX allow / deny lists. Deny wins on any sub-expression match in an SPDX expression ((MIT OR GPL-3.0)is blocked whenGPL-3.0is on the deny list).installScripts— surfaces packages declaring lifecycle scripts (preinstall,install,postinstall,prepare, or an implicitnode-gyp rebuildfrom abinding.gyp).strict: trueturns unapproved scripts into block-severity decisions.vulnerability— OSV findings, gated by the existingfailOn/usageknobs. Findings now also flow through the unified policy report and the JSON / SARIF / HTML formatters.unexpectedDeps— flags packages that aren't on a static allow-list, or that didn't exist in a saved baseline lockfile.
Network-bound policies (malware, firstSeen, publisherChange,
score) plug into the same engine in a follow-up commit; until then
they're surfaced via the existing Socket-direct integration.
CLI surface
| Flag | Behaviour |
|---|---|
--policies <list> | Narrow this run to a subset of policies. Comma list. Accepts camelCase or snake_case names. |
--policies all | Force every known policy regardless of config. |
--policies none | Skip the engine entirely. |
| no flag | Evaluate every policy that has a configured security.policies.<name> entry. |
Accepted risks per policy
security.acceptedRisks is shared by every policy. Use the optional
policies array to scope an acceptance to a specific gate, and
expiresAt to set a sunset date:
acceptedRisks: {
"legacy-thing": {
reason: "Vendor refuses to relicense; replacing in Q3",
acceptedAt: "2026-01-01",
expiresAt: "2026-09-30",
policies: ["license"],
},
}5. Accepted-risk workflow
Some vulnerabilities are unfixable, vendor-disputed, or out of scope. Document them in vis.config.ts:
export default defineConfig({
security: {
socket: {
acceptedRisks: [
{
name: "left-pad",
reason: "Vendored fork — upstream advisory does not apply",
expiresAt: "2026-12-31",
},
],
},
},
});The acceptance is workspace-local. To mirror it into the native package-manager config (so other tooling — Snyk, Renovate, Dependabot — picks it up), run:
vis audit --syncThis writes the accepted risks into the appropriate PM config (pnpm-workspace.yaml's auditConfig, or Yarn's .yarnrc.yml).
--show-accepted re-includes them in audit output so you can review what's been silenced.
6. Security providers
vis ships a pluggable provider interface for the network-bound supply-chain signals that complement the offline OSV audit. Each provider returns a per-package report (score axes + alerts); when more than one is enabled, results are merged rather than replaced.
| Provider | Auth | Strengths |
|---|---|---|
Socket.dev (socket) | Public token / VIS_SOCKET_TOKEN | Five-axis package score (quality, maintenance, supply chain, license, vulnerability), curated alerts (malware, install scripts, telemetry, typosquats), version-scoped data. |
Google deps.dev (deps-dev) | None | OpenSSF Scorecard signals (16 automated checks) + GHSA advisories, no API token, generous rate limits, completely free. |
// vis.config.ts
import { defineConfig } from "@visulima/vis/config";
export default defineConfig({
security: {
socket: { enabled: true }, // requires VIS_SOCKET_TOKEN
depsDev: { enabled: true }, // no auth — safe to enable in CI
// Which provider's `score` wins on conflict. Alerts from the other
// provider are still appended and deduped by `key`.
primaryProvider: "socket",
},
});Provider merge semantics
When both providers return data for the same package:
- Score — the primary provider's score axes win wholesale. The secondary's score is discarded (it's a different rubric and averaging the two would be meaningless).
- Alerts — concatenated and deduped by
key. Cross-provider duplicates collapse to the first occurrence. - License / author / size — primary wins; secondary fills in only when primary is missing the field.
- Failures are isolated — a provider that times out or errors contributes an empty map. The remaining providers still report.
Per-run escape hatches
| Env var | Effect |
|---|---|
MARSHALL_DISABLE_SOCKET=1 | Skip the Socket.dev provider for the current process. |
MARSHALL_DISABLE_DEPS_DEV=1 | Skip the deps.dev provider for the current process. |
MARSHALL_DISABLE_ALL=1 | Skip every marshall-gated check (including providers). |
--offline on vis audit | Disables both providers for this command only. |
Cache locations
Each provider writes a JSON-on-disk cache under ~/.vis/cache/:
socket-security/— Socket.dev reports (default TTL: 1 hour)deps-dev/— deps.dev version + project + advisory entries (default TTL: 24h for project data, 7d for version and advisory data — those are immutable once published)
Clear with vis cache clean --type=socket or vis cache clean --type=deps-dev. vis cache clean (no flag) clears every store.
Reference
vis audit— full flag reference.vis advisories— cache management.- vis MCP tools —
auditandadvisory_statusare exposed for AI agents.