Skip to content

Generate notice, security & risk reports

Beyond SBOM generation, BomLens produces an open-source notice (NOTICE) and a security vulnerability report in one run. This page covers how to generate those outputs. For how to read and interpret them, see What the reports mean.

Quick start (5 minutes)

If this is your first time, this is all you need. With the Docker engine running, generate the SBOM, notice, and security report in one run — from the browser or the CLI.

Browser UI (no command line)

Launch the UI, then enter a project name and version, pick a scan target, run, and download the notice and security report.

./scripts/scan-sbom.sh --ui     # opens http://localhost:8080 (port taken? UI_PORT=9090 ./scripts/scan-sbom.sh --ui)
#   Windows: double-click scripts\sbom-ui.bat

CLI

From the project folder you want to scan:

cd /path/to/your-project
/path/to/bomlens/scripts/scan-sbom.sh --project MyApp --version 1.0.0 --all --generate-only

On Windows, use scripts\scan-sbom.bat (Git Bash) or run as-is under WSL2. Installation is in Getting started.

When it finishes, open MyApp_1.0.0/MyApp_1.0.0_NOTICE.html and MyApp_1.0.0/MyApp_1.0.0_security.html to check the results right away — each run's files land together in that MyApp_1.0.0/ subfolder. See below for the detailed options.


Prerequisites

  • Docker engine 20.10+ (free: WSL2 + docker-ce, or Rancher Desktop / Docker Desktop, which is paid for organizational use)
  • Pull the scanner image:
    docker pull ghcr.io/sktelecom/bomlens:latest   # the former name sbom-scanner is the same image
    
  • Run every example from the root of the project to scan.

Output flags are best used together with --generate-only (save locally). Omit it to also auto-upload to an external system (a Dependency-Track server or TRUSCA); pick the target with UPLOAD_TARGET.


Generate everything at once (--all)

--all is shorthand for --notice --security --spdx. It produces the SBOM, the notice, the security report, and an SPDX copy of the SBOM in a single scan.

In the web UI and the desktop app the notice and the security report are generation options on the New scan screen, and the SPDX copy is exported from the results screen once the scan is done.

./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --all --generate-only

Generated files, all together in a MyApp_1.0.0/ subfolder next to where you ran the command (see Where outputs go):

MyApp_1.0.0_bom.json            # SBOM (CycloneDX 1.6)
MyApp_1.0.0_NOTICE.txt          # notice (text)
MyApp_1.0.0_NOTICE.html         # notice (HTML)
MyApp_1.0.0_security.json       # security report (raw Trivy)
MyApp_1.0.0_security.md         # security report (summary)
MyApp_1.0.0_security.html       # security report (visual)
MyApp_1.0.0_risk-report.md      # open-source risk report (summary)
MyApp_1.0.0_risk-report.html    # open-source risk report (visual)

The open-source risk report (_risk-report) is generated by default in every analysis mode (license + vulnerability tally, with response deadlines). To skip it, use --no-report. For handling each of the seven input forms, see the Scenarios guide.

For the full list of artifact kinds, see the artifacts reference; to generate them from a browser, see the web UI.


Open-source notice (--notice)

Gathers the components[].licenses information from the SBOM and generates a notice that groups components by license.

./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --notice --generate-only
  • _NOTICE.txt — standard text, good to ship with the distribution.
  • _NOTICE.html — a browser-friendly format. All package metadata is HTML-escaped and safe.
  • Components with no license information are classified as NOASSERTION.

For license normalization and full-text bundling behavior, see What the reports mean.

Example (text):

License: Apache-2.0
Components (1):
  - requests@2.31


Security vulnerability report (--security)

Scans the generated SBOM with Trivy and reports known vulnerabilities (CVEs). (NVD + OSV + GHSA DB)

./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --security --generate-only
  • _security.json — raw Trivy JSON. For CI or machine processing.
  • _security.md — a per-severity tally table and CVE list. Good to attach to a PR or issue.
  • _security.html — a visual report with severity badges and tables.

The report does not fail the scan even when vulnerabilities exist (report-only). If you need a gate, post-process _security.json.

Trivy matches a component by its package PURL. In a firmware, root filesystem or binary scan, a component that has no package PURL (none, pkg:generic, or one that points at a source repository such as pkg:github) but has a CPE is also matched against the NVD CPE index bundled in the image, and its CVEs are merged into the same report. This covers binaries identified by their version string, such as OpenSSL or BusyBox in an embedded root filesystem with no package database. The index works offline.

