GCA · OUTPUTS & REPORTING

    SBOM Generation

    Producing a CycloneDX / SPDX Software Bill of Materials for the scanned repo.

    SBOM Generation

    gadriel code sbom produces a Software Bill of Materials (SBOM) for the scanned project. A single canonical component set is discovered once and then emitted in two industry-standard formats, so the two artifacts can never disagree on what was scanned.

    Related pages: Reports & outputs · Compliance reporting · Troubleshooting.


    Usage

    gadriel code sbom [PATH] [--format both|spdx|cyclonedx] [--diff <git-ref>]
    FlagMeaning
    PATHDirectory to scan (defaults to the current directory).
    --formatboth (default), spdx, or cyclonedx.
    --diff <git-ref>Compare against a prior SBOM snapshot (e.g. HEAD~1, a tag, or a short SHA). See Diffing.

    Outputs

    Both files are written into .security/:

    FileFormatSpec version
    .security/sbom.spdx.jsonSPDXSPDX-2.3 (dataLicense: CC0-1.0)
    .security/sbom.cyclonedx.jsonCycloneDX1.5 (bomFormat: CycloneDX)

    Both are generated from the same discovered component set, so a component that appears in one always appears in the other with the same identity. The CycloneDX bom-ref for each component is set equal to its PURL, which is also the key used for diffing.


    Enrichment: reachability and dependency health

    Beyond a plain component inventory, Gadriel annotates each component with two signals unique to the tool. Because SPDX and CycloneDX model extension data differently, the same information is attached through each format's native mechanism.

    CycloneDX — as components[*].properties[] name/value pairs:

    PropertyValues
    gadriel:reachabilityreachable · not-reachable · unknown
    gadriel:health_scorehealthy · poor · abandoned · unknown
    gadriel:directtrue · false
    gadriel:dependency_typedirect · transitive
    gadriel:manifestlockfile path the component came from
    gadriel:license_sourcelockfile · bundled-pypi-top-N · online-pypi-pending (when known)

    SPDX — as a single OTHER annotation per package whose comment carries the same fields (note the gadriel. dot namespace here, versus gadriel: colon in CycloneDX):

    gadriel.reachability=<v>; gadriel.health=<v>; gadriel.direct=<v>; gadriel.manifest=<v>
    
    • Reachability answers "is this dependency actually reachable from your code?" It is computed by building a call graph, linking lockfile dependencies into it, and consulting a reachability oracle per dependency. If the graph cannot be built, every component is left unknown.
    • Dependency health is a categorical bucket (healthy / poor / abandoned / unknown) rather than a numeric score.

    Honest limits on enrichment. In the standalone gadriel code sbom path, dependency health is currently always unknown (the metadata pipeline that populates it has not yet landed), and reachability is only non-unknown when an on-demand call-graph build succeeds — otherwise it, too, is unknown. Populated reachability and health are surfaced most reliably when the SBOM is produced as part of a full scan that has already built the graph.


    Diffing against a prior snapshot

    gadriel code sbom --diff HEAD~1

    --diff compares the current component set against a previously stored SPDX snapshot and prints what changed. Snapshots are written per commit to .security/history/sbom-<commit-sha>.spdx.json (the short SHA comes from git rev-parse --short HEAD, falling back to uncommitted).

    Reference resolution: an exact sbom-<ref>.spdx.json match is tried first; if the ref is HEAD or begins with HEAD~, the newest snapshot by modification time is used. An unresolvable ref is an error.

    The diff is keyed by PURL (with the version stripped, so version bumps are detected rather than reported as add+remove) and prints a summary plus per-change lines:

    → SBOM diff against HEAD~1 (.security/history/sbom-<sha>.spdx.json)
      +N added, -N removed, ↑N version-bumped, ⟳N cve-changed
        + pkg:npm/left-pad@1.3.0 (cves=0)
        - pkg:pypi/requests@2.28.1
        ↑ lodash 4.17.20 → 4.17.21 (resolved=1, introduced=0)
        ⟳ pkg:cargo/openssl@0.10.55 (resolved=2, introduced=1)
    

    Change kinds are Added, Removed, VersionBumped, and CvesChanged.

    Diff limits. Diffing is SPDX-only — there is no CycloneDX diff path — and there is no "reachability changed" section (a component whose reachability flipped is not reported as a change).


    Ecosystem coverage

    Nine ecosystems have a defined PURL type mapping:

    EcosystemPURL typeOSV name
    npmnpmnpm
    PyPIpypiPyPI
    crates.iocargocrates.io
    GogolangGo
    MavenmavenMaven
    RubyGemsgemRubyGems
    NuGetnugetNuGet
    PackagistcomposerPackagist
    ConanconanConanCenter

    Parser coverage caveat. All nine ecosystems are PURL-type-mappable, but the SCA scanner's lockfile parsers currently cover PyPI, npm, crates.io, and Go. The other five (Maven, RubyGems, NuGet, Packagist, Conan) are recognized as types but are not yet fed by dedicated lockfile parsers, so components in those ecosystems may not be discovered even though the type mapping exists.


    Known limits

    Gadriel favors correctness over guessing; several fields are deliberately conservative:

    • License passthrough is best-effort, never inferred. When a license is unknown, SPDX emits NOASSERTION for both licenseConcluded and licenseDeclared, and CycloneDX omits the licenses[] array entirely. No SPDX-expression normalization is performed (that is considered an upstream SCA concern). PyPI license enrichment comes from a bundled top-N table only; online lookup is deferred and merely stamps online-pypi-pending.
    • PURL namespace handling. The PURL itself (including scoped npm names like @scope/pkg) is preserved in externalRefs, and the diff tolerates namespace renames as long as the PURL is stable. However, the SPDX SPDXID for a scoped package is lossily sanitized (@ and / become -), so the SPDXID is not a reliable round-trip of the original name — use the PURL for identity.
    • supplier and downloadLocation are NOASSERTION in SPDX by default (download location is redacted as a secret-safety measure).
    • VEX. CycloneDX always serializes a vulnerabilities[] array (empty when there are none) and stamps a gadriel:vex_status metadata property (populated / empty / skipped-osv-bypass / skipped-no-snapshot).

    Note on spec version. Some in-code doc comments and the originating design document still reference CycloneDX 1.4; the shipped serializer emits 1.5. Trust the emitted specVersion field.