Skip to content

fix(cyclonedx): decode evidence occurrence locations - #5114

Open
Eljees wants to merge 7 commits into
anchore:mainfrom
Eljees:agent/cyclonedx-evidence-locations
Open

Eljees wants to merge 7 commits into
anchore:mainfrom
Eljees:agent/cyclonedx-evidence-locations

Conversation

@Eljees

@Eljees Eljees commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Description

CycloneDX 1.5+ represents component file locations in the standard evidence.occurrences[].location field. Syft only decoded its proprietary syft:location properties, so locations from third-party CycloneDX BOMs were dropped during import.

This change:

  • converts non-empty evidence occurrence locations into Syft file locations
  • merges them with existing syft:location properties without changing backward compatibility
  • deduplicates repeated locations through the existing LocationSet
  • adds component-level and end-to-end CycloneDX JSON decoder regression tests

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have added unit tests that cover changed behavior
  • I have tested my code in common scenarios and confirmed there are no regressions
  • I have added comments to my code, particularly in hard-to-understand sections

Issue references

Fixes #3880

Signed-off-by: Eljees <3.14hell@gmail.com>
@Eljees

Eljees commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Ping — this has been open since 28 July with no review yet.

CycloneDX 1.5+ carries component file locations in evidence.occurrences[].location, but Syft only decoded its own syft:location properties, so locations from third-party BOMs were dropped on import. This decodes the standard field as well, merges it with the existing properties without changing backward compatibility, and deduplicates through LocationSet.

Unit tests, lint and static analysis are green on 0296719.

Happy to close it if you would rather fix #3880 differently.

@Eljees

Eljees commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Adding the real-world case this was missing. The tests here are hand-written decoder units; what follows is a document produced by an unmodified third-party generator, which is the situation the change is actually for.

cdxgen 12.8.4 (the OWASP CycloneDX generator), invoked plainly as cdxgen -t js, with no evidence-specific flags, on a one-dependency project whose src/index.js starts with require('lodash'):

"components": [{
  "name": "lodash", "version": "4.17.21", "purl": "pkg:npm/lodash@4.17.21",
  "evidence": {
    "identity": [{ "field": "purl", "confidence": 1, "concludedValue": "package-lock.json" }],
    "occurrences": [{ "location": "src/index.js", "line": 1 }]
  }
}]

Feeding that document to syft convert bom.json -o syft-json:

main (3325493)   lodash 4.17.21   locations=[]
this PR          lodash 4.17.21   locations=['src/index.js']

So the file the generator recorded is dropped today, on a default invocation of a mainstream tool, at the current spec version (the document declares specVersion 1.7). evidence.occurrences is where CycloneDX puts this, and Syft's own encoder writes locations as syft:location properties instead — which is fine for a Syft-to-Syft round trip, and is exactly why documents from other tools lose it on the way in.

Housekeeping, since the branch was cut in August: it merges into current main cleanly. On the merge result, syft/format/internal/cyclonedxutil and .../helpers pass, and syft/format/cyclonedxjson fails exactly one test, TestCycloneDxImageEncoder, which fails identically on a pristine main checkout here because it needs docker to build its fixture. Nothing is attributable to this change.

Happy to add the generated document as a testdata fixture if you would rather the regression be pinned to a real file than to the hand-written snippets.

🤖 Generated with Claude Code

https://claude.ai/code/session_015CsUTRwsaFCwavDJTPwEpW

…dence-locations

