Safeguard
Supply Chain Security•

SPDX 3.0.1 to CycloneDX 1.7: Field by Field Mapping and Conversion Loss

How SPDX 3.0.1 fields map to CycloneDX 1.7, which open source tools convert in which direction, and how to test a round trip for lost or altered data.

Safeguard Security Team
39 min read

Most core SBOM data in SPDX 3.0.1 maps cleanly to CycloneDX 1.7: names, versions, purls, suppliers, hashes, dependency edges, license expressions and basic VEX status. What gets lost is the SPDX graph: typed relationships, per element creation data, lifecycle scopes and most AI and Dataset fields. No standards body tool converts between them directly, so map, validate and diff yourself.

Key takeaways

  • Document fields map almost one to one, with two catches: SPDX created may be a commit date, and an SPDX document IRI is not a valid CycloneDX serialNumber.
  • Package fields map well. Losses sit at the edges: extra CPEs, some hash algorithms, builtTime, releaseTime, validUntilTime, supportLevel and additionalPurpose.
  • SPDX 3.0.1 has 59 relationship types. CycloneDX 1.7 has dependsOn and provides, plus nesting, pedigree and external references.
  • Declared and concluded licenses map directly through the acknowledgement field added in CycloneDX 1.6.
  • VEX status maps; VEX justifications do not map one to one.
  • Much of the AI and Dataset profile detail has no CycloneDX field.
  • The CycloneDX CLI converts SPDX JSON 2.3, protobom lists SPDX 3.0 as planned, and SPDX tools-java converts SPDX 2 to 3.0.1. Build a small mapper, validate against the 1.7 schema and diff a round trip.

If you want the short version of SPDX and CycloneDX conversion first, read Can you convert an SBOM between SPDX and CycloneDX without losing data?. For whether to convert at all, see convert SPDX to CycloneDX at intake or keep both formats. This post goes field by field.

Why SPDX 3 to CycloneDX is a different problem from SPDX 2.3

Most published mapping work was built for SPDX 2.x, where a document held packages, files and a relationship list under one set of creation fields. That shape is close to a CycloneDX BOM.

SPDX 3.0 rebuilt the model. Everything is an Element with a globally unique IRI (spdxId). An SpdxDocument is one kind of element collection, independent of any serialization. Each element points to its own CreationInfo. Relationships are elements with their own IDs, a completeness value and optional startTime and endTime. VEX statements are relationship subclasses, and licenses are elements linked by hasDeclaredLicense and hasConcludedLicense.

CycloneDX 1.7 stayed a document oriented tree: one metadata block, nestable components, a dependencies array keyed by bom-ref, plus compositions, vulnerabilities, services, formulation, declarations, definitions, citations and annotations. Mapping a graph onto a tree is where most of the loss comes from.

Version timeline

Both formats moved in the last two years, and the next versions are already visible. Dates below come from the standards bodies' own release pages.

DateReleaseWhat it means for mapping
15 Apr 2024SPDX 3.0 released on GitHubNew element and relationship graph, profiles (Core, Software, Security, Licensing, AI, Dataset, Build, Lite, Extension)
9 Apr 2024CycloneDX 1.6Adds CBOM, attestations (CDXA) and the license acknowledgement field
1 Jul 2024ECMA-424 1st editionCycloneDX newsroom announces CycloneDX 1.6 as an Ecma International standard
17 Dec 2024SPDX 3.0.1 release published on GitHubCurrent SPDX 3 release; the model pages and JSON schema used in this post
21 Oct 2025CycloneDX 1.7Citations, patents, expanded cryptography registry, distributionConstraints (TLP), license expressionDetails; announced as the final 1.x release
Dec 2025ECMA-424 2nd editionDefines CycloneDX 1.7
24 Jan 2026SPDX 3.1 RC1Release candidate; spdx.dev says the 3.1 model expands to safety, service, hardware, supply chain and operations. Not final, may change
5 Aug 2026CycloneDX 2.0 announcedCycloneDX newsroom: release expected fall 2026, Ecma ratification in December

CycloneDX 1.7 is a stable target: the last 1.x schema and an Ecma standard. SPDX 3.1 adds a services area, which will narrow one reverse direction gap. Record the versions you used in your conversion notes.

Document level fields: what maps one to one

The SPDX 3.0.1 CreationInfo class has five properties: specVersion (required), created (required), createdBy (one or more agents), createdUsing (tools) and comment. SpdxDocument adds dataLicense, import, namespaceMap and inherits element, rootElement and profileConformance.

SPDX 3.0.1CycloneDX 1.7FidelityNotes
CreationInfo.specVersionspecVersionReplacedEach side states its own format version. Record the source version in a property
CreationInfo.createdmetadata.timestampMeaning shiftSPDX notes created is often the date of last change, such as a git commit date, to support reproducible builds. CycloneDX describes the timestamp as when the BOM was created
createdBy (Person)metadata.authorsGoodCycloneDX describes authors as the person(s) who created the BOM
createdBy (Organization)metadata.manufacturerPartialCycloneDX suggests manufacturer for BOMs created by automated means. It holds one organization, not a list
createdBy (SoftwareAgent)metadata.tools.services or tools.componentsJudgementPick one convention and document it
createdUsing (Tool)metadata.tools.componentsGoodTool versions in SPDX are often folded into the name. Split them if you can
SpdxDocument.spdxId (an IRI)serialNumberNo direct matchserialNumber must match urn:uuid followed by an RFC 4122 UUID. Derive a UUIDv5 from the IRI so the result is repeatable, and keep the IRI in a property
(none)version (integer BOM version)GapSPDX 3 has no BOM revision counter. Start at 1
rootElementmetadata.componentPartialSPDX allows several roots; CycloneDX holds one. Extra roots become top level components
profileConformance(none)LostKeep as a property if downstream tools care
dataLicensemetadata.licensesGood when presentCycloneDX describes metadata.licenses as the license for the BOM document itself
namespaceMap(none)LostOnly needed to shorten IRIs; harmless to drop once IRIs are expanded
import (ExternalMap)externalReferences of type bom, or BOM-LinkPartialSPDX imports elements defined elsewhere. CycloneDX can point at another BOM but does not import its elements
Per element creationInfo(none)LostCycloneDX has one metadata block. If elements carry different creators, you have to choose

The serialNumber row catches people often. The schema enforces the urn:uuid pattern, so an https:// IRI fails validation. A UUIDv5 over the document IRI gives the same serial number on every run, which matters because CycloneDX tells consumers to prefer the highest version for a given serial number.

Package and component fields

In SPDX 3.0.1 a software_Package inherits from SoftwareArtifact, Artifact and Element, so its fields come from four classes. The CycloneDX 1.7 component object has 36 properties.

SPDX 3.0.1 PackageCycloneDX 1.7 componentFidelityNotes
spdxIdbom-refGoodAny string is a valid bom-ref. Reusing the IRI keeps traceability
namenameExactRequired on both sides
packageVersionversionExactCycloneDX 1.7 also has versionRange, which SPDX lacks
packageUrlpurlExact
suppliedBy (Agent)supplier (organizationalEntity)GoodCycloneDX notes the supplier may be a distributor or repackager
originatedBy (Agents, many)manufacturer or authorsPartialmanufacturer holds one organization; persons go to authors. author (singular) is deprecated in 1.7
externalIdentifier cpe22 or cpe23cpePartialCycloneDX holds one CPE string. Extra CPEs can go into evidence.identity
externalIdentifier swhidswhidGood
externalIdentifier gitoid, or contentIdentifier gitoidomniborIdGoodSPDX recommends contentIdentifier for artifact gitoids
externalIdentifier swidswidPartialCycloneDX swid requires tagId and name; an SPDX identifier string alone is not enough
verifiedUsing HashhashesMostly goodSee the algorithm table below
downloadLocationexternalReferences type distributionGood
homePageexternalReferences type websiteGood
copyrightTextcopyrightExact
attributionText(none)LostKeep as a property or in a NOTICE file
sourceInfo(none)LostKeep as a property or in description
primaryPurposetypePartialVocabulary differs; see below
additionalPurpose(none)LostCycloneDX has a single type
builtTime, releaseTime, validUntilTime(none on component)LostProperties, or releaseNotes.timestamp for release time if you build release notes
supportLevel(none)LostProperties
standardName(none on component)LostCycloneDX 1.7 models standards under definitions.standards, which is a different construct
summary, description, commentdescriptionPartialThree fields collapse into one

Identifiers: purl, CPE, SWHID and gitoid

SPDX 3.0.1 ExternalIdentifierType has 11 values, from cpe22 and cpe23 to swhid, swid and gitoid. CycloneDX gives purl, CPE, OmniBOR and SWHID their own fields. The common loss is the second CPE, which can break CPE based matching for C, C++ and firmware components.

Hash algorithms

SPDX 3.0.1 lists 22 hash algorithm values; CycloneDX 1.7 lists 14.

SPDX 3.0.1 algorithmCycloneDX 1.7 alg
md5, sha1, sha256, sha384, sha512MD5, SHA-1, SHA-256, SHA-384, SHA-512
sha3_256, sha3_384, sha3_512SHA3-256, SHA3-384, SHA3-512
blake2b256, blake2b384, blake2b512, blake3BLAKE2b-256, BLAKE2b-384, BLAKE2b-512, BLAKE3
sha224, sha3_224, md2, md4, md6, adler32No enum value: lost
crystalsKyber, crystalsDilithium, falcon, otherNo enum value: lost
No SPDX valueStreebog-256, Streebog-512

Purpose to component type

SPDX 3.0.1 SoftwarePurpose has 29 values; CycloneDX 1.7 component type has 13. Direct pairs include application, framework, library, container, platform, device, firmware and file, plus operatingSystem, deviceDriver, model and data to their hyphenated or renamed CycloneDX forms. Purposes such as archive, diskImage, install, module, patch, source and test need a fallback type plus a property recording the original.

Relationships, the dependency graph and compositions

SPDX 3.0.1 RelationshipType has 59 values. Grouped by family, here is where each lands.

SPDX 3.0.1 relationship familyExamplesCycloneDX 1.7 landing spotFidelity
Runtime and build dependenciesdependsOn, hasOptionalDependency, hasProvidedDependency, hasStaticLink, hasDynamicLink, hasPrerequisitedependencies[].dependsOnEdge kept, edge type lost. Optional can become scope: optional on the target
Containmentcontains, hasDataFile, expandsTo, packagedByNested components[].components or dependsOnGood if you nest; nesting is a tree, so shared children duplicate
Descriptiondescribesmetadata.componentGood for one root
LineageancestorOf, descendantOf, hasVariant, copiedTo, patchedBy, modifiedBy, amendedBypedigree.ancestors, descendants, variants, patchesPartial; pedigree embeds components instead of pointing at them
Implementation(no SPDX type)dependencies[].providesCycloneDX only; added for things like a crypto library that implements an algorithm
LicensinghasDeclaredLicense, hasConcludedLicenselicenses[] with acknowledgementGood
Security and VEXaffects, doesNotAffect, fixedIn, underInvestigationFor, hasAssessmentFor, hasAssociatedVulnerability, foundBy, reportedBy, coordinatedBy, exploitCreatedBy, fixedBy, publishedBy, republishedByvulnerabilities[] with affects, analysis, credits, sourceStatus good; agent roles partial
Build and provenancegenerates, hasInput, hasOutput, invokedBy, delegatedTo, usesTool, configures, hasHostformulation (workflows, tasks, steps)Different model; usually kept as properties or dropped
Documentation and evidencehasDocumentation, hasSpecification, hasRequirement, hasEvidence, hasExample, hasMetadata, hasDependencyManifest, hasDistributionArtifactexternalReferences (documentation, evidence and so on)Partial; the target must be a URL, not an element
TestinghasTest, hasTestCase, testedOn(none)Lost unless kept as properties
AI and datatrainedOn, testedOnmodelCard.modelParameters.datasets (by ref)Partial
File changehasAddedFile, hasDeletedFilepedigree.patches or diffPartial
Catch allother, availableFrom, serializedInArtifact, hasOptionalComponentPropertiesJudgement

Two facts drive the table. CycloneDX's dependency object has exactly three properties: ref, dependsOn and provides. And both ref and dependsOn entries are plain bom-ref references inside the same BOM, so an SPDX edge to an element brought in through import has no target after conversion.

Completeness versus compositions

SPDX 3.0.1 puts completeness on each relationship: complete, incomplete or noAssertion. The NoneElement individual in to says no such relationship exists. CycloneDX 1.7 states completeness separately in compositions, each with an aggregate value (complete, incomplete, six first and third party variants, unknown, not_specified) and a list of assemblies, dependencies or vulnerabilities. A complete SPDX dependsOn becomes a dependency entry plus a complete composition naming the from component. A converter that skips compositions turns "we know this list is complete" into "nobody said".

Lifecycle scope and time bounds

SPDX 3.0.1 LifecycleScopedRelationship carries a scope of design, development, build, test, runtime or other. CycloneDX 1.7 has no per edge lifecycle; the closest tools are component scope (required, optional, excluded) and document level metadata.lifecycles. A build only dependency becomes an ordinary dependency unless you split BOMs by phase. Time bounds are lost.

Licensing: declared versus concluded, expressions and LicenseRef

SPDX 3.0.1 SimpleLicensing holds LicenseExpression (licenseExpression, licenseListVersion, customIdToUri); ExpandedLicensing models expressions as objects. CycloneDX 1.7 licenses items are either a license object or an expression object, each with acknowledgement set to declared or concluded.

SPDX 3.0.1CycloneDX 1.7Notes
hasDeclaredLicense to a LicenseExpressionlicenses[] item with expression and acknowledgement: declaredExact, if both sides use valid SPDX expression syntax
hasConcludedLicenseSame, with acknowledgement: concludedExact
Single listed license, such as MITlicense.id or expressionlicense.id is checked against the SPDX license list enum in CycloneDX's bundled spdx.schema.json
LicenseRef-acme-eula inside an expressionexpression (valid) or license.nameLicenseRef identifiers are not valid license.id values; they belong in an expression or a named license
customIdToUriexpressionDetails[].licenseIdentifier with a URL, or license.urlNew in 1.7; older converters drop it
licenseListVersion(none)Lost. Matters when an identifier was deprecated between list versions
SimpleLicensingText (full text)license.textGood
ExpandedLicensing object treesFlatten to an expression stringStructure lost; meaning kept if serialized correctly
NoAssertionLicense, NoneLicense individualsOmit licenses, or a propertyNo exact CycloneDX equivalent for "the creator asserts none"
dataLicense on the documentmetadata.licensesGood

Output that puts a LicenseRef into license.id fails CycloneDX 1.7 validation, so make validation a gate. For the license side in brief, see the SPDX vs CycloneDX cheat sheet.

Vulnerabilities and VEX: Security profile to CycloneDX vulnerabilities

The SPDX 3.0.1 Security profile attaches assessments to a Vulnerability element as relationship subclasses: VEX affected, not affected, fixed and under investigation, plus CVSS v2, v3 and v4, EPSS, SSVC and exploit catalog assessments. CycloneDX 1.7 keeps a top level vulnerabilities array whose analysis object holds state, justification, response, detail, firstIssued and lastUpdated.

Status

SPDX 3.0.1 classCycloneDX 1.7 analysis.stateNotes
VexAffectedexploitableactionStatement goes to recommendation or analysis.detail; response is an enum (can_not_fix, will_not_fix, update, rollback, workaround_available) that you must infer
VexNotAffectednot_affectedJustification mapping below
VexFixedresolved
VexUnderInvestigationin_triage
(none)false_positiveCycloneDX only; no SPDX status on the way back
(none)resolved_with_pedigreeCycloneDX only; means the fix is shown in the component's pedigree

Justifications

SPDX 3.0.1 has five justification values; CycloneDX 1.7 has nine. The mapping below is our reading of the two definitions, not a published crosswalk.

SPDX 3.0.1 justificationTypeClosest CycloneDX 1.7 justificationFit
componentNotPresentcode_not_presentApproximate; CycloneDX usually expresses an absent component by not listing it
vulnerableCodeNotPresentcode_not_presentGood
vulnerableCodeNotInExecutePathcode_not_reachableGood
vulnerableCodeCannotBeControlledByAdversaryNo clean match; requires_configuration or requires_environment depending on the reasonWeak; keep the impactStatement text
inlineMitigationsAlreadyExistprotected_by_mitigating_control, protected_at_runtime or protected_by_compilerOne to many; needs the impact statement to choose

On the way back, CycloneDX's requires_dependency and protected_at_perimeter have no SPDX value. A tool that guesses will turn a precise statement into a vague one.

Scores, exploitation and timestamps

CVSS maps to ratings[] with method CVSSv2, CVSSv3, CVSSv31 or CVSSv4; SSVC to method SSVC. EPSS has no scoreMethod and exploit catalog listings have no field, so both become other ratings, advisories or properties. publishedTime and modifiedTime map to firstIssued and lastUpdated; withdrawnTime, vexVersion and per statement suppliedBy are lost. If VEX is your main reason to convert, keep the supplier's statements in their original format and load them separately.

AI and Dataset profiles to modelCard and data components

SPDX 3.0.1 ai_AIPackage adds fields such as typeOfModel, hyperparameter, metric, limitation, energyConsumption, safetyRiskAssessment and autonomyType. CycloneDX 1.7 uses a machine-learning-model component with a modelCard of modelParameters, quantitativeAnalysis and considerations.

SPDX 3.0.1 AIPackageCycloneDX 1.7Fidelity
Package core fieldscomponent of type machine-learning-modelGood
typeOfModelmodelParameters.architectureFamily or modelArchitecturePartial
informationAboutTrainingmodelParameters.approach.type (enum: supervised, unsupervised, reinforcement-learning, semi-supervised, self-supervised) plus free text in a propertyPartial
domain, informationAboutApplicationconsiderations.useCasesPartial
limitationconsiderations.technicalLimitationsGood
metricquantitativeAnalysis.performanceMetrics[] (type, value)Good
metricDecisionThreshold(none)Lost; properties
energyConsumptionconsiderations.environmentalConsiderations.energyConsumptionsPartial; the two energy models differ
trainedOn relationship to a DatasetPackagemodelParameters.datasets[] with refGood
hyperparameter(none)Lost; properties
modelDataPreprocessing, modelExplainability(none)Lost
safetyRiskAssessment(none)Lost; ethicalConsiderations is not equivalent
autonomyType, useSensitivePersonalInformation(none)Lost
standardCompliance(none on the model card)Lost

For datasets, CycloneDX 1.7 uses the data array of a component of type data (the schema says it must not appear on other types). Each entry has type, name, contents, classification, sensitiveData, graphics, description and governance.

