Supported ecosystems¶
A hands-on guide using the per-language example projects under examples/. Run each example to see the SBOM output right away.
Example directory structure¶
examples/
├── java-maven/ # Java + Maven
├── java-gradle/ # Java + Gradle
├── nodejs/ # Node.js + npm
├── python/ # Python + pip / Poetry
├── go/ # Go modules
├── ruby/ # Ruby + Bundler
├── php/ # PHP + Composer
├── rust/ # Rust + Cargo
├── dotnet/ # .NET + NuGet
├── swift/ # Swift + SPM (Swift Package Manager)
├── modelica/ # Modelica (.mo) uses() declarations
└── docker/ # Docker image analysis
Common run steps¶
Every source-code example runs the same way from the repository root: point --target at the example folder and pick a project name. The results are saved in a {Project}_{Version}/ subfolder. For the Node.js example:
# 1. Generate the SBOM (from the repository root)
./scripts/scan-sbom.sh --project "NodeExample" --version "1.0.0" --target examples/nodejs --generate-only
# 2. Check the result
jq '.components | length' NodeExample_1.0.0/NodeExample_1.0.0_bom.json
By default a source scan leaves out the manifests under test, fixture, example, benchmark and demo folders (test, tests, spec, fixtures, testdata, __tests__, e2e, example, examples, benches, benchmarks, playground, samples) and the GitHub Actions workflows in .github/workflows, because none of them ship with the product. The SBOM records the patterns in the bomlens:excluded-paths property and the manifest files it left out in bomlens:excluded-manifests. To include them, set BOMLENS_INCLUDE_NON_SHIPPED=1 (Docker image environment variables).
For npm, Python (pip), Go and Rust (Cargo) packages, a source scan also fills each component's copyright from the license files the installed package ships (LICENSE, LICENCE, COPYING, NOTICE, COPYRIGHT), so the NOTICE can print an attribution line. Only a line that begins with Copyright followed directly by (c), the copyright sign, a year or by (or with (c) or the sign followed by a year), and that names a holder, is used. A notice with no year, such as Copyright Acme, Inc., is not taken. Lines come from the package's own folder only (its .dist-info folder for Python; for Go the folder go list reports, which is the module cache, the replacement's folder for a replaced module, or vendor/; for Rust the folder cargo metadata reports, which is the cargo registry, a git checkout or a path dependency), at most 6 files, the first 64 KiB and 400 lines of each, and at most 5 statements per component, joined with ;. Lines that are an unfilled template (<year>, [fullname]) or the text of a license itself (for example the Free Software Foundation's own line in a GNU license) are skipped, and so are files that link outside the package. A component that already has a copyright is not changed, a package with no such line stays empty, and each value set carries the property bomlens:copyrightSource. Set BOMLENS_NO_COPYRIGHT=1 to turn this off. Java (Maven) is not covered: Maven downloads jar files rather than source folders, so the license files of a dependency are not on disk to read. A replaced Go module carries the statement of the code that is actually built, which can be a different version from the one the component names. The scanned project's own Go module and Rust workspace crates are not filled. The Rust step reads the crate sources that are already on disk and never goes to the network for them, so with FETCH_LICENSE=false or --byte-stable a crate that was not downloaded stays empty. A Go or Rust component whose folder is not on disk, for example because the download failed, stays empty. When no folder of an ecosystem could be listed at all, the SBOM records it in the bomlens:copyrightUnread metadata property (golang, cargo), so that a missing copyright can be told from a step that did not run.
The sections below give the ready-to-paste command for each language.
Java (Maven)¶
./scripts/scan-sbom.sh --project "JavaMavenExample" --version "1.0.0" --target examples/java-maven --generate-only
Detected file: pom.xml
<!-- example pom.xml -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.0</version>
</dependency>
</dependencies>
Note: cdxgen resolves the whole build graph, so BomLens filters the SBOM to the deployable set — compile and runtime scope — and drops the test and provided toolchain (JUnit, Lombok, and the like) so the result reflects what ships rather than the full build. To keep the complete resolved graph instead, set
BOMLENS_MAVEN_FULL_GRAPH=1(Docker image environment variables). A runtime dependency declared with<optional>true</optional>is dropped as well, because cdxgen gives it the same tag as a test-scope one. A multi-module reactor can carry modules that never get deployed alongside the ones that do: a demo, or a module the project itself excludes frommvn deploy(maven-deploy-plugin'sskipresolving totrue, however it is set). Such a module's own component is left out. A dependency only that module needs is left out too, but only when no deployed module in the same reactor declares it at any scope (a deployed module's ownprovidedortestdeclaration of it, not just acompile/runtimeone, is enough to keep it, since the SBOM's dependency graph does not record which scope each edge represents).BOMLENS_MAVEN_FULL_GRAPH=1keeps everything this leaves out. The excluded modules and the components dropped because of them are recorded on the SBOM asbomlens:excluded-modulesandbomlens:excluded-components.Note: cdxgen reads a Maven component's license from its own
pom.xmlonly, so one that declares no<licenses>and relies on Maven's own inheritance from a parent POM comes through with none. BomLens now walks that parent chain itself (the reactor's own modules, then the local repository for a parent outside it) and fills the license from the first ancestor that declares one, leaving the component untouched if none do or if it already carries a license. The filled component is recorded with abomlens:licenseSourceproperty set toparent POM.Note: dependency and license resolution need the container to reach Maven Central, or your mirror. When it cannot, the result holds only the dependencies written in
pom.xml, with no transitive dependencies and no licenses, and the scan finishes without a warning. If the component count is far below what you expect, check network access first.
Java (Gradle)¶
./scripts/scan-sbom.sh --project "JavaGradleExample" --version "1.0.0" --target examples/java-gradle --generate-only
Detected file: build.gradle or build.gradle.kts
Note: as with Maven, resolution needs the container to reach Maven Central, or your mirror. When it cannot, the result can hold only the direct dependencies, with no licenses. If the component count is far below what you expect, check network access first.
Android¶
Detected when the project has a Gradle root (settings.gradle[.kts] or build.gradle[.kts] at the root) and shows a sign of the Android Gradle plugin: the plugin id in the version catalog (gradle/libs.versions.toml), the plugin id or a catalog alias in a build script (root or app/), a Kotlin DSL namespace declaration, or an AndroidManifest.xml a few levels down. A manifest with no Gradle root does not count on its own, which also keeps a .NET MAUI app's Platforms/Android/AndroidManifest.xml from being misread as an Android project.
Android needs an SDK platform image BomLens does not publish: the Android SDK it contains is not open source, and Google's terms do not allow redistributing it, so the image is built locally instead. The project's compileSdk/compileSdkVersion picks the API level (34 if none is found); the scan then looks for bomlens-android-sdk<API>:latest, and if it is missing, prints the build command instead of failing on a missing image:
Building it means accepting Google's SDK terms yourself. Set ANDROID_IMAGE_PREFIX to use an image built elsewhere.
Note: images published under
ghcr.io/sktelecom/bomlens-android-sdk<API>up to v1.9.0 still work for scans on that release line, but nothing new is pushed to them; a current scan needs a locally built image.
Node.js¶
./scripts/scan-sbom.sh --project "NodeExample" --version "1.0.0" --target examples/nodejs --generate-only
Detected file: package.json + package-lock.json (or yarn.lock, pnpm-lock.yaml)
Note: a lock file pins the exact installed versions. Dependencies are still captured from
package.jsonwithout one, but committing a lock file makes the result reproducible.Note: the SBOM is filtered to the production dependency set, so devDependencies are dropped and the result reflects what ships. To keep the full dev-plus-production graph instead, set
BOMLENS_NODE_FULL_GRAPH=1(Docker image environment variables).Note: an npm workspace member registered in
package-lock.jsonsurvives the file-level exclusion above even when its own directory sits under an excluded tree, because cdxgen readspackage-lock.jsondirectly. Such a member's own component is left out too, along with a dependency only that member needs, unless a kept member reaches it as well.BOMLENS_INCLUDE_NON_SHIPPED=1(the same switch as the file-level exclusion above) keeps everything this leaves out. The excluded members and the components dropped because of them are recorded on the SBOM asbomlens:excluded-membersandbomlens:excluded-components. A yarn workspace member is not caught the same way yet.
Python¶
./scripts/scan-sbom.sh --project "PythonExample" --version "1.0.0" --target examples/python --generate-only
Detected file: requirements.txt, or pyproject.toml + poetry.lock
Note: pin versions with
==or commit a lock file such aspoetry.lock. Without them the SBOM lists the versions available at scan time, not the ones you run. The bundled example pins its versions, and every component carries a license.
Go¶
Detected file: go.mod + go.sum
Note:
go.sumis required for accurate version hashes. Rungo mod tidyfirst, then try again.
Ruby¶
./scripts/scan-sbom.sh --project "RubyExample" --version "1.0.0" --target examples/ruby --generate-only
Detected file: Gemfile.lock
Note: with no
Gemfile.lockcommitted, BomLens runsbundle lockbefore scanning. Commit the lock file so the SBOM matches what you deploy.
PHP¶
./scripts/scan-sbom.sh --project "PHPExample" --version "1.0.0" --target examples/php --generate-only
Detected file: composer.lock
Note: with no
composer.lockcommitted, BomLens resolves one itself (composer update --no-dev, on a copy ofcomposer.jsonwithoutrequire-devandconfig.lock, with platform requirements ignored) before scanning, the same as it does for Ruby'sGemfile.lock. Yourcomposer.jsonis put back afterwards. A dependency version that needs a newer PHP than the project declares can be picked, because the PHP version is not enforced. cdxgen already tags each composer component with its resolved scope (requirebecomes required,require-devbecomes optional), so BomLens filters the SBOM to the required set, the same way it does for Maven. To keep the full require-plus-require-dev graph instead, setBOMLENS_PHP_FULL_GRAPH=1(Docker image environment variables).
Rust¶
./scripts/scan-sbom.sh --project "RustExample" --version "1.0.0" --target examples/rust --generate-only
Detected file: Cargo.lock
Note: cdxgen reads only
Cargo.lock, which carries no licenses, so BomLens fills them fromcargo metadata, which reads each crate's own manifest (your own workspace crates included). The container needs a route to the crate registry (crates.io or your mirror) for this. Without one, the SBOM records a license for almost no Cargo packages and the notice file built from the scan is nearly empty. The scan then records the stepcargo-license-metadataas failed on the SBOM, which the result screen and the conformance report show, and the log sayscould not read crate licenses. A crate that declares its license only as a file (license-file) stays without one. Alternatives such asMIT OR Apache-2.0are written in a fixed order, so the same pair is one entry in the notice however a crate spells it, and a value that is not an SPDX expression is kept as a plain license name. The bundled example lists 147 libraries and 146 of them carry a license.BOMLENS_NO_CARGO_LICENSE=1turns this off, and so doesFETCH_LICENSE=falseon the command line.--deep-licensedoes not help here: it scans your own source files, not the dependencies, and writes a separate report.Note: a Cargo workspace member registered in
Cargo.locksurvives the file-level exclusion above even when its own directory sits under an excluded tree, because cdxgen readsCargo.lockdirectly. Such a member's own component is left out too, along with a dependency only that member needs, unless a kept member reaches it as well (any way at all).BOMLENS_INCLUDE_NON_SHIPPED=1(the same switch as the file-level exclusion above) keeps everything this leaves out. The excluded members and the components dropped because of them are recorded on the SBOM asbomlens:excluded-membersandbomlens:excluded-components.
.NET¶
./scripts/scan-sbom.sh --project "DotNetExample" --version "1.0.0" --target examples/dotnet --generate-only
Detected files: *.csproj, *.fsproj, *.sln or *.slnx, at the root or in folders up to three levels below it. packages.lock.json is read when present
Note: detection needs a project or solution file.
packages.lock.jsonis not required, but committing one (dotnet restore --use-lock-file) pins the resolved versions so the SBOM is reproducible.
Swift / iOS¶
./scripts/scan-sbom.sh --project "SwiftExample" --version "1.0.0" --target examples/swift --generate-only
Detected files: Package.swift (+ Package.resolved) for Swift Package Manager, or Podfile.lock for CocoaPods.
Dependencies are read from the committed lockfiles, so include them in the scan:
- Swift Package Manager:
Package.resolved(runswift package resolvefirst if it is missing). - CocoaPods:
Podfile.lock(produced bypod install). BomLens parses it directly, so the scanning machine needs neither macOS nor a CocoaPods install.
Note: UIKit and other Xcode-driven platform dependencies require macOS and are not resolved in the Linux scanner.
Note: the bundled example lists 6 components, of which 5 carry a PURL (83%) and 2 a license (33%). Both are below the default thresholds of the conformance checks: the PURL check fails and the license check warns. The two resolved packages (
swift-argument-parser,swift-log) have a PURL and a license. The rest are the package itself and the platform modules it imports (Foundation,XCTest,dispatch), which have no license to declare. BomLens drops the versionless duplicates of a resolved package that the import scan adds, so a package is counted once.
Modelica¶
./scripts/scan-sbom.sh --project "ModelicaExample" --version "1.0.0" --target examples/modelica --generate-only
Detected files: *.mo
cdxgen has no Modelica cataloger, so the usual package-manager approach reads no dependencies. Instead, BomLens parses the annotation(uses(...)) block a .mo package declares directly, picking up each library named there together with its version. A uses() declaration carries only a name and a version, so a license comes from BomLens's own table of known libraries (docker/lib/modelica-library-map.json), not from your project. The table records the license read from each library's upstream license file (Modelica Standard Library 3.2.3 and later: BSD-3-Clause; Buildings 13.0.0 and later: BSD-3-Clause-LBNL). Both libraries in the bundled example get a license this way (2 of 2), recorded with the property bomlens:licenseSource = modelica-library-map. A library outside the table, or a version older than the recorded one, has no license.
Note: only the libraries declared directly in
uses()are identified. Modelica has no lockfile, so transitive dependencies are not resolved, and the model's own internal structure (its components, parameters, connections) is not open-source dependency data, so it is left out.
Conda¶
Detected files: environment.yml or environment.yaml at the root of the scanned folder.
cdxgen has no Conda cataloger, so BomLens reads the dependencies: list of the environment file itself. Each conda entry becomes a pkg:conda/... component, and each entry under a pip: item becomes a pkg:pypi/... component.
When the file is read successfully, BomLens turns off cdxgen's Python step for that scan. Otherwise a setup.py or requirements.txt elsewhere in the tree would replace the environment file as the source of truth.
Note: only the two-level layout shown above is recognized, with
dependencies:at the left edge, entries indented by two spaces andpip:items by four. A file that uses another layout (a different indent width, a flow-style list, a tab) is not partly read. It is skipped as a whole and cdxgen's Python step runs as before. An entry with no version or only a range such as>=1.0does not name a version, so it is recorded without a version or PURL and carries the propertybomlens:versionUnpinned. Editable and VCS installs underpip:are left out.
Docker image analysis¶
Run Docker image analysis from the project root.
# Analyze a public image
./scripts/scan-sbom.sh \
--project "NginxSBOM" \
--version "1.25" \
--target "nginx:1.25-alpine" \
--generate-only
# Ubuntu-based image
./scripts/scan-sbom.sh \
--project "UbuntuSBOM" \
--version "22.04" \
--target "ubuntu:22.04" \
--generate-only
Note:
--targettakes an image name or an image tar here. Pointing it at theexamples/dockerfolder, which holds no dependency manifest, finds no components.
Files required for detection¶
If source analysis finds no dependencies, check for the lock file below.
| Language | Required file |
|---|---|
| Java (Maven) | pom.xml |
| Java (Gradle) | build.gradle or build.gradle.kts |
| Node.js | package.json + package-lock.json or yarn.lock |
| Python | requirements.txt or pyproject.toml + poetry.lock |
| Go | go.mod + go.sum |
| Conda | environment.yml or environment.yaml (root folder) |
| Rust | Cargo.lock |
| Ruby | Gemfile.lock |
| PHP | composer.lock |
| .NET | *.csproj, *.fsproj, *.sln or *.slnx (root or up to three folders down); packages.lock.json is optional |
| Swift | Package.swift (+ Package.resolved) or Podfile.lock |
| Modelica | a .mo file with an annotation(uses(...)) block |
| Android | a Gradle root (settings.gradle[.kts] or build.gradle[.kts]) plus a sign of the Android Gradle plugin, see Android |
| Docker image | no file: pass an image name or an image tar as --target |
Results on the bundled examples¶
The bundled examples were scanned on 2026-09-21 with BomLens 1.12.0 and the source-scan changes made after it. The table counts software components only. The Copyright column is the share of components whose copyright is filled (see above); Ruby, PHP, .NET, Swift and Modelica are not covered. The default conformance thresholds are 90% for PURL and 80% for license.
| Example | Components | PURL | License | Copyright |
|---|---|---|---|---|
| Node.js | 120 | 100% | 100% | 95% |
| Python | 39 | 100% | 100% | 82% |
| Go | 19 | 100% | 100% | 94% |
| Ruby | 9 | 100% | 100% | 0% |
| PHP | 16 | 100% | 100% | 0% |
| .NET | 70 | 100% | 98% | 0% |
| Rust | 157 | 100% | 99% | 83% |
| Swift | 6 | 83% | 33% | 0% |
| Modelica | 2 | 100% | 100% | 0% |
Java (Maven), Java (Gradle) and Docker are not in the table. The Java scans could not be measured on the network used, where Maven Central did not serve artifacts (see the notes above). The Docker example is an image-scan example and finds nothing when scanned as a folder. These are results on small example projects, not a forecast for yours: a real project's coverage depends on its own dependencies.
Comparing results¶
The PURL (Package URL) format in the generated SBOM differs by language.
| Language | PURL example |
|---|---|
| Java | pkg:maven/org.springframework.boot/spring-boot@3.2.0 |
| Node.js | pkg:npm/express@4.18.2 |
| Python | pkg:pypi/requests@2.31.0 |
| Go | pkg:golang/github.com/gin-gonic/gin@v1.9.1 |
| Rust | pkg:cargo/serde@1.0.193 |
| Ruby | pkg:gem/rails@7.1.2 |
| PHP | pkg:composer/laravel/laravel@10.3.3 |
| .NET | pkg:nuget/Newtonsoft.Json@13.0.3 |
| Swift | pkg:swift/github.com/apple/swift-log@1.5.0 |
| Docker (OS packages) | pkg:deb/debian/curl@7.88.1 |
Troubleshooting¶
If you run into trouble running an example, see the troubleshooting section of the CLI reference.
Related: First scan | CLI reference