Skip to content

SBOM: say what the document does not cover, via --coverage - #186

Merged
villelaitila merged 4 commits into
softagram:mainfrom
villelaitila:feature/sbom-coverage-compositions
Aug 24, 2026
Merged

SBOM: say what the document does not cover, via --coverage#186
villelaitila merged 4 commits into
softagram:mainfrom
villelaitila:feature/sbom-coverage-compositions

Conversation

@villelaitila

Copy link
Copy Markdown
Contributor

The coverage ledger added in 1.13.0 reached no BOM — attach_coverage_summary had no caller. This makes it reachable and adds the completeness claim in CycloneDX's own slot.

--coverage (single-SBOM mode only) adds:

  • four metadata.properties counting what was and was not identified under External
  • one compositions entry: {"aggregate": "incomplete", "assemblies": ["/Org"]}

complete is never emitted. It asserts that no further components are known to exist; the ledger proves only that every element the walk saw was classified. unknown is the spec's word for a best-effort of inconclusive completeness, and is the strongest honest claim here.

Single-SBOM only, enforced. The report is model-wide, so on a --level document it would claim that document's assembly is incomplete because another subtree's is. Rejected at the CLI.

Opt-in. Verified identical default output vs main across 33 (model, mode) pairs / 11 models, normalising only the UUID serial, timestamp and BOM-links. Every generator hunk is inside __main__.

Suite 485 → 502. Emitted compositions validated against bom-1.7.schema.json.

Also fixes a doc line 1.13.0 made stale (purl is now omitted, not empty).

…s own slot

The coverage ledger answers half of what a BOM is for -- what the document does
not cover -- and until now it could only be said in Softagram-specific metadata
properties, which a consumer has no reason to read. CycloneDX defines
compositions[].aggregate for exactly this claim, so the claim now has a home in
a vocabulary consumers already know.

The four counts stay where they are. A composition holds aggregate, assemblies,
dependencies and vulnerabilities and nothing else, with additionalProperties
false, so a ten-category taxonomy with counts and samples genuinely has no home
there -- the module's existing comment was right about that and is left standing.
What it was silent about is completeness, which is the part that does fit.

'complete' is unreachable by construction. The enum defines it as "no further
relationships ... are KNOWN to exist"; the ledger proves only that every element
the walk SAW was classified, never that the analyzer saw everything, and between
those two lies every dependency an unrun analyzer would have found. Claiming
completeness makes a consumer stop looking, which is the wrong direction to be
wrong in for a security artifact, so the strongest honest claim is the enum's own
'unknown'. A test walks a grid of ledger states and asserts none reaches it.

An empty walk is 'not_specified' rather than 'unknown': a model with no External
subtree cannot distinguish an estate that depends on nothing from an analyzer
that never ran, and 'unknown' would assert the best effort 'not_specified'
declines to assert.

Nothing calls this yet, so no emitted document differs by a byte -- asserted by
the existing call-graph test over all three public entry points, which still
passes unchanged. Values enumerated from bom-1.7.schema.json, the specVersion
this generator declares, rather than from recollection.

Suite 485 -> 492, seven added: one guards the hand-built ledger against drifting
from the real report's shape, so the grid test cannot pass while proving nothing.
…ject

coverage_compositions builds the claim; this puts it on a document. The subject
is the metadata component, because the claim is about the assembly of THAT
document rather than of the model -- a per-element SBOM and the single-document
one describe different assemblies and must not both claim to describe the model.

A document whose metadata component carries no bom-ref names no subject, and the
composition is then emitted with no assemblies rather than with an empty array.
An empty array reads as "these zero components are incomplete"; an absent key
reads as "the subject was not named", which is what is true.

Default-off stays in the call graph rather than moving to a parameter default,
matching attach_coverage_summary. A completeness claim nobody asked for is still
a claim, so the guard is asserted over all three public entry points -- the same
test shape that already guards the summary, because the failure it prevents is
the same: a call site added by reflex changing every emitted document.

Suite 492 -> 497. Pre-registered as 495 and reported here as measured: the
default-off guard is parametrized over three entry points, so three test
functions collect as five tests. Counting functions instead of collected tests
is the same error this project pinned a rule against; the number stands as
collected rather than being reconciled after the fact.
…document

The claim and its attachment existed; nothing could ask for them. A capability
reachable only by importing two modules and calling three functions in the right
order is half-shipped, and the half that is missing is the half a user sees.

Single-document mode only, enforced rather than documented. The coverage report
is model-wide, so attaching it to a per-element SBOM would claim that THIS
document's third-party assembly is incomplete because some other subtree's is --
a statement the document carries no evidence for. --coverage with --level or
--element-path is a CLI error, matching how every other unsupported combination
here is handled. Per-document coverage is a real thing to build; asserting it
before it exists is not.

The import of external_identification is function-local and that is the design,
not a workaround: that module imports this one, the direction is deliberate --
the generator emits, the report reads what was emitted -- and a module-level
import would make it a cycle. A CLI is the composition above both and the one
place entitled to know about each. No library signature changed, so no
programmatic caller sees anything new.

Default-off, asserted: a document produced without the flag is byte-for-byte
what 1.13.0 produced. Every changed hunk in the generator is inside
__main__, which is checkable from the diff rather than from this claim.

