OSPulse

Threat intelligence

A feed of everything bad happening in open source is not intelligence. It is a firehose. Intelligence is the small subset of that firehose that intersects your dependency graph — and knowing, for each intersection, exactly what of yours is standing in the blast radius.

Two different objects: the advisory and your exposure

The single most important distinction in this part of the product is between a fact about the world and a fact about you.

A vulnerability is a statement about a package: some versions of left-pad are affected by something, with a severity, a source, and a fix. It is true whether or not you have ever heard of that package. Millions of organisations share the identical record.

An exposure is a statement about your estate: this repository, at this resolved version, reached through this dependency path, is affected. It has a lifecycle — it gets triaged, assigned, accepted, mitigated, or fixed — and it belongs to you.

The API mirrors that split, and so should your workflow.

bash
# The world: advisories OSPulse knows about
curl https://api.ospulse.app/api/v1/vulnerabilities \
  -H "Authorization: Bearer $OSPULSE_TOKEN"
 
# You: where those advisories actually land in your estate
curl "https://api.ospulse.app/api/v1/vulnerability-exposures?take=50" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Teams that work the advisory list end up reading security news. Teams that work the exposure list end up shipping fixes. Exposures are the queue; advisories are the reference material behind it.

Exposures carry state, advisories do not

PATCH /api/v1/vulnerability-exposures/{exposureId} transitions an exposure through your triage workflow. An advisory has no state to change — it is upstream fact. Anything you want to record as a decision (accepted risk, not exploitable in our configuration, fixed in the next release) belongs on the exposure.

The aggregation model

No single feed is sufficient, and the gaps between them are not random — they are systematic, because each source exists for a different reason and is fed by a different process.

SourceWhat it contributesCharacteristic weakness alone
OSV.devBroad, ecosystem-native advisories with precise affected-version rangesCoverage depends on upstream ecosystems reporting
GitHub Advisory DatabaseCurated advisories with good ecosystem mapping and reviewSkewed toward projects developed on GitHub
NVD / NISTCanonical CVE records and CVSS scoringSlow; version ranges are often coarse or wrong for package ecosystems
CISA KEVVulnerabilities with confirmed real-world exploitationDeliberately narrow — presence is a strong signal, absence means little
npm security advisoriesRegistry-level actions, malicious-package removalsnpm only
PyPI malware reportsReported malicious Python packagesPyPI only
Socket.devBehavioural signals — install scripts, network access, filesystem access, obfuscationBehaviour is suspicion, not proof
SonatypeResearch on malicious and suspicious components across ecosystemsCommercial coverage boundaries
ReversingLabsBinary and artefact analysis of published packagesFocused on the artefact, not the project around it

Enterprise plans can additionally register custom and private feeds, which matters more than it sounds: internal red-team findings, sector ISAC bulletins, and vendor-specific notices are frequently the earliest signal an organisation has, and they are useless if they live in an inbox instead of in the same pipeline as everything else.

Why aggregation is not concatenation

Merging feeds naively produces the worst of both worlds — duplicates and gaps at the same time. Three problems have to be solved before a feed is useful.

Identity. The same issue appears as a CVE, a GHSA, an OSV record, and a vendor identifier. Unless those are reconciled to one vulnerability, a single problem shows up four times and your team learns that the counts are meaningless.

Version range fidelity. An advisory that says "affects versions before 2.4.1" has to be evaluated against the version you resolve, per ecosystem, with that ecosystem's own comparison rules. Coarse ranges are the main cause of false positives, and false positives are the main cause of teams switching alerts off.

Severity in context. A high CVSS score on a parser you never expose to untrusted input is less urgent than a moderate one in your authentication path. Presence on CISA KEV — confirmed exploitation in the wild — usually outranks the raw score, because it is evidence of what attackers are actually doing rather than of what is theoretically possible.

The attack classes

Advisories describe vulnerabilities. Supply-chain attacks are frequently not vulnerabilities at all — the code does exactly what its author intended. These are the classes OSPulse looks for, and the signals that give each one away.

Account takeover

A maintainer's registry credentials are stolen or phished, and a malicious version is published under a name with years of accumulated trust. ua-parser-js in 2021 is the reference case: three versions were published from a compromised account and pulled cryptominer and credential-stealing payloads onto machines that installed them within hours.

Signals: a publish from an unfamiliar identity, a release that breaks an established cadence, a version that appears without a corresponding commit in the source repository, sudden install-script additions.

Malicious injection

Malicious behaviour introduced through the normal contribution path, by a maintainer who was granted access legitimately. event-stream in 2018 is the archetype — an unmaintained package handed to a volunteer who then added a dependency that targeted a specific cryptocurrency wallet.

Signals: recent changes in publishing rights, a new maintainer with no history, a new dependency added by a project that had been dormant.

Typosquatting

A package published under a name close to a popular one, relying on a typo, a hyphen, a singular/plural, or a namespace that looks official. Costs nothing to attempt, so it is attempted constantly across every registry.

