Skip to content

chore(ci): audit only the dependencies a PR adds - #6337

Merged
jbocce merged 6 commits into
masterfrom
chore/security-check-ng
Oct 7, 2026
Merged

jbocce merged 6 commits into
masterfrom
chore/security-check-ng

Conversation

@jbocce

@jbocce jbocce commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Context

The current security audit in CircleCI runs pnpm audit on the whole lockfile. So any PR that touches pnpm-lock.yaml fails because of problems that are already on master, even when the PR didn't cause them. It also always compares against master, even for PRs into release/*.

This PR replaces it with a check that only looks at what the PR adds. It also removes some secrets and unpinned downloads from the PR builds.

Changes & Results

  • New GitHub workflow: Dependency Audit
    • Compares the PR's pnpm-lock.yaml with its target branch and checks only the package versions the PR adds.
    • A new high or critical advisory fails the check. Problems already on the target branch never block a PR.
    • To accept an advisory, add its GHSA to auditConfig.ignoreGhsas in pnpm-workspace.yaml with a reason. New ignores show up as an extra New dependency audit ignores line in the PR's checks, so reviewers notice them.
    • Runs on pull_request_target, so it always uses master's copy of the workflow and script. It only reads the PR's lockfile and pnpm-workspace.yaml as data and never runs PR code.
  • Removed the old "Security Audit" step from CircleCI's BUILD_PACKAGES_QUICK.
  • No publish credentials in the PR build: removed the npm token and the ohif-bot git setup from BUILD_PACKAGES_QUICK. That job only builds; it never publishes.
  • Cypress job: waits for the dev server with curl instead of downloading wait-on@latest on every run.

Note: the new check can't run on this PR. pull_request_target uses the workflow from master, so it starts working on PRs opened after this one merges.

Testing

Tested on my fork, with this branch set as the fork's default branch:

Test PR Change Result
jbocce#9 Lockfile downgrades axios to 1.18.1 ❌ Fails with 7 high advisories
jbocce#9 (2nd commit) Same, plus 1 ignore ❌ Fails with 6, plus the "New dependency audit ignores" line
jbocce#10 Same, plus all 7 ignored ✅ Passes, plus the "New dependency audit ignores" line
jbocce#11 README only ✅ Passes: lockfile unchanged

Also run locally against past PRs (#6327, and fork PR #6286), and against a target with no pnpm lockfile.

Checklist

PR

  • My Pull Request title is descriptive, accurate and follows the
    semantic-release format and guidelines.

Code

  • My code has been well-documented (function documentation, inline comments,
    etc.)

Public Documentation Updates

  • The documentation page has been updated as necessary for any public API
    additions or removals. (None needed: CI only.)

Tested Environment

  • OS: Windows 11 (local), GitHub Actions ubuntu-latest
  • Node version: 24.15.0
  • Browser: N/A (CI only)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Pull requests now receive an automated check for newly added dependencies with high or critical security advisories. Existing approved exceptions are respected, and findings are reported directly in the pull request.
    • The check distinguishes newly introduced advisories from issues already present in the base branch, helping teams focus on security risks added by a change.
    • The check also flags dependency changes it cannot fully evaluate, so those gaps are visible during review.

jbocce and others added 4 commits October 7, 2026 10:04

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@netlify

netlify Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for ohif-dev ready!

Name Link
🔨 Latest commit 62bf66a
🔍 Latest deploy log https://app.netlify.com/projects/ohif-dev/deploys/6ac663aaa34b460008304161
😎 Deploy Preview https://deploy-preview-6337--ohif-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: b650c145-bac3-4278-b4d0-0365fbc6f502
📥 Commits

Reviewing files that changed from the base of the PR and between 715a6c8 and 62bf66a.

📒 Files selected for processing (4)
  • .circleci/config.yml
  • .scripts/dependency-audit/audit.mjs
  • .scripts/dependency-audit/audit.test.mjs
  • .scripts/dependency-audit/package.json

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


📝 Walkthrough

Walkthrough

The pull request adds a GitHub Actions workflow that audits package versions added in a pull request. It updates CircleCI steps related to the audit and replaces the Cypress server wait command with bounded curl polling.

Changes

Pull request dependency audit

Layer / File(s) Summary
Identify added packages and check advisories
.scripts/dependency-audit/audit.mjs, .scripts/dependency-audit/audit.test.mjs, .scripts/dependency-audit/package.json, .gitignore, pnpm-workspace.yaml
The script compares package name and version pairs in base and head lockfiles, queries npm advisories for added versions, applies configured ignore entries, and reports results. Tests cover parsing, ignore handling, workflow-command escaping, and audit outcomes.
Fetch inputs and run the PR audit
.github/workflows/dependency-audit.yml, .circleci/config.yml
The workflow fetches pull request and merge-base lockfiles and workspace files, then runs the audit. It posts a successful commit status when the audit reports newly added ignored advisories. CircleCI tests the audit package and no longer runs its lockfile-triggered audit or configures the removed Git and NPM settings.

Cypress server startup wait

Layer / File(s) Summary
Poll the local server before Cypress
.circleci/config.yml
The Cypress command polls localhost:3000 every two seconds for up to 600 seconds before running the existing recorded parallel Cypress command.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Other

Sequence Diagram(s)

sequenceDiagram
  participant PullRequest
  participant DependencyAuditWorkflow
  participant audit.mjs
  participant npmBulkAdvisoryAPI
  participant GitHubCommitStatus
  PullRequest->>DependencyAuditWorkflow: Open, synchronize, or reopen event
  DependencyAuditWorkflow->>DependencyAuditWorkflow: Fetch head and merge-base lockfiles and workspace files
  DependencyAuditWorkflow->>audit.mjs: Run audit with downloaded inputs
  audit.mjs->>npmBulkAdvisoryAPI: Query advisories for added package versions
  audit.mjs-->>DependencyAuditWorkflow: Return audit result and new_ignores output
  DependencyAuditWorkflow->>GitHubCommitStatus: Post success status when new_ignores is nonzero
Loading

Suggested reviewers: wayfarer3130

Merge Risk: ⚪ Minimal · up to 62bf6

This change moves the dependency audit from CircleCI to a GitHub workflow and adds tests for the audit script. No concrete merge-blocking risk was found in the supplied changes.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 2 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: auditing only dependencies added by a pull request. It follows the repository's semantic-release format.
Description check ✅ Passed The description includes context, detailed changes and results, testing evidence, and completed checklist items. It explains the workflow behavior, security changes, limitations, and tested environmen…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 64.71% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 17 functions across 2 files. (2 skipped: 2 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · 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.

@jbocce
jbocce deployed to unrestricted October 7, 2026 14:17 — with GitHub Actions Active

@wayfarer3130 wayfarer3130 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.

I reviewed this PR with a focus on security. The design is sound, and the PR improves the security of CI.

Good points

  • The workflow uses pull_request_target safely. The workflow does not check out the PR head. PR values go to the shell steps only through env:. The permissions are minimal.
  • I checked the action pins. df4cb1c… is the commit of the annotated tag actions/checkout@v6.0.3. 48b55a0… is the commit of actions/setup-node@v6.4.0.
  • The step that parses the PR YAML has no token. The yaml dependency has an integrity pin, and npm ci runs with --ignore-scripts. yaml v2 does not execute code, and toJS() limits alias expansion by default.
  • A PR can add its own ignores. .github/CODEOWNERS already requires a code owner review for /pnpm-workspace.yaml, and the new status makes the ignores visible. These two controls together are sufficient.
  • The removal of the NPM token and of the git identity from BUILD_PACKAGES_QUICK is a good change. The removal of npx wait-on@latest from the Cypress job is also good, because that job holds the Cypress record key.

Findings (details in the inline comments)

  1. Low: text from the PR goes to stdout without escape, so the PR can inject workflow commands. The injection does not bypass the check, but it can hide the annotations for reviewers.
  2. The check blocks a merge only when branch protection lists "Dependency audit" as a required check. Please confirm this for master and release/* before the merge.
  3. No job audits the lockfile of master now. A new advisory against a version that is already on master gets no report.
  4. Low: two edge cases pass the check without an audit.

Other review checks

  • Reuse of existing methods: pnpm has no audit mode for a lockfile diff, so a custom script is correct. One dependency (yaml) is a better choice for a security tool than the @pnpm/lockfile-* packages.
  • Comments: the header comment of the workflow is long. The comment records security decisions, so I think that the length is correct.
  • Tests: the script has no automated tests. For a security gate, I recommend 2 or 3 tests of the type "the API must do this": the lockfile diff (with the two-document format), the ignore logic, and the escape of PR text from finding 1. audit.mjs calls main() on import, so the tests need exports of the pure functions.
  • User specification: the viewer behaviour does not change. The contributor rule changes: the check reads the ignores from the PR copy of pnpm-workspace.yaml. The PR description and the comment in pnpm-workspace.yaml state this change clearly.

}

const markdown = lines.join('\n');
console.log(markdown);

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.

Security (low): workflow-command injection through stdout.

The Markdown report holds text from the PR without escape:

  • ignoreGhsas entries that do not match GHSA_ID (line 173 removes only backticks).
  • Lockfile keys in the "not audited" list (line 232).

A YAML string such as "x\n::stop-commands::abc" puts a workflow command at the start of a line. The runner then executes the command. With this injection, a PR can:

  • hide the later ::warning title=New audit ignore and ::error annotations,
  • add a false ::notice,
  • add ::add-mask:: for words in the log.

The exit code and the commit status stay correct. The script writes new_ignores to $GITHUB_OUTPUT after the stdout commands, so the check is not bypassed. The injection only reduces what the reviewers see.

Suggested fix: put the output between ::stop-commands::<random token> and ::<random token>::. Another fix is the validation in the comments on lines 173 and 232. The same text also goes to $GITHUB_STEP_SUMMARY, where the text can add false headings. GitHub sanitizes the HTML of the summary, so that risk is small.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catch, thanks. Fixed in d26655fbed: the report is now printed between ::stop-commands::<random token> and ::<random token>:: (crypto.randomUUID()), so no line of it can act as a workflow command. Our own ::warning and ::error annotations are printed after commands are resumed. The PR text is also cleaned at the source (replies 2 and 3).

Comment thread .scripts/dependency-audit/audit.mjs Outdated
for (const ghsa of newIgnores) {
const id = GHSA_ID.test(ghsa)
? `[${ghsa}](https://github.com/advisories/${ghsa})`
: `\`${ghsa.replace(/`/g, '')}\``;

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.

An ignore that does not match GHSA_ID keeps its newlines and control characters here. I suggest that the script rejects such an entry, or that the script shows the entry as invalid after the removal of all characters outside [A-Za-z0-9-]. In pnpm audit, an entry that is not a GHSA never matches an advisory, so a strict rule here costs nothing. Note that the script itself creates npm-<id> values for advisories without a GHSA. If you want to allow those values in the ignore list, add npm-\d+ to the accepted pattern.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in d26655fbed. An ignore entry is valid only if it is a GHSA ID or npm-<number>. Anything else is listed as "invalid entry", reduced to [A-Za-z0-9-], with a note that it matches no advisory. It does not fail the check, since it cannot silence anything and the "New dependency audit ignores" status already asks reviewers to look.

Comment thread .scripts/dependency-audit/audit.mjs Outdated
`<details><summary>${notAudited.length} added entry(ies) not from the npm registry, not audited</summary>`,
'',
// Unvalidated lockfile keys: drop backticks so they stay inside the code span.
...notAudited.map(p => `- \`${`${p.name}@${p.version}`.replace(/`/g, '')}\``),

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.

These lockfile keys failed the PACKAGE_NAME / REGISTRY_VERSION checks, so the keys can contain any characters, newlines included. Please remove control characters here as well as backticks, for example with .replace(/[\x00-\x1f\x7f]/g, '')`.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in d26655fbed, with your regex: .replace(/[\x00-\x1f\x7f`]/g, '').

Comment thread .scripts/dependency-audit/audit.mjs Outdated
let blocking = [];
const headText = readText(values.head);
const baseText = readText(values.base);
if (headText === null) {

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.

Edge case (low): when the base has a pnpm-lock.yaml and the head has no lockfile, the check passes with "nothing to audit". In CI, an install with a frozen lockfile fails in that case, so the risk is small. But a security gate must fail when it cannot do its work. I suggest that the script fails when baseText !== null && headText === null.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, done in d26655fbed. If the target branch has pnpm-lock.yaml and the PR does not, the check now fails ("This PR deletes pnpm-lock.yaml, so its dependencies cannot be audited"). If neither side has one (old pre-pnpm branches), it still passes.

lines.push('`pnpm-lock.yaml` is unchanged; nothing to audit.', '');
} else {
const added = addedPackages(lockfilePackages(baseText ?? ''), lockfilePackages(headText));
const auditable = added.filter(

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.

Edge case (low): if a future pnpm version changes the format of the packages: keys (for example, back to /name@version), every added entry fails these patterns. All entries then go to the "not audited" list, and the check passes without a report. A small guard prevents that result: fail when the "not audited" entries are more than a small part of the added entries (for example, more than 10% and more than 5 entries).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done in d26655fbed, with your numbers: the check fails when more than 5 added entries, and more than 10% of them, are not name@version from the npm registry. The message says the lockfile format may have changed and the script needs an update. Today's lockfile has 0 such entries out of 2,683.

# a PR to another branch, push a commit (or close and reopen) to rerun.

on:
pull_request_target:

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.

Two points about the trigger:

  1. This PR removes the CircleCI audit. After the merge, this check blocks a merge only when branch protection lists "Dependency audit" as a required check. Please add the check to the branch protection of master and release/* when this PR merges.
  2. This check audits only the versions that a PR adds. No job audits master now. When an advisory appears for a version that is already on master, no job reports the advisory. I suggest a schedule run of pnpm audit --audit-level high on master that does not block, or an explicit decision to rely on Dependabot alerts.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

  1. Yes. I will add "Dependency audit" as a required check on master and release/* once this merges. GitHub only offers a check there after it has run at least once.
  2. A daily check of master is planned as a separate task. It needs to run privately, since this repo's Actions logs and issues are public, so it will live outside this repo. Dependabot alerts don't work as the source for us: GitHub's dependency graph can't read our pnpm 12 lockfile (two YAML documents), so the alerts only reflect package.json declarations, not the installed versions.

jbocce and others added 2 commits October 7, 2026 11:02
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jbocce

jbocce commented Oct 7, 2026

Copy link
Copy Markdown
Collaborator Author

@wayfarer3130 On the tests from your review: added in 62bf66abc5: .scripts/dependency-audit/audit.test.mjs, run with Node's built-in test runner (npm test in that folder) and by a new step in CircleCI's UNIT_TESTS job (the PR's copy of the script, since the workflow always runs master's). The script now exports its helpers and runs main() only when started directly. The 9 tests cover the lockfile diff (both YAML documents, scoped names), the ignore logic (new ignores, valid IDs, cleaned labels), escapeCommand, the stop-commands wrapping with an injected ignore, and the guards (deleted lockfile, many un-auditable entries, no lockfile on either side). None of them call the npm advisory endpoint.

@jbocce
jbocce requested a review from wayfarer3130 October 7, 2026 15:24
@cypress

cypress Bot commented Oct 7, 2026

Copy link
Copy Markdown

Viewers    Run #6874

Run Properties:  status check passed Passed #6874  •  git commit 62bf66abc5: test(ci): add tests for the dependency audit script
Project Viewers
Branch Review chore/security-check-ng
Run status status check passed Passed #6874
Run duration 01m 47s
Commit git commit 62bf66abc5: test(ci): add tests for the dependency audit script
Committer Joe Boccanfuso
View all properties for this run ↗︎

Test results
Tests that failed  Failures 0
Tests that were flaky  Flaky 0
Tests that did not run due to a developer annotating a test with .skip  Pending 0
Tests that did not run due to a failure in a mocha hook  Skipped 0
Tests that passed  Passing 28
View all changes introduced in this branch ↗︎

@jbocce
jbocce deployed to unrestricted October 7, 2026 15:30 — with GitHub Actions Active
@jbocce
jbocce merged commit 8c47b04 into master Oct 7, 2026
15 of 16 checks passed

This branch was successfully deployed

1 active deployment
unrestricted — 62bf66ab Deployed Oct 7, 2026 by jbocce via playwright-tests (24.15.0) #5188
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants