Dependency Review: Catching Malicious Packages Before They Merge
Ran into this the other day. A developer on a team I was working with opened a PR that added a single npm package. One package. That one package pulled in 47 transitive dependencies, three of which had known critical vulnerabilities and one that had quietly switched from MIT to AGPL.
Nobody caught it. The PR got approved, merged, shipped to production. The vulnerabilities were discovered six weeks later during an unrelated audit.
This is what actually happens when you don't have dependency review in your pipeline.
The Problem With "Just Adding a Package"
Every dependency you add is code you didn't write running in your environment. That's not paranoia — it's just the reality of modern software development.
When a developer adds cool-new-library to their package.json or requirements.txt, they're not just adding that library. They're adding everything it depends on. And everything those depend on. The dependency tree fans out fast.
I've seen a single Express middleware package bring in 30+ transitive dependencies. Each one of those is an attack surface. Each one has its own maintainers, its own release cadence, its own vulnerability history.
And here's the part people forget — license changes. A package you've been using under MIT can release a new version under AGPL, and if your dependency range is ^2.0.0, your next install might pull that in automatically. AGPL has copyleft requirements that can have real legal implications for proprietary software. Nobody's checking this manually at PR time.
GitHub Dependency Review Action
GitHub has a built-in action for this called dependency-review-action. It diffs the dependency manifests in your PR against the base branch and flags anything new that has known vulnerabilities or license concerns. This is one piece of the broader shift-left security approach — catching issues in the pipeline, not in production.
Here's what a basic setup looks like:
# .github/workflows/dependency-review.yml
name: Dependency Review
on:
pull_request:
branches: [main]
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Dependency Review
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d7 # v4.7.1
with:
fail-on-severity: critical
deny-licenses: AGPL-3.0-only, GPL-3.0-only
comment-summary-in-pr: always
That's it. Every PR that touches a dependency manifest (package.json, requirements.txt, go.mod, Gemfile, etc.) gets scanned. If a new dependency — direct or transitive — has a critical vulnerability, the workflow fails. If a dependency uses a denied license, it fails.
A few things worth noting in that config:
Pin your actions to commit SHAs. I'm using the full SHA for both actions/checkout and actions/dependency-review-action. Tags are mutable — someone can move a v4 tag to point at different code. SHAs can't be changed. This is a supply chain security basic that most teams skip.
fail-on-severity: critical blocks the PR if any new dependency has a critical-severity vulnerability. You can also set this to high if you want tighter controls:
fail-on-severity: high
deny-licenses is where you list licenses that are incompatible with your project. Customize this based on your legal requirements. Common denials for proprietary software:
deny-licenses: AGPL-3.0-only, GPL-3.0-only, GPL-2.0-only, EUPL-1.1
comment-summary-in-pr: always posts a summary comment directly on the PR so the developer can see exactly what got flagged without digging through workflow logs.
Tightening It Up
The basic config catches the obvious stuff. Here's a more production-ready version:
name: Dependency Review
on:
pull_request:
branches: [main, release/*]
permissions:
contents: read
pull-requests: write
jobs:
dependency-review:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Dependency Review
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d7 # v4.7.1
with:
fail-on-severity: high
deny-licenses: AGPL-3.0-only, GPL-3.0-only, GPL-2.0-only
comment-summary-in-pr: always
allow-dependencies-licenses: pkg:npm/some-internal-exception
warn-only: false
Setting fail-on-severity to high instead of critical is the move for most teams. Critical-only sounds safe, but a lot of genuinely dangerous vulnerabilities get classified as high. The difference between critical and high is often just whether there's a known exploit in the wild yet.
warn-only: false makes it a hard gate. The PR can NOT be merged if the check fails. If you're just getting started and don't want to block developers immediately, you can set this to true first, let the team see what gets flagged for a couple weeks, then flip it.
Two Layers: Dependency Review + Dependabot
Dependency review and Dependabot solve different problems. They're not redundant — they're complementary.
Dependency review catches bad dependencies coming in. It runs on PRs and asks: "Is the thing you're adding safe?"
Dependabot catches bad dependencies already there. It monitors your existing dependency tree and opens PRs when updates are available for known vulnerabilities.
Together, they cover both directions:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
groups:
production-dependencies:
dependency-type: "production"
development-dependencies:
dependency-type: "development"
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
Note that last block — package-ecosystem: "github-actions". Dependabot can also keep your GitHub Actions updated, which matters because those actions run in your CI environment with access to your secrets and cloud credentials.
The flow looks like this:
- Developer adds a new dependency → dependency review catches vulnerabilities and license issues at PR time
- Existing dependency gets a CVE published → Dependabot opens a PR to update it
- Dependabot's update PR goes through dependency review → confirms the update doesn't introduce new issues
Each layer catches what the other misses.
The Transitive Dependency Problem
This is the part that surprises people. The dependency review action doesn't just check the package you explicitly added. It checks the full resolved dependency tree.
When you add express-rate-limit to your project, you're not just adding that one package. You're adding everything in its dependency tree. And those transitive dependencies are where the real risk lives, because nobody's reading through the dependency tree of every package they install.
The dependency review action diffs the full lockfile — package-lock.json, yarn.lock, poetry.lock, whatever your ecosystem uses. So when that one package you added brings in 50 others, every single one gets checked against the GitHub Advisory Database.
This is why lockfiles matter. If you're not committing your lockfile, dependency review can't diff what changed. Commit your lockfiles.
Make It a Branch Protection Rule
Last step — make sure this actually blocks merges. Go to your repo settings:
Settings → Branches → Branch protection rules → Require status checks to pass before merging
Add dependency-review as a required status check. Now it's not optional. No PR that introduces a high-severity vulnerability or a denied license can be merged, period. Guardrails, not gates — but this particular guardrail should be non-negotiable.
The whole setup takes maybe 20 minutes. Two YAML files and a branch protection rule. And it catches the class of problem that leads to headlines about supply chain compromises.
What does your dependency review setup look like? If the answer is "we don't have one," those two config files above are a good place to start this week. And if you want to pair this with Infrastructure as Code security and a broader CI/CD security approach, check out our free SaaS Security Checklist for the full picture.
Secure your stack
Get our free 65-item SaaS Security Checklist delivered to your inbox.
No spam. Just the checklist + practical security tips.
Check your inbox!
Want help securing your infrastructure?
I help B2B SaaS teams eliminate static credentials, harden containers, and lock down CI/CD pipelines — delivered as code you keep.
Let's talk