Skip to content

SBOM: publish each element's model path, parent path and repository URL - #174

Merged
villelaitila merged 11 commits into
softagram:mainfrom
villelaitila:feature/sbom-element-path
Aug 11, 2026
Merged

SBOM: publish each element's model path, parent path and repository URL#174
villelaitila merged 11 commits into
softagram:mainfrom
villelaitila:feature/sbom-element-path

Conversation

@villelaitila

Copy link
Copy Markdown
Contributor

The problem

At --level 3 a per-repository SBOM identifies its subject by name only:

{ "bom-ref": "repoa", "name": "repoA",
  "type": "application", "version": "", "purl": "", "externalReferences": [] }

Nothing records that the element lives at /OrgName/GroupA/repoA. Since the bulk export is
the only endpoint usable at scale, a consumer that needs the parent group has to download the
whole model XML — hundreds of megabytes on a large estate — purely to rebuild a tree the SBOM
set already implicitly contains. One consumer today infers the group heuristically from
sourceCodeReferences paths, which is collected model-wide and therefore ambiguous; for a
repository mirrored into two groups the inference is unresolvable.

The path is not expensive to publish. The generator already computes it, and already depends
on it — the serial number is derived from it.

The identity that was missing is the only stable one

Mirrored repositories do not share a bom-ref. The collision suffix gives them distinct refs,
assigned in traversal order:

shared | bom-ref: shared    | serial: urn:uuid:905b7d45...
shared | bom-ref: shared-2  | serial: urn:uuid:71368b41...

Delete GroupA's copy and GroupB's silently becomes shared. So bom-ref distinguishes the two
documents but identifies neither across time, name identifies neither, and serialNumber is
an opaque hash. After this change softagram:elementPath is the only stable, human-legible
per-element identity in the document.

What this adds

Every component that describes a model element — the metadata component of each document,
and the internal components inlined by --transitive — now publishes its location:

{ "bom-ref": "repoa",
  "name": "repoA",
  "group": "/OrgName/GroupA",
  "type": "application",
  "version": "",
  "purl": "",
  "externalReferences": [
    { "url": "https://example.org/org/repoA.git", "type": "vcs" }
  ],
  "properties": [
    { "name": "softagram:elementPath", "value": "/OrgName/GroupA/repoA" }
  ] }
Field Value Why
group the parent element's full path Native CycloneDX field, so the common case needs no custom-property parsing. The full path rather than the bare name keeps two identically named groups under different roots distinguishable.
properties[softagram:elementPath] the element's own full path A property because the CycloneDX 1.7 component schema sets additionalProperties: false. It is also the exact string deterministic_serial() hashes, so a consumer can verify a document's identity offline.
externalReferences[type=vcs] repo_url of the element or its nearest ancestor carrying a non-blank one CycloneDX has a proper field for it and the model already holds it. Removes another reason to fetch the model. Absent when nothing carries one — never a placeholder.

Components describing 3rd-party packages get none of these; their identity is the purl.

Guarantees

  • deterministic_serial(elementPath) == serialNumber
  • group + '/' + name == elementPath below the top level; group is omitted at the top
  • Two mirrors of one repository are told apart by group and elementPath

Deliberately unchanged

  • bom-ref — referenced from dependencies[].dependsOn and from BOM-Link URNs in other
    documents. Changing it would break cross-SBOM resolution for everything already ingested.
  • serialNumber — already derived from the path; must stay stable so re-ingesting a model
    updates projects rather than creating new ones.
  • purl and version — a repository has no package identity and no version. A path is not
    a valid purl; placing one there would make purl-parsing consumers reject or mis-parse the
    component, the same failure class as the pkg:??? placeholder removed in 1.7.1.
  • The UNKNOWN-REPOSITORY_LOCATION fallback in analyze_component_section — fabricating a
    URL for a repository whose remote is unknown is a defect, but removing it changes output for
    existing consumers and is its own decision. That path therefore keeps its own vcs logic and
    gets only the location field; the new code paths never fabricate. The asymmetry is commented
    at the call site.

Verification

  • 256 tests pass (237 before, 19 added). The new tests live in one
    # --- Element location tests --- section.
  • Three of them are characterization tests written before any production code, pinning that
    purl, version, bom-ref and serialNumber do not move.
  • The change is additive: the only removed lines in src/ are one deliberately restructured
    block, and no removed line touches a bom-ref, serialNumber, purl or version value.
  • flake8 on the changed file is unchanged (two pre-existing E501s, untouched); yapf --diff
    hunk count is unchanged.
  • Invariants verified programmatically across both fixtures at levels 1–3 in both plain and
    transitive modes.

A new fixture modelfile_for_sbom_mirrored_tests.xml covers one repository name under two
groups — the case the feature exists to disambiguate, which no existing fixture covered. In
transitive mode it produces a document holding two components named shared, one of which is
its own metadata component, separated only by their published locations.

Consumer impact

Additive — no existing field changes value, so nothing already ingested breaks. Consumers can
drop the model download for tree reconstruction, and the group-inference heuristic can go away
along with the mirrored-repository ambiguity it could not resolve. Transitive-mode consumers
additionally gain the location of every link in the exposure chain, not just its root.

Two caveats, both documented in docs/data-formats.md: the first path segment is the estate
root and is not stable across model generations, so read it from the path rather than
hardcoding it; and group holds a slash-delimited path where the CycloneDX specification
suggests a package-namespace-shaped value, so tools that render group as a package coordinate
will display the path.

Documentation

docs/data-formats.md gains a CycloneDX section describing the output contract, since it is now
parsed by downstream tools. docs/superpowers/ is excluded from the Jekyll build — it holds an
internal design document that belongs in the repository for contributors but must not publish to
the GitHub Pages site.

Publishes each SBOM's model location as a native CycloneDX 'group' (the
parent element's full path) plus a 'softagram:elementPath' property, and
adds the repository URL as a vcs externalReference. Additive only:
bom-ref, serialNumber, purl and version are untouched.
A detached element would have published its bare name as elementPath -
a value that looks like a path but resolves to nothing. Raising at the
call site is the better failure.
Swept in twice by a careless 'git add docs/'. It is pre-existing local
scratch output, unrelated to this change.
Adds a CycloneDX section to docs/data-formats.md: what group and
softagram:elementPath hold, the nearest-ancestor vcs rule, and the
caveats a consumer needs (unstable estate root, group holds a path
rather than a package namespace, inheritance by proximity).

Comments three decisions that a future reader would otherwise
reasonably 'fix': why 3rd-party components stay unlocated, why the
ancestor walk reaches past the repository, and why the legacy path
keeps its own vcs logic instead of adopting _add_vcs_reference.

Excludes docs/superpowers/ from the Jekyll build - internal design
notes must not publish to the GitHub Pages site.
@softagram-bot

Copy link
Copy Markdown

Softagram Impact Report for pull/174 (head commit: 1d855d4)

TL;DR Changed code files: 4 | Directly impacted code files: 1

⭐ 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, 1 impacted, +58/-0 deps)

Change overview

Head 1d855d4caae5 vs base fb65e0d4fe60. 4 code files changed. 1 unchanged files directly depend on the changed files (see Impacted files). Dependencies: 58 added, 0 removed. New external components: 0. Removed external components: 0.

Added dependencies (56, showing 50)

