Docs / Intel feed

Protet Intel feed guide

Protet publishes the npm package versions it has confirmed as malicious, with counts of what it screened. The feed is public, free and needs no account or key.

What is in it

Only named entries are published: an npm package version that a Protet reviewer confirmed as malicious, or marked BTP (behaviour that matches malware patterns from a publisher assessed as legitimate, for example security tooling; this is not malware and is never reported to OSV). Everything else Protet looked at appears only in the totals. Today the only ecosystem is npm; every address carries the ecosystem so others can be added without changing the URLs.

Browse the same data as pages: /intel (numbers and latest entries), /intel/npm (all npm entries) and one page per entry. The numbers are defined on /intel/methodology.

Endpoints

All are GET, served from https://protet.io, unauthenticated, byte-for-byte as published.

URLContent typeContents
/intel/v1/all/findings.ndjsonapplication/x-ndjsonOne JSON object per line, every named entry.
/intel/v1/npm/findings.ndjsonapplication/x-ndjsonThe same for npm only.
/intel/v1/all/feed.atomapplication/atom+xmlAtom feed of every named entry.
/intel/v1/npm/feed.atomapplication/atom+xmlAtom feed, npm only.
/intel/v1/npm/osv/PROTET-NPM-YYYY-N.jsonapplication/jsonOne OSV record (schema 1.6) per confirmed package version. The id comes from public_id in the NDJSON.
/intel/v1/stats.jsonapplication/jsonThe counts shown on /intel.

Any other path under /intel/v1/ returns 404. If the storage behind the feed is briefly unavailable you get 503 with Retry-After: 30; retry.

Update cadence and polling

The files are served with Cache-Control: public, max-age=300 (cached for 5 minutes), so polling more often than that returns the same data. A sensible schedule is every 15 to 60 minutes. The responses carry no ETag, so fetch the whole file each time.

NDJSON fields

FieldMeaning
schema_versionThe version of this file format, MAJOR.MINOR (now 1.0); stats.json carries it too. New fields keep the major version, so ignore fields you do not know; a breaking change gets a new /intel/v2/ path.
public_idStable id, for example PROTET-NPM-2026-29. Also the OSV record id.
ecosystem, package, versionWhat the entry names. npm, the package name and the exact version.
classificationtp (confirmed malicious) or btp (legitimate publisher, malware-shaped behaviour).
statusactive, withdrawn or reclassified. See Withdrawals.
summary, detailsA one-line summary and a longer description of what the package does. Plain text.
published_at, updated_atUTC timestamps (ISO 8601).
urlThe entry's page on protet.io. Use this rather than building the path yourself.
osv_aliasesIds for the same package version in other OSV databases, when known.
iocsOptional. Indicators found in the package (domains, ips, urls, paths). Treat them as text; do not open them.

Examples

Count entries and list the newest five (requires jq):

curl -s https://protet.io/intel/v1/all/findings.ndjson | wc -l

curl -s https://protet.io/intel/v1/npm/findings.ndjson \
  | jq -r 'select(.classification == "tp") | [.published_at, .package, .version, .public_id] | @tsv' \
  | sort -r | head -5

Check an npm lockfile (package-lock.json, lockfile version 2 or 3) against the confirmed-malicious list. It prints every installed package version that is in the feed, and prints nothing when none is:

curl -s https://protet.io/intel/v1/npm/findings.ndjson \
  | jq -r 'select(.classification == "tp" and .status == "active") | "\(.package)@\(.version)"' \
  | sort -u > protet-bad.txt

jq -r '.packages | to_entries[] | select(.key | startswith("node_modules/"))
       | "\(.key | sub(".*node_modules/"; ""))@\(.value.version)"' package-lock.json \
  | sort -u > installed.txt

comm -12 protet-bad.txt installed.txt

Run in CI, fail the job when the last command prints anything:

test -z "$(comm -12 protet-bad.txt installed.txt)" || { echo "confirmed-malicious package installed"; exit 1; }

OSV records

Each confirmed package version has one record in the OSV schema, version 1.6, at /intel/v1/npm/osv/<public_id>.json:

curl -s https://protet.io/intel/v1/npm/osv/PROTET-NPM-2026-29.json | jq '{id, summary, affected, published, modified}'

The record names the package under affected[].package and lists the exact affected versions; references links back to the entry page, and credits names Protet as finder. Any tool or script that reads OSV records can load them. To mirror them all:

mkdir -p protet-osv
curl -s https://protet.io/intel/v1/npm/findings.ndjson \
  | jq -r 'select(.classification == "tp") | .public_id' \
  | while read -r id; do curl -sf "https://protet.io/intel/v1/npm/osv/$id.json" -o "protet-osv/$id.json"; done

BTP entries have no OSV record. If an entry is later withdrawn, its OSV record is kept and marked withdrawn.

Atom

Point a feed reader at https://protet.io/intel/v1/all/feed.atom (or the npm one). Each entry has a stable id (tag:protet.io,YYYY-MM-DD:intel/PROTET-NPM-YYYY-N), a title such as name@version confirmed malicious, the summary, and a link to the entry page. Quick check:

curl -s https://protet.io/intel/v1/all/feed.atom | grep -c "<entry>"

Stats and definitions

stats.json holds the numbers on /intel: screened, flagged, confirmed, false_positives, btp, missed_found_later, in_review, precision, false_positive_rate and a last_7_days block, once for all and once per ecosystem under ecosystems. Units differ (some count package versions, some version-days), so read the methodology before comparing them. When false_positive_rate_is_floor is true the rate is a lower bound because some flagged versions are still in review.

curl -s https://protet.io/intel/v1/stats.json | jq '.all | {screened, flagged, confirmed, false_positives, btp, in_review}'

Withdrawals and corrections

A published entry is never deleted. If a reviewer changes a verdict, the entry's status becomes withdrawn (no longer confirmed) or reclassified (moved between confirmed and BTP), with updated_at set to the date of the change. Filter on status == "active" if you only want current entries. Think an entry is wrong? Write to hello@protet.io.