skip to content

SPDX Versus CycloneDX

SPDX grew out of license lineage and CycloneDX out of security use, and it shows in relationships, dependency edges and external references. Choosing wrong makes conversion lossy later.

on this pageshow

questions

4

How do SPDX relationships and a CycloneDX dependencies array model the same component graph?

level: middleimportance: must knowfreq 62%

answer

  1. one format names the edge
  2. the other nests, then lists edges
  3. SPDXID and bom-ref are document-local
  4. contains and depends-on flatten into one
  5. absent from the array means unknown

basics

~20 s

SPDX records typed edges in a relationships array — CONTAINS, DEPENDS_ON, DESCRIBES and many more — between SPDXIDs. CycloneDX records one kind of edge, ref to dependsOn, between bom-ref values, and expresses containment by nesting components instead of by naming the edge.

solid answer

~40 s

SPDX treats the graph as first-class: a `relationships` array of entries, each naming a source element, a relationship type and a target element, with a vocabulary covering containment, dependency, generation, variance and more. CycloneDX splits the job: assembly is expressed structurally by nesting components inside components, and dependency is expressed in a flat `dependencies` array of `ref` to `dependsOn` edges keyed by `bom-ref`. So a Helm chart that bundles a vendor's Kafka operator, which in turn depends on a TLS library, is two differently typed SPDX edges — CONTAINS then DEPENDS_ON — but converters usually flatten both into `dependsOn`, because that is the only edge CycloneDX's dependency graph has. Both identifier schemes are document-local, not global, so the graph is only meaningful within one BOM.

code

json · 14 lines
json
{ "relationships": [
  { "spdxElementId": "SPDXRef-Chart",
    "relationshipType": "CONTAINS",
    "relatedSpdxElement": "SPDXRef-KafkaOperator" },
  { "spdxElementId": "SPDXRef-KafkaOperator",
    "relationshipType": "DEPENDS_ON",
    "relatedSpdxElement": "SPDXRef-Tls" }
] }
...
{ "dependencies": [
  { "ref": "chart-ref",          "dependsOn": ["kafka-operator-ref"] },
  { "ref": "kafka-operator-ref", "dependsOn": ["tls-ref"] },
  { "ref": "tls-ref",            "dependsOn": [] }
] }

go deeper

for a junior

Know that both formats list components and how those components relate, and that SPDX writes the relationship type out by name while CycloneDX mostly records a plain depends-on edge.

for a middle

Explain the mechanics: an SPDX relationships array of source, type and target versus CycloneDX component nesting plus a flat ref and dependsOn array, and that both identifier schemes are local to one document.

for a senior

Demonstrate that you know where a round trip degrades the graph, and that you check identifier stability before promising anyone that BOM diffs are meaningful evidence of what changed.

for a principal

Decide what your organisation keys its component records on. Building an estate-wide inventory on document-local identifiers is a rework project waiting to happen; the durable key is component coordinates, not the BOM's internal ids.

