Skip to content

Supply chain

Forge activity tells you whether a project is alive. It does not tell you what you are shipping. Ospobox handles both through the same machinery: an SBOM and an advisory feed are evidence sources like any other, and dependency exposure is a scorer like any other.

What you get today

Point it at a CycloneDX document and an advisory feed, and you get a scored breakdown of what a repository depends on:

SBOM:       48 new evidence records
Components: 47
Advisories: 3 new records

dependency-exposure: 74.5/100
  known-advisories      64.0 (weight 0.7)  3 advisories across 2 of 47 components (1 critical, 1 high)
  clean-components      95.7 (weight 0.3)  45 of 47 components have no advisory on file

Both components explain themselves, as every scorer must.

Running it

uv run python demo/supply_chain.py \
    --repo https://codeberg.org/abilian/hop3 \
    --sbom path/to/bom.json

It does three things in order:

  1. Ingests the SBOM. One sbom.document record for the repository listing the package URLs it found, and one sbom.component record per package, keyed by the package's own purl so that anything downstream can attach facts to a package directly.
  2. Queries OSV for each component, storing advisories as evidence against those purls. --offline skips this when you want the loop without the network.
  3. Scores the repository's exposure over what it just collected.

Nothing is re-collected: an unchanged SBOM adds no records on a second run, and an advisory already on file is not duplicated.

Generating an SBOM

Ospobox does not build SBOMs; it consumes them. Any CycloneDX JSON works:

# Python
uv run cyclonedx-py environment > bom.json

# Node
npx @cyclonedx/cyclonedx-npm --output-file bom.json

# Containers, source trees, and most things else
syft dir:. -o cyclonedx-json > bom.json

Components without a package URL are skipped. Without one there is no subject to attach an advisory, a licence or an attestation to.

Planned

Uploading an SBOM through the dashboard, and attaching one to a repository so it refreshes on a schedule. The evidence and scoring paths behind it already run; only the surface is missing.

The limits of the reference plugins

They are deliberately naive, and meant to be replaced:

  • One OSV query per package, without caching or batching.
  • No reachability analysis. An advisory against a package you import once in a test fixture counts the same as one in your request path.
  • No version-range evaluation beyond what OSV returns for the exact purl.
  • No transitive resolution. The SBOM's declared components are the ones scored.

They prove that a non-forge evidence type flows through the same store, scorers and rules as everything else, with no change to the core. If you need better, the extension points are the way in, and a real connector replacing osv changes nothing outside its own package.

Research

Transitive reachability, exploitability in context, provenance and build attestations, and post-quantum migration exposure. The counting above is where that work starts.

The shape of the limitation

The evidence store is a table, and deliberately not a graph. The repository→component relationship lives inside the sbom.document payload as a list of purls, because there is nowhere else to put it.

That serves for scoring one repository's dependencies. It breaks down on "which of our repositories depend on this compromised package" across an estate, a query that wants edges. See Architecture for where that goes.