* origin/main: (238 commits)
  deps: bump brotli (anchore#5543)
  chore(deps): bump github.com/gpustack/gguf-parser-go (anchore#5541)
  chore(deps): bump github.com/moby/sys/mountinfo from 0.7.2 to 0.7.3 (anchore#5540)
  chore(deps): bump github.com/bmatcuk/doublestar/v4 from 4.10.3 to 4.10.4 (anchore#5537)
  chore(deps): bump github.com/gkampitakis/go-snaps from 0.5.24 to 0.5.25 (anchore#5539)
  chore(deps): bump golang.org/x/net from 0.61.0 to 0.62.0 (anchore#5538)
  chore(deps): bump go.yaml.in/yaml/v3 from 3.0.5 to 3.0.6 (anchore#5536)
  chore(deps): update anchore dependencies (anchore#5513)
  fix(installer): repair OpenSSL fallback in hash_sha256 (anchore#5521)
  feat(executable): capture GNU ELF build IDs (anchore#5426)
  perf: build lower layer squashes only when a scan reads them (anchore#5492)
  fix(terraform): generate NVD-style terraform_provider CPEs for terraform lock packages (anchore#5261)
  fix: keep dot-separated pre-release identifiers in Go ldflags versions (anchore#5422)
  fix: do not generate CPEs for msrc-kb packages (anchore#5510)
  chore(deps): update anchore dependencies (anchore#5479)
  chore(deps): update tool versions (anchore#5509)
  chore(deps): bump github.com/github/go-spdx/v2 from 2.7.1 to 2.8.0 (anchore#5502)
  chore(deps): bump github.com/go-jose/go-jose/v4 from 4.1.5 to 4.1.6 (anchore#5504)
  chore(deps): bump github.com/deitch/magic (anchore#5503)
  chore(deps): bump github.com/gpustack/gguf-parser-go (anchore#5500)
  ...

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
@spiffcs

spiffcs commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

Looking at this one today. Apologies for the delay here

…location

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
@spiffcs

spiffcs commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

@Eljees thanks for this, and apologies for how long it sat without a review.

I picked it up and pushed on top of your branch rather than ask you for another round to keep this moving.

Added

  • Dedupe fix. A path present in both a syft:location property and an evidence occurrence decoded to two locations. This was because the two never compare equal in a LocationSet.
  • Encode. Syft now writes evidence.occurrences for CycloneDX 1.5+, one per distinct path. This should make encode/decode symmetrical.
  • Layout. Evidence logic moved to helpers/evidence.go with its own tests.
  • Tests. Your cases are now rows in Test_decodeComponent. The inline document in the JSON decoder test is replaced by an unmodified cdxgen 12.8.4 document, as a row in TestDecoder_Decode. I also added a round-trip test (no doubled locations), a spec-version test, and a test that decoding memory stays proportional to input, in line with #5404.
  • Merged main and regenerated the CycloneDX golden snapshots.

Behavior changes

  • Every CycloneDX 1.5+ document syft emits gains an evidence element and gets larger. syft:location properties are kept, so each path is written twice.
  • Third-party BOMs with evidence.occurrences now decode with locations. For components without a bom-ref, that changes the package ID. Components with a bom-ref keep it as their ID. This is also a section I'd like another maintainer to look at so we can discuss if we should be more discriminating of what content can be in the evidence.occurences field if they have the potential to not 1:1 to locations.
  • Syft-generated BOMs decode to the same locations as before.

Open question

Running cdxgen 12.8.4 and syft on the same one-dependency project:

  • cdxgen: occurrences: [{location: "src/index.js", line: 1}] (where lodash is imported); the manifest is in evidence.identity[].methods[] (manifest-analysis, package-lock.json).
  • syft: occurrences: [{location: "/package-lock.json"}] (where the package was discovered).

Syft locations mean "what led to discovering this package", which is closer to cdxgen's identity than its occurrences. This PR maps locations to occurrences, which is what #3880 and Dependency-Track use. We want to settle whether that is the right long-term home before merging.

Before merging

  • Decide the occurrences vs identity.methods question above
  • CI green on the new commits, including CycloneDX schema validation of the new output
  • Check some downstream consequences in grype and how it uses decoded locations
  • If it merges as is: make a note for the output growth and the package ID change

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
Evidence occurrences are the home for package locations in CycloneDX.
From spec 1.7 (the default) the syft:location properties are no longer
written; documents pinned to 1.5 or 1.6 keep the properties alongside the
occurrences, and documents older than 1.5 keep only the properties.

Decoded occurrence paths are rooted, as file component names are, so a
package location and the file component it names agree.

Changelog:
- CycloneDX 1.7 output (the default, or pinned to 1.7) no longer carries
  syft:location properties. Package locations are in
  evidence.occurrences, one per distinct path; the layer ID and the path
  within an archive are not written.
- CycloneDX output pinned to 1.5 or 1.6 grows: each location path is
  written both as a syft:location property and as an evidence occurrence.
- Decoding a CycloneDX document whose components have evidence
  occurrences now yields package locations. For components without a
  bom-ref this changes the derived package ID.

Tests: occurrences of files within archives, rooting, package IDs with
and without a bom-ref, and a format-level round trip per spec version.

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
An evidence occurrence only has a path, so with the syft:location
properties gone from spec 1.7 a file found in several layers decoded as
one location with no layer. Write an occurrence for each distinct set of
coordinates and state the layer and the full path through any archives
in its additionalContext ("layerID: <id>; fullPath: <path>"), which the
decoder reads back.

Documents older than 1.7 keep the syft:location properties, so their
occurrences stay one per distinct path with no additional context.

Changelog:
- CycloneDX 1.7 output (the default, or pinned to 1.7) no longer carries
  syft:location properties. Package locations are in
  evidence.occurrences, one per location, with the layer ID and the path
  within an archive in additionalContext. Other syft properties are
  unchanged.
- CycloneDX output pinned to 1.5 or 1.6 grows: each location path is
  written both as a syft:location property and as an evidence occurrence.
- Decoding a CycloneDX document whose components have evidence
  occurrences now yields package locations. For components without a
  bom-ref this changes the derived package ID.

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
cyclonedx-go writes the additionalContext of an evidence occurrence as an
XML attribute, where the schema defines an element, so a CycloneDX 1.7
XML document that stated the layer there failed schema validation. An XML
document of any spec version keeps the syft:location properties and
writes one occurrence per distinct path, as documents older than 1.7 do.

Changelog:
- CycloneDX 1.7 JSON output (the default, or pinned to 1.7) no longer
  carries syft:location properties. Package locations are in
  evidence.occurrences, one per location, with the layer ID and the path
  within an archive in additionalContext.
- CycloneDX XML output, and JSON output pinned to 1.5 or 1.6, grows: each
  location path is written both as a syft:location property and as an
  evidence occurrence.

Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>

This branch has not been deployed

No deployments
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.

Support CycloneDX evidence for file locations

2 participants