from to type roles signal
sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/_sbom_for_content_element sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/_add_element_location func_ref -→- regular
sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/_sbom_for_content_element sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/_add_vcs_reference func_ref -→- regular
sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/_transitive_components_and_dependencies sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/_add_element_location func_ref -→- regular
sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/_transitive_components_and_dependencies sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/_add_vcs_reference func_ref -→- regular
sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/analyze_component_section sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/_add_element_location func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_a_blank_repo_url_does_not_mask_a_real_one_further_up sgraph/src> sgraph/sgraph.py/SGraph import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_bom_ref_stays_the_slug_not_the_path External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_bom_ref_stays_the_slug_not_the_path External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_element_location_is_level_agnostic sgraph/tests/converters/sbom_cyclonedx_generator_test.py/find_property func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_element_location_is_level_agnostic sgraph/tests/converters/sbom_cyclonedx_generator_test.py/sbom_of func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_element_location_is_level_agnostic External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_element_location_is_level_agnostic External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_element_path_matches_the_serial_number_for_every_sbom sgraph/tests/converters/sbom_cyclonedx_generator_test.py/find_property func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_element_path_matches_the_serial_number_for_every_sbom External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_element_path_matches_the_serial_number_for_every_sbom External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_group_is_absent_at_the_top_level sgraph/tests/converters/sbom_cyclonedx_generator_test.py/find_property func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_group_is_absent_at_the_top_level sgraph/tests/converters/sbom_cyclonedx_generator_test.py/sbom_of func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_group_is_absent_at_the_top_level External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_group_is_absent_at_the_top_level External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_inlined_mirror_is_told_apart_from_its_host_by_its_published_location sgraph/tests/converters/sbom_cyclonedx_generator_test.py/find_property func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_inlined_mirror_is_told_apart_from_its_host_by_its_published_location External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_inlined_mirror_is_told_apart_from_its_host_by_its_published_location External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_legacy_single_sbom_carries_the_element_path sgraph/tests/converters/sbom_cyclonedx_generator_test.py/find_property func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_legacy_single_sbom_carries_the_element_path External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_metadata_component_carries_element_path sgraph/tests/converters/sbom_cyclonedx_generator_test.py/find_property func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_metadata_component_carries_element_path sgraph/tests/converters/sbom_cyclonedx_generator_test.py/sbom_of func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_metadata_component_carries_element_path External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_metadata_component_carries_element_path External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_metadata_component_carries_the_parent_path_as_group External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_metadata_component_carries_the_parent_path_as_group sgraph/tests/converters/sbom_cyclonedx_generator_test.py/sbom_of func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_metadata_component_carries_the_parent_path_as_group External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_mirrored_repositories_are_distinguished_by_their_location sgraph/tests/converters/sbom_cyclonedx_generator_test.py/find_property func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_mirrored_repositories_are_distinguished_by_their_location External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_mirrored_repositories_are_distinguished_by_their_location External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_mirrored_repositories_carry_their_own_distinct_repository_urls External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_mirrored_repositories_carry_their_own_distinct_repository_urls External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_nearest_repo_url_wins_over_a_more_distant_ancestor External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_nearest_repo_url_wins_over_a_more_distant_ancestor External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_no_vcs_reference_when_no_ancestor_has_one External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_no_vcs_reference_when_no_ancestor_has_one sgraph/tests/converters/sbom_cyclonedx_generator_test.py/sbom_of func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_no_vcs_reference_when_no_ancestor_has_one External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_purl_and_version_stay_empty_on_the_metadata_component External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_purl_and_version_stay_empty_on_the_metadata_component External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_selected_element_sbom_also_carries_its_location External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_selected_element_sbom_also_carries_its_location sgraph/tests/converters/sbom_cyclonedx_generator_test.py/find_property func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_selected_element_sbom_also_carries_its_location External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_serial_numbers_stay_derived_from_the_element_path External/Python/modelapi_test/get_model_and_model_api import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_serial_numbers_stay_derived_from_the_element_path External/Python/Usual dependencies import -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_transitive_internal_components_carry_their_location sgraph/tests/converters/sbom_cyclonedx_generator_test.py/find_property func_ref -→- regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_transitive_internal_components_carry_their_location External/Python/modelapi_test/get_model_and_model_api import -→- regular

6 more omitted. Complete data: https://opensource.softagram.com/cdn/impact/d20d45f2-ea4e-4ae4-a9ca-0f350bd8fac5_sgraph_174_impact_change_graph_TObH6sOkJXoEatCBiWrPSbQaF5a5V9.png_change_info.json

Removed dependencies (0)

None.

Impacted files (1)

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 1: sgraph/src/sgraph/graphdataservice.py

Complete data

[]

📄 Full report

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

@villelaitila
villelaitila merged commit 3e5ea4f into softagram:main Aug 11, 2026
1 check passed
@villelaitila
villelaitila deleted the feature/sbom-element-path branch August 11, 2026 17:19
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