Safeguard
Supply Chain Security•

Data BOMs: SPDX 3 Dataset Profile and CycloneDX Data, Field by Field

No standard defines a DataBOM. SPDX 3 has a Dataset profile and CycloneDX has data components. Here is every field, a mapping and a validated example.

Safeguard Security Team
41 min read

A "data BOM" is not a document type that either major standard defines. What exists is the SPDX 3 Dataset profile, which adds a DatasetPackage class with 13 dataset specific fields, and CycloneDX's data component type with a data array, available since CycloneDX 1.5. Either one can describe a training dataset with hashes, licence, origin and handling rules.

This post goes field by field through both, maps one onto the other, and builds a working example for a fine tuning dataset that validates against SPDX 3.0.1 and CycloneDX 1.7. If you want the shorter version of which facts to record for a dataset, read what to record for a dataset in an AI bill of materials first. This post is the format level companion: which property holds each fact, which ones are required, and what you lose when you move between the two.

Key takeaways

  • Neither SPDX nor CycloneDX defines "DataBOM" or "data BOM" as a named document type. SPDX 3 has a Dataset profile; CycloneDX has a data component type and places datasets under its ML-BOM capability.
  • SPDX 3.0.1 DatasetPackage is a subclass of software_Package and adds 13 properties: datasetType, dataCollectionProcess, dataPreprocessing, anonymizationMethodUsed, hasSensitivePersonalInformation, confidentialityLevel, datasetAvailability, datasetSize, datasetNoise, datasetUpdateMechanism, intendedUse, knownBias and sensor.
  • On the 3.0.1 model page, a DatasetPackage must carry builtTime, releaseTime, originatedBy, downloadLocation, primaryPurpose and at least one datasetType, and profile conformance requires exactly one hasDeclaredLicense and one hasConcludedLicense relationship.
  • CycloneDX records a dataset as a component of type data, with a data array of componentData objects. Each has a type (dataset, configuration, source-code, definition or other), contents, classification, sensitiveData, description, governance and graphics. Only type is required.
  • CycloneDX has no fields for collection process, preprocessing, bias, noise or update mechanism, so converting SPDX to CycloneDX loses structure.
  • A model's CycloneDX modelCard.modelParameters.datasets can either embed a dataset or point to a data component by bom-ref. In SPDX 3, a trainedOn or testedOn relationship links an ai_AIPackage to a dataset_DatasetPackage.
  • Schema validation is not the whole check: in our test, an SPDX 3 validator accepted a DatasetPackage with the required builtTime removed.

Version timeline: where dataset fields came from

Dates below come from the GitHub release pages of the two specification repositories and from the publications themselves. GitHub release dates record when a release was published on GitHub, which can lag ratification.

DateRelease or documentWhat it means for dataset records
12 Jan 2022CycloneDX 1.4No data component type and no model card. Datasets could only be squeezed into file components.
3 Nov 2022SPDX 2.3No dataset class. A dataset could only be an ordinary package or file.
26 Jun 2023CycloneDX 1.5Adds the data and machine-learning-model component types, the componentData object and modelCard.
9 Apr 2024CycloneDX 1.6Keeps the data model; the schema's componentData structure carries forward.
15 Apr 2024SPDX 3.0Introduces profiles, including AI and Dataset, with the DatasetPackage class.
13 Jun 2024EU AI Act, Regulation (EU) 2024/1689 (published in the Official Journal 12 Jul 2024)Article 10 sets data governance duties for training, validation and testing data of high risk systems.
Oct 2024Linux Foundation paper "Implementing AI Bill of Materials (AI BOM) with SPDX 3.0"Explains why AI and Dataset were split into separate profiles and lists the fields chosen.
7 Nov 2024CycloneDX 1.6.1Patch release.
17 Dec 2024SPDX 3.0.1Current SPDX 3 release used in this post.
21 Oct 2025CycloneDX 1.7Current CycloneDX minor version used in this post. componentData is unchanged in shape.
24 Jan 2026SPDX 3.1 release candidate 1 tagged3.1 is still published under a development path on the spec site; do not target it for exchange yet.
2 Jun 2026 and 17 Sep 2026CycloneDX 1.7.1 and 1.7.2Patch releases of the 1.7 line.

We checked the CycloneDX JSON schemas for 1.4, 1.5, 1.6 and 1.7 directly: data is absent from the component type list in 1.4 and present in 1.5, 1.6 and 1.7, and the same holds for componentData and modelCard.

Is "DataBOM" a defined term?

No. Search either specification and you will not find a document type called DataBOM, DBOM or data BOM. The phrase is shorthand used by vendors, researchers and policy writers for "a bill of materials that describes data", usually the training, validation and test sets behind a machine learning model.

What each standard actually provides:

  • SPDX 3.0.1 defines a Dataset profile. The profile summary says it "provides additional metadata, based on Software Profile, that is useful for datasets". It contributes one class, DatasetPackage, plus three vocabularies (DatasetType, ConfidentialityLevelType, DatasetAvailabilityType). A dataset in SPDX is therefore a package that is also a dataset, and it inherits every package field: name, version, download location, hashes, supplier, package URL and so on.
  • CycloneDX treats data as one of its component types. The component type list in the 1.7 schema describes data as "A collection of discrete values that convey information." The CycloneDX capabilities site has no separate data BOM page; its ML-BOM page says CycloneDX "represents datasets, models, and configurations for AI and machine learning systems". So in CycloneDX terms a data BOM is a BOM whose components are of type data, normally inside an ML-BOM.

The framing matters. SPDX gives you typed fields for the questions a data governance reviewer asks: how was this collected, what was done to it, does it hold sensitive personal information, who may see it. CycloneDX gives a smaller structured core (kind of data, location, classification, ownership) and expects the rest in free text or properties.

What is the SPDX 3 Dataset profile?

