Safeguard
Vulnerability Management•

"Fully Compromised": What 7,700 npm Malware Advisories Say, and What the Analysis Adds

GitHub's npm malware advisories tell you to treat the machine as fully compromised and rotate every secret. OSV's 2026 data: 7,700 npm records carry that paragraph, 2,018 carry nothing else, and 5,682 include an analysis of what the code did. Where they part ways, and a script to scope your response.

Safeguard Security Team
Security Research
11 min read

GitHub's npm malware advisories carry the same paragraph: any computer with the package installed "should be considered fully compromised", and every secret on it should be rotated. In 2026, 7,700 npm malware records in OSV carry that text. For 2,018 of them it's the only description of the package. For the rest, another source's analysis says what the code actually did, and occasionally it describes something much narrower, or nothing at all.

This post is for whoever gets the Dependabot malware alert: incident responders, platform teams and the developer whose laptop it was. It covers what the generic text says and why, what OSV's 2026 data adds, where the analysis and the generic text part ways, and a script that pulls the analysis for every malware hit in a lockfile so you can scope the response.

The short answer

GitHub's malware advisories use one fixed paragraph: treat the machine as fully compromised, rotate all secrets from another computer, and don't trust removal alone. That's the right first move, because the advisory can't know what ran on your machine. But the OSV record behind the advisory often holds more. Of 7,700 npm malware records published in 2026 with GitHub's paragraph, 5,682 also carry an analysis from Amazon Inspector, Google, or OpenSSF's Package Analysis, which says what runs, when, and where data goes. 2,018 carry nothing else. In 134 records, the analysis matches wording that says the version it looked at doesn't read or send credentials. Read the analysis before you finish the response, not instead of starting it.

What the generic text says, and why it's written that way

The paragraph is the advisory's whole description in GitHub's database. The record for a package flagged on 19 October 2026, GHSA-q66v-22v7-2qj9, is typical: type malware, severity critical, and the same three sentences about full compromise, rotating secrets from a different computer, and removal not being enough.

It's written for the worst case on purpose. GitHub publishes malware advisories automatically from the npm security team and the OpenSSF Malicious Packages project, and its documentation says "Our malware advisories are mostly about substitution attacks." Dependabot can't see which machines ran an install script, whether the package came from your private registry, or what the payload fetched. One conservative paragraph that's right for an infostealer is safer than a tailored one that's wrong.

What OSV's 2026 data shows

OSV merges each malware record from several sources, and each source's text sits under its own heading in the record. We read every malware record published in 2026 in OSV's npm and PyPI data dumps, downloaded on 19 October 2026, leaving out withdrawn records:

npmPyPI
Malware records published in 202612,8711,448
With GitHub's "fully compromised" paragraph7,7000
... and no other description2,0180
... and at least one other source's analysis5,6820

The paragraph appears only on npm records. On PyPI, the descriptions come from the analysis sources directly. One of those sources, the researcher Kamil Mańkowski (kam193), also labels each campaign. Of the 2026 PyPI records carrying that label, 339 say "probably pentest" and 979 say malicious. That's exactly the scoping signal GitHub's text leaves out.

On npm, the most common pairing is GitHub's paragraph plus an Amazon Inspector analysis (4,801 records). Those analyses typically describe what runs and when, what it reads, and where it sends data. Some records also list indicators such as domains and file hashes.

Where the analysis and the generic text part ways

We searched the non-GitHub text of the 7,700 records for explicit statements that the analysed version doesn't read or send credentials, such as "no exfiltration" or "no code that sends". 134 records matched. We then read a random sample of 20:

What the analysis describedRecords in the sample
No harmful behaviour found in the analysed version (placeholders, empty name squats, broken or unrelated code)10
Harm that doesn't involve reading secrets7
The match was on other wording, such as a README's false claims or a comparison with the genuine package3

The middle row is the interesting one, because the right response there is neither "rotate everything" nor "nothing happened":

  • WhatsApp channel follows. The PhantomSub family are forks of the Baileys WhatsApp library that use the installer's own WhatsApp session to follow channels the publisher picked. The analyses say the code doesn't send credentials or session keys off the machine and adds no persistence. The account to check is the WhatsApp account, not the AWS keys.
  • A malicious dependency. One record covers a library whose own code an analysis calls benign, but whose package.json declares a dependency that is itself malware. The response belongs to that dependency's record.
  • Using the machine against others. MAL-2026-12183's analysis says the package doesn't take the installer's secrets. Instead, running it turns the installer's machine and accounts into a tool for abusing third parties. Rotating secrets doesn't fix that. Removing the package and checking outbound traffic does.

The 2,018 records with no analysis are the opposite case. There's nothing to scope with, so the generic paragraph is all you have, and it should be followed in full.

Pull the analysis for your own hits

This script reads a package-lock.json, asks OSV which installed versions have malware records, and for each record prints every source's first sentence plus up to three sentences about credentials, exfiltration, persistence or install time, and any indicator domains. It says plainly when a record has no analysis. Standard library only:

#!/usr/bin/env python3
"""For each package in a package-lock.json that has an OSV malware (MAL) record,
print each source's first sentence and up to three sentences about scope
(credentials, exfiltration, persistence, install time), so incident response
can start from the evidence as well as the generic advisory text.

Usage: python3 mal_scope.py package-lock.json"""
import json
import re
import sys
import urllib.request

OSV = "https://api.osv.dev/v1"
BOILERPLATE = "should be considered fully compromised"
SCOPE = re.compile(r"exfiltrat|credential|secret|token|persist|install|session|execut", re.I)


def post(url, body):
    req = urllib.request.Request(url, json.dumps(body).encode(), {"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=60) as r:
        return json.load(r)


def get(url):
    with urllib.request.urlopen(url, timeout=60) as r:
        return json.load(r)


def packages(lock):
    data = json.load(open(lock, encoding="utf-8"))
    seen = set()
    for path, meta in data.get("packages", {}).items():
        if path and "version" in meta:
            name = meta.get("name") or path.split("node_modules/")[-1]
            seen.add((name, meta["version"]))
    return sorted(seen)


def sections(details):
    parts = re.split(r"^## Source: ([\w-]+) \([0-9a-f]+\)\s*$", details, flags=re.M)
    head = parts[0].split("_-= Per source details")[0].strip(" \n-")
    out = [("summary", head)] if head else []
    out += [(parts[i], parts[i + 1].strip()) for i in range(1, len(parts) - 1, 2)]
    return out


def main(lock):
    pkgs = packages(lock)
    queries = [{"package": {"ecosystem": "npm", "name": n}, "version": v} for n, v in pkgs]
    results = post(f"{OSV}/querybatch", {"queries": queries})["results"]
    hits = [(p, v["id"]) for p, r in zip(pkgs, results) for v in r.get("vulns", []) if v["id"].startswith("MAL-")]
    print(f"{len(pkgs)} packages checked, {len(hits)} malware records")
    for (name, version), mid in hits:
        rec = get(f"{OSV}/vulns/{mid}")
        print(f"\n{name}@{version}: {mid}")
        analysed = False
        for source, text in sections(rec.get("details", "")):
            if BOILERPLATE in text:
                print(f"  {source}: generic advice (treat the machine as compromised, rotate secrets)")
                continue
            analysed = True
            sentences = re.split(r"(?<=[.!?])\s+", text.replace("\n", " "))
            print(f"  {source}: {sentences[0][:200]}")
            for line in [x for x in sentences[1:] if SCOPE.search(x)][:3]:
                print(f"    - {line[:200]}")
        iocs = (rec.get("database_specific") or {}).get("iocs") or {}
        if iocs.get("domains"):
            print(f"  IOC domains to look for in DNS and proxy logs: {', '.join(iocs['domains'][:6])}"
                  + (" ..." if len(iocs["domains"]) > 6 else ""))
        if not analysed:
            print("  no behaviour analysis in the record: assume the worst and rotate everything")
    return 1 if hits else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "package-lock.json"))

Run against a test lockfile with three known malware packages and one clean one:

$ python3 mal_scope.py package-lock.json
4 packages checked, 3 malware records

@apexacc/cli@1.6.2: MAL-2026-17924
  summary: @apexacc/cli is distributed by "Apex Accelerator" (Apex Arena), a fake Web3 accelerator that lures crypto founders with free AI credits and audits.
    - Elastic Security Labs (REF8801, published 2026-10-12) found it is a repackaged Oh My Pi coding agent (1,351 of 1,363 files identical) with malicious additions that hijack the developer's AI coding age
    - A bootstrap retrieves commands from vrf.apexaccs.org: on macOS it drops CloudSyncD (fake password prompt, credential theft, C2 ccmihr.com); on Windows a staged loader chain ends in an infostealer that
    - Independently confirmed by static inspection of the live 1.6.2 npm tarball (sha256 7ca5e3908dd8721d0cf6af2488f0825c0d91a5e0b7c8529ef830141a4a3ba31f) on 2026-10-16, without execution: the bin launcher 
  IOC domains to look for in DNS and proxy logs: apexaccs.org, arena.apexaccs.org, token.apexaccs.org, vrf.apexaccs.org, arena.apexfdn.xyz, verify.apexfdn.xyz ...

ichigo-baileys@1.0.5: MAL-2026-17445
  summary: This package is a fork of the Baileys WhatsApp Web library (npm) and belongs to the "PhantomSub" family described by OX Security in September 2026: the publisher injected code that, without the instal
    - Static review of the published tarball(s) found no code that sends WhatsApp credentials or session keys off-host, no install-time payload (the only install hook is Baileys' stock Node.js version check
  ghsa-malware: generic advice (treat the machine as compromised, rotate secrets)

winston-prism@1.0.1: MAL-2026-6905
  ghsa-malware: generic advice (treat the machine as compromised, rotate secrets)
  no behaviour analysis in the record: assume the worst and rotate everything
exit=1

The script doesn't decide anything. It puts the evidence next to the alert, so the person running the response can see whether they're dealing with an infostealer, a session abuser or an empty name squat. It exits with status 1 whenever there's a hit, so it can't be used to wave one through.

Detection is the start of that response. Here's Safeguard's interactive walkthrough of how malware and typosquat detection works:

What to do when the alert fires

  1. Start with the generic advice. Isolate the machine, stop builds that installed the package, and begin rotating the secrets that were on it. Don't wait for the analysis to decide.
  2. Read the full OSV record, not just the GitHub advisory. Each source has its own section. Look for when the code runs (install time or only when called), what it reads, where it sends data, and any indicators.
  3. Scope with the evidence. Search DNS and proxy logs for the indicator domains. Check which machines and CI runners actually installed that version. If the payload abuses a session (WhatsApp, a browser, a cloud CLI), secure that account too.
  4. Treat "no analysis" as worst case. If the record holds only GitHub's paragraph, you have nothing to narrow it with.
  5. Write down why you stopped where you did. "Rotated npm and GitHub tokens; analysis shows no other credential reads; no traffic to indicator domains" is a record an auditor can follow. Our npm incident response playbook covers the rest.

The fair view

GitHub's paragraph is doing its job. Most of these packages are built to steal, the advisory is generated without seeing your environment, and a responder who over-rotates loses an afternoon while one who under-rotates can lose a company. The analyses also describe one version at one point in time. A package that was empty yesterday can ship a payload today, so "no harmful behaviour found" is about the version analysed, not the publisher's intent.

The cost is that the same words go out for an empty placeholder and for a staged infostealer, and the evidence that tells them apart sits one click away in a different database. Teams that read it respond faster and more precisely. Teams that don't either rotate everything every time, which gets harder to sustain as alerts pile up, or start ignoring the paragraph, which is worse.

Check yourself

A Dependabot malware alert fires for a package in your lockfile, and the OSV record has only GitHub's paragraph. What do you do?

Follow it in full. Treat the machine as compromised, rotate its secrets from another computer, and don't rely on removing the package. With no analysis in the record, there's nothing to narrow the response with.

The record's analysis says the code uses the installer's WhatsApp session to follow channels and sends no credentials off the machine. Is rotating cloud keys enough?

No. Start with the generic steps, but the specific harm is to the WhatsApp account the library ran with. Remove the package, check which channels that account now follows, and secure the session.

An analysis says the analysed version contains no harmful code. Can you close the alert?

Not on that alone. Check that the version you installed is the one analysed, that nothing else in the record or its indicators shows up in your logs, and record why you closed it. A publisher can ship a payload in a later version.

Never miss an update

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

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.