Signals: high name similarity to a widely-used package, disproportionately new, no meaningful history, install scripts on a package that has no reason to need them.

Dependency confusion

Demonstrated at scale by Alex Birsan in 2021. An organisation uses an internal package name; an attacker publishes that same name on the public registry with a higher version; the resolver, given both, prefers the public one. No vulnerability exists anywhere — the resolution order is the whole attack.

Signals: an internal package name appearing on a public registry, a public package matching your private namespace, resolution sources that differ from what your configuration intends.

Domain and infrastructure hijack

The package is untouched; the infrastructure it depends on changes hands. polyfill.io in 2024 is the clean example — a widely-embedded script domain was acquired and began serving malicious content to a subset of visitors, with no change to any package version anywhere.

Signals: ownership changes to domains referenced in package metadata, changed download or CDN origins, install-time fetches from hosts that recently changed control.

Sabotage by the maintainer

The author deliberately breaks or weaponises their own package. colors.js and faker.js in 2022 — an infinite loop and garbage output pushed to packages with tens of millions of weekly downloads, as protest. Not malware in the traditional sense; identical in effect to an outage you cannot roll back.

Signals: maintainer statements about funding or licensing disputes, abrupt unexplained releases, sudden functional removals, repository archival with continued publishing.

Long-horizon backdoor

The patient version. A contributor builds genuine trust over months or years, becomes a maintainer, and then introduces a backdoor through a path that ordinary review does not cover. xz-utils (CVE-2024-3094) in 2024 hid the payload in test fixtures and the build system, and was found by an engineer investigating an anomalous latency measurement rather than by any scanner.

Signals: build-system and test-fixture changes disproportionate to source changes, obfuscated or binary assets in a source distribution, a release artefact that does not match what the repository would produce.

Detection is probabilistic, and that is the honest position

None of these signals is proof. Individually most have innocent explanations — projects legitimately change maintainers, add install scripts, and archive repositories. Threat intelligence raises the probability that something deserves a human decision; it does not replace the decision. Any product claiming certainty here is selling you a false one.

Blast radius

When a signal fires, the operational question is never "is this bad in general". It is:

  1. Do we have it at all?
  2. Where, and at which resolved versions?
  3. How did it get in — did we choose it, or did something else bring it?
  4. What do we have to change to be rid of it?
  5. Which of those places actually matter?

Blast-radius analysis answers all five from the dependency graph.

Find every place the package appears

Usage resolves the package to the repositories that actually depend on it, with the versions they resolve.

bash
curl https://api.ospulse.app/api/v1/packages/$PACKAGE_ID/usage \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Trace how it got there

The dependency graph gives you the path. This is the step that determines the remediation: a direct dependency is a version bump you control, while a package four levels deep is a conversation with an intermediate maintainer — or a temporary resolution override.

bash
curl "https://api.ospulse.app/api/v1/repositories/$REPO_ID/dependency-graph?maxEdges=500" \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Confirm the exposure precisely

Package-level advisories, matched against the versions you resolve rather than the ranges you declare.

bash
curl https://api.ospulse.app/api/v1/packages/$PACKAGE_ID/vulnerabilities \
  -H "Authorization: Bearer $OSPULSE_TOKEN"

Rank by consequence, not by score

An exposure in an internal tool and the same exposure in an internet-facing service that handles customer data are not the same work item. Repository criticality and lifecycle status, set when you import a repository, are what let the queue sort itself sensibly.

Record the decision on the exposure

Every exposure ends in a decision — fixed, mitigated, accepted, or not applicable. Recording it on the exposure is what stops the same finding being re-triaged next month, and it is what your auditors will ask to see.

bash
curl -X PATCH https://api.ospulse.app/api/v1/vulnerability-exposures/$EXPOSURE_ID \
  -H "Authorization: Bearer $OSPULSE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "...": "state transition" }'

For the portfolio view across every repository at once, GET /api/v1/dashboards/global returns the aggregated exposure summary and the riskiest packages and repositories — the same data, arranged for a weekly review rather than an incident.

Speed is the whole game

Every documented incident above shares one shape: a window between the malicious thing going live and the world knowing about it. ua-parser-js was live for a matter of hours. polyfill.io served payloads for days. xz-utils sat in place for months. Your total exposure is that window plus your own detection and response time, and the second term is the only one you control.

Three mechanisms shorten it.

Continuous re-observation. Threat intelligence is worthless if it is only consulted at build time. A dependency you have not rebuilt in six weeks is still deployed and still exposed. Scan on a cadence, not only on commit.

Push, not poll. Route new exposures at the severities you care about into the channel your team actually reads — see Alerts and Set up alerts. A finding discovered on a dashboard nobody opened is not a detection.

Automatic enforcement at the boundary. Policies stop a known-bad component entering the build in the first place, which converts an incident into a failed pipeline. See Policy gates.

Rehearse the removal

Pick a package deep in your graph and answer, end to end, how you would remove it today. Most teams discover their real constraint is not detection but the transitive path — and the time to find that out is not during an incident.

Next steps