Use the Docker image directly¶
For everyday use we recommend the scan-sbom.sh script, which handles language detection, image selection, and volume mounts for you. This document explains how to call the image directly with docker run in environments where the script cannot live (CI runners, Kubernetes jobs, and so on).
Images and tags¶
| Image | Purpose |
|---|---|
ghcr.io/sktelecom/bomlens |
Scanning and post-processing (canonical name) |
ghcr.io/sktelecom/sbom-generator, ghcr.io/sktelecom/sbom-scanner |
Aliases of the same image (former names, same digest) |
ghcr.io/sktelecom/bomlens-firmware |
Firmware analysis (includes GPL tools, opt-in) (legacy alias: sbom-scanner-firmware) |
ghcr.io/sktelecom/bomlens-deep-cve |
Bundles grype for deep CVE matching (opt-in). Used by the CLI's --deep-cve and by the web UI's Deep CVE matching toggle, both of which pull it automatically as a sibling container when it is not already the running image |
ghcr.io/sktelecom/bomlens-deep-license |
Bundles scancode for licence detection in the project's own files (opt-in). Used by the CLI's --deep-license for a source scan, pulled automatically. Carries LGPL components (7-Zip on linux/amd64, intbitset, chardet); the source for 7-Zip and intbitset is in the image under /usr/share/bomlens/sources/, and chardet ships as Python source |
ghcr.io/sktelecom/bomlens-aibom |
Generates an AI-model ML-BOM (opt-in, legacy alias: sbom-scanner-aibom). Used by --model/--model-file and the web UI's AI model tile, pulled automatically as a sibling container |
latest and version tags are available. All published images (ghcr.io/sktelecom/bomlens, bomlens-firmware, bomlens-deep-cve, bomlens-deep-license, bomlens-aibom, and their aliases) support both linux/amd64 and linux/arm64, so pulling any of them on an arm64 host (an Apple Silicon Mac, an Arm server) works natively. Images are signed with cosign before publishing.
On an arm64 host, do not force linux/amd64 (--platform linux/amd64 or DOCKER_DEFAULT_PLATFORM=linux/amd64): the image then runs under emulation, where syft in the firmware image can crash. A firmware scan tries a failed syft pass once more (not one that ran out of its time limit or was killed for memory), says in the log that it runs under emulation, and records generate as degraded in bomlens-result.json when the second try also fails.
What is in the image¶
It is a lightweight image (based on python 3.12 slim) without language toolchains. For source scans, transitive dependency resolution is handled by the script, which pulls per-language cdxgen images separately. See Architecture for the structure.
| Tool | Version | Role |
|---|---|---|
| syft | v1.54.1 | Scans images, binaries, and directories |
| Trivy | v0.75.0 | Vulnerability report |
| cosign | v3.1.3 | SBOM signing |
| jq | — | SBOM normalization and notice generation |
| ScanCode Toolkit | 32.5.0 | Precise license detection (included only in opt-in builds) |
| SCANOSS | 1.54.2 | Identifies open source copied into C/C++ source (--identify-vendored) |
| docker CLI | 29.9.0 | Starts a sibling cdxgen container when the web UI runs a source scan |
| cdxgen | 13.5.0 | Model pedigree enrichment (bomlens-aibom image only) |
Tool versions are pinned with ARG in docker/Dockerfile.
Every image also carries the NVD CPE index (cpe_match.sqlite, about 146 MB), built from the NVD data when the image is built. Firmware, root filesystem and binary scans with --security match the CPEs of components that have no package PURL against it, with no network (see How CVEs are matched). Building it clones the NVD data feeds from GitHub. When you build an image yourself where GitHub is not reachable, the build stops at that step and names --build-arg SBOM_CPE_INDEX=false, which leaves the index out; a firmware build does not accept it, so the firmware image always has the index.
Running directly¶
Select the analysis mode with the MODE environment variable. All examples below leave their outputs in the current directory and do not upload anything (UPLOAD_ENABLED=false).
Analyze a Docker image¶
On Git Bash (Windows), MSYS rewrites both /var/run/docker.sock and the
container-side /host-output into Windows paths before Docker ever sees
them, silently breaking both mounts. MSYS_NO_PATHCONV and
MSYS2_ARG_CONV_EXCL below turn that rewriting off, and cygpath -m
converts the host side back by hand instead (scripts/scan-sbom.sh uses the
same pair for the same reason). WSL2, macOS and Linux shells don't rewrite
paths and skip straight to the plain docker run line.
HOSTPATH="$(cygpath -m "$(pwd)" 2>/dev/null || pwd)"
MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' docker run --rm \
-v "$HOSTPATH":/host-output \
-v /var/run/docker.sock:/var/run/docker.sock \
-e MODE=IMAGE \
-e TARGET_IMAGE="nginx:alpine" \
-e UPLOAD_ENABLED=false \
-e HOST_OUTPUT_DIR=/host-output \
-e PROJECT_NAME="Nginx" \
-e PROJECT_VERSION="alpine" \
ghcr.io/sktelecom/bomlens:latest
Analyze a binary file¶
HOSTPATH="$(cygpath -m "$(pwd)" 2>/dev/null || pwd)"
MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' docker run --rm \
-v "$HOSTPATH":/target \
-v "$HOSTPATH":/host-output \
-e MODE=BINARY \
-e TARGET_FILE=/target/firmware.bin \
-e UPLOAD_ENABLED=false \
-e HOST_OUTPUT_DIR=/host-output \
-e PROJECT_NAME="Firmware" \
-e PROJECT_VERSION="1.0" \
ghcr.io/sktelecom/bomlens:latest
Analyze a source directory¶
HOSTPATH="$(cygpath -m "$(pwd)" 2>/dev/null || pwd)"
MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' docker run --rm \
-v "$HOSTPATH":/src \
-v "$HOSTPATH":/host-output \
-e MODE=SOURCE \
-e UPLOAD_ENABLED=false \
-e HOST_OUTPUT_DIR=/host-output \
-e PROJECT_NAME="MyApp" \
-e PROJECT_VERSION="1.0.0" \
ghcr.io/sktelecom/bomlens:latest
In direct runs, SOURCE mode reads a lock file that covers the whole tree (a package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml, bun.lock or gradle.lockfile, with no other manifest below the root) as it is, with no build and no Docker socket, and fills the licenses it lacks from the package registry; the SBOM records this as bomlens:detection. That is the form to use where there is no Docker to start a sibling container in, such as a CI job that runs this image as its job image. For any other tree, syft reads the package manifests inside the container, so it may only capture direct dependencies; if you need transitive dependencies there, use scan-sbom.sh, which routes to the per-language cdxgen images. Syft's manifest readers need a lockfile (package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml for Node, and the equivalent for other ecosystems) to resolve any of a project's declared dependencies; without one, direct-run SOURCE mode has nothing to read and the scan fails with guidance rather than reporting an empty result as complete.
Notice and reports in one run¶
In direct runs, the notice and security reports are off by default. Turn on the following variables to get the same outputs as the CLI's --all.
HOSTPATH="$(cygpath -m "$(pwd)" 2>/dev/null || pwd)"
MSYS_NO_PATHCONV=1 MSYS2_ARG_CONV_EXCL='*' docker run --rm \
-v "$HOSTPATH":/host-output \
-v /var/run/docker.sock:/var/run/docker.sock \
-e MODE=IMAGE \
-e TARGET_IMAGE="nginx:alpine" \
-e GENERATE_NOTICE=true \
-e GENERATE_SECURITY=true \
-e GENERATE_REPORT=true \
-e UPLOAD_ENABLED=false \
-e HOST_OUTPUT_DIR=/host-output \
-e PROJECT_NAME="Nginx" \
-e PROJECT_VERSION="alpine" \
ghcr.io/sktelecom/bomlens:latest
Environment variables¶
| Variable | Required | Default | Description |
|---|---|---|---|
MODE |
No | POSTPROCESS |
Analysis mode: SOURCE, IMAGE, BINARY, ROOTFS, FIRMWARE, ANALYZE, or PLAN (write the execution plan of a source scan, see Engine contract). Left unset, the container only post-processes an SBOM already sitting in the output directory — set it explicitly to actually scan something. |
PROJECT_NAME |
Yes | — | Project name |
PROJECT_VERSION |
Yes | — | Project version |
TARGET_IMAGE |
Per mode | — | Image for IMAGE mode. With the docker.sock mount it is read from the Docker daemon; without it, from the registry, or from an archive named with its scheme (docker-archive:, oci-archive:, oci-dir:) at a path mounted into the container |
SYFT_REGISTRY_AUTH_USERNAME, SYFT_REGISTRY_AUTH_PASSWORD, SYFT_REGISTRY_AUTH_TOKEN, SYFT_REGISTRY_AUTH_AUTHORITY |
— | — | IMAGE mode: credentials for a private registry. A docker config file mounted and named in DOCKER_CONFIG works too. SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY and SYFT_REGISTRY_INSECURE_USE_HTTP reach a registry with a self-signed certificate or plain HTTP. SYFT_PLATFORM picks the platform of a multi-platform image |
TARGET_FILE |
Per mode | — | File path for BINARY/FIRMWARE mode (path inside the container) |
TARGET_DIR |
Per mode | — | Directory path for ROOTFS mode |
UPLOAD_ENABLED |
— | true |
If false, save locally without uploading (same as CLI --generate-only) |
HOST_OUTPUT_DIR |
— | — | Mounted path to copy the outputs to |
BOMLENS_STAGE1_EXIT, BOMLENS_STAGE1_OOM, BOMLENS_STAGE1_LOG, BOMLENS_STAGE1_REASON |
— | — | POSTPROCESS after a stage 1 you ran yourself: its exit code, true if it was killed for memory, and its log's file name in the output folder; or, instead of the last two, the reason already classified (oom, disk-space, network, cdxgen-crash, cdxgen-unavailable). Exit code 127 is read as cdxgen-unavailable: pass it for an unsupported plan, where no stage 1 ran. Stage 2 then falls back to a syft scan when stage 1 left no SBOM (see Engine contract) |
BOMLENS_EVENTS |
— | — | file or stderr: write progress events, one JSON object per line, to bomlens-events.jsonl in the output folder or to stderr (see Engine contract) |
BOMLENS_IMAGE_REF, BOMLENS_IMAGE_DIGEST |
— | — | The image reference and digest the caller ran, repeated in bomlens-result.json (see Engine contract) |
GENERATE_NOTICE |
— | false |
Generate the open-source notice (CLI --notice) |
GENERATE_SECURITY |
— | false |
Generate the Trivy security report (CLI --security) |
DEEP_LICENSE |
— | false |
Detect licences in the project's own files with scancode (CLI --deep-license; needs scancode in the image) |
DEEP_LICENSE_MAX_FILES |
— | 20000 |
DEEP_LICENSE: a tree with more of the project's own files than this is not scanned |
DEEP_LICENSE_TIMEOUT |
— | 600 |
DEEP_LICENSE: seconds before scancode is stopped |
BOMLENS_DEEP_LICENSE_ALL |
— | — | DEEP_LICENSE: set 1 to scan dependency, build and tool folders too (left out by default) |
DETECTION |
— | auto |
SOURCE mode only. auto reads a covering lock file statically and otherwise runs the build or syft path as before; static always reads statically; build never does (CLI --detection). Static detection fills missing licenses from the package registry, so auto builds under BYTE_STABLE=true or FETCH_LICENSE=false |
REACHABILITY |
— | false |
Judge with govulncheck whether each Go advisory's code is reached (CLI --reachability): on the executable in BINARY mode; in SOURCE and POSTPROCESS, from the govulncheck output stage 1 left (the CLI hands stage 1 the image's govulncheck) |
GENERATE_REPORT |
— | false |
Generate the open-source risk report (off in direct runs, unlike the CLI default) |
ENRICH_MAVEN_CPE |
— | true |
Attach an NVD-matchable cpe:2.3 to maven components (derived from the groupId) so a CPE-aware engine can reach their NVD-only CVEs; unmapped groups get no CPE (skipped for AI SBOMs) |
ENRICH_GITHUB_CPE |
— | true |
Attach an NVD-matchable cpe:2.3 to a small, hand-verified set of pkg:github/ components (source-repository-only coordinates, typical for large C/C++ projects with no package-manager ecosystem) so a CPE-aware engine can reach their NVD-only CVEs; anything not in the curated list gets no CPE (skipped for AI SBOMs) |
ENRICH_UPSTREAM_PURL |
— | true |
For a firmware, root filesystem or binary scan, or an SBOM read from a Yocto build's records, set the purl of the upstream source repository (pkg:github/<owner>/<repo>@<tag>, pkg:sourceforge, pkg:gnu) on components that have no purl, a pkg:generic one or a pkg:opkg one, from the closed table in docker/lib/upstream-purl-map.json, or from a source download address that states a GitHub or GNU release. Any other component keeps its identifier |
ENRICH_INTERPRETER_CPE |
— | true |
Attach an NVD-matchable cpe:2.3 to a small, hand-verified set of interpreter components (e.g. Python packaged as a conda or NuGet artifact) so a CPE-aware engine can reach their NVD-only CVEs; anything not in the curated list gets no CPE (skipped for AI SBOMs) |
SECURITY_NVD_VERIFY |
— | false |
With --deep-cve: verify each grype nvd:cpe finding against the live NVD version range and drop out-of-range false positives (needs NVD_API_KEY + network; adds minutes). Off by default — findings are kept and flagged version-unverified |
NVD_API_KEY |
For SECURITY_NVD_VERIFY |
— | NVD API key used by the deep-cve version filter; passed to the container by name only (never inlined) |
BOMLENS_OS_LICENSES |
— | true |
IMAGE and ROOTFS modes: read the licenses of Debian packages with Trivy, which reads the license names, common-licenses references and license text in the copyright files syft leaves as a hash. It reads the image a second time; false turns it off (the hashes are still removed) |
BOMLENS_TRIVY_DB_MAX_AGE_DAYS |
— | 7 |
With a Trivy database mounted at /trivy-db (read as it is, offline): the age in days past which the security step is recorded as degraded |
ENRICH_EOL |
— | true |
Flag components past their upstream end-of-life from a bundled offline snapshot (skipped for AI SBOMs) |
ENRICH_MALICIOUS |
— | true |
Flag components that are known-malicious packages (typosquats, hijacked accounts) from a bundled offline OSV snapshot. A separate signal from vulnerabilities: the response is removal and credential rotation, not an upgrade |
ENRICH_OS_CONTEXT |
— | true |
Synthesize an operating-system component from distro package PURLs (rpm, deb, and apk). Trivy selects the distro vulnerability feed from that component, so without it the OS packages in a supplier SBOM or rootfs scan would get no OS CVE matches at all. No-op when the SBOM has no recognizable distro packages — including a distro Trivy carries no feed for, such as OpenWRT (skipped for AI SBOMs) |
ENRICH_DISTRO_SUPPLIER |
— | true |
Fill supplier on an rpm, deb, or apk component with the distro project's name, read from the operating-system component ENRICH_OS_CONTEXT synthesizes. Left blank for a distro with no confirmed supplier name, an ambiguous (mixed-distro) SBOM, or a component that already carries a supplier (skipped for AI SBOMs) |
STALENESS_ENRICH |
— | false |
Add deps.dev version currency (how many releases behind latest); needs network access |
ENRICH_HF_SECURITY |
— | true |
AIBOM mode: read HuggingFace's per-file security scan results (ClamAV + picklescan) into the ML-BOM; metadata only, no file download |
API_KEY, API_URL |
For uploads | — | Upload credential and server URL. DT uses X-Api-Key; TRUSCA uses a Bearer token |
UPLOAD_TARGET |
— | dependency-track |
Upload destination: dependency-track (DT-compatible) or trusca (native ingest, not DT-compatible) |
TRUSCA_PROJECT_ID |
When trusca |
— | Target TRUSCA project id (UUID). Must already exist (no auto-create) |
TRUSCA_REF |
— | main |
Ingest ref label |
TRUSCA_RELEASE |
— | PROJECT_VERSION |
Ingest release label |
BOMLENS_MAVEN_FULL_GRAPH |
— | — | Maven source scans: set 1 to keep the full resolved graph instead of filtering to compile/runtime scope |
BOMLENS_NODE_FULL_GRAPH |
— | — | Node.js source scans: set 1 to keep the full dev-plus-production graph instead of the production-only set |
BOMLENS_ANDROID_FULL_GRAPH |
- | - | Android source scans (Android SDK image): set 1 to keep the full graph, build and test tooling included, instead of filtering to the release runtime classpath |
BOMLENS_PHP_FULL_GRAPH |
- | - | PHP (Composer) source scans: set 1 to keep the full require-plus-require-dev graph instead of filtering to the required set |
BOMLENS_KEEP_BUILD_OUTPUT |
— | — | Source scans: set 1 to leave the resolved tree in place. By default the scan restores the files its resolvers rewrote (go.mod, go.sum, Cargo.lock, Gemfile.lock, Package.resolved) and removes the build directories they created, so the scanned project is handed back as it was |
BOMLENS_PREP_TIMEOUT |
- | 900 (1800 for Gradle/Android steps) |
Seconds a single dependency-resolution step (Cargo, Go, Bundler, pip, npm, pnpm, PHP Composer, Swift, Gradle/Android) is allowed to run before it is stopped and the scan moves on without it. Set to override both defaults for every step. A step that fails or times out is logged with its own output and recorded on the SBOM as bomlens:pipeline-step-failed; the scan itself still completes |
BOMLENS_CANCEL_GRACE |
— | 30 |
Seconds a cancelled scan (CLI Ctrl+C, or the web UI's cancel button) gets to stop cleanly before it is force-stopped. Applies to the CLI and --ui; the desktop app always uses the default |
BOMLENS_INCLUDE_NON_SHIPPED |
- | - | Source scans: set 1 to keep the manifests under test, example, benchmark and demo folders, the GitHub Actions workflows in .github/workflows and the Gradle wrapper in gradle/wrapper, which are left out by default |
BOMLENS_INCLUDE_DEV_DEPENDENCIES |
- | - | Source scans: set 1 to keep the development-only components that are dropped by default (the ones cdxgen marks, and the gems a root Gemfile declares only in its development and test groups). The dropped components are recorded in bomlens:excluded-components |
BOMLENS_NO_COPYRIGHT |
- | - | Source scans: set 1 to skip filling npm, Python, Go and Rust component copyright from the installed packages' license files |
BOMLENS_NO_CARGO_LICENSE |
- | - | Source scans: set 1 to skip filling Rust component licenses from cargo metadata (FETCH_LICENSE=false on the command line skips it too) |
CYCLONEDX_SPEC_VERSION |
— | 1.6 |
CycloneDX version of the generated SBOM, 1.6 or 1.7 (the CLI's --spec-version). AI model and dataset SBOMs are always 1.7. Any other value stops the run |
CYCLONEDX_SPEC_VERSIONS |
— | 1.3 1.4 1.5 1.6 1.7 |
Accepted CycloneDX spec versions for the conformance check (space-separated); overrides the default range |
AI_CYCLONEDX_SPEC_VERSIONS |
— | 1.3 1.4 1.5 1.6 1.7 |
Accepted CycloneDX versions for AI SBOMs (ML-BOM); always includes 1.7, the version the AIBOM toolchain emits |
SPDX_SPEC_VERSIONS |
— | SPDX-2.2 SPDX-2.3 |
Accepted SPDX spec versions for the conformance check |
PURL_MIN_PCT |
— | 90 |
Conformance check: minimum percentage of components with a PURL (mandatory check). Applies to a --deep-cve scan too, CLI or web UI |
LICENSE_MIN_PCT |
— | 80 |
Conformance check: minimum percentage of components with a license (recommended, warn only). Applies to a --deep-cve scan too, CLI or web UI |
HASH_MIN_PCT |
— | 50 |
Conformance check: minimum percentage of components with a hash (recommended, warn only). Applies to a --deep-cve scan too, CLI or web UI |
FIELD_MIN_PCT |
— | 80 |
Conformance check: minimum percentage coverage for advisory per-component regulatory fields. Applies to a --deep-cve scan too, CLI or web UI |
PURL_TYPES_FILE |
— | docker/lib/purl-types.json |
Conformance check: the purl-spec type data that says which types exist and which of them require a namespace |
NS_ADVISORY_TYPES |
— | golang huggingface |
Conformance check: types whose missing namespace is reported on the advisory row instead of failing the SBOM (space-separated) |
LICENSE_REGISTRY |
— | false |
The script's --license-registry: look up missing licences, and check a single strong-copyleft or non-commercial licence, in the package registry (deps.dev). Uses the network |
LICENSE_REGISTRY_VERIFY |
— | GPL, AGPL, SSPL-1.0, BUSL-1.1 and CC-BY-NC ids | SPDX ids whose single occurrence is checked against the registry, space- or comma-separated |
LICENSE_REGISTRY_BUDGET |
— | 240 |
Seconds the lookups may take in total. Components it does not reach are left as they were, and the step is recorded as degraded |
LICENSE_REGISTRY_API |
— | https://api.deps.dev/v3 |
Base URL of a server that answers the deps.dev v3 version lookup, for a network that reaches the registries only through its own mirror or proxy. When nothing answers, the components stay as they were and the step is recorded as degraded (unavailable) |
PURL_RESOLVE |
— | false |
--analyze only (the script's --resolve-purl): look each identifier up in its package repository (deps.dev) and add the answer to the conformance check report as an advisory row. Uses the network |
PURL_RESOLVE_IGNORE |
— | — | Namespace prefixes to leave out of that lookup, space- or comma-separated. Use it for namespaces published only to an internal repository, which cannot be told apart from a wrong identifier |
PURL_RESOLVE_BUDGET |
— | 240 |
Seconds the lookup may take in total. Identifiers it does not reach are reported as unchecked, never as missing |
TRUSCA's (formerly TrustedOSS Portal) native ingest endpoint (
POST /v1/projects/{id}/sbom-ingest, Bearer auth) is not Dependency-Track compatible. To push to a regular Dependency-Track server, keepUPLOAD_TARGET=dependency-track(the default).
For the full mapping between CLI flags and environment variables, see the flag mapping in Architecture.
Building and publishing the image¶
The procedure for building the image yourself or publishing it for multiple platforms is in the contributor-facing docker/README.
Related: Getting started | CLI reference | Architecture