Safeguard
Product

Pull-request-level dependency scanning on GitHub

Socket.dev popularized flagging risky dependencies inside GitHub pull requests. Here's how that scanning works, where it falls short, and what closes the gaps.

Priya Mehta
DevSecOps Engineer
7 min read

In mid-March 2025, security teams watched a GitHub Action used by more than 23,000 repositories — tj-actions/changed-files — get quietly rewritten to dump CI secrets into build logs. The attacker didn't touch npm or PyPI. They compromised a dependency that lives inside .github/workflows/ files, the exact place most dependency scanners never look. Within days, GitHub flagged the tag (tracked as CVE-2025-30066), maintainers rotated keys, and postmortems went out. But for every repository that had already merged a pull request pulling in the poisoned tag, the damage was done before anyone reviewed a diff.

This is the core argument for pull-request-level dependency scanning: risk doesn't arrive at deploy time, it arrives the moment a PR adds or bumps a package. Socket.dev built a business around catching it there. This post looks at how Socket.dev's approach works, where its blind spots are, what recent incidents reveal about the stakes, and how Safeguard closes the gaps.

Why does dependency risk need to be caught at the pull request, not after merge?

Because a pull request is the last point in the software lifecycle where a human — or a required check — can stop a bad package before it reaches main, CI secrets, and production artifacts. Once a package.json, requirements.txt, or go.mod change merges, the new dependency typically installs on the very next CI run, which means install scripts, postinstall hooks, and build-time code execution have already fired by the time anyone notices. The npm registry alone processes tens of thousands of new package versions every week, and independent research from the past few years has repeatedly found that malicious or typosquatted packages get published, downloaded, and removed within a matter of hours — often faster than manual review cycles. If scanning only happens at deploy time or via periodic registry audits, it catches the incident after exposure, not before it. PR-level scanning is the only checkpoint that lines up with how dependencies are actually introduced: one line in one diff, reviewed by one engineer who is almost never a security specialist and is not going to manually audit a transitive dependency tree three levels deep.

How does Socket.dev approach PR-level dependency scanning?

Socket.dev's core product is a GitHub App that comments directly on pull requests when a diff adds, removes, or changes a dependency, attaching a risk score built from static analysis of the package itself. It looks for signals like install scripts that execute arbitrary code, use of eval or obfuscation, network or filesystem access declared in the package, new maintainers on an existing package, and name similarity to popular packages (typosquatting). Socket surfaces these as inline PR comments and a dashboard risk score, and can be wired into required status checks so a sufficiently risky change blocks merge rather than just generating a warning. This model was genuinely novel when Socket launched — it moved supply-chain review from "audit the lockfile quarterly" to "review the package the same way you review the code," and it's the reason PR-level scanning became a category rather than a niche feature.

What are the blind spots in diff-only, heuristic PR scanning?

The main blind spot is that a risk score computed from a package's own code doesn't tell you whether your application actually calls the risky code path, so teams either drown in false positives on unused functionality or start reflexively overriding warnings. Socket's checks are also concentrated on the ecosystems it built first — npm and PyPI, with narrower support elsewhere — which leaves gaps for organizations running Go, Rust, Java/Maven, or container base images through the same pull request workflow. And because the scoring is diff-scoped, it evaluates the package that changed in this PR, not always the full transitive graph pulled in underneath it, or whether the artifact that ships actually matches the source that was scanned. The tj-actions incident is instructive here precisely because it targeted a GitHub Action, not a language package registry — a category most PR dependency scanners, including Socket's, were not originally built to treat as a first-class dependency at all.

What actually happens when a malicious dependency slips past PR review?

It typically means credential theft, build compromise, or a self-propagating worm, and recent history has examples of all three. The xz-utils backdoor (CVE-2024-3094) was discovered by chance on March 29, 2024, after a Microsoft engineer noticed a 500-millisecond SSH login delay — the attacker had spent over two years building trust as a co-maintainer before slipping obfuscated code into release tarballs that would have shipped in major Linux distributions. In September 2025, a self-replicating npm worm nicknamed "Shai-Hulud" compromised several hundred packages, including ones under the widely-used @ctrl namespace, by stealing publishing tokens from infected machines and using them to automatically republish trojanized versions of every other package that maintainer controlled. And going back further, the 2018 event-stream incident showed the pattern doesn't need to be sophisticated to work: a new maintainer volunteered to take over an abandoned package, then quietly added a dependency (flatmap-stream) that only activated its malicious payload inside the Copay Bitcoin wallet's build. In every one of these cases, the compromise entered downstream projects through an ordinary-looking pull request — a version bump, a new maintainer, a routine dependency update — that a PR-level scanner was the last realistic chance to catch.

What should engineering teams look for in a PR-level dependency scanner?

Teams should look for reachability analysis, multi-ecosystem coverage, provenance verification, and enforcement that blocks rather than just comments. Reachability matters because a risk score on a package's total surface area is far less actionable than knowing whether your application's actual call graph touches the flagged function. Ecosystem coverage matters because modern software supply chains span npm, PyPI, Go modules, Maven, container images, and CI/CD configuration like GitHub Actions and Docker workflows — a scanner that only covers two of those leaves the rest of the PR unreviewed. Provenance verification — checking build attestations and SLSA-style signing rather than trusting that published code matches source — closes the gap that let the xz backdoor hide inside a release tarball that never appeared in the public git history. And enforcement matters because a PR comment that engineers can click past under deadline pressure is a suggestion, not a control; SOC 2 and similar compliance frameworks increasingly expect documented, enforced gates rather than advisory tooling.

How Safeguard Helps

Safeguard treats the pull request as a policy enforcement point, not just a notification channel. When a PR changes a manifest, lockfile, GitHub Actions workflow, or container base image, Safeguard resolves the full transitive dependency graph — not just the direct change — and cross-references it against known-malicious package feeds, typosquat detection, and behavioral signals like new install scripts or maintainer changes, the same class of signal Socket pioneered. Where Safeguard extends that model is in tying the finding to reachability: rather than scoring a package in isolation, Safeguard checks whether your codebase's call graph actually exercises the flagged code, so teams see fewer PRs blocked on dependencies that are present but never invoked.

Safeguard also treats CI/CD configuration as a dependency surface in its own right, scanning GitHub Actions references and pinned SHAs alongside language-level packages — directly addressing the class of attack that hit tj-actions/changed-files. Every flagged change is checked against provenance and build attestation data where available, so a package that looks clean in its published metadata but doesn't match its declared source is caught rather than trusted by default. And because Safeguard's checks run as a required GitHub status check, teams get real enforcement: a PR carrying a high-severity or unresolved-provenance dependency is blocked from merge, with an audit trail suitable for SOC 2 evidence, rather than a comment that can be dismissed. The result is PR-level scanning that covers the ecosystems and attack surfaces — npm, PyPI, Go, Maven, containers, and CI workflows alike — that a single-registry approach was never built to see in one pass.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.