SPDX 3.0.1 DatasetPackageCycloneDX 1.7Fidelity
Core package fieldscomponent of type dataGood
datasetTypedata[].type is dataset; the SPDX dataset kind goes to a propertyPartial
confidentialityLeveldata[].classificationPartial
hasSensitivePersonalInformationdata[].sensitiveDataPartial
intendedUse, dataCollectionProcessdata[].descriptionCollapsed
knownBiasModel level fairnessAssessments at bestWeak
anonymizationMethodUsed, datasetSize, datasetAvailability, datasetNoise, datasetUpdateMechanism, sensor(none)Lost
originatedBydata[].governance.ownersJudgement

If you need these fields for an EU AI Act or procurement question, keep the SPDX original as the record.

What CycloneDX 1.7 carries that SPDX 3.0.1 cannot receive

These CycloneDX 1.7 areas have no SPDX 3.0.1 home, so they vanish on a round trip:

  • services (SaaSBOM): endpoints, data flows, trust zones. SPDX 3.1 RC1 adds a services area; 3.0.1 does not have one.
  • formulation: how the product was built, as workflows, tasks and steps. SPDX's Build profile overlaps but is shaped differently.
  • declarations and definitions: attestations, claims, evidence and the standards they refer to.
  • citations (new in 1.7): who supplied which piece of data, through which process.
  • Patents and patent families (new in 1.7).
  • cryptoProperties (CBOM) and the 1.7 cryptography registry of algorithm families and elliptic curves.
  • metadata.distributionConstraints.tlp, the Traffic Light Protocol label.
  • evidence.occurrences, callstack and identity evidence with confidence values.
  • Component scope, isExternal and versionRange.
  • The integer BOM version and JSON signatures (JSF).

The SPDX cdx2spdx README says unmapped CycloneDX properties become SPDX annotations of type OTHER with a comment starting MISSING_CDX_PROPERTY:, which gives a searchable record of the loss.

One SBOM in both formats

Here is the same small SBOM in both formats: an application, acme-web 2.4.0, that depends on lodash 4.17.21, with declared and concluded MIT licenses, a SHA-512 hash and a VEX fixed statement for CVE-2021-23337 (OSV lists 4.17.21 as the fixed version). The SHA-512 value is the real npm integrity hash for that tarball, converted from base64 to hex.

This SPDX document validates against the SPDX 3.0.1 JSON schema published at spdx.org/schema/3.0.1/spdx-json-schema.json. We checked it with Python jsonschema 4.25.1, including negative tests that confirm the schema rejects unknown properties and bad enum values.

{
  "@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/acme-web-2.4.0#org-acme"],
      "createdUsing": ["https://example.com/spdx/acme-web-2.4.0#tool-gen"]
    },
    {
      "type": "Organization",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#org-acme",
      "creationInfo": "_:creationinfo",
      "name": "Acme Corp"
    },
    {
      "type": "Tool",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#tool-gen",
      "creationInfo": "_:creationinfo",
      "name": "example-sbom-generator 1.0.0"
    },
    {
      "type": "SpdxDocument",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#doc",
      "creationInfo": "_:creationinfo",
      "name": "acme-web 2.4.0 SBOM",
      "profileConformance": ["core", "software", "simpleLicensing", "security"],
      "rootElement": ["https://example.com/spdx/acme-web-2.4.0#pkg-app"],
      "element": [
        "https://example.com/spdx/acme-web-2.4.0#org-acme",
        "https://example.com/spdx/acme-web-2.4.0#tool-gen",
        "https://example.com/spdx/acme-web-2.4.0#pkg-app",
        "https://example.com/spdx/acme-web-2.4.0#pkg-lodash",
        "https://example.com/spdx/acme-web-2.4.0#rel-dep",
        "https://example.com/spdx/acme-web-2.4.0#lic-mit",
        "https://example.com/spdx/acme-web-2.4.0#rel-declared",
        "https://example.com/spdx/acme-web-2.4.0#rel-concluded",
        "https://example.com/spdx/acme-web-2.4.0#vuln-1",
        "https://example.com/spdx/acme-web-2.4.0#vex-1"
      ]
    },
    {
      "type": "software_Package",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#pkg-app",
      "creationInfo": "_:creationinfo",
      "name": "acme-web",
      "software_packageVersion": "2.4.0",
      "software_primaryPurpose": "application",
      "suppliedBy": "https://example.com/spdx/acme-web-2.4.0#org-acme",
      "software_packageUrl": "pkg:npm/acme-web@2.4.0"
    },
    {
      "type": "software_Package",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#pkg-lodash",
      "creationInfo": "_:creationinfo",
      "name": "lodash",
      "software_packageVersion": "4.17.21",
      "software_primaryPurpose": "library",
      "software_packageUrl": "pkg:npm/lodash@4.17.21",
      "software_downloadLocation": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
      "externalIdentifier": [
        {
          "type": "ExternalIdentifier",
          "externalIdentifierType": "cpe23",
          "identifier": "cpe:2.3:a:lodash:lodash:4.17.21:*:*:*:*:node.js:*:*"
        }
      ],
      "verifiedUsing": [
        {
          "type": "Hash",
          "algorithm": "sha512",
          "hashValue": "bf690311ee7b95e713ba568322e3533f2dd1cb880b189e99d4edef13592b81764daec43e2c54c61d5c558dc5cfb35ecb85b65519e74026ff17675b6f8f916f4a"
        }
      ]
    },
    {
      "type": "Relationship",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#rel-dep",
      "creationInfo": "_:creationinfo",
      "from": "https://example.com/spdx/acme-web-2.4.0#pkg-app",
      "relationshipType": "dependsOn",
      "to": ["https://example.com/spdx/acme-web-2.4.0#pkg-lodash"],
      "completeness": "complete"
    },
    {
      "type": "simplelicensing_LicenseExpression",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#lic-mit",
      "creationInfo": "_:creationinfo",
      "simplelicensing_licenseExpression": "MIT"
    },
    {
      "type": "Relationship",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#rel-declared",
      "creationInfo": "_:creationinfo",
      "from": "https://example.com/spdx/acme-web-2.4.0#pkg-lodash",
      "relationshipType": "hasDeclaredLicense",
      "to": ["https://example.com/spdx/acme-web-2.4.0#lic-mit"]
    },
    {
      "type": "Relationship",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#rel-concluded",
      "creationInfo": "_:creationinfo",
      "from": "https://example.com/spdx/acme-web-2.4.0#pkg-lodash",
      "relationshipType": "hasConcludedLicense",
      "to": ["https://example.com/spdx/acme-web-2.4.0#lic-mit"]
    },
    {
      "type": "security_Vulnerability",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#vuln-1",
      "creationInfo": "_:creationinfo",
      "externalIdentifier": [
        {
          "type": "ExternalIdentifier",
          "externalIdentifierType": "cve",
          "identifier": "CVE-2021-23337"
        }
      ]
    },
    {
      "type": "security_VexFixedVulnAssessmentRelationship",
      "spdxId": "https://example.com/spdx/acme-web-2.4.0#vex-1",
      "creationInfo": "_:creationinfo",
      "from": "https://example.com/spdx/acme-web-2.4.0#vuln-1",
      "relationshipType": "fixedIn",
      "to": ["https://example.com/spdx/acme-web-2.4.0#pkg-lodash"],
      "security_assessedElement": "https://example.com/spdx/acme-web-2.4.0#pkg-lodash",
      "suppliedBy": "https://example.com/spdx/acme-web-2.4.0#org-acme",
      "security_publishedTime": "2026-10-20T09:00:00Z"
    }
  ]
}

