Skip to content

GitHub Dependency Review

Lesson 4 of 8Intermediate14 min readGit Security & DevSecOps · Code & Dependency SecurityVerified: GitHub dependency review and actions/dependency-review-action v5, September 2026

Dependabot tells you about a problem you already have. Dependency review tells you about one you are about to acquire — in the pull request, while somebody can still decide not to.

That difference in timing is the entire value. Preventing a vulnerable dependency from entering costs a conversation. Removing one that has been in production for six months costs an upgrade project.

Dependency review compares the dependency graph of the pull request’s head against its base, and reports what changed.

In the GitHub interface, a pull request touching a manifest or lock file gets a dependency review view showing what was added, removed and updated, along with known vulnerabilities, licence information, release dates and adoption signals.

As an action, the same comparison runs in CI and can fail the check, which is what turns it from information into a gate.

.github/workflows/dependency-review.yml
name: dependency review
on: [pull_request]
permissions:
contents: read
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/dependency-review-action@v5
with:
fail-on-severity: high
comment-summary-in-pr: on-failure

More than vulnerabilities, and the non-vulnerability signals are often the more useful ones.

Dependencies added, removed and updated, including indirect ones that moved because a lock file changed. That last category is the one nobody reads and the one where surprises live: a pull request described as “bump the HTTP client” can move fifteen transitive packages.

Known vulnerabilities in anything being added or upgraded to.

Licences, so a copyleft dependency arriving in a proprietary codebase is visible before it is a legal problem rather than after.

Release dates and adoption signals, which is the closest thing available to “is this package real?” A dependency published four days ago with almost no adoption, appearing in a pull request that was supposed to be a patch bump, is worth a second look.

That last signal is the one that catches the attacks Dependabot structurally cannot: a typosquatted package name, or a fresh malicious release. There is no advisory for a package nobody has reported yet — but “published three days ago, nobody uses it, and it is now a dependency of your web server” is a pattern a human recognises immediately.

Verified against actions/dependency-review-action v5.

OptionDefaultEffect
fail-on-severitylowThreshold: low, moderate, high, critical
fail-on-scopesruntimeWhich scopes count: runtime, development, unknown
allow-ghsasSpecific advisory IDs to skip
vulnerability-checktrueDisable vulnerability checking entirely
show-patched-versionsfalseInclude the fixed version in the output

The default of low fails on essentially anything, which is the right default for a new repository and usually too aggressive for an existing one. Start at high, and tighten once the baseline is clean.

fail-on-scopes defaults to runtime, which is a well-chosen default: a vulnerability in a test framework is genuinely different from one in something you ship. Widening it to include development is defensible — development dependencies execute on developer machines and in CI, which is where your credentials are — and it will increase volume substantially.

allow-ghsas is the escape hatch for a specific advisory you have assessed and accepted. Use it with a comment saying who decided and why; it is a standing exception with no expiry.

OptionEffect
allow-licensesAn allowlist of SPDX identifiers
deny-licensesA denylist — deprecated in favour of the allowlist
license-checkTurn licence checking off
allow-dependencies-licensesExempt specific packages from licence checks

Prefer allow-licenses. A denylist fails open: a licence nobody thought to deny passes. An allowlist fails closed, which for a legal-compliance control is the correct direction.

- uses: actions/dependency-review-action@v5
with:
fail-on-severity: high
allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC

Expect a first run to flag packages with unusual or unrecognised SPDX identifiers, including some with no declared licence at all. allow-dependencies-licenses handles the individual cases without widening the policy.

OptionEffect
deny-packagesBlock specific packages, by exact version or wildcard
deny-groupsBlock whole namespaces

deny-groups is the more interesting one. Blocking an entire namespace is how you prevent dependency confusion: if your internal packages live under a scope, denying public packages in that scope stops a public package of the same name being resolved instead of yours. See Dependency security for the full attack.

