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.
cd my-project
kunnus sbom repo --output sbom.cdx.jsonWithout --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.
kunnus sbom repo --ecosystem npm --ecosystem pythonHashes 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.