diff --git a/src/main/java/org/cyclonedx/CycloneDxSchema.java b/src/main/java/org/cyclonedx/CycloneDxSchema.java index 1e942272a9..1ebe729ed4 100644 --- a/src/main/java/org/cyclonedx/CycloneDxSchema.java +++ b/src/main/java/org/cyclonedx/CycloneDxSchema.java @@ -64,12 +64,14 @@ public abstract class CycloneDxSchema public static final String NS_BOM_16 = "http://cyclonedx.org/schema/bom/1.6"; + public static final String NS_BOM_17 = "http://cyclonedx.org/schema/bom/1.7"; + @Deprecated public static final String NS_DEPENDENCY_GRAPH_10 = "http://cyclonedx.org/schema/ext/dependency-graph/1.0"; - public static final String NS_BOM_LATEST = NS_BOM_16; + public static final String NS_BOM_LATEST = NS_BOM_17; - public static final Version VERSION_LATEST = Version.VERSION_16; + public static final Version VERSION_LATEST = Version.VERSION_17; public static final List ALL_VERSIONS = Arrays.asList(Version.values()); @@ -104,6 +106,10 @@ public JsonSchema getJsonSchema(Version schemaVersion, final ObjectMapper mapper getClass().getClassLoader().getResource("bom-1.5.schema.json").toExternalForm()); offlineMappings.put("http://cyclonedx.org/schema/bom-1.6.schema.json", getClass().getClassLoader().getResource("bom-1.6.schema.json").toExternalForm()); + offlineMappings.put("http://cyclonedx.org/schema/bom-1.7.schema.json", + getClass().getClassLoader().getResource("bom-1.7.schema.json").toExternalForm()); + offlineMappings.put("http://cyclonedx.org/schema/cryptography-defs.schema.json", + getClass().getClassLoader().getResource("cryptography-defs.schema.json").toExternalForm()); JsonNode schemaNode = mapper.readTree(spdxInstream); final MapSchemaMapper offlineSchemaMapper = new MapSchemaMapper(offlineMappings); @@ -127,9 +133,12 @@ else if (Version.VERSION_14 == schemaVersion) { else if(Version.VERSION_15 == schemaVersion){ return this.getClass().getClassLoader().getResourceAsStream("bom-1.5.schema.json"); } - else { + else if(Version.VERSION_16 == schemaVersion){ return this.getClass().getClassLoader().getResourceAsStream("bom-1.6.schema.json"); } + else { + return this.getClass().getClassLoader().getResourceAsStream("bom-1.7.schema.json"); + } } /** @@ -159,9 +168,12 @@ else if (Version.VERSION_14 == schemaVersion) { else if (Version.VERSION_15 == schemaVersion) { return getXmlSchema15(); } - else { + else if (Version.VERSION_16 == schemaVersion) { return getXmlSchema16(); } + else { + return getXmlSchema17(); + } } /** @@ -269,6 +281,21 @@ private Schema getXmlSchema16() throws SAXException { ); } + /** + * Returns the CycloneDX XML Schema from the specifications XSD. + * + * @return a Schema + * @throws SAXException a SAXException + * @since 10.0.0 + */ + private Schema getXmlSchema17() throws SAXException { + // Use local copies of schemas rather than resolving from the net. It's faster, and less prone to errors. + return getXmlSchema( + this.getClass().getClassLoader().getResourceAsStream("spdx.xsd"), + this.getClass().getClassLoader().getResourceAsStream("bom-1.7.xsd") + ); + } + public Schema getXmlSchema(InputStream... inputStreams) throws SAXException { final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); schemaFactory.setProperty(XMLConstants.ACCESS_EXTERNAL_DTD, ""); diff --git a/src/main/java/org/cyclonedx/Version.java b/src/main/java/org/cyclonedx/Version.java index 5452c43982..bcfd20b8a9 100644 --- a/src/main/java/org/cyclonedx/Version.java +++ b/src/main/java/org/cyclonedx/Version.java @@ -12,7 +12,8 @@ public enum Version VERSION_13(CycloneDxSchema.NS_BOM_13, "1.3", 1.3, EnumSet.of(XML, JSON)), VERSION_14(CycloneDxSchema.NS_BOM_14, "1.4", 1.4, EnumSet.of(XML, JSON)), VERSION_15(CycloneDxSchema.NS_BOM_15, "1.5", 1.5, EnumSet.of(XML, JSON)), - VERSION_16(CycloneDxSchema.NS_BOM_16, "1.6", 1.6, EnumSet.of(XML, JSON)); + VERSION_16(CycloneDxSchema.NS_BOM_16, "1.6", 1.6, EnumSet.of(XML, JSON)), + VERSION_17(CycloneDxSchema.NS_BOM_17, "1.7", 1.7, EnumSet.of(XML, JSON)); private final String namespace; @@ -61,6 +62,7 @@ public static Version fromVersionString(String versionString) { case "1.4": return VERSION_14; case "1.5": return VERSION_15; case "1.6": return VERSION_16; + case "1.7": return VERSION_17; } } return null; diff --git a/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java b/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java index 97a672218b..a5e0b200ce 100644 --- a/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java +++ b/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java @@ -64,7 +64,7 @@ protected void setupObjectMapper(boolean isXml) { mapper.registerModule(licenseModule); SimpleModule lifecycleModule = new SimpleModule(); - lifecycleModule.addSerializer(new LifecycleSerializer(isXml)); + lifecycleModule.addSerializer(new LifecycleSerializer(isXml, version)); mapper.registerModule(lifecycleModule); SimpleModule metadataModule = new SimpleModule(); @@ -76,11 +76,11 @@ protected void setupObjectMapper(boolean isXml) { mapper.registerModule(vulnerabilityModule); SimpleModule inputTypeModule = new SimpleModule(); - inputTypeModule.addSerializer(new InputTypeSerializer(isXml)); + inputTypeModule.addSerializer(new InputTypeSerializer(isXml, version)); mapper.registerModule(inputTypeModule); SimpleModule outputTypeModule = new SimpleModule(); - outputTypeModule.addSerializer(new OutputTypeSerializer(isXml)); + outputTypeModule.addSerializer(new OutputTypeSerializer(isXml, version)); mapper.registerModule(outputTypeModule); SimpleModule evidenceModule = new SimpleModule(); diff --git a/src/main/java/org/cyclonedx/model/Bom.java b/src/main/java/org/cyclonedx/model/Bom.java index 57327c8b67..38eee02157 100644 --- a/src/main/java/org/cyclonedx/model/Bom.java +++ b/src/main/java/org/cyclonedx/model/Bom.java @@ -59,6 +59,7 @@ "formulation", "declarations", "definitions", + "citations", "signature" }) public class Bom extends ExtensibleElement { @@ -100,6 +101,9 @@ public class Bom extends ExtensibleElement { @VersionFilter(Version.VERSION_15) private List annotations; + @VersionFilter(Version.VERSION_17) + private List citations; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private List properties; @@ -239,6 +243,24 @@ public void setAnnotations(List annotations) { this.annotations = annotations; } + @JacksonXmlElementWrapper(localName = "citations") + @JacksonXmlProperty(localName = "citation") + @VersionFilter(Version.VERSION_17) + public List getCitations() { + return citations; + } + + public void setCitations(List citations) { + this.citations = citations; + } + + public void addCitation(Citation citation) { + if (this.citations == null) { + this.citations = new ArrayList<>(); + } + this.citations.add(citation); + } + @JacksonXmlElementWrapper(localName = "properties") @JacksonXmlProperty(localName = "property") @JsonInclude(JsonInclude.Include.NON_EMPTY) diff --git a/src/main/java/org/cyclonedx/model/Citation.java b/src/main/java/org/cyclonedx/model/Citation.java new file mode 100644 index 0000000000..5ebebfe597 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/Citation.java @@ -0,0 +1,140 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.util.serializer.CustomDateSerializer; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * A citation indicates which entity supplied information for specific fields within the BOM. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({"bom-ref", "pointers", "expressions", "timestamp", "attributedTo", "process", "note"}) +public class Citation extends ExtensibleElement { + + @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") + @JsonProperty("bom-ref") + private String bomRef; + + @JacksonXmlElementWrapper(localName = "pointers") + @JacksonXmlProperty(localName = "pointer") + private List pointers; + + @JacksonXmlElementWrapper(localName = "expressions") + @JacksonXmlProperty(localName = "expression") + private List expressions; + + @JsonSerialize(using = CustomDateSerializer.class) + private Date timestamp; + + private String attributedTo; + + private String process; + + private String note; + + public String getBomRef() { + return bomRef; + } + + public void setBomRef(String bomRef) { + this.bomRef = bomRef; + } + + public List getPointers() { + return pointers; + } + + public void setPointers(List pointers) { + this.pointers = pointers; + } + + public List getExpressions() { + return expressions; + } + + public void setExpressions(List expressions) { + this.expressions = expressions; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public String getAttributedTo() { + return attributedTo; + } + + public void setAttributedTo(String attributedTo) { + this.attributedTo = attributedTo; + } + + public String getProcess() { + return process; + } + + public void setProcess(String process) { + this.process = process; + } + + public String getNote() { + return note; + } + + public void setNote(String note) { + this.note = note; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Citation)) return false; + Citation citation = (Citation) o; + return Objects.equals(bomRef, citation.bomRef) && + Objects.equals(pointers, citation.pointers) && + Objects.equals(expressions, citation.expressions) && + Objects.equals(timestamp, citation.timestamp) && + Objects.equals(attributedTo, citation.attributedTo) && + Objects.equals(process, citation.process) && + Objects.equals(note, citation.note); + } + + @Override + public int hashCode() { + return Objects.hash(bomRef, pointers, expressions, timestamp, attributedTo, process, note); + } +} diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index 16c4718415..c450edcd71 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -43,6 +43,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import com.github.packageurl.PackageURL; +import org.cyclonedx.util.deserializer.LicenseChoiceDeserializer; import org.cyclonedx.util.deserializer.LicenseDeserializer; import org.cyclonedx.util.deserializer.PropertiesDeserializer; @@ -53,7 +54,9 @@ @JsonPropertyOrder( { "type", + "mime-type", "bom-ref", + "isExternal", "supplier", "manufacturer", "authors", @@ -62,11 +65,13 @@ "group", "name", "version", + "versionRange", "description", "scope", "hashes", "licenses", "copyright", + "patentAssertions", "cpe", "purl", "omniborId", @@ -82,6 +87,7 @@ "modelCard", "data", "cryptoProperties", + "tags", "signature", "provides" }) @@ -94,22 +100,29 @@ public enum Type { FRAMEWORK("framework"), @JsonProperty("library") LIBRARY("library"), + @VersionFilter(Version.VERSION_12) @JsonProperty("container") CONTAINER("container"), + @VersionFilter(Version.VERSION_15) @JsonProperty("platform") PLATFORM("platform"), @JsonProperty("operating-system") OPERATING_SYSTEM("operating-system"), @JsonProperty("device") DEVICE("device"), + @VersionFilter(Version.VERSION_15) @JsonProperty("device-driver") DEVICE_DRIVER("device-driver"), + @VersionFilter(Version.VERSION_12) @JsonProperty("firmware") FIRMWARE("firmware"), + @VersionFilter(Version.VERSION_11) @JsonProperty("file") FILE("file"), + @VersionFilter(Version.VERSION_15) @JsonProperty("machine-learning-model") MACHINE_LEARNING_MODEL("machine-learning-model"), + @VersionFilter(Version.VERSION_15) @JsonProperty("data") DATA("data"), @VersionFilter(Version.VERSION_16) @@ -132,6 +145,7 @@ public enum Scope { REQUIRED("required"), @JsonProperty("optional") OPTIONAL("optional"), + @VersionFilter(Version.VERSION_12) @JsonProperty("excluded") EXCLUDED("excluded"); @@ -158,6 +172,11 @@ public String getScopeName() { @JacksonXmlProperty(isAttribute = true) private Type type; + @JacksonXmlProperty(isAttribute = true, localName = "isExternal") + @JsonProperty("isExternal") + @VersionFilter(Version.VERSION_17) + private Boolean isExternal; + @VersionFilter(Version.VERSION_12) private OrganizationalEntity supplier; @@ -165,11 +184,14 @@ public String getScopeName() { @VersionFilter(Version.VERSION_12) private String author; - @VersionFilter(Version.VERSION_11) private String publisher; private String group; private String name; private String version; + + @VersionFilter(Version.VERSION_17) + private String versionRange; + private String description; private Scope scope; private List hashes; @@ -220,6 +242,9 @@ public String getScopeName() { @JsonProperty("provides") private List provides; + @VersionFilter(Version.VERSION_17) + private List patentAssertions; + @VersionFilter(Version.VERSION_16) @JsonUnwrapped private Tags tags; @@ -315,6 +340,24 @@ public void setVersion(String version) { this.version = version; } + @VersionFilter(Version.VERSION_17) + public String getVersionRange() { + return versionRange; + } + + public void setVersionRange(String versionRange) { + this.versionRange = versionRange; + } + + @VersionFilter(Version.VERSION_17) + public Boolean getIsExternal() { + return isExternal; + } + + public void setIsExternal(Boolean isExternal) { + this.isExternal = isExternal; + } + public String getDescription() { return description; } @@ -350,11 +393,15 @@ public void addHash(Hash hash) { } @JsonDeserialize(using = LicenseDeserializer.class) + @JacksonXmlElementWrapper (useWrapping = false) public LicenseChoice getLicenses() { + if (licenses != null && (licenses.getItems() == null || licenses.getItems().isEmpty())) { + return null; + } return licenses; } - @JacksonXmlElementWrapper (useWrapping = false) + @JsonDeserialize(using = LicenseChoiceDeserializer.class) public void setLicenses(LicenseChoice licenses) { this.licenses = licenses; } @@ -565,6 +612,17 @@ public void setProvides(final List provides) { this.provides = provides; } + @JacksonXmlElementWrapper(localName = "patentAssertions") + @JacksonXmlProperty(localName = "patentAssertion") + @VersionFilter(Version.VERSION_17) + public List getPatentAssertions() { + return patentAssertions; + } + + public void setPatentAssertions(List patentAssertions) { + this.patentAssertions = patentAssertions; + } + public Tags getTags() { return tags; } @@ -603,10 +661,49 @@ public void setManufacturer(final OrganizationalEntity manufacturer) { this.manufacturer = manufacturer; } + /** + * Validates that the component conforms to CycloneDX 1.7 choice group constraints. + * Specifically: + * - version and versionRange are mutually exclusive (xs:choice group) + * - versionRange should have isExternal=true + * + * @throws IllegalStateException if validation fails + */ + public void validate() { + validateVersionChoice(); + validateVersionRangeRequirements(); + } + + /** + * Validates that version and versionRange are not both set (xs:choice constraint) + */ + private void validateVersionChoice() { + if (version != null && versionRange != null) { + throw new IllegalStateException( + "Component cannot have both 'version' and 'versionRange' set. " + + "These fields are mutually exclusive per CycloneDX 1.7 schema (xs:choice group). " + + "Component: " + (name != null ? name : "unknown")); + } + } + + /** + * Validates that versionRange is used correctly with isExternal. + * Note: This is a warning-level validation. versionRange typically requires isExternal=true, + * but we don't enforce it as a hard error to allow flexibility. + */ + private void validateVersionRangeRequirements() { + if (versionRange != null && !Boolean.TRUE.equals(isExternal)) { + // This is informational - in 1.7, versionRange is typically used with isExternal=true + // to indicate external components with version ranges rather than fixed versions. + // We don't throw an exception here, just allow it through with this note. + } + } + @Override public int hashCode() { return Objects.hash(author, publisher, group, name, version, description, scope, hashes, licenses, copyright, - cpe, purl, omniborId, swhid, swid, modified, components, evidence, releaseNotes, type, modelCard, data); + cpe, purl, omniborId, swhid, swid, modified, components, evidence, releaseNotes, type, modelCard, data, + isExternal, versionRange, patentAssertions, tags); } @Override @@ -637,6 +734,10 @@ public boolean equals(Object o) { Objects.equals(releaseNotes, component.releaseNotes) && Objects.equals(data, component.data) && Objects.equals(modelCard, component.modelCard) && - Objects.equals(type, component.type); + Objects.equals(type, component.type) && + Objects.equals(isExternal, component.isExternal) && + Objects.equals(versionRange, component.versionRange) && + Objects.equals(patentAssertions, component.patentAssertions) && + Objects.equals(tags, component.tags); } } diff --git a/src/main/java/org/cyclonedx/model/Composition.java b/src/main/java/org/cyclonedx/model/Composition.java index ce611f4db9..01397410e6 100644 --- a/src/main/java/org/cyclonedx/model/Composition.java +++ b/src/main/java/org/cyclonedx/model/Composition.java @@ -43,14 +43,18 @@ public enum Aggregate { INCOMPLETE("incomplete"), @JsonProperty("incomplete_first_party_only") INCOMPLETE_FIRST_PARTY_ONLY("incomplete_first_party_only"), + @VersionFilter(Version.VERSION_15) @JsonProperty("incomplete_first_party_proprietary_only") INCOMPLETE_FIRST_PARTY_PROPRIETARY_ONLY("incomplete_first_party_proprietary_only"), + @VersionFilter(Version.VERSION_15) @JsonProperty("incomplete_first_party_opensource_only") INCOMPLETE_FIRST_PARTY_OPENSOURCE_ONLY("incomplete_first_party_opensource_only"), @JsonProperty("incomplete_third_party_only") INCOMPLETE_THIRD_PARTY_ONLY("incomplete_third_party_only"), + @VersionFilter(Version.VERSION_15) @JsonProperty("incomplete_third_party_proprietary_only") INCOMPLETE_THIRD_PARTY_PROPRIETARY_ONLY("incomplete_third_party_proprietary_only"), + @VersionFilter(Version.VERSION_15) @JsonProperty("incomplete_third_party_opensource_only") INCOMPLETE_THIRD_PARTY_OPENSOURCE_ONLY("incomplete_third_party_opensource_only"), @JsonProperty("unknown") diff --git a/src/main/java/org/cyclonedx/model/DistributionConstraints.java b/src/main/java/org/cyclonedx/model/DistributionConstraints.java new file mode 100644 index 0000000000..dda1ef4276 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/DistributionConstraints.java @@ -0,0 +1,60 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +import java.util.Objects; + +/** + * Distribution constraints and sharing controls for the BOM. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({"tlp"}) +public class DistributionConstraints { + + private TlpClassification tlp; + + public TlpClassification getTlp() { + return tlp; + } + + public void setTlp(TlpClassification tlp) { + this.tlp = tlp; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof DistributionConstraints)) return false; + DistributionConstraints that = (DistributionConstraints) o; + return tlp == that.tlp; + } + + @Override + public int hashCode() { + return Objects.hash(tlp); + } +} diff --git a/src/main/java/org/cyclonedx/model/ExternalReference.java b/src/main/java/org/cyclonedx/model/ExternalReference.java index 816d90aba8..9aa0802020 100644 --- a/src/main/java/org/cyclonedx/model/ExternalReference.java +++ b/src/main/java/org/cyclonedx/model/ExternalReference.java @@ -32,7 +32,7 @@ @SuppressWarnings("unused") @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) -@JsonPropertyOrder({"url", "comment", "hashes"}) +@JsonPropertyOrder({"url", "comment", "hashes", "properties"}) public class ExternalReference { public enum Type { @@ -56,10 +56,12 @@ public enum Type { DOCUMENTATION("documentation"), @JsonProperty("support") SUPPORT("support"), + @VersionFilter(Version.VERSION_16) @JsonProperty("source-distribution") SOURCE_DISTRIBUTION("source-distribution"), @JsonProperty("distribution") DISTRIBUTION("distribution"), + @VersionFilter(Version.VERSION_15) @JsonProperty("distribution-intake") DISTRIBUTION_INTAKE("distribution-intake"), @JsonProperty("license") @@ -68,51 +70,73 @@ public enum Type { BUILD_META("build-meta"), @JsonProperty("build-system") BUILD_SYSTEM("build-system"), + @VersionFilter(Version.VERSION_14) @JsonProperty("release-notes") RELEASE_NOTES("release-notes"), @VersionFilter(Version.VERSION_15) @JsonProperty("security-contact") SECURITY_CONTACT("security-contact"), - @JsonProperty("model_card") - MODEL_CARD("model_card"), + @VersionFilter(Version.VERSION_15) + @JsonProperty("model-card") + MODEL_CARD("model-card"), + @VersionFilter(Version.VERSION_15) @JsonProperty("attestation") ATTESTATION("attestation"), + @VersionFilter(Version.VERSION_15) @JsonProperty("threat-model") THREAT_MODEL("threat-model"), + @VersionFilter(Version.VERSION_15) @JsonProperty("adversary-model") ADVERSARY_MODEL("adversary-model"), + @VersionFilter(Version.VERSION_15) @JsonProperty("risk-assessment") RISK_ASSESSMENT("risk-assessment"), + @VersionFilter(Version.VERSION_15) @JsonProperty("vulnerability-assertion") VULNERABILITY_ASSERTION("vulnerability-assertion"), + @VersionFilter(Version.VERSION_15) @JsonProperty("exploitability-statement") EXPLOITABILITY_STATEMENT("exploitability-statement"), + @VersionFilter(Version.VERSION_15) @JsonProperty("pentest-report") PENTEST_REPORT("pentest-report"), + @VersionFilter(Version.VERSION_15) @JsonProperty("static-analysis-report") STATIC_ANALYSIS_REPORT("static-analysis-report"), + @VersionFilter(Version.VERSION_15) @JsonProperty("dynamic-analysis-report") DYNAMIC_ANALYSIS_REPORT("dynamic-analysis-report"), + @VersionFilter(Version.VERSION_15) @JsonProperty("runtime-analysis-report") RUNTIME_ANALYSIS_REPORT("runtime-analysis-report"), + @VersionFilter(Version.VERSION_15) @JsonProperty("component-analysis-report") COMPONENT_ANALYSIS_REPORT("component-analysis-report"), + @VersionFilter(Version.VERSION_15) @JsonProperty("maturity-report") MATURITY_REPORT("maturity-report"), + @VersionFilter(Version.VERSION_15) @JsonProperty("certification-report") CERTIFICATION_REPORT("certification-report"), + @VersionFilter(Version.VERSION_15) @JsonProperty("codified-infrastructure") CODIFIED_INFRASTRUCTURE("codified-infrastructure"), + @VersionFilter(Version.VERSION_15) @JsonProperty("quality-metrics") QUALITY_METRICS("quality-metrics"), + @VersionFilter(Version.VERSION_15) @JsonProperty("log") LOG("log"), + @VersionFilter(Version.VERSION_15) @JsonProperty("configuration") CONFIGURATION("configuration"), + @VersionFilter(Version.VERSION_15) @JsonProperty("evidence") EVIDENCE("evidence"), + @VersionFilter(Version.VERSION_15) @JsonProperty("formulation") FORMULATION("formulation"), + @VersionFilter(Version.VERSION_16) @JsonProperty("rfc-9116") RFC_9116("rfc-9116"), @VersionFilter(Version.VERSION_16) @@ -121,6 +145,21 @@ public enum Type { @VersionFilter(Version.VERSION_16) @JsonProperty("digital-signature") DIGITAL_SIGNATURE("digital-signature"), + @VersionFilter(Version.VERSION_17) + @JsonProperty("patent") + PATENT("patent"), + @VersionFilter(Version.VERSION_17) + @JsonProperty("patent-family") + PATENT_FAMILY("patent-family"), + @VersionFilter(Version.VERSION_17) + @JsonProperty("patent-assertion") + PATENT_ASSERTION("patent-assertion"), + @VersionFilter(Version.VERSION_17) + @JsonProperty("citation") + CITATION("citation"), + @VersionFilter(Version.VERSION_15) + @JsonProperty("poam") + POAM("poam"), @JsonProperty("other") OTHER("other"); @@ -152,6 +191,9 @@ public static Type fromString(String text) { @VersionFilter(Version.VERSION_13) private List hashes; + @VersionFilter(Version.VERSION_17) + private List properties; + public String getUrl() { return url; } @@ -194,6 +236,17 @@ public void addHash(Hash hash) { this.hashes.add(hash); } + @JacksonXmlElementWrapper(localName = "properties") + @JacksonXmlProperty(localName = "property") + @VersionFilter(Version.VERSION_17) + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -202,11 +255,12 @@ public boolean equals(Object o) { return Objects.equals(url, reference.url) && type == reference.type && Objects.equals(comment, reference.comment) && - Objects.equals(hashes, reference.hashes); + Objects.equals(hashes, reference.hashes) && + Objects.equals(properties, reference.properties); } @Override public int hashCode() { - return Objects.hash(url, type, comment, hashes); + return Objects.hash(url, type, comment, hashes, properties); } } diff --git a/src/main/java/org/cyclonedx/model/LicenseChoice.java b/src/main/java/org/cyclonedx/model/LicenseChoice.java index c3618d095f..a019e4768b 100644 --- a/src/main/java/org/cyclonedx/model/LicenseChoice.java +++ b/src/main/java/org/cyclonedx/model/LicenseChoice.java @@ -21,49 +21,191 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; -import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.Version; import org.cyclonedx.model.license.Expression; -import org.cyclonedx.util.deserializer.LicenseDeserializer; +import org.cyclonedx.model.license.ExpressionDetailed; +import org.cyclonedx.util.deserializer.LicenseChoiceDeserializer; +/** + * Represents a choice of licenses for a component or service. + * In CycloneDX 1.7+, this implements an item-level choice model where an array + * can contain a mix of License, Expression, and ExpressionDetailed objects. + * For earlier versions (1.6 and below), this enforces an array-level choice where + * the entire array must be either all licenses, or a single expression, or a single + * detailed expression. + * + * @since 9.0.0 + */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) -@JsonDeserialize(using = LicenseDeserializer.class) +@JsonDeserialize(using = LicenseChoiceDeserializer.class) public class LicenseChoice { - @JacksonXmlElementWrapper(useWrapping = false) - private List license; - private Expression expression; + private List items; - @JacksonXmlProperty(localName = "license") - public List getLicenses() { - return license; + /** + * Gets the list of license items. Each item can be a License, Expression, or ExpressionDetailed. + * This is the primary API for CycloneDX 1.7+ support. + */ + public List getItems() { + return items; } - public void setLicenses(List licenses) { - this.license = licenses; - this.expression = null; + public void setItems(List items) { + this.items = items; } + /** + * Adds a license item to the choice + */ + public void addItem(LicenseItem item) { + if (this.items == null) { + this.items = new ArrayList<>(); + } + this.items.add(item); + } + + /** + * Convenience method to add a License + */ public void addLicense(License license) { - if (this.license == null) { - this.license = new ArrayList<>(); + addItem(LicenseItem.ofLicense(license)); + } + + /** + * Convenience method to add an Expression + */ + public void addExpression(Expression expression) { + addItem(LicenseItem.ofExpression(expression)); + } + + /** + * Convenience method to add an ExpressionDetailed + */ + public void addExpressionDetailed(ExpressionDetailed expressionDetailed) { + addItem(LicenseItem.ofExpressionDetailed(expressionDetailed)); + } + + // ========== Backward Compatibility Methods ========== + + /** + * @deprecated Use {@link #getItems()} and filter by type instead. + * Returns only License items for backward compatibility with pre-1.7 API. + */ + @Deprecated + @JsonIgnore + public List getLicenses() { + if (items == null) return null; + List licenses = items.stream() + .filter(item -> item.getLicense() != null) + .map(LicenseItem::getLicense) + .collect(Collectors.toList()); + return licenses.isEmpty() ? null : licenses; + } + + /** + * @deprecated Use {@link #setItems(List)} with LicenseItem.ofLicense() instead. + * Sets licenses, clearing all other items. For backward compatibility with pre-1.7 API. + */ + @Deprecated + public void setLicenses(List licenses) { + if (licenses != null && !licenses.isEmpty()) { + this.items = new ArrayList<>(); + for (License license : licenses) { + this.items.add(LicenseItem.ofLicense(license)); + } + } else { + this.items = null; } - this.license.add(license); - this.expression = null; } - @JacksonXmlProperty(localName = "expression") + /** + * @deprecated Use {@link #getItems()} and filter by type instead. + * Returns the first Expression item for backward compatibility with pre-1.7 API. + */ + @Deprecated + @JsonIgnore public Expression getExpression() { - return expression; + if (items == null) return null; + return items.stream() + .filter(item -> item.getExpression() != null) + .map(LicenseItem::getExpression) + .findFirst() + .orElse(null); } + /** + * @deprecated Use {@link #setItems(List)} with LicenseItem.ofExpression() instead. + * Sets a single expression, clearing all other items. For backward compatibility with pre-1.7 API. + */ + @Deprecated public void setExpression(Expression expression) { - this.expression = expression; - this.license = null; + if (expression != null) { + this.items = new ArrayList<>(); + this.items.add(LicenseItem.ofExpression(expression)); + } else { + this.items = null; + } + } + + + /** + * Returns the first ExpressionDetailed item. + * Note: This is part of the 1.7 API, not deprecated. + */ + @JsonIgnore + public ExpressionDetailed getExpressionDetailed() { + if (items == null) return null; + return items.stream() + .filter(item -> item.getExpressionDetailed() != null) + .map(LicenseItem::getExpressionDetailed) + .findFirst() + .orElse(null); + } + + /** + * Sets a single detailed expression, clearing all other items. + * Note: This is part of the 1.7 API, not deprecated. + */ + public void setExpressionDetailed(ExpressionDetailed expressionDetailed) { + if (expressionDetailed != null) { + this.items = new ArrayList<>(); + this.items.add(LicenseItem.ofExpressionDetailed(expressionDetailed)); + } else { + this.items = null; + } + } + + /** + * Validates whether this choice conforms to a specific version's constraints. + * For 1.6 and below: array-level choice (all licenses OR single expression OR single expressionDetailed) + * For 1.7+: item-level choice (mix allowed) + */ + public boolean isValidForVersion(Version version) { + if (items == null || items.isEmpty()) { + return true; + } + + if (version.getVersion() >= Version.VERSION_17.getVersion()) { + // 1.7+: All items must be valid + return items.stream().allMatch(LicenseItem::isValid); + } else { + // 1.6 and below: Array-level choice + long licenseCount = items.stream().filter(i -> i.getLicense() != null).count(); + long expressionCount = items.stream().filter(i -> i.getExpression() != null).count(); + long expressionDetailedCount = items.stream().filter(i -> i.getExpressionDetailed() != null).count(); + + // Must be: all licenses, OR single expression, OR single expressionDetailed + return (licenseCount > 0 && expressionCount == 0 && expressionDetailedCount == 0) || + (licenseCount == 0 && expressionCount == 1 && expressionDetailedCount == 0) || + (licenseCount == 0 && expressionCount == 0 && expressionDetailedCount == 1); + } } @Override @@ -71,12 +213,12 @@ public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof LicenseChoice)) return false; LicenseChoice that = (LicenseChoice) o; - return Objects.equals(license, that.license) && - Objects.equals(expression, that.expression); + return Objects.equals(items, that.items); } @Override public int hashCode() { - return Objects.hash(license, expression); + return Objects.hash(items); } } + diff --git a/src/main/java/org/cyclonedx/model/LicenseItem.java b/src/main/java/org/cyclonedx/model/LicenseItem.java new file mode 100644 index 0000000000..b1b49a4ce8 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/LicenseItem.java @@ -0,0 +1,168 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model; + +import java.util.Objects; + +import org.cyclonedx.Version; +import org.cyclonedx.model.license.Expression; +import org.cyclonedx.model.license.ExpressionDetailed; + +/** + * Represents a single item in a license choice, where each item can be one of: + * - A License + * - An Expression + * - An ExpressionDetailed + * + * This implements the CycloneDX 1.7 item-level choice model, where an array can + * contain a mix of different license types. + * + * @since 9.0.0 + */ +public class LicenseItem { + + private License license; + private Expression expression; + @VersionFilter(Version.VERSION_17) + private ExpressionDetailed expressionDetailed; + + /** + * Default constructor for deserialization + */ + public LicenseItem() { + } + + /** + * Private constructor to enforce factory methods + */ + private LicenseItem(License license, Expression expression, ExpressionDetailed expressionDetailed) { + this.license = license; + this.expression = expression; + this.expressionDetailed = expressionDetailed; + } + + /** + * Creates a LicenseItem containing a License + */ + public static LicenseItem ofLicense(License license) { + if (license == null) { + throw new IllegalArgumentException("License cannot be null"); + } + return new LicenseItem(license, null, null); + } + + /** + * Creates a LicenseItem containing an Expression + */ + public static LicenseItem ofExpression(Expression expression) { + if (expression == null) { + throw new IllegalArgumentException("Expression cannot be null"); + } + return new LicenseItem(null, expression, null); + } + + /** + * Creates a LicenseItem containing an ExpressionDetailed + */ + public static LicenseItem ofExpressionDetailed(ExpressionDetailed expressionDetailed) { + if (expressionDetailed == null) { + throw new IllegalArgumentException("ExpressionDetailed cannot be null"); + } + return new LicenseItem(null, null, expressionDetailed); + } + + public License getLicense() { + return license; + } + + public void setLicense(License license) { + if (license != null) { + this.expression = null; + this.expressionDetailed = null; + } + this.license = license; + } + + public Expression getExpression() { + return expression; + } + + public void setExpression(Expression expression) { + if (expression != null) { + this.license = null; + this.expressionDetailed = null; + } + this.expression = expression; + } + + public ExpressionDetailed getExpressionDetailed() { + return expressionDetailed; + } + + public void setExpressionDetailed(ExpressionDetailed expressionDetailed) { + if (expressionDetailed != null) { + this.license = null; + this.expression = null; + } + this.expressionDetailed = expressionDetailed; + } + + /** + * Returns the type of license item + */ + public LicenseItemType getType() { + if (license != null) return LicenseItemType.LICENSE; + if (expression != null) return LicenseItemType.EXPRESSION; + if (expressionDetailed != null) return LicenseItemType.EXPRESSION_DETAILED; + return LicenseItemType.NONE; + } + + /** + * Validates that exactly one field is set + */ + public boolean isValid() { + int count = 0; + if (license != null) count++; + if (expression != null) count++; + if (expressionDetailed != null) count++; + return count == 1; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof LicenseItem)) return false; + LicenseItem that = (LicenseItem) o; + return Objects.equals(license, that.license) && + Objects.equals(expression, that.expression) && + Objects.equals(expressionDetailed, that.expressionDetailed); + } + + @Override + public int hashCode() { + return Objects.hash(license, expression, expressionDetailed); + } + + public enum LicenseItemType { + LICENSE, + EXPRESSION, + EXPRESSION_DETAILED, + NONE + } +} diff --git a/src/main/java/org/cyclonedx/model/Metadata.java b/src/main/java/org/cyclonedx/model/Metadata.java index 4d57fe9192..391ffe7387 100644 --- a/src/main/java/org/cyclonedx/model/Metadata.java +++ b/src/main/java/org/cyclonedx/model/Metadata.java @@ -42,7 +42,7 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyOrder({ "timestamp", "lifecycles", "tools", "authors", "component", "manufacturer", "manufacture", "supplier", "licenses", - "properties" + "properties", "distributionConstraints" }) @JsonDeserialize(using = MetadataDeserializer.class) public class Metadata @@ -92,6 +92,9 @@ public class Metadata @VersionFilter(Version.VERSION_13) private List properties; + @VersionFilter(Version.VERSION_17) + private DistributionConstraints distributionConstraints; + public Date getTimestamp() { return timestamp; } @@ -199,6 +202,15 @@ public void addProperty(Property property) { this.properties.add(property); } + @VersionFilter(Version.VERSION_17) + public DistributionConstraints getDistributionConstraints() { + return distributionConstraints; + } + + public void setDistributionConstraints(DistributionConstraints distributionConstraints) { + this.distributionConstraints = distributionConstraints; + } + public Lifecycles getLifecycles() { return lifecycles; } diff --git a/src/main/java/org/cyclonedx/model/OrganizationalChoice.java b/src/main/java/org/cyclonedx/model/OrganizationalChoice.java index 6bc2f0eaa8..6e5a6e054c 100644 --- a/src/main/java/org/cyclonedx/model/OrganizationalChoice.java +++ b/src/main/java/org/cyclonedx/model/OrganizationalChoice.java @@ -22,9 +22,12 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import org.cyclonedx.util.deserializer.OrganizationalChoiceDeserializer; @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonDeserialize(using = OrganizationalChoiceDeserializer.class) public class OrganizationalChoice { private OrganizationalContact individual; diff --git a/src/main/java/org/cyclonedx/model/Patent.java b/src/main/java/org/cyclonedx/model/Patent.java new file mode 100644 index 0000000000..20c0523fe7 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/Patent.java @@ -0,0 +1,273 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.util.serializer.OrganizationalChoiceSerializer; + +import java.util.Date; +import java.util.List; +import java.util.Objects; + +/** + * Patent information. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({ + "bomRef", "patentNumber", "applicationNumber", "jurisdiction", "priorityApplication", + "publicationNumber", "title", "abstract", "filingDate", "grantDate", "patentExpirationDate", + "patentLegalStatus", "patentAssignee", "externalReferences" +}) +public class Patent extends ExtensibleElement { + + public enum PatentLegalStatus { + @JsonProperty("pending") + PENDING("pending"), + @JsonProperty("granted") + GRANTED("granted"), + @JsonProperty("expired") + EXPIRED("expired"), + @JsonProperty("abandoned") + ABANDONED("abandoned"), + @JsonProperty("revoked") + REVOKED("revoked"), + @JsonProperty("withdrawn") + WITHDRAWN("withdrawn"), + @JsonProperty("lapsed") + LAPSED("lapsed"), + @JsonProperty("suspended") + SUSPENDED("suspended"), + @JsonProperty("reinstated") + REINSTATED("reinstated"), + @JsonProperty("opposed") + OPPOSED("opposed"), + @JsonProperty("terminated") + TERMINATED("terminated"), + @JsonProperty("invalidated") + INVALIDATED("invalidated"), + @JsonProperty("in-force") + IN_FORCE("in-force"); + + private final String value; + + PatentLegalStatus(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static PatentLegalStatus fromValue(String value) { + if (value != null) { + for (PatentLegalStatus status : values()) { + if (status.value.equalsIgnoreCase(value)) { + return status; + } + } + } + return null; + } + } + + @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") + @JsonProperty("bom-ref") + private String bomRef; + + private String patentNumber; + private String applicationNumber; + private String jurisdiction; + private PriorityApplication priorityApplication; + private String publicationNumber; + private String title; + + @JsonProperty("abstract") + private String patentAbstract; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC") + private Date filingDate; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC") + private Date grantDate; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC") + private Date patentExpirationDate; + + private PatentLegalStatus patentLegalStatus; + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "patentAssignee") + @JsonSerialize(contentUsing = OrganizationalChoiceSerializer.class) + private List patentAssignee; + + @JacksonXmlElementWrapper(localName = "externalReferences") + @JacksonXmlProperty(localName = "reference") + private List externalReferences; + + public String getBomRef() { + return bomRef; + } + + public void setBomRef(String bomRef) { + this.bomRef = bomRef; + } + + public String getPatentNumber() { + return patentNumber; + } + + public void setPatentNumber(String patentNumber) { + this.patentNumber = patentNumber; + } + + public String getApplicationNumber() { + return applicationNumber; + } + + public void setApplicationNumber(String applicationNumber) { + this.applicationNumber = applicationNumber; + } + + public String getJurisdiction() { + return jurisdiction; + } + + public void setJurisdiction(String jurisdiction) { + this.jurisdiction = jurisdiction; + } + + public PriorityApplication getPriorityApplication() { + return priorityApplication; + } + + public void setPriorityApplication(PriorityApplication priorityApplication) { + this.priorityApplication = priorityApplication; + } + + public String getPublicationNumber() { + return publicationNumber; + } + + public void setPublicationNumber(String publicationNumber) { + this.publicationNumber = publicationNumber; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getPatentAbstract() { + return patentAbstract; + } + + public void setPatentAbstract(String patentAbstract) { + this.patentAbstract = patentAbstract; + } + + public Date getFilingDate() { + return filingDate; + } + + public void setFilingDate(Date filingDate) { + this.filingDate = filingDate; + } + + public Date getGrantDate() { + return grantDate; + } + + public void setGrantDate(Date grantDate) { + this.grantDate = grantDate; + } + + public Date getPatentExpirationDate() { + return patentExpirationDate; + } + + public void setPatentExpirationDate(Date patentExpirationDate) { + this.patentExpirationDate = patentExpirationDate; + } + + public PatentLegalStatus getPatentLegalStatus() { + return patentLegalStatus; + } + + public void setPatentLegalStatus(PatentLegalStatus patentLegalStatus) { + this.patentLegalStatus = patentLegalStatus; + } + + public List getPatentAssignee() { + return patentAssignee; + } + + public void setPatentAssignee(List patentAssignee) { + this.patentAssignee = patentAssignee; + } + + public List getExternalReferences() { + return externalReferences; + } + + public void setExternalReferences(List externalReferences) { + this.externalReferences = externalReferences; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Patent)) return false; + Patent patent = (Patent) o; + return Objects.equals(bomRef, patent.bomRef) && + Objects.equals(patentNumber, patent.patentNumber) && + Objects.equals(applicationNumber, patent.applicationNumber) && + Objects.equals(jurisdiction, patent.jurisdiction) && + Objects.equals(priorityApplication, patent.priorityApplication) && + Objects.equals(publicationNumber, patent.publicationNumber) && + Objects.equals(title, patent.title) && + Objects.equals(patentAbstract, patent.patentAbstract) && + Objects.equals(filingDate, patent.filingDate) && + Objects.equals(grantDate, patent.grantDate) && + Objects.equals(patentExpirationDate, patent.patentExpirationDate) && + patentLegalStatus == patent.patentLegalStatus && + Objects.equals(patentAssignee, patent.patentAssignee) && + Objects.equals(externalReferences, patent.externalReferences); + } + + @Override + public int hashCode() { + return Objects.hash(bomRef, patentNumber, applicationNumber, jurisdiction, priorityApplication, + publicationNumber, title, patentAbstract, filingDate, grantDate, patentExpirationDate, + patentLegalStatus, patentAssignee, externalReferences); + } +} diff --git a/src/main/java/org/cyclonedx/model/PatentAssertion.java b/src/main/java/org/cyclonedx/model/PatentAssertion.java new file mode 100644 index 0000000000..01a41bd4f0 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/PatentAssertion.java @@ -0,0 +1,163 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.util.deserializer.PatentAssertionDeserializer; +import org.cyclonedx.util.serializer.PatentAssertionSerializer; + +import java.util.List; +import java.util.Objects; + +/** + * A patent assertion represents a claim about patent ownership or licensing. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({"bom-ref", "assertionType", "patentRefs", "asserter", "notes"}) +@JsonDeserialize(using = PatentAssertionDeserializer.class) +@JsonSerialize(using = PatentAssertionSerializer.class) +public class PatentAssertion extends ExtensibleElement { + + public enum AssertionType { + @JsonProperty("ownership") + OWNERSHIP("ownership"), + @JsonProperty("license") + LICENSE("license"), + @JsonProperty("third-party-claim") + THIRD_PARTY_CLAIM("third-party-claim"), + @JsonProperty("standards-inclusion") + STANDARDS_INCLUSION("standards-inclusion"), + @JsonProperty("prior-art") + PRIOR_ART("prior-art"), + @JsonProperty("exclusive-rights") + EXCLUSIVE_RIGHTS("exclusive-rights"), + @JsonProperty("non-assertion") + NON_ASSERTION("non-assertion"), + @JsonProperty("research-or-evaluation") + RESEARCH_OR_EVALUATION("research-or-evaluation"); + + private final String value; + + AssertionType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static AssertionType fromValue(String value) { + if (value != null) { + for (AssertionType type : values()) { + if (type.value.equalsIgnoreCase(value)) { + return type; + } + } + } + return null; + } + } + + @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") + @JsonProperty("bom-ref") + private String bomRef; + + private AssertionType assertionType; + + private List patentRefs; + + private OrganizationalChoice asserter; + + private String notes; + + public String getBomRef() { + return bomRef; + } + + public void setBomRef(String bomRef) { + // Guard against Jackson XML overwriting the attribute value when processing + // child elements inside (same local name conflict) + if (bomRef != null && !bomRef.isEmpty()) { + this.bomRef = bomRef; + } + } + + public AssertionType getAssertionType() { + return assertionType; + } + + public void setAssertionType(AssertionType assertionType) { + this.assertionType = assertionType; + } + + @JacksonXmlElementWrapper(localName = "patentRefs") + @JacksonXmlProperty(localName = "bom-ref") + @JsonProperty("patentRefs") + public List getPatentRefs() { + return patentRefs; + } + + public void setPatentRefs(List patentRefs) { + this.patentRefs = patentRefs; + } + + public OrganizationalChoice getAsserter() { + return asserter; + } + + public void setAsserter(OrganizationalChoice asserter) { + this.asserter = asserter; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PatentAssertion)) return false; + PatentAssertion that = (PatentAssertion) o; + return Objects.equals(bomRef, that.bomRef) && + assertionType == that.assertionType && + Objects.equals(patentRefs, that.patentRefs) && + Objects.equals(asserter, that.asserter) && + Objects.equals(notes, that.notes); + } + + @Override + public int hashCode() { + return Objects.hash(bomRef, assertionType, patentRefs, asserter, notes); + } +} diff --git a/src/main/java/org/cyclonedx/model/PatentFamily.java b/src/main/java/org/cyclonedx/model/PatentFamily.java new file mode 100644 index 0000000000..74e4073bba --- /dev/null +++ b/src/main/java/org/cyclonedx/model/PatentFamily.java @@ -0,0 +1,114 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; + +import java.util.List; +import java.util.Objects; + +/** + * A patent family represents a group of related patents. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({"bomRef", "familyId", "priorityApplication", "members", "externalReferences"}) +public class PatentFamily extends ExtensibleElement { + + @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") + @JsonProperty("bom-ref") + private String bomRef; + + private String familyId; + + private PriorityApplication priorityApplication; + + @JacksonXmlElementWrapper(localName = "members") + @JacksonXmlProperty(localName = "ref") + private List members; + + @JacksonXmlElementWrapper(localName = "externalReferences") + @JacksonXmlProperty(localName = "reference") + private List externalReferences; + + public String getBomRef() { + return bomRef; + } + + public void setBomRef(String bomRef) { + this.bomRef = bomRef; + } + + public String getFamilyId() { + return familyId; + } + + public void setFamilyId(String familyId) { + this.familyId = familyId; + } + + public PriorityApplication getPriorityApplication() { + return priorityApplication; + } + + public void setPriorityApplication(PriorityApplication priorityApplication) { + this.priorityApplication = priorityApplication; + } + + public List getMembers() { + return members; + } + + public void setMembers(List members) { + this.members = members; + } + + public List getExternalReferences() { + return externalReferences; + } + + public void setExternalReferences(List externalReferences) { + this.externalReferences = externalReferences; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PatentFamily)) return false; + PatentFamily that = (PatentFamily) o; + return Objects.equals(bomRef, that.bomRef) && + Objects.equals(familyId, that.familyId) && + Objects.equals(priorityApplication, that.priorityApplication) && + Objects.equals(members, that.members) && + Objects.equals(externalReferences, that.externalReferences); + } + + @Override + public int hashCode() { + return Objects.hash(bomRef, familyId, priorityApplication, members, externalReferences); + } +} diff --git a/src/main/java/org/cyclonedx/model/PatentItem.java b/src/main/java/org/cyclonedx/model/PatentItem.java new file mode 100644 index 0000000000..041fa2da15 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/PatentItem.java @@ -0,0 +1,77 @@ +package org.cyclonedx.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cyclonedx.util.deserializer.PatentItemDeserializer; +import org.cyclonedx.util.serializer.PatentItemSerializer; + +import java.util.Objects; + +/** + * Discriminated union for the polymorphic patents array in definitions. + * Can hold either a {@link Patent} or a {@link PatentFamily}. + * + * @since 10.0.0 + */ +@JsonDeserialize(using = PatentItemDeserializer.class) +@JsonSerialize(using = PatentItemSerializer.class) +public class PatentItem { + + public enum Type { + PATENT, + PATENT_FAMILY + } + + private Patent patent; + private PatentFamily patentFamily; + + private PatentItem() { + } + + public static PatentItem ofPatent(Patent patent) { + PatentItem item = new PatentItem(); + item.patent = patent; + return item; + } + + public static PatentItem ofPatentFamily(PatentFamily patentFamily) { + PatentItem item = new PatentItem(); + item.patentFamily = patentFamily; + return item; + } + + public Patent getPatent() { + return patent; + } + + public PatentFamily getPatentFamily() { + return patentFamily; + } + + @JsonIgnore + public Type getType() { + if (patent != null) return Type.PATENT; + if (patentFamily != null) return Type.PATENT_FAMILY; + return null; + } + + @JsonIgnore + public boolean isValid() { + return patent != null || patentFamily != null; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PatentItem)) return false; + PatentItem that = (PatentItem) o; + return Objects.equals(patent, that.patent) && + Objects.equals(patentFamily, that.patentFamily); + } + + @Override + public int hashCode() { + return Objects.hash(patent, patentFamily); + } +} diff --git a/src/main/java/org/cyclonedx/model/PriorityApplication.java b/src/main/java/org/cyclonedx/model/PriorityApplication.java new file mode 100644 index 0000000000..fe3b3bd038 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/PriorityApplication.java @@ -0,0 +1,84 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.annotation.JsonFormat; + +import java.util.Date; +import java.util.Objects; + +/** + * Priority application information for a patent. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({"applicationNumber", "jurisdiction", "filingDate"}) +public class PriorityApplication { + + private String applicationNumber; + private String jurisdiction; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC") + private Date filingDate; + + public String getApplicationNumber() { + return applicationNumber; + } + + public void setApplicationNumber(String applicationNumber) { + this.applicationNumber = applicationNumber; + } + + public String getJurisdiction() { + return jurisdiction; + } + + public void setJurisdiction(String jurisdiction) { + this.jurisdiction = jurisdiction; + } + + public Date getFilingDate() { + return filingDate; + } + + public void setFilingDate(Date filingDate) { + this.filingDate = filingDate; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PriorityApplication)) return false; + PriorityApplication that = (PriorityApplication) o; + return Objects.equals(applicationNumber, that.applicationNumber) && + Objects.equals(jurisdiction, that.jurisdiction) && + Objects.equals(filingDate, that.filingDate); + } + + @Override + public int hashCode() { + return Objects.hash(applicationNumber, jurisdiction, filingDate); + } +} diff --git a/src/main/java/org/cyclonedx/model/Service.java b/src/main/java/org/cyclonedx/model/Service.java index 69f1fe8c7b..eedf466a74 100644 --- a/src/main/java/org/cyclonedx/model/Service.java +++ b/src/main/java/org/cyclonedx/model/Service.java @@ -85,7 +85,12 @@ public class Service extends ExtensibleElement { @VersionFilter(Version.VERSION_16) @JsonUnwrapped private Tags tags; + + @VersionFilter(Version.VERSION_17) + private List patentAssertions; + private List services; + @VersionFilter(Version.VERSION_14) private ReleaseNotes releaseNotes; @JsonOnly @VersionFilter(Version.VERSION_14) @@ -272,6 +277,17 @@ public void setTags(final Tags tags) { this.tags = tags; } + @JacksonXmlElementWrapper(localName = "patentAssertions") + @JacksonXmlProperty(localName = "patentAssertion") + @VersionFilter(Version.VERSION_17) + public List getPatentAssertions() { + return patentAssertions; + } + + public void setPatentAssertions(List patentAssertions) { + this.patentAssertions = patentAssertions; + } + public String getTrustZone() { return trustZone; } diff --git a/src/main/java/org/cyclonedx/model/TlpClassification.java b/src/main/java/org/cyclonedx/model/TlpClassification.java new file mode 100644 index 0000000000..baab38e011 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/TlpClassification.java @@ -0,0 +1,60 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Traffic Light Protocol (TLP) classification for data sharing and distribution control. + * + * @since 10.0.0 + */ +public enum TlpClassification { + @JsonProperty("CLEAR") + CLEAR("CLEAR"), + @JsonProperty("GREEN") + GREEN("GREEN"), + @JsonProperty("AMBER") + AMBER("AMBER"), + @JsonProperty("AMBER_AND_STRICT") + AMBER_AND_STRICT("AMBER_AND_STRICT"), + @JsonProperty("RED") + RED("RED"); + + private final String value; + + TlpClassification(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public static TlpClassification fromValue(String value) { + if (value != null) { + for (TlpClassification tlp : values()) { + if (tlp.value.equalsIgnoreCase(value)) { + return tlp; + } + } + } + return null; + } +} diff --git a/src/main/java/org/cyclonedx/model/component/crypto/AbstractIkeV2Transform.java b/src/main/java/org/cyclonedx/model/component/crypto/AbstractIkeV2Transform.java new file mode 100644 index 0000000000..7d92d36c57 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/component/crypto/AbstractIkeV2Transform.java @@ -0,0 +1,85 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model.component.crypto; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; + +/** + * Abstract base class for IKEv2 transform types. + * All IKEv2 transforms share algorithm and name fields. + * Subclasses add type-specific fields (keyLength, group, etc.). + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public abstract class AbstractIkeV2Transform { + + private String name; + private String algorithm; + + protected AbstractIkeV2Transform() { + } + + protected AbstractIkeV2Transform(String algorithm) { + this.algorithm = algorithm; + } + + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(final String algorithm) { + this.algorithm = algorithm; + } + + /** + * Returns true if this transform was constructed from a plain string value + * (1.6 backward compatibility format) and should be serialized as a string. + */ + public abstract boolean isStringOnly(); + + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (!(object instanceof AbstractIkeV2Transform)) { + return false; + } + AbstractIkeV2Transform that = (AbstractIkeV2Transform) object; + return Objects.equals(name, that.name) && Objects.equals(algorithm, that.algorithm); + } + + @Override + public int hashCode() { + return Objects.hash(name, algorithm); + } +} diff --git a/src/main/java/org/cyclonedx/model/component/crypto/AlgorithmProperties.java b/src/main/java/org/cyclonedx/model/component/crypto/AlgorithmProperties.java index 8173f288dd..99192c4704 100644 --- a/src/main/java/org/cyclonedx/model/component/crypto/AlgorithmProperties.java +++ b/src/main/java/org/cyclonedx/model/component/crypto/AlgorithmProperties.java @@ -9,6 +9,8 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.Version; +import org.cyclonedx.model.VersionFilter; import org.cyclonedx.model.component.crypto.enums.CertificationLevel; import org.cyclonedx.model.component.crypto.enums.CryptoFunction; import org.cyclonedx.model.component.crypto.enums.ExecutionEnvironment; @@ -21,8 +23,10 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyOrder({ "primitive", + "algorithmFamily", "parameterSetIdentifier", "curve", + "ellipticCurve", "executionEnvironment", "implementationPlatform", "certificationLevel", @@ -30,7 +34,8 @@ "padding", "cryptoFunctions", "classicalSecurityLevel", - "nistQuantumSecurityLevel" + "nistQuantumSecurityLevel", + "relatedCryptographicAssets" }) public class AlgorithmProperties { @@ -38,6 +43,12 @@ public class AlgorithmProperties private String parameterSetIdentifier; + @VersionFilter(Version.VERSION_17) + private String algorithmFamily; + + @VersionFilter(Version.VERSION_17) + private String ellipticCurve; + private String curve; private ExecutionEnvironment executionEnvironment; @@ -56,6 +67,9 @@ public class AlgorithmProperties private Integer nistQuantumSecurityLevel; + @VersionFilter(Version.VERSION_17) + private List relatedCryptographicAssets; + public Primitive getPrimitive() { return primitive; } @@ -72,6 +86,22 @@ public void setParameterSetIdentifier(final String parameterSetIdentifier) { this.parameterSetIdentifier = parameterSetIdentifier; } + public String getAlgorithmFamily() { + return algorithmFamily; + } + + public void setAlgorithmFamily(final String algorithmFamily) { + this.algorithmFamily = algorithmFamily; + } + + public String getEllipticCurve() { + return ellipticCurve; + } + + public void setEllipticCurve(final String ellipticCurve) { + this.ellipticCurve = ellipticCurve; + } + public String getCurve() { return curve; } @@ -149,6 +179,16 @@ public void setNistQuantumSecurityLevel(final Integer nistQuantumSecurityLevel) this.nistQuantumSecurityLevel = nistQuantumSecurityLevel; } + @JacksonXmlElementWrapper(localName = "relatedCryptographicAssets") + @JacksonXmlProperty(localName = "relatedCryptographicAsset") + public List getRelatedCryptographicAssets() { + return relatedCryptographicAssets; + } + + public void setRelatedCryptographicAssets(final List relatedCryptographicAssets) { + this.relatedCryptographicAssets = relatedCryptographicAssets; + } + @Override public boolean equals(final Object object) { if (this == object) { @@ -159,16 +199,19 @@ public boolean equals(final Object object) { } AlgorithmProperties that = (AlgorithmProperties) object; return primitive == that.primitive && Objects.equals(parameterSetIdentifier, that.parameterSetIdentifier) && + Objects.equals(algorithmFamily, that.algorithmFamily) && Objects.equals(ellipticCurve, that.ellipticCurve) && Objects.equals(curve, that.curve) && executionEnvironment == that.executionEnvironment && implementationPlatform == that.implementationPlatform && certificationLevel == that.certificationLevel && mode == that.mode && padding == that.padding && Objects.equals(cryptoFunctions, that.cryptoFunctions) && Objects.equals(classicalSecurityLevel, that.classicalSecurityLevel) && - Objects.equals(nistQuantumSecurityLevel, that.nistQuantumSecurityLevel); + Objects.equals(nistQuantumSecurityLevel, that.nistQuantumSecurityLevel) && + Objects.equals(relatedCryptographicAssets, that.relatedCryptographicAssets); } @Override public int hashCode() { - return Objects.hash(primitive, parameterSetIdentifier, curve, executionEnvironment, implementationPlatform, - certificationLevel, mode, padding, cryptoFunctions, classicalSecurityLevel, nistQuantumSecurityLevel); + return Objects.hash(primitive, parameterSetIdentifier, algorithmFamily, ellipticCurve, curve, executionEnvironment, + implementationPlatform, certificationLevel, mode, padding, cryptoFunctions, classicalSecurityLevel, + nistQuantumSecurityLevel, relatedCryptographicAssets); } } diff --git a/src/main/java/org/cyclonedx/model/component/crypto/CertificateProperties.java b/src/main/java/org/cyclonedx/model/component/crypto/CertificateProperties.java index ac14144e45..c8a3007ab6 100644 --- a/src/main/java/org/cyclonedx/model/component/crypto/CertificateProperties.java +++ b/src/main/java/org/cyclonedx/model/component/crypto/CertificateProperties.java @@ -1,10 +1,15 @@ package org.cyclonedx.model.component.crypto; +import java.util.List; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.Version; +import org.cyclonedx.model.VersionFilter; @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) @@ -16,7 +21,8 @@ "signatureAlgorithmRef", "subjectPublicKeyRef", "certificateFormat", - "certificateExtension" + "certificateExtension", + "relatedCryptographicAssets" }) public class CertificateProperties { @@ -36,6 +42,9 @@ public class CertificateProperties private String certificateExtension; + @VersionFilter(Version.VERSION_17) + private List relatedCryptographicAssets; + public String getSubjectName() { return subjectName; } @@ -100,6 +109,16 @@ public void setCertificateExtension(final String certificateExtension) { this.certificateExtension = certificateExtension; } + @JacksonXmlElementWrapper(localName = "relatedCryptographicAssets") + @JacksonXmlProperty(localName = "relatedCryptographicAsset") + public List getRelatedCryptographicAssets() { + return relatedCryptographicAssets; + } + + public void setRelatedCryptographicAssets(final List relatedCryptographicAssets) { + this.relatedCryptographicAssets = relatedCryptographicAssets; + } + @Override public boolean equals(final Object object) { if (this == object) { @@ -116,12 +135,13 @@ public boolean equals(final Object object) { Objects.equals(signatureAlgorithmRef, that.signatureAlgorithmRef) && Objects.equals(subjectPublicKeyRef, that.subjectPublicKeyRef) && Objects.equals(certificateFormat, that.certificateFormat) && - Objects.equals(certificateExtension, that.certificateExtension); + Objects.equals(certificateExtension, that.certificateExtension) && + Objects.equals(relatedCryptographicAssets, that.relatedCryptographicAssets); } @Override public int hashCode() { return Objects.hash(subjectName, issuerName, notValidBefore, notValidAfter, signatureAlgorithmRef, - subjectPublicKeyRef, certificateFormat, certificateExtension); + subjectPublicKeyRef, certificateFormat, certificateExtension, relatedCryptographicAssets); } } diff --git a/src/main/java/org/cyclonedx/model/component/crypto/CipherSuite.java b/src/main/java/org/cyclonedx/model/component/crypto/CipherSuite.java index d66f0b5fb2..d950c1c9d9 100644 --- a/src/main/java/org/cyclonedx/model/component/crypto/CipherSuite.java +++ b/src/main/java/org/cyclonedx/model/component/crypto/CipherSuite.java @@ -8,10 +8,12 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.Version; +import org.cyclonedx.model.VersionFilter; @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) -@JsonPropertyOrder({"name", "algorithms", "identifiers"}) +@JsonPropertyOrder({"name", "algorithms", "identifiers", "tlsGroups", "tlsSignatureSchemes"}) public class CipherSuite { @@ -21,6 +23,12 @@ public class CipherSuite private List identifiers; + @VersionFilter(Version.VERSION_17) + private List tlsGroups; + + @VersionFilter(Version.VERSION_17) + private List tlsSignatureSchemes; + public String getName() { return name; } @@ -49,6 +57,28 @@ public void setIdentifiers(final List identifiers) { this.identifiers = identifiers; } + @JacksonXmlElementWrapper(localName = "tlsGroups") + @JacksonXmlProperty(localName = "group") + @VersionFilter(Version.VERSION_17) + public List getTlsGroups() { + return tlsGroups; + } + + public void setTlsGroups(final List tlsGroups) { + this.tlsGroups = tlsGroups; + } + + @JacksonXmlElementWrapper(localName = "tlsSignatureSchemes") + @JacksonXmlProperty(localName = "signatureScheme") + @VersionFilter(Version.VERSION_17) + public List getTlsSignatureSchemes() { + return tlsSignatureSchemes; + } + + public void setTlsSignatureSchemes(final List tlsSignatureSchemes) { + this.tlsSignatureSchemes = tlsSignatureSchemes; + } + @Override public boolean equals(final Object object) { if (this == object) { @@ -59,11 +89,12 @@ public boolean equals(final Object object) { } CipherSuite that = (CipherSuite) object; return Objects.equals(name, that.name) && Objects.equals(algorithms, that.algorithms) && - Objects.equals(identifiers, that.identifiers); + Objects.equals(identifiers, that.identifiers) && Objects.equals(tlsGroups, that.tlsGroups) && + Objects.equals(tlsSignatureSchemes, that.tlsSignatureSchemes); } @Override public int hashCode() { - return Objects.hash(name, algorithms, identifiers); + return Objects.hash(name, algorithms, identifiers, tlsGroups, tlsSignatureSchemes); } } diff --git a/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Auth.java b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Auth.java new file mode 100644 index 0000000000..d9afa00a90 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Auth.java @@ -0,0 +1,46 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model.component.crypto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cyclonedx.util.serializer.IkeV2TransformSerializer; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"name", "algorithm"}) +@JsonSerialize(using = IkeV2TransformSerializer.class) +public class IkeV2Auth extends AbstractIkeV2Transform { + + public IkeV2Auth() { + } + + @JsonCreator + public IkeV2Auth(String algorithm) { + super(algorithm); + } + + @Override + public boolean isStringOnly() { + return getName() == null && getAlgorithm() != null; + } +} diff --git a/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Enc.java b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Enc.java new file mode 100644 index 0000000000..01351c028e --- /dev/null +++ b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Enc.java @@ -0,0 +1,78 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model.component.crypto; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cyclonedx.util.serializer.IkeV2TransformSerializer; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"name", "keyLength", "algorithm"}) +@JsonSerialize(using = IkeV2TransformSerializer.class) +public class IkeV2Enc extends AbstractIkeV2Transform { + + private Integer keyLength; + + public IkeV2Enc() { + } + + @JsonCreator + public IkeV2Enc(String algorithm) { + super(algorithm); + } + + public Integer getKeyLength() { + return keyLength; + } + + public void setKeyLength(final Integer keyLength) { + this.keyLength = keyLength; + } + + @Override + public boolean isStringOnly() { + return getName() == null && keyLength == null && getAlgorithm() != null; + } + + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (!(object instanceof IkeV2Enc)) { + return false; + } + if (!super.equals(object)) { + return false; + } + IkeV2Enc that = (IkeV2Enc) object; + return Objects.equals(keyLength, that.keyLength); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), keyLength); + } +} diff --git a/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Integ.java b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Integ.java new file mode 100644 index 0000000000..84df428d4b --- /dev/null +++ b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Integ.java @@ -0,0 +1,46 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model.component.crypto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cyclonedx.util.serializer.IkeV2TransformSerializer; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"name", "algorithm"}) +@JsonSerialize(using = IkeV2TransformSerializer.class) +public class IkeV2Integ extends AbstractIkeV2Transform { + + public IkeV2Integ() { + } + + @JsonCreator + public IkeV2Integ(String algorithm) { + super(algorithm); + } + + @Override + public boolean isStringOnly() { + return getName() == null && getAlgorithm() != null; + } +} diff --git a/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Ke.java b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Ke.java new file mode 100644 index 0000000000..a5e877457c --- /dev/null +++ b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Ke.java @@ -0,0 +1,78 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model.component.crypto; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cyclonedx.util.serializer.IkeV2TransformSerializer; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"group", "algorithm"}) +@JsonSerialize(using = IkeV2TransformSerializer.class) +public class IkeV2Ke extends AbstractIkeV2Transform { + + private Integer group; + + public IkeV2Ke() { + } + + @JsonCreator + public IkeV2Ke(String algorithm) { + super(algorithm); + } + + public Integer getGroup() { + return group; + } + + public void setGroup(final Integer group) { + this.group = group; + } + + @Override + public boolean isStringOnly() { + return getName() == null && group == null && getAlgorithm() != null; + } + + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (!(object instanceof IkeV2Ke)) { + return false; + } + if (!super.equals(object)) { + return false; + } + IkeV2Ke that = (IkeV2Ke) object; + return Objects.equals(group, that.group); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), group); + } +} diff --git a/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Prf.java b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Prf.java new file mode 100644 index 0000000000..07fd791c4a --- /dev/null +++ b/src/main/java/org/cyclonedx/model/component/crypto/IkeV2Prf.java @@ -0,0 +1,46 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model.component.crypto; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import org.cyclonedx.util.serializer.IkeV2TransformSerializer; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({"name", "algorithm"}) +@JsonSerialize(using = IkeV2TransformSerializer.class) +public class IkeV2Prf extends AbstractIkeV2Transform { + + public IkeV2Prf() { + } + + @JsonCreator + public IkeV2Prf(String algorithm) { + super(algorithm); + } + + @Override + public boolean isStringOnly() { + return getName() == null && getAlgorithm() != null; + } +} diff --git a/src/main/java/org/cyclonedx/model/component/crypto/Ikev2TransformTypes.java b/src/main/java/org/cyclonedx/model/component/crypto/Ikev2TransformTypes.java index 50cff8bc38..f85129a0d5 100644 --- a/src/main/java/org/cyclonedx/model/component/crypto/Ikev2TransformTypes.java +++ b/src/main/java/org/cyclonedx/model/component/crypto/Ikev2TransformTypes.java @@ -1,32 +1,37 @@ package org.cyclonedx.model.component.crypto; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import java.util.List; +import java.util.Objects; +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) public class Ikev2TransformTypes { @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "encr") @JsonProperty("encr") - private List encr; + private List encr; @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "prf") @JsonProperty("prf") - private List prf; + private List prf; @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "integ") @JsonProperty("integ") - private List integ; + private List integ; @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "ke") @JsonProperty("ke") - private List ke; + private List ke; @JsonProperty("esn") private Boolean esn; @@ -34,40 +39,40 @@ public class Ikev2TransformTypes { @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "auth") @JsonProperty("auth") - private List auth; + private List auth; public Ikev2TransformTypes() { } - public List getEncr() { + public List getEncr() { return encr; } - public void setEncr(List encr) { + public void setEncr(List encr) { this.encr = encr; } - public List getPrf() { + public List getPrf() { return prf; } - public void setPrf(List prf) { + public void setPrf(List prf) { this.prf = prf; } - public List getInteg() { + public List getInteg() { return integ; } - public void setInteg(List integ) { + public void setInteg(List integ) { this.integ = integ; } - public List getKe() { + public List getKe() { return ke; } - public void setKe(List ke) { + public void setKe(List ke) { this.ke = ke; } @@ -79,12 +84,30 @@ public void setEsn(Boolean esn) { this.esn = esn; } - public List getAuth() { + public List getAuth() { return auth; } - public void setAuth(List auth) { + public void setAuth(List auth) { this.auth = auth; } -} + @Override + public boolean equals(final Object object) { + if (this == object) { + return true; + } + if (!(object instanceof Ikev2TransformTypes)) { + return false; + } + Ikev2TransformTypes that = (Ikev2TransformTypes) object; + return Objects.equals(encr, that.encr) && Objects.equals(prf, that.prf) && + Objects.equals(integ, that.integ) && Objects.equals(ke, that.ke) && + Objects.equals(esn, that.esn) && Objects.equals(auth, that.auth); + } + + @Override + public int hashCode() { + return Objects.hash(encr, prf, integ, ke, esn, auth); + } +} diff --git a/src/main/java/org/cyclonedx/model/component/crypto/ProtocolProperties.java b/src/main/java/org/cyclonedx/model/component/crypto/ProtocolProperties.java index 24926356ac..5ae72e99ac 100644 --- a/src/main/java/org/cyclonedx/model/component/crypto/ProtocolProperties.java +++ b/src/main/java/org/cyclonedx/model/component/crypto/ProtocolProperties.java @@ -9,11 +9,13 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.Version; +import org.cyclonedx.model.VersionFilter; import org.cyclonedx.model.component.crypto.enums.ProtocolType; @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) -@JsonPropertyOrder({"type", "version", "cipherSuites", "ikev2TransformTypes", "cryptoRefArray"}) +@JsonPropertyOrder({"type", "version", "cipherSuites", "ikev2TransformTypes", "cryptoRefArray", "relatedCryptographicAssets"}) public class ProtocolProperties { private ProtocolType type; @@ -27,6 +29,9 @@ public class ProtocolProperties private List cryptoRefArray; + @VersionFilter(Version.VERSION_17) + private List relatedCryptographicAssets; + public ProtocolType getType() { return type; } @@ -73,6 +78,16 @@ public void setCryptoRefArray(final List cryptoRefArray) { this.cryptoRefArray = cryptoRefArray; } + @JacksonXmlElementWrapper(localName = "relatedCryptographicAssets") + @JacksonXmlProperty(localName = "relatedCryptographicAsset") + public List getRelatedCryptographicAssets() { + return relatedCryptographicAssets; + } + + public void setRelatedCryptographicAssets(final List relatedCryptographicAssets) { + this.relatedCryptographicAssets = relatedCryptographicAssets; + } + @Override public boolean equals(final Object object) { if (this == object) { @@ -85,11 +100,12 @@ public boolean equals(final Object object) { return type == that.type && Objects.equals(version, that.version) && Objects.equals(cipherSuites, that.cipherSuites) && Objects.equals(ikev2TransformTypes, that.ikev2TransformTypes) && - Objects.equals(cryptoRefArray, that.cryptoRefArray); + Objects.equals(cryptoRefArray, that.cryptoRefArray) && + Objects.equals(relatedCryptographicAssets, that.relatedCryptographicAssets); } @Override public int hashCode() { - return Objects.hash(type, version, cipherSuites, ikev2TransformTypes, cryptoRefArray); + return Objects.hash(type, version, cipherSuites, ikev2TransformTypes, cryptoRefArray, relatedCryptographicAssets); } } diff --git a/src/main/java/org/cyclonedx/model/component/crypto/RelatedCryptoMaterialProperties.java b/src/main/java/org/cyclonedx/model/component/crypto/RelatedCryptoMaterialProperties.java index 0a34b2e2ea..babc7e46b2 100644 --- a/src/main/java/org/cyclonedx/model/component/crypto/RelatedCryptoMaterialProperties.java +++ b/src/main/java/org/cyclonedx/model/component/crypto/RelatedCryptoMaterialProperties.java @@ -1,10 +1,15 @@ package org.cyclonedx.model.component.crypto; +import java.util.List; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.Version; +import org.cyclonedx.model.VersionFilter; import org.cyclonedx.model.component.crypto.enums.RelatedCryptoMaterialType; import org.cyclonedx.model.component.crypto.enums.State; @@ -13,7 +18,7 @@ @JsonPropertyOrder({ "type", "id", "state", "algorithmRef", "creationDate", "activationDate", "updateDate", "expirationDate", "value", - "size", "format", "securedBy" + "size", "format", "securedBy", "relatedCryptographicAssets" }) public class RelatedCryptoMaterialProperties { @@ -30,6 +35,9 @@ public class RelatedCryptoMaterialProperties private String format; private SecuredBy securedBy; + @VersionFilter(Version.VERSION_17) + private List relatedCryptographicAssets; + public RelatedCryptoMaterialType getType() { return type; } @@ -126,6 +134,16 @@ public void setSecuredBy(final SecuredBy securedBy) { this.securedBy = securedBy; } + @JacksonXmlElementWrapper(localName = "relatedCryptographicAssets") + @JacksonXmlProperty(localName = "relatedCryptographicAsset") + public List getRelatedCryptographicAssets() { + return relatedCryptographicAssets; + } + + public void setRelatedCryptographicAssets(final List relatedCryptographicAssets) { + this.relatedCryptographicAssets = relatedCryptographicAssets; + } + @Override public boolean equals(final Object object) { if (this == object) { @@ -142,12 +160,13 @@ public boolean equals(final Object object) { Objects.equals(updateDate, that.updateDate) && Objects.equals(expirationDate, that.expirationDate) && Objects.equals(value, that.value) && Objects.equals(size, that.size) && Objects.equals(format, that.format) && - Objects.equals(securedBy, that.securedBy); + Objects.equals(securedBy, that.securedBy) && + Objects.equals(relatedCryptographicAssets, that.relatedCryptographicAssets); } @Override public int hashCode() { return Objects.hash(type, id, state, algorithmRef, creationDate, activationDate, updateDate, expirationDate, value, - size, format, securedBy); + size, format, securedBy, relatedCryptographicAssets); } } diff --git a/src/main/java/org/cyclonedx/model/component/crypto/RelatedCryptographicAsset.java b/src/main/java/org/cyclonedx/model/component/crypto/RelatedCryptographicAsset.java new file mode 100644 index 0000000000..bd5595ac8f --- /dev/null +++ b/src/main/java/org/cyclonedx/model/component/crypto/RelatedCryptographicAsset.java @@ -0,0 +1,48 @@ +package org.cyclonedx.model.component.crypto; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JsonPropertyOrder({"type", "ref"}) +public class RelatedCryptographicAsset +{ + private String type; + + @JsonProperty("ref") + private String ref; + + public String getType() { + return type; + } + + public void setType(final String type) { + this.type = type; + } + + public String getRef() { + return ref; + } + + public void setRef(final String ref) { + this.ref = ref; + } + + @Override + public boolean equals(final Object object) { + if (this == object) return true; + if (!(object instanceof RelatedCryptographicAsset)) return false; + RelatedCryptographicAsset that = (RelatedCryptographicAsset) object; + return Objects.equals(type, that.type) && Objects.equals(ref, that.ref); + } + + @Override + public int hashCode() { + return Objects.hash(type, ref); + } +} \ No newline at end of file diff --git a/src/main/java/org/cyclonedx/model/definition/Definition.java b/src/main/java/org/cyclonedx/model/definition/Definition.java index 18e357ee56..88eb9a55c1 100644 --- a/src/main/java/org/cyclonedx/model/definition/Definition.java +++ b/src/main/java/org/cyclonedx/model/definition/Definition.java @@ -1,23 +1,37 @@ package org.cyclonedx.model.definition; +import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.Version; +import org.cyclonedx.model.Patent; +import org.cyclonedx.model.PatentFamily; +import org.cyclonedx.model.PatentItem; +import org.cyclonedx.model.VersionFilter; +import org.cyclonedx.util.deserializer.PatentsDeserializer; @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyOrder({ - "standards" + "standards", "patents" }) public class Definition { private List standards; + @VersionFilter(Version.VERSION_17) + @JsonDeserialize(using = PatentsDeserializer.class) + private List patents; + @JacksonXmlElementWrapper(localName = "standards") @JacksonXmlProperty(localName = "standard") public List getStandards() { @@ -28,6 +42,59 @@ public void setStandards(final List standards) { this.standards = standards; } + @JacksonXmlElementWrapper(localName = "patents") + @JacksonXmlProperty(localName = "patent") + @VersionFilter(Version.VERSION_17) + public List getPatents() { + return patents; + } + + public void setPatents(final List patents) { + this.patents = patents; + } + + /** + * @deprecated Use {@link #getPatents()} and filter by type instead. + */ + @Deprecated + @JsonIgnore + public List getPatentList() { + if (patents == null) return null; + List result = patents.stream() + .filter(item -> item.getPatent() != null) + .map(PatentItem::getPatent) + .collect(Collectors.toList()); + return result.isEmpty() ? null : result; + } + + /** + * @deprecated Use {@link #getPatents()} and filter by type instead. + */ + @Deprecated + @JsonIgnore + public List getPatentFamilyList() { + if (patents == null) return null; + List result = patents.stream() + .filter(item -> item.getPatentFamily() != null) + .map(PatentItem::getPatentFamily) + .collect(Collectors.toList()); + return result.isEmpty() ? null : result; + } + + public void addPatent(Patent patent) { + if (this.patents == null) { + this.patents = new ArrayList<>(); + } + this.patents.add(PatentItem.ofPatent(patent)); + } + + public void addPatentFamily(PatentFamily patentFamily) { + if (this.patents == null) { + this.patents = new ArrayList<>(); + } + this.patents.add(PatentItem.ofPatentFamily(patentFamily)); + } + @Override public boolean equals(final Object object) { if (this == object) { @@ -37,11 +104,12 @@ public boolean equals(final Object object) { return false; } Definition that = (Definition) object; - return Objects.equals(standards, that.standards); + return Objects.equals(standards, that.standards) && + Objects.equals(patents, that.patents); } @Override public int hashCode() { - return Objects.hashCode(standards); + return Objects.hash(standards, patents); } } diff --git a/src/main/java/org/cyclonedx/model/definition/Level.java b/src/main/java/org/cyclonedx/model/definition/Level.java index 5cec39ae00..e9f5b1f5b1 100644 --- a/src/main/java/org/cyclonedx/model/definition/Level.java +++ b/src/main/java/org/cyclonedx/model/definition/Level.java @@ -15,7 +15,6 @@ @JsonPropertyOrder({ "identifier", "title", - "text", "description", "requirements" }) diff --git a/src/main/java/org/cyclonedx/model/formulation/FormulationCommon.java b/src/main/java/org/cyclonedx/model/formulation/FormulationCommon.java index 3f4ad4a324..9aa90c4fef 100644 --- a/src/main/java/org/cyclonedx/model/formulation/FormulationCommon.java +++ b/src/main/java/org/cyclonedx/model/formulation/FormulationCommon.java @@ -155,8 +155,8 @@ public enum TaskType { COPY("copy"), @JsonProperty("clone") CLONE("clone"), - @JsonProperty("LINT") - LINT("LINT"), + @JsonProperty("lint") + LINT("lint"), @JsonProperty("scan") SCAN("scan"), @JsonProperty("merge") diff --git a/src/main/java/org/cyclonedx/model/license/ExpressionDetail.java b/src/main/java/org/cyclonedx/model/license/ExpressionDetail.java new file mode 100644 index 0000000000..c86cd23843 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/license/ExpressionDetail.java @@ -0,0 +1,100 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model.license; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.model.AttachmentText; + +import java.util.Objects; + +/** + * Individual license detail within a license expression. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({"licenseIdentifier", "bomRef", "text", "url"}) +public class ExpressionDetail { + + @JacksonXmlProperty(isAttribute = true, localName = "license-identifier") + @JsonProperty("licenseIdentifier") + private String licenseIdentifier; + + @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") + @JsonProperty("bom-ref") + private String bomRef; + + private AttachmentText text; + + private String url; + + public String getLicenseIdentifier() { + return licenseIdentifier; + } + + public void setLicenseIdentifier(String licenseIdentifier) { + this.licenseIdentifier = licenseIdentifier; + } + + public String getBomRef() { + return bomRef; + } + + public void setBomRef(String bomRef) { + this.bomRef = bomRef; + } + + public AttachmentText getText() { + return text; + } + + public void setText(AttachmentText text) { + this.text = text; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ExpressionDetail)) return false; + ExpressionDetail that = (ExpressionDetail) o; + return Objects.equals(licenseIdentifier, that.licenseIdentifier) && + Objects.equals(bomRef, that.bomRef) && + Objects.equals(text, that.text) && + Objects.equals(url, that.url); + } + + @Override + public int hashCode() { + return Objects.hash(licenseIdentifier, bomRef, text, url); + } +} diff --git a/src/main/java/org/cyclonedx/model/license/ExpressionDetailed.java b/src/main/java/org/cyclonedx/model/license/ExpressionDetailed.java new file mode 100644 index 0000000000..8584cc353d --- /dev/null +++ b/src/main/java/org/cyclonedx/model/license/ExpressionDetailed.java @@ -0,0 +1,134 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.model.license; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; +import org.cyclonedx.model.ExtensibleElement; +import org.cyclonedx.model.Licensing; +import org.cyclonedx.model.Property; + +import java.util.List; +import java.util.Objects; + +/** + * A detailed license expression with additional metadata for parts of the expression. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({ + "expression", "expressionDetails", "acknowledgement", "bomRef", "licensing", "properties" +}) +public class ExpressionDetailed extends ExtensibleElement { + + @JacksonXmlProperty(isAttribute = true) + private String expression; + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "details") + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) + private List expressionDetails; + + @JacksonXmlProperty(isAttribute = true) + private Acknowledgement acknowledgement; + + @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") + @JsonProperty("bom-ref") + private String bomRef; + + private Licensing licensing; + + @JacksonXmlElementWrapper(localName = "properties") + @JacksonXmlProperty(localName = "property") + private List properties; + + public String getExpression() { + return expression; + } + + public void setExpression(String expression) { + this.expression = expression; + } + + public List getExpressionDetails() { + return expressionDetails; + } + + public void setExpressionDetails(List expressionDetails) { + this.expressionDetails = expressionDetails; + } + + public Acknowledgement getAcknowledgement() { + return acknowledgement; + } + + public void setAcknowledgement(Acknowledgement acknowledgement) { + this.acknowledgement = acknowledgement; + } + + public String getBomRef() { + return bomRef; + } + + public void setBomRef(String bomRef) { + this.bomRef = bomRef; + } + + public Licensing getLicensing() { + return licensing; + } + + public void setLicensing(Licensing licensing) { + this.licensing = licensing; + } + + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ExpressionDetailed)) return false; + ExpressionDetailed that = (ExpressionDetailed) o; + return Objects.equals(expression, that.expression) && + Objects.equals(expressionDetails, that.expressionDetails) && + acknowledgement == that.acknowledgement && + Objects.equals(bomRef, that.bomRef) && + Objects.equals(licensing, that.licensing) && + Objects.equals(properties, that.properties); + } + + @Override + public int hashCode() { + return Objects.hash(expression, expressionDetails, acknowledgement, bomRef, licensing, properties); + } +} diff --git a/src/main/java/org/cyclonedx/util/deserializer/ExternalReferencesDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/ExternalReferencesDeserializer.java index d64dde5a25..e440a49112 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/ExternalReferencesDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/ExternalReferencesDeserializer.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import org.cyclonedx.model.ExternalReference; +import org.cyclonedx.model.Property; import java.io.IOException; import java.util.ArrayList; @@ -32,6 +33,7 @@ public class ExternalReferencesDeserializer extends JsonDeserializer> { private final HashesDeserializer hashesDeserializer = new HashesDeserializer(); + private final PropertiesDeserializer propertiesDeserializer = new PropertiesDeserializer(); @Override public List deserialize(JsonParser parser, DeserializationContext context) throws IOException { @@ -69,6 +71,12 @@ private ExternalReference parseExternalReference(JsonNode node, JsonParser p, De hashesParser.nextToken(); reference.setHashes(hashesDeserializer.deserialize(hashesParser, ctxt)); } + if (node.has("properties")) { + JsonParser propertiesParser = node.get("properties").traverse(p.getCodec()); + propertiesParser.nextToken(); + List properties = propertiesDeserializer.deserialize(propertiesParser, ctxt); + reference.setProperties(properties); + } return reference; } diff --git a/src/main/java/org/cyclonedx/util/deserializer/LicenseChoiceDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/LicenseChoiceDeserializer.java new file mode 100644 index 0000000000..b41fb02d18 --- /dev/null +++ b/src/main/java/org/cyclonedx/util/deserializer/LicenseChoiceDeserializer.java @@ -0,0 +1,166 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.util.deserializer; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; +import org.cyclonedx.model.License; +import org.cyclonedx.model.LicenseChoice; +import org.cyclonedx.model.LicenseItem; +import org.cyclonedx.model.license.Expression; +import org.cyclonedx.model.license.ExpressionDetailed; + +/** + * Deserializer for LicenseChoice that handles both CycloneDX 1.6 (array-level choice) + * and 1.7+ (item-level choice) formats. + */ +public class LicenseChoiceDeserializer extends JsonDeserializer +{ + + final ExpressionDeserializer expressionDeserializer = new ExpressionDeserializer(); + + @Override + public LicenseChoice deserialize( + final JsonParser p, final DeserializationContext ctxt) throws IOException + { + ObjectMapper codec = (ObjectMapper) p.getCodec(); + boolean isXml = codec instanceof XmlMapper; + JsonNode rootNode = p.getCodec().readTree(p); + + if (!rootNode.isEmpty()) { + LicenseChoice licenseChoice = new LicenseChoice(); + + if (isXml) { + // For XML, the root node contains all license choice items as fields + // (license, expression, expression-detailed) + processXmlNode(p, rootNode, licenseChoice, ctxt); + } else { + // For JSON, the root node is an array of individual license items + ArrayNode nodes = DeserializerUtils.getArrayNode(rootNode, null); + for (JsonNode node : nodes) { + processJsonNode(p, node, licenseChoice, ctxt); + } + } + return licenseChoice; + } + return null; + } + + private void processXmlNode(JsonParser p, JsonNode node, LicenseChoice licenseChoice, DeserializationContext ctxt) + throws IOException { + // XML format: node contains fields for "license", "expression", and/or "expression-detailed" + // Each field can be a single item or an array + + // Process all license elements + if (node.has("license")) { + processLicenseNode(p, node.get("license"), licenseChoice); + } + + // Process all expression elements + if (node.has("expression")) { + JsonNode exprNode = node.get("expression"); + if (exprNode.isArray()) { + for (JsonNode expr : exprNode) { + processExpression(p, expr, licenseChoice, ctxt); + } + } else { + processExpression(p, exprNode, licenseChoice, ctxt); + } + } + + // Process all expression-detailed elements + if (node.has("expression-detailed")) { + JsonNode exprDetailedNode = node.get("expression-detailed"); + if (exprDetailedNode.isArray()) { + for (JsonNode exprDetailed : exprDetailedNode) { + processExpressionDetailed(p, exprDetailed, licenseChoice); + } + } else { + processExpressionDetailed(p, exprDetailedNode, licenseChoice); + } + } + } + + private void processJsonNode(JsonParser p, JsonNode node, LicenseChoice licenseChoice, DeserializationContext ctxt) + throws IOException { + // JSON format for 1.7+: object with "license", "expression", or "expression-detailed" property + // JSON format for 1.6-: license/expression object directly in array + if (node.has("expression-detailed")) { + processExpressionDetailed(p, node.get("expression-detailed"), licenseChoice); + } + else if (node.has("expressionDetails") || + (node.has("expression") && (node.has("licensing") || node.has("properties")))) { + // ExpressionDetailed in JSON format: has expressionDetails, or expression with licensing/properties. + // These fields only exist on ExpressionDetailed, not on simple Expression. + processExpressionDetailed(p, node, licenseChoice); + } + else if (node.has("license")) { + // 1.7+ format: {"license": {...}} + processLicenseNode(p, node.get("license"), licenseChoice); + } + else if (node.has("expression")) { + // 1.6- format: expression object directly in array (e.g., {"expression": "MIT", "acknowledgement": "declared"}) + // The node itself IS the expression object + processExpression(p, node, licenseChoice, ctxt); + } + else { + // 1.6- format: license object directly in array + License license = p.getCodec().treeToValue(node, License.class); + licenseChoice.addLicense(license); + } + } + + private void processLicenseNode(JsonParser p, JsonNode licenseNode, LicenseChoice licenseChoice) + throws IOException { + ArrayNode licenseNodes = DeserializerUtils.getArrayNode(licenseNode, null); + + for (JsonNode license : licenseNodes) { + License licenseObj = p.getCodec().treeToValue(license, License.class); + licenseChoice.addLicense(licenseObj); + } + } + + private void processExpression( + final JsonParser p, + JsonNode node, + LicenseChoice licenseChoice, + DeserializationContext ctxt) throws IOException + { + JsonParser expressionParser = node.traverse(p.getCodec()); + expressionParser.nextToken(); + Expression expression = expressionDeserializer.deserialize(expressionParser, ctxt); + licenseChoice.addExpression(expression); + } + + private void processExpressionDetailed( + final JsonParser p, + JsonNode node, + LicenseChoice licenseChoice) throws IOException + { + ExpressionDetailed expressionDetailed = p.getCodec().treeToValue(node, ExpressionDetailed.class); + licenseChoice.addExpressionDetailed(expressionDetailed); + } +} diff --git a/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java index 542ff9f01d..ede96cde8b 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/MetadataDeserializer.java @@ -10,6 +10,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.cyclonedx.model.Component; +import org.cyclonedx.model.DistributionConstraints; import org.cyclonedx.model.LicenseChoice; import org.cyclonedx.model.Lifecycles; import org.cyclonedx.model.Metadata; @@ -92,6 +93,11 @@ public Metadata deserialize(JsonParser jsonParser, DeserializationContext ctxt) metadata.setToolChoice(toolsParser.getToolInformation()); } + if (node.has("distributionConstraints")) { + DistributionConstraints dc = mapper.convertValue(node.get("distributionConstraints"), DistributionConstraints.class); + metadata.setDistributionConstraints(dc); + } + return metadata; } diff --git a/src/main/java/org/cyclonedx/util/deserializer/OrganizationalChoiceDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/OrganizationalChoiceDeserializer.java index 451409a501..b07b357de2 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/OrganizationalChoiceDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/OrganizationalChoiceDeserializer.java @@ -21,57 +21,75 @@ import java.io.IOException; import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; import com.fasterxml.jackson.databind.JsonNode; import org.cyclonedx.model.OrganizationalChoice; import org.cyclonedx.model.OrganizationalContact; import org.cyclonedx.model.OrganizationalEntity; -public class OrganizationalChoiceDeserializer - extends JsonDeserializer +/** + * Deserializer for OrganizationalChoice that handles: + * 1. Wrapped format: {"individual": {...}} or {"organization": {...}} (Licensing) + * 2. String format: "bom-ref" (reference to an organization defined elsewhere) + * 3. XML ref format: {"ref": "bom-ref"} (XML asserter with ref element) + * 4. Unwrapped format: direct entity/contact object fields (patent asserter/assignee JSON) + */ +public class OrganizationalChoiceDeserializer extends JsonDeserializer { @Override - public OrganizationalChoice deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException { - JsonNode node = jp.getCodec().readTree(jp); - OrganizationalChoice organizationalChoice = new OrganizationalChoice(); + public OrganizationalChoice deserialize(JsonParser p, DeserializationContext ctxt) throws IOException + { + if (p.currentToken() == JsonToken.VALUE_STRING) { + // Simple string format - this is a bom-ref reference + String bomRef = p.getText(); + OrganizationalEntity org = new OrganizationalEntity(); + org.setBomRef(bomRef); - if (node.has("individual")) { - OrganizationalContact individual = jp.getCodec().treeToValue(node.get("individual"), OrganizationalContact.class); - organizationalChoice.setIndividual(individual); - } else if (node.has("organization")) { - JsonNode organizationNode = node.get("organization"); - OrganizationalEntity organization = deserializeOrganization(jp, organizationNode); - organizationalChoice.setOrganization(organization); + OrganizationalChoice choice = new OrganizationalChoice(); + choice.setOrganization(org); + return choice; } - return organizationalChoice; - } + // Object format + JsonNode node = p.getCodec().readTree(p); + OrganizationalChoice choice = new OrganizationalChoice(); - private OrganizationalEntity deserializeOrganization(JsonParser jp, JsonNode organizationNode) throws JsonProcessingException { - OrganizationalEntity organization = new OrganizationalEntity(); - if (organizationNode.has("name")) { - organization.setName(organizationNode.get("name").asText()); + // Wrapped format (Licensing): {"individual": {...}} or {"organization": {...}} + if (node.has("individual")) { + OrganizationalContact individual = p.getCodec().treeToValue(node.get("individual"), OrganizationalContact.class); + choice.setIndividual(individual); + } else if (node.has("organization")) { + OrganizationalEntity organization = p.getCodec().treeToValue(node.get("organization"), OrganizationalEntity.class); + choice.setOrganization(organization); } - - if (organizationNode.has("contact")) { - JsonNode contactsNode = organizationNode.get("contact"); - if (contactsNode.isArray()) { - for (JsonNode contactNode : contactsNode) { - addContactToOrganization(jp, organization, contactNode); - } - } else if (contactsNode.isObject()) { - addContactToOrganization(jp, organization, contactsNode); + // XML ref format: bom-ref → {"ref": "bom-ref"} + else if (node.has("ref")) { + String bomRef = node.get("ref").asText(); + OrganizationalEntity org = new OrganizationalEntity(); + org.setBomRef(bomRef); + choice.setOrganization(org); + } + // XML contact format: ... (asserter XSD uses "contact" not "individual") + else if (node.has("contact") && !node.has("name")) { + OrganizationalContact contact = p.getCodec().treeToValue(node.get("contact"), OrganizationalContact.class); + choice.setIndividual(contact); + } + // Unwrapped format (patent JSON): direct entity or contact fields + else if (node.size() > 0) { + // Distinguish entity from contact by field presence + if (node.has("email") || node.has("phone")) { + // OrganizationalContact fields + OrganizationalContact contact = p.getCodec().treeToValue(node, OrganizationalContact.class); + choice.setIndividual(contact); + } else { + // OrganizationalEntity fields (name, url, contact, address, bom-ref) + OrganizationalEntity entity = p.getCodec().treeToValue(node, OrganizationalEntity.class); + choice.setOrganization(entity); } } - return organization; - } - private void addContactToOrganization(JsonParser jp, OrganizationalEntity organization, JsonNode node) - throws JsonProcessingException - { - OrganizationalContact contact = jp.getCodec().treeToValue(node, OrganizationalContact.class); - organization.addContact(contact); + return choice; } } diff --git a/src/main/java/org/cyclonedx/util/deserializer/OrganizationalEntityDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/OrganizationalEntityDeserializer.java index 065e1197c2..94b8ad7aa1 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/OrganizationalEntityDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/OrganizationalEntityDeserializer.java @@ -52,16 +52,19 @@ public OrganizationalEntity deserialize(JsonParser jsonParser, DeserializationCo } private List parseUrls(JsonNode urlNode) { + if (urlNode == null) { + return null; + } + List urls = new ArrayList<>(); - if (urlNode != null) { - if (urlNode.isArray()) { - for (JsonNode urlElement : urlNode) { - urls.add(urlElement.asText()); - } - } else if (urlNode.isTextual()) { - urls.add(urlNode.asText()); + if (urlNode.isArray()) { + for (JsonNode urlElement : urlNode) { + urls.add(urlElement.asText()); } + } else if (urlNode.isTextual()) { + urls.add(urlNode.asText()); } - return urls; + + return urls.isEmpty() ? null : urls; } } diff --git a/src/main/java/org/cyclonedx/util/deserializer/PatentAssertionDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/PatentAssertionDeserializer.java new file mode 100644 index 0000000000..1ce88080a3 --- /dev/null +++ b/src/main/java/org/cyclonedx/util/deserializer/PatentAssertionDeserializer.java @@ -0,0 +1,104 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.util.deserializer; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.cyclonedx.model.OrganizationalChoice; +import org.cyclonedx.model.PatentAssertion; +import org.cyclonedx.model.PatentAssertion.AssertionType; + +/** + * Custom deserializer for PatentAssertion that handles the bom-ref attribute/element conflict + * in XML. In XML, "bom-ref" appears as both an attribute on <patentAssertion> and as + * child elements inside <patentRefs>. Jackson XML's default property resolution confuses + * these, so this deserializer handles them manually. + */ +public class PatentAssertionDeserializer extends JsonDeserializer { + + @Override + public PatentAssertion deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.readValueAsTree(); + PatentAssertion pa = new PatentAssertion(); + + // bom-ref (attribute in XML, property in JSON) + if (node.has("bom-ref")) { + JsonNode bomRefNode = node.get("bom-ref"); + if (bomRefNode.isTextual()) { + pa.setBomRef(bomRefNode.asText()); + } + } + + // assertionType + if (node.has("assertionType")) { + pa.setAssertionType(AssertionType.fromValue(node.get("assertionType").asText())); + } + + // patentRefs - in JSON it's an array of strings, in XML it's an object with bom-ref children + if (node.has("patentRefs")) { + JsonNode patentRefsNode = node.get("patentRefs"); + List refs = new ArrayList<>(); + if (patentRefsNode.isArray()) { + // JSON format: ["patent-1", "patent-2"] + for (JsonNode ref : patentRefsNode) { + refs.add(ref.asText()); + } + } else if (patentRefsNode.isObject()) { + // XML format: {"bom-ref": "patent-1"} or {"bom-ref": ["patent-1", "patent-2"]} + JsonNode bomRefChildren = patentRefsNode.get("bom-ref"); + if (bomRefChildren != null) { + if (bomRefChildren.isArray()) { + for (JsonNode ref : bomRefChildren) { + refs.add(ref.asText()); + } + } else { + refs.add(bomRefChildren.asText()); + } + } + } + if (!refs.isEmpty()) { + pa.setPatentRefs(refs); + } + } + + // asserter (OrganizationalChoice) + if (node.has("asserter")) { + JsonNode asserterNode = node.get("asserter"); + OrganizationalChoiceDeserializer choiceDeser = new OrganizationalChoiceDeserializer(); + JsonParser asserterParser = asserterNode.traverse(p.getCodec()); + asserterParser.nextToken(); + OrganizationalChoice asserter = choiceDeser.deserialize(asserterParser, ctxt); + pa.setAsserter(asserter); + } + + // notes + if (node.has("notes")) { + pa.setNotes(node.get("notes").asText()); + } + + return pa; + } +} diff --git a/src/main/java/org/cyclonedx/util/deserializer/PatentItemDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/PatentItemDeserializer.java new file mode 100644 index 0000000000..edac27694c --- /dev/null +++ b/src/main/java/org/cyclonedx/util/deserializer/PatentItemDeserializer.java @@ -0,0 +1,33 @@ +package org.cyclonedx.util.deserializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.cyclonedx.model.Patent; +import org.cyclonedx.model.PatentFamily; +import org.cyclonedx.model.PatentItem; + +import java.io.IOException; + +/** + * Deserializes a JSON object that could be either a Patent or a PatentFamily. + * Discrimination heuristic: if the object contains "familyId" or "members", it's a PatentFamily. + */ +public class PatentItemDeserializer extends JsonDeserializer { + + @Override + public PatentItem deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + JsonNode node = p.readValueAsTree(); + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + + if (node.has("familyId") || node.has("members")) { + PatentFamily pf = mapper.treeToValue(node, PatentFamily.class); + return PatentItem.ofPatentFamily(pf); + } else { + Patent patent = mapper.treeToValue(node, Patent.class); + return PatentItem.ofPatent(patent); + } + } +} diff --git a/src/main/java/org/cyclonedx/util/deserializer/PatentsDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/PatentsDeserializer.java new file mode 100644 index 0000000000..adfd1fd2a0 --- /dev/null +++ b/src/main/java/org/cyclonedx/util/deserializer/PatentsDeserializer.java @@ -0,0 +1,96 @@ +package org.cyclonedx.util.deserializer; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser; +import org.cyclonedx.model.Patent; +import org.cyclonedx.model.PatentFamily; +import org.cyclonedx.model.PatentItem; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * Deserializes the polymorphic patents array which can contain + * both Patent and PatentFamily objects. + * + * For JSON: reads the array and discriminates by presence of "familyId" or "members". + * For XML: reads the tree and groups by "patent" and "patentFamily" element names. + */ +public class PatentsDeserializer extends JsonDeserializer> { + + @Override + public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + if (p instanceof FromXmlParser) { + return deserializeXml(p, ctxt); + } + return deserializeJson(p, ctxt); + } + + private List deserializeJson(JsonParser p, DeserializationContext ctxt) throws IOException { + List items = new ArrayList<>(); + ObjectMapper mapper = (ObjectMapper) p.getCodec(); + + if (p.currentToken() == JsonToken.START_ARRAY) { + while (p.nextToken() != JsonToken.END_ARRAY) { + JsonNode node = mapper.readTree(p); + if (node.has("familyId") || node.has("members")) { + PatentFamily pf = mapper.treeToValue(node, PatentFamily.class); + items.add(PatentItem.ofPatentFamily(pf)); + } else { + Patent patent = mapper.treeToValue(node, Patent.class); + items.add(PatentItem.ofPatent(patent)); + } + } + } + + return items.isEmpty() ? null : items; + } + + private List deserializeXml(JsonParser p, DeserializationContext ctxt) throws IOException { + List items = new ArrayList<>(); + + // For XML, we're inside the wrapper element. + // Children can be or elements. + // Jackson XML presents them as field names within the current object. + if (p.currentToken() == JsonToken.START_OBJECT) { + while (p.nextToken() != JsonToken.END_OBJECT) { + if (p.currentToken() == JsonToken.FIELD_NAME) { + String fieldName = p.currentName(); + p.nextToken(); + + if ("patent".equals(fieldName)) { + if (p.currentToken() == JsonToken.START_ARRAY) { + while (p.nextToken() != JsonToken.END_ARRAY) { + Patent patent = ctxt.readValue(p, Patent.class); + items.add(PatentItem.ofPatent(patent)); + } + } else { + Patent patent = ctxt.readValue(p, Patent.class); + items.add(PatentItem.ofPatent(patent)); + } + } else if ("patentFamily".equals(fieldName)) { + if (p.currentToken() == JsonToken.START_ARRAY) { + while (p.nextToken() != JsonToken.END_ARRAY) { + PatentFamily pf = ctxt.readValue(p, PatentFamily.class); + items.add(PatentItem.ofPatentFamily(pf)); + } + } else { + PatentFamily pf = ctxt.readValue(p, PatentFamily.class); + items.add(PatentItem.ofPatentFamily(pf)); + } + } else { + p.skipChildren(); + } + } + } + } + + return items.isEmpty() ? null : items; + } +} diff --git a/src/main/java/org/cyclonedx/util/serializer/CustomSerializerModifier.java b/src/main/java/org/cyclonedx/util/serializer/CustomSerializerModifier.java index 3b3d59bd94..941d83e0e5 100644 --- a/src/main/java/org/cyclonedx/util/serializer/CustomSerializerModifier.java +++ b/src/main/java/org/cyclonedx/util/serializer/CustomSerializerModifier.java @@ -7,13 +7,18 @@ import com.fasterxml.jackson.databind.ser.BeanSerializerModifier; import org.cyclonedx.Version; import org.cyclonedx.model.Bom; +import org.cyclonedx.model.VersionFilter; import java.util.Iterator; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; public class CustomSerializerModifier extends BeanSerializerModifier { + private static final Logger LOGGER = Logger.getLogger(CustomSerializerModifier.class.getName()); + private final Version version; private final boolean isXml; @@ -29,12 +34,28 @@ public List changeProperties( BeanDescription beanDesc, List beanProperties) { - //Properties were introduced in 1.3 for XML and 1.5 for JSON - //Meaning that we should only serialize properties if the version is 1.3 or higher for XML - //and 1.5 or higher for JSON - //This is to ensure backwards compatibility with older versions of the schema + // Automatically remove fields annotated with @VersionFilter whose version + // is above the target generation version. This applies to ALL model classes, + // ensuring version-specific fields are never serialized for older BOM versions. + Iterator iterator = beanProperties.iterator(); + while (iterator.hasNext()) { + BeanPropertyWriter writer = iterator.next(); + VersionFilter filter = writer.getAnnotation(VersionFilter.class); + if (filter != null && filter.value().getVersion() > version.getVersion()) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(String.format( + "Removing field '%s' on %s: introduced in version %s but generating for version %s", + writer.getName(), beanDesc.getBeanClass().getSimpleName(), + filter.value().getVersionString(), version.getVersionString())); + } + iterator.remove(); + } + } + + // Special case: Bom.properties has different version thresholds for XML (1.3) and JSON (1.5). + // This cannot be expressed with a single @VersionFilter annotation. if (Bom.class.isAssignableFrom(beanDesc.getBeanClass())) { - Iterator iterator = beanProperties.iterator(); + iterator = beanProperties.iterator(); while (iterator.hasNext()) { BeanPropertyWriter writer = iterator.next(); if (isValidAttribute(writer)) { @@ -66,4 +87,4 @@ private boolean isValidAttribute(BeanPropertyWriter writer) { return "properties".equals(writer.getName()); } } -} \ No newline at end of file +} diff --git a/src/main/java/org/cyclonedx/util/serializer/EnvironmentVarsSerializer.java b/src/main/java/org/cyclonedx/util/serializer/EnvironmentVarsSerializer.java index d85ba62344..b981cd6c43 100644 --- a/src/main/java/org/cyclonedx/util/serializer/EnvironmentVarsSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/EnvironmentVarsSerializer.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import org.cyclonedx.Version; import org.cyclonedx.model.Property; import org.cyclonedx.model.formulation.common.EnvironmentVars; @@ -15,13 +16,16 @@ public class EnvironmentVarsSerializer { private final boolean isXml; - public EnvironmentVarsSerializer(boolean isXml) { - this(null, isXml); + private final Version version; + + public EnvironmentVarsSerializer(boolean isXml, Version version) { + this(null, isXml, version); } - public EnvironmentVarsSerializer(Class t, boolean isXml) { + public EnvironmentVarsSerializer(Class t, boolean isXml, Version version) { super(t); this.isXml = isXml; + this.version = version; } @Override diff --git a/src/main/java/org/cyclonedx/util/serializer/ExternalReferenceSerializer.java b/src/main/java/org/cyclonedx/util/serializer/ExternalReferenceSerializer.java index 35ad97bba5..fbab4223c4 100644 --- a/src/main/java/org/cyclonedx/util/serializer/ExternalReferenceSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/ExternalReferenceSerializer.java @@ -19,6 +19,7 @@ package org.cyclonedx.util.serializer; import java.io.IOException; +import java.util.List; import java.util.function.BiPredicate; import com.fasterxml.jackson.core.JsonGenerator; @@ -30,7 +31,6 @@ import org.cyclonedx.model.ExternalReference; import org.cyclonedx.model.ExternalReference.Type; import org.cyclonedx.model.Hash; -import org.cyclonedx.model.VersionFilter; import org.cyclonedx.util.BomUtils; import static org.cyclonedx.util.serializer.SerializerUtils.serializeHashJson; @@ -60,7 +60,7 @@ public void serialize( return; } - if(!shouldSerializeField(extRef.getType())) { + if (!SerializerUtils.shouldSerializeEnumValue(extRef.getType(), version)) { return; } @@ -84,10 +84,12 @@ private void serializeXml(final ToXmlGenerator toXmlGenerator, final ExternalRef if (extRef.getComment() != null) { toXmlGenerator.writeStringField("comment", extRef.getComment()); } - if (CollectionUtils.isNotEmpty(extRef.getHashes())) { + + List hashes = SerializerUtils.filterHashesByVersion(extRef.getHashes(), version); + if (CollectionUtils.isNotEmpty(hashes)) { toXmlGenerator.writeFieldName("hashes"); toXmlGenerator.writeStartObject(); - for (Hash hash : extRef.getHashes()) { + for (Hash hash : hashes) { toXmlGenerator.writeFieldName("hash"); SerializerUtils.serializeHashXml(toXmlGenerator, hash); } @@ -103,10 +105,12 @@ private void serializeJson(final JsonGenerator gen, final ExternalReference extR if (extRef.getComment() != null) { gen.writeStringField("comment", extRef.getComment()); } - if (CollectionUtils.isNotEmpty(extRef.getHashes())) { + + List hashes = SerializerUtils.filterHashesByVersion(extRef.getHashes(), version); + if (CollectionUtils.isNotEmpty(hashes)) { gen.writeFieldName("hashes"); gen.writeStartArray(); - for (Hash hash : extRef.getHashes()) { + for (Hash hash : hashes) { serializeHashJson(gen, hash); } gen.writeEndArray(); @@ -114,19 +118,6 @@ private void serializeJson(final JsonGenerator gen, final ExternalReference extR gen.writeEndObject(); } - private boolean shouldSerializeField(Object obj) { - try { - if (obj instanceof Type) { - Type type = (Type) obj; - VersionFilter filter = type.getClass().getField(type.name()).getAnnotation(VersionFilter.class); - return filter == null || filter.value().getVersion() <= version.getVersion(); - } - return true; - }catch (NoSuchFieldException e) { - return false; - } - } - @Override public Class handledType() { return ExternalReference.class; diff --git a/src/main/java/org/cyclonedx/util/serializer/HashSerializer.java b/src/main/java/org/cyclonedx/util/serializer/HashSerializer.java index 4b9ad50800..5970b09f26 100644 --- a/src/main/java/org/cyclonedx/util/serializer/HashSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/HashSerializer.java @@ -27,7 +27,6 @@ import org.cyclonedx.Version; import org.cyclonedx.model.Hash; import org.cyclonedx.model.Hash.Algorithm; -import org.cyclonedx.model.VersionFilter; import static org.cyclonedx.util.serializer.SerializerUtils.serializeHashJson; @@ -49,7 +48,7 @@ public HashSerializer(final Class t, final Version version) { public void serialize( final Hash hash, final JsonGenerator gen, final SerializerProvider provider) throws IOException { - if (!shouldSerializeField(hash.getAlgorithm())) { + if (!shouldSerializeHash(hash)) { return; } @@ -66,14 +65,16 @@ public Class handledType() { return Hash.class; } - private boolean shouldSerializeField(String value) { - try { - Algorithm algorithm = Algorithm.fromSpec(value); - VersionFilter filter = algorithm.getClass().getField(algorithm.name()).getAnnotation(VersionFilter.class); - return filter == null || filter.value().getVersion() <= version.getVersion(); + private boolean shouldSerializeHash(Hash hash) { + if (hash.getAlgorithm() == null) { + return true; } - catch (NoSuchFieldException e) { - return false; + try { + Algorithm algorithm = Algorithm.fromSpec(hash.getAlgorithm()); + return SerializerUtils.shouldSerializeEnumValue(algorithm, version); + } catch (IllegalArgumentException e) { + // Unknown algorithm - include it + return true; } } } diff --git a/src/main/java/org/cyclonedx/util/serializer/IkeV2TransformSerializer.java b/src/main/java/org/cyclonedx/util/serializer/IkeV2TransformSerializer.java new file mode 100644 index 0000000000..3d54ec1d3d --- /dev/null +++ b/src/main/java/org/cyclonedx/util/serializer/IkeV2TransformSerializer.java @@ -0,0 +1,76 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.util.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.cyclonedx.Version; +import org.cyclonedx.model.component.crypto.AbstractIkeV2Transform; +import org.cyclonedx.model.component.crypto.IkeV2Enc; +import org.cyclonedx.model.component.crypto.IkeV2Ke; + +import java.io.IOException; + +import static org.cyclonedx.util.serializer.SerializerUtils.shouldSerializeField; + +public class IkeV2TransformSerializer extends JsonSerializer { + + private final Version version; + + public IkeV2TransformSerializer() { + this(null); + } + + public IkeV2TransformSerializer(Version version) { + this.version = version; + } + + @Override + public void serialize(AbstractIkeV2Transform value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (value.isStringOnly()) { + gen.writeString(value.getAlgorithm()); + return; + } + + gen.writeStartObject(); + if (value instanceof IkeV2Ke) { + IkeV2Ke ke = (IkeV2Ke) value; + if (ke.getGroup() != null && shouldSerializeField(ke, version, "group")) { + gen.writeNumberField("group", ke.getGroup()); + } + } else { + if (value.getName() != null && shouldSerializeField(value, version, "name")) { + gen.writeStringField("name", value.getName()); + } + if (value instanceof IkeV2Enc) { + IkeV2Enc enc = (IkeV2Enc) value; + if (enc.getKeyLength() != null && shouldSerializeField(enc, version, "keyLength")) { + gen.writeNumberField("keyLength", enc.getKeyLength()); + } + } + } + if (value.getAlgorithm() != null && shouldSerializeField(value, version, "algorithm")) { + gen.writeStringField("algorithm", value.getAlgorithm()); + } + gen.writeEndObject(); + } +} diff --git a/src/main/java/org/cyclonedx/util/serializer/InputTypeSerializer.java b/src/main/java/org/cyclonedx/util/serializer/InputTypeSerializer.java index ade5fdb991..bf617aa4eb 100644 --- a/src/main/java/org/cyclonedx/util/serializer/InputTypeSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/InputTypeSerializer.java @@ -7,20 +7,26 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; import org.apache.commons.collections4.CollectionUtils; +import org.cyclonedx.Version; import org.cyclonedx.model.formulation.common.InputType; +import static org.cyclonedx.util.serializer.SerializerUtils.shouldSerializeField; + public class InputTypeSerializer extends StdSerializer { private final boolean isXml; - public InputTypeSerializer(boolean isXml) { - this(null, isXml); + private final Version version; + + public InputTypeSerializer(boolean isXml, Version version) { + this(null, isXml, version); } - public InputTypeSerializer(Class t, boolean isXml) { + public InputTypeSerializer(Class t, boolean isXml, Version version) { super(t); this.isXml = isXml; + this.version = version; } @Override @@ -39,25 +45,31 @@ private void createInputChoice(final InputType input, final JsonGenerator jsonGe { jsonGenerator.writeStartObject(); - if (input.getResource() != null) { + if (input.getResource() != null && shouldSerializeField(input, version, "resource")) { jsonGenerator.writeFieldName("resource"); jsonGenerator.writeObject(input.getResource()); } - else if (CollectionUtils.isNotEmpty(input.getParameters())) { + else if (CollectionUtils.isNotEmpty(input.getParameters()) && shouldSerializeField(input, version, "parameters")) { jsonGenerator.writeFieldName("parameters"); jsonGenerator.writeObject(input.getParameters()); } - else if (input.getEnvironmentVars() != null) { - new EnvironmentVarsSerializer(isXml).serialize(input.getEnvironmentVars(), jsonGenerator, serializerProvider); + else if (input.getEnvironmentVars() != null && shouldSerializeField(input, version, "environmentVars")) { + new EnvironmentVarsSerializer(isXml, version).serialize(input.getEnvironmentVars(), jsonGenerator, serializerProvider); } - else if (input.getData() != null) { + else if (input.getData() != null && shouldSerializeField(input, version, "data")) { jsonGenerator.writeFieldName("data"); jsonGenerator.writeObject(input.getData()); } - SerializerUtils.writeField(jsonGenerator, "source", input.getSource()); - SerializerUtils.writeField(jsonGenerator, "target", input.getTarget()); - SerializerUtils.writeField(jsonGenerator, "properties", input.getProperties()); + if (input.getSource() != null && shouldSerializeField(input, version, "source")) { + SerializerUtils.writeField(jsonGenerator, "source", input.getSource()); + } + if (input.getTarget() != null && shouldSerializeField(input, version, "target")) { + SerializerUtils.writeField(jsonGenerator, "target", input.getTarget()); + } + if (input.getProperties() != null && shouldSerializeField(input, version, "properties")) { + SerializerUtils.writeField(jsonGenerator, "properties", input.getProperties()); + } jsonGenerator.writeEndObject(); } diff --git a/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java b/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java index 5438d9110b..7277a52361 100644 --- a/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java @@ -29,9 +29,12 @@ import org.cyclonedx.Version; import org.cyclonedx.model.License; import org.cyclonedx.model.LicenseChoice; +import org.cyclonedx.model.LicenseItem; import org.cyclonedx.model.Property; import org.cyclonedx.model.license.Acknowledgement; import org.cyclonedx.model.license.Expression; +import org.cyclonedx.model.license.ExpressionDetailed; +import org.cyclonedx.model.license.ExpressionDetail; import static org.cyclonedx.util.serializer.SerializerUtils.shouldSerializeField; @@ -61,6 +64,10 @@ public void serialize( return; } + // Note: We don't throw an exception for version incompatibility. + // If a 1.7 BOM with mixed license types is being serialized to an earlier version, + // we'll serialize what we can. The schema validation will catch any issues. + if (isXml && gen instanceof ToXmlGenerator) { ToXmlGenerator toXmlGenerator = (ToXmlGenerator) gen; serializeXml(toXmlGenerator, licenseChoice, provider); @@ -73,58 +80,63 @@ public void serialize( private void serializeXml(ToXmlGenerator toXmlGenerator, LicenseChoice lc, final SerializerProvider provider) throws IOException { - if (CollectionUtils.isNotEmpty(lc.getLicenses())) { + toXmlGenerator.writeStartObject(); + + for (LicenseItem item : lc.getItems()) { + if (item.getLicense() != null) { + serializeLicenseToXml(toXmlGenerator, item.getLicense(), provider); + } else if (item.getExpression() != null) { + serializeExpressionToXml(toXmlGenerator, item.getExpression()); + } else if (item.getExpressionDetailed() != null && shouldSerializeField(item, version, "expressionDetailed")) { + serializeExpressionDetailedToXml(toXmlGenerator, item.getExpressionDetailed(), provider); + } + } + + toXmlGenerator.writeEndObject(); + } + + private void serializeLicenseToXml(ToXmlGenerator toXmlGenerator, License l, final SerializerProvider provider) + throws IOException + { + toXmlGenerator.writeFieldName("license"); + toXmlGenerator.writeStartObject(); + serializeXmlAttributes(toXmlGenerator, l.getBomRef(), l.getAcknowledgement(), l); + + if (StringUtils.isNotBlank(l.getId())) { + toXmlGenerator.writeStringField("id", l.getId()); + } + else if (StringUtils.isNotBlank(l.getName())) { + toXmlGenerator.writeStringField("name", l.getName()); + } + + if (l.getLicensing() != null && shouldSerializeField(l, version,"licensing")) { + toXmlGenerator.writeObjectField("licensing", l.getLicensing()); + } + + if (l.getAttachmentText() != null) { + toXmlGenerator.writeObjectField("text", l.getAttachmentText()); + } + + if (StringUtils.isNotBlank(l.getUrl())) { + toXmlGenerator.writeStringField("url", l.getUrl()); + } + + if (CollectionUtils.isNotEmpty(l.getProperties()) && shouldSerializeField(l, version, "properties")) { + toXmlGenerator.writeFieldName("properties"); toXmlGenerator.writeStartObject(); - toXmlGenerator.writeFieldName("license"); - toXmlGenerator.writeStartArray(); - for (License l : lc.getLicenses()) { - serializeXmlAttributes(toXmlGenerator, l.getBomRef(), l.getAcknowledgement(), l); - - if (StringUtils.isNotBlank(l.getId())) { - toXmlGenerator.writeStringField("id", l.getId()); - } - else if (StringUtils.isNotBlank(l.getName())) { - toXmlGenerator.writeStringField("name", l.getName()); - } - - if (l.getLicensing() != null && shouldSerializeField(l, version,"licensing")) { - toXmlGenerator.writeObjectField("licensing", l.getLicensing()); - } - - if (l.getAttachmentText() != null) { - toXmlGenerator.writeObjectField("text", l.getAttachmentText()); - } - - if (StringUtils.isNotBlank(l.getUrl())) { - toXmlGenerator.writeStringField("url", l.getUrl()); - } - - if (CollectionUtils.isNotEmpty(l.getProperties()) && shouldSerializeField(l, version, "properties")) { - toXmlGenerator.writeFieldName("properties"); - toXmlGenerator.writeStartObject(); - - for (Property property : l.getProperties()) { - toXmlGenerator.writeObjectField("property", property); - } - toXmlGenerator.writeEndObject(); - } - - //It might have extensible types - if(CollectionUtils.isNotEmpty(l.getExtensibleTypes())) { - new ExtensibleTypesSerializer().serialize(l.getExtensibleTypes(), toXmlGenerator, provider); - } - - toXmlGenerator.writeEndObject(); + + for (Property property : l.getProperties()) { + toXmlGenerator.writeObjectField("property", property); } - toXmlGenerator.writeEndArray(); toXmlGenerator.writeEndObject(); } - else if (lc.getExpression() != null) { - serializeExpressionToXml(lc, toXmlGenerator); - } else { - toXmlGenerator.writeStartArray(); - toXmlGenerator.writeEndArray(); + + //It might have extensible types + if(CollectionUtils.isNotEmpty(l.getExtensibleTypes())) { + new ExtensibleTypesSerializer().serialize(l.getExtensibleTypes(), toXmlGenerator, provider); } + + toXmlGenerator.writeEndObject(); } private void serializeXmlAttributes( @@ -133,8 +145,6 @@ private void serializeXmlAttributes( final Acknowledgement acknowledgement, final Object object) throws IOException { - toXmlGenerator.writeStartObject(); - if (StringUtils.isNotBlank(bomRef) && shouldSerializeField(object, version, "bomRef")) { toXmlGenerator.setNextIsAttribute(true); toXmlGenerator.writeFieldName("bom-ref"); @@ -153,50 +163,81 @@ private void serializeJson( final LicenseChoice licenseChoice, final JsonGenerator gen, final SerializerProvider provider) throws IOException { - if (CollectionUtils.isNotEmpty(licenseChoice.getLicenses())) { - serializeLicensesToJsonArray(licenseChoice, gen, provider); - } - else if (licenseChoice.getExpression() != null && - StringUtils.isNotBlank(licenseChoice.getExpression().getValue())) { - serializeExpressionToJson(licenseChoice, gen); - } else { - gen.writeStartArray(); - gen.writeEndArray(); - } + gen.writeStartArray(); + for (LicenseItem item : licenseChoice.getItems()) { + if (item.getLicense() != null) { + gen.writeStartObject(); + provider.defaultSerializeField("license", item.getLicense(), gen); + gen.writeEndObject(); + } else if (item.getExpression() != null) { + gen.writeStartObject(); + serializeExpressionToJson(item.getExpression(), gen); + gen.writeEndObject(); + } else if (item.getExpressionDetailed() != null && shouldSerializeField(item, version, "expressionDetailed")) { + gen.writeStartObject(); + serializeExpressionDetailedToJson(item.getExpressionDetailed(), gen, provider); + gen.writeEndObject(); + } + } + gen.writeEndArray(); } private void serializeExpressionToXml( - final LicenseChoice licenseChoice, final ToXmlGenerator toXmlGenerator) + final ToXmlGenerator toXmlGenerator, final Expression expression) throws IOException { - toXmlGenerator.writeStartObject(); - Expression expression = licenseChoice.getExpression(); toXmlGenerator.writeFieldName("expression"); + toXmlGenerator.writeStartObject(); serializeXmlAttributes(toXmlGenerator, expression.getBomRef(), expression.getAcknowledgement(), expression); toXmlGenerator.setNextIsUnwrapped(true); toXmlGenerator.writeStringField("", expression.getValue()); toXmlGenerator.writeEndObject(); - toXmlGenerator.writeEndObject(); } - private void serializeLicensesToJsonArray( - final LicenseChoice licenseChoice, final JsonGenerator gen, final SerializerProvider provider) + private void serializeExpressionDetailedToXml( + final ToXmlGenerator toXmlGenerator, + final ExpressionDetailed expressionDetailed, + final SerializerProvider provider) throws IOException { - gen.writeStartArray(); - for (License license : licenseChoice.getLicenses()) { - gen.writeStartObject(); - provider.defaultSerializeField("license", license, gen); - gen.writeEndObject(); + toXmlGenerator.writeFieldName("expression-detailed"); + toXmlGenerator.writeStartObject(); + + // Write expression as an attribute (required) + if (StringUtils.isNotBlank(expressionDetailed.getExpression())) { + toXmlGenerator.setNextIsAttribute(true); + toXmlGenerator.writeFieldName("expression"); + toXmlGenerator.writeString(expressionDetailed.getExpression()); + toXmlGenerator.setNextIsAttribute(false); } - gen.writeEndArray(); + + // Write other attributes (bom-ref, acknowledgement) + serializeXmlAttributes(toXmlGenerator, expressionDetailed.getBomRef(), expressionDetailed.getAcknowledgement(), expressionDetailed); + + if (CollectionUtils.isNotEmpty(expressionDetailed.getExpressionDetails())) { + for (ExpressionDetail detail : expressionDetailed.getExpressionDetails()) { + toXmlGenerator.writeObjectField("details", detail); + } + } + + if (expressionDetailed.getLicensing() != null && shouldSerializeField(expressionDetailed, version, "licensing")) { + toXmlGenerator.writeObjectField("licensing", expressionDetailed.getLicensing()); + } + + if (CollectionUtils.isNotEmpty(expressionDetailed.getProperties()) && shouldSerializeField(expressionDetailed, version, "properties")) { + toXmlGenerator.writeFieldName("properties"); + toXmlGenerator.writeStartObject(); + for (Property property : expressionDetailed.getProperties()) { + toXmlGenerator.writeObjectField("property", property); + } + toXmlGenerator.writeEndObject(); + } + + toXmlGenerator.writeEndObject(); } - private void serializeExpressionToJson(final LicenseChoice licenseChoice, final JsonGenerator gen) + private void serializeExpressionToJson(final Expression expression, final JsonGenerator gen) throws IOException { - Expression expression = licenseChoice.getExpression(); - gen.writeStartArray(); - gen.writeStartObject(); gen.writeStringField("expression", expression.getValue()); if (expression.getAcknowledgement() != null && shouldSerializeField(expression, version, "acknowledgement")) { gen.writeStringField("acknowledgement", expression.getAcknowledgement().getValue()); @@ -204,7 +245,29 @@ private void serializeExpressionToJson(final LicenseChoice licenseChoice, final if (StringUtils.isNotBlank(expression.getBomRef()) && shouldSerializeField(expression, version, "bomRef")) { gen.writeStringField("bom-ref", expression.getBomRef()); } - gen.writeEndObject(); - gen.writeEndArray(); + } + + private void serializeExpressionDetailedToJson( + final ExpressionDetailed expressionDetailed, final JsonGenerator gen, final SerializerProvider provider) + throws IOException { + // Flatten the expressionDetailed fields into the license item object + if (StringUtils.isNotBlank(expressionDetailed.getBomRef())) { + gen.writeStringField("bom-ref", expressionDetailed.getBomRef()); + } + if (expressionDetailed.getAcknowledgement() != null) { + gen.writeObjectField("acknowledgement", expressionDetailed.getAcknowledgement()); + } + if (StringUtils.isNotBlank(expressionDetailed.getExpression())) { + gen.writeStringField("expression", expressionDetailed.getExpression()); + } + if (CollectionUtils.isNotEmpty(expressionDetailed.getExpressionDetails())) { + gen.writeObjectField("expressionDetails", expressionDetailed.getExpressionDetails()); + } + if (expressionDetailed.getLicensing() != null) { + gen.writeObjectField("licensing", expressionDetailed.getLicensing()); + } + if (CollectionUtils.isNotEmpty(expressionDetailed.getProperties())) { + gen.writeObjectField("properties", expressionDetailed.getProperties()); + } } } diff --git a/src/main/java/org/cyclonedx/util/serializer/LifecycleSerializer.java b/src/main/java/org/cyclonedx/util/serializer/LifecycleSerializer.java index 1992645b4c..6eb396c4b7 100644 --- a/src/main/java/org/cyclonedx/util/serializer/LifecycleSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/LifecycleSerializer.java @@ -4,24 +4,30 @@ import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import org.cyclonedx.Version; import org.cyclonedx.model.LifecycleChoice; import org.cyclonedx.model.Lifecycles; import java.io.IOException; import java.util.List; +import static org.cyclonedx.util.serializer.SerializerUtils.shouldSerializeField; + public class LifecycleSerializer extends StdSerializer { private final boolean isXml; - public LifecycleSerializer(boolean isXml) { - this(null, isXml); + private final Version version; + + public LifecycleSerializer(boolean isXml, Version version) { + this(null, isXml, version); } - public LifecycleSerializer(Class t, boolean isXml) { + public LifecycleSerializer(Class t, boolean isXml, Version version) { super(t); this.isXml = isXml; + this.version = version; } @Override @@ -47,11 +53,15 @@ private void createLifecycleChoice(final Lifecycles lifecycles, final JsonGenera List lifecycleChoices = lifecycles.getLifecycleChoice(); for (LifecycleChoice choice : lifecycleChoices) { jsonGenerator.writeStartObject(); - if (choice.getPhase() != null) { + if (choice.getPhase() != null && shouldSerializeField(choice, version, "phase")) { jsonGenerator.writeStringField("phase", choice.getPhase().getPhaseName()); } else { - jsonGenerator.writeStringField("name", choice.getName()); - jsonGenerator.writeStringField("description", choice.getDescription()); + if (choice.getName() != null && shouldSerializeField(choice, version, "name")) { + jsonGenerator.writeStringField("name", choice.getName()); + } + if (choice.getDescription() != null && shouldSerializeField(choice, version, "description")) { + jsonGenerator.writeStringField("description", choice.getDescription()); + } } jsonGenerator.writeEndObject(); } diff --git a/src/main/java/org/cyclonedx/util/serializer/MetadataSerializer.java b/src/main/java/org/cyclonedx/util/serializer/MetadataSerializer.java index 10c3389b11..ccdef1e85f 100644 --- a/src/main/java/org/cyclonedx/util/serializer/MetadataSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/MetadataSerializer.java @@ -60,7 +60,7 @@ private void createMetadataInfo( if (metadata.getLifecycles() != null && shouldSerializeField(metadata, version, "lifecycles")) { jsonGenerator.writeFieldName("lifecycles"); - new LifecycleSerializer(isXml).serialize(metadata.getLifecycles(), jsonGenerator, serializerProvider); + new LifecycleSerializer(isXml, version).serialize(metadata.getLifecycles(), jsonGenerator, serializerProvider); } //Tools diff --git a/src/main/java/org/cyclonedx/util/serializer/OrganizationalChoiceSerializer.java b/src/main/java/org/cyclonedx/util/serializer/OrganizationalChoiceSerializer.java new file mode 100644 index 0000000000..bf08af1e36 --- /dev/null +++ b/src/main/java/org/cyclonedx/util/serializer/OrganizationalChoiceSerializer.java @@ -0,0 +1,120 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.util.serializer; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import org.cyclonedx.Version; +import org.cyclonedx.model.OrganizationalChoice; +import org.cyclonedx.model.OrganizationalEntity; + +import static org.cyclonedx.util.serializer.SerializerUtils.shouldSerializeField; + +/** + * Serializer for OrganizationalChoice in patent contexts where the JSON schema uses + * unwrapped format (direct entity/contact/string ref) rather than the Licensing wrapped + * format ({"organization": {...}} / {"individual": {...}}). + * + *

JSON output: + *

    + *
  • Org with only bomRef: string "org-acme-inc"
  • + *
  • Org with other fields: direct entity object {"name": "...", "url": [...]}
  • + *
  • Individual: direct contact object {"name": "...", "email": "..."}
  • + *
+ * + *

XML output (inside the parent element like <asserter> or <patentAssignee>): + *

    + *
  • Org with only bomRef: <ref>bomRef</ref>
  • + *
  • Org with other fields: <organization>...</organization>
  • + *
  • Individual: <individual>...</individual>
  • + *
+ */ +public class OrganizationalChoiceSerializer extends JsonSerializer { + + private final Version version; + + public OrganizationalChoiceSerializer() { + this(null); + } + + public OrganizationalChoiceSerializer(Version version) { + this.version = version; + } + + @Override + public void serialize(OrganizationalChoice value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (gen instanceof ToXmlGenerator) { + serializeXml(value, (ToXmlGenerator) gen, provider); + } else { + serializeJson(value, gen, provider); + } + } + + private void serializeJson(OrganizationalChoice value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (value.getOrganization() != null && shouldSerializeField(value, version, "organization")) { + OrganizationalEntity org = value.getOrganization(); + if (isRefOnly(org)) { + gen.writeString(org.getBomRef()); + } else { + provider.defaultSerializeValue(org, gen); + } + } else if (value.getIndividual() != null && shouldSerializeField(value, version, "individual")) { + provider.defaultSerializeValue(value.getIndividual(), gen); + } else { + gen.writeNull(); + } + } + + private void serializeXml(OrganizationalChoice value, ToXmlGenerator gen, SerializerProvider provider) + throws IOException + { + // In XML, the parent element (e.g. or ) wraps this serializer's output. + // We write an object with a single child element: , , or . + gen.writeStartObject(); + if (value.getOrganization() != null && shouldSerializeField(value, version, "organization")) { + OrganizationalEntity org = value.getOrganization(); + if (isRefOnly(org)) { + gen.writeStringField("ref", org.getBomRef()); + } else { + gen.writeFieldName("organization"); + provider.defaultSerializeValue(org, gen); + } + } else if (value.getIndividual() != null && shouldSerializeField(value, version, "individual")) { + gen.writeFieldName("individual"); + provider.defaultSerializeValue(value.getIndividual(), gen); + } + gen.writeEndObject(); + } + + private boolean isRefOnly(OrganizationalEntity org) { + return org.getBomRef() != null + && org.getName() == null + && (org.getUrls() == null || org.getUrls().isEmpty()) + && (org.getContacts() == null || org.getContacts().isEmpty()) + && org.getAddress() == null; + } +} diff --git a/src/main/java/org/cyclonedx/util/serializer/OutputTypeSerializer.java b/src/main/java/org/cyclonedx/util/serializer/OutputTypeSerializer.java index a8de77f292..021e4bfdd5 100644 --- a/src/main/java/org/cyclonedx/util/serializer/OutputTypeSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/OutputTypeSerializer.java @@ -7,20 +7,26 @@ import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; import org.apache.commons.collections4.CollectionUtils; +import org.cyclonedx.Version; import org.cyclonedx.model.formulation.common.OutputType; +import static org.cyclonedx.util.serializer.SerializerUtils.shouldSerializeField; + public class OutputTypeSerializer extends StdSerializer { private final boolean isXml; - public OutputTypeSerializer(boolean isXml) { - this(null, isXml); + private final Version version; + + public OutputTypeSerializer(boolean isXml, Version version) { + this(null, isXml, version); } - public OutputTypeSerializer(Class t, boolean isXml) { + public OutputTypeSerializer(Class t, boolean isXml, Version version) { super(t); this.isXml = isXml; + this.version = version; } @Override @@ -39,22 +45,30 @@ private void createOutputChoiceJson(final OutputType output, final JsonGenerator { jsonGenerator.writeStartObject(); - if (output.getResource() != null) { + if (output.getResource() != null && shouldSerializeField(output, version, "resource")) { jsonGenerator.writeFieldName("resource"); - jsonGenerator.writeObject( output.getResource()); + jsonGenerator.writeObject(output.getResource()); } - else if (output.getEnvironmentVars() != null) { - new EnvironmentVarsSerializer(isXml).serialize(output.getEnvironmentVars(), jsonGenerator, serializerProvider); + else if (output.getEnvironmentVars() != null && shouldSerializeField(output, version, "environmentVars")) { + new EnvironmentVarsSerializer(isXml, version).serialize(output.getEnvironmentVars(), jsonGenerator, serializerProvider); } - else if (output.getData() != null) { + else if (output.getData() != null && shouldSerializeField(output, version, "data")) { jsonGenerator.writeFieldName("data"); - jsonGenerator.writeObject( output.getData()); + jsonGenerator.writeObject(output.getData()); } - SerializerUtils.writeField(jsonGenerator, "type", output.getType()); - SerializerUtils.writeField(jsonGenerator, "source", output.getSource()); - SerializerUtils.writeField(jsonGenerator, "target", output.getTarget()); - SerializerUtils.writeField(jsonGenerator, "properties", output.getProperties()); + if (output.getType() != null && shouldSerializeField(output, version, "type")) { + SerializerUtils.writeField(jsonGenerator, "type", output.getType()); + } + if (output.getSource() != null && shouldSerializeField(output, version, "source")) { + SerializerUtils.writeField(jsonGenerator, "source", output.getSource()); + } + if (output.getTarget() != null && shouldSerializeField(output, version, "target")) { + SerializerUtils.writeField(jsonGenerator, "target", output.getTarget()); + } + if (output.getProperties() != null && shouldSerializeField(output, version, "properties")) { + SerializerUtils.writeField(jsonGenerator, "properties", output.getProperties()); + } jsonGenerator.writeEndObject(); } @@ -63,33 +77,33 @@ private void createOutputChoiceXml(final OutputType output, final ToXmlGenerator { xmlGenerator.writeStartObject(); - if (output.getResource() != null) { + if (output.getResource() != null && shouldSerializeField(output, version, "resource")) { xmlGenerator.writeFieldName("resource"); - xmlGenerator.writeObject( output.getResource()); + xmlGenerator.writeObject(output.getResource()); } - else if (output.getEnvironmentVars() != null) { - new EnvironmentVarsSerializer(isXml).serialize(output.getEnvironmentVars(), xmlGenerator, serializerProvider); + else if (output.getEnvironmentVars() != null && shouldSerializeField(output, version, "environmentVars")) { + new EnvironmentVarsSerializer(isXml, version).serialize(output.getEnvironmentVars(), xmlGenerator, serializerProvider); } - else if (output.getData() != null) { + else if (output.getData() != null && shouldSerializeField(output, version, "data")) { xmlGenerator.writeFieldName("data"); - xmlGenerator.writeObject( output.getData()); + xmlGenerator.writeObject(output.getData()); } - if (output.getType() != null) { + if (output.getType() != null && shouldSerializeField(output, version, "type")) { xmlGenerator.writeFieldName("type"); xmlGenerator.writeObject(output.getType()); } - if (output.getSource() != null) { + if (output.getSource() != null && shouldSerializeField(output, version, "source")) { xmlGenerator.writeFieldName("source"); xmlGenerator.writeObject(output.getSource()); } - if (output.getTarget() != null) { + if (output.getTarget() != null && shouldSerializeField(output, version, "target")) { xmlGenerator.writeFieldName("target"); xmlGenerator.writeObject(output.getTarget()); } - if (CollectionUtils.isNotEmpty(output.getProperties())) { + if (CollectionUtils.isNotEmpty(output.getProperties()) && shouldSerializeField(output, version, "properties")) { xmlGenerator.writeFieldName("properties"); - xmlGenerator.writeObject( output.getProperties()); + xmlGenerator.writeObject(output.getProperties()); } xmlGenerator.writeEndObject(); } diff --git a/src/main/java/org/cyclonedx/util/serializer/PatentAssertionSerializer.java b/src/main/java/org/cyclonedx/util/serializer/PatentAssertionSerializer.java new file mode 100644 index 0000000000..1dc5d5c14a --- /dev/null +++ b/src/main/java/org/cyclonedx/util/serializer/PatentAssertionSerializer.java @@ -0,0 +1,135 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx.util.serializer; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import org.cyclonedx.Version; +import org.cyclonedx.model.PatentAssertion; + +import javax.xml.namespace.QName; + +import static org.cyclonedx.util.serializer.SerializerUtils.shouldSerializeField; + +/** + * Custom serializer for PatentAssertion that handles the bom-ref attribute/element name + * conflict in XML. In XML, "bom-ref" appears as both an attribute on <patentAssertion> + * and as child elements inside <patentRefs>. Jackson XML cannot distinguish these, + * so this serializer handles the XML output manually. + */ +public class PatentAssertionSerializer extends JsonSerializer { + + private final Version version; + + private final OrganizationalChoiceSerializer choiceSerializer; + + public PatentAssertionSerializer() { + this(null); + } + + public PatentAssertionSerializer(Version version) { + this.version = version; + this.choiceSerializer = new OrganizationalChoiceSerializer(version); + } + + @Override + public void serialize(PatentAssertion value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (gen instanceof ToXmlGenerator) { + serializeXml(value, (ToXmlGenerator) gen, provider); + } else { + serializeJson(value, gen, provider); + } + } + + private void serializeJson(PatentAssertion value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + gen.writeStartObject(); + if (value.getBomRef() != null && shouldSerializeField(value, version, "bomRef")) { + gen.writeStringField("bom-ref", value.getBomRef()); + } + if (value.getAssertionType() != null && shouldSerializeField(value, version, "assertionType")) { + gen.writeStringField("assertionType", value.getAssertionType().getValue()); + } + if (value.getPatentRefs() != null && !value.getPatentRefs().isEmpty() && shouldSerializeField(value, version, "patentRefs")) { + gen.writeArrayFieldStart("patentRefs"); + for (String ref : value.getPatentRefs()) { + gen.writeString(ref); + } + gen.writeEndArray(); + } + if (value.getAsserter() != null && shouldSerializeField(value, version, "asserter")) { + gen.writeFieldName("asserter"); + choiceSerializer.serialize(value.getAsserter(), gen, provider); + } + if (value.getNotes() != null && shouldSerializeField(value, version, "notes")) { + gen.writeStringField("notes", value.getNotes()); + } + gen.writeEndObject(); + } + + private void serializeXml(PatentAssertion value, ToXmlGenerator gen, SerializerProvider provider) + throws IOException + { + gen.writeStartObject(); + + // Write bom-ref as XML attribute + if (value.getBomRef() != null && shouldSerializeField(value, version, "bomRef")) { + gen.setNextIsAttribute(true); + gen.setNextName(new QName("bom-ref")); + gen.writeFieldName("bom-ref"); + gen.writeString(value.getBomRef()); + gen.setNextIsAttribute(false); + } + + // assertionType + if (value.getAssertionType() != null && shouldSerializeField(value, version, "assertionType")) { + gen.writeStringField("assertionType", value.getAssertionType().getValue()); + } + + // patentRefs wrapper with bom-ref child elements + if (value.getPatentRefs() != null && !value.getPatentRefs().isEmpty() && shouldSerializeField(value, version, "patentRefs")) { + gen.writeFieldName("patentRefs"); + gen.writeStartObject(); + for (String ref : value.getPatentRefs()) { + gen.writeStringField("bom-ref", ref); + } + gen.writeEndObject(); + } + + // asserter (delegates to OrganizationalChoiceSerializer) + if (value.getAsserter() != null && shouldSerializeField(value, version, "asserter")) { + gen.writeFieldName("asserter"); + choiceSerializer.serialize(value.getAsserter(), gen, provider); + } + + // notes + if (value.getNotes() != null && shouldSerializeField(value, version, "notes")) { + gen.writeStringField("notes", value.getNotes()); + } + + gen.writeEndObject(); + } +} diff --git a/src/main/java/org/cyclonedx/util/serializer/PatentItemSerializer.java b/src/main/java/org/cyclonedx/util/serializer/PatentItemSerializer.java new file mode 100644 index 0000000000..0ecaba3bab --- /dev/null +++ b/src/main/java/org/cyclonedx/util/serializer/PatentItemSerializer.java @@ -0,0 +1,67 @@ +package org.cyclonedx.util.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import org.cyclonedx.Version; +import org.cyclonedx.model.PatentItem; + +import javax.xml.namespace.QName; +import java.io.IOException; + +import static org.cyclonedx.util.serializer.SerializerUtils.shouldSerializeField; + +/** + * Serializes a PatentItem by flattening — writing the inner Patent or PatentFamily + * object directly. For XML, sets the correct element name. + */ +public class PatentItemSerializer extends JsonSerializer { + + private final Version version; + + public PatentItemSerializer() { + this(null); + } + + public PatentItemSerializer(Version version) { + this.version = version; + } + + @Override + public void serialize(PatentItem value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (gen instanceof ToXmlGenerator) { + serializeXml(value, (ToXmlGenerator) gen, provider); + } else { + serializeJson(value, gen, provider); + } + } + + private void serializeJson(PatentItem value, JsonGenerator gen, SerializerProvider provider) + throws IOException + { + if (value.getPatent() != null && shouldSerializeField(value, version, "patent")) { + provider.defaultSerializeValue(value.getPatent(), gen); + } else if (value.getPatentFamily() != null && shouldSerializeField(value, version, "patentFamily")) { + provider.defaultSerializeValue(value.getPatentFamily(), gen); + } else { + gen.writeNull(); + } + } + + private void serializeXml(PatentItem value, ToXmlGenerator gen, SerializerProvider provider) + throws IOException + { + if (value.getPatentFamily() != null && shouldSerializeField(value, version, "patentFamily")) { + gen.setNextName(new QName("patentFamily")); + provider.defaultSerializeValue(value.getPatentFamily(), gen); + } else if (value.getPatent() != null && shouldSerializeField(value, version, "patent")) { + // patent is the default element name from @JacksonXmlProperty + provider.defaultSerializeValue(value.getPatent(), gen); + } else { + gen.writeNull(); + } + } +} diff --git a/src/main/java/org/cyclonedx/util/serializer/SerializerUtils.java b/src/main/java/org/cyclonedx/util/serializer/SerializerUtils.java index 4f0ad14d0a..8158d5cb32 100644 --- a/src/main/java/org/cyclonedx/util/serializer/SerializerUtils.java +++ b/src/main/java/org/cyclonedx/util/serializer/SerializerUtils.java @@ -2,16 +2,24 @@ import java.io.IOException; import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; import org.cyclonedx.Version; +import org.cyclonedx.model.ExternalReference; import org.cyclonedx.model.Hash; +import org.cyclonedx.model.Hash.Algorithm; import org.cyclonedx.model.Property; import org.cyclonedx.model.VersionFilter; public class SerializerUtils { + private static final Logger LOGGER = Logger.getLogger(SerializerUtils.class.getName()); + public static void serializeHashXml(final ToXmlGenerator toXmlGenerator, final Hash hash) throws IOException { toXmlGenerator.writeStartObject(); toXmlGenerator.setNextIsAttribute(true); @@ -33,16 +41,105 @@ public static void serializeHashJson(final JsonGenerator gen, final Hash hash) } public static boolean shouldSerializeField(Object obj, Version version, String fieldName) { + if (version == null) { + return true; + } + VersionFilter filter = findVersionFilter(obj.getClass(), fieldName); + if (filter != null && filter.value().getVersion() > version.getVersion()) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(String.format( + "Skipping field '%s' on %s: introduced in version %s but generating for version %s", + fieldName, obj.getClass().getSimpleName(), + filter.value().getVersionString(), version.getVersionString())); + } + return false; + } + return true; + } + + private static VersionFilter findVersionFilter(Class clazz, String fieldName) { + // Walk up the class hierarchy to find the field (getDeclaredField only checks the immediate class) + Class current = clazz; + while (current != null && current != Object.class) { + try { + Field field = current.getDeclaredField(fieldName); + return field.getAnnotation(VersionFilter.class); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + return null; + } + + /** + * Checks whether an enum constant should be serialized for the target version by inspecting + * the {@link VersionFilter} annotation on the enum constant's field. + * + * @param enumValue the enum constant to check + * @param version the target BOM version being generated + * @return true if the enum value is valid for the target version, false otherwise + */ + public static boolean shouldSerializeEnumValue(Enum enumValue, Version version) { try { - Field field = obj.getClass().getDeclaredField(fieldName); - VersionFilter filter = field.getAnnotation(VersionFilter.class); - return filter == null || filter.value().getVersion() <= version.getVersion(); + VersionFilter filter = enumValue.getClass() + .getField(enumValue.name()) + .getAnnotation(VersionFilter.class); + if (filter != null && filter.value().getVersion() > version.getVersion()) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine(String.format( + "Skipping %s.%s: introduced in version %s but generating for version %s", + enumValue.getClass().getSimpleName(), enumValue.name(), + filter.value().getVersionString(), version.getVersionString())); + } + return false; + } + return true; } catch (NoSuchFieldException e) { - // If the field does not exist, assume it should be serialized return true; } } + /** + * Filters a list of ExternalReferences, removing entries whose Type has a + * {@link VersionFilter} above the target version. + */ + public static List filterExternalReferencesByVersion( + List refs, Version version) { + if (refs == null) return null; + List filtered = new ArrayList<>(); + for (ExternalReference ref : refs) { + if (ref.getType() == null || shouldSerializeEnumValue(ref.getType(), version)) { + filtered.add(ref); + } + } + return filtered.isEmpty() ? null : filtered; + } + + /** + * Filters a list of Hashes, removing entries whose Algorithm has a + * {@link VersionFilter} above the target version. + */ + public static List filterHashesByVersion(List hashes, Version version) { + if (hashes == null) return null; + List filtered = new ArrayList<>(); + for (Hash hash : hashes) { + if (hash.getAlgorithm() == null) { + filtered.add(hash); + continue; + } + try { + Algorithm algorithm = Algorithm.fromSpec(hash.getAlgorithm()); + if (shouldSerializeEnumValue(algorithm, version)) { + filtered.add(hash); + } + } catch (IllegalArgumentException e) { + // Unknown algorithm - include it + filtered.add(hash); + } + } + return filtered.isEmpty() ? null : filtered; + } + public static void serializeProperty(String propertyName, Property prop, ToXmlGenerator xmlGenerator) throws IOException { xmlGenerator.writeFieldName(propertyName); xmlGenerator.writeStartObject(); diff --git a/src/main/resources/cryptography-defs.schema.json b/src/main/resources/cryptography-defs.schema.json new file mode 100644 index 0000000000..e178150572 --- /dev/null +++ b/src/main/resources/cryptography-defs.schema.json @@ -0,0 +1,592 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://cyclonedx.org/schema/cryptography-defs.schema.json", + "$comment": "2026-03-05T14:27:50Z", + "title": "Cryptographic Algorithm Family Definitions", + "description": "Enumerates cryptographic algorithm families and their specific metadata.", + "type": "object", + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string" + }, + "lastUpdated": { + "type": "string", + "format": "date-time", + "title": "Last Updated", + "description": "The date and time (timestamp) when the data was last updated." + }, + "algorithms": { + "type": "array", + "title": "Algorithm Families", + "description": "An array of cryptographic algorithm family definitions.", + "items": { + "type": "object", + "title": "Algorithm Family", + "description": "Defines a cryptographic algorithm family and its metadata.", + "additionalProperties": false, + "properties": { + "family": { + "type": "string", + "title": "Algorithm Family", + "description": "The name of the cryptographic algorithm family." + }, + "standard": { + "type": "array", + "title": "Standards", + "description": "List of standards defining or relating to the algorithm family.", + "items": { + "type": "object", + "title": "Standard Reference", + "description": "Reference to a standard, including its name and URL.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "Standard Name", + "description": "The name or identifier of the standard." + }, + "url": { + "type": "string", + "format": "iri-reference", + "title": "Standard URL", + "description": "A URL pointing to the standard's official documentation." + } + }, + "required": [ + "name", + "url" + ] + } + }, + "variant": { + "type": "array", + "title": "Variants", + "description": "Defines algorithm variants by a naming pattern and the corresponding cryptographic primitive.", + "items": { + "type": "object", + "title": "Standard Reference", + "description": "Reference to a standard, including its name and URL.", + "additionalProperties": false, + "properties": { + "pattern": { + "type": "string", + "title": "Standard Name", + "description": "Defines the pattern used to construct the complete algorithm name. Placeholders are defined by {} for algorithm-specific properties." + }, + "primitive": { + "type": "string", + "title": "Primitive", + "description": "Type of cryptographic primitive (e.g., signature, encryption, hash)." + }, + "standard": { + "type": "array", + "title": "Standards", + "description": "List of standards defining or relating to the algorithm variant.", + "items": { + "type": "object", + "title": "Standard Reference", + "description": "Reference to a standard, including its name and URL.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "Standard Name", + "description": "The name or identifier of the standard." + }, + "url": { + "type": "string", + "format": "iri-reference", + "title": "Standard URL", + "description": "A URL pointing to the standard's official documentation." + } + }, + "required": [ + "name", + "url" + ] + } + } + }, + "required": [ + "pattern", + "primitive" + ] + } + } + }, + "required": [ + "family", + "variant" + ] + } + }, + "ellipticCurves": { + "type": "array", + "title": "Elliptic Curves", + "description": "An array of elliptic curve family definitions.", + "items": { + "type": "object", + "title": "Elliptic Curve Family", + "description": "Defines an elliptic curve family and its metadata.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "Curve Family Name", + "description": "The name of the elliptic curve family." + }, + "description": { + "type": [ + "string", + "null" + ], + "title": "Description", + "description": "A description of the elliptic curve family." + }, + "curves": { + "type": "array", + "title": "Curves", + "description": "List of curves in this family.", + "items": { + "type": "object", + "title": "Curve", + "description": "Defines a specific elliptic curve and its metadata.", + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "title": "Curve Name", + "description": "The name of the elliptic curve." + }, + "description": { + "type": [ + "string", + "null" + ], + "title": "Description", + "description": "A description of the elliptic curve." + }, + "oid": { + "type": [ + "string", + "null" + ], + "title": "OID", + "description": "The Object Identifier (OID) of the elliptic curve." + }, + "form": { + "type": "string", + "title": "Form", + "description": "The form of the elliptic curve.", + "enum": [ + "Weierstrass", + "Edwards", + "TwistedEdwards", + "Montgomery" + ] + }, + "aliases": { + "type": "array", + "title": "Aliases", + "description": "List of aliases for this curve.", + "items": { + "type": "object", + "title": "Alias", + "description": "An alias for the curve.", + "additionalProperties": false, + "properties": { + "category": { + "type": "string", + "title": "Category", + "description": "The category of the alias." + }, + "name": { + "type": "string", + "title": "Name", + "description": "The name of the alias." + } + }, + "required": [ + "category", + "name" + ] + } + } + }, + "required": [ + "name", + "description", + "oid", + "form" + ] + } + } + }, + "required": [ + "name", + "description", + "curves" + ] + } + } + }, + "required": [ + "lastUpdated", + "algorithms", + "ellipticCurves" + ], + "definitions": { + "algorithmFamiliesEnum": { + "type": "string", + "title": "Algorithm Families", + "description": "An enum for the algorithm families.", + "enum": [ + "3DES", + "3GPP-XOR", + "A5/1", + "A5/2", + "AES", + "ARIA", + "Argon2", + "Ascon", + "BLAKE2", + "BLAKE3", + "BLS", + "Blowfish", + "CAMELLIA", + "CAST5", + "CAST6", + "CMAC", + "CMEA", + "CTR_DRBG", + "ChaCha", + "ChaCha20", + "DES", + "DSA", + "ECDH", + "ECDSA", + "ECIES", + "EdDSA", + "ElGamal", + "FFDH", + "Fortuna", + "GOST", + "HC", + "HKDF", + "HMAC", + "HMAC_DRBG", + "HPKE", + "Hash_DRBG", + "IDEA", + "IKE-PRF", + "J-PAKE", + "LMS", + "MD2", + "MD4", + "MD5", + "MILENAGE", + "ML-DSA", + "ML-KEM", + "MQV", + "OPAQUE", + "PBES1", + "PBES2", + "PBKDF1", + "PBKDF2", + "PBMAC1", + "Poly1305", + "RABBIT", + "RC2", + "RC4", + "RC5", + "RC6", + "RIPEMD", + "RSAES-OAEP", + "RSAES-PKCS1", + "RSASSA-PKCS1", + "RSASSA-PSS", + "SEED", + "SHA-1", + "SHA-2", + "SHA-3", + "SLH-DSA", + "SM2", + "SM3", + "SM4", + "SM9", + "SNOW3G", + "SP800-108", + "SPAKE2", + "SPAKE2PLUS", + "SRP", + "Salsa20", + "Serpent", + "SipHash", + "Skipjack", + "TUAK", + "Twofish", + "UMAC", + "Whirlpool", + "X3DH", + "XMSS", + "Yarrow", + "ZUC", + "bcrypt", + "scrypt", + "yescrypt" + ] + }, + "ellipticCurvesEnum": { + "type": "string", + "enum": [ + "anssi/FRP256v1", + "bls/BLS12-377", + "bls/BLS12-381", + "bls/BLS12-446", + "bls/BLS12-455", + "bls/BLS12-638", + "bls/BLS24-477", + "bls/Bandersnatch", + "bn/bn158", + "bn/bn190", + "bn/bn222", + "bn/bn254", + "bn/bn286", + "bn/bn318", + "bn/bn350", + "bn/bn382", + "bn/bn414", + "bn/bn446", + "bn/bn478", + "bn/bn510", + "bn/bn542", + "bn/bn574", + "bn/bn606", + "bn/bn638", + "brainpool/brainpoolP160r1", + "brainpool/brainpoolP160t1", + "brainpool/brainpoolP192r1", + "brainpool/brainpoolP192t1", + "brainpool/brainpoolP224r1", + "brainpool/brainpoolP224t1", + "brainpool/brainpoolP256r1", + "brainpool/brainpoolP256t1", + "brainpool/brainpoolP320r1", + "brainpool/brainpoolP320t1", + "brainpool/brainpoolP384r1", + "brainpool/brainpoolP384t1", + "brainpool/brainpoolP512r1", + "brainpool/brainpoolP512t1", + "gost/gost256", + "gost/gost512", + "gost/id-GostR3410-2001-CryptoPro-A-ParamSet", + "gost/id-GostR3410-2001-CryptoPro-B-ParamSet", + "gost/id-GostR3410-2001-CryptoPro-C-ParamSet", + "gost/id-tc26-gost-3410-12-512-paramSetA", + "gost/id-tc26-gost-3410-12-512-paramSetB", + "gost/id-tc26-gost-3410-2012-256-paramSetA", + "gost/id-tc26-gost-3410-2012-512-paramSetC", + "mnt/mnt1", + "mnt/mnt2/1", + "mnt/mnt2/2", + "mnt/mnt3/1", + "mnt/mnt3/2", + "mnt/mnt3/3", + "mnt/mnt4", + "mnt/mnt5/1", + "mnt/mnt5/2", + "mnt/mnt5/3", + "nist/B-163", + "nist/B-233", + "nist/B-283", + "nist/B-409", + "nist/B-571", + "nist/K-163", + "nist/K-233", + "nist/K-283", + "nist/K-409", + "nist/K-571", + "nist/P-192", + "nist/P-224", + "nist/P-256", + "nist/P-384", + "nist/P-521", + "nums/ed-254-mont", + "nums/ed-255-mers", + "nums/ed-256-mont", + "nums/ed-382-mont", + "nums/ed-383-mers", + "nums/ed-384-mont", + "nums/ed-510-mont", + "nums/ed-511-mers", + "nums/ed-512-mont", + "nums/numsp256d1", + "nums/numsp256t1", + "nums/numsp384d1", + "nums/numsp384t1", + "nums/numsp512d1", + "nums/numsp512t1", + "nums/w-254-mont", + "nums/w-255-mers", + "nums/w-256-mont", + "nums/w-382-mont", + "nums/w-383-mers", + "nums/w-384-mont", + "nums/w-510-mont", + "nums/w-511-mers", + "nums/w-512-mont", + "oakley/192-bit Random ECP Group", + "oakley/224-bit Random ECP Group", + "oakley/256-bit Random ECP Group", + "oakley/384-bit Random ECP Group", + "oakley/521-bit Random ECP Group", + "oakley/Oakley Group 3", + "oakley/Oakley Group 4", + "oscaa/SM2", + "other/BADA55-R-256", + "other/BADA55-VPR-224", + "other/BADA55-VPR2-224", + "other/BADA55-VR-224", + "other/BADA55-VR-256", + "other/BADA55-VR-384", + "other/Curve1174", + "other/Curve22103", + "other/Curve25519", + "other/Curve383187", + "other/Curve41417", + "other/Curve4417", + "other/Curve448", + "other/Curve67254", + "other/E-222", + "other/E-382", + "other/E-521", + "other/Ed25519", + "other/Ed448", + "other/Ed448-Goldilocks", + "other/FourQ", + "other/Fp224BN", + "other/Fp254BNa", + "other/Fp254BNb", + "other/Fp254n2BNa", + "other/Fp256BN", + "other/Fp384BN", + "other/Fp512BN", + "other/JubJub", + "other/M-221", + "other/M-383", + "other/M-511", + "other/MDC201601", + "other/Pallas", + "other/Tom-256", + "other/Tom-384", + "other/Tom-521", + "other/Tweedledee", + "other/Tweedledum", + "other/Vesta", + "other/ssc-160", + "other/ssc-192", + "other/ssc-224", + "other/ssc-256", + "other/ssc-288", + "other/ssc-320", + "other/ssc-384", + "other/ssc-512", + "secg/secp112r1", + "secg/secp112r2", + "secg/secp128r1", + "secg/secp128r2", + "secg/secp160k1", + "secg/secp160r1", + "secg/secp160r2", + "secg/secp192k1", + "secg/secp192r1", + "secg/secp224k1", + "secg/secp224r1", + "secg/secp256k1", + "secg/secp256r1", + "secg/secp384r1", + "secg/secp521r1", + "secg/sect113r1", + "secg/sect113r2", + "secg/sect131r1", + "secg/sect131r2", + "secg/sect163k1", + "secg/sect163r1", + "secg/sect163r2", + "secg/sect193r1", + "secg/sect193r2", + "secg/sect233k1", + "secg/sect233r1", + "secg/sect239k1", + "secg/sect283k1", + "secg/sect283r1", + "secg/sect409k1", + "secg/sect409r1", + "secg/sect571k1", + "secg/sect571r1", + "wtls/wap-wsg-idm-ecid-wtls1", + "wtls/wap-wsg-idm-ecid-wtls10", + "wtls/wap-wsg-idm-ecid-wtls11", + "wtls/wap-wsg-idm-ecid-wtls12", + "wtls/wap-wsg-idm-ecid-wtls3", + "wtls/wap-wsg-idm-ecid-wtls4", + "wtls/wap-wsg-idm-ecid-wtls5", + "wtls/wap-wsg-idm-ecid-wtls6", + "wtls/wap-wsg-idm-ecid-wtls7", + "wtls/wap-wsg-idm-ecid-wtls8", + "wtls/wap-wsg-idm-ecid-wtls9", + "x962/c2onb191v4", + "x962/c2onb191v5", + "x962/c2onb239v4", + "x962/c2onb239v5", + "x962/c2pnb163v1", + "x962/c2pnb163v2", + "x962/c2pnb163v3", + "x962/c2pnb176w1", + "x962/c2pnb208w1", + "x962/c2pnb272w1", + "x962/c2pnb304w1", + "x962/c2pnb368w1", + "x962/c2tnb191v1", + "x962/c2tnb191v2", + "x962/c2tnb191v3", + "x962/c2tnb239v1", + "x962/c2tnb239v2", + "x962/c2tnb239v3", + "x962/c2tnb359v1", + "x962/c2tnb431r1", + "x962/prime192v1", + "x962/prime192v2", + "x962/prime192v3", + "x962/prime239v1", + "x962/prime239v2", + "x962/prime239v3", + "x962/prime256v1", + "x963/ansip160k1", + "x963/ansip160r1", + "x963/ansip160r2", + "x963/ansip192k1", + "x963/ansip224k1", + "x963/ansip224r1", + "x963/ansip256k1", + "x963/ansip384r1", + "x963/ansip521r1", + "x963/ansit163k1", + "x963/ansit163r1", + "x963/ansit163r2", + "x963/ansit193r1", + "x963/ansit193r2", + "x963/ansit233k1", + "x963/ansit233r1", + "x963/ansit239k1", + "x963/ansit283k1", + "x963/ansit283r1", + "x963/ansit409k1", + "x963/ansit409r1", + "x963/ansit571k1", + "x963/ansit571r1" + ] + } + } +} \ No newline at end of file diff --git a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java index 14f2f4add4..77310c3cd1 100644 --- a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java @@ -680,6 +680,352 @@ public void testVulnerabilityParsing14_xml() throws Exception { assertTrue(parser.isValid(jsonString.getBytes(StandardCharsets.UTF_8), version)); } + // ==================== CycloneDX 1.7 License Tests ==================== + + @Test + public void schema17_testLicenseMixedChoice() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-license-choice-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseMixedChoice_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-license-choice-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseExpressionWithText() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-license-expression-with-text-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseExpressionWithText_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-license-expression-with-text-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseExpressionWithLicensing() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-license-expression-with-licensing-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseExpressionWithLicensing_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-license-expression-with-licensing-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseDeclaredConcludedMix() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-license-declared-concluded-mix-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseDeclaredConcludedMix_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-license-declared-concluded-mix-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + // ==================== CycloneDX 1.7 Citation Tests ==================== + + @Test + public void schema17_testCitations() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-citations-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCitations_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-citations-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + // ==================== CycloneDX 1.7 Patent Tests ==================== + + @Test + public void schema17_testPatent() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-patent-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testPatent_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-patent-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + // ==================== CycloneDX 1.7 External Component Tests ==================== + + @Test + public void schema17_testComponentExternalVersionRange() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-component-external-with-versionRange.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalVersionRange_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-component-external-with-versionRange.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalWithVersion() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-component-external-with-version.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalWithVersion_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-component-external-with-version.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalWithoutVersion() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-component-external-without-version.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalWithoutVersion_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-component-external-without-version.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testExternalReferenceProperties() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-external-reference-properties-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testExternalReferenceProperties_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-external-reference-properties-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testMetadataDistribution() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-metadata-distribution-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testMetadataDistribution_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-metadata-distribution-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + // ==================== CycloneDX 1.7 Cryptography Tests ==================== + + @Test + public void schema17_testCryptographyFull() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-cryptography-full-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyFull_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-cryptography-full-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyCertificateAdvanced() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-cryptography-certificate-advanced-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyCertificateAdvanced_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-cryptography-certificate-advanced-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyImplementation() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-cryptography-implementation-1.7.json"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyImplementation_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonXmlBom("/1.7/valid-cryptography-implementation-1.7.xml"); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + File loadedFile = writeToFile(generator.toJsonString()); + + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + private void assertExternalReferenceInfo(Bom bom) { assertEquals(3, bom.getExternalReferences().size()); assertEquals(3, bom.getComponents().get(0).getExternalReferences().size()); diff --git a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java index 91debb101a..406f5d44c1 100644 --- a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java @@ -398,7 +398,7 @@ public void testIssue439Regression_xmlEmptyLicense() throws Exception { assertFalse(xmlString.isEmpty()); XmlParser parser = new XmlParser(); - assertTrue(parser.isValid(xmlString.getBytes(StandardCharsets.UTF_8))); + assertTrue(parser.isValid(xmlString.getBytes(StandardCharsets.UTF_8), version)); } @Test @@ -869,6 +869,352 @@ public void testIssue408Regression_jsonToXml_externalReferenceBom() throws Excep assertTrue(parser.isValid(loadedFile, version)); } + // ==================== CycloneDX 1.7 License Tests ==================== + + @Test + public void schema17_testLicenseMixedChoice() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-license-choice-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseMixedChoice_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-license-choice-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseExpressionWithText() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-license-expression-with-text-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseExpressionWithText_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-license-expression-with-text-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseExpressionWithLicensing() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-license-expression-with-licensing-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseExpressionWithLicensing_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-license-expression-with-licensing-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseDeclaredConcludedMix() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-license-declared-concluded-mix-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testLicenseDeclaredConcludedMix_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-license-declared-concluded-mix-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + // ==================== CycloneDX 1.7 Citation Tests ==================== + + @Test + public void schema17_testCitations() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-citations-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCitations_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-citations-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + // ==================== CycloneDX 1.7 Patent Tests ==================== + + @Test + public void schema17_testPatent() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-patent-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testPatent_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-patent-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + // ==================== CycloneDX 1.7 External Component Tests ==================== + + @Test + public void schema17_testComponentExternalVersionRange() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-component-external-with-versionRange.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalVersionRange_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-component-external-with-versionRange.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalWithVersion() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-component-external-with-version.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalWithVersion_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-component-external-with-version.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalWithoutVersion() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-component-external-without-version.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testComponentExternalWithoutVersion_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-component-external-without-version.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testExternalReferenceProperties() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-external-reference-properties-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testExternalReferenceProperties_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-external-reference-properties-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testMetadataDistribution() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-metadata-distribution-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testMetadataDistribution_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-metadata-distribution-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + // ==================== CycloneDX 1.7 Cryptography Tests ==================== + + @Test + public void schema17_testCryptographyFull() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-cryptography-full-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyFull_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-cryptography-full-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyCertificateAdvanced() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-cryptography-certificate-advanced-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyCertificateAdvanced_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-cryptography-certificate-advanced-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyImplementation() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonJsonBom("/1.7/valid-cryptography-implementation-1.7.json"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + + @Test + public void schema17_testCryptographyImplementation_xml() throws Exception { + Version version = Version.VERSION_17; + Bom bom = createCommonBomXml("/1.7/valid-cryptography-implementation-1.7.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + File loadedFile = writeToFile(generator.toXmlString()); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + } + private void assertExternalReferenceInfo(Bom bom) { assertEquals(3, bom.getExternalReferences().size()); assertEquals(3, bom.getComponents().get(0).getExternalReferences().size()); diff --git a/src/test/java/org/cyclonedx/VersionFilteringTest.java b/src/test/java/org/cyclonedx/VersionFilteringTest.java new file mode 100644 index 0000000000..5b60ee3394 --- /dev/null +++ b/src/test/java/org/cyclonedx/VersionFilteringTest.java @@ -0,0 +1,297 @@ +/* + * This file is part of CycloneDX Core (Java). + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright (c) OWASP Foundation. All Rights Reserved. + */ +package org.cyclonedx; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.stream.Stream; + +import org.cyclonedx.exception.GeneratorException; +import org.cyclonedx.exception.ParseException; +import org.cyclonedx.generators.BomGeneratorFactory; +import org.cyclonedx.generators.json.BomJsonGenerator; +import org.cyclonedx.generators.xml.BomXmlGenerator; +import org.cyclonedx.model.Bom; +import org.cyclonedx.model.Component; +import org.cyclonedx.model.ExternalReference; +import org.cyclonedx.model.Hash; +import org.cyclonedx.model.Metadata; +import org.cyclonedx.model.ReleaseNotes; +import org.cyclonedx.model.Service; +import org.cyclonedx.parsers.JsonParser; +import org.cyclonedx.parsers.XmlParser; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.w3c.dom.Document; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Verifies that version-specific fields are correctly filtered when generating + * BOMs for older CycloneDX versions. A BOM populated with features from the + * latest version (1.7) must pass schema validation when generated for any + * supported target version. + * + * This test ensures that: + *
    + *
  • {@code @VersionFilter} annotations on model fields cause automatic removal + * by {@code CustomSerializerModifier} for classes using default Jackson serialization
  • + *
  • Custom serializers (LicenseChoiceSerializer, ExternalReferenceSerializer, etc.) + * properly check versions via {@code SerializerUtils.shouldSerializeField()}
  • + *
  • Enum constants with {@code @VersionFilter} (ExternalReference.Type, Hash.Algorithm, + * Component.Type) are filtered at serialization time
  • + *
+ */ +public class VersionFilteringTest { + + /** + * Creates a BOM populated with features spanning all CycloneDX versions. + * Fields from newer versions should be automatically filtered out when + * generating for older versions. + */ + private Bom createFullFeaturedBom() { + Bom bom = new Bom(); + bom.setSerialNumber("urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"); + + // Metadata with timestamp (v1.0+) + Metadata metadata = new Metadata(); + metadata.setTimestamp(new Date()); + bom.setMetadata(metadata); + + // Component with v1.0+ fields + Component component = new Component(); + component.setType(Component.Type.LIBRARY); + component.setName("acme-lib"); + component.setVersion("1.0.0"); + component.setPublisher("Acme Inc."); + component.setDescription("A test library"); + component.setModified(false); + + // bom-ref (v1.1+) + component.setBomRef("comp-1"); + + // ExternalReferences with types from various versions + List extRefs = new ArrayList<>(); + + // v1.0+ type + ExternalReference websiteRef = new ExternalReference(); + websiteRef.setType(ExternalReference.Type.WEBSITE); + websiteRef.setUrl("https://example.com"); + extRefs.add(websiteRef); + + // v1.4+ type + ExternalReference releaseNotesRef = new ExternalReference(); + releaseNotesRef.setType(ExternalReference.Type.RELEASE_NOTES); + releaseNotesRef.setUrl("https://example.com/releases"); + extRefs.add(releaseNotesRef); + + // v1.5+ type + ExternalReference attestationRef = new ExternalReference(); + attestationRef.setType(ExternalReference.Type.ATTESTATION); + attestationRef.setUrl("https://example.com/attestation"); + extRefs.add(attestationRef); + + // v1.6+ type + ExternalReference sourceDistRef = new ExternalReference(); + sourceDistRef.setType(ExternalReference.Type.SOURCE_DISTRIBUTION); + sourceDistRef.setUrl("https://example.com/source"); + extRefs.add(sourceDistRef); + + component.setExternalReferences(extRefs); + + // Hashes (v1.0+) + List hashes = new ArrayList<>(); + hashes.add(new Hash(Hash.Algorithm.SHA_256, + "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2")); + component.setHashes(hashes); + + bom.setComponents(Collections.singletonList(component)); + + // Service with releaseNotes (v1.4+) + Service service = new Service(); + service.setName("acme-service"); + service.setBomRef("svc-1"); + ReleaseNotes releaseNotes = new ReleaseNotes(); + releaseNotes.setType("major"); + releaseNotes.setTitle("v2.0"); + service.setReleaseNotes(releaseNotes); + bom.setServices(Collections.singletonList(service)); + + return bom; + } + + static Stream allXmlVersions() { + return Arrays.stream(Version.values()) + .filter(v -> v.getFormats().contains(Format.XML)) + .map(Arguments::of); + } + + static Stream allJsonVersions() { + return Arrays.stream(Version.values()) + .filter(v -> v.getFormats().contains(Format.JSON)) + .map(Arguments::of); + } + + @ParameterizedTest(name = "XML v{0}: full-featured BOM passes schema validation") + @MethodSource("allXmlVersions") + void xmlSchemaValidation(Version targetVersion) throws Exception { + Bom bom = createFullFeaturedBom(); + BomXmlGenerator generator = BomGeneratorFactory.createXml(targetVersion, bom); + Document doc = generator.generate(); + + byte[] xmlBytes = generator.toXmlString().getBytes(StandardCharsets.UTF_8); + XmlParser parser = new XmlParser(); + List errors = parser.validate(xmlBytes, targetVersion); + + assertTrue(errors.isEmpty(), + String.format("XML v%s schema validation failed with %d error(s): %s", + targetVersion.getVersionString(), errors.size(), + errors.isEmpty() ? "" : errors.get(0).getMessage())); + } + + @ParameterizedTest(name = "JSON v{0}: full-featured BOM passes schema validation") + @MethodSource("allJsonVersions") + void jsonSchemaValidation(Version targetVersion) throws Exception { + Bom bom = createFullFeaturedBom(); + BomJsonGenerator generator = BomGeneratorFactory.createJson(targetVersion, bom); + String json = generator.toJsonString(); + + JsonParser parser = new JsonParser(); + List errors = parser.validate(json.getBytes(StandardCharsets.UTF_8), targetVersion); + + assertTrue(errors.isEmpty(), + String.format("JSON v%s schema validation failed with %d error(s): %s", + targetVersion.getVersionString(), errors.size(), + errors.isEmpty() ? "" : errors.get(0).getMessage())); + } + + @ParameterizedTest(name = "XML v{0}: version-gated ExternalReference types are filtered") + @MethodSource("allXmlVersions") + void xmlExternalReferenceTypeFiltering(Version targetVersion) throws Exception { + Bom bom = createFullFeaturedBom(); + BomXmlGenerator generator = BomGeneratorFactory.createXml(targetVersion, bom); + String xml = generator.toXmlString(); + + // externalReferences on Component are v1.1+ (VersionFilter(VERSION_11)) + if (targetVersion.getVersion() >= Version.VERSION_11.getVersion()) { + // WEBSITE (v1.0+) should be present when externalReferences are supported + assertTrue(xml.contains("https://example.com"), + "WEBSITE reference should be present in v" + targetVersion.getVersionString()); + } + + // RELEASE_NOTES (v1.4+) + if (targetVersion.getVersion() < Version.VERSION_14.getVersion()) { + assertFalse(xml.contains("release-notes"), + "release-notes type should not appear in v" + targetVersion.getVersionString()); + } + + // ATTESTATION (v1.5+) + if (targetVersion.getVersion() < Version.VERSION_15.getVersion()) { + assertFalse(xml.contains("attestation"), + "attestation type should not appear in v" + targetVersion.getVersionString()); + } + + // SOURCE_DISTRIBUTION (v1.6+) + if (targetVersion.getVersion() < Version.VERSION_16.getVersion()) { + assertFalse(xml.contains("source-distribution"), + "source-distribution type should not appear in v" + targetVersion.getVersionString()); + } + } + + @ParameterizedTest(name = "JSON v{0}: version-gated ExternalReference types are filtered") + @MethodSource("allJsonVersions") + void jsonExternalReferenceTypeFiltering(Version targetVersion) throws Exception { + Bom bom = createFullFeaturedBom(); + BomJsonGenerator generator = BomGeneratorFactory.createJson(targetVersion, bom); + String json = generator.toJsonString(); + + // WEBSITE (v1.0+) should always be present + assertTrue(json.contains("website"), + "WEBSITE reference should be present in v" + targetVersion.getVersionString()); + + // RELEASE_NOTES (v1.4+) + if (targetVersion.getVersion() < Version.VERSION_14.getVersion()) { + assertFalse(json.contains("release-notes"), + "release-notes type should not appear in v" + targetVersion.getVersionString()); + } + + // ATTESTATION (v1.5+) + if (targetVersion.getVersion() < Version.VERSION_15.getVersion()) { + assertFalse(json.contains("attestation"), + "attestation type should not appear in v" + targetVersion.getVersionString()); + } + + // SOURCE_DISTRIBUTION (v1.6+) + if (targetVersion.getVersion() < Version.VERSION_16.getVersion()) { + assertFalse(json.contains("source-distribution"), + "source-distribution type should not appear in v" + targetVersion.getVersionString()); + } + } + + @ParameterizedTest(name = "JSON v{0}: Service.releaseNotes filtered before v1.4") + @MethodSource("allJsonVersions") + void jsonServiceReleaseNotesFiltering(Version targetVersion) throws Exception { + Bom bom = createFullFeaturedBom(); + BomJsonGenerator generator = BomGeneratorFactory.createJson(targetVersion, bom); + String json = generator.toJsonString(); + + if (targetVersion.getVersion() < Version.VERSION_14.getVersion()) { + assertFalse(json.contains("releaseNotes"), + "releaseNotes should not appear in JSON v" + targetVersion.getVersionString()); + } else { + assertTrue(json.contains("releaseNotes"), + "releaseNotes should appear in JSON v" + targetVersion.getVersionString()); + } + } + + @ParameterizedTest(name = "XML v{0}: Component.bomRef filtered before v1.1") + @MethodSource("allXmlVersions") + void xmlComponentBomRefFiltering(Version targetVersion) throws Exception { + Bom bom = createFullFeaturedBom(); + BomXmlGenerator generator = BomGeneratorFactory.createXml(targetVersion, bom); + String xml = generator.toXmlString(); + + if (targetVersion.getVersion() < Version.VERSION_11.getVersion()) { + assertFalse(xml.contains("bom-ref"), + "bom-ref should not appear in XML v" + targetVersion.getVersionString()); + } else { + assertTrue(xml.contains("bom-ref"), + "bom-ref should appear in XML v" + targetVersion.getVersionString()); + } + } + + @ParameterizedTest(name = "JSON v{0}: Component.bomRef filtered before v1.1") + @MethodSource("allJsonVersions") + void jsonComponentBomRefFiltering(Version targetVersion) throws Exception { + Bom bom = createFullFeaturedBom(); + BomJsonGenerator generator = BomGeneratorFactory.createJson(targetVersion, bom); + String json = generator.toJsonString(); + + // JSON starts at v1.2, so bom-ref should always be present + assertTrue(json.contains("bom-ref"), + "bom-ref should appear in JSON v" + targetVersion.getVersionString()); + } +} diff --git a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java index d0efdbbea1..1bd3973133 100644 --- a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java +++ b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java @@ -24,6 +24,7 @@ import org.cyclonedx.model.Component.Type; import org.cyclonedx.model.Dependency; import org.cyclonedx.model.ExternalReference; +import org.cyclonedx.model.TlpClassification; import org.cyclonedx.model.License; import org.cyclonedx.model.LicenseChoice; import org.cyclonedx.model.OrganizationalEntity; @@ -63,6 +64,13 @@ import org.cyclonedx.model.definition.Standard; import org.cyclonedx.model.license.Acknowledgement; import org.cyclonedx.model.license.Expression; +import org.cyclonedx.model.license.ExpressionDetailed; +import org.cyclonedx.model.license.ExpressionDetail; +import org.cyclonedx.model.Citation; +import org.cyclonedx.model.Patent; +import org.cyclonedx.model.PatentFamily; +import org.cyclonedx.model.PatentAssertion; +import org.cyclonedx.model.PatentItem; import org.junit.jupiter.api.Test; import java.io.File; import java.util.ArrayList; @@ -301,6 +309,244 @@ public void schema16_license_expression_acknowledgement() throws Exception { assertEquals(Acknowledgement.DECLARED, expression.getAcknowledgement()); } + @Test + public void schema17_license_mixed_choice() throws Exception { + final Bom bom = getJsonBom("1.7/valid-license-choice-1.7.json"); + + assertNotNull(bom.getComponents()); + Component component = bom.getComponents().get(0); + LicenseChoice lc = component.getLicenses(); + assertNotNull(lc); + assertNotNull(lc.getItems()); + assertEquals(4, lc.getItems().size()); + + // First item: license with id + assertNotNull(lc.getItems().get(0).getLicense()); + assertEquals("Apache-2.0", lc.getItems().get(0).getLicense().getId()); + + // Second item: expression + assertNotNull(lc.getItems().get(1).getExpression()); + assertEquals("EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", lc.getItems().get(1).getExpression().getValue()); + + // Third item: license with name and text + assertNotNull(lc.getItems().get(2).getLicense()); + assertEquals("My Own License", lc.getItems().get(2).getLicense().getName()); + assertNotNull(lc.getItems().get(2).getLicense().getAttachmentText()); + assertTrue(lc.getItems().get(2).getLicense().getAttachmentText().getText().contains("Lorem ipsum")); + + // Fourth item: expression-detailed + assertNotNull(lc.getItems().get(3).getExpressionDetailed()); + ExpressionDetailed ed = lc.getItems().get(3).getExpressionDetailed(); + assertEquals("LicenseRef-MIT-Style-2", ed.getExpression()); + assertNotNull(ed.getExpressionDetails()); + assertEquals(1, ed.getExpressionDetails().size()); + assertEquals("LicenseRef-MIT-Style-2", ed.getExpressionDetails().get(0).getLicenseIdentifier()); + assertEquals("https://example.com/license", ed.getExpressionDetails().get(0).getUrl()); + } + + @Test + public void schema17_license_expression_detailed_with_text() throws Exception { + final Bom bom = getJsonBom("1.7/valid-license-expression-with-text-1.7.json"); + + assertNotNull(bom.getComponents()); + Component component = bom.getComponents().get(0); + LicenseChoice lc = component.getLicenses(); + assertNotNull(lc); + assertNotNull(lc.getItems()); + assertEquals(1, lc.getItems().size()); + + ExpressionDetailed ed = lc.getItems().get(0).getExpressionDetailed(); + assertNotNull(ed); + assertEquals("LicenseRef-my-custom-license AND (EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0) AND MIT", ed.getExpression()); + assertEquals("my-application-license", ed.getBomRef()); + assertEquals(Acknowledgement.DECLARED, ed.getAcknowledgement()); + + assertNotNull(ed.getExpressionDetails()); + assertEquals(5, ed.getExpressionDetails().size()); + + // First detail: LicenseRef-my-custom-license + ExpressionDetail detail0 = ed.getExpressionDetails().get(0); + assertEquals("LicenseRef-my-custom-license", detail0.getLicenseIdentifier()); + assertNotNull(detail0.getText()); + assertTrue(detail0.getText().getText().contains("Lorem ipsum")); + assertEquals("https://my-application.example.com/license.txt", detail0.getUrl()); + + // Second detail: EPL-2.0 + ExpressionDetail detail1 = ed.getExpressionDetails().get(1); + assertEquals("EPL-2.0", detail1.getLicenseIdentifier()); + assertNotNull(detail1.getText()); + assertTrue(detail1.getText().getText().contains("Eclipse Public License")); + + // Third detail: GPL-2.0 WITH Classpath-exception-2.0 + ExpressionDetail detail2 = ed.getExpressionDetails().get(2); + assertEquals("GPL-2.0 WITH Classpath-exception-2.0", detail2.getLicenseIdentifier()); + assertNotNull(detail2.getText()); + assertTrue(detail2.getText().getText().contains("GNU GENERAL PUBLIC LICENSE")); + assertEquals("text/plain", detail2.getText().getContentType()); + + // Fourth detail: MIT (component B) + ExpressionDetail detail3 = ed.getExpressionDetails().get(3); + assertEquals("MIT", detail3.getLicenseIdentifier()); + assertEquals("LicenseDetails-component-B", detail3.getBomRef()); + assertNotNull(detail3.getText()); + assertTrue(detail3.getText().getText().contains("Component-B-Creators Inc")); + + // Fifth detail: MIT (component C) + ExpressionDetail detail4 = ed.getExpressionDetails().get(4); + assertEquals("MIT", detail4.getLicenseIdentifier()); + assertEquals("LicenseDetails-component-C", detail4.getBomRef()); + assertNotNull(detail4.getText()); + assertTrue(detail4.getText().getText().contains("Component-C-Creators Org")); + } + + @Test + public void schema17_license_expression_detailed_with_licensing() throws Exception { + final Bom bom = getJsonBom("1.7/valid-license-expression-with-licensing-1.7.json"); + + assertNotNull(bom.getComponents()); + Component component = bom.getComponents().get(0); + LicenseChoice lc = component.getLicenses(); + assertNotNull(lc); + assertNotNull(lc.getItems()); + assertEquals(1, lc.getItems().size()); + + ExpressionDetailed ed = lc.getItems().get(0).getExpressionDetailed(); + assertNotNull(ed); + assertEquals("LicenseRef-AcmeCommercialLicense", ed.getExpression()); + assertEquals("acme-license-1", ed.getBomRef()); + + assertNotNull(ed.getLicensing()); + assertNotNull(ed.getLicensing().getAltIds()); + assertEquals(2, ed.getLicensing().getAltIds().size()); + assertTrue(ed.getLicensing().getAltIds().contains("acme")); + assertTrue(ed.getLicensing().getAltIds().contains("acme-license")); + + assertNotNull(ed.getLicensing().getLicensor()); + assertNotNull(ed.getLicensing().getLicensor().getOrganization()); + assertEquals("Acme Inc", ed.getLicensing().getLicensor().getOrganization().getName()); + + assertNotNull(ed.getLicensing().getLicensee()); + assertNotNull(ed.getLicensing().getLicensee().getOrganization()); + assertEquals("Example Co.", ed.getLicensing().getLicensee().getOrganization().getName()); + + assertNotNull(ed.getLicensing().getPurchaser()); + assertNotNull(ed.getLicensing().getPurchaser().getIndividual()); + assertEquals("Samantha Wright", ed.getLicensing().getPurchaser().getIndividual().getName()); + + assertEquals("PO-12345", ed.getLicensing().getPurchaseOrder()); + + assertNotNull(ed.getLicensing().getLicenseTypes()); + assertEquals(1, ed.getLicensing().getLicenseTypes().size()); + } + + @Test + public void schema17_license_declared_concluded_mix() throws Exception { + final Bom bom = getJsonBom("1.7/valid-license-declared-concluded-mix-1.7.json"); + + assertNotNull(bom.getComponents()); + assertEquals(5, bom.getComponents().size()); + + // Situation A: Multiple declared licenses + concluded expression + Component sitA = bom.getComponents().get(0); + assertEquals("situation-A", sitA.getName()); + LicenseChoice lcA = sitA.getLicenses(); + assertNotNull(lcA); + assertNotNull(lcA.getItems()); + assertEquals(4, lcA.getItems().size()); + // 3 declared licenses + assertNotNull(lcA.getItems().get(0).getLicense()); + assertEquals("MIT", lcA.getItems().get(0).getLicense().getId()); + assertEquals(Acknowledgement.DECLARED, lcA.getItems().get(0).getLicense().getAcknowledgement()); + assertNotNull(lcA.getItems().get(1).getLicense()); + assertEquals("PostgreSQL", lcA.getItems().get(1).getLicense().getId()); + assertNotNull(lcA.getItems().get(2).getLicense()); + assertEquals("Apache Software License", lcA.getItems().get(2).getLicense().getName()); + // 1 concluded expression + assertNotNull(lcA.getItems().get(3).getExpression()); + assertEquals(Acknowledgement.CONCLUDED, lcA.getItems().get(3).getExpression().getAcknowledgement()); + + // Situation B: declared expression + concluded expression + Component sitB = bom.getComponents().get(1); + assertEquals("situation-B", sitB.getName()); + LicenseChoice lcB = sitB.getLicenses(); + assertNotNull(lcB); + assertNotNull(lcB.getItems()); + assertEquals(2, lcB.getItems().size()); + assertNotNull(lcB.getItems().get(0).getExpression()); + assertEquals(Acknowledgement.DECLARED, lcB.getItems().get(0).getExpression().getAcknowledgement()); + assertNotNull(lcB.getItems().get(1).getExpression()); + assertEquals(Acknowledgement.CONCLUDED, lcB.getItems().get(1).getExpression().getAcknowledgement()); + + // Situation C: declared expression + concluded license ID + Component sitC = bom.getComponents().get(2); + assertEquals("situation-C", sitC.getName()); + LicenseChoice lcC = sitC.getLicenses(); + assertNotNull(lcC); + assertNotNull(lcC.getItems()); + assertEquals(2, lcC.getItems().size()); + assertNotNull(lcC.getItems().get(0).getExpression()); + assertEquals(Acknowledgement.DECLARED, lcC.getItems().get(0).getExpression().getAcknowledgement()); + assertNotNull(lcC.getItems().get(1).getLicense()); + assertEquals("GPL-3.0-only", lcC.getItems().get(1).getLicense().getId()); + assertEquals(Acknowledgement.CONCLUDED, lcC.getItems().get(1).getLicense().getAcknowledgement()); + + // Situation D: declared expression-detailed with texts + concluded license with text + Component sitD = bom.getComponents().get(3); + assertEquals("situation-D", sitD.getName()); + LicenseChoice lcD = sitD.getLicenses(); + assertNotNull(lcD); + assertNotNull(lcD.getItems()); + assertEquals(2, lcD.getItems().size()); + assertNotNull(lcD.getItems().get(0).getExpressionDetailed()); + ExpressionDetailed edD = lcD.getItems().get(0).getExpressionDetailed(); + assertEquals("GPL-3.0-or-later OR GPL-2.0", edD.getExpression()); + assertEquals(Acknowledgement.DECLARED, edD.getAcknowledgement()); + assertNotNull(edD.getExpressionDetails()); + assertEquals(2, edD.getExpressionDetails().size()); + assertNotNull(lcD.getItems().get(1).getLicense()); + assertEquals(Acknowledgement.CONCLUDED, lcD.getItems().get(1).getLicense().getAcknowledgement()); + + // Situation E: declared licenses with URLs + concluded expression-detailed with URLs + Component sitE = bom.getComponents().get(4); + assertEquals("situation-E", sitE.getName()); + LicenseChoice lcE = sitE.getLicenses(); + assertNotNull(lcE); + assertNotNull(lcE.getItems()); + assertEquals(4, lcE.getItems().size()); + // 3 declared licenses with URLs + assertNotNull(lcE.getItems().get(0).getLicense()); + assertEquals("https://example.com/licenses/MIT", lcE.getItems().get(0).getLicense().getUrl()); + // 1 concluded expression-detailed with URLs + assertNotNull(lcE.getItems().get(3).getExpressionDetailed()); + ExpressionDetailed edE = lcE.getItems().get(3).getExpressionDetailed(); + assertEquals(Acknowledgement.CONCLUDED, edE.getAcknowledgement()); + assertNotNull(edE.getExpressionDetails()); + assertEquals(3, edE.getExpressionDetails().size()); + assertEquals("https://example.com/licenses/MIT", edE.getExpressionDetails().get(0).getUrl()); + } + + @Test + public void schema17_license_backward_compat_getLicenses() throws Exception { + final Bom bom = getJsonBom("1.7/valid-license-choice-1.7.json"); + + Component component = bom.getComponents().get(0); + LicenseChoice lc = component.getLicenses(); + + // Deprecated getLicenses() should still return only License items + assertNotNull(lc.getLicenses()); + assertEquals(2, lc.getLicenses().size()); + assertEquals("Apache-2.0", lc.getLicenses().get(0).getId()); + assertEquals("My Own License", lc.getLicenses().get(1).getName()); + + // Deprecated getExpression() should return the first expression + assertNotNull(lc.getExpression()); + assertEquals("EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", lc.getExpression().getValue()); + + // getExpressionDetailed() should return the first expression-detailed + assertNotNull(lc.getExpressionDetailed()); + assertEquals("LicenseRef-MIT-Style-2", lc.getExpressionDetailed().getExpression()); + } + @Test public void schema16_ml_considerations() throws Exception { final Bom bom = getJsonBom("1.6/valid-machine-learning-considerations-env-1.6.json"); @@ -601,4 +847,193 @@ public void testIssue492Regression() throws Exception { final Bom bom = getJsonBom("regression/issue492.json"); assertEquals(2, bom.getMetadata().getTools().size()); } + + // ==================== CycloneDX 1.7 Citation Tests ==================== + + @Test + public void schema17_citations() throws Exception { + final Bom bom = getJsonBom("1.7/valid-citations-1.7.json"); + + assertNotNull(bom.getCitations()); + assertEquals(4, bom.getCitations().size()); + + // Citation 1: pointers + attributedTo + Citation c1 = bom.getCitations().get(0); + assertEquals("citation-1", c1.getBomRef()); + assertNotNull(c1.getPointers()); + assertEquals(1, c1.getPointers().size()); + assertEquals("/components/0/name", c1.getPointers().get(0)); + assertNotNull(c1.getTimestamp()); + assertEquals("person-1", c1.getAttributedTo()); + assertNull(c1.getProcess()); + assertNotNull(c1.getNote()); + + // Citation 2: pointers + process + Citation c2 = bom.getCitations().get(1); + assertEquals("citation-2", c2.getBomRef()); + assertNotNull(c2.getPointers()); + assertEquals("task-license-scan", c2.getProcess()); + assertNull(c2.getAttributedTo()); + + // Citation 3: expressions + process + Citation c3 = bom.getCitations().get(2); + assertEquals("citation-3", c3.getBomRef()); + assertNotNull(c3.getExpressions()); + assertEquals(1, c3.getExpressions().size()); + assertNull(c3.getPointers()); + + // Citation 4: expressions + attributedTo + process + Citation c4 = bom.getCitations().get(3); + assertEquals("citation-4", c4.getBomRef()); + assertNotNull(c4.getExpressions()); + assertEquals("scan-tool-1", c4.getAttributedTo()); + assertEquals("task-license-scan", c4.getProcess()); + } + + // ==================== CycloneDX 1.7 External Component Tests ==================== + + @Test + public void schema17_component_external_with_versionRange() throws Exception { + final Bom bom = getJsonBom("1.7/valid-component-external-with-versionRange.json"); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component c = bom.getComponents().get(0); + assertEquals("libcurl", c.getName()); + assertEquals(true, c.getIsExternal()); + assertEquals("vers:generic/>=8.7.1|<9.0.0", c.getVersionRange()); + assertNull(c.getVersion()); + assertEquals("libcurl ^8.7.1", c.getDescription()); + } + + @Test + public void schema17_component_external_with_version() throws Exception { + final Bom bom = getJsonBom("1.7/valid-component-external-with-version.json"); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component c = bom.getComponents().get(0); + assertEquals("Ubuntu", c.getName()); + assertEquals(true, c.getIsExternal()); + assertEquals("24.04", c.getVersion()); + assertNull(c.getVersionRange()); + } + + @Test + public void schema17_component_external_without_version() throws Exception { + final Bom bom = getJsonBom("1.7/valid-component-external-without-version.json"); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component c = bom.getComponents().get(0); + assertEquals("Windows 11 (64bit)", c.getName()); + assertEquals(true, c.getIsExternal()); + assertNull(c.getVersion()); + assertNull(c.getVersionRange()); + } + + @Test + public void schema17_external_reference_properties() throws Exception { + final Bom bom = getJsonBom("1.7/valid-external-reference-properties-1.7.json"); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component c = bom.getComponents().get(0); + assertNotNull(c.getExternalReferences()); + assertEquals(1, c.getExternalReferences().size()); + + ExternalReference ref = c.getExternalReferences().get(0); + assertEquals(ExternalReference.Type.COMPONENT_ANALYSIS_REPORT, ref.getType()); + assertEquals("http://example.com/extref/component-analysis-report", ref.getUrl()); + assertNotNull(ref.getProperties()); + assertEquals(2, ref.getProperties().size()); + assertEquals("author", ref.getProperties().get(0).getName()); + assertEquals("John Doe", ref.getProperties().get(0).getValue()); + assertEquals("timestamp", ref.getProperties().get(1).getName()); + } + + @Test + public void schema17_metadata_distribution() throws Exception { + final Bom bom = getJsonBom("1.7/valid-metadata-distribution-1.7.json"); + + assertNotNull(bom.getMetadata()); + assertNotNull(bom.getMetadata().getDistributionConstraints()); + assertEquals(TlpClassification.RED, bom.getMetadata().getDistributionConstraints().getTlp()); + } + + // ==================== CycloneDX 1.7 Patent Tests ==================== + + @Test + public void schema17_patent() throws Exception { + final Bom bom = getJsonBom("1.7/valid-patent-1.7.json"); + + // Definitions patents + assertNotNull(bom.getDefinitions()); + assertNotNull(bom.getDefinitions().getPatents()); + assertEquals(4, bom.getDefinitions().getPatents().size()); + + // First 3 are patents + PatentItem item1 = bom.getDefinitions().getPatents().get(0); + assertNotNull(item1.getPatent()); + assertNull(item1.getPatentFamily()); + Patent p1 = item1.getPatent(); + assertEquals("patent-1", p1.getBomRef()); + assertEquals("US1234567890", p1.getPatentNumber()); + assertEquals("US", p1.getJurisdiction()); + assertEquals("Efficient Data Processing Algorithm", p1.getTitle()); + assertNotNull(p1.getFilingDate()); + assertNotNull(p1.getGrantDate()); + assertEquals(Patent.PatentLegalStatus.IN_FORCE, p1.getPatentLegalStatus()); + assertNotNull(p1.getPatentAssignee()); + assertEquals(1, p1.getPatentAssignee().size()); + assertNotNull(p1.getExternalReferences()); + assertEquals(1, p1.getExternalReferences().size()); + assertEquals(ExternalReference.Type.PATENT, p1.getExternalReferences().get(0).getType()); + + // Patent 2 has priority application + Patent p2 = bom.getDefinitions().getPatents().get(1).getPatent(); + assertEquals("patent-2", p2.getBomRef()); + assertNotNull(p2.getPriorityApplication()); + assertEquals("US1234567890", p2.getPriorityApplication().getApplicationNumber()); + assertEquals("US", p2.getPriorityApplication().getJurisdiction()); + + // Fourth is a patent family + PatentItem item4 = bom.getDefinitions().getPatents().get(3); + assertNull(item4.getPatent()); + assertNotNull(item4.getPatentFamily()); + PatentFamily pf = item4.getPatentFamily(); + assertEquals("patent-family-1", pf.getBomRef()); + assertEquals("PF-2023001", pf.getFamilyId()); + assertNotNull(pf.getMembers()); + assertEquals(2, pf.getMembers().size()); + assertTrue(pf.getMembers().contains("patent-1")); + assertTrue(pf.getMembers().contains("patent-2")); + assertNotNull(pf.getExternalReferences()); + assertEquals(ExternalReference.Type.PATENT_FAMILY, pf.getExternalReferences().get(0).getType()); + + // Component patent assertions + assertNotNull(bom.getComponents()); + Component comp = bom.getComponents().get(0); + assertNotNull(comp.getPatentAssertions()); + assertEquals(2, comp.getPatentAssertions().size()); + PatentAssertion pa1 = comp.getPatentAssertions().get(0); + assertEquals("patent-assertion-1", pa1.getBomRef()); + assertEquals(PatentAssertion.AssertionType.OWNERSHIP, pa1.getAssertionType()); + assertEquals(1, pa1.getPatentRefs().size()); + assertEquals("patent-1", pa1.getPatentRefs().get(0)); + + PatentAssertion pa2 = comp.getPatentAssertions().get(1); + assertEquals(PatentAssertion.AssertionType.LICENSE, pa2.getAssertionType()); + + // Service patent assertions + assertNotNull(bom.getServices()); + assertNotNull(bom.getServices().get(0).getPatentAssertions()); + assertEquals(1, bom.getServices().get(0).getPatentAssertions().size()); + PatentAssertion pa3 = bom.getServices().get(0).getPatentAssertions().get(0); + assertEquals(PatentAssertion.AssertionType.EXCLUSIVE_RIGHTS, pa3.getAssertionType()); + } } diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java index 0074cdbd32..385c6f9da5 100644 --- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java +++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java @@ -26,6 +26,7 @@ import org.cyclonedx.model.Component.Type; import org.cyclonedx.model.Dependency; import org.cyclonedx.model.ExternalReference; +import org.cyclonedx.model.TlpClassification; import org.cyclonedx.model.LicenseChoice; import org.cyclonedx.model.OrganizationalEntity; import org.cyclonedx.model.Pedigree; @@ -65,6 +66,13 @@ import org.cyclonedx.model.definition.Standard; import org.cyclonedx.model.license.Acknowledgement; import org.cyclonedx.model.license.Expression; +import org.cyclonedx.model.license.ExpressionDetailed; +import org.cyclonedx.model.license.ExpressionDetail; +import org.cyclonedx.model.Citation; +import org.cyclonedx.model.Patent; +import org.cyclonedx.model.PatentFamily; +import org.cyclonedx.model.PatentAssertion; +import org.cyclonedx.model.PatentItem; import org.junit.jupiter.api.Test; import java.io.File; @@ -450,6 +458,248 @@ public void schema16_license_expression_acknowledgement() throws Exception { assertEquals(Acknowledgement.DECLARED, expression.getAcknowledgement()); } + @Test + public void schema17_license_mixed_choice() throws Exception { + final Bom bom = getXmlBom("1.7/valid-license-choice-1.7.xml"); + + assertNotNull(bom.getComponents()); + Component component = bom.getComponents().get(0); + LicenseChoice lc = component.getLicenses(); + assertNotNull(lc); + assertNotNull(lc.getItems()); + assertEquals(4, lc.getItems().size()); + + // First item: license with id + assertNotNull(lc.getItems().get(0).getLicense()); + assertEquals("Apache-2.0", lc.getItems().get(0).getLicense().getId()); + + // Second item: license with name and text + assertNotNull(lc.getItems().get(1).getLicense()); + assertEquals("My Own License", lc.getItems().get(1).getLicense().getName()); + assertNotNull(lc.getItems().get(1).getLicense().getAttachmentText()); + assertTrue(lc.getItems().get(1).getLicense().getAttachmentText().getText().contains("Lorem ipsum")); + + // Third item: expression + assertNotNull(lc.getItems().get(2).getExpression()); + assertEquals("EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", lc.getItems().get(2).getExpression().getValue()); + + // Fourth item: expression-detailed + assertNotNull(lc.getItems().get(3).getExpressionDetailed()); + ExpressionDetailed ed = lc.getItems().get(3).getExpressionDetailed(); + assertEquals("LicenseRef-MIT-Style-2", ed.getExpression()); + assertNotNull(ed.getExpressionDetails()); + assertEquals(1, ed.getExpressionDetails().size()); + assertEquals("LicenseRef-MIT-Style-2", ed.getExpressionDetails().get(0).getLicenseIdentifier()); + assertEquals("https://example.com/license", ed.getExpressionDetails().get(0).getUrl()); + } + + @Test + public void schema17_license_expression_detailed_with_text() throws Exception { + final Bom bom = getXmlBom("1.7/valid-license-expression-with-text-1.7.xml"); + + assertNotNull(bom.getComponents()); + Component component = bom.getComponents().get(0); + LicenseChoice lc = component.getLicenses(); + assertNotNull(lc); + assertNotNull(lc.getItems()); + assertEquals(1, lc.getItems().size()); + + ExpressionDetailed ed = lc.getItems().get(0).getExpressionDetailed(); + assertNotNull(ed); + assertEquals("LicenseRef-my-custom-license AND (EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0) AND MIT", ed.getExpression()); + assertEquals("my-application-license", ed.getBomRef()); + assertEquals(Acknowledgement.DECLARED, ed.getAcknowledgement()); + + assertNotNull(ed.getExpressionDetails()); + assertEquals(5, ed.getExpressionDetails().size()); + + // First detail: LicenseRef-my-custom-license + ExpressionDetail detail0 = ed.getExpressionDetails().get(0); + assertEquals("LicenseRef-my-custom-license", detail0.getLicenseIdentifier()); + assertNotNull(detail0.getText()); + assertTrue(detail0.getText().getText().contains("Lorem ipsum")); + assertEquals("https://my-application.example.com/license.txt", detail0.getUrl()); + + // Second detail: EPL-2.0 + ExpressionDetail detail1 = ed.getExpressionDetails().get(1); + assertEquals("EPL-2.0", detail1.getLicenseIdentifier()); + assertNotNull(detail1.getText()); + assertTrue(detail1.getText().getText().contains("Eclipse Public License")); + + // Third detail: GPL-2.0 WITH Classpath-exception-2.0 + ExpressionDetail detail2 = ed.getExpressionDetails().get(2); + assertEquals("GPL-2.0 WITH Classpath-exception-2.0", detail2.getLicenseIdentifier()); + assertNotNull(detail2.getText()); + + assertTrue(detail2.getText().getText().contains("GNU GENERAL PUBLIC LICENSE")); + assertEquals("text/plain", detail2.getText().getContentType()); + + // Fourth detail: MIT (component B) + ExpressionDetail detail3 = ed.getExpressionDetails().get(3); + assertEquals("MIT", detail3.getLicenseIdentifier()); + assertEquals("LicenseDetails-component-B", detail3.getBomRef()); + assertNotNull(detail3.getText()); + assertTrue(detail3.getText().getText().contains("Component-B-Creators Inc")); + + // Fifth detail: MIT (component C) + ExpressionDetail detail4 = ed.getExpressionDetails().get(4); + assertEquals("MIT", detail4.getLicenseIdentifier()); + assertEquals("LicenseDetails-component-C", detail4.getBomRef()); + assertNotNull(detail4.getText()); + assertTrue(detail4.getText().getText().contains("Component-C-Creators Org")); + } + + @Test + public void schema17_license_expression_detailed_with_licensing() throws Exception { + final Bom bom = getXmlBom("1.7/valid-license-expression-with-licensing-1.7.xml"); + + assertNotNull(bom.getComponents()); + Component component = bom.getComponents().get(0); + LicenseChoice lc = component.getLicenses(); + assertNotNull(lc); + assertNotNull(lc.getItems()); + assertEquals(1, lc.getItems().size()); + + ExpressionDetailed ed = lc.getItems().get(0).getExpressionDetailed(); + assertNotNull(ed); + assertEquals("LicenseRef-AcmeCommercialLicense", ed.getExpression()); + assertEquals("acme-license-1", ed.getBomRef()); + + assertNotNull(ed.getLicensing()); + assertNotNull(ed.getLicensing().getAltIds()); + assertEquals(2, ed.getLicensing().getAltIds().size()); + assertTrue(ed.getLicensing().getAltIds().contains("acme")); + assertTrue(ed.getLicensing().getAltIds().contains("acme-license")); + + assertNotNull(ed.getLicensing().getLicensor()); + assertNotNull(ed.getLicensing().getLicensor().getOrganization()); + assertEquals("Acme Inc", ed.getLicensing().getLicensor().getOrganization().getName()); + + assertNotNull(ed.getLicensing().getLicensee()); + assertNotNull(ed.getLicensing().getLicensee().getOrganization()); + assertEquals("Example Co.", ed.getLicensing().getLicensee().getOrganization().getName()); + + assertNotNull(ed.getLicensing().getPurchaser()); + assertNotNull(ed.getLicensing().getPurchaser().getIndividual()); + assertEquals("Samantha Wright", ed.getLicensing().getPurchaser().getIndividual().getName()); + + assertEquals("PO-12345", ed.getLicensing().getPurchaseOrder()); + + assertNotNull(ed.getLicensing().getLicenseTypes()); + assertEquals(1, ed.getLicensing().getLicenseTypes().size()); + } + + @Test + public void schema17_license_declared_concluded_mix() throws Exception { + final Bom bom = getXmlBom("1.7/valid-license-declared-concluded-mix-1.7.xml"); + + assertNotNull(bom.getComponents()); + assertEquals(5, bom.getComponents().size()); + + // Situation A: Multiple declared licenses + concluded expression + Component sitA = bom.getComponents().get(0); + assertEquals("situation-A", sitA.getName()); + LicenseChoice lcA = sitA.getLicenses(); + assertNotNull(lcA); + assertNotNull(lcA.getItems()); + assertEquals(4, lcA.getItems().size()); + // 3 declared licenses + assertNotNull(lcA.getItems().get(0).getLicense()); + assertEquals("MIT", lcA.getItems().get(0).getLicense().getId()); + assertEquals(Acknowledgement.DECLARED, lcA.getItems().get(0).getLicense().getAcknowledgement()); + assertNotNull(lcA.getItems().get(1).getLicense()); + assertEquals("PostgreSQL", lcA.getItems().get(1).getLicense().getId()); + assertNotNull(lcA.getItems().get(2).getLicense()); + assertEquals("Apache Software License", lcA.getItems().get(2).getLicense().getName()); + // 1 concluded expression + assertNotNull(lcA.getItems().get(3).getExpression()); + assertEquals(Acknowledgement.CONCLUDED, lcA.getItems().get(3).getExpression().getAcknowledgement()); + + // Situation B: declared expression + concluded expression + Component sitB = bom.getComponents().get(1); + assertEquals("situation-B", sitB.getName()); + LicenseChoice lcB = sitB.getLicenses(); + assertNotNull(lcB); + assertNotNull(lcB.getItems()); + assertEquals(2, lcB.getItems().size()); + assertNotNull(lcB.getItems().get(0).getExpression()); + assertEquals(Acknowledgement.DECLARED, lcB.getItems().get(0).getExpression().getAcknowledgement()); + assertNotNull(lcB.getItems().get(1).getExpression()); + assertEquals(Acknowledgement.CONCLUDED, lcB.getItems().get(1).getExpression().getAcknowledgement()); + + // Situation C: declared expression + concluded license ID + // Note: XML deserializer groups by element type (license, expression, expression-detailed) + // so license comes before expression regardless of document order + Component sitC = bom.getComponents().get(2); + assertEquals("situation-C", sitC.getName()); + LicenseChoice lcC = sitC.getLicenses(); + assertNotNull(lcC); + assertNotNull(lcC.getItems()); + assertEquals(2, lcC.getItems().size()); + assertNotNull(lcC.getItems().get(0).getLicense()); + assertEquals("GPL-3.0-only", lcC.getItems().get(0).getLicense().getId()); + assertEquals(Acknowledgement.CONCLUDED, lcC.getItems().get(0).getLicense().getAcknowledgement()); + assertNotNull(lcC.getItems().get(1).getExpression()); + assertEquals(Acknowledgement.DECLARED, lcC.getItems().get(1).getExpression().getAcknowledgement()); + + // Situation D: declared expression-detailed with texts + concluded license with text + // Note: XML deserializer groups by element type: license first, then expression-detailed + Component sitD = bom.getComponents().get(3); + assertEquals("situation-D", sitD.getName()); + LicenseChoice lcD = sitD.getLicenses(); + assertNotNull(lcD); + assertNotNull(lcD.getItems()); + assertEquals(2, lcD.getItems().size()); + assertNotNull(lcD.getItems().get(0).getLicense()); + assertEquals(Acknowledgement.CONCLUDED, lcD.getItems().get(0).getLicense().getAcknowledgement()); + assertNotNull(lcD.getItems().get(1).getExpressionDetailed()); + ExpressionDetailed edD = lcD.getItems().get(1).getExpressionDetailed(); + assertEquals("GPL-3.0-or-later OR GPL-2.0", edD.getExpression()); + assertEquals(Acknowledgement.DECLARED, edD.getAcknowledgement()); + assertNotNull(edD.getExpressionDetails()); + assertEquals(2, edD.getExpressionDetails().size()); + + // Situation E: declared licenses with URLs + concluded expression-detailed with URLs + Component sitE = bom.getComponents().get(4); + assertEquals("situation-E", sitE.getName()); + LicenseChoice lcE = sitE.getLicenses(); + assertNotNull(lcE); + assertNotNull(lcE.getItems()); + assertEquals(4, lcE.getItems().size()); + // 3 declared licenses with URLs + assertNotNull(lcE.getItems().get(0).getLicense()); + assertEquals("https://example.com/licenses/MIT", lcE.getItems().get(0).getLicense().getUrl()); + // 1 concluded expression-detailed with URLs + assertNotNull(lcE.getItems().get(3).getExpressionDetailed()); + ExpressionDetailed edE = lcE.getItems().get(3).getExpressionDetailed(); + assertEquals(Acknowledgement.CONCLUDED, edE.getAcknowledgement()); + assertNotNull(edE.getExpressionDetails()); + assertEquals(3, edE.getExpressionDetails().size()); + assertEquals("https://example.com/licenses/MIT", edE.getExpressionDetails().get(0).getUrl()); + } + + @Test + public void schema17_license_backward_compat_getLicenses() throws Exception { + final Bom bom = getXmlBom("1.7/valid-license-choice-1.7.xml"); + + Component component = bom.getComponents().get(0); + LicenseChoice lc = component.getLicenses(); + + // Deprecated getLicenses() should still return only License items + assertNotNull(lc.getLicenses()); + assertEquals(2, lc.getLicenses().size()); + assertEquals("Apache-2.0", lc.getLicenses().get(0).getId()); + assertEquals("My Own License", lc.getLicenses().get(1).getName()); + + // Deprecated getExpression() should return the first expression + assertNotNull(lc.getExpression()); + assertEquals("EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0", lc.getExpression().getValue()); + + // getExpressionDetailed() should return the first expression-detailed + assertNotNull(lc.getExpressionDetailed()); + assertEquals("LicenseRef-MIT-Style-2", lc.getExpressionDetailed().getExpression()); + } + @Test public void schema16_ml_considerations() throws Exception { final Bom bom = getXmlBom("1.6/valid-machine-learning-considerations-env-1.6.xml"); @@ -751,6 +1001,176 @@ public void testIssue492Regression() throws Exception { assertEquals(2, bom.getMetadata().getTools().size()); } + // ==================== CycloneDX 1.7 Citation Tests ==================== + + @Test + public void schema17_citations() throws Exception { + final Bom bom = getXmlBom("1.7/valid-citations-1.7.xml"); + + assertNotNull(bom.getCitations()); + assertEquals(4, bom.getCitations().size()); + + // Citation 1: pointers + attributedTo + Citation c1 = bom.getCitations().get(0); + assertEquals("citation-1", c1.getBomRef()); + assertNotNull(c1.getPointers()); + assertEquals(1, c1.getPointers().size()); + assertEquals("/components/0/name", c1.getPointers().get(0)); + assertNotNull(c1.getTimestamp()); + assertEquals("person-1", c1.getAttributedTo()); + assertNotNull(c1.getNote()); + + // Citation 4: expressions + attributedTo + process + Citation c4 = bom.getCitations().get(3); + assertEquals("citation-4", c4.getBomRef()); + assertNotNull(c4.getExpressions()); + assertEquals(1, c4.getExpressions().size()); + assertEquals("scan-tool-1", c4.getAttributedTo()); + assertEquals("task-license-scan", c4.getProcess()); + } + + // ==================== CycloneDX 1.7 External Component Tests ==================== + + @Test + public void schema17_component_external_with_versionRange() throws Exception { + final Bom bom = getXmlBom("1.7/valid-component-external-with-versionRange.xml"); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component c = bom.getComponents().get(0); + assertEquals("libcurl", c.getName()); + assertEquals(true, c.getIsExternal()); + assertEquals("vers:generic/>=8.7.1|<9.0.0", c.getVersionRange()); + assertNull(c.getVersion()); + assertEquals("libcurl ^8.7.1", c.getDescription()); + } + + @Test + public void schema17_component_external_with_version() throws Exception { + final Bom bom = getXmlBom("1.7/valid-component-external-with-version.xml"); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component c = bom.getComponents().get(0); + assertEquals("Ubuntu", c.getName()); + assertEquals(true, c.getIsExternal()); + assertEquals("24H2", c.getVersion()); + assertNull(c.getVersionRange()); + } + + @Test + public void schema17_component_external_without_version() throws Exception { + final Bom bom = getXmlBom("1.7/valid-component-external-without-version.xml"); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component c = bom.getComponents().get(0); + assertEquals("Windows 11 (64bit)", c.getName()); + assertEquals(true, c.getIsExternal()); + assertNull(c.getVersion()); + assertNull(c.getVersionRange()); + } + + @Test + public void schema17_external_reference_properties() throws Exception { + final Bom bom = getXmlBom("1.7/valid-external-reference-properties-1.7.xml"); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component c = bom.getComponents().get(0); + assertNotNull(c.getExternalReferences()); + assertEquals(1, c.getExternalReferences().size()); + + ExternalReference ref = c.getExternalReferences().get(0); + assertEquals(ExternalReference.Type.COMPONENT_ANALYSIS_REPORT, ref.getType()); + assertNotNull(ref.getProperties()); + assertEquals(2, ref.getProperties().size()); + assertEquals("author", ref.getProperties().get(0).getName()); + assertEquals("John Doe", ref.getProperties().get(0).getValue()); + } + + @Test + public void schema17_metadata_distribution() throws Exception { + final Bom bom = getXmlBom("1.7/valid-metadata-distribution-1.7.xml"); + + assertNotNull(bom.getMetadata()); + assertNotNull(bom.getMetadata().getDistributionConstraints()); + assertEquals(TlpClassification.RED, bom.getMetadata().getDistributionConstraints().getTlp()); + } + + // ==================== CycloneDX 1.7 Patent Tests ==================== + + @Test + public void schema17_patent() throws Exception { + final Bom bom = getXmlBom("1.7/valid-patent-1.7.xml"); + + // Definitions patents + assertNotNull(bom.getDefinitions()); + assertNotNull(bom.getDefinitions().getPatents()); + assertEquals(4, bom.getDefinitions().getPatents().size()); + + // First 3 are patents + PatentItem item1 = bom.getDefinitions().getPatents().get(0); + assertNotNull(item1.getPatent()); + assertNull(item1.getPatentFamily()); + Patent p1 = item1.getPatent(); + assertEquals("patent-1", p1.getBomRef()); + assertEquals("US1234567890", p1.getPatentNumber()); + assertEquals("US", p1.getJurisdiction()); + assertEquals("Efficient Data Processing Algorithm", p1.getTitle()); + assertNotNull(p1.getFilingDate()); + assertNotNull(p1.getGrantDate()); + assertEquals(Patent.PatentLegalStatus.IN_FORCE, p1.getPatentLegalStatus()); + assertNotNull(p1.getPatentAssignee()); + assertEquals(1, p1.getPatentAssignee().size()); + assertNotNull(p1.getExternalReferences()); + assertEquals(1, p1.getExternalReferences().size()); + assertEquals(ExternalReference.Type.PATENT, p1.getExternalReferences().get(0).getType()); + + // Patent 2 has priority application + Patent p2 = bom.getDefinitions().getPatents().get(1).getPatent(); + assertEquals("patent-2", p2.getBomRef()); + assertNotNull(p2.getPriorityApplication()); + assertEquals("US1234567890", p2.getPriorityApplication().getApplicationNumber()); + assertEquals("US", p2.getPriorityApplication().getJurisdiction()); + + // Fourth is a patent family + PatentItem item4 = bom.getDefinitions().getPatents().get(3); + assertNull(item4.getPatent()); + assertNotNull(item4.getPatentFamily()); + PatentFamily pf = item4.getPatentFamily(); + assertEquals("patent-family-1", pf.getBomRef()); + assertEquals("PF-2023001", pf.getFamilyId()); + assertNotNull(pf.getMembers()); + assertEquals(2, pf.getMembers().size()); + assertTrue(pf.getMembers().contains("patent-1")); + assertTrue(pf.getMembers().contains("patent-2")); + assertNotNull(pf.getExternalReferences()); + assertEquals(ExternalReference.Type.PATENT_FAMILY, pf.getExternalReferences().get(0).getType()); + + // Component patent assertions + assertNotNull(bom.getComponents()); + Component comp = bom.getComponents().get(0); + assertNotNull(comp.getPatentAssertions()); + assertEquals(2, comp.getPatentAssertions().size()); + PatentAssertion pa1 = comp.getPatentAssertions().get(0); + assertEquals("patent-assertion-1", pa1.getBomRef()); + assertEquals(PatentAssertion.AssertionType.OWNERSHIP, pa1.getAssertionType()); + assertEquals(1, pa1.getPatentRefs().size()); + assertEquals("patent-1", pa1.getPatentRefs().get(0)); + + // Service patent assertions + assertNotNull(bom.getServices()); + assertNotNull(bom.getServices().get(0).getPatentAssertions()); + assertEquals(1, bom.getServices().get(0).getPatentAssertions().size()); + PatentAssertion pa3 = bom.getServices().get(0).getPatentAssertions().get(0); + assertEquals(PatentAssertion.AssertionType.EXCLUSIVE_RIGHTS, pa3.getAssertionType()); + } + @Test void validateShouldNotBeVulnerableToXxe() throws Exception { final byte[] bomBytes; diff --git a/src/test/java/org/cyclonedx/schema/JsonSchemaVerificationTest.java b/src/test/java/org/cyclonedx/schema/JsonSchemaVerificationTest.java index 0304af3ce4..5949b32ff2 100644 --- a/src/test/java/org/cyclonedx/schema/JsonSchemaVerificationTest.java +++ b/src/test/java/org/cyclonedx/schema/JsonSchemaVerificationTest.java @@ -55,6 +55,9 @@ else if (file.endsWith("-1.5.json")) { else if (file.endsWith("-1.6.json")) { schemaVersion = Version.VERSION_16; } + else if (file.endsWith("-1.7.json")) { + schemaVersion = Version.VERSION_17; + } else { schemaVersion = null; } diff --git a/src/test/java/org/cyclonedx/schema/XmlSchemaVerificationTest.java b/src/test/java/org/cyclonedx/schema/XmlSchemaVerificationTest.java index 03ff737b13..79671a9c73 100644 --- a/src/test/java/org/cyclonedx/schema/XmlSchemaVerificationTest.java +++ b/src/test/java/org/cyclonedx/schema/XmlSchemaVerificationTest.java @@ -62,6 +62,9 @@ else if (file.endsWith("-1.5.xml")) { else if (file.endsWith("-1.6.xml")) { schemaVersion = Version.VERSION_16; } + else if (file.endsWith("-1.7.xml")) { + schemaVersion = Version.VERSION_17; + } else { schemaVersion = null; } diff --git a/src/test/resources/1.7/valid-citations-1.7.json b/src/test/resources/1.7/valid-citations-1.7.json index 4c33f4195b..3b008a9327 100644 --- a/src/test/resources/1.7/valid-citations-1.7.json +++ b/src/test/resources/1.7/valid-citations-1.7.json @@ -69,7 +69,7 @@ "name": "My Scan Tool" } ], - "bom-ref": "workflow-1", + "bom-ref": "formula-1", "workflows": [ { "bom-ref": "workflow-1",