OptionDefaultEffect
comment-summary-in-prneveralways, on-failure, never
warn-onlyfalseReport everything as a warning, failing nothing
config-fileMove the configuration into a file
base-ref / head-refCompare arbitrary refs
show-openssf-scorecardtrueInclude OpenSSF Scorecard data
warn-on-openssf-scorecard-level3Warn below this score

warn-only: true is the right way to adopt this. Run it non-blocking for a few weeks, see what it would have failed, then turn it into a gate. This is the same principle as a ruleset’s evaluate mode: measure before enforcing.

comment-summary-in-pr: on-failure puts the finding where the author is looking. A check that fails with a red tick and no explanation sends people to the logs; a comment naming the package and the advisory does not.

Once the options list grows past three or four, move it out of the workflow:

.github/dependency-review-config.yml
fail-on-severity: high
fail-on-scopes:
- runtime
allow-licenses:
- MIT
- Apache-2.0
- BSD-2-Clause
- BSD-3-Clause
- ISC
deny-groups:
- "@your-internal-scope"
comment-summary-in-pr: on-failure
- uses: actions/dependency-review-action@v5
with:
config-file: ./.github/dependency-review-config.yml

Two reasons this is worth doing beyond tidiness.

The policy becomes reviewable on its own. A pull request that loosens fail-on-severity is a change to a security policy, visible as such, rather than a line buried in a workflow file nobody reads in full.

It can be shared. config-file accepts a reference to a file in another repository, with external-repo-token for a private one — which means one organisation-wide policy file rather than fifty copies that have drifted.

A blocking check needs an answer to “and now what?”, or the answer becomes “remove the check”.

A vulnerability in the new dependency. Check whether a fixed version exists — show-patched-versions: true puts it in the output. If it does, take it. If it does not, the decision is whether the dependency is worth the exposure, and that is a real decision rather than a formality.

A vulnerability in a transitive dependency. Usually resolvable by overriding the resolved version through your package manager’s override mechanism, without waiting for the direct dependency to update. This works, and it puts you on a combination the maintainer never tested — a trade worth making knowingly.

A licence failure. Either the licence is genuinely unacceptable, in which case the dependency is not usable, or your allowlist is missing something reasonable. Add it to the allowlist deliberately rather than exempting the package. Note that a package with no declared licence is not the same as a permissive one — it is a package you have no explicit permission to use, and it will show up here as a failure that looks like a tooling problem.

A finding you have assessed and accepted. allow-ghsas for a specific advisory, or allow-dependencies-licenses for a specific package. Both are standing exceptions with no expiry, so each one deserves a comment naming who decided, when, and on what basis — because the person who reads it next will otherwise have to redo the assessment from nothing.

A false alarm from a scope you do not ship. If a development-only dependency is failing a runtime policy, the manifest’s scope declaration is probably wrong — which is worth fixing at the source rather than working around here.

The pattern across all five: the resolution should either fix the problem or record a decision. Neither warn-only nor removing the step is on that list.

The action needs contents: read and nothing more, which is what makes it safe on pull requests from forks.

It works by comparing dependency graphs through the API rather than by building the project, so it does not execute the contributor’s code and does not need credentials. That is a meaningful property: it is one of the few dependency checks that is genuinely safe to run against an untrusted pull request.

Compare that with running the project’s own package manager to produce a dependency list, which for several ecosystems executes install scripts from the contributor’s manifest — in your CI, with whatever that job holds. The API-based comparison avoids the whole category.

If you add comment-summary-in-pr, the job needs pull-requests: write to post it, which a fork pull request’s token does not have. The usual resolutions are to accept that the summary appears only on internal pull requests, or to post it from a separate workflow_run job that does not check out the fork’s code — the same pattern described in Dependabot and GitHub Actions.

The action surfaces OpenSSF Scorecard data for dependencies, warning below a configurable level.

Scorecard measures a package’s development practices rather than its code: does it run automated tests, does it pin its own dependencies, are releases signed, is there a security policy, are maintainers using two-factor authentication.

This is a genuinely different signal from vulnerability data, and it is worth understanding what it does and does not tell you.

It is a proxy, not a verdict. A low score means a project follows fewer of the practices Scorecard checks. It does not mean the code is bad, and a small, careful, single-maintainer library may score poorly while being entirely trustworthy.

It is most useful as a prompt. A new dependency with a low score is a reason to look at it, not a reason to reject it. Treating the score as a gate produces a policy that rejects small good libraries and accepts large mediocre ones.

The action enforces a policy. The interface shows a person something a policy cannot evaluate, and that half is easy to skip because it does not produce a red tick.

Four things worth looking at on any pull request that moves dependencies:

The count of indirect changes. A pull request whose title says “update lodash” and which moves thirty transitive packages is not the change described. Something in the resolution moved, and it is worth knowing what before merging.

The age of anything new. A package published in the last week, entering your tree, deserves a glance at where it came from. Legitimate new releases are constant; a legitimate new package appearing as a transitive dependency of something you already had is less common.

Adoption relative to position. A package with very few dependents sitting deep inside a widely-used library is unusual. That is the shape of a compromised release adding a payload dependency, and it is one of the few observable signals available before anybody has filed an advisory.

Licences on anything new, even when the check passes. An allowlist that happens to include a licence is not the same as somebody having read it and decided it is acceptable for this product in this jurisdiction.

None of these produce a failure, and all of them are the reason the review view exists rather than only the check. The check is the floor; this is what the review is for.

They are complementary, and the distinction is timing.

Dependency reviewDependabot alerts
RunsOn a pull requestContinuously
Question“Does this change make things worse?”“Is anything I have known-bad?”
OutputA check, and a pull request annotationAn alert
ScopeThe diffThe whole dependency tree
TimingBefore the dependency exists in your codebaseAfter
Failure modeNothing changed, so nothing is reviewedRetrospective by nature
PreventsYesNo

Both are needed, for a reason worth stating: dependency review sees only what changes. A repository whose dependencies have not moved in a year passes every dependency review while accumulating advisories the whole time. Dependabot covers exactly that gap, and dependency review covers the gap Dependabot cannot — the malicious package that has no advisory yet.

Threat. A pull request introduces a dependency that is vulnerable, malicious, incompatibly licensed, or resolves to something other than what the author intended.

Attack surface. Every manifest and lock file change, including ones where the author only meant to bump one package. Plus the resolution process itself, which decides which artefact a name resolves to — the mechanism dependency confusion and typosquatting exploit.

Impact. A malicious dependency is code execution in your build and in your product, arriving through a change somebody approved. This is the highest-impact row in the cluster, because it bypasses review by looking like a routine version bump.

Control. Review the dependency change at the pull request, with a policy that fails closed on licences, blocks your internal namespace, and surfaces the age and adoption signals a human can act on.

Verification. Open a pull request adding a package with a known advisory and confirm the check fails. Then open one adding a brand-new package with no advisory and confirm the review still surfaces its age and adoption — because that is the case the advisory database cannot help with.

The second half is the important verification. A dependency review configured only to fail on advisories catches yesterday’s problems. The signals that catch tomorrow’s are the ones a person has to read.

An unenforced check is advice. Two mechanisms make it a gate, and they operate at different levels.

A required status check in a ruleset, by job name:

steps:
- uses: actions/checkout@v7
- name: dependency-review
uses: actions/dependency-review-action@v5

Require the check name in an organisation ruleset targeting the repositories that need it. See Repository rulesets for security.

One trap worth knowing: if the workflow uses paths: filters so it only runs when manifests change, then a pull request touching no manifest never produces the check — and a required check that never reports blocks the merge indefinitely. Make the job always run and exit early instead, so it reports success rather than not reporting.