Both reachable outcomes are exercised on fixtures measured to produce them --
'unknown' where every external is identified, 'incomplete' where one is not.
The first fixture chosen was in the quiet state, where the flag could have been
wired to a constant and still passed; the second is what makes the test able to
fail. The two rejection tests assert the exact parser message, because with the
flag absent they passed on argparse's "unrecognized arguments: --coverage" --
a pass for the wrong reason that would have survived the flag doing nothing.

Emitted compositions were validated against definitions.compositions in
bom-1.7.schema.json out of band; the committed test keeps an offline proxy
(the key set the schema defines, since it sets additionalProperties false).

Suite 497 -> 502, four collected added and one more than the 501 registered:
parametrizing the happy path over two fixtures added the fifth.
The flag is documented where the other SBOM options are, with the three
reachable aggregate values and the reason the fourth is unreachable: 'complete'
asserts that no further components are known to exist, and an analyzer that was
not run leaves no evidence that it was missing.

Also corrects a line this project's own 1.13.0 invalidated. The page still said
an element publishing several packages "keeps the element name and an empty
purl"; A6 removed that key rather than emptying it, and a test now asserts no
fixture carries an empty purl in any mode. The old behaviour is named with the
version that changed it, because a reader hitting an older document needs to
know which of the two they are looking at.
@softagram-bot

Copy link
Copy Markdown

Softagram Impact Report for pull/186 (head commit: 86b4286)

TL;DR Arch. Impact: -2 | Changed code files: 4 | Directly impacted code files: 2

⭐ Change Overview

Showing the changed files, dependency changes and the impact - click for full size
(Open in Softagram Desktop for full details)

⭐ Details of Dependency Changes (diagram)

details of dependency changes - click for full size
(Open in Softagram Desktop for full details)

🤖 AGENTS - machine-readable impact data (4 files changed, 2 impacted, +25/-0 deps)

Change overview

Head 86b428612f48 vs base b7dd46cb2253. 4 code files changed. 2 unchanged files directly depend on the changed files (see Impacted files). Dependencies: 25 added, 0 removed. New external components: 0. Removed external components: 0.

Added dependencies (25)

from to type roles signal
sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py sgraph/src> sgraph/converters/external_identification.py/external_coverage_report import prod→prod high upstream impact, upstream 13.0
sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py sgraph/src> sgraph/converters/external_identification.py/attach_coverage_compositions import prod→prod high upstream impact, upstream 13.0
sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py sgraph/src> sgraph/converters/external_identification.py/attach_coverage_summary import prod→prod high upstream impact, upstream 13.0
sgraph/tests/converters/sbom_cyclonedx_generator_test.py sgraph/src> sgraph/converters/external_identification.py/SUMMARY_PROPERTIES import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_a_composition_carries_only_keys_cyclonedx_defines sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_a_composition_with_no_subject_omits_assemblies sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_a_model_with_no_externals_makes_no_completeness_claim sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_an_estate_whose_externals_all_identified_is_unknown_not_complete sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_attaching_compositions_names_the_documents_own_subject sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_attaching_compositions_to_a_subjectless_document_omits_assemblies sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_no_generator_path_attaches_compositions sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_the_synthetic_ledger_matches_a_real_reports_shape sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_unidentified_externals_make_the_assembly_incomplete sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/test_external_identification.py/test_no_generator_path_attaches_compositions External/Python/generate ref test→external external dependency
sgraph/tests/converters/test_external_identification.py/ledger_state sgraph/tests/converters/test_external_identification.py/identification func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_a_composition_carries_only_keys_cyclonedx_defines sgraph/tests/converters/test_external_identification.py/external func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_a_composition_with_no_subject_omits_assemblies sgraph/tests/converters/test_external_identification.py/external func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_an_estate_whose_externals_all_identified_is_unknown_not_complete sgraph/tests/converters/test_external_identification.py/external func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_attaching_compositions_names_the_documents_own_subject sgraph/tests/converters/test_external_identification.py/external func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_attaching_compositions_to_a_subjectless_document_omits_assemblies sgraph/tests/converters/test_external_identification.py/external func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_no_generator_path_attaches_compositions sgraph/tests/converters/test_external_identification.py/external func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_no_ledger_state_can_claim_the_assembly_is_complete sgraph/tests/converters/test_external_identification.py/ledger_state func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_the_synthetic_ledger_matches_a_real_reports_shape sgraph/tests/converters/test_external_identification.py/external func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_the_synthetic_ledger_matches_a_real_reports_shape sgraph/tests/converters/test_external_identification.py/ledger_state func_ref test→test regular
sgraph/tests/converters/test_external_identification.py/test_unidentified_externals_make_the_assembly_incomplete sgraph/tests/converters/test_external_identification.py/external func_ref test→test regular

Removed dependencies (0)

None.

Impacted files (2)

Unchanged files that directly depend on files changed in this PR - check them for behavioral impact. Grouped by changed file:

changed file directly impacted dependents
sgraph/src/sgraph/converters/sbom_cyclonedx_generator.py 2: sgraph/src/sgraph/graphdataservice.py, sgraph/tests/converters/test_external_root_semantics.py

Complete data

[]

📄 Full report

Impact Report explained. Give feedback on this report to support@softagram.com

@villelaitila
villelaitila merged commit 340c70f into softagram:main Aug 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants