Dallas skyline illustration

CYC / Field notes

You Reviewed the PR. Nobody Reviewed the 1,400 Packages It Pulled In.

Code review scrutinizes the lines your team writes while thousands of imported dependencies often pass unseen. Here's how to bring software supply chain risk into the review process.

← Back to blog

You reviewed the pull request.

You checked the logic. You looked for weird edge cases. You left a comment about a function name, caught an unnecessary API call, and asked for one more test. Everything looked good, so you hit Approve. Nice work. Ship it.

There is just one small problem.

That PR also added one direct dependency, which pulled in 34 other packages, which pulled in a few hundred more, and somewhere down the tree your application quietly inherited code from people nobody on your team has ever heard of. Maybe it was 400 packages. Maybe it was 1,400. Maybe more.

Nobody reviewed those.

We have built an entire culture around carefully reviewing the 40 lines we wrote while completely ignoring the 4 million lines we imported. Let's talk about that.

Welcome to Modern Software Development

Let's be real: we are not building applications from scratch. We are assembling them.

That is not an insult. It is how software gets built now, and honestly, it is one of the great things about open source. Need date formatting? There is a package for that. Need auth, routing, validation, logging, or a little animation that makes the product manager happy? Packages for all of it. A modern app might contain thousands of open source components before your team writes a single line of business logic.

That is powerful. It is also a little wild when you stop and think about it.

We have strong processes for the code our coworkers write. We require pull requests. We protect branches. We argue about formatting rules until somebody rage-quits the Slack channel. Then one line gets added to package.json and a giant pile of outside code enters the application:

package.json
{
  "dependencies": {
    "some-helpful-package": "^4.2.0"
  }
}

Looks harmless, right? What that line does not show you is everything behind it. The transitive dependencies. The install scripts that may execute automatically. The maintainers who can publish updates. The accounts that can be phished. The abandoned projects nobody realized were abandoned.

That single line is not just code reuse. It is a trust decision. Most of the time, we treat it like autocomplete.

The Diff Is a Lie (Kind Of)

Code review works on one assumption: the diff is the change. But the diff is just the change you can see.

Your review interface shows one green line. Your software supply chain sees an entirely new neighborhood moving in - hundreds of new packages, dozens of new maintainers, post-install behavior, and access to whatever secrets exist in your build environment.

And nobody is opening package-lock.json in a code review. That file is 30,000 lines of hashes and registry URLs. GitHub literally collapses it by default. The most security-relevant file in the entire PR, and the UI hides it. Wild.

A realistic version of this: a 15-person startup adds a date-picker component. The review takes ten minutes and most of it is about the button color. The date picker drags in 200+ transitive packages, one of which has a postinstall script. Nobody knows. Nobody checked. That's not negligence. That's just Tuesday.

To be clear, I am not saying every dependency is dangerous. Most maintainers are doing excellent work, often for free, while the rest of us build businesses on top of it. Open source maintainers deserve way more credit and funding than they get. The problem is that organizations consume their work at enormous scale while acting like every package is a self-contained, fully reviewed unit.

It is not.

This Isn't Hypothetical, and You Know It

If you've been around the JavaScript world long enough, you've got the scars.

left-pad got yanked from npm in 2016 and broke builds across the internet over eleven lines of code that pad a string. That was the funny one. The ones after it were not funny.

event-stream in 2018 is the one that should live rent-free in your head. A burned-out maintainer handed the package off to a helpful stranger who had been contributing for a while. That stranger added a dependency with a hidden payload targeting a cryptocurrency wallet app. Roughly two million weekly downloads at the time. Many of those installs happened downstream of a dependency change that somebody approved without ever seeing the malicious code.

Then colors and faker in early 2022, where the maintainer sabotaged his own packages and apps everywhere started printing garbage in an infinite loop.

Then September 2025, the big one. Attackers phished the maintainer behind chalk, debug, ansi-styles, and more using a fake npm support domain, then pushed malicious versions of 18 packages that collectively pull over two billion downloads per week. The payload silently rewrote cryptocurrency transactions in the browser. The community caught it within a couple of hours, sharply limiting the window in which users could download the malicious versions. Two hours of exposure on packages that sit under basically every Node project on Earth. Let that one sink in.

And it's not just JavaScript flexing on us. The xz backdoor in 2024 (CVE-2024-3094) showed the long game: years of patient, friendly contributions to a compression library, all to plant a backdoor aimed at SSH. It got caught because one engineer noticed SSH logins were about half a second slower than they should be. Half a second. That's the security perimeter we're working with, y'all.

The pattern in every single one of these? The attack didn't come through a PR anyone reviewed. It came through the supply chain everyone trusted by default.

"But We Run a Vulnerability Scanner" - Bless Your Heart

I say that with love. I've been that person.

Scanning is important. Keep doing it. But npm audit and friends check your tree against a database of known vulnerabilities, and that's a fundamentally different question from whether something should have been trusted in the first place. A scanner may not tell you that a package came from a compromised account, that a malicious release was published twenty minutes ago, that the source repo and the published artifact don't match, or that an install script is quietly collecting environment variables.

That timing gap matters. By the time a CVE exists, your build pipeline may have downloaded the bad version hundreds of times.

BAM. It's already in production.

And don't get me started on "we didn't install it directly." Developers love to talk about transitive dependencies like they're somebody else's problem. Imagine explaining an incident like this:

We did not install the compromised package directly. A utility package depended on a parsing package that depended on another package that included it.

Technically accurate. Not especially comforting. Code does not become less capable of causing damage just because it entered your application three layers down the tree. You may not have picked it, but you run it, your CI downloads it, and your company owns the consequences.

Developers Cannot Manually Review the Internet

At this point, somebody usually suggests that developers should carefully inspect every package before using it.

Okay. Let's play that out.

Before approving one dependency update, you read the source of the direct package. Then every transitive dependency. Then their dependencies. You verify each maintainer, compare repo source with the published artifact, inspect install scripts, review release history, check licensing, and repeat all of it for every future version.

Cool. Your two-point ticket is now scheduled for Q4.

Manual review has a place, especially for sensitive or foundational components. But telling developers to personally vet thousands of packages is not a supply chain security strategy. It is security theater with extra homework. The graph changes too fast, maintainers change, ownership changes, and previously safe packages become unsafe. This is not a one-time review problem. It is a continuous trust problem.

Same energy for lockfiles. Lockfiles are great - use them, commit them, review changes to them. But reproducibility is not security. A lockfile can help you install the same compromised package every single time. Pinning reduces surprise. It does not create trust.

Your Build Pipeline Is a High-Value Target

One reason these attacks work so well: we install dependencies in places full of valuable stuff. CI/CD environments hold registry tokens, cloud credentials, signing keys, deployment permissions, and sometimes straight-up production secrets. Then we execute third-party code inside that environment as part of a normal build.

Read that sentence again.

We spend so much energy protecting production, but the build system is often where outside code first gains access to the organization. A malicious install script doesn't need to exploit your finished app if it can grab credentials while the app is being assembled. The attacker doesn't have to break through the front door.

Sometimes we download their code and run it ourselves.

Okay, Chris, So What Do We Actually Do?

You're not going to hand-review 1,400 packages, and I'm not going to pretend you should. But "we can't review everything" is not the same as "so review nothing," and that's the trap most teams are sitting in. The move is to put friction where the attacks actually enter and make the safe path the default path.

If someone adds a new dependency in a PR -> make the dependency itself part of the review. Not its source code - the decision. Weekly downloads, last publish date, maintainer count, transitive dep count, whether it runs install scripts. Five minutes. A package with two downloads a week and a fresh publish date entering your tree deserves a comment more than the variable name does.

If your lockfile changed and nobody knows why -> stop the merge until someone does. Lockfile churn with no matching package.json change is either tooling noise or something worth a very close look, and you want to know which.

If you have CI -> disable install scripts by default and allowlist the exceptions, pin your versions, and use npm ci with a committed, reviewed lockfile. Your build should install exactly what was reviewed, not whatever satisfies the semver range today.

If you're an org with more than a handful of repos -> you need tooling and policy, full stop. An approved internal registry or proxy instead of unrestricted access. Scanning before packages reach dev and build environments. Provenance checks that tie artifacts back to the expected source. Policies for package age, maintenance activity, and licensing. And a cooldown window so brand-new versions can't hit your builds for a few days - malicious releases tend to get caught fast, and the damage happens in the first hours. Even a short cooldown period can reduce exposure to malicious releases that are discovered within their first few hours or days. This is exactly the gap something like TuxCare's SecureChain exists to close - a curated registry where packages are rebuilt from verified source, shipped with provenance and an SBOM, and kept patched even after upstream moves on.

If a dependency does one trivial thing -> just write the function. I promise you can pad a string. The best dependency review is the dependency you didn't add.

The goal is not to make developers afraid of npm install, and it's definitely not to make each developer personally responsible for evaluating an entire package registry every time they need a utility. When the secure workflow is also the normal workflow, nobody has to choose between shipping and doing the right thing. That's where security programs actually work. None of this is glamorous - there's no conference talk energy in "we added a cooldown to our registry proxy" - but the boring controls are the ones that hold.

The Part Where I Land the Plane

Here's my actual stance, no hedging: code review, as most teams practice it, reviews the wrong thing. We put our best human attention on the code with the least leverage to hurt us - code our teammates wrote, with names attached, in a repo we control - and we extend blind, automatic, transitive trust to code from strangers with none of those properties.

That's backwards. And we all know it's backwards. We've just agreed not to look.

The application is bigger than the diff. It includes every package that entered with it, every transitive component underneath, and every future update your version ranges allow. Somebody should be asking where those 1,400 packages came from. Somebody should be watching what happens to them after the merge. And that somebody cannot just be the developer trying to close one ticket before stand-up.

The next event-stream is already in someone's dependency tree right now. Maybe yours. The diff looked clean, the tests passed, and somebody hit approve.

We reviewed the PR. Now it's time to review the software supply chain it brought along for the ride.

Thanks for coming to my TED talk!

If you're heading to Commit Your Code, come find the TuxCare crew and we'll show you SecureChain up close.

TuxCare logo

TuxCare helps organizations secure and maintain open-source systems with live patching, lifecycle support, and software supply chain security.