A reusable workflow so the configuration lives in one place:

jobs:
dependency-review:
uses: YOUR_ORG/.github/.github/workflows/dependency-review.yml@v1

Combined with a shared config-file, that gives one policy definition, one place to change it, and a three-line file in each repository. See Reusable workflows.

Not enabling the dependency graph. Dependency review depends on it, and without it the action reports nothing meaningful.

Leaving fail-on-severity at low on an established repository. Every pull request fails, and the check gets removed.

Using deny-licenses instead of allow-licenses. A denylist fails open on anything nobody anticipated.

Enabling it as a blocking gate on day one. warn-only first; measure; then enforce.

Ignoring the transitive changes. The direct dependency is what the pull request is about; the lock file is where the surprises are.

Treating a Scorecard score as a verdict. It measures practices, not safety.

Assuming it covers unchanged dependencies. It reviews the diff. Everything else is Dependabot’s job.

No deny-groups for your internal namespace. Dependency confusion is cheap to prevent here and expensive to discover later.

The adoption sequence that avoids the two ways this check gets removed — too noisy on day one, and blocking with no explanation.

  1. Confirm the dependency graph is enabled on the repositories you are targeting. Without it, everything below is inert.

  2. Add the action with warn-only: true and default settings. It fails nothing. Let it run for a few weeks of ordinary pull requests.

  3. Read what it would have blocked. This is the measurement, and it usually surfaces two things: a licence in your tree that nobody had noticed, and a steady trickle of low-severity advisories in development dependencies.

  4. Write the policy from what you learned — a severity threshold you can live with, a licence allowlist covering what you already have, and deny-groups for your internal namespace.

  5. Move the policy into a config file so it is reviewable and shareable.

  6. Remove warn-only and add comment-summary-in-pr: on-failure, so a failure explains itself.

  7. Make the check required through an organisation ruleset.

  8. Tighten the threshold once the baseline is clean, not before.

Step 3 is where the value is. Most teams discover that their real dependency risk is not what they expected — the licence question is usually more urgent than the vulnerability question, and the development-scope volume is usually the reason a stricter policy would have been abandoned.

Dependency review is code review for the dependency tree. It answers one question — does this change make our dependency risk worse? — at the one moment when the answer is cheap to act on.

  • Dependency review compares the pull request’s dependency graph against its base
  • It surfaces vulnerabilities, licences, release dates and adoption signals, not just advisories
  • Release date and adoption are the signals that catch attacks with no advisory yet
  • fail-on-severity defaults to low; high is a more workable starting point
  • fail-on-scopes defaults to runtime, which correctly separates shipped from development code
  • Prefer allow-licenses over the deprecated deny-licenses — allowlists fail closed
  • deny-groups prevents dependency confusion on your internal namespace
  • warn-only lets you measure the impact before making it a gate
  • OpenSSF Scorecard measures practices, and is a prompt rather than a verdict
  • It only reviews what changed, which is exactly why Dependabot is also required

Use a disposable repository with a small manifest.

  1. Confirm the dependency graph is enabled. Predict: what does the action report if it is not?

  2. Add the workflow with warn-only: true. Open a pull request adding a dependency with a known advisory. Predict: does the check pass or fail?

  3. Remove warn-only and set fail-on-severity: high. Repeat. Predict: does the same finding now block?

  4. Add comment-summary-in-pr: always and re-run. Compare how findable the finding is.

  5. Add a dependency whose licence is outside a strict allow-licenses list. Predict: does it fail for the licence even though it has no vulnerabilities?

  6. Open a pull request that upgrades one direct dependency and observe how many indirect ones moved.

  7. Set fail-on-scopes: runtime, development and observe the change in volume.

  8. Delete the repository.

GitHub Actions Security ChecklistToken permissions, fork pull requests, script injection and supply chain — with the attack each item prevents.

The repository security templates — secrets management and least-privilege token guides — are in the Professional Toolkit.