This CycloneDX BOM validates against bom-1.7.schema.json from the CycloneDX specification repository, with its bundled spdx.schema.json, jsf-0.82.schema.json and cryptography-defs.schema.json.

{
  "$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
  "bomFormat": "CycloneDX",
  "specVersion": "1.7",
  "serialNumber": "urn:uuid:5b0c3d6e-8f3a-4e9b-9c41-2a7d1f0e6b55",
  "version": 1,
  "metadata": {
    "timestamp": "2026-10-20T09:00:00Z",
    "lifecycles": [{ "phase": "build" }],
    "tools": {
      "components": [
        { "type": "application", "name": "example-sbom-generator", "version": "1.0.0" }
      ]
    },
    "manufacturer": { "name": "Acme Corp" },
    "component": {
      "type": "application",
      "bom-ref": "pkg:npm/acme-web@2.4.0",
      "supplier": { "name": "Acme Corp" },
      "name": "acme-web",
      "version": "2.4.0",
      "purl": "pkg:npm/acme-web@2.4.0"
    }
  },
  "components": [
    {
      "type": "library",
      "bom-ref": "pkg:npm/lodash@4.17.21",
      "name": "lodash",
      "version": "4.17.21",
      "purl": "pkg:npm/lodash@4.17.21",
      "cpe": "cpe:2.3:a:lodash:lodash:4.17.21:*:*:*:*:node.js:*:*",
      "hashes": [
        {
          "alg": "SHA-512",
          "content": "bf690311ee7b95e713ba568322e3533f2dd1cb880b189e99d4edef13592b81764daec43e2c54c61d5c558dc5cfb35ecb85b65519e74026ff17675b6f8f916f4a"
        }
      ],
      "licenses": [
        { "license": { "id": "MIT", "acknowledgement": "declared" } },
        { "license": { "id": "MIT", "acknowledgement": "concluded" } }
      ],
      "externalReferences": [
        {
          "type": "distribution",
          "url": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
        }
      ]
    }
  ],
  "dependencies": [
    { "ref": "pkg:npm/acme-web@2.4.0", "dependsOn": ["pkg:npm/lodash@4.17.21"] },
    { "ref": "pkg:npm/lodash@4.17.21", "dependsOn": [] }
  ],
  "compositions": [
    { "aggregate": "complete", "dependencies": ["pkg:npm/acme-web@2.4.0"] }
  ],
  "vulnerabilities": [
    {
      "id": "CVE-2021-23337",
      "source": { "name": "NVD", "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-23337" },
      "analysis": {
        "state": "resolved",
        "firstIssued": "2026-10-20T09:00:00Z"
      },
      "affects": [{ "ref": "pkg:npm/lodash@4.17.21" }]
    }
  ]
}

The SPDX relationship IDs, edge completeness, document IRI and profile list have no slot in the CycloneDX file, and the CycloneDX serialNumber, version and lifecycles were never stated in the SPDX document. A converter has to invent them.

Which open source tools convert, and in which direction

Tool capabilities below come only from each project's own README, as read in October 2026.

ToolHomeDirections per its READMESPDX 3.0.1CycloneDX 1.7
CycloneDX CLI (v0.33.1)CycloneDX GitHub organisationConverts between CycloneDX XML, JSON, Protobuf, CSV and SPDX JSON v2.3NoYes; output version selectable up to v1_7, and validate defaults to v1.7
SPDX tools-java (v2.0.7)SPDX GitHub organisationConverts between SPDX formats (Tag, RDF/XML, XLSX, XLS, JSON, XML, YAML, JSON-LD); converts SPDX 2 to SPDX 3.0.1Yes, as output from SPDX 2No
SPDX tools-pythonSPDX GitHub organisationParses, converts and validates SPDX 2.2 and 2.3; experimental SPDX 3 support that can convert 2.x to 3.0 and write JSON-LD; the README says v0.8 writes but does not read SPDX 3Experimental, write onlyNo
cdx2spdxSPDX GitHub organisationCycloneDX to SPDX; marked a prototype, "not considered stable"; unmapped CycloneDX data kept as annotationsThe README does not state the SPDX version writtenInput version not stated; check its test suite
protobom (v0.6.2)OpenSSF sandbox projectNeutral Go library; reads and writes SPDX 2.2 and 2.3 and CycloneDX 1.4 to 1.7 JSONListed as "planned"Yes
bomctlOpenSSF sandbox projectUses protobom; export formats listed up to SPDX 2.3 and CycloneDX 1.6NoNot in its export list

The CycloneDX CLI README also notes that SPDX conversion "can result in the loss of some information" and points to the CycloneDX .NET library for details. No row in the table goes from SPDX 3.0.1 to CycloneDX 1.7 in one step. Your realistic options are:

  1. Ask the supplier for CycloneDX 1.7 directly, or for SPDX 2.3 JSON alongside SPDX 3.0.1, and use the CycloneDX CLI on the 2.3 file.
  2. Write a small mapper for the subset you need (an example follows) and gate it with schema validation and a diff.
  3. Keep SPDX 3.0.1 as the record and generate CycloneDX from source yourself with the same build, rather than converting someone else's document.

