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:

  1. Populating the offline cache.
  2. Running an audit and choosing an output format.
  3. Apply loops — patching direct and transitive vulnerabilities.
  4. CI integration.
  5. 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 sync

This 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 HTML

Reachability

--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 bumps

The 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 managerSurface
pnpm v10+pnpm-workspace.yaml#overrides
pnpm v9package.json#pnpm.overrides
npmpackage.json#overrides
Bunpackage.json#overrides
Yarnpackage.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:

  1. --yes on the CLI.
  2. security.audit.apply.transitive.enabled = true in vis.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 too

4. 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 high

GitLab 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.xml

The --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

FlagExit behaviour
noneAlways exit 0. Audit is informational by default.
--exit-codeExit 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 when GPL-3.0 is on the deny list).
  • installScripts — surfaces packages declaring lifecycle scripts (preinstall, install, postinstall, prepare, or an implicit node-gyp rebuild from a binding.gyp). strict: true turns unapproved scripts into block-severity decisions.
  • vulnerability — OSV findings, gated by the existing failOn / usage knobs. 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

FlagBehaviour
--policies <list>Narrow this run to a subset of policies. Comma list. Accepts camelCase or snake_case names.
--policies allForce every known policy regardless of config.
--policies noneSkip the engine entirely.
no flagEvaluate 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 --sync

This 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.

ProviderAuthStrengths
Socket.dev (socket)Public token / VIS_SOCKET_TOKENFive-axis package score (quality, maintenance, supply chain, license, vulnerability), curated alerts (malware, install scripts, telemetry, typosquats), version-scoped data.
Google deps.dev (deps-dev)NoneOpenSSF 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 varEffect
MARSHALL_DISABLE_SOCKET=1Skip the Socket.dev provider for the current process.
MARSHALL_DISABLE_DEPS_DEV=1Skip the deps.dev provider for the current process.
MARSHALL_DISABLE_ALL=1Skip every marshall-gated check (including providers).
--offline on vis auditDisables 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

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues