Skip to content

audit: do not rank an unknown severity as moderate - #45668

Open
robobun wants to merge 1 commit into
mainfrom
robobun/ca180673/audit-unranked-severity
Open

robobun wants to merge 1 commit into
mainfrom
robobun/ca180673/audit-unranked-severity

Conversation

@robobun

@robobun robobun commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • bun audit --audit-level=high exits 0 for an advisory whose severity is HIGH, another unknown word, or missing. It prints No vulnerabilities found (checked 1 package, 1 below --audit-level=high). --json and bun audit fix agree.
  • Cause: AuditLevel::should_include_severity ranked every unknown word as moderate (src/install/PackageManager/CommandLineArguments.rs:867). Three more sites defaulted to moderate.

Fix

  • parse_vulnerability reads the severity once: Info, Ranked(AuditLevel) or Unranked(word).
  • An unranked advisory is never below a level. It prints as unknown severity (HIGH), and --ignore <id> accepts it. info ranks below low, as in npm, pnpm and yarn.
  • Verified: test/cli/install/bun-audit.test.ts. Eight new tests and one changed test fail on main.
  • Self-reviewed: 16 concerns raised, 15 addressed. The --audit-level help line is unchanged (Notes).

Background

Downsides

  • Behaviour changes to sign off: an unknown or missing severity exits 1, not 0, at --audit-level=high|critical. info prints as info, not moderate, and exits 0, not 1, at --audit-level=low|moderate.
  • Stricter than npm on purpose: npm skips an unknown word in its exit code and reads a missing severity as high. Found by review: no user report, no registry known to send such a severity.
  • No added cost: 4 fewer malloc calls per advisory (15 advisories: 472, main 530).
Notes

Cells. One package, loopback registry, npm 11.16.0 with a cold cache for each run, main at 21620e2.

advisories on the package flag npm main this PR
HIGH --audit-level=critical text: exit 1 (TypeError), --json: exit 0 exit 0 exit 1
HIGH and low --audit-level=critical exit 0 exit 0 exit 1
HIGH and low --audit-level=moderate exit 0 exit 1, as moderate exit 1
no severity --audit-level=high exit 1, as high exit 0 exit 1
no severity --audit-level=critical exit 0 exit 0 exit 1
info none text: exit 1 (TypeError), --json: exit 0 exit 1, as moderate exit 1, as info
info and low --audit-level=moderate exit 0 exit 1 exit 0
info --audit-level=critical text: exit 1 (TypeError), --json: exit 0 exit 0 exit 0
  • npm ranks a package by its highest known severity (@npmcli/arborist/lib/vuln.js), and its exit code skips a word that it does not know (npm-audit-report/lib/exit-code.js). The TypeError is in the text reporter, when a package has no ranked advisory.
  • Same rank order as npm, pnpm and yarn. npm and pnpm default the level to low. Bun has no default, so an audit with only info advisories and no flag exits 1, as on main.
  • The match is case-sensitive, as in npm. --audit-level=HIGH is still rejected.

Prior art. Tools that have an explicit unknown severity and a threshold are split. NuGet audit skips a vulnerability below the audit level unless its severity is Unknown (AuditUtility.cs). The rustsec query behind cargo audit says: "Vulnerabilities without associated CVSS information will always match regardless of what this is set to." Grype and Trivy order unknown as the lowest severity.

Questions for a maintainer.

  1. A missing severity: unranked (this PR), or high as npm reads it. It is one arm in parse_vulnerability.
  2. Whether --audit-level=info is accepted. This PR does not add it. It also needs the help text and the three completion files.

The test that this PR replaces. an unknown severity is counted and filtered as moderate came with #38333. Its fixture is info. Its exit 0 at --audit-level=high stays. Its exit 1 at --audit-level=moderate is now 0.

Help text. bun audit --help still says Only print advisories with severity greater than or equal to <level>. An unranked advisory has no severity to compare. The docs have one new paragraph. A new wording belongs with question 2, which edits the same lines.

