Skip to content

SBOM: fix five CycloneDX generator defects - #169

Merged
villelaitila merged 1 commit into
softagram:mainfrom
villelaitila:feature/sbom-generator-defects-2a
Aug 5, 2026
Merged

SBOM: fix five CycloneDX generator defects#169
villelaitila merged 1 commit into
softagram:mainfrom
villelaitila:feature/sbom-generator-defects-2a

Conversation

@villelaitila

@villelaitila villelaitila commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Fixes five defects in the CycloneDX generator, identified in a review of 674 level-3 SBOMs (12 459 component rows, 5 650 distinct purls).

Each fix was pre-registered with a falsifiable row delta before implementation, landed as a separate green step, and verified independently. A main-vs-final diff across 72 stored models classified every behavioural difference in the corpus:

defect rows changed what it fixes
G1 316 Container images emitted as "type": "library" — the value was hardcoded. Now mapped from the purl type, so docker/oci yield container.
G4 51 The internal __slash__ placeholder was decoded in names but never versions, so an npm specifier survived raw into the purl. Now decoded in extract_version, correcting the disclosed version field too.
G9 16 A binary committed into source control was typed nuget by extension inference, asserting a public package that does not exist. Now falls back to generic when the element's name is the stem of a file referencing it.
G3 0 MSBuild $(Version) references reached purls verbatim. The unresolved-version guard covered only Maven's ${...}, and only inside maven_purl; now widened to both dialects behind one shared predicate used for every type.
G5 0 Case-variant NuGet ids emitted as two components, splitting vulnerability matches. Now deduplicated on a case-folded key.
unclassified 0 every difference in the corpus is attributable to an intended fix

Scope — stated honestly

Validated against real models: G1, G4's decode half, and G9, at or above the counts in the review. G1 was additionally applied to a real SBOM artifact generated in 2025-04 by an older generator (2 885 components, pre-generic era): exactly its 7 pkg:docker/ rows retype, and the 15 legacy ??? rows map safely to library.

Fixture-only, unconfirmed against real data: G3, G5, and G4's URL half change zero rows in 72 stored models. This corpus lacks the .NET/MSBuild content the reviewed estate has, so the reported 21 MSBuild rows and 4 case-variant pairs cannot be reproduced here. The fixes are sound against the purl-spec reasoning but are guarded only by fixtures.

Design notes

  • G5 folds {nuget, pypi} only. Their purl type definitions declare identity case-insensitive. npm is deliberately excluded — case_sensitive: true, with pre-2015 mixed-case packages grandfathered, so JSONStream and jsonstream are distinct identities. generic is excluded because its names are opaque. Both exclusions are guarded by control tests.
  • Only the dedup key is folded — the emitted purl keeps the model's casing. Normalising it would change component identity, which is a migration, not a fix.
  • G9's accepted false positive: a vendored third-party binary matching the stem rule is downgraded to generic, losing a real vulnerability match. Accepted because the branch is rarely reached — only 17 components across 72 models reach extension inference, and no .dll has ever won — not because vendored binaries were measured to be rare; this corpus holds essentially no vendored .NET content. Every downgrade cites the referencing file names so the decision is auditable. The one real discriminating case available, aiortc (a genuine public PyPI package inferred from a .whl), is correctly spared.

Not included

Percent-encoding of purl name/version segments — it rewrites ~18 % of purls and changes component identity for any downstream system already storing the unencoded form, so it needs coordinating with ingest rather than riding along here.

Interaction with #168

Independent of #168 and based on main. Both touch maven_purl's guard — #168 extends its condition, this refactors it into a shared predicate — so whichever merges second resolves a one-line conflict.

Verification

211 passed (193 pre-existing + 18 new). flake8 clean apart from two pre-existing E501s. Fixtures were mutation-tested: fold-set widening and narrowing, fullmatchsearch, dropping the $(...) dialect, removing the decode, dropping versionSource, and .lower().casefold() are all caught by controls.

@villelaitila villelaitila changed the title SBOM: fix five CycloneDX generator defects found in a customer review SBOM: fix five CycloneDX generator defects Aug 5, 2026
Fixing defects about how components are typed, versioned and
deduplicated. Each fix below was pre-registered with a falsifiable row delta
and verified against a 72-model corpus; a main-vs-final diff over that corpus
classified every behavioural difference, with zero unclassified.

G1  Container images were emitted as "type": "library", because the value was
    hardcoded. Map the purl type to the CycloneDX component type instead, so
    docker/oci yield "container".

G3  MSBuild property references such as $(Version) reached purls verbatim,
    matching no package and silently misreporting the version. The existing
    unresolved-version guard covered only Maven's ${...} and applied only
    inside maven_purl. Widen it to both dialects behind one shared predicate
    used for every type. Still fullmatch, deliberately: 1.0-$(Suffix) is partly
    known and keeps its version.

G4  The internal __slash__ placeholder was decoded in names but never in
    versions, so an npm specifier survived raw into the purl. Decode it in
    extract_version so the disclosed version field is corrected too. 51 rows.

G5  Case-variant NuGet ids were emitted as two components, splitting any
    vulnerability match across two identities. Deduplicate on a case-folded key
    for the ecosystems whose purl type definitions declare identity
    case-insensitive (nuget, pypi). The key only: the emitted purl keeps the
    model's casing, since normalising it would change component identity.

G9  A binary committed into source control was typed nuget by extension
    inference, asserting a public package that does not exist -- a
    dependency-confusion-shaped false positive. Fall back to generic when the
    element's name is the stem of a file referencing it, citing the referencing
    file names so the decision is auditable. 16 rows.
@villelaitila
villelaitila force-pushed the feature/sbom-generator-defects-2a branch from 095ae3c to a5dc72e Compare August 5, 2026 05:07
@villelaitila
villelaitila merged commit 5bebc50 into softagram:main Aug 5, 2026
1 check passed
@softagram-bot

Copy link
Copy Markdown

Softagram Impact Report for pull/169 (head commit: a5dc72e)

TL;DR Changed code files: 3 | 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

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

[]

📄 Full report

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

villelaitila added a commit to villelaitila/sgraph that referenced this pull request Aug 5, 2026
 fixes

Rebasing this branch over the five-defect commit resolved both overlap
sites textually but broke each side's semantics at one line:

- extract_version: the parent_version fallback landed as a plain 'if'
  after the restructured elif chain, overriding an explicit version
  instead of remaining the last resort. Restore it as the final elif;
  it now also flows through the __slash__ decode, which is the correct
  combined behavior.

- purl_for: the URL-shaped-version branch lost its versionless return
  when the empty-version guard was woven in, so a URL version recorded
  its versionSource property but still spliced '@https://...' into the
  purl. Restore the return.

Both regressions were caught by the existing tests of the respective
sides (test_an_explicit_version_outranks_parent_version,
test_a_url_shaped_version_yields_a_versionless_purl_and_records_its_source).
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