A component that no check could reach is not counted as clean. When a component has no version, or has no package PURL and no CPE that was matched, the report says how many there are and lists them under "Components that could not be checked", in place of "No known vulnerabilities found". _security.json keeps the same list as UncheckedComponents (Count, and Components with Name, Version, PURL and Reason: no-version or no-identifier), and the risk report repeats it in its vulnerability section.

A finding that Trivy files under a distribution's advisory number instead of a vulnerability id (a Debian advisory such as DLA-4792-1 or DSA-..., Oracle Linux ELSA-..., SUSE and openSUSE SUSE-SU-... or openSUSE-SU-..., Bottlerocket BRSA-...) is not listed as a vulnerability. Trivy does this when the advisory data names no CVE for the finding; Red Hat, Ubuntu, Amazon Linux, AlmaLinux and Rocky Linux findings already come out under their CVE. When the advisory names a CVE, the finding is reported under that CVE and the advisory number is kept in the CVE's VendorIDs; the web UI shows it next to the CVE id. A CVE that Trivy did not already report for the package takes the advisory's severity and fixed version, not a severity of its own: when several advisories name it, the highest of their severities and all their fixed versions, and it is marked bomlens:fromAdvisory. A CVE counts as named when it is in VendorIDs, or when PrimaryURL or a reference is that CVE's own record page (on nvd.nist.gov, cve.org, cve.mitre.org, or SUSE's CVE page); a CVE that only appears in some other link is not taken. When the advisory names no CVE, the finding moves to the top-level Advisories list in _security.json, with Trivy's own fields (VulnerabilityID, PkgName, PkgIdentifier, InstalledVersion, FixedVersion, Severity, Title) and the Target it came from. These advisories are not counted in the totals or the severity counts, get no EPSS or KEV lookup, and do not trip --fail-on. The security report lists them under "Advisories with no CVE", and the web UI shows them in the security section as a list of their own below the findings.

Every other id stays a finding, including records from vulnerability databases that describe one vulnerability with no CVE (GHSA, GO, PYSEC, RUSTSEC, MAL, OSV, Debian's TEMP and others). A Node.js security working group entry (NSWG-ECO-...) that names a CVE is reported under that CVE in the same way, with its number in VendorIDs; one that names none stays as it is. A lower-case ghsa- or cve- prefix is written in upper case. This is done to Trivy's result before the other engines' results are merged in, so a CVE that another engine also reports for the same package is listed once. When anything is changed, Trivy's unchanged result is kept in _security_trivy.json.

For severity, CVSS, EPSS, and KEV priority signals and follow-up interpretation, see What the reports mean.


Deep CVE matching (--deep-cve)

Trivy matches vulnerabilities by package identity (PURL), which covers the ecosystem advisory databases well. Some CVEs in older Java libraries, however, are recorded only in the NVD against a CPE identifier, so a PURL-based scan never reaches them. --deep-cve adds a second pass: BomLens derives an NVD-matchable CPE for each Maven component from its groupId (that enrichment step targets Maven), then grype matches every component's CPE, whatever ecosystem it came from, against a bundled NVD database. In practice most of the extra findings are still Maven, since that is where the enrichment adds a CPE the SBOM did not already carry.

./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --deep-cve --generate-only
  • --deep-cve implies --security. The extra findings merge into the same security report (_security.json/.md/.html), tagged with their nvd:cpe source.
  • The scan runs on the opt-in ghcr.io/sktelecom/bomlens-deep-cve:latest image, which bundles grype and its database; it is pulled automatically when the flag is set (override with SBOM_DEEP_CVE_IMAGE). It applies to the base-image modes (source, image, binary, rootfs, SBOM analysis); a firmware or AI-model scan prints a warning and runs without grype, since neither produces package purls for it to match. A source scan runs its post-processing step (where the security report is made) on the deep-cve image; when that image cannot be pulled (offline, or SBOM_PULL=never), the source scan goes on without the grype match, says so, and records the deep-cve step as skipped in bomlens-result.json. So it does when SBOM_SCANNER_IMAGE is a local build with no registry path (bomlens:dev), a version before 1.13.2 or not a BomLens image name, and SBOM_DEEP_CVE_IMAGE is not set: no deep-cve image of that build is known, so post-processing stays on the image you named. If that image has grype (built with SBOM_DEEP_CVE=true), the match runs there and is recorded as it came out. The deep-cve image has no scancode, so with --deep-license as well the licence scan runs first in the deep-license image and the post-processing step then runs on the deep-cve image: the run writes both _scancode.json and _security_grype.json. The web UI does the same when it runs from the deep-license image. A first use pulls each opt-in image once, with docker's progress shown; a pull that prints nothing for two minutes is stopped and the scan goes on without that image.
  • In the web UI (and the desktop app), the same option is the Deep CVE matching (NVD CPE) toggle in the scan options. It is offered on every scan mode that uses the base image — source, Docker image, rootfs, package upload, and SBOM upload — and hidden only for firmware and AI-model scans, matching the CLI. Turning it on switches the security report on too, and the deep-cve image downloads once on first use, either as a sibling container or in-process if the UI itself was launched from that image.
  • CPE matching is looser than PURL matching, because NVD version ranges can be recorded coarsely. By default the scan stays offline: such findings are kept and marked version-unverified in the report (a dagger with a footnote), so a reader knows which rows may be loose-version false positives. To tighten them, set SECURITY_NVD_VERIFY=true with an NVD_API_KEY: each finding is then checked against the live NVD version range and out-of-range false positives are dropped. The verification needs network access and adds minutes.
  • For an OS package (rootfs or image scan), the component's cpe carries the plain upstream version; the distro's own package revision (a Debian +deb12u3, an RPM .el9, an Alpine -r0, ...) stays only in its purl and version. A distro that has already backported a fix keeps the same upstream version in its CPE, so a --deep-cve match against NVD's version range can still flag that CVE even though the installed package is patched. The base security report above, which matches by PURL and OS context, already knows the distro's own advisory state and is the one to trust for an OS package's real vulnerability status; read a --deep-cve finding on an OS package the same way as a version-unverified one, as a lead to check rather than a final verdict.
SECURITY_NVD_VERIFY=true NVD_API_KEY="your-key" \
  ./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --deep-cve --generate-only

Go reachability (--reachability)

A vulnerability in a Go module matters only when the program uses the affected code. --reachability asks govulncheck, the Go project's own checker, which advisories the scanned Go program actually reaches. For a source tree it follows the call graph from the program's entry points; for a Go executable (--target ./app) it looks for the affected functions in the executable's symbols.

./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --reachability --generate-only
./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --target ./app --reachability --generate-only
  • Each Go advisory against a component of the SBOM gets a judgement in _reachability.cdx.json, a CycloneDX VEX document. When the affected code is not reached, analysis.state is not_affected and analysis.detail says why. The justification is code_not_present when the affected package is not in the build, and code_not_reachable when it is there but no affected function is called (source) or linked (executable). When it is reached, the state is exploitable and the detail gives the call path or the symbol.
  • The security report shows the not-reachable findings in a section of their own, with the reason, and leaves them out of its totals. Nothing is hidden: the findings are still in _security.json, each marked with bomlens:reachability. A reached finding is marked with a double dagger in the findings table. The risk report and --fail-on vulnerability= count the same way as the security report.
  • --reachability implies --security. It applies to Go source and Go executables; other scans print a warning and run without it. A source scan runs govulncheck inside the Go image of stage 1, which has the Go toolchain; the post-processing image carries only the govulncheck executable.
  • A source scan judges every Go module the SBOM reads, one for each go.mod (up to 20). The modules are found as the SBOM's are: not under vendor/, node_modules/, a folder whose name starts with ., or the non-shipped folders a source scan leaves out (BOMLENS_INCLUDE_NON_SHIPPED=1 keeps them in both). Each module is read for GOOS linux, darwin and windows with GOARCH amd64, and an advisory reached in any module or on any of the three GOOS is exploitable. A module that a go.work lists is read in workspace mode. A module with a vendor/ folder is read from that folder, as the tree was before the scan's go mod tidy; with BOMLENS_KEEP_BUILD_OUTPUT=1 the tree is not put back, so a module outside a workspace is read from the module cache instead (-mod=mod). With more than 20 modules the step is skipped (too_large).
  • A GOOS the module's code does not build for (every file is constrained to other systems, or it uses a field or function only other systems have) is left out for that module and named in the not_affected details and in bomlens:reachability:goos-not-covered; the module is judged from the GOOS that built. A module that builds for none of the three is not judged (bomlens:reachability:modules-not-judged), and the step is recorded as degraded; every module its go.mod requires then gets no not_affected from the other modules' runs (bomlens:reachability:not-affected-withheld), and none is written at all when that go.mod cannot be read. Any other failure of a govulncheck run (a module that cannot be downloaded, a type error on every GOOS, a run over the time limit) fails the step, the runs left are skipped and nothing is written.
  • govulncheck reads the Go vulnerability database (vuln.go.dev) over the network. When it cannot, the step is recorded as skipped (unavailable) in bomlens-result.json and the security report is as without the option.
  • Only the code govulncheck can see is judged. A function called through reflection or from a plugin is not on the call graph, and an executable's symbols show what is linked, not what runs. A source scan reads the files that GOOS linux, darwin or windows (GOARCH amd64) builds with the default build tags: code that only a custom build tag, another GOOS or another GOARCH compiles in is not seen, and neither is code called only from _test.go files (govulncheck leaves tests out by default). The VEX says so in each not_affected detail and in its metadata.properties (bomlens:reachability:goos, goarch, build-tags, modules). Treat not_affected as the tool's judgement and keep an advisory open where the code is loaded in ways the call graph does not show, or where your build sets its own tags.

Deep license detection (--deep-license)

The basic notice covers the licenses of dependencies (third-party). --deep-license uses scancode-toolkit to also detect license headers in the project's own source code (first-party).

./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --notice --deep-license --generate-only

scancode is heavy and slow (minutes to tens of minutes for a large repo), so it is not in the base image. The CLI runs a source scan's post-process step on the opt-in ghcr.io/sktelecom/bomlens-deep-license:latest image when the flag is set, and pulls it automatically (override with SBOM_DEEP_LICENSE_IMAGE). In the web UI, deep license detection is one of the Generation options and needs the UI itself to run from that image:

SBOM_SCANNER_IMAGE=ghcr.io/sktelecom/bomlens-deep-license:latest ./scripts/scan-sbom.sh --ui

Only the project's own files are scanned: dependency folders (node_modules, vendor, virtual environments), build output and tool folders are left out, since their licences come from the package managers. A tree with more than 20,000 such files is not scanned, and scancode is stopped after 600 seconds; DEEP_LICENSE_MAX_FILES and DEEP_LICENSE_TIMEOUT change the limits (Docker image environment variables). Either case is noted in the log and in the run's result manifest.

Additional output: MyApp_1.0.0_scancode.json


Outbound-license conflicts (--license)

Declares the license you distribute under, so each dependency can be judged against it. A source scan cannot infer this — cdxgen leaves the root license empty for maven and gradle trees — so without the flag no verdict is produced.

./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --license Apache-2.0 --all --generate-only

The value is recorded on the SBOM's root component, and the risk report gains a conflict section. An existing root license — a supplier SBOM's own declaration — is never replaced. For how to read the verdicts see What the reports mean.


Deterministic output (--byte-stable)

Produces a byte-identical SBOM for the same input. It removes meaningless diffs in CI (timestamps, random IDs, ordering differences) and ensures reproducibility.

./scripts/scan-sbom.sh --project "MyApp" --version "1.0.0" --byte-stable --generate-only

What it does: fixes metadata.timestamp to 1970-01-01T00:00:00Z, removes the random serialNumber, sorts components by purl, and sorts keys.


SBOM signing (--sign)

Creates a detached signature on the SBOM with cosign to establish supply-chain trust. It is offline key-based signing (--tlog-upload=false), so no network or OIDC is needed.

# 1) Generate a key (first time only). For a passwordless key, use COSIGN_PASSWORD=""
docker run --rm -v "$PWD":/keys -w /keys -e COSIGN_PASSWORD="" \
  --entrypoint cosign ghcr.io/sktelecom/bomlens:latest generate-key-pair

# 2) Scan while signing (COSIGN_KEY=private key path, COSIGN_PASSWORD=key password)
COSIGN_KEY="$PWD/cosign.key" COSIGN_PASSWORD="" \
  ./scripts/scan-sbom.sh --project MyApp --version 1.0.0 --sign --generate-only

# 3) Verify
docker run --rm -v "$PWD":/w -w /w --entrypoint cosign \
  ghcr.io/sktelecom/bomlens:latest \
  verify-blob --key cosign.pub --signature MyApp_1.0.0_bom.json.sig \
  --insecure-ignore-tlog MyApp_1.0.0_bom.json

The private key is mounted read-only into the container. Additional output: MyApp_1.0.0_bom.json.sig


Troubleshooting

Symptom Cause / fix
trivy not installed ... skipping old image. Pull the latest image with docker pull.
--deep-license requested but scancode not in image run with the bomlens-deep-license image (the CLI does this for a source scan unless SBOM_SCANNER_IMAGE names an image that is not a BomLens image, a short local name with no registry path, or a version before 1.13.2), or build one with --build-arg SBOM_DEEP_LICENSE=true.
Docker is not running in the UI start the Docker engine (Rancher Desktop/Docker Desktop, etc.) and run it again.
Many NOASSERTION in the notice the dependencies have no license metadata. Supplement with --deep-license or check manually.
Port conflict (--ui) specify a different port with UI_PORT.