Skip to content

SBOM: publish an unrecognised licence as a name, not as a fake SPDX id - #196

Merged
villelaitila merged 1 commit into
softagram:mainfrom
villelaitila:fix/sbom-non-spdx-license
Aug 30, 2026
Merged

SBOM: publish an unrecognised licence as a name, not as a fake SPDX id#196
villelaitila merged 1 commit into
softagram:mainfrom
villelaitila:fix/sbom-non-spdx-license

Conversation

@villelaitila

Copy link
Copy Markdown
Contributor

license.id is constrained to the SPDX identifier enum — CycloneDX $refs the SPDX schema
there. resolve_license_spdx_id put the literal string 'UNKNOWN LICENSE' in it for every
licence except exactly MIT (the mapping table it consulted first has always been {}), and
bom_licenses paired that with the literal url 'UNKNOWN'.

That does not degrade one field. It makes the whole document fail validation, at every spec
version this converter can declare, so a consumer that validates on ingest rejects the entire
BOM — one ordinarily-licensed package costs a repository its whole SBOM. The failure then
surfaces as a rejected upload rather than as a licence problem, which is the hardest kind to
trace back.

It also discarded what the model knew: OTHER, Apache-2.0 and AGPL;GPL all collapsed to the
same placeholder, unrecoverably.

Measured

tests/converters/modelfile_for_sbom_tests.xml carries license="OTHER", so the committed
fixture already produces an invalid document today:

BEFORE @ 1.4: 1 error   components/2/licenses/0/license/id
              'UNKNOWN LICENSE' is not one of ['0BSD', '3D-Slicer-1.0', 'AAL', ...]
BEFORE @ 1.5: 1 error
BEFORE @ 1.6: 1 error
AFTER  @ 1.4/1.5/1.6: 0 errors      licences: [{'license': {'name': 'OTHER'}}]

The fix

name is the slot the specification designates for a licence that cannot be expressed as an
identifier, and it is free text, so the real value survives. Everything not in
SPDX_LICENSE_URLS goes there — including values that look like valid SPDX identifiers,
because "looks valid" is precisely the assumption that caused this. Widening that table is
evidence work, one identifier at a time, and until an entry exists the name path reports the
licence accurately anyway.

A url is emitted only alongside an identifier we hold one for. 'UNKNOWN', 'TODO' and ''
are not IRIs, and an invented url is worse than an absent one because a consumer cannot tell it
from a real one — the same reasoning _add_vcs_reference already applies to a missing
repo_url.

An attribute present but empty now reads as absent, instead of producing a licence entry
invented out of an empty string.

Blast radius: none today, and one mkdir away

No analyzer stamps a licence on an /External/ element unless the .licenses cache directory
exists — dependencyanalysiswrapper.py gates the clearlydefined.io lookups on
os.path.exists(licenses_cache), and its comment invites creating it: "can be created to get
this functionality activated, it needs to be just an empty directory."
Not present on any
instance checked.

Verified that internal elements cannot reach this code at all: bom_licenses is called only from
elem_as_bom_data (the External-component path); _internal_element_component emits no
licenses key. So the ~1,850 licence attributes that real models carry on internal elements are
not affected either way.

Corpus impact: none. 20 real models, 892 documents, generated on main and on this branch:
identical, 0 differing. The fixture is the only place the defect is reachable today — which is
why it has gone unnoticed, not evidence that it is harmless.

Tests

556 → 566 passed (rebased on 1.17.0). Five new tests; with the implementation reverted in a
scratch copy, 4 of the 5 fail and the MIT case passes throughout as the control — so the
suite distinguishes the fix rather than merely accompanying it.

Nothing else in the suite asserted the licenses field except one MIT case, which is
unchanged and still passes.

CycloneDX $refs the SPDX identifier list from license.id, so only a checked
identifier may go there. resolve_license_spdx_id put the literal string
'UNKNOWN LICENSE' there for every licence except MIT -- the mapping table it
consulted first has always been empty -- and bom_licenses paired it with the
literal url 'UNKNOWN'.

