module
CycloneDX::VersionGate
Overview
Spec-version field and enum gating.
CycloneDX grew over successive spec versions in two different ways, and both have to be gated:
- fields were added — a BOM declared as
specVersion1.4 must NEVER contain a field that only exists from 1.5 on; - enum values were added —
component/@typewent from 8 permitted values in 1.4 to 13 in 1.6, andexternalReference/@typefrom 16 to 47. A structurally-correct document whosetypeiscryptographic-assetstill fails 1.4 validation.
VersionGate is the single source of truth for "what was introduced when".
It drives:
- a post-serialization JSON filter (
.filter_json_any), - an equivalent XML filter (
.filter_xml), - the
Validator, which reports whatever the filter had to change.
The last point matters: .violations does not re-implement the rules, it
runs the JSON filter and collects what it did. The validator therefore
cannot fall out of sync with the serializers.
What the gate does about an out-of-range value
Too-new fields are stripped. Too-new enum values are downgraded to the
enum's designated catch-all where the spec defines one (other for
externalReferenceType, not_specified for aggregateType). Where there is
no catch-all the value is left alone, because inventing a substitute would
misrepresent the component — Validator reports it instead, and the CLI
validates before writing so it can never emit such a document. The one
exception is hash/@alg, which is required and has no catch-all: there the
whole hash entry is dropped, since an optional hash is better lost than
invalid.
The field map is keyed by context (the kind of object a field lives in)
rather than by bare field name, because some names are ambiguous (e.g. tags
exists on component only from 1.6 but on releaseNotes since 1.4, and
bom-ref is core on a component but 1.5-only on a license).
Defined in:
cyclonedx/version_gate.crConstant Summary
-
AGGREGATE_VERSIONS =
{"incomplete_first_party_proprietary_only" => "1.5", "incomplete_first_party_opensource_only" => "1.5", "incomplete_third_party_proprietary_only" => "1.5", "incomplete_third_party_opensource_only" => "1.5"} -
aggregateType(composition/aggregate). -
CHILD_CONTEXT =
{ {:bom, "metadata"} => :metadata, {:bom, "components"} => :component, {:bom, "services"} => :service, {:bom, "compositions"} => :composition, {:bom, "vulnerabilities"} => :vulnerability, {:bom, "dependencies"} => :dependency, {:metadata, "component"} => :component, {:metadata, "licenses"} => :licenses_array, {:metadata, "tools"} => :tools, {:tools, "components"} => :component, {:tools, "services"} => :service, {:component, "components"} => :component, {:component, "licenses"} => :licenses_array, {:component, "pedigree"} => :pedigree, {:component, "evidence"} => :evidence, {:pedigree, "ancestors"} => :component, {:pedigree, "descendants"} => :component, {:pedigree, "variants"} => :component, {:evidence, "licenses"} => :licenses_array, {:service, "services"} => :service, {:service, "licenses"} => :licenses_array, {:vulnerability, "analysis"} => :vulnerability_analysis, {:licenses_array, "license"} => :license} -
{parent context, json key} => the context to filter the value under.
externalReferencesandhashesare matched by key alone (seechild_context) because they hang off many different parents and the key name is unambiguous across the whole schema. -
COMPONENT_TYPE_VERSIONS =
{"platform" => "1.5", "device-driver" => "1.5", "machine-learning-model" => "1.5", "data" => "1.5", "cryptographic-asset" => "1.6"} -
classification(component/@type). -
ENUM_FALLBACK =
{external_reference: {"type" => "other"}, composition: {"aggregate" => "not_specified"}} -
The catch-all each enum designates for "a type this document cannot express". Enums with no catch-all are absent, which is what makes a value non-downgradeable.
-
EXTERNAL_REFERENCE_TYPE_VERSIONS =
{"distribution-intake" => "1.5", "security-contact" => "1.5", "model-card" => "1.5", "log" => "1.5", "configuration" => "1.5", "evidence" => "1.5", "formulation" => "1.5", "attestation" => "1.5", "threat-model" => "1.5", "adversary-model" => "1.5", "risk-assessment" => "1.5", "vulnerability-assertion" => "1.5", "exploitability-statement" => "1.5", "pentest-report" => "1.5", "static-analysis-report" => "1.5", "dynamic-analysis-report" => "1.5", "runtime-analysis-report" => "1.5", "component-analysis-report" => "1.5", "maturity-report" => "1.5", "certification-report" => "1.5", "quality-metrics" => "1.5", "codified-infrastructure" => "1.5", "poam" => "1.5", "source-distribution" => "1.6", "electronic-signature" => "1.6", "digital-signature" => "1.6", "rfc-9116" => "1.6", "patent" => "1.7", "patent-family" => "1.7", "patent-assertion" => "1.7", "citation" => "1.7"} -
externalReferenceType(externalReference/@type). -
GATED =
{bom: {"annotations" => "1.5", "formulation" => "1.5", "definitions" => "1.6", "declarations" => "1.6"}, metadata: {"lifecycles" => "1.5", "manufacturer" => "1.6"}, component: {"modelCard" => "1.5", "data" => "1.5", "tags" => "1.6", "omniborId" => "1.6", "swhid" => "1.6", "cryptoProperties" => "1.6", "manufacturer" => "1.6", "authors" => "1.6"}, license: {"bom-ref" => "1.5", "acknowledgement" => "1.6"}, licenses_array: {"bom-ref" => "1.5", "acknowledgement" => "1.6"}, service: {"trustZone" => "1.5", "tags" => "1.6"}, composition: {"bom-ref" => "1.5", "vulnerabilities" => "1.5"}, vulnerability: {"workaround" => "1.5", "proofOfConcept" => "1.5", "rejected" => "1.5"}, vulnerability_analysis: {"firstIssued" => "1.5", "lastUpdated" => "1.5"}, evidence: {"identity" => "1.5", "occurrences" => "1.5", "callstack" => "1.5"}, dependency: {"provides" => "1.6"}, tools: {"components" => "1.5", "services" => "1.5"}} -
Context => { json_key => minimum_spec_version }.
The contexts identify which kind of object the gated keys live in; see
CHILD_CONTEXTfor how the JSON filter descends into them andXML_CONTEXTfor the XML equivalent.Only fields the object model can actually produce are listed. 1.7 added
bom.citations,metadata.distributionConstraints,component.isExternal/patentAssertions/versionRange,service.patentAssertionsandexternalReference.properties; none are modelled yet, so none appear here. -
GATED_ENUMS =
{component: {"type" => COMPONENT_TYPE_VERSIONS}, external_reference: {"type" => EXTERNAL_REFERENCE_TYPE_VERSIONS}, composition: {"aggregate" => AGGREGATE_VERSIONS}, hash: {"alg" => HASH_ALG_VERSIONS}} -
Context => { json_key => value-version table }.
-
GATED_JSON_ONLY =
{bom: {"properties" => "1.5"}} -
Fields the JSON schema gained later than the XSD did, so they must be stripped from JSON but kept in XML.
bom.propertiesis the only one: the 1.4 XSD listspropertiesamong thebomelement's children, but the 1.4 JSON schema does not allow it at the document root. -
HASH_ALG_VERSIONS =
{"Streebog-256" => "1.7", "Streebog-512" => "1.7"} -
hashAlg(hash/@alg). -
VERSION_ORDER =
{"1.4" => 0, "1.5" => 1, "1.6" => 2, "1.7" => 3} -
Ordering of the supported spec versions, oldest first.
-
XML_CONTEXT =
{"bom" => :bom, "metadata" => :metadata, "component" => :component, "service" => :service, "composition" => :composition, "vulnerability" => :vulnerability, "analysis" => :vulnerability_analysis, "evidence" => :evidence, "dependency" => :dependency, "tools" => :tools} -
The XML filter reuses
GATED: every gated JSON key maps to an XML element of the same name, and the gating context is identified by the parent element's name.GATED_JSON_ONLYis deliberately not consulted here. -
XML_ENUM_ATTR_FALLBACK =
{"reference" => {"type" => "other"}} -
Catch-alls for
XML_ENUM_ATTRS, mirroringENUM_FALLBACK. -
XML_ENUM_ATTRS =
{"component" => {"type" => COMPONENT_TYPE_VERSIONS}, "reference" => {"type" => EXTERNAL_REFERENCE_TYPE_VERSIONS}, "hash" => {"alg" => HASH_ALG_VERSIONS}} -
Enum-valued attributes, by element name.
-
XML_ENUM_TEXT =
{"aggregate" => AGGREGATE_VERSIONS} -
Elements whose text content is an enum value, with their catch-all.
-
XML_ENUM_TEXT_FALLBACK =
{"aggregate" => "not_specified"} -
XML_GATED_ATTRS =
{"license" => {"bom-ref" => "1.5", "acknowledgement" => "1.6"}, "expression" => {"bom-ref" => "1.5", "acknowledgement" => "1.6"}, "composition" => {"bom-ref" => "1.5"}} -
Fields that are attributes in XML rather than elements, by element name.
Class Method Summary
-
.each_violation(bom, & : Violation -> ) : Nil
Yields a
Violationfor everything the gate has to strip, drop or rewrite to makebomvalid at its declaredspecVersion. -
.filter_json(json : String, spec_version : String) : String
String-in/string-out convenience wrapper around
.filter_json_any. -
.filter_json_any(json : String, spec_version : String, violations : Array(Violation) | Nil = nil) : JSON::Any
Returns a copy of
json(a serialized CycloneDX BOM document) filtered down to whatspec_versionpermits. -
.filter_xml(xml : String, spec_version : String) : String
Returns a copy of
xmlwith elements, attributes and enum values newer thanspec_versionremoved or downgraded. -
.newer?(field_version : String, spec_version : String) : Bool
Returns true when
field_versionis newer than the declaredspec_version(i.e. -
.violations(bom) : Array(Violation)
Collects all spec-version violations on
bom.
Class Method Detail
Yields a Violation for everything the gate has to strip, drop or rewrite
to make bom valid at its declared specVersion. Used by Validator.
String-in/string-out convenience wrapper around .filter_json_any.
Returns a copy of json (a serialized CycloneDX BOM document) filtered
down to what spec_version permits. When .violations is given, every
edit the filter makes is appended to it.
Returns a copy of xml with elements, attributes and enum values newer
than spec_version removed or downgraded.
Returns true when field_version is newer than the declared
spec_version (i.e. the field must be stripped / flagged).
Collects all spec-version violations on bom.
This runs the JSON filter over the pre-gate document and reports what it changed, rather than re-deriving the rules. Anything the filter learns to handle is therefore reported automatically, and the two can never disagree.