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 used | What counts as current |
|---|---|
| A repository | the SBOM of its default branch head |
| A release | the newest release of the repository |
| A container image | every image digest still published in your registry |
| A registry package | every package file that has not been deleted, once package policies are available to your organisation |
| An uploaded SBOM | the 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:
| Parameter | Matches |
|---|---|
q | the component name, exact or prefix; a value starting with pkg: matches the package URL |
purl | a package URL; without a version it matches every version |
range | a purl-spec version range, such as vers:npm/>=4.17.0|<4.17.21; needs purl without a version |
ecosystem, version | the ecosystem, and an exact version |
licence | an SPDX id in the licence expression, or unknown |
environment | sandbox, development, test, staging, production or unclassified |
environment_source | where the class came from: deployment, reported or rule |
subject_kind | repo, release, image, package or artifact |
subject | the repository, image, package or artefact name |
current | true (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.
| Endpoint | Returns |
|---|---|
GET /orgs/{slug}/components | components 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}/usages | where that component is used |
GET /orgs/{slug}/components/vulnerable | vulnerable components in use, joined to the open findings; min_severity=low|medium|high|critical |
GET /orgs/{slug}/components/vulnerable/summary | counts of vulnerable components and uses, by severity and environment |
GET /orgs/{slug}/repos/{repo}/dependencies/paths | which direct dependencies bring a component into the repository, see Dependency paths |
GET /orgs/{slug}/components/environment-rules | your environment rules and labels |
PUT /orgs/{slug}/components/environment-rules | replace 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:
| Ecosystem | File | Paths |
|---|---|---|
| npm | package-lock.json, pnpm-lock.yaml, yarn.lock | yes |
| PyPI | uv.lock, poetry.lock | yes |
| Cargo | Cargo.lock | yes |
| Go | go.mod | yes: fremforge reads each module’s go.mod from its Go module cache, without running any code from your repository |
| Maven | pom.xml | direct dependencies only: push a CycloneDX SBOM from CI, see below |
| Gradle | no: push a CycloneDX SBOM from CI, see below | |
| NuGet | packages.lock.json | yes, 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.lock | no: 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 aboveFor 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 cyclonedxDirectBomGo, 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:
deployment: a deployment your CI/CD reported withPOST /orgs/{slug}/repos/{repo}/deployments, whose latest status issuccess, names the use’s image digest, commit, release tag or package version. When several do, the highest class wins. The usage row carriesdeployment_idanddeployment_environment. See the API reference.reported: an SBOM upload gave its ownenvironment.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.