The Dataset profile is one of the SPDX 3 profiles that an SpdxDocument can declare in profileConformance. Its namespace is https://spdx.org/rdf/3.0.1/terms/Dataset, and in the JSON-LD serialisation its terms carry the prefix dataset_, so the class is written dataset_DatasetPackage and its fields dataset_datasetType, dataset_intendedUse and so on.

Class hierarchy

DatasetPackage sits at the end of this chain, according to the 3.0.1 model page:

Core/Element then Core/Artifact then Software/SoftwareArtifact then Software/Package then Dataset/DatasetPackage.

Because it is a package, everything you know from describing software packages in SPDX 3 applies: name, software_packageVersion, software_downloadLocation, software_packageUrl, software_homePage, verifiedUsing for hashes, suppliedBy, originatedBy, externalIdentifier, externalRef, copyrightText, attributionText and the time fields builtTime, releaseTime and validUntilTime.

Profile conformance rules

The profile page states two conformance rules. For every DatasetPackage there must be exactly one Relationship of type hasDeclaredLicense from the dataset to a licence, and exactly one of type hasConcludedLicense. In other words, a conformant dataset record always says what licence the data claims and what licence the document author concluded applies. For datasets assembled from many sources, that second relationship is where your legal review lands. Our post on which licence fields to record for a model and its datasets covers how to fill them.

Required fields on a DatasetPackage

The 3.0.1 class page lists "external properties cardinality updates", which tighten inherited properties for datasets. Combined with the base rules, a DatasetPackage must have:

PropertyInherited fromCardinality on DatasetPackage
spdxIdCore/Elementexactly 1
creationInfoCore/Elementexactly 1
nameCore/Elementexactly 1
builtTimeCore/Artifactat least 1 (the property itself allows 1)
releaseTimeCore/Artifactat least 1
originatedByCore/Artifactexactly 1
software_downloadLocationSoftware/Packageexactly 1
software_primaryPurposeSoftware/SoftwareArtifactexactly 1
dataset_datasetTypeDataset1 or more

originatedBy is capped at one agent, so if several organisations created the data, name the one responsible for the release and describe the others in dataCollectionProcess.

Where the model page and the Linux Foundation paper differ

The Linux Foundation paper, written by members of the working group that built the profiles, includes a table of mandatory DatasetPackage fields. It is useful context, but it does not match the published 3.0.1 model exactly:

FieldLF paper (Oct 2024)SPDX 3.0.1 model page
datasetTypeRequired, 1..1Required, 1..*
downloadLocationRequired, 1..*Required, 1..1
originatedByRequired, 1..*Required, 1..1
packageVersionRequired, 1..1Optional (0..1)
Field named for build time"buildTime"builtTime

The paper also says the working group selected "36 fields", then describes 20 for the AI Profile and 18 for the Dataset Profile, which adds up to 38. We could not reconcile that from the text. When you write tooling, follow the model pages and the published JSON schema, and treat the paper as design rationale. Recording packageVersion anyway is good practice: a dataset that changes monthly without a version string is impossible to pin.

Every Dataset profile field, field by field

These are the 13 properties the profile adds. Types and cardinalities come from the 3.0.1 DatasetPackage page; the descriptions are summarised from each property's page. The last column suggests what to actually write, which the spec leaves to you.

Property (JSON-LD name)TypeCardinalityWhat the spec says it recordsWhat to write in practice
dataset_datasetTypeDatasetType vocabulary1..*The structure of data in the dataset; a dataset can match several typesEvery type that applies, for example text and structured for a JSONL file of prompts and labels
dataset_dataCollectionProcessstring0..1How the dataset was collected, such as sources scraped or the interview protocolSources, date range, collection method and the legal basis or consent route
dataset_dataPreprocessingstring0..*Steps applied to raw data, such as normalisation, deduplication, tokenisationOne entry per step, in order, so a reviewer can replay them
dataset_anonymizationMethodUsedstring0..*Methods used to anonymise the dataset or its fieldsThe technique and its scope, for example "email and phone replaced with typed placeholders"
dataset_hasSensitivePersonalInformationPresenceType (yes, no, noAssertion)0..1Whether sensitive personal data, or data that allows conclusions about identity, is presentyes or no only when someone checked; otherwise noAssertion
dataset_confidentialityLevelConfidentialityLevelType0..1Confidentiality of the data points, using Traffic Light Protocol levelsclear, green, amber or red, aligned with your own classification policy
dataset_datasetAvailabilityDatasetAvailabilityType0..1How the data can be obtaineddirectDownload, query, clickthrough, registration or scrapingScript
dataset_datasetSizenon negative integer0..1Size of the dataset, measured in bytesByte count of the exact artifact you hashed
dataset_datasetNoisestring0..1Noisy fields or samples, or noise affecting the whole setKnown label error rates, stale entries, duplicated sources
dataset_datasetUpdateMechanismstring0..1How the dataset is updatedRelease cadence, versioning rule, retention of older versions
dataset_intendedUsestring0..1What the dataset should be used for, including population limitsThe task and population it is valid for, and uses it is not fit for
dataset_knownBiasstring0..*Biases the dataset is known to containOne entry per known skew, phrased so a model owner can test for it
dataset_sensorDictionaryEntry (key and value)0..*A sensor used to collect the data and its calibration valueSensor model as key, calibration as value; only for sensor data

The three Dataset vocabularies

DatasetType has 14 values: audio, categorical, graph, image, noAssertion, numeric, other, sensor, structured, syntactic, text, timeseries, timestamp and video. The vocabulary page notes that one dataset can match several, for example sensor data that is also a time series.

ConfidentialityLevelType follows the Traffic Light Protocol with four values. In the spec's words: red means data points "can only be shared with named recipients"; amber means sharing only "with specific organizations and their clients on a need to know basis"; green means sharing "within a community of peers and partners"; and clear means it may be distributed freely.

DatasetAvailabilityType has five values: directDownload (public, downloadable), query (public but only reachable through queries that return parts), clickthrough (access only after accepting terms), registration (email registration without an affirmative acceptance of terms) and scrapingScript (the provider does not distribute the data; you reassemble it with a script).

Two fields borrow vocabularies from elsewhere. hasSensitivePersonalInformation uses the Core PresenceType (yes, no, noAssertion), and its property page points to the matching useSensitivePersonalInformation field on ai_AIPackage. primaryPurpose uses the Software SoftwarePurpose vocabulary, where the value for a dataset is data ("The Element is data").

Relationships that matter for datasets

The Core RelationshipType vocabulary carries the links you need:

RelationshipDefinition in 3.0.1Typical use for data
trainedOn"The from Element has been trained on the to Element(s)."Model package to its training datasets
testedOn"The from Element has been tested on the to Element(s)."Model package to evaluation datasets
hasDeclaredLicenseLicence found in or declared by the artifactRequired once per dataset for conformance
hasConcludedLicenseLicence the SPDX data creator concludedRequired once per dataset for conformance
hasDataFile"The from Element treats each to Element as a data file."A package that reads a data file at runtime
hasDocumentation"The from Element is documented by each to Element."Dataset to its dataset card or datasheet file
descendantOf and ancestorOfLineage between elementsA cleaned split derived from a raw corpus
generates"The from Element generates each to Element."A preprocessing script that produced the dataset

The SPDX relationship page states that trainedOn and testedOn do not imply a dependency in the package management sense. That is useful: a model trained on a dataset does not ship the dataset, and your dependency graph should not pretend it does.

Why SPDX split AI and Dataset into two profiles

According to the Linux Foundation paper, the working group started in 2021, analysed 103 proposed fields drawn from model cards, datasheets and fact sheets, and kept those that were relevant, representable and agreed by consensus. They separated datasets from models because "there is not always a direct relationship between a specific dataset and a specific model, as each can be acquired separately." One dataset trains many models; one model draws on many datasets. Separate elements joined by relationships fit that many to many shape better than nesting.

What does CycloneDX record for data?

CycloneDX has two places where dataset facts live: a component of type data with its data array, and the datasets list inside a model component's modelCard.

Data components and the componentData object

A component with "type": "data" behaves like any other CycloneDX component. It has bom-ref, name, version, supplier, hashes, licenses, externalReferences, pedigree and properties. The 1.7 schema says the component's data array "SHOULD be specified for any component of type data and must not be specified for other component types."

Each entry in that array is a componentData object. Its fields, from the 1.7 schema:

FieldTypeWhat it records
typeenum, requiredThe general subject of the data: source-code, configuration, dataset, definition or other
bom-refstringAn identifier to reference this data entry elsewhere in the BOM
namestringThe name of the dataset
contentsobjectThe data itself or a pointer to it: attachment (inline text or encoded data), url, and properties (name and value pairs)
classificationstringFree text tag for type, sensitivity and value "if altered, stolen, or destroyed"
sensitiveDataarray of stringsA description of any sensitive data in the dataset
graphicsobjectA collection of images, such as charts that describe the data
descriptionstringFree text; the schema suggests size and use (training, testing, validation)
governanceobjectowners, stewards and custodians, each a list of organisations or individual contacts

The governance block is the most distinctive thing CycloneDX adds. The schema defines the three roles: custodians handle "safe custody, transport, and storage of data", stewards are "responsible for data content, context, and associated business rules", and owners "are concerned with risk and appropriate access to data". SPDX 3 has no equivalent structure; you would express the same roles with suppliedBy, originatedBy and free text.

The componentData.type enum is about the subject of the data, not its modality. A training set is dataset; a feature store schema could be definition; a model's runtime settings could be configuration. There is no CycloneDX equivalent of SPDX's 14 value DatasetType list for text, image, audio and so on.

The data section of a model card

A machine-learning-model component can carry a modelCard, which the schema describes as covering "the intended uses of a machine learning model and potential limitations, including biases and ethical considerations." Its modelParameters object has approach, task, architectureFamily, modelArchitecture, datasets, inputs and outputs.

modelParameters.datasets is "The datasets used to train and evaluate the model." Each item is one of two shapes:

  1. Inline data information: a full componentData object embedded in the model card.
  2. Data reference: an object with a single ref field pointing to a data component's bom-ref in the same BOM, or to a BOM-Link (urn:cdx:<serialNumber>/<version>#<bom-ref>) in another BOM.

Prefer the reference form. It lets one data component carry the hashes, licence and governance once, while several models point at it. The model card's considerations object (users, useCases, technicalLimitations, performanceTradeoffs, ethicalConsiderations, environmentalConsiderations, fairnessAssessments) is about the model, not the dataset, but it is often where dataset bias shows up in CycloneDX documents, because the data component has no bias field. Our post on dataset cards and which fields matter for supply chain review explains how the card documents relate.

On the tooling side, the cdxgen README documents an aibom command that generates CycloneDX AI/ML BOMs from source, Hugging Face URLs or package URLs, Modelfiles and GGUF files. It describes models and AI services; check its output before assuming it emits data components for your own datasets.

CycloneDX also has an external reference type named model-card, plus documentation, distribution and license, which you can use to point a data component at a human readable dataset card.

Field mapping: SPDX 3 Dataset profile to CycloneDX 1.7

This is the table most teams need when one consumer wants SPDX and another wants CycloneDX. "Lossy" means the fact survives only as free text or a custom property.

FactSPDX 3.0.1CycloneDX 1.7Fidelity
It is a datasettype: dataset_DatasetPackage, software_primaryPurpose: datacomponent type: data; componentData.type: datasetDirect
Namenamecomponent name; componentData.nameDirect
Versionsoftware_packageVersioncomponent versionDirect
Where to get itsoftware_downloadLocationcomponentData.contents.url, or an external reference of type distributionDirect
HashesverifiedUsing with Hash (algorithm, hashValue)component hashes (alg, content)Direct; algorithm names differ in spelling (sha256 vs SHA-256)
Package identifiersoftware_packageUrl, externalIdentifierpurl, externalReferencesDirect
CreatororiginatedBy (exactly one agent)authors, manufacturer or governance ownersPartial; pick a convention
SuppliersuppliedBysupplierDirect
Build and release timesbuiltTime, releaseTime (required)No component level fields; BOM metadata.timestamp or propertiesLossy
Declared and concluded licencehasDeclaredLicense and hasConcludedLicense relationshipslicenses with acknowledgement of declared or concludedDirect in meaning
Data modalitydataset_datasetType (14 values)None; description or propertiesLossy
Collection processdataset_dataCollectionProcessNone; descriptionLossy
Preprocessing stepsdataset_dataPreprocessing (list)None; description, or pedigree for lineageLossy
Anonymisationdataset_anonymizationMethodUsedsensitiveData entries, as textPartial
Sensitive personal information presentdataset_hasSensitivePersonalInformation (yes, no, noAssertion)sensitiveData (list of descriptions)Partial; CycloneDX has no tri state flag
Confidentialitydataset_confidentialityLevel (TLP)classification (free text)Partial; write the TLP value into the text
Availabilitydataset_datasetAvailabilityNoneLossy
Sizedataset_datasetSize (bytes)None; description or contents.propertiesLossy
Known biasdataset_knownBiasNone on data; model card considerations or fairnessAssessments on the modelLossy
Noisedataset_datasetNoiseNoneLossy
Update mechanismdataset_datasetUpdateMechanismNoneLossy
Intended usedataset_intendedUseNone on data; model card considerations.useCases on the modelLossy
Sensor and calibrationdataset_sensor (key and value)contents.propertiesPartial
Ownership rolesNone (use agents and text)governance owners, stewards, custodiansLossy going to SPDX
Inline contentNone (point to files)contents.attachmentLossy going to SPDX
Model trained on ittrainedOn relationship from ai_AIPackagemodelCard.modelParameters.datasets with refDirect
Model evaluated on ittestedOn relationshipSame datasets list; no train or test distinction in structurePartial
Dataset card documenthasDocumentation to a file elementexternalReferences of type documentation or model-cardDirect

In short: for structured answers about collection, bias or availability, SPDX 3 is the better carrier and a CycloneDX copy is a summary. For data ownership roles, CycloneDX is richer, and that structure is lost going the other way.

When you must keep SPDX only facts in CycloneDX, put them in properties under a namespace you own and document it, for example example:dataset:collectionProcess. A consumer that does not know your namespace will ignore the values, so do not rely on them for a policy decision in someone else's tool.

Worked example: a data BOM for a fine tuning dataset

The scenario: a support team fine tunes an assistant model on resolved support tickets. The dataset is a JSONL file, versioned monthly, with personal data replaced by placeholders before release. We will create the file, hash it, write the SPDX 3.0.1 record, write the CycloneDX 1.7 record, validate both, and link the dataset to the model.

Step 1: create and hash the exact artifact

A data BOM describes bytes, not an idea. Hash the file you publish, and record its size from the same file. This two line sample stands in for the real dataset so you can reproduce the hash:

printf '{"prompt":"How do I rotate an API key?","completion":"Open Settings, then Keys, then choose Rotate."}\n{"prompt":"Where do I download invoices?","completion":"Billing, then Invoices, then Download PDF."}\n' > support-ft-train.jsonl

# Linux
sha256sum support-ft-train.jsonl
# macOS
shasum -a 256 support-ft-train.jsonl
# 362f40d1ecc405e4ea628de9a15e2571d702d1ef76bd4e93fa4d3b17905b42be  support-ft-train.jsonl

wc -c < support-ft-train.jsonl
# 203

For sharded datasets, hash every shard, or hash a manifest of shard hashes. Our guide on verifying a downloaded dataset with checksums covers the consumer side.

Step 2: write the SPDX 3.0.1 Dataset record

This document validates against SPDX 3.0.1 (JSON schema and SHACL shapes) using spdx3-validate 0.0.7. It declares the Core, Software, SimpleLicensing and Dataset profiles, carries every required DatasetPackage property, and includes the two licence relationships the profile demands.

