Dependency Autopsy: event-stream

We applied Commit's trust scoring retrospectively to every stage of the 2018 event-stream supply chain attack. The package itself scored 66 with two risk flags. But the real signal was the dependency it ingested: flatmap-stream, a brand-new package with a trust score of 13. Here's the full breakdown.

The event-stream attack is the Rosetta Stone of supply chain security. Everything that has happened since — ua-parser-js in 2021, colors.js in 2022, node-ipc in 2022 — follows the pattern it established. And unlike those later incidents, event-stream had time to be studied. We know the exact timeline. We know every commit. We know exactly when each structural signal changed.

So we did something we haven't done for any other incident: we reconstructed what proof-of-commitment scoring would have shown at every stage of the attack, using the published specification and the data that was publicly available at each point in time.

The results are honest. The tool wouldn't have prevented the attack. But it would have made the signal loud enough to investigate — and investigation was all that was needed.


The timeline

For anyone unfamiliar: event-stream was a popular Node.js utility for working with streams, created by Dominic Tarr in 2011. By 2018, it had roughly 2 million weekly downloads and was a transitive dependency across thousands of projects. Tarr was the sole maintainer and had publicly lost interest in the package years earlier.

In September 2018, a GitHub user called right9ctrl offered to take over maintenance. Tarr agreed and transferred npm publish access. In early October, the new maintainer published event-stream 3.3.6, which added a single new dependency: flatmap-stream. This package, also created by right9ctrl, contained an encrypted payload targeting Copay — a Bitcoin wallet — to steal cryptocurrency from users. The attack went undetected for nearly two months until a developer noticed unusual code during a routine dependency check on November 20, 2018.

No automated tool caught it. Not npm audit (no CVE existed). Not any static analysis tool (the payload was AES-encrypted and only executed in a specific downstream context). Not GitHub (the code looked like a normal functional programming utility). The attack was invisible to anything that analyzed code.

The structural signals, however, were not invisible. They were screaming.


Trust score timeline

We scored both packages — event-stream and flatmap-stream — at four key moments. All scores use the published specification (v1.0.0) with data that was available at each date. The methodology is fully reproducible; every threshold and weight is documented in the spec.

Date Event event-stream flatmap-stream Risk flags
Aug 2018 Stable state. Tarr is sole maintainer, package unchanged for years. 66 🟠 HIGH · ⚠️ WARN
Sep 2018 Tarr transfers publish access to right9ctrl. 66 🟠 HIGH · ⚠️ WARN
Oct 5, 2018 flatmap-stream published. event-stream 3.3.6 adds it as a dependency. 73 ↑ 13 🔴 New dep: MINIMAL
Nov 26, 2018 Attack discovered. npm unpublishes malicious versions. 73 13 (too late)

Two things jump out of this timeline.

First: event-stream's own score went up when the malicious version was published. From 66 to 73. The new release triggered a recency bonus in the Release Consistency dimension. By point-in-time scoring, event-stream looked healthier after the attack than before it. This is a real limitation — and a lesson about what structural scoring can and cannot do.

Second: flatmap-stream scored 13 out of 100. Thirteen. In the MINIMAL trust tier. Every dimension was near zero except the recency bonus for having just been published. This is not a marginal signal. A dependency with a trust score of 13 appearing in a package with 2 million weekly downloads is a structural anomaly that should trigger immediate investigation.


Dimension-by-dimension: event-stream

Before the attack (August 2018), event-stream's breakdown:

Dimension Max Score Reasoning
Longevity 25 25 Created 2011. 7 years old. Full marks.
Download Momentum 25 22 ~2M/week (≥1M = 22 base). Stable trend (+0).
Release Consistency 20 12 ~50 versions (≥30 = 12 base). Last publish >365 days (+0 recency).
Maintainer Depth 15 4 1 maintainer (Dominic Tarr). Minimum score for a registered package.
GitHub Backing 15 3 Repo ~1.7K stars but >730 days without push → 0.5× penalty. Repo score ~22.
Total 100 66

Risk flags (pre-attack):

  • 🟠 HIGH — Single maintainer AND >1M weekly downloads. The structural profile that attackers select for.
  • ⚠️ WARN — No publish in >365 days. The package was abandoned by its creator while still being installed 2 million times a week.

Neither flag alone is a smoking gun. Together, they describe the exact conditions that enabled the attack: an abandoned package with a massive install base, controlled by a single credential, with an owner who had publicly stated he no longer cared about it. That's not an obscure risk factor. That's a neon sign.


Dimension-by-dimension: flatmap-stream

This is where the analysis gets interesting. flatmap-stream was the vehicle for the attack — the package right9ctrl created to carry the malicious payload. Let's score it at the moment it appeared:

Dimension Max Score Reasoning
Longevity 25 1 Brand new. <6 months old. Minimum score.
Download Momentum 25 0 Zero organic downloads. No install history.
Release Consistency 20 8 1 version (≥1 = 3 base). Recently published (+5 recency).
Maintainer Depth 15 4 1 maintainer (right9ctrl). Unknown account with no other packages.
GitHub Backing 15 0 Zero stars. Zero forks. Zero watchers. No community signal whatsoever.
Total 100 13 MINIMAL trust (0–39 tier)

Thirteen out of a hundred. Every dimension except Release Consistency scored at or near zero. The only points came from the fact that it existed and had been recently published — which is true of literally any newly created package.

