Scanning Source Code

kunnus sbom repo walks a source tree, detects every package ecosystem present, and emits one CycloneDX 1.6 SBOM covering all of them. No configuration file, no per-language setup.

shell
cd my-project
kunnus sbom repo --output sbom.cdx.json

Without --output the SBOM goes to stdout so you can pipe it; all log output goes to stderr. The path argument defaults to the current directory, so kunnus sbom repo ~/src/firmware works too.

What gets detected

Detection is marker-based: kunnus looks for the manifest and lockfiles each ecosystem uses (package.json, go.mod, Cargo.lock, pom.xml, requirements.txt, …) and enables the matching extractors. One scan covers more than 20 language ecosystems — npm/pnpm/yarn/bun, Python (requirements, poetry, pdm, Pipfile, uv, conda), Go, Rust, Java (Maven, Gradle), .NET, PHP, Ruby, Swift (SwiftPM, CocoaPods), Haskell, Lua, R, and C/C++ (Conan) — plus embedded firmware manifests and vendored C/C++ library directories.

Monorepos need nothing special: every ecosystem found anywhere in the tree lands in the same SBOM, and duplicate declarations across sub-projects collapse in the deduplication stage.

Restricting the scan

To scan only specific ecosystems, pass --ecosystem (repeatable). To add or remove individual scalibr plugins, use --enable / --disable.

shell
kunnus sbom repo --ecosystem npm --ecosystem python

Hashes and licences

Where a lockfile embeds content digests (npm, yarn, pnpm, bun, poetry, uv, Cargo, Conan, ESP-IDF, …), kunnus carries them into the SBOM as component hashes — a BSI TR-03183-2 requirement. Licences are attached from package data that is available offline; for ecosystems whose lockfiles carry no licence (Go, Rust, .NET), the opt-in --online-licenses flag fills the gap via deps.dev. That flag is the only scan feature that touches the network.

Dependency relationships

Where a lockfile pins the resolved graph, kunnus mines the real edges between components into the CycloneDX dependencies[] array — CISA's Component Dependency Relationship element, not just a flat inventory. Six lockfile formats carry that data today: Cargo.lock, composer.lock, Gemfile.lock, package-lock.json / npm-shrinkwrap.json (v2/v3 path-keyed maps; v1 is not parsed), packages.lock.json (NuGet), and renv.lock. Everything else contributes a root-presence claim only — and the SBOM states that incompleteness in compositions[] instead of implying a graph it does not have. An edge whose target matches no component in the document is dropped; a ref is never invented.

Declared ranges vs. lockfile pins

For Cargo, NuGet and Python the manifest declares a constraint (>=2.0, [3.0.0,4.0.0)) while the lockfile beside it pins what actually resolved. Emitting both double-counts the component and invents a phantom at a version that was never released, carrying a CPE that matches no advisory — on a real Rust workspace that inflated the SBOM by 74 components. The resolved pin wins and the declared twin goes, but only as far as the lockfile's authority reaches: a Cargo.lock covering every manifest in the workspace disables the manifest extractor outright, while for .NET and Python a declaration is dropped per path — only when the manifest sits under a lockfile directory whose lockfile pins the same name. Where one project is locked and another is not, the unlocked project keeps its declarations.

The full flag reference lives in the repository: docs/cli.md on GitHub.