Malloc calls. Debug builds of main 21620e2 and of this PR, one registry package, counted by ASAN (print_stats=1:atexit=1), the same count in every run of a build.

command main this PR
bun audit --json, 15 advisories 530 472
bun audit, 15 advisories 534 476
bun audit, no advisory 257 259

Each advisory is parsed twice (collect_vulnerabilities validates the response, then reads it for the report), and each parse made 2 allocations for the severity. The 2 calls of the last row are not from this change: an earlier build of the same source made 257. VulnerabilityInfo stays 96 bytes.

Limit. --ignore matches the id or the URL of an advisory, as on main. An advisory that has neither cannot be ignored. With this PR that also applies at --audit-level=high|critical, when the advisory has no rank.

Empty title. An advisory whose title is an empty string now prints as Vulnerability found, like an advisory with no title. Before, the report counted it and did not print it (#19855), so an audit could fail with no advisory line.

History. The moderate default came with the first implementation (#19855) and the level filter (#21833). Also considered: refusing the whole answer when one severity has no rank. One unusual advisory then hides all the others and stops bun audit fix.

Related. #38557 is closed: #44968 included it. A second PR records the version of tarball and git packages in the lockfile so that bun audit sends them. It touches other regions of audit_command.rs and merges cleanly with this PR in either order. The report still lists every installed version of a package under an advisory. A change that matches the range per installed copy follows separately.

@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 7e35d86d-e5f3-4253-96ed-95cfaedb215b
📥 Commits

Reviewing files that changed from the base of the PR and between 5e1d229 and e9e02d0.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: oven-sh/bun/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 093ef8e0-2deb-45c4-9593-f7c22184c7db
📥 Commits

Reviewing files that changed from the base of the PR and between c5eb28d and 5e1d229.

📒 Files selected for processing: 1
  • src/runtime/cli/audit_command.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Walkthrough

The audit command now distinguishes ranked, info, and unknown or missing severities. It applies threshold filtering and reports severity labels and counts. Documentation and tests cover reporting, escaping, empty titles, and audit-fix behavior.

Changes

Audit Severity Handling

Layer / File(s) Summary
Severity parsing and filtering
src/install/PackageManager/CommandLineArguments.rs, src/runtime/cli/audit_command.rs
AuditLevel supports ordering and lowercase names. The audit command parses ranked, info, and unranked severities. info is below every threshold, while unranked severities remain reported at every threshold.
Severity reporting and counts
src/runtime/cli/audit_command.rs
Reports display ranked, info, and unknown severity labels. Counts are tracked separately. Advisories with empty titles remain in the report with the default title.
Audit validation and documentation
test/cli/install/bun-audit.test.ts, docs/pm/cli/audit.mdx
Tests cover thresholds, unknown severities, escaping, empty titles, ignoring advisories, and audit-fix behavior. Documentation describes the severity rules.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 5e1d2

No actionable merge-blocking issue remains. The severity thresholds and reporting can proceed through normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title is concise and accurately identifies the primary change: unknown audit severities are no longer ranked as moderate.
Description check Passed The description clearly explains the problem, implementation, behavior changes, verification, limitations, and follow-up questions. It does not use the exact template headings, but it provides the req…
Linked Issues check Passed Issue #44968 is closed and provides historical context only. No active directly linked issue provides coding requirements. The reported audit severity changes, documentation, and tests therefore have …
Out of Scope Changes check Passed The changes are limited to audit severity parsing, audit reporting and filtering, audit-fix behavior, documentation, and related tests. These changes support the pull request objectives. No unrelated …
Full details: Description check

Explanation

The description clearly explains the problem, implementation, behavior changes, verification, limitations, and follow-up questions. It does not use the exact template headings, but it provides the required change summary and verification details.

Full details: Linked Issues check

Explanation

Issue #44968 is closed and provides historical context only. No active directly linked issue provides coding requirements. The reported audit severity changes, documentation, and tests therefore have no active linked-issue requirement to fail.

Full details: Out of Scope Changes check

Explanation

The changes are limited to audit severity parsing, audit reporting and filtering, audit-fix behavior, documentation, and related tests. These changes support the pull request objectives. No unrelated change is demonstrated.

✨ Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:58 AM PT - Oct 20th, 2026

✅ @robobun, your commit e9e02d0010d30066fdedbb2ad79afb064b6d7d86 passed in Build #129421! 🎉


🧪   To try this PR locally:

bunx bun-pr 45668

That installs a local version of the PR into your bun-45668 executable, so you can run:

bun-45668 --bun

@robobun

robobun commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. CI is green on this head (build 129421, 181 of 181 jobs passed).

How it was reproduced. A loopback registry answers the bulk advisory endpoint with one advisory for an installed package. When the advisory has "severity": "HIGH", bun audit --audit-level=high on main prints No vulnerabilities found (checked 1 package, 1 below --audit-level=high) and exits 0. With this PR it prints unknown severity (HIGH): ... and exits 1.

The eight new tests and the changed escaping test in test/cli/install/bun-audit.test.ts fail on a build without the change and pass on a debug build of this branch.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline finding, I also checked two things and ruled them out: the --json and bun audit fix paths both go through the same keep_vulnerability in src/runtime/cli/audit_command.rs, so there is no second classification site left on the old "moderate" default; and the unranked word is passed to pretty! as a runtime argument (after escape_control_chars), so registry-supplied <red>-style markup is printed literally, not interpreted as a color tag. The [u32; 4] summary index matches the four repr(u8) discriminants of AuditLevel.

Extended reasoning...

The change replaces the raw severity bytes in bun audit with a three-way enum (info, ranked, unranked) across src/runtime/cli/audit_command.rs and src/install/PackageManager/CommandLineArguments.rs, with docs and tests updated. It touches how untrusted registry strings reach the terminal and how the exit code is decided, and it intentionally changes exit codes for missing or non-npm severity words. One confirmed finding about unsuppressible id-less unranked advisories is posted inline, so a human should weigh that and the deliberate stricter-than-npm behavior change; the sibling sites and output-injection concerns were checked and are not affected.

Additional findings (outside the current diff — GitHub can't attach inline comments there):

  • 🟡 src/runtime/cli/audit_command.rs — An advisory with no id, no url and an unrankable or missing severity now fails every bun audit and bun audit fix at every --audit-level, and nothing the docs offer can suppress it. src/runtime/cli/audit_command.rs:878 keeps every unranked advisory, and the --ignore match at audit_command.rs:886-889 compares against id and url only, both empty here. On the base branch such an advisory ranked as moderate and --audit-level=high dropped it. docs/pm/cli/audit.mdx says to pass --ignore <id> to accept it, which has nothing to match. Fix: give every kept advisory an ignore handle (for example fall back to matching the title, or print a note that the advisory has no id and cannot be ignored) so the documented escape hatch covers the unranked population it was added for. [also at: src/runtime/cli/audit_command.rs:1067 - Users at --audit-level=high|critical whose registry sends an unranked advisory with an empty title now get exit 1 and a summary that says 1 unknown severity with no advisory line to read or ignore. src/runtime/cli/audit_command.rs:1065-1067 skips printing any advisory whose title is empty, but the counting loop at audit_command.rs:1036-1042 and the exit code at audit_command.rs:1118 still include it.]

    Why this was flagged

    If any registry sends an advisory object with only title, parse_vulnerability at src/runtime/cli/audit_command.rs:721-760 leaves id and url as empty boxes and severity as Unranked. keep_vulnerability at audit_command.rs:875-879 never marks it below a level. The ignore check at audit_command.rs:886-889 is strings::eql(&vulnerability.id, ignored_cve) || strings::index_of(&vulnerability.url, ignored_cve).is_some(); with an empty id and empty url, strings::index_of returns None for an empty haystack (src/bun_core/string/immutable.rs:598), so no --ignore value matches. The result is exit 1 from every audit and the fixer at audit_command.rs:604 acting on it, with no remedy short of removing the dependency. The base branch gave this advisory the moderate rank and --audit-level=high|critical filtered it out. The new docs paragraph in docs/pm/cli/audit.mdx promises --ignore <id> as the way to accept an unranked advisory, which is false for this shape. Remedy: ensure every advisory the new rule keeps has an ignore handle, or fail loudly that it has none.

    Verification: A registry returns an advisory object with no id, no url, and a missing or unrankable severity. keep_vulnerability at src/runtime/cli/audit_command.rs:878: Severity::Unranked(_) => false, so it survives --audit-level=high|critical. Ignore check lines 886-889 matches id or url; both empty, so no --ignore <id> value matches and the command exits 1. On the base branch the same advisory was dropped at --audit-level=high|critical and the command exited 0.

@robobun

robobun commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator Author

The finding is correct about the code. Both parts are the behaviour of main, and this PR widens where they apply.

  • --ignore matches the id or the URL of an advisory (keep_vulnerability). An advisory that has neither has no handle. On main it cannot be ignored with no flag or at --audit-level=low|moderate. With this PR, an advisory that also has no rank cannot be ignored at high and critical either.
  • The report does not print an advisory whose title is an empty string, but the summary counts it. That skip came with bun audit #19855. A missing title prints as Vulnerability found.

I did not change either in this PR. No registry is known to send an advisory without an id, and a new way to match --ignore (for example on the title) needs a decision of its own. Both cases are now in the Notes of the PR body, under Limits.

The follow-up that matches each advisory range against each installed copy changes the same report loop. It will print every advisory that the summary counts.

@robobun
robobun force-pushed the robobun/ca180673/audit-unranked-severity branch from aa369b2 to 8315679 Compare October 20, 2026 00:30
Comment thread src/runtime/cli/audit_command.rs Resolved
Comment thread src/runtime/cli/audit_command.rs Resolved
@robobun

robobun commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

The last push also closes the second part of the finding. An advisory whose title is an empty string now prints as Vulnerability found, so every advisory that the summary counts has a line in the report. A new test covers it.

The first part is unchanged: an advisory with no id and no URL has no handle for --ignore. It is in the Notes of the PR body as a limit.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

@robobun
robobun force-pushed the robobun/ca180673/audit-unranked-severity branch from 8315679 to c5eb28d Compare October 20, 2026 00:51

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline nit, I also checked for sibling sites that still classify raw severity bytes: --json and bun audit fix both go through collect_vulnerabilities -> keep_vulnerability, so the single Severity match is the only filter, and the derived Ord on AuditLevel follows the repr(u8) declaration order (low < moderate < high < critical), matching the old as u8 comparison.

Extended reasoning...

The change replaces raw-byte severity handling in bun audit with a Severity enum resolved once at parse time, changing exit codes for unknown, missing, and info severities under --audit-level. It touches no auth or crypto surface, but it alters a security tool's fail-open/fail-closed behavior, which the author explicitly asks a maintainer to sign off on. I confirmed every consumer routes through the one keep_vulnerability predicate and that the ordinal comparison preserves the previous ranking.

Comment thread src/runtime/cli/audit_command.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review found no issues

No high-confidence issues detected in this change.

An advisory whose severity is not low, moderate, high or critical had
the rank moderate, so --audit-level=high and --audit-level=critical
dropped it and the audit passed. The severity is now read once in
parse_vulnerability: info, one of the four levels, or the word the
registry sent. info is below every --audit-level, as in npm, pnpm and
yarn. An advisory without a rank is never below --audit-level, in the
report, with --json, and in bun audit fix.
@robobun
robobun force-pushed the robobun/ca180673/audit-unranked-severity branch from 5e1d229 to e9e02d0 Compare October 20, 2026 08:56

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants