2026-07-30
Add a blocking Bun audit gate without losing the report
Use a pinned Bun audit job to block high-severity findings, retain JSON evidence, and govern the audit's coverage limits.
bun install --frozen-lockfile and bun ci answer a reproducibility question: can CI install the dependency graph recorded in the lockfile without changing it? They do not answer whether that graph became vulnerable after it was committed. Bun documents bun ci as the frozen-lockfile equivalent for CI. Bun install for CI
Add an audit gate after the frozen install. It evaluates the resolved dependency set rather than treating a successful install as a security decision.
Keep install and audit separate
This GitHub Actions job pins the article's tested Bun boundary to 1.3.14. It keeps installation and auditing in separate steps, so an install failure cannot look like a vulnerability result. The action's documented setup is oven-sh/setup-bun, and Bun 1.3.14 is the release used for the command behavior noted below. Bun 1.3.14 release
name: dependency-audit
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.3.14
- name: Install the locked dependency graph
run: bun install --frozen-lockfile
- name: Capture the raw audit response
shell: bash
continue-on-error: true
run: |
set -o pipefail
bun audit --json | tee bun-audit.json
- name: Block high-severity dependency findings
shell: bash
run: bun audit --audit-level=high
- name: Retain the audit response
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: bun-audit-json
path: bun-audit.json
if-no-files-found: warnbun audit --audit-level=high reports findings at high severity or above. Bun sends installed package names and versions to npm, and exits 1 when it finds vulnerabilities. In Bun 1.3.14, the JSON branch writes the raw registry response and exits 1 for any nonempty advisory response before the formatted-report path receives the audit level or ignore list. It therefore cannot enforce the high threshold or approved ignores. The capture step is deliberately non-gating, while the separate formatted command is the gate. Bun 1.3.14 audit source
The pipeline matters for the evidence step. GitHub's explicit shell: bash wrapper already uses bash --noprofile --norc -eo pipefail {0}; keeping set -o pipefail makes that intent explicit and portable. In a local plain-Bash experiment, the bun audit --json | tee pipeline returned 0 without it and 1 with it. I retained the command statuses, raw output, checksums, and both pipeline captures through immutable review. The experiment proves the JSON exit status and plain-Bash pipeline behavior only; it does not prove high-threshold filtering. GitHub Actions shell behavior
The actions are pinned to the full commit SHAs reviewed for this workflow, with release comments to keep updates readable. Full-SHA pinning makes an action reference immutable and is GitHub's recommended way to use a third-party action. Secure use of third-party actions
Do not add bun update to this job. Bun's report can include update hints, but an audit gate should report and fail, leaving lockfile changes to a reviewed dependency update. Bun lockfiles are the record of resolved versions and integrity data, so an automated update would change the thing the frozen install is meant to verify. Bun lockfiles
State the coverage boundary
This is not a complete inventory of every package in every registry. Bun says bun audit sends package and version data to npm and skips packages from registries other than the default registry. Private and custom-registry dependencies therefore need a separate inventory and scanner path; a green audit must not be presented as coverage for them. Bun audit
Use --prod only when the policy deliberately covers production dependencies and intentionally excludes development dependencies. It narrows the question. The default gate above audits the installed graph because build tooling, tests, and lifecycle execution are still part of CI's supply-chain exposure. Bun documents that package lifecycle scripts can run during installation, subject to its trust model. Bun lifecycle scripts
Use two checks for two boundaries
A configured Security Scanner API integration runs during package operations such as bun install and bun add, before installation. Scanner results can be fatal, which stops installation, or warn, which exits in CI. That makes it a pre-install policy boundary for packages entering the operation. Bun Security Scanner API
bun audit is a post-resolution check of the installed packages and versions against npm's vulnerability service. Run both when both policies matter: the scanner decides whether package acquisition may proceed, while the audit gate rejects a resolved, locked graph when its vulnerability status is unacceptable. Neither removes the need to account for packages skipped because they came from a non-default registry.
Set expiration dates for exceptions
Do not let a CVE ignore become an undocumented permanent pass. Keep every approved exception in a reviewed policy file or ticket with the advisory identifier, affected package and locked version, owner, rationale, approval date, expiry date, remediation target, and the condition that removes the exception. Make CI fail when an exception has no owner or expiry, when it is expired, or when the lockfile no longer matches the stated affected version.
An exception is not an audit-level change. Keep --audit-level=high fixed, and put each narrowly approved --ignore=<advisory> argument only on the formatted, non-JSON gate. Review the raw JSON evidence as well as the gate output. When a remediation is available, update the dependency in a dedicated change, regenerate and review the lockfile, then remove the exception. This preserves the difference between accepting a temporary risk and changing the dependency graph.
Acceptance rule
Accept a change only when the frozen install exits 0, raw JSON evidence is retained, the separate bun audit --audit-level=high gate exits 0, every custom-registry dependency has separate coverage, and every ignore has an owner and is unexpired.