That does not degrade one field. It makes the whole document fail validation, at
every spec version this converter can declare, so a consumer that validates on
ingest rejects the entire BOM: one ordinarily-licensed package costs a repository
its whole SBOM. The failure then surfaces as a rejected upload rather than as a
licence problem, which is the hardest kind to trace back. Measured on
modelfile_for_sbom_tests.xml, whose single licence attribute is "OTHER":
1 schema error at 1.4, 1.5 and 1.6 before, 0 after.

It also discarded what the model knew. 'OTHER', 'Apache-2.0' and 'AGPL;GPL' all
became the same placeholder, which tells a reader nothing and cannot be recovered.

name is the slot the specification designates for a licence that cannot be
expressed as an identifier, and it is free text, so the real value survives.
Every value not in SPDX_LICENSE_URLS goes there -- including ones that look like
valid SPDX identifiers, because looking valid is what the previous code assumed
and is exactly the assumption that has to stop. Widening that table is evidence
work, one identifier at a time, and until an entry exists the name path reports
the licence accurately anyway.

A url is emitted only alongside an identifier we hold one for. 'UNKNOWN', 'TODO'
and '' are not IRIs, and an invented url is worse than an absent one because a
consumer cannot tell it from a real one -- the same reason _add_vcs_reference
omits a missing repo_url rather than emitting a blank.

An attribute present but empty now reads as absent instead of producing a licence
entry invented out of an empty string.

Dormant today: no analyzer stamps a licence on an External element unless the
.licenses cache directory exists, and it does not on any instance checked. But
creating that empty directory is all it takes to activate the clearlydefined.io
lookups that do, at which point every non-MIT package would emit an invalid
document.
@softagram-bot

Copy link
Copy Markdown

Softagram Impact Report for pull/196 (head commit: 02d454e)

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

⭐ 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 (2 files changed, 4 impacted, +8/-1 deps)

Change overview

Head 02d454ea42c8 vs base f29f864b2456. 2 code files changed. 4 unchanged files directly depend on the changed files (see Impacted files). Dependencies: 8 added, 1 removed. New external components: 0. Removed external components: 0.

Added dependencies (8)

from to type roles signal
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/_licence_of sgraph/src> sgraph/sgraph.py/SGraph import test→prod expands test coverage
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/_licence_of sgraph/tests/converters/sbom_cyclonedx_generator_test.py/referenced_external func_ref test→test regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/_licence_of sgraph/tests/converters/sbom_cyclonedx_generator_test.py/admission_components func_ref test→test regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_a_blank_licence_attribute_emits_no_licences sgraph/tests/converters/sbom_cyclonedx_generator_test.py/_licence_of func_ref test→test regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_a_known_spdx_identifier_still_publishes_an_id_and_its_url sgraph/tests/converters/sbom_cyclonedx_generator_test.py/_licence_of func_ref test→test regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_a_licence_expression_is_published_as_a_name_rather_than_discarded sgraph/tests/converters/sbom_cyclonedx_generator_test.py/_licence_of func_ref test→test regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_a_licence_that_is_not_a_known_spdx_identifier_is_published_as_a_name sgraph/tests/converters/sbom_cyclonedx_generator_test.py/_licence_of func_ref test→test regular
sgraph/tests/converters/sbom_cyclonedx_generator_test.py/test_no_licence_is_published_with_an_invented_url sgraph/tests/converters/sbom_cyclonedx_generator_test.py/_licence_of func_ref test→test regular

Removed dependencies (1)

from to type roles signal
sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/bom_licenses sgraph/src> sgraph/converters/sbom_cyclonedx_generator.py/resolve_license_spdx_id func_ref prod→prod regular

Impacted files (4)

Unchanged files that directly depend on files changed in this PR - check them for behavioral impact. Grouped by changed file; dependent paths starting with ./ are relative to the changed file's directory:

changed file directly impacted dependents
sgraph/src/sgraph/converters/sbom_cyclonedx_generator.py 4: ./external_identification.py, sgraph/src/sgraph/graphdataservice.py, sgraph/tests/converters/test_external_identification.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 ee95d67 into softagram:main Aug 30, 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