Option 3 often gives the best CycloneDX, because a generator reading the lockfile or image fills scope, evidence and lifecycles from first hand data.

Step by step: convert, then prove what was lost

The walkthrough below uses only tools from the standards bodies' own GitHub organisations plus Python and jq. It assumes the supplier's SPDX 3.0.1 file is called supplier.spdx3.json.

Step 1: keep the original and validate it

Store the supplier file untouched, with its hash, before you do anything else. Then validate it against the SPDX 3.0.1 JSON schema.

mkdir -p sbom-intake && cp supplier.spdx3.json sbom-intake/
shasum -a 256 sbom-intake/supplier.spdx3.json > sbom-intake/supplier.spdx3.json.sha256

python3 -m venv .venv && . .venv/bin/activate
pip install jsonschema
curl -sSLo spdx3-schema.json https://spdx.org/schema/3.0.1/spdx-json-schema.json

python3 - <<'EOF'
import json, jsonschema
schema = json.load(open("spdx3-schema.json"))
doc = json.load(open("sbom-intake/supplier.spdx3.json"))
errors = list(jsonschema.Draft202012Validator(schema).iter_errors(doc))
for e in errors[:20]:
    print(list(e.absolute_path), e.message[:200])
raise SystemExit(1 if errors else 0)
EOF

A schema failure here is a supplier problem. Send it back rather than converting a broken file.

Step 2: inventory what the SPDX file actually uses

Before mapping, count what is in the graph. This tells you in advance which rows of the tables above apply.

jq -r '."@graph"[].type' sbom-intake/supplier.spdx3.json | sort | uniq -c | sort -rn
jq -r '."@graph"[] | select(.relationshipType) | .relationshipType' \
  sbom-intake/supplier.spdx3.json | sort | uniq -c | sort -rn
jq '[."@graph"[] | select(.type=="SpdxDocument") | .rootElement[]] | length' \
  sbom-intake/supplier.spdx3.json

AI, Dataset or Build classes mean heavy loss. Relationship types beyond dependsOn, contains, describes and the two license types need a decision. More than one root needs a choice for metadata.component.

Step 3: map with a script that reports its own losses

This minimal mapper handles packages, dependsOn, declared and concluded licenses, and VEX status. It prints a LOSS: line on stderr for everything it does not carry over. It is a starting point, not a complete converter. Extend the dictionaries and the warning list to match what Step 2 found.

"""Minimal SPDX 3.0.1 JSON-LD to CycloneDX 1.7 JSON mapper (illustrative).
Maps packages, dependsOn, declared/concluded licenses and VEX status.
Everything it does not map is listed on stderr so loss is visible."""
import json, sys, uuid

HASH = {"sha1": "SHA-1", "sha256": "SHA-256", "sha384": "SHA-384",
        "sha512": "SHA-512", "sha3_256": "SHA3-256", "sha3_384": "SHA3-384",
        "sha3_512": "SHA3-512", "blake2b256": "BLAKE2b-256",
        "blake2b384": "BLAKE2b-384", "blake2b512": "BLAKE2b-512",
        "blake3": "BLAKE3", "md5": "MD5"}
PURPOSE = {"application": "application", "framework": "framework",
           "library": "library", "container": "container",
           "operatingSystem": "operating-system", "device": "device",
           "deviceDriver": "device-driver", "firmware": "firmware",
           "file": "file", "model": "machine-learning-model", "data": "data"}
VEX = {"security_VexAffectedVulnAssessmentRelationship": "exploitable",
       "security_VexNotAffectedVulnAssessmentRelationship": "not_affected",
       "security_VexFixedVulnAssessmentRelationship": "resolved",
       "security_VexUnderInvestigationVulnAssessmentRelationship": "in_triage"}
JUST = {"vulnerableCodeNotPresent": "code_not_present",
        "componentNotPresent": "code_not_present",
        "vulnerableCodeNotInExecutePath": "code_not_reachable",
        "inlineMitigationsAlreadyExist": "protected_by_mitigating_control"}

doc = json.load(open(sys.argv[1]))
g = {o.get("spdxId") or o.get("@id"): o for o in doc["@graph"]}
lost = []
def warn(msg): lost.append(msg)

sdoc = next(o for o in g.values() if o["type"] == "SpdxDocument")
ci = next(o for o in g.values() if o["type"] == "CreationInfo")
roots = sdoc.get("rootElement", [])
if len(roots) != 1:
    warn(f"{len(roots)} rootElements; CycloneDX metadata.component holds one")

def component(p):
    c = {"type": PURPOSE.get(p.get("software_primaryPurpose"), "library"),
         "bom-ref": p["spdxId"], "name": p["name"]}
    if "software_packageVersion" in p: c["version"] = p["software_packageVersion"]
    if "software_packageUrl" in p: c["purl"] = p["software_packageUrl"]
    if "suppliedBy" in p: c["supplier"] = {"name": g[p["suppliedBy"]]["name"]}
    cpes = [x["identifier"] for x in p.get("externalIdentifier", [])
            if x["externalIdentifierType"] in ("cpe22", "cpe23")]
    if cpes: c["cpe"] = cpes[0]
    if len(cpes) > 1: warn(f"{p['name']}: {len(cpes)-1} extra CPE(s) dropped")
    hashes = []
    for h in p.get("verifiedUsing", []):
        if h.get("algorithm") in HASH:
            hashes.append({"alg": HASH[h["algorithm"]], "content": h["hashValue"]})
        else:
            warn(f"{p['name']}: hash algorithm {h.get('algorithm')} has no CycloneDX enum")
    if hashes: c["hashes"] = hashes
    if "software_downloadLocation" in p:
        c["externalReferences"] = [{"type": "distribution",
                                    "url": p["software_downloadLocation"]}]
    for k in ("builtTime", "releaseTime", "validUntilTime", "supportLevel",
              "software_additionalPurpose", "software_sourceInfo", "originatedBy"):
        if k in p: warn(f"{p['name']}: {k} not mapped")
    return c

comps, deps, vulns = {}, {}, []
for o in g.values():
    if o["type"] in ("software_Package", "ai_AIPackage", "dataset_DatasetPackage"):
        comps[o["spdxId"]] = component(o)
        if o["type"] != "software_Package":
            warn(f"{o['name']}: {o['type']} profile fields need modelCard/data mapping")
for o in g.values():
    t, rt = o["type"], o.get("relationshipType")
    if t == "Relationship" and rt == "dependsOn":
        deps.setdefault(o["from"], []).extend(o["to"])
        if o.get("completeness") == "complete":
            warn(f"completeness=complete on {o['spdxId']} dropped; CycloneDX states this with compositions")
    elif t == "Relationship" and rt in ("hasDeclaredLicense", "hasConcludedLicense"):
        ack = "declared" if rt == "hasDeclaredLicense" else "concluded"
        for lic in o["to"]:
            expr = g[lic].get("simplelicensing_licenseExpression")
            comps[o["from"]].setdefault("licenses", []).append(
                {"expression": expr, "acknowledgement": ack})
    elif t in VEX:
        v = g[o["from"]]
        vid = next((x["identifier"] for x in v.get("externalIdentifier", [])
                    if x["externalIdentifierType"] == "cve"), v["spdxId"])
        a = {"state": VEX[t]}
        if o.get("security_justificationType") in JUST:
            a["justification"] = JUST[o["security_justificationType"]]
        elif "security_justificationType" in o:
            warn(f"{vid}: justification {o['security_justificationType']} has no exact match")
        if "security_impactStatement" in o: a["detail"] = o["security_impactStatement"]
        if "security_publishedTime" in o: a["firstIssued"] = o["security_publishedTime"]
        vulns.append({"id": vid, "analysis": a,
                      "affects": [{"ref": r} for r in o["to"]]})
    elif t == "Relationship":
        warn(f"relationship {rt} ({o['spdxId']}) not mapped")

root = comps.pop(roots[0]) if roots else None
bom = {"$schema": "http://cyclonedx.org/schema/bom-1.7.schema.json",
       "bomFormat": "CycloneDX", "specVersion": "1.7",
       "serialNumber": "urn:uuid:" + str(uuid.uuid5(uuid.NAMESPACE_URL, sdoc["spdxId"])),
       "version": 1,
       "metadata": {"timestamp": ci["created"],
                    "tools": {"components": [{"type": "application", "name": g[t]["name"]}
                                             for t in ci.get("createdUsing", [])]},
                    "properties": [{"name": "spdx:spdxId", "value": sdoc["spdxId"]}]},
       "components": list(comps.values()),
       "dependencies": [{"ref": r, "dependsOn": d} for r, d in deps.items()]}
people = [g[a]["name"] for a in ci["createdBy"] if g[a]["type"] == "Person"]
orgs = [g[a]["name"] for a in ci["createdBy"] if g[a]["type"] == "Organization"]
if people: bom["metadata"]["authors"] = [{"name": n} for n in people]
if orgs: bom["metadata"]["manufacturer"] = {"name": orgs[0]}
if len(orgs) > 1: warn(f"{len(orgs)-1} extra creating organization(s) dropped")
if root: bom["metadata"]["component"] = root
if vulns: bom["vulnerabilities"] = vulns
json.dump(bom, sys.stdout, indent=2)
for m in lost: print("LOSS:", m, file=sys.stderr)
python3 spdx3_to_cdx17.py sbom-intake/supplier.spdx3.json \
  > converted.cdx.json 2> conversion-loss.log
cat conversion-loss.log

Run against the example above, the script produced a CycloneDX 1.7 file that passed schema validation and logged one loss: the complete marker on the dependsOn edge. Known and named loss is what you want.

Step 4: validate the CycloneDX output with the CycloneDX CLI

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

Run it in CI so that an invalid license ID or serial number breaks the build. The same tool reports duplicate versions:

cyclonedx-cli analyze --input-file converted.cdx.json --multiple-component-versions

Step 5: round trip back to SPDX and diff

To see what a full loop does, convert the CycloneDX file back with cdx2spdx, then raise the result to SPDX 3.0.1 with tools-java, and compare with the original.

java -jar cdx2spdx-<version>-jar-with-dependencies.jar converted.cdx.json roundtrip.spdx.json
jq -r '.spdxVersion' roundtrip.spdx.json
grep -c 'MISSING_CDX_PROPERTY' roundtrip.spdx.json

java -jar tools-java-2.0.7-jar-with-dependencies.jar Convert \
  roundtrip.spdx.json roundtrip.spdx3.json JSON JSONLD

The jar names follow each README; replace the version placeholder with the release you downloaded. Check the spdxVersion that cdx2spdx wrote before you continue, because its README does not state one. Every MISSING_CDX_PROPERTY annotation is a CycloneDX field that SPDX could not hold.

Now compare the two SPDX 3.0.1 documents on the fields that matter for security and licensing:

pkgs () {
  jq -r '."@graph"[] | select(.type=="software_Package")
    | [.name, (.software_packageVersion // ""), (.software_packageUrl // "")] | @tsv' "$1" | sort
}
rels () {
  jq -r '."@graph"[] | select(.relationshipType) | .relationshipType' "$1" | sort | uniq -c
}
hashes () {
  jq -r '."@graph"[] | select(.type=="software_Package") | .name as $n
    | (.verifiedUsing // [])[] | [$n, .algorithm, .hashValue] | @tsv' "$1" | sort
}
for f in pkgs rels hashes; do
  echo "== $f"; diff <($f sbom-intake/supplier.spdx3.json) <($f roundtrip.spdx3.json)
done

Read the diff by category:

  • Disappeared: packages missing from the round trip output, relationship types whose count dropped to zero, hashes for algorithms CycloneDX lacks.
  • Altered: versions or purls that changed (often normalisation), license expressions that came back as a different but equivalent string, created timestamps that now carry the conversion time instead of the commit time.
  • Invented: values the round trip added, such as new IDs, a new document IRI, or tool names for the converter rather than the original generator.

Keep the logs from Steps 3 to 5 next to the original file. The record should name the tool, version, date and operator for each conversion. The NTIA minimum elements mapping post explains why the author of SBOM data changes when you convert.

How the NTIA minimum elements survive the trip

The NTIA 2021 report lists seven baseline data fields: Supplier Name, Component Name, Version of the Component, Other Unique Identifiers, Dependency Relationship, Author of SBOM Data and Timestamp. Here is how each one fares from SPDX 3.0.1 to CycloneDX 1.7.

NTIA fieldSPDX 3.0.1CycloneDX 1.7Survives?
Supplier NamesuppliedBysupplier.nameYes
Component NamenamenameYes
Version of the ComponentpackageVersionversionYes
Other Unique IdentifierspackageUrl, externalIdentifier, contentIdentifierpurl, cpe, swhid, omniborIdMostly; extra CPEs at risk
Dependency RelationshipdependsOn and related typesdependenciesYes for the edge; type and scope often lost
Author of SBOM DataCreationInfo.createdBymetadata.authors or manufacturerYes, but the converter is now an author too
TimestampCreationInfo.createdmetadata.timestampValue yes; meaning may shift

For the wider interoperability picture, see the SBOM interoperability bridge post.

Where Safeguard fits

Safeguard's fit here is indirect. Safeguard is not an SPDX 3.0.1 to CycloneDX 1.7 converter. Its SBOM generation and analysis is generally available and produces SPDX (JSON, XML and RDF) and CycloneDX (JSON and XML), with more than 30 export formats including CSV, Excel and PDF. If you control the build, generating CycloneDX from source sidesteps most of the loss above. Our offering list does not name SPDX 3.0.1 JSON-LD by version, so confirm format coverage for your case before planning around it. The dual support post covers what Safeguard reads and writes today.

Three generally available capabilities help with the problems in this post. CSAF and VEX upload lets you load a supplier's exploitability statements in their own format instead of forcing them through a lossy VEX mapping. License compliance parses SPDX expressions, applies policy and generates NOTICE and attribution files, which helps when a LicenseRef or a declared versus concluded mismatch survives conversion. The Third Party Risk Manager lets you onboard vendors and request SBOMs from them, so the format request can be part of the vendor relationship from the start. For AI assets, AI-BOM discovery is in early access. It inventories AI assets across code, cloud, SaaS and vector databases and describes them as a CycloneDX ML-BOM or with the SPDX AI profile. It does not recover AI profile fields that a conversion has already dropped.

Once you have a converted CycloneDX file, you can scan it like any other SBOM:

safeguard scan --sbom converted.cdx.json

Results are written as JSON under ./output.

Common mistakes

  • Trusting a clean exit code. Converters rarely report what they omit. Validate, count and diff.
  • Copying the document IRI into serialNumber. It fails the 1.7 pattern; derive a UUIDv5.
  • Dropping the second CPE. Many C, C++ and firmware matches depend on it.
  • Flattening declared and concluded licenses. Use acknowledgement to keep both.
  • Putting LicenseRef identifiers in license.id. They are only valid inside an expression or as a named license.
  • Ignoring completeness. Without compositions, the supplier's claim of an exhaustive list is gone.
  • Assuming VEX justifications map one to one. Keep the impact statement and record your mapping.
  • Converting AI SBOMs and discarding the original. Most AI and Dataset profile detail has no CycloneDX field.
  • Replacing the original author. The converter is an additional author of SBOM data, not a replacement.

FAQ

Can the CycloneDX CLI convert SPDX 3.0.1 to CycloneDX 1.7?

Not according to its README. The CLI converts between CycloneDX XML, JSON, Protobuf, CSV and SPDX JSON v2.3. It can write CycloneDX 1.7 output, so an SPDX 2.3 JSON file from the supplier is the easiest route through it.

Is converting SPDX 3 to CycloneDX lossless?

No. Core inventory fields usually survive; typed relationships, per element creation data, lifecycle scopes, some hashes, extra CPEs and most AI and Dataset fields do not.

What is the CycloneDX equivalent of an SPDX relationship?

Dependency edges become dependencies entries, containment becomes nested components and lineage becomes pedigree. Many of the 59 SPDX types have no equivalent.

How do SPDX declared and concluded licenses appear in CycloneDX?

As separate entries in the component's licenses array, each with acknowledgement set to declared or concluded. That field arrived in CycloneDX 1.6 and is still there in 1.7.

Does SPDX 3.0 support VEX?

Yes. The SPDX 3.0.1 Security profile defines VEX affected, not affected, fixed and under investigation assessments as relationship classes, along with CVSS, EPSS, SSVC and exploit catalog assessments.

Does protobom support SPDX 3?

Its README lists SPDX 3.0 and 3.1 JSON as "planned" for both reading and writing, and CycloneDX 1.4 to 1.7 JSON as supported.

How do I map the SPDX AI profile to a CycloneDX ML-BOM?

Map the AIPackage to a component of type machine-learning-model and fill modelCard: typeOfModel to the architecture fields, metric to performanceMetrics, limitation to technicalLimitations, and trainedOn to modelParameters.datasets. Fields like hyperparameter, safetyRiskAssessment and autonomyType have no slot and need properties.

Does a converted SBOM still meet the NTIA minimum elements?

It can. All seven NTIA fields exist in both formats; check that the author and timestamp still describe the original data.

What comes after SPDX 3.0.1 and CycloneDX 1.7?

SPDX 3.1 has been a release candidate since January 2026. CycloneDX announced 2.0 in August 2026, with release expected in fall 2026. Rerun your mapping tests when either becomes final.

Fix it without waiting for upstream

An SBOM, converted or not, only tells you that a vulnerable component is there. Someone still has to ship the fixed build, and the tables above are worth little if the dependency they describe stays unpatched for months.

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

The first demo is a real CLI session: install, run safeguard, ask it to fix the lodash CVE in a repo. It reads the finding and advisory, asks before each command, edits package.json to lodash 4.17.21, runs npm install and npm test with approval, prints a summary and pushes nothing, then shows /permissions, /model and /scan. It is the flow you would use once a converted SBOM points at a vulnerable dependency.

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

The second demo sets up the runner on your own machine, approves it with its device code, then scans from the terminal. If SBOM intake runs on your own build host, scans of converted files can run there too.

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

The third demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. After an intake, you can then ask in plain words about the findings in your projects.

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

Download Safeguard or read the CLI documentation.

Quick check

Why can't you paste an SPDX 3.0.1 document IRI into a CycloneDX 1.7 serialNumber?

The CycloneDX 1.7 schema requires serialNumber to match urn:uuid followed by an RFC 4122 UUID. Derive a deterministic UUIDv5 from the IRI and keep the IRI in a property.

Which CycloneDX 1.7 construct carries SPDX relationship completeness?

The compositions array. Each composition has an aggregate value such as complete or incomplete and lists the assemblies, dependencies or vulnerabilities it covers.

Sources

Last reviewed: 23 October 2026. Both specifications have newer versions in progress; check the linked specification pages for the latest status.

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.