{
  "@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
  "@graph": [
    {
      "type": "CreationInfo",
      "@id": "_:creationinfo",
      "specVersion": "3.0.1",
      "created": "2026-10-20T09:00:00Z",
      "createdBy": ["https://example.com/spdx/agent/ml-data-team"]
    },
    {
      "type": "Organization",
      "spdxId": "https://example.com/spdx/agent/ml-data-team",
      "creationInfo": "_:creationinfo",
      "name": "Example Corp ML Data Team"
    },
    {
      "type": "SpdxDocument",
      "spdxId": "https://example.com/spdx/doc/support-ft-2026-10",
      "creationInfo": "_:creationinfo",
      "profileConformance": ["core", "software", "simpleLicensing", "dataset"],
      "rootElement": ["https://example.com/spdx/dataset/support-ft"],
      "element": [
        "https://example.com/spdx/agent/ml-data-team",
        "https://example.com/spdx/dataset/support-ft",
        "https://example.com/spdx/license/cc-by-4.0",
        "https://example.com/spdx/rel/declared",
        "https://example.com/spdx/rel/concluded"
      ]
    },
    {
      "type": "dataset_DatasetPackage",
      "spdxId": "https://example.com/spdx/dataset/support-ft",
      "creationInfo": "_:creationinfo",
      "name": "support-ft",
      "software_packageVersion": "2026.10.0",
      "software_downloadLocation": "https://data.example.com/datasets/support-ft/2026.10.0/support-ft-train.jsonl",
      "software_primaryPurpose": "data",
      "builtTime": "2026-10-18T14:00:00Z",
      "releaseTime": "2026-10-19T10:00:00Z",
      "originatedBy": ["https://example.com/spdx/agent/ml-data-team"],
      "verifiedUsing": [
        {
          "type": "Hash",
          "algorithm": "sha256",
          "hashValue": "362f40d1ecc405e4ea628de9a15e2571d702d1ef76bd4e93fa4d3b17905b42be"
        }
      ],
      "dataset_datasetType": ["text"],
      "dataset_datasetSize": 203,
      "dataset_dataCollectionProcess": "Resolved support tickets from the internal helpdesk, January to September 2026, exported by query with customer consent under the support terms.",
      "dataset_dataPreprocessing": [
        "Removed tickets with attachments",
        "Replaced names, emails and phone numbers with typed placeholders",
        "Deduplicated on normalised prompt text"
      ],
      "dataset_anonymizationMethodUsed": ["Named entity redaction with typed placeholders"],
      "dataset_hasSensitivePersonalInformation": "no",
      "dataset_confidentialityLevel": "amber",
      "dataset_datasetAvailability": "registration",
      "dataset_intendedUse": "Fine tuning the support assistant model for English language product questions.",
      "dataset_knownBias": ["Over represents billing questions from enterprise accounts"],
      "dataset_datasetNoise": "Some completions reference menu labels that changed in August 2026.",
      "dataset_datasetUpdateMechanism": "New version cut monthly; previous versions retained for 24 months."
    },
    {
      "type": "simplelicensing_LicenseExpression",
      "spdxId": "https://example.com/spdx/license/cc-by-4.0",
      "creationInfo": "_:creationinfo",
      "simplelicensing_licenseExpression": "CC-BY-4.0"
    },
    {
      "type": "Relationship",
      "spdxId": "https://example.com/spdx/rel/declared",
      "creationInfo": "_:creationinfo",
      "from": "https://example.com/spdx/dataset/support-ft",
      "relationshipType": "hasDeclaredLicense",
      "to": ["https://example.com/spdx/license/cc-by-4.0"]
    },
    {
      "type": "Relationship",
      "spdxId": "https://example.com/spdx/rel/concluded",
      "creationInfo": "_:creationinfo",
      "from": "https://example.com/spdx/dataset/support-ft",
      "relationshipType": "hasConcludedLicense",
      "to": ["https://example.com/spdx/license/cc-by-4.0"]
    }
  ]
}

Choices worth explaining:

  • CC-BY-4.0 is a placeholder licence for the example. Internal support tickets usually have no public licence at all; in that case write the expression your legal team concludes, which may be a LicenseRef- identifier for an internal terms document.
  • hasSensitivePersonalInformation is no because the redaction ran and was checked. If nobody checked, write noAssertion, never a hopeful no.
  • confidentialityLevel is amber because the content stays with the company and its contractors; align TLP values with your own classification scheme.
  • dataPreprocessing and knownBias are lists, and one entry per item diffs cleanly between dataset versions.

Step 3: validate the SPDX record

SPDX 3 documents have two layers of checks: the JSON schema at https://spdx.org/schema/3.0.1/spdx-json-schema.json, and SHACL shapes that check the RDF graph. spdx3-validate is an open source command line tool, maintained in the JPEWdev GitHub repository by an SPDX tooling contributor, that runs both.

python3 -m venv .venv && . .venv/bin/activate
pip install spdx3-validate
spdx3-validate --json dataset.spdx3.json --spdx-version 3.0.1

A clean run prints only progress lines. To see what an error looks like, delete the dataset_datasetType line and run it again; the SHACL check reports "Less than 1 values" on the datasetType path for the dataset element.

The caveat: when we deleted builtTime instead, the tool passed the document, although the 3.0.1 class page lists it as required. The JSON schema does not encode that tightening either, so add your own check:

jq -e '."@graph"[] | select(.type=="dataset_DatasetPackage")
  | (.builtTime and .releaseTime and .originatedBy and .software_downloadLocation
     and .software_primaryPurpose and (.dataset_datasetType | length > 0))' \
  dataset.spdx3.json

jq -e exits non zero if the expression is false or null, which makes it usable as a CI gate.

Step 4: write the CycloneDX 1.7 record

