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:
- 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 withUPLOAD_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.
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.
_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):
Security vulnerability report (--security)¶
Scans the generated SBOM with Trivy and reports known vulnerabilities (CVEs). (NVD + OSV + GHSA DB)
_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.
--deep-cveimplies--security. The extra findings merge into the same security report (_security.json/.md/.html), tagged with theirnvd:cpesource.- The scan runs on the opt-in
ghcr.io/sktelecom/bomlens-deep-cve:latestimage, which bundles grype and its database; it is pulled automatically when the flag is set (override withSBOM_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, orSBOM_PULL=never), the source scan goes on without the grype match, says so, and records thedeep-cvestep as skipped inbomlens-result.json. So it does whenSBOM_SCANNER_IMAGEis a local build with no registry path (bomlens:dev), a version before 1.13.2 or not a BomLens image name, andSBOM_DEEP_CVE_IMAGEis 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 withSBOM_DEEP_CVE=true), the match runs there and is recorded as it came out. The deep-cve image has no scancode, so with--deep-licenseas 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.jsonand_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=truewith anNVD_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
cpecarries the plain upstream version; the distro's own package revision (a Debian+deb12u3, an RPM.el9, an Alpine-r0, ...) stays only in itspurlandversion. A distro that has already backported a fix keeps the same upstream version in its CPE, so a--deep-cvematch 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-cvefinding 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.stateisnot_affectedandanalysis.detailsays why. Thejustificationiscode_not_presentwhen the affected package is not in the build, andcode_not_reachablewhen it is there but no affected function is called (source) or linked (executable). When it is reached, the state isexploitableand 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 withbomlens: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. --reachabilityimplies--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 undervendor/,node_modules/, a folder whose name starts with., or the non-shipped folders a source scan leaves out (BOMLENS_INCLUDE_NON_SHIPPED=1keeps 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 isexploitable. A module that ago.worklists is read in workspace mode. A module with avendor/folder is read from that folder, as the tree was before the scan'sgo mod tidy; withBOMLENS_KEEP_BUILD_OUTPUT=1the 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_affecteddetails and inbomlens: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 asdegraded; every module itsgo.modrequires then gets nonot_affectedfrom the other modules' runs (bomlens:reachability:not-affected-withheld), and none is written at all when thatgo.modcannot 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) inbomlens-result.jsonand 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.gofiles (govulncheck leaves tests out by default). The VEX says so in eachnot_affecteddetail and in itsmetadata.properties(bomlens:reachability:goos,goarch,build-tags,modules). Treatnot_affectedas 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).
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:
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.
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. |