SBOM dependency resolution

This page covers language detection, licence resolution (local manifests and the package registry), system dependencies, and test dependency classification. The root project’s own manifests, output formats, and usage are on The sbom subcommand.

Language detection

For every other component – vendored trees, vendor/, node_modules, resources, and loose source drops – adacovex infers the language from the file extensions actually present. The extension is the source of truth: a .py file reports Python even when a Cargo.toml sits next to it, and the manifest language only breaks ties.

Supported extensions:

  • Ada: .ads, .adb, .ada, .gpr

  • JavaScript: .js, .mjs, .cjs

  • TypeScript: .ts, .tsx

  • CSS: .css

  • HTML: .html, .htm

  • Python: .py

  • Go: .go

  • Rust: .rs

  • C: .c, .h

  • C++: .cpp, .cc, .cxx, .hpp, .hh, .hxx

  • C#: .cs

  • Java: .java

  • Ruby: .rb

  • PHP: .php

  • Swift: .swift

  • Kotlin: .kt, .kts

  • Scala: .scala

  • OCaml: .ml, .mli

  • Lua: .lua

  • Perl: .pl

  • Haskell: .hs

  • Elixir: .ex, .exs

  • Erlang: .erl, .hrl

  • Clojure: .clj, .cljs

  • Dart: .dart

  • Shell: .sh, .bash

  • PowerShell: .ps1

  • SQL: .sql

  • Fortran: .f, .f90, .f95, .f03

  • Assembly: .s, .asm

  • R: .r

  • Julia: .jl

  • Zig: .zig

  • VHDL: .vhd, .vhdl

  • Tcl: .tcl

A directory that mixes languages reports its top 3 languages by file count (for example "Ada; C; C++"), so a mixed-language vendored drop is summarised by what it actually contains rather than by a single guess.

Licence resolution

Vendored manifest ecosystems report their licence from the local manifest: package.json (license) for npm/pnpm, Cargo.toml for cargo, and pyproject.toml / composer.json for pypi / composer. A licence file beside the manifest is classified into an SPDX identifier when the manifest carries none. When both are silent, adacovex resolves the version, website, and licence from the package registry as an online fallback. The resolver dispatches on the ecosystem (the PURL type) through one static table, so adding a language is one row rather than a new code path:

  • npm – npm view <pkg> version license homepage --json, parsed for the three fields from one JSON object.

  • pnpm – pnpm show <pkg> version license homepage --json, likewise.

  • cargo (Rust) – cargo search <pkg>, with the SPDX id read from the (license: ...) token in the output.

  • go – go list -m --json <module>@latest, with the version read from the Version field. See Go modules below: the Go toolchain reports no licence and no website, so both come from files instead.

  • An ecosystem with no portable, reliable registry query keeps an empty licence, version, and website; nothing is ever guessed.

Set ADACOVEX_OFFLINE=1 (see Global configuration and state) to skip every registry query. The resolver then returns only the offline-known fields, and the native suite runs with it, so the assessment gates never touch the network.

Go modules

A vendored Go component resolves its version and licence offline, from files, and only falls back to the registry for the version. Nothing in this path names a forge, so the same code serves a module on GitHub, GitLab, Gitea, Codeberg, or a private mirror.

  • Version, offline. A vendored module’s own require block lists only its dependencies, so the module’s version cannot come from its own go.mod. It comes from the vendor root’s modules.txt, the manifest go mod vendor writes for every vendored module, as a # <module> <version> line – the only offline source of it.

  • Version, fallback. When modules.txt is absent, go list -m --json <module>@latest supplies the latest published version. The query goes through whatever module proxy GOPROXY names, so the Go toolchain decides which source answers and adacovex never constructs a host name.

  • Licence. The Go toolchain reports no licence in any form, so the licence is read from the licence file beside the manifest (LICENSE, LICENCE, or COPYING, each with an optional .md or .txt suffix) and classified into an SPDX identifier by matching marker phrases from the licence text: Apache-2.0, MIT, MPL-2.0, ISC, Unlicense, and the two- and three-clause BSD variants. A row matches only when every marker it requires is present and none it excludes is present, so a four-clause BSD or an ambiguous GPL text reports no licence. A licence is never guessed.

  • Website. Always empty. go list -m has no website field, and its Origin.URL names a forge-specific VCS endpoint, not a project home page, so adacovex does not surface it.

A vanity import path (one whose first element is not a forge host, such as golang.org/x/text, gopkg.in/yaml.v3, or k8s.io/api) resolves the same way as a forge-hosted path: the version and the licence come from files, and no URL is ever constructed from the import path.

The PURL type stays pkg:golang and the PURL is pkg:golang/<module path>@<version>. The registry ecosystem token is go, not golang, so the vendored-manifest reader carries both: the PURL type follows the package-url specification while the resolver dispatches on the name of the CLI it spawns.

The npm and pnpm rows answer for all three fields with one --json call, so each component boots node once instead of once per field. The fallback runs only when the offline read finds nothing, so a vendored package that ships a licence never touches the network – and the same holds for a Go tree that carries a modules.txt and a licence file. The resolved licence flows into every SBOM format (CycloneDX licenses, SPDX licenseConcluded / licenseDeclared, Markdown License column) and the dashboard detail panel; the resolved version and website appear in the dashboard detail panel and the /api/deps JSON.

