Skip to content

Interest: maintainer-change and typosquat detection as additional supply-chain signals (complementary to OSV) #602

Open

Description

@presendapp

Disclosure: I'm the developer of Presend (https://presend.pages.dev), flagging that upfront -- this is a self-interested suggestion.

Saw osv_client.py and static_patterns_supply_chain.py -- good to see OSV.dev used directly for known vulnerabilities, that's the right call (not proposing a wrapper around that).

What might be a genuine addition to the supply-chain analyzer specifically: Presend's maintainer-change-check flags an npm publisher change (npm-only for now) after a long dormancy period -- the event-stream pattern. This is a leading signal (a package becoming capable of shipping something malicious) rather than a lagging one (OSV only has already-disclosed CVEs), so it's genuinely complementary rather than overlapping. typosquat-check is the other potentially relevant one. Both free, no key.

I don't have visibility into your analyzer node interface/contract, so I don't want to guess at Python that might not match your real conventions. Happy to look at an existing analyzer node's structure and put together a properly-formatted one if this is a direction you'd want.

Docs: https://presend.pages.dev/api


Edit (2026-09-25): corrected an inaccurate claim above. maintainer-change-check matches the event-stream pattern (a previously unseen publisher taking over after long dormancy). It does not detect a hijacked existing account (ua-parser-js) or a malicious release by an existing maintainer (colors.js), and it is npm-only. It now also only flags recent takeovers (last 365 days), after testing showed false positives on years-old legitimate handoffs.

Activity

  1. rng1995 commented on Sep 28, 2026

    @rng1995
    Collaborator

    Related work is now proposed in PR #647, which refines SC6 typosquat matching and reduces known-package false positives. It is open and unmerged, and covers only the typosquat portion of this discussion—not maintainer-change detection. Keeping the broader proposal open.

  2. rng1995 commented on Oct 4, 2026

    @rng1995
    Collaborator

    The SC6 typosquat work in PR #647 and its performance/false-positive follow-up PR #687 have both merged. They add native matching improvements and controls without a new external service dependency.

    This supersedes the earlier in-progress status for #647. Maintainer-change detection is not implemented by either PR, so this broader proposal remains open.

  3. presendapp commented on Oct 4, 2026

    @presendapp
    ContributorAuthor

    Thanks for the update. Once #722 is settled, I'll post a native design for maintainer-change detection here before writing any code: npm only (the registry metadata it needs, _npmUser per version and publish times, is already in the packument; PyPI's JSON API does not expose the uploading account per release, and its author/maintainer fields are self-declared), flagging a new publisher after a dormant period, the event-stream pattern. It would not catch a compromised existing account. To keep noise down, a publisher who already maintains another widely used package is reported separately rather than flagged, and only registry trusted publishing (_npmUser.trustedPublisher), not account names, exempts CI releases. In my measurements on the top 200 npm-high-impact packages, replayed at 9 dates over two years, that takes flagged packages from 13 to 1, and an event-stream replay at 2018-11-26 is still detected. No external service would be involved.

  4. presendapp commented on Oct 5, 2026

    @presendapp
    ContributorAuthor

    Here is the design I mentioned, for review before any code. It stays native: a new SC rule would read the npm registry directly, the same way SC4 reads OSV, and no other service is involved.

    What it detects, and what it doesn't

    • The event-stream pattern: a release published by an account that had never published the package, after the package had been dormant (the 2018 case: event-stream 3.3.5, published by right9ctrl).
    • It does not detect a hijacked existing account (ua-parser-js) or a malicious release by the original maintainer (colors.js): the publisher identity does not change in those cases.
    • npm only. PyPI's JSON API does not expose the uploading account per release, and its author/maintainer fields are self-declared.

    Data
    One GET of the full packument, https://registry.npmjs.org/<name> (scoped names encoded as @scope%2Fname). The abbreviated install document has no _npmUser, so the full one is needed. Per version: versions[v]._npmUser.name, versions[v]._npmUser.trustedPublisher and time[v].

    Rule (a new SC rule; the ID is yours to pick, since SC10 is dependency-source redirection)
    For each non-prerelease version published in the last 365 days, in publish order:

    • skip it if the previous release is less than 180 days older (the package was not dormant);
    • skip it if _npmUser.name already published an earlier version;
    • if _npmUser.trustedPublisher is set (written by the registry on OIDC trusted publishing, not chosen by the publisher), report a CI handover, not a finding. Account names such as *-bot or *-release are not exempted, since anyone can pick them;
    • otherwise it is a candidate.

    Prereleases are ignored, since default semver ranges do not install them.

    Established publishers
    On popular packages, most candidates are legitimate handovers (Express maintainers taking over mime-types or http-errors, ljharb taking over hasown). A candidate whose new publisher already maintains another package with 100,000+ weekly downloads is not flagged. That takes one more npm request per new publisher: registry.npmjs.org/-/v1/search?text=maintainer:<name>&size=20, whose results already include weekly download counts (downloads.weekly), so the first 20 results are considered. This endpoint rate-limits quickly (HTTP 429 after about 10 fast requests in my tests), so results would be cached per publisher and counted in the budget; when the check cannot complete, the candidate stays flagged and the finding says so. This is a triage signal, not a guarantee: an attacker who already controls a popular package passes it.

    Integration

    • npm_registry_client.py, modelled on osv_client.py: httpx, SKILLSPECTOR_NPM_REGISTRY_TIMEOUT, one aggregate budget shared across manifests (packages, requests, bytes), non-fatal limitations surfaced like OSV's.
    • Response size: full packuments of large packages are big (TypeScript's is about 16 MB), so the byte cap would need to be well above OSV's 4 MB, or those packages are listed as a limitation instead of checked. Your call on the number.
    • Registry unreachable: no finding from this rule, plus a limitation. There is no meaningful static fallback here, and "no finding" must not read as "checked".
    • Packages whose npm source SC10 reports as redirected are skipped, since registry.npmjs.org would describe a different package.
    • Severity: MEDIUM, confidence around 0.5, with a message along the lines of "Possible maintainer takeover: was published by after days of inactivity". LOW when the manifest pins an exact version published before the event, since that release is not affected. Ranges are not evaluated, to avoid adding a semver dependency.

    Measurements (rerun today)
    Top 200 npm-high-impact packages, replayed at 9 dates over two years: 15 raw events on 13 packages, 13 -> 1 after the established-publisher step (source-map, whose new publishers have no other popular package). In the current 365-day window: 4 -> 1 out of 200. event-stream replayed at 2018-11-26 is still detected. Reproduction: https://github.com/presendapp/presend-source/tree/main/tests/maintainer-change (a Node script that reads the npm registry directly; no Presend service is called).

    Tests
    Offline fixtures only: a trimmed event-stream packument, a trusted-publishing handover, a bot-named account without trusted publishing (flagged), a prerelease, a gap under 180 days, and a 429 from the search endpoint.

    If this shape works for you, I'll open a PR on this basis. Any part can be dropped or changed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions