# Lifecycle data: updates, signing, and trust

Offing's findings come from lifecycle rules (`registry/`). The rules that ship inside each release are always available. Between releases you can fetch newer rules, which are signed so that Offing can tell they came from the project's release workflow. This page explains how that works, what is checked, and what Offing promises.

## What you can rely on

- **Scans never use the network.** A scan, `check`, `explain`, `context`, `baseline`, `doctor`, and `data status` read local files only. The only command that connects to anything is `offing data update`, and it runs only when you run it. A test fails if any other code path opens a socket, resolves a name, or calls `fetch`.
- **Downloaded data is verified before it is used, every time it is loaded.** If it cannot be verified, Offing uses the data shipped with the installed release and says so in the result. It never loads data it could not verify.
- **You can see how fresh the evidence is.** Every scan and check says which data it used and how old it is, and warns when that is more than 30 days.
- **You can fix the data version** for reproducible CI.
- **No account, no telemetry.** Updating asks a public release list and downloads public files. Nothing about you or your repository is sent.

## Commands

```bash
offing data status                       # which data is in use, where it came from, how old it is (--json for tools)
offing data update                       # fetch and verify the newest signed bundle
offing data update --data-version 2026.10.01   # fetch exactly that bundle, without making it active
offing scan . --data-version 2026.10.01  # use exactly that bundle (every command takes this)
OFFING_DATA_VERSION=bundled offing scan .   # use only the data shipped with this release
```

`data status` reports one of three sources: **bundled** (shipped with the release), **cache** (the newest verified download), or **pinned** (the version you asked for). Its age is counted from when a downloaded bundle was built, or from the newest review date among the rules for bundled data.

### Updating

`offing data update` lists the project's `data-v…` releases, picks the newest one that has a bundle and a signature, downloads both (https only, with size limits and timeouts), verifies them completely, and only then stores them in your cache and makes them active. If anything is wrong the previous data stays active and nothing is written. Running it again when nothing changed says so.

It refuses a bundle that is older than the active one (a replayed old bundle would roll your rules back), a bundle the installed release already beats, and anything that fails verification. Three bundles are kept.

Downloads honor `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` (Node.js 22.21 or newer). To use a mirror, point `EBB_DATA_URL` at its release list.

### Pinning

`--data-version X` or `OFFING_DATA_VERSION=X` (the flag wins) makes Offing use exactly bundle `X` from your cache, or `bundled` for the data shipped with the release. Unlike the automatic choice, a pin that cannot be met **stops the run** with exit code 3 (a data failure) instead of quietly using something else, so a CI job that pins its data either uses it or fails. The message tells you how to fetch it: `offing data update --data-version X`. A pinned bundle is verified just like any other, and is used whatever its age (the stale warning still appears).

### Where the data is stored

The data belongs to Ebb, so it lives in Ebb's cache: `EBB_DATA_DIR` if set, otherwise `~/Library/Caches/ebb` (macOS), `$XDG_CACHE_HOME/ebb` or `~/.cache/ebb` (Linux), or `%LOCALAPPDATA%\ebb\Cache` (Windows). Inside, `data/<version>/` holds `bundle.json` and its signature `bundle.json.sigstore.json`, and `data/active.json` names the active version. Deleting the directory returns Offing to the bundled data.

## What is in a bundle

One JSON file (`ebb.data-bundle.v1`, [schema](https://github.com/velella-sh/ebb/blob/main/schemas/data-bundle.v1.schema.json), [decision record](decisions/0006-data-bundle.md)) with a manifest and the files the detectors read: the lifecycle registries (one JSON file per ecosystem under `registry/`), the GitHub Actions SHA map, and the OSV cross-reference (`registry/osv.json`). The manifest lists each file's SHA-256, size, and rule count, a content address for the whole set, the build time, and the oldest Offing version that can read it. Versions look like `2026.10.16`, with `.N` for a second bundle on the same day. Building the bundle is deterministic: the same registry files and build time give the same bytes.

### OSV advisories

`registry/osv.json` maps lifecycle rule ids to OSV advisory identifiers that have **no fixed version** for that package. The `osv` [importer](https://github.com/velella-sh/ebb/blob/main/docs/IMPORTERS.md) drafts those rows at maintainer time from the OSV API; scans never call OSV. When a scan produces a finding whose `ruleId` appears in that file, the finding lists those ids in `advisoryIds` (and as an `Advisories:` line in text output). The risk score can use the count; Offing never fetches advisories during a scan.

## Trust model

A bundle is signed with **Sigstore keyless signing**. There is no long-lived signing key to steal or lose: the release workflow proves who it is to Sigstore with a short-lived GitHub identity token, receives a certificate that lasts minutes, signs the bundle, and the signature is recorded in a public transparency log.

Offing accepts a bundle only if **all** of these hold, checked offline:

1. The signature verifies against the exact bytes downloaded (certificate chain, validity period, transparency-log inclusion).
2. The signer is the project's release workflow: issuer `https://token.actions.githubusercontent.com` and an identity matching

   ```text
   ^https://github\.com/velella-sh/ebb/\.github/workflows/data-release\.yml@refs/(?:tags/data-v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?|heads/main)$
   ```

   A valid signature from any other workflow, repository, or branch is refused.
3. The format is one Offing understands and its `minimumEbbVersion` is not newer than the installed release.
4. The bundle is complete, and every file matches its digest and size, with nothing missing or extra.

The certificates and log keys come from the Sigstore public-good trusted root, which ships in the `@velella/ebb` package (`data/sigstore-trusted-root.json`) so that verification needs no network. That is also why you should keep Offing reasonably current: if Sigstore rotates its keys and the root in your installed release is too old, signatures made after the rotation stop verifying. Offing then falls back to its bundled data and tells you; it does not skip the check.

To check a release by hand with [cosign](https://docs.sigstore.dev/):

```bash
cosign verify-blob \
  --bundle ebb-data-2026.10.16.json.sigstore.json \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github\.com/velella-sh/ebb/\.github/workflows/data-release\.yml@refs/(?:tags/data-v\d{4}\.\d{2}\.\d{2}(?:\.\d+)?|heads/main)$' \
  ebb-data-2026.10.16.json
```

### What this does not protect against

- A compromise of the repository's release workflow or of a maintainer with permission to push a `data-v…` tag: the workflow would sign whatever it was given. Rule changes are reviewed before they reach `main`, and the signature proves where a bundle came from, not that its contents are correct.
- A compromised Offing package: the trusted root and the signer identity are part of the release you installed.
- Wrong evidence. Signing says who published a rule, not that the claim is true; each rule cites its sources and review date for that reason.

### Release log

Every data release also publishes `release-log.jsonl`, a tamper-evident log of all data releases so far, with its own keyless signature (`release-log.jsonl.sigstore.json`). Each line is one release: version, `contentDigest`, the SHA-256 of the bundle file, the build time, the hash of the previous line, and its own hash. Because every entry commits to the one before it, changing, removing, or reordering a past release changes every later hash.

The release workflow downloads the previous release's log, refuses one that does not verify, appends the new bundle, and requires the result to extend the old log before signing it. The signatures of the bundle and of the log are recorded in the public Sigstore transparency log (Rekor), so a quiet rewrite would need the log's hashes and Rekor's entries to change together.

Check a log, and optionally that it extends an older copy you kept:

```bash
gh release download data-vYYYY.MM.DD --repo velella-sh/ebb --pattern 'release-log.jsonl*'
node dist/tools/data-release-log.js check release-log.jsonl --previous older-release-log.jsonl   # in a checkout of Ebb
```

The first release made after this was added starts the log; earlier releases are not listed.

## When something is wrong

Offing reports every refusal in the result, in the scan's `diagnostics` and in `data status`, for example:

```text
Ignoring downloaded lifecycle data 2026.10.16: registry/npm.json does not match its digest. Using the data bundled with Offing 0.3.0.
```

Common causes are a damaged download (run `offing data update` again), a bundle that needs a newer Offing (upgrade), and an expired trust root (upgrade). Data that is merely old is not an error; it is flagged stale after 30 days, and `offing data update` is the remedy.

## License and attribution

The lifecycle dataset is licensed separately from the scanner. It ships in the [`@velella/ebb`](https://github.com/velella-sh/ebb) package: the registry JSON under `registry/` and the first-party schemas under `schemas/` are licensed under [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/), which allows use and redistribution, including commercially, with attribution and an indication of changes. Ebb's code is MIT. The package ships the data license as `LICENSE-DATA` and the attribution notice as `ATTRIBUTION.md`. Everything in this repository is [Apache-2.0](../LICENSE).

To reuse the dataset, credit it and link the license, for example:

> Lifecycle data from Ebb (`@velella/ebb`), by Armin Azimi and Ebb contributors, licensed under CC BY 4.0. https://github.com/velella-sh/ebb

Every rule carries its own `sources` (title, URL, and the `accessedAt` day it was read) and `reviewedAt`, so a consumer can trace a claim back to the page that states it and the date it was retrieved. `registry/sources.lock.json` records the current title and final URL of each cited page for maintainers and stays out of the published package. The upstream facts remain subject to the terms of the pages they come from; the license covers Ebb's selection, arrangement, normalization, and original text. Product and project names are trademarks of their owners and do not imply endorsement.

## Publishing a bundle

Bundles are published from [Ebb](https://github.com/velella-sh/ebb): a maintainer pushes a `data-vYYYY.MM.DD` tag there, and its [release workflow](https://github.com/velella-sh/ebb/blob/main/.github/workflows/data-release.yml) builds the bundle twice and requires identical bytes, signs it, verifies the signature, and attaches the bundle, the signature, and checksums to a GitHub release. See Ebb's [guide](https://github.com/velella-sh/ebb/blob/main/docs/DATA.md#publishing-a-bundle) and [Rule data](RELEASING.md#rule-data) here.