Even after flatmap-stream inherited event-stream's 2 million weekly downloads through transitive installs, the score only climbed to 38 — still firmly in the MINIMAL tier. Downloads alone cannot rescue a package that has no history, no community, no governance, and no reason to exist except as a dependency of one other package.


The signal that mattered

Here's the critical insight, and the reason this incident is worth a deep autopsy rather than a quick case study.

Scoring event-stream alone would not have caught this attack. The existing Three npm Disasters analysis was honest about this. Event-stream's score was 66 with a HIGH flag — concerning, but millions of packages have concerning profiles. The point-in-time score doesn't isolate the attack.

Scoring the dependency tree catches it. The question isn't "what does event-stream score?" It's "what changed in my dependency tree, and what does the new entry score?"

When event-stream 3.3.6 was published, every project that depended on it gained a new transitive dependency: flatmap-stream. If those projects had been running npx proof-of-commitment --file package.json on each lockfile update, the output would have included a new line:

flatmap-stream  score=13  1 maintainer  0 downloads/week  🔴 MINIMAL
  └ new transitive dependency via [email protected]

A package with a trust score of 13 suddenly appearing in your dependency tree is not a subtle signal. It is the dependency equivalent of a fire alarm. The natural response — "what is this package, and why was it added?" — leads directly to the attack. Five minutes of investigation would have revealed that flatmap-stream was created by the same person who just took over event-stream, had no independent purpose, and contained unusual code patterns.

No CVE database needed. No malware scanner needed. No code analysis needed. Just the structural question: does this new dependency deserve the trust my project is about to grant it?


Why event-stream's score went up

This is the uncomfortable detail worth examining. When right9ctrl published event-stream 3.3.6, the package's Release Consistency score improved. A new release within the last 30 days earned a +5 recency bonus, pushing the dimension from 12 to 17 and the overall score from 66 to 73.

By the numbers, event-stream looked healthier after the attack than before. The WARN flag for staleness would have cleared. To a point-in-time scoring system, the new maintainer appeared to be reviving an abandoned project.

This is a real limitation, and we are transparent about it. A static score cannot distinguish between genuine revival and social engineering. The spec scores the current structural state, not the trajectory of change. Detecting maintainer handoffs, sudden dependency additions, and behavioral diffs over time — these require temporal analysis that goes beyond what the current specification covers.

But notice what the dependency tree scoring catches even without temporal analysis: the 73-scoring parent package now depends on a 13-scoring child. The gap — 73 vs. 13 — is itself a structural anomaly. A well-established package suddenly depending on an untrusted one is not a pattern that requires sophisticated detection. It requires the right question.


What we can't catch (and what we can)

Being precise about claims matters more than being impressive. Here's the honest ledger:

Attack stage What happened Detectable? How
Pre-condition Abandoned package, sole maintainer, 2M installs/week HIGH + WARN flags. Score 66 — weakest in peer group.
Social engineering right9ctrl convinces Tarr to hand over publish access Not currently scored. Maintainer change detection is on the roadmap.
Dependency injection flatmap-stream added as new dependency flatmap-stream scores 13/100 (MINIMAL). Dependency tree diff catches it.
Payload delivery Encrypted malicious code in flatmap-stream targets Copay wallet Out of scope. Commit scores structure, not code.

Two out of four stages are detectable. The two that aren't — social engineering and payload analysis — are fundamentally different problems that require different tools. But catching either of the two detectable stages would have been sufficient. Flagging the pre-conditions raises the alarm. Flagging the dependency injection identifies the weapon.


The pattern since 2018

Every major npm supply chain attack since event-stream has exploited the same structural condition: a sole maintainer controlling publish access for a high-traffic package. The attack vectors differ — social engineering, token compromise, account takeover — but the structural precondition is identical.

Incident Vector Maintainers Downloads/wk Flag
event-stream (2018) Social engineering 1 2M 🟠 HIGH
ua-parser-js (2021) Token compromise 1 7M 🟠 HIGH
LiteLLM (2026) Token compromise 1 95M/mo 🔴 CRITICAL
axios (2026) Token compromise 1 101M 🔴 CRITICAL

The structural condition was visible before every single one of these attacks. In each case, the flag existed for months or years before the incident. The tools to see it existed. The data was public. The only thing missing was someone asking the right question.

Proof-of-commitment asks the right question, automatically, against every package in your dependency tree.


Try it yourself

The entire methodology is open source. Every weight, threshold, and flag is documented in the specification. You can verify every number in this analysis.

# Score event-stream today
npx proof-of-commitment event-stream

# Audit your whole project
npx proof-of-commitment --file package.json

# Web interface
# https://getcommit.dev/audit

MCP server for Claude Desktop, Cursor, or Windsurf:

{
  "mcpServers": {
    "proof-of-commitment": {
      "type": "streamable-http",
      "url": "https://poc-backend.amdal-dev.workers.dev/mcp"
    }
  }
}

GitHub Action (runs on every PR):

- uses: piiiico/proof-of-commitment@main
  with:
    fail-on-critical: false
    comment-on-pr: true

The question isn't whether event-stream was predictable in hindsight. The question is whether the same structural conditions exist in your dependency tree right now. For 30% of the top 50 npm packages, they do.

Related: The Axios Signal · Three npm Disasters That Were Predictable · Scoring Specification v1.0.0

Open source: github.com/piiiico/proof-of-commitment

Stay in the loop

Early access, research updates, and the occasional strong opinion.