## Two designs for the same picture Both formats answer *what is in this artifact and how are the pieces related*, but they put the answer in different places. **SPDX** makes the edge an object. Every element in the document has a document-local identifier, conventionally written with an `SPDXRef-` prefix, and a separate `relationships` array holds entries of the shape *source element, relationship type, target element*. The type vocabulary is large — containment, dependency in several flavours, generation, description, variance, static and dynamic linking, and more. The document also names its own subject with a DESCRIBES relationship, so the root of the graph is stated rather than inferred. **CycloneDX** splits the same information across two mechanisms. Structural containment is expressed by nesting: a component may carry its own `components` array, which is how an assembly, an installer or a chart says *these things are inside me*. Dependency is expressed separately in a flat `dependencies` array, where each entry has a `ref` naming one component's `bom-ref` and a `dependsOn` list of other `bom-ref` values. The subject of the whole document is `metadata.component`. ## Identifier scope Both `SPDXID` and `bom-ref` are **document-local**. They are unique inside one BOM and carry no meaning outside it. SPDX gives the document itself a namespace URI, which is what makes an element globally addressable when combined with its local id; CycloneDX gives the document a `serialNumber` plus a revision number for the same document-identity purpose. Assuming either local id is a stable global handle for a component is a common and consequential mistake, because it invites downstream systems to key their own records on a value the producer may regenerate at will. That leads to a subtle operational property: identifier **stability across releases** is a producer discipline, not a format guarantee. If one team's `bom-ref` values are derived from component coordinates and stay stable release to release, while another team's element ids are regenerated on every build, then diffing two consecutive BOMs from the second team is pure noise. Nobody can point at the diff and say which components actually changed, which is precisely the evidence you want when the question is whether a change was legitimate — an unwanted change introduced by someone with ordinary commit access hides best inside a diff that is noisy every time. ## What a round trip loses The honest answer to *is conversion lossless* is no, and the relationship layer is where the loss starts. - **Type collapse.** SPDX distinguishes *contains* from *depends on*; a naive conversion writes both as `dependsOn` edges. The resulting graph is still connected and still traversable, but the statement *this operator is bundled inside the chart* has become *this chart depends on the operator*, which is a weaker and different claim. - **Direction and inverse types.** SPDX offers both a relationship and its inverse. A converter that normalises one direction silently rewrites who the subject of the claim is. - **Nesting versus edges.** Going the other way, CycloneDX nesting must be re-expressed as explicit SPDX relationships; a converter that flattens the tree into a component list first loses the containment structure before it ever gets to the relationships array. - **Scope hints.** SPDX has dependency relationship types that distinguish development, test, optional and runtime dependencies; CycloneDX carries a per-component `scope`. These do not map cleanly onto each other, and dropping them turns a build-only tool into an apparent runtime component. ## Absent is not empty One CycloneDX detail is worth memorising because it is asked directly: a component that appears in `dependencies` with an empty `dependsOn` is asserted to have no dependencies, whereas a component that does not appear in `dependencies` at all has *unknown* dependencies. Treating the second as the first turns *we did not analyse this* into *there is nothing under here*, which is the same class of mistake as reading an unasserted SPDX field as a clean one. ## What the graph does not tell you Neither format's edge means *this code runs*. A dependency edge records a declared or resolved relationship. Whether the vulnerable code inside a dependency is actually called is reachability, and whether an attacker can drive execution to it is exploitability; an SBOM graph is input to those questions, not an answer to them. A candidate who says *the SBOM shows we depend on it, therefore we are affected* has skipped both. ## How to answer in an interview State the structural difference in one breath — typed edges in an array versus nesting plus a single untyped dependency edge — then immediately give one consequence: conversion flattens the type, and both identifier schemes are document-local. That combination shows you have read both formats rather than one, which is the actual thing being tested.

  • In CycloneDX, what is the difference between a component with an empty dependsOn and a component absent from the dependencies array?
    An empty `dependsOn` asserts that the component has no dependencies. Absence from the array means its dependencies are unknown, not that it has none. Conflating them reports *we never looked* as *there is nothing beneath this*, which is how whole subtrees quietly vanish from an inventory.
  • A studio's CycloneDX bom-ref values are stable across releases while its SPDX element ids are regenerated every build. Why does that matter?
    Because a release-to-release diff of the regenerated document is noise: every element looks new, so nobody can prove which components actually changed. Identifier stability is what makes a BOM auditable over time. Neither format guarantees it — it is a producer discipline, since both id schemes are only required to be unique within one document.
  • Does a dependency edge in either format tell you the dependency is actually used at runtime?
    No. It records a declared or resolved relationship. SPDX has relationship types that mark development, test, optional and runtime dependencies, and CycloneDX has a per-component scope, but those are producer claims about intent. Whether vulnerable code is actually called is reachability, and that needs analysis the BOM does not contain.
  • How does each format say what the document is actually about?
    SPDX names its subject with a DESCRIBES relationship from the document to one or more elements, so the root is an ordinary edge in the same array. CycloneDX names it in `metadata.component`. Converters that drop the subject produce a bag of components with no stated root, which is surprisingly common.

saying these in an interview costs you the question

  • Says the two formats express identical graphs so conversion is lossless
  • Treats SPDX CONTAINS and DEPENDS_ON as synonyms
  • Assumes bom-ref or an SPDX element id is globally unique
  • Reads a component missing from dependencies as having none
  • Claims a dependency edge proves the vulnerable code runs

context

open as a page

In an SPDX SBOM, what is the difference between NOASSERTION and NONE in a license field?

level: juniorimportance: should knowfreq 45%

basics

~20 s

NONE is a positive claim that nothing is there: the producer looked and found no license. NOASSERTION means the producer makes no claim at all, whether undetermined or withheld. Neither is the same as an empty field.

open as a page

What is lost when you convert a CycloneDX BOM with an inline vulnerabilities array into SPDX?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The vulnerability records themselves. SPDX 2.x has no object to hold a vulnerability record, so a converter either drops the array or degrades it to an external link. Pedigree, composition-completeness and service entries have no clean home either.

open as a page

A supplier's SPDX BOM declares a compound license expression; your ingestion stores one license name — what breaks?

level: middleimportance: nice to knowfreq 33%

basics

~20 s

An SPDX license expression is a grammar, not a label: AND means all apply, OR means a choice, WITH attaches an exception. Storing one name picks a branch and drops the exception, so your record no longer matches the supplier's.

open as a page