The resolver caches each answer in a per-project store under the project’s result cache (the same --cache-dir the scan uses), keyed by the target directory as well as the ecosystem and package name, with a 7-day TTL – the same scheme as the system-tool version probes. The content-addressed result cache does not cover these registry calls, so without this layer a warm run still re-paid them; the meta cache serves every repeat from disk with no subprocess spawn, and two projects that share a cache directory never serve each other’s resolved licence or version.

Bundled dashboard assets (FlexSearch, nomnoml, graphre) resolve their licence and website live from the package registry when a loose vendored copy is scanned, preferring pnpm show <pkg> license and falling back to npm, yarn, then bun – the same preference chain as every JavaScript component (see Licence resolution). The SBOM and the Credits tab therefore track the real upstream licence instead of a hard-coded copy.

System dependencies

Discover_System_Dev_Deps scans the project’s build and dev files (Makefiles, shell scripts, Python tools, CI workflows, GPR files, Ada sources) for a curated set of known system binaries, then keeps only the tools that are installed on PATH. Each becomes a system-scope component of the root with a pkg:generic/<name> PURL, a resolved version, and no external link or licence – by design adacovex provisions only the version for system tools and never guesses a repository or licence for them.

The table is deny-by-default and grouped by category (build drivers, language implementations and package managers, VCS, documentation tooling, CI and container plumbing, performance engineering). A tool lands in the SBOM only when (a) its exact lowercase name appears as a whole word in one of the scanned build files, and (b) it is installed on PATH; whole-word matching keeps “makefile” from registering “make”. The table stores only the name and category: the version-probe flag is inferred at run time by trying --version, then -v, then the version subcommand, and taking the first flag that yields a version token – so a subcommand-only tool (go, fossil, git-lfs) needs no special-cased column and a misconfigured entry cannot exist.

adacovex and its covex alias take one exception to rule (b): the running binary is the dependency, so a reference registers adacovex from the reference alone, at the running binary’s version. Three rules order it: the audited project being adacovex itself keeps it the root; a manifest declaration keeps its own scope (base from alire.toml, dev from alire-dev.toml); otherwise the installation method decides, as adacovex status reports it – a source build is dev, a binary installed outside a project is system.

Resolved versions are never hardcoded. Each probe result is cached per machine (outside the result cache) together with the identity of the binary it was probed from: the PATH-resolved executable path plus its size and mtime. A cached version is served only after adacovex re-resolves the tool on PATH and compares that identity, so an upgraded, replaced, or PATH-shadowed binary re-probes on the next run while an unchanged toolchain serves from cache with no subprocess spawn. Every version in the SBOM describes the binary that is installed now.

system is a first-class dependency scope, distinct from base, dev, transitive, vendored, and test; the dashboard gives it its own filter checkbox, badge colour, and legend entry, and the SBOM lists it under system scope.

The language shows up in the dashboard Dependency tab (per-dependency detail popup) and in every SBOM renderer: CycloneDX components[].language, SPDX/JSON adacovex:language property, and the Markdown table’s Language column.

Tools that are really language packages are never registered as system tools; they register as language components instead. The root project’s own manifests (package.json, go.mod, requirements*.txt) register the same way, and every read of them is structural – the rules are on Root project declarations.

Test dependencies

A dependency used only by the project’s tests is classified test (the adacovex:dep_scope property value "test"). adacovex recognises test-only declarations in every supported ecosystem’s manifest, in addition to the Alire [[test-depends-on]] sections and test project files:

Manifest file

Test label

package.json

a dependency section whose key contains test (for example testDependencies or devTestDependencies); a name whose last segment starts or ends with test (for example @playwright/test, vitest)

Cargo.toml

the [dev-dependencies] section (Cargo’s test-only section); any section whose name contains test (for example [target.'cfg(test)'.dependencies])

go.mod

no native test-only section: the name heuristic is the signal (a module path whose last path segment starts or ends with test, for example github.com/stretchr/testify)

composer.json

the require-dev section

Gemfile

gems inside a group :test block (any group name containing test)

pom.xml

<dependency> blocks whose <scope> is test

pyproject.toml

optional-dependencies extras whose name contains test (for example the test extra); Poetry sections such as [tool.poetry.group.test.dependencies]

Package.swift

dependencies declared inside a .testTarget(...) block

requirements*.txt

no native test section: the name heuristic is the signal

A vendored component (for example a package under node_modules or vendor/) is classified test when the project manifest that owns the vendor directory declares it under one of these test labels, or when its name carries the test label. The name heuristic checks the full name and then the last segment after any / or :, so @playwright/test, test-case, github.com/stretchr/testify, and org.testng:testng all qualify, and it applies to lockfile-resolved names as well: pnpm-lock.yaml / package-lock.json / yarn.lock entries next to an owner package.json, Cargo.lock crate names, and alire.lock crates that the manifest sets leave transitive. The e2e fixture’s @playwright/test is the canonical example: it stays a devDependencies entry of tests/e2e/package.json (and a pnpm-lock.yaml entry) and is classified test by name.