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.
# 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.
| Source | What it contributes | Characteristic weakness alone |
|---|---|---|
| OSV.dev | Broad, ecosystem-native advisories with precise affected-version ranges | Coverage depends on upstream ecosystems reporting |
| GitHub Advisory Database | Curated advisories with good ecosystem mapping and review | Skewed toward projects developed on GitHub |
| NVD / NIST | Canonical CVE records and CVSS scoring | Slow; version ranges are often coarse or wrong for package ecosystems |
| CISA KEV | Vulnerabilities with confirmed real-world exploitation | Deliberately narrow — presence is a strong signal, absence means little |
| npm security advisories | Registry-level actions, malicious-package removals | npm only |
| PyPI malware reports | Reported malicious Python packages | PyPI only |
| Socket.dev | Behavioural signals — install scripts, network access, filesystem access, obfuscation | Behaviour is suspicion, not proof |
| Sonatype | Research on malicious and suspicious components across ecosystems | Commercial coverage boundaries |
| ReversingLabs | Binary and artefact analysis of published packages | Focused 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:
- Do we have it at all?
- Where, and at which resolved versions?
- How did it get in — did we choose it, or did something else bring it?
- What do we have to change to be rid of it?
- 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.
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.
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.
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.
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.