This BOM validates against the official bom-1.7.schema.json from the CycloneDX specification repository (we checked it with a Draft 7 JSON Schema validator and the schema's referenced SPDX licence and JSF schemas). The model is the BOM subject in metadata.component, and its model card points at the data component by bom-ref.

{
  "bomFormat": "CycloneDX",
  "specVersion": "1.7",
  "serialNumber": "urn:uuid:3f6c2a8e-6b1d-4c1e-9d2a-7a0b5e4c9f11",
  "version": 1,
  "metadata": {
    "timestamp": "2026-10-20T09:00:00Z",
    "component": {
      "type": "machine-learning-model",
      "bom-ref": "model-support-assistant",
      "name": "support-assistant",
      "version": "1.4.0",
      "modelCard": {
        "modelParameters": {
          "task": "text-generation",
          "datasets": [
            { "ref": "data-support-ft" }
          ]
        }
      }
    }
  },
  "components": [
    {
      "type": "data",
      "bom-ref": "data-support-ft",
      "name": "support-ft",
      "version": "2026.10.0",
      "supplier": { "name": "Example Corp ML Data Team" },
      "hashes": [
        { "alg": "SHA-256", "content": "362f40d1ecc405e4ea628de9a15e2571d702d1ef76bd4e93fa4d3b17905b42be" }
      ],
      "licenses": [
        { "license": { "id": "CC-BY-4.0" } }
      ],
      "data": [
        {
          "bom-ref": "data-support-ft-train",
          "type": "dataset",
          "name": "support-ft train split",
          "contents": {
            "url": "https://data.example.com/datasets/support-ft/2026.10.0/support-ft-train.jsonl",
            "properties": [
              { "name": "example:sizeBytes", "value": "203" },
              { "name": "example:format", "value": "jsonl" }
            ]
          },
          "classification": "Confidential (TLP:AMBER)",
          "sensitiveData": [
            "Customer names, emails and phone numbers replaced with typed placeholders before release"
          ],
          "description": "Resolved support tickets, January to September 2026. Training split for fine tuning. Deduplicated on normalised prompt text.",
          "governance": {
            "owners": [ { "organization": { "name": "Example Corp Support Operations" } } ],
            "stewards": [ { "organization": { "name": "Example Corp ML Data Team" } } ],
            "custodians": [ { "organization": { "name": "Example Corp Platform Storage" } } ]
          }
        }
      ]
    }
  ],
  "dependencies": [
    { "ref": "model-support-assistant", "dependsOn": ["data-support-ft"] },
    { "ref": "data-support-ft" }
  ]
}

Compare it with the SPDX record and the loss is visible. Collection process, preprocessing and size are folded into description and custom properties. The known bias, the noise note, the availability value and the update mechanism have nowhere structured to go, so we left them out rather than invent fields. The governance roles, on the other hand, have no SPDX equivalent.

The dependencies entry is a judgement call: a deployed model does not need its training set at runtime. Some teams rely only on the model card reference; others include datasets so graph tools show lineage. Pick one convention and document it.

Step 5: validate the CycloneDX record

The CycloneDX CLI, maintained in the CycloneDX GitHub organisation, has a validate command that accepts --input-version values from v1_0 to v1_7 and a --fail-on-errors flag for CI:

# macOS with Homebrew
brew install cyclonedx/cyclonedx/cyclonedx-cli

cyclonedx validate --input-file dataset.cdx.json --input-format json \
  --input-version v1_7 --fail-on-errors

Step 6: check that the BOMs still match the bytes

A data BOM goes stale when someone regenerates the dataset without updating it. Compare hashes in CI:

want=$(jq -r '."@graph"[] | select(.type=="dataset_DatasetPackage") | .verifiedUsing[0].hashValue' dataset.spdx3.json)
have=$(sha256sum support-ft-train.jsonl | cut -d' ' -f1)
[ "$want" = "$have" ] || { echo "dataset hash mismatch"; exit 1; }

cdx=$(jq -r '.components[] | select(.type=="data") | .hashes[] | select(.alg=="SHA-256") | .content' dataset.cdx.json)
[ "$cdx" = "$have" ] || { echo "CycloneDX hash mismatch"; exit 1; }

Step 7: run it all in GitHub Actions

name: data-bom
on:
  pull_request:
    paths: ["data/**", "boms/**"]
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - name: Install validators
        run: |
          pip install spdx3-validate
          curl -sSL -o /usr/local/bin/cyclonedx \
            https://github.com/CycloneDX/cyclonedx-cli/releases/latest/download/cyclonedx-linux-x64
          chmod +x /usr/local/bin/cyclonedx
      - name: Validate SPDX 3.0.1 dataset record
        run: spdx3-validate --json boms/dataset.spdx3.json --spdx-version 3.0.1
      - name: Check inherited required fields
        run: |
          jq -e '."@graph"[] | select(.type=="dataset_DatasetPackage")
            | (.builtTime and .releaseTime and .originatedBy and .software_downloadLocation
               and .software_primaryPurpose and (.dataset_datasetType | length > 0))' \
            boms/dataset.spdx3.json
      - name: Validate CycloneDX 1.7 BOM
        run: cyclonedx validate --input-file boms/dataset.cdx.json --input-format json --input-version v1_7 --fail-on-errors
      - name: Hashes match the dataset
        run: |
          have=$(sha256sum data/support-ft-train.jsonl | cut -d' ' -f1)
          want=$(jq -r '."@graph"[] | select(.type=="dataset_DatasetPackage") | .verifiedUsing[0].hashValue' boms/dataset.spdx3.json)
          test "$want" = "$have"

Pin the CLI to a specific release rather than latest once the workflow works, so a new CLI version cannot change your results without a reviewed change.

How a data BOM links to the model AIBOM and the dataset card

A dataset record is rarely read on its own. Reviewers start from a model or a product and walk down. Three links make that walk possible.

In SPDX 3: relationships between elements

Add the model and the dataset card to the same graph and connect them. The fragment below was added to the Step 2 document (with "ai" added to profileConformance and the new IDs added to element), and the combined document validated against SPDX 3.0.1 with the same tool:

{
  "type": "ai_AIPackage",
  "spdxId": "https://example.com/spdx/model/support-assistant",
  "creationInfo": "_:creationinfo",
  "name": "support-assistant",
  "software_packageVersion": "1.4.0",
  "software_downloadLocation": "https://models.example.com/support-assistant/1.4.0/",
  "software_primaryPurpose": "model",
  "releaseTime": "2026-10-22T08:00:00Z",
  "suppliedBy": "https://example.com/spdx/agent/ml-data-team",
  "ai_typeOfModel": ["transformer"]
},
{
  "type": "software_File",
  "spdxId": "https://example.com/spdx/file/support-ft-datacard",
  "creationInfo": "_:creationinfo",
  "name": "DATASET_CARD.md",
  "software_primaryPurpose": "documentation"
},
{
  "type": "Relationship",
  "spdxId": "https://example.com/spdx/rel/trained",
  "creationInfo": "_:creationinfo",
  "from": "https://example.com/spdx/model/support-assistant",
  "relationshipType": "trainedOn",
  "to": ["https://example.com/spdx/dataset/support-ft"]
},
{
  "type": "Relationship",
  "spdxId": "https://example.com/spdx/rel/card",
  "creationInfo": "_:creationinfo",
  "from": "https://example.com/spdx/dataset/support-ft",
  "relationshipType": "hasDocumentation",
  "to": ["https://example.com/spdx/file/support-ft-datacard"]
}

On ai_AIPackage, the 3.0.1 model page tightens releaseTime, suppliedBy, downloadLocation, packageVersion and primaryPurpose to required, which is why they appear above. If the dataset record lives in a separate document from the model, SPDX 3 lets the model document refer to the dataset's spdxId and declare it through the import mechanism on SpdxDocument, so one dataset record can serve many model documents.

In CycloneDX: references and BOM-Link

Inside one BOM, modelCard.modelParameters.datasets[].ref points to the data component's bom-ref, as in Step 4. When the dataset has its own BOM, the reference can be a BOM-Link, which the 1.7 schema defines with the pattern urn:cdx:<serialNumber uuid>/<version>#<bom-ref>. For our example, a separate dataset BOM with serial number 3f6c2a8e-6b1d-4c1e-9d2a-7a0b5e4c9f11, version 1, would be referenced as urn:cdx:3f6c2a8e-6b1d-4c1e-9d2a-7a0b5e4c9f11/1#data-support-ft. The schema notes that a plain bom-ref should not start with urn:cdx: to avoid confusion with BOM-Links.

The dataset card is a document, not a BOM

A dataset card or datasheet is written for people. The Linux Foundation paper mapped datasheet questions onto SPDX fields and found most land in dataCollectionProcess, intendedUse, knownBias or hasSensitivePersonalInformation, while some, such as sampling strategy, survive only as discussion text. Keep the card, link it from the BOM, and copy into the BOM only the facts a machine acts on: hashes, licence, sensitivity, confidentiality, availability and version. The card says why; the BOM says exactly which bytes.

Which EU AI Act Article 10 duties a data BOM helps with

Article 10 of Regulation (EU) 2024/1689 requires that training, validation and testing data sets for high risk AI systems be "subject to data governance and management practices appropriate for the intended purpose." Paragraph 2 lists what those practices concern. A data BOM does not satisfy Article 10, but it can carry the evidence for several points. Our Article 10 data governance post covers the obligation itself; this table is only the field view.

Article 10(2) pointWhat it concernsSPDX 3 Dataset fieldCycloneDX 1.7 place
(a)Relevant design choicesNot a dedicated field; description or the dataset carddescription
(b)Collection processes, origin, original purpose for personal datadataCollectionProcess, originatedBy, suppliedBydescription, supplier, governance owners
(c)Preparation: annotation, labelling, cleaning, updating, enrichment, aggregationdataPreprocessing, datasetUpdateMechanismdescription, pedigree
(d)Assumptions about what the data measures and representsintendedUseModel card considerations
(e)Availability, quantity and suitabilitydatasetAvailability, datasetSize, intendedUsedescription, contents.properties
(f) and (g)Examination for biases and measures against themknownBias, datasetNoiseModel card considerations.fairnessAssessments
(h)Data gaps and shortcomingsknownBias, datasetNoise, free textdescription

Paragraph 5 lets providers process special categories of personal data for bias correction only under strict conditions, including deletion once the bias is corrected. hasSensitivePersonalInformation, anonymizationMethodUsed and confidentialityLevel are where a reviewer would expect to see that data flagged. SPDX carries this evidence in structured form; CycloneDX mostly as text.

Which format should carry your data BOM?

SituationBetter fitWhy
Governance or legal review needs structured answers on collection, bias, sensitivitySPDX 3 Dataset profileDedicated, typed fields and controlled vocabularies
You need owner, steward and custodian roles recordedCycloneDX data componentgovernance block has no SPDX equivalent
Your SBOM pipeline and consumers already speak CycloneDXCycloneDX, with namespaced properties for extrasOne format across software, models and data
One dataset trains many models owned by different teamsEither, as a separate document per datasetSPDX import or CycloneDX BOM-Link keeps one source of truth
You want bothSPDX 3 as the record of truth, CycloneDX as a derived summaryConversion from SPDX to CycloneDX loses fields, not the other way round for most dataset facts

If you produce both, generate them from one internal source, such as a YAML file in the dataset repository, rather than converting one into the other.

Where Safeguard fits, honestly

Safeguard does not generate dataset or data BOMs, and it does not produce SPDX 3 Dataset profile documents or CycloneDX data components describing your training data. The fit here is indirect, and it is worth being exact about it.

What is relevant: SBOM generation and analysis is generally available and handles SPDX (JSON, XML, RDF) and CycloneDX (JSON, XML) for the software around your models, including the training scripts, preprocessing code and serving containers that sit next to a dataset. AI-BOM discovery is in early access; it inventories AI assets across code (SDK imports), cloud (SageMaker, Bedrock, Vertex), SaaS and vector databases, and produces ML-BOM in CycloneDX and the SPDX AI profile. That is the model and AI asset side of the picture, not the dataset records described in this post. License compliance, generally available, parses SPDX licence expressions and applies policy, which is the same expression syntax a dataset's hasDeclaredLicense uses, but we do not claim it evaluates dataset records. Policies, gates and guardrails are generally available for the software pipeline that builds and ships your model.

So the practical split is: write the dataset record with the open source steps above, keep it in the dataset repository, and use Safeguard on the code and images that consume it.

Common mistakes

  1. Calling a CSV of filenames a data BOM. Without hashes, version and licence, a list of datasets is an inventory, not a bill of materials. Hash the exact bytes you trained on.
  2. Hashing the source, then training on a filtered copy. If preprocessing produces a new file, that file is the dataset. Record the raw corpus separately and link the two with descendantOf or CycloneDX pedigree.
  3. Writing no for sensitive personal information by default. The PresenceType vocabulary has noAssertion for a reason. Use no only after a check you can describe in anonymizationMethodUsed or dataPreprocessing.
  4. Trusting schema validation alone for SPDX 3. The JSON schema and, in our test, the SHACL shapes did not enforce every class specific required property. Add a check for builtTime, releaseTime, originatedBy, downloadLocation and primaryPurpose.
  5. Forgetting the two licence relationships. Dataset profile conformance requires exactly one hasDeclaredLicense and one hasConcludedLicense per DatasetPackage. A licence in description does not count.
  6. Inventing CycloneDX fields. CycloneDX schemas reject unknown properties in many objects. Use properties with a namespace you own for facts with no home, and document that namespace.
  7. Embedding the full dataset in every model card. Inline componentData in each model card duplicates hashes and licences that will drift. Define one data component and reference it.
  8. Letting the BOM drift from the bytes. Regenerated datasets with an unchanged record are worse than no record. Gate merges on a hash comparison.

FAQ

What is a data BOM?

A data BOM is an informal name for a bill of materials that describes datasets: what the data is, where it came from, how it was processed, its hashes, licence and handling rules. Neither SPDX nor CycloneDX defines it as a named document type. SPDX 3 provides it through the Dataset profile, and CycloneDX through components of type data.

Does CycloneDX support datasets?

Yes, since CycloneDX 1.5. A component with "type": "data" carries a data array of componentData objects, and a machine learning model's modelCard.modelParameters.datasets lists the datasets used to train and evaluate it, either inline or by reference. CycloneDX groups this under its ML-BOM capability.

What fields are required in an SPDX 3 DatasetPackage?

On the SPDX 3.0.1 model page: spdxId, creationInfo, name, builtTime, releaseTime, originatedBy, software_downloadLocation, software_primaryPurpose and at least one dataset_datasetType. Profile conformance also requires exactly one hasDeclaredLicense and one hasConcludedLicense relationship for each dataset.

What is the difference between the SPDX AI profile and the Dataset profile?

The AI profile describes models and AI systems through ai_AIPackage, with fields such as type of model, hyperparameters, metrics and limitations. The Dataset profile describes data through dataset_DatasetPackage. They were kept separate because one dataset can train many models and one model can use many datasets; trainedOn and testedOn relationships connect them.

Can I convert an SPDX 3 dataset record to CycloneDX without losing information?

Not fully. Name, version, hashes, download location, supplier and licences map directly. Collection process, preprocessing, data modality, availability, size, noise, known bias, intended use and update mechanism have no dedicated CycloneDX fields and survive only as description text or custom properties. Going the other way, CycloneDX's governance roles and inline attachments have no SPDX 3 equivalent.

How do I link a dataset to the model that was trained on it?

In SPDX 3, add a Relationship with relationshipType trainedOn (or testedOn for evaluation data) from the ai_AIPackage to the dataset_DatasetPackage. In CycloneDX, add an entry with ref set to the data component's bom-ref in the model's modelCard.modelParameters.datasets, or a BOM-Link if the dataset is described in another BOM.

Does a data BOM satisfy EU AI Act Article 10?

No. Article 10 requires data governance and management practices for high risk AI systems. A data BOM records evidence for several of the points those practices concern, such as collection processes, preparation steps and bias examination, but the practices themselves, and the assessment behind them, are what the regulation asks for.

Should a dataset appear in my software SBOM?

Only if it ships with the software. A model trained on a dataset does not normally redistribute it, and SPDX says trainedOn does not imply dependency. Keep dataset records alongside the model's AIBOM and link them, and include a dataset in the software SBOM when files from it are actually packaged and delivered.

Fix it without waiting for upstream

A data BOM tells you which bytes trained a model, but the code that loads, preprocesses and serves those bytes is ordinary open source software with ordinary vulnerabilities. The fit is indirect for datasets themselves; for the parsers, training frameworks and serving images around them, someone still has to ship the fixed build.

Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.

1. Autonomous remediation: pull requests, not tickets

When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.

2. Gold Open Source: the safe version in seconds, free

gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.

3. Gold Registry: start from components that are already fixed

registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.

When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.

Your situationWhat to use
A fixed version existsAutonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version
You need to know today whether you are affectedGold Open Source lookup and "Am I affected?" checker, free and without login
You want vulnerable versions to stop shippingThe Gold scan GitHub Action in CI, plus Safeguard policies and gates
Upstream has no fix yet, or will not ship oneA custom Gold request; fork and patch (early access)
You want to start clean on new projectsPull hardened components and images from Gold Registry

One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.

See how Safeguard would help

None of these demos generate a data BOM; Safeguard does not do that. They show the software side of an AI project, where the same CI discipline from the walkthrough applies.

This demo is a real CLI session: the assistant reads a finding and its advisory, asks before each command, edits package.json, runs install and tests with approval, and pushes nothing. The same approval driven flow is how you would update a vulnerable parsing or training library in the repository that holds your dataset records.

Click through it at your own pace, or open it full screen.

This one sets up the runner on your own machine, approves it with a device code, then scans from the terminal. It suits teams whose training code and data must stay on their own hardware, since the scan runs where the code lives.

Click through it at your own pace, or open it full screen.

The last demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. It is useful if your ML engineers already work in Claude and want to ask about the components in their training and serving projects without leaving it.

Click through it at your own pace, or open it full screen.

To try it on your own projects, download Safeguard or read the CLI documentation.

Quick check

Which two relationships must exist for every DatasetPackage in a conformant SPDX 3.0.1 Dataset profile document?

Exactly one hasDeclaredLicense and exactly one hasConcludedLicense, each from the dataset to a licence element.

Where would you record a dataset's known bias in CycloneDX 1.7?

There is no dedicated field on a data component. Use the description, a namespaced property, or the model card considerations (for example fairnessAssessments) on the model trained with it, and expect that information to be lost in automated conversion.

Sources

Last reviewed: 23 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.