Skip to main content

Component inventory

The component inventory answers two questions without opening a single SBOM: where is this component used, and which vulnerable components are in use right now. It is built from the SBOMs fremforge holds for your organisation and is on for every organisation.

Find it in the org admin under Security → Components, with the tabs Inventory, Vulnerable in use, Pulled versions, Deployments and Environment rules. Pulled versions lists the package versions your consumers actually downloaded, see Pulled package versions. Deployments is described below.

What is indexed

Each current CycloneDX SBOM is read into the inventory, one row per component and place it is used:

Where it is usedWhat counts as current
A repositorythe SBOM of its default branch head
A releasethe newest release of the repository
A container imageevery image digest still published in your registry
A registry packageevery package file that has not been deleted, once package policies are available to your organisation
An uploaded SBOMthe newest upload of each artefact name

A new SBOM updates only what changed: a component that is still there keeps its row and the date it was first seen, and a component that has gone is marked as removed. Removed uses are kept for 180 days, so you can still answer “when did we stop using X”. New SBOMs are indexed when they arrive; a background job catches up anything missed every 30 minutes.

Search

Every list takes the same filters, all optional and combined:

ParameterMatches
qthe component name, exact or prefix; a value starting with pkg: matches the package URL
purla package URL; without a version it matches every version
rangea purl-spec version range, such as vers:npm/>=4.17.0|<4.17.21; needs purl without a version
ecosystem, versionthe ecosystem, and an exact version
licencean SPDX id in the licence expression, or unknown
environmentsandbox, development, test, staging, production or unclassified
environment_sourcewhere the class came from: deployment, reported or rule
subject_kindrepo, release, image, package or artifact
subjectthe repository, image, package or artefact name
currenttrue (the default): in use now; false also returns removed uses

REST API

The API is served under https://frem.sh/_app/api/v1. Use an org API token or a PAT with the components:read scope; findings:read is accepted as well.

EndpointReturns
GET /orgs/{slug}/componentscomponents in use, one row per package URL, with how many places use each, in which environments and kinds of use
GET /orgs/{slug}/components/usages“where is X used”: one row per component and place
GET /orgs/{slug}/components/{id}one component
GET /orgs/{slug}/components/{id}/usageswhere that component is used
GET /orgs/{slug}/components/vulnerablevulnerable components in use, joined to the open findings; min_severity=low|medium|high|critical
GET /orgs/{slug}/components/vulnerable/summarycounts of vulnerable components and uses, by severity and environment
GET /orgs/{slug}/repos/{repo}/dependencies/pathswhich direct dependencies bring a component into the repository, see Dependency paths
GET /orgs/{slug}/components/environment-rulesyour environment rules and labels
PUT /orgs/{slug}/components/environment-rulesreplace the rules (needs policy:write, recorded in the audit log)

For example, every place lodash below 4.17.21 is in use:

curl -sS -H "Authorization: Bearer $FREMFORGE_TOKEN" \
  "https://frem.sh/_app/api/v1/orgs/<org>/components/usages?purl=pkg:npm/lodash&range=vers:npm/%3C4.17.21"

Pagination

Lists are paged by cursor. Each response is { "items": [...], "next_cursor": "..." }; pass next_cursor back as cursor for the next page, until it is null. limit is 1 to 500 and defaults to 100. A cursor belongs to the endpoint that issued it; another endpoint refuses it with invalid-cursor.

CSV export

Add format=csv, or send Accept: text/csv, to any list (/components, /components/usages, /components/{id}/usages, /components/vulnerable) and the whole result comes back as one CSV file with the same filters, without paging. A CSV carries at most 200,000 rows; above that the API answers 413 too-many-rows, and you narrow the filters, page through the JSON, or use the data export. The admin pages have the same export as a download button.

The full inventory, including removed uses, is also in every data export as metadata/components.ndjson, see Export schema.

Dependency paths

For a transitive dependency, the inventory shows which direct dependency brings it in, the way GitHub’s dependency graph does. On Vulnerable in use each row says Introduced via followed by the path, for example async@2.6.3 → lodash; on a component’s page the Dependency column shows the same path, or Direct when the repository depends on the component itself.

The path comes from the dependency graph inside an SBOM. fremforge keeps a compact copy of that graph for every SBOM it indexes, so a path costs no SBOM download. Paths exist only where an SBOM has dependency edges. With the SBOMs fremforge generates itself (syft, from the files in the repository) that is:

EcosystemFilePaths
npmpackage-lock.json, pnpm-lock.yaml, yarn.lockyes
PyPIuv.lock, poetry.lockyes
CargoCargo.lockyes
Gogo.modyes: fremforge reads each module’s go.mod from its Go module cache, without running any code from your repository
Mavenpom.xmldirect dependencies only: push a CycloneDX SBOM from CI, see below
Gradleno: push a CycloneDX SBOM from CI, see below
NuGetpackages.lock.jsonyes, for SBOMs generated since 2026-10-10 (syft 1.54.1); older SBOMs show none, because earlier syft versions wrote these edges wrongly
Composer, RubyGems, requirements.txt, Pipfile.lockno: the SBOM has no dependency edges

Where there is no path, the row shows no path rather than a guess.

How a direct dependency is decided depends on the lockfile. An npm package-lock.json, a uv.lock, a Cargo.lock and a go.mod list the project itself, and its dependencies are the direct ones (in go.mod, the requirements not marked // indirect). pnpm-lock.yaml, yarn.lock, poetry.lock and packages.lock.json do not list the project in the SBOM, so a direct dependency is taken to be a package that nothing else in the lockfile depends on. A direct dependency that another package also requires is then shown through that package. An SBOM whose root component lists its own dependencies, as the CycloneDX build plugins write it, says which are direct. The API says which rule applied in graph.direct_basis (project, roots or declared).

On the API, GET /orgs/{slug}/repos/{repo}/dependencies/paths answers for the repository’s default-branch head, and for the SBOMs your CI pushed for it (below), with one shortest path per direct dependency that leads to the component, shortest first. Pass purl (without a version it matches every version) or component_id, plus limit (1 to 100, default 20) and cursor for the next page. Each path says where it came from in source (scan or upload) and sbom_id, and graph.sources lists every graph that was read. The items of /components/vulnerable and /components/usages carry via, the direct dependency’s package URL, and dependency_path, the shortest path from it to the component.

curl -sS -H "Authorization: Bearer $FREMFORGE_TOKEN" \
  "https://frem.sh/_app/api/v1/orgs/<org>/repos/<repo>/dependencies/paths?purl=pkg:npm/lodash"

Paths for Maven, Gradle and Go from CI

Resolving a Maven or Gradle dependency tree means running the build tool, and the build tool runs plugins from your build files. fremforge does not run your build on its servers, so for these ecosystems the graph comes from your own CI: generate a CycloneDX SBOM with the build tool’s CycloneDX plugin, which writes the full resolved dependencies graph, and push it with source_repo set to the repository. The paths API, via on Vulnerable in use and the component page then use that graph for the repository. Push it from the default branch, because the newest upload of each artifact_name is the one that counts.

The same push step works for all three. The FREMFORGE_API_TOKEN secret holds a token with the sboms:write scope; fremforge creates it in every new repository.

      - name: Push the SBOM to fremforge
        env:
          FREMFORGE_API_TOKEN: ${{ secrets.FREMFORGE_API_TOKEN }}
        run: |
          set -euo pipefail
          DIGEST=$(sha256sum "$SBOM" | cut -d' ' -f1)
          curl -sS --fail-with-body \
            -H "Authorization: Bearer $FREMFORGE_API_TOKEN" \
            -F "sbom=@$SBOM" \
            -F "format=cyclonedx-json" \
            -F "sha256_digest=$DIGEST" \
            -F "artifact_name=${GITHUB_REPOSITORY#*/}-cyclonedx" \
            -F "artifact_version=$GITHUB_SHA" \
            -F "source_repo=$GITHUB_REPOSITORY" \
            -F "repo_full_name=$GITHUB_REPOSITORY" \
            -F "commit_sha=$GITHUB_SHA" \
            -F "git_ref=$GITHUB_REF" \
            "$GITHUB_SERVER_URL/_app/api/v1/orgs/$GITHUB_REPOSITORY_OWNER/sboms"

Maven, with the cyclonedx-maven-plugin. Nothing needs to change in your pom.xml:

name: sbom
on:
  push:
    branches: [main]

jobs:
  sbom:
    runs-on: fremforge
    env:
      SBOM: target/bom.json
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          distribution: temurin
          java-version: '21'
      - name: CycloneDX SBOM with the full dependency graph
        run: mvn -B -q org.cyclonedx:cyclonedx-maven-plugin:2.9.1:makeBom -DoutputFormat=json -DoutputName=bom -DschemaVersion=1.6
      # then the push step above

For a multi-module build, use the makeAggregateBom goal instead; its SBOM is target/bom.json of the top-level project.

Gradle, with the CycloneDX Gradle plugin. Add the plugin to build.gradle:

plugins {
    id 'org.cyclonedx.bom' version '3.5.1'
}

and generate the SBOM in the job, with SBOM: build/reports/cyclonedx-direct/bom.json:

      - name: CycloneDX SBOM with the full dependency graph
        run: gradle cyclonedxDirectBom

Go, with cyclonedx-gomod. fremforge already finds paths for Go on its own (see the table above). Push from CI when fremforge cannot fetch your modules, for example private modules its Go module cache cannot reach, with SBOM: bom.json:

      - uses: actions/setup-go@v5
        with:
          go-version: '1.27'
      - name: CycloneDX SBOM with the module graph
        run: |
          go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@v1.9.0
          "$(go env GOPATH)/bin/cyclonedx-gomod" mod -json -output "$SBOM" .

The pushed SBOM is also indexed as an uploaded artefact of its own, so its components appear in the inventory under its artifact_name as well as under the repository. A dependency scan of a pom.xml or Gradle build file sees only the dependencies the file declares. So every Maven component of a pushed graph, including the transitive ones, is matched against the same advisory database the scans use, and each match is a dependency finding of the repository named in source_repo. These findings have the manifest sbom:<artifact_name>. They appear on Vulnerable in use with their path, in the dependency findings on the Security pages and in the findings API, and they are dismissed like any other finding. They are recomputed when the SBOM is pushed, after each default-branch scan of the repository, and after each refresh of the advisory database, so a new advisory adds a finding and a withdrawn one removes it. An advisory the repository’s own scan already reports is not listed twice. They are default-branch findings: a pushed SBOM does not change a pull request’s fremforge/dep-scan status. A NuGet graph pushed this way is used as written, unless syft older than 1.54.1 wrote it.

Environment classes

Each use can carry an environment class: sandbox, development, test, staging or production. It comes from one of three places, and the first that applies wins:

  1. deployment: a deployment your CI/CD reported with POST /orgs/{slug}/repos/{repo}/deployments, whose latest status is success, names the use’s image digest, commit, release tag or package version. When several do, the highest class wins. The usage row carries deployment_id and deployment_environment. See the API reference.
  2. reported: an SBOM upload gave its own environment.
  3. rule: the first matching rule under Environment rules, matching on the kind of use, a repository, a tag, an image or an artefact name.

The class is for search and reporting; it does not change retention or any gate.

Deployments

The Deployments tab lists the deployments your CI/CD reported, newest first: the repository, the environment name and its class, the state (a deployment whose latest status is success is marked as deployed now), the commit or ref, the image digests and package versions it names, and who reported it (a CI job with its run, a registry robot, a user or an API token). Filter by repository, environment name, environment class and state, or tick Deployed now only. Next page and First page move through the list. The same list is on the API at GET /orgs/{slug}/deployments/reported (scope components:read), and every data export contains it as metadata/deployments.ndjson with each deployment’s status history, see Export schema.

Related

  • SBOMs: where the SBOMs come from.
  • Compliance report: its “riskiest components” section reads the inventory.
  • Pulled package versions: which vulnerable versions your consumers actually download, at GET /orgs/{slug}/components/pulled.