Skip to content

Online Resolution

github-actions[bot] edited this page Aug 26, 2026 · 5 revisions

Online resolution

--online is the only networked part of postmortem. For each unique dependency it:

  1. asks the ecosystem's registry for the source repository (npm's repository, PyPI's project_urls, crates.io's repository, …),
  2. resolves it to a host/owner/repo and pulls reputation stats from that host (stars, created-at, last activity, archived, primary language),
  3. scores it against risk thresholds and surfaces the suspicious ones.

See Ecosystems & Hosts for the registry/host matrix. Everything is cached under ~/.postmortem/cache/ - see Cache.

Risk signals

Signal Tier Points
typosquat of <pkg> High (red) 45
starjacking (<repo> doesn't own it) (npm) High 45
install-script-added (npm) High 40
recently-created (Nd ago) High 40
provenance-removed (npm, crates.io) High 30
low-stars (N★) High 30
archived Medium (amber) 30
new-publisher (npm, crates.io) Medium 25
dangling-repo (<repo> not found) (GitHub) Medium 25
stale (Nd idle) Medium 20
dormant-release (Nd gap) (npm, crates.io, PyPI) Medium 20
newborn-package (Nd old) (npm, crates.io, PyPI) Medium 20
fresh-release (Nh old) (npm, crates.io, PyPI) Low 15
no-repository / resolve-failed / stats-* Info (unchecked) 0
  • Reputation signals come from the source repo's stats vs. your thresholds (min_stars, recent_days, stale_days).

  • Identity signals: typosquat (see Typosquatting — it covers six ecosystems, offline) and starjacking (the linked repo doesn't declare the package — its stars are borrowed).

  • Provenance signals compare the installed version against the one published before it, in whichever document its registry publishes a release history in — npm's packument, crates.io's crate record, PyPI's project JSON: install-script-added, dormant-release, new-publisher, newborn-package (first-ever release <30d), fresh-release (this version <48h — the release-age cooldown), and provenance-removed (a version that dropped the OIDC/Trusted-Publishing attestation an earlier one had — the axios pattern; dist.attestations on npm, trustpub_data on crates.io).

    See provenance coverage for which registry answers which question.

  • dangling-repo — a declared GitHub repo that 404s (deleted/renamed), so the handle is re-registerable (repojacking exposure).

  • no-repository means "we couldn't find a source repo to assess" - an absence of information, so it's counted as unchecked, not suspicious.

Provenance coverage

No registry answers every question, and one that cannot be asked is never reported as a clean answer — an unanswerable signal is simply absent:

Signal npm crates.io PyPI
install-script-added yes — —
dormant-release yes yes yes
new-publisher yes yes —
provenance-removed yes yes —
fresh-release / newborn-package yes yes yes
maintainer set (--human) yes — yes

RubyGems, Packagist and deps.dev (Java, Go) publish a package's current state rather than its history, so none of these apply there. The OS package managers have no registry of this kind at all.

The gaps are registry limits, not pending work:

  • PyPI records no per-release uploader, so new-publisher has nothing to compare. Its PEP 740 attestations exist but need one /integrity/{project}/{version}/{file}/provenance request per file, rather than riding along in a document already fetched.
  • crates.io does not say whether a crate has a build.rs, and its owner list is a separate /owners call.

What each costs

Ecosystem Requests for the history
crates.io none — the crate record fetched for the repo and license already carries every version.
PyPI one. The version-pinned document postmortem fetches (a license is per-version) has no releases map; the name-only document does.
npm one packument, shared with timeline.

The risk:dep score

Every node shows a (risk:dep) pair, each 0-100:

  • risk - the package's own risk, summed from its signal points (capped).
  • dep - its dependency-subtree risk: distinct flagged deps weighted by severity. Platform/scope splits of the same module (e.g. @napi-rs/nice-*) don't inflate it.

Coloring: a package flagged on its own is red/amber; a clean package that drags in a rotten tree (high dep) is painted blue. The closing gochi's recap aggregates the whole forest into overall risk N · dep M plus a headcount of high-risk / suspicious / unchecked packages.

--languages

By default the node shows the repo's primary language (free - GitHub returns it in the repo object): express@4.18.2 ★66000 (0:0) (JavaScript).

--languages fetches the full breakdown (one extra, cached, /languages call per repo - paid once per repo, ever):

ripgrep@14.1.0 ★66000 (0:0) (Rust:95.0|Python:2.4|Shell:1.9|Other:0.7)
  • (Lang) - primary only (GitHub, free).
  • (L1:%|L2:%|Other:%) - full breakdown (--languages).
  • (?) - a repo resolved but the host reported no language (e.g. GitLab/Codeberg without --languages).

Tokens & rate limits

Without a token, GitHub's anonymous API is 60 requests/hour - the tightest budget, so postmortem fans out gently (2 workers) and wide (8) with a token. GitLab and Codeberg resolve anonymously for public repos. See Configuration.

Clone this wiki locally