From c6e144d17a214933c5bbbc0da40d707e7362e58e Mon Sep 17 00:00:00 2001 From: Alex Alzate Date: Mon, 19 Jan 2026 19:49:56 -0500 Subject: [PATCH 1/8] Add new classes for new types --- .../java/org/cyclonedx/CycloneDxSchema.java | 33 +- src/main/java/org/cyclonedx/Version.java | 4 +- src/main/java/org/cyclonedx/model/Bom.java | 34 +++ .../java/org/cyclonedx/model/Citation.java | 118 ++++++++ .../org/cyclonedx/model/Classifications.java | 60 ++++ .../java/org/cyclonedx/model/Component.java | 38 +++ .../model/DistributionConstraints.java | 60 ++++ .../cyclonedx/model/ExternalReference.java | 12 + .../org/cyclonedx/model/LicenseChoice.java | 24 +- .../java/org/cyclonedx/model/Metadata.java | 13 +- src/main/java/org/cyclonedx/model/Patent.java | 284 ++++++++++++++++++ .../org/cyclonedx/model/PatentAssertion.java | 178 +++++++++++ .../org/cyclonedx/model/PatentFamily.java | 103 +++++++ .../cyclonedx/model/PriorityApplication.java | 84 ++++++ .../java/org/cyclonedx/model/Service.java | 14 + .../cyclonedx/model/TlpClassification.java | 60 ++++ .../model/component/crypto/CipherSuite.java | 35 ++- .../model/license/ExpressionDetail.java | 98 ++++++ .../model/license/ExpressionDetailed.java | 131 ++++++++ 19 files changed, 1372 insertions(+), 11 deletions(-) create mode 100644 src/main/java/org/cyclonedx/model/Citation.java create mode 100644 src/main/java/org/cyclonedx/model/Classifications.java create mode 100644 src/main/java/org/cyclonedx/model/DistributionConstraints.java create mode 100644 src/main/java/org/cyclonedx/model/Patent.java create mode 100644 src/main/java/org/cyclonedx/model/PatentAssertion.java create mode 100644 src/main/java/org/cyclonedx/model/PatentFamily.java create mode 100644 src/main/java/org/cyclonedx/model/PriorityApplication.java create mode 100644 src/main/java/org/cyclonedx/model/TlpClassification.java create mode 100644 src/main/java/org/cyclonedx/model/license/ExpressionDetail.java create mode 100644 src/main/java/org/cyclonedx/model/license/ExpressionDetailed.java diff --git a/src/main/java/org/cyclonedx/CycloneDxSchema.java b/src/main/java/org/cyclonedx/CycloneDxSchema.java index 1e942272a9..b279e022f4 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,8 @@ 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()); JsonNode schemaNode = mapper.readTree(spdxInstream); final MapSchemaMapper offlineSchemaMapper = new MapSchemaMapper(offlineMappings); @@ -127,9 +131,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 +166,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 +279,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/model/Bom.java b/src/main/java/org/cyclonedx/model/Bom.java index 57327c8b67..f9a77aa740 100644 --- a/src/main/java/org/cyclonedx/model/Bom.java +++ b/src/main/java/org/cyclonedx/model/Bom.java @@ -59,6 +59,8 @@ "formulation", "declarations", "definitions", + "distributionConstraints", + "citations", "signature" }) public class Bom extends ExtensibleElement { @@ -100,6 +102,12 @@ public class Bom extends ExtensibleElement { @VersionFilter(Version.VERSION_15) private List annotations; + @VersionFilter(Version.VERSION_17) + private DistributionConstraints distributionConstraints; + + @VersionFilter(Version.VERSION_17) + private List citations; + @JsonInclude(JsonInclude.Include.NON_EMPTY) private List properties; @@ -239,6 +247,32 @@ public void setAnnotations(List annotations) { this.annotations = annotations; } + public DistributionConstraints getDistributionConstraints() { + return distributionConstraints; + } + + public void setDistributionConstraints(DistributionConstraints distributionConstraints) { + this.distributionConstraints = distributionConstraints; + } + + @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..51c8b8a1d0 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/Citation.java @@ -0,0 +1,118 @@ +/* + * 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({"bomRef", "pointers", "expressions", "source", "timestamp"}) +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; + + private OrganizationalChoice source; + + @JsonSerialize(using = CustomDateSerializer.class) + private Date timestamp; + + 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 OrganizationalChoice getSource() { + return source; + } + + public void setSource(OrganizationalChoice source) { + this.source = source; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + @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(source, citation.source) && + Objects.equals(timestamp, citation.timestamp); + } + + @Override + public int hashCode() { + return Objects.hash(bomRef, pointers, expressions, source, timestamp); + } +} diff --git a/src/main/java/org/cyclonedx/model/Classifications.java b/src/main/java/org/cyclonedx/model/Classifications.java new file mode 100644 index 0000000000..04423cc603 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/Classifications.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; + +/** + * Data sharing and distribution classifications. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({"tlp"}) +public class Classifications { + + 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 Classifications)) return false; + Classifications that = (Classifications) o; + return tlp == that.tlp; + } + + @Override + public int hashCode() { + return Objects.hash(tlp); + } +} diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index 16c4718415..2cecbbf87d 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -158,6 +158,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; @@ -170,6 +175,10 @@ public String getScopeName() { 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 +229,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 +327,22 @@ public void setVersion(String version) { this.version = version; } + public String getVersionRange() { + return versionRange; + } + + public void setVersionRange(String versionRange) { + this.versionRange = versionRange; + } + + public Boolean getIsExternal() { + return isExternal; + } + + public void setIsExternal(Boolean isExternal) { + this.isExternal = isExternal; + } + public String getDescription() { return description; } @@ -565,6 +593,16 @@ public void setProvides(final List provides) { this.provides = provides; } + @JacksonXmlElementWrapper(localName = "patentAssertions") + @JacksonXmlProperty(localName = "patentAssertion") + public List getPatentAssertions() { + return patentAssertions; + } + + public void setPatentAssertions(List patentAssertions) { + this.patentAssertions = patentAssertions; + } + public Tags getTags() { return tags; } 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..57b20945e7 100644 --- a/src/main/java/org/cyclonedx/model/ExternalReference.java +++ b/src/main/java/org/cyclonedx/model/ExternalReference.java @@ -121,6 +121,18 @@ 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"), @JsonProperty("other") OTHER("other"); diff --git a/src/main/java/org/cyclonedx/model/LicenseChoice.java b/src/main/java/org/cyclonedx/model/LicenseChoice.java index c3618d095f..9d4ac8c3a9 100644 --- a/src/main/java/org/cyclonedx/model/LicenseChoice.java +++ b/src/main/java/org/cyclonedx/model/LicenseChoice.java @@ -26,7 +26,9 @@ 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.model.license.ExpressionDetailed; import org.cyclonedx.util.deserializer.LicenseDeserializer; @JsonIgnoreProperties(ignoreUnknown = true) @@ -38,6 +40,10 @@ public class LicenseChoice { private List license; private Expression expression; + @VersionFilter(Version.VERSION_17) + @JacksonXmlProperty(localName = "expression-detailed") + private ExpressionDetailed expressionDetailed; + @JacksonXmlProperty(localName = "license") public List getLicenses() { return license; @@ -46,6 +52,7 @@ public List getLicenses() { public void setLicenses(List licenses) { this.license = licenses; this.expression = null; + this.expressionDetailed = null; } public void addLicense(License license) { @@ -54,6 +61,7 @@ public void addLicense(License license) { } this.license.add(license); this.expression = null; + this.expressionDetailed = null; } @JacksonXmlProperty(localName = "expression") @@ -64,6 +72,17 @@ public Expression getExpression() { public void setExpression(Expression expression) { this.expression = expression; this.license = null; + this.expressionDetailed = null; + } + + public ExpressionDetailed getExpressionDetailed() { + return expressionDetailed; + } + + public void setExpressionDetailed(ExpressionDetailed expressionDetailed) { + this.expressionDetailed = expressionDetailed; + this.license = null; + this.expression = null; } @Override @@ -72,11 +91,12 @@ public boolean equals(Object o) { if (!(o instanceof LicenseChoice)) return false; LicenseChoice that = (LicenseChoice) o; return Objects.equals(license, that.license) && - Objects.equals(expression, that.expression); + Objects.equals(expression, that.expression) && + Objects.equals(expressionDetailed, that.expressionDetailed); } @Override public int hashCode() { - return Objects.hash(license, expression); + return Objects.hash(license, expression, expressionDetailed); } } diff --git a/src/main/java/org/cyclonedx/model/Metadata.java b/src/main/java/org/cyclonedx/model/Metadata.java index 4d57fe9192..b02ed92077 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", "classifications" }) @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 Classifications classifications; + public Date getTimestamp() { return timestamp; } @@ -199,6 +202,14 @@ public void addProperty(Property property) { this.properties.add(property); } + public Classifications getClassifications() { + return classifications; + } + + public void setClassifications(Classifications classifications) { + this.classifications = classifications; + } + public Lifecycles getLifecycles() { return lifecycles; } 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..80b37c7796 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/Patent.java @@ -0,0 +1,284 @@ +/* + * 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; + +/** + * Patent information. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({ + "bomRef", "patentNumber", "applicationNumber", "jurisdiction", "priorityApplication", + "publicationNumber", "title", "abstract", "filingDate", "grantDate", "expirationDate", + "patentLegalStatus", "patentAssignee", "externalReferences", "properties" +}) +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; + + @JsonSerialize(using = CustomDateSerializer.class) + private Date filingDate; + + @JsonSerialize(using = CustomDateSerializer.class) + private Date grantDate; + + @JsonSerialize(using = CustomDateSerializer.class) + private Date expirationDate; + + private PatentLegalStatus patentLegalStatus; + + @JacksonXmlElementWrapper(localName = "patentAssignee") + @JacksonXmlProperty(localName = "patentAssignee") + private List patentAssignee; + + @JacksonXmlElementWrapper(localName = "externalReferences") + @JacksonXmlProperty(localName = "reference") + private List externalReferences; + + @JacksonXmlElementWrapper(localName = "properties") + @JacksonXmlProperty(localName = "property") + private List properties; + + 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 getExpirationDate() { + return expirationDate; + } + + public void setExpirationDate(Date expirationDate) { + this.expirationDate = expirationDate; + } + + 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; + } + + 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 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(expirationDate, patent.expirationDate) && + patentLegalStatus == patent.patentLegalStatus && + Objects.equals(patentAssignee, patent.patentAssignee) && + Objects.equals(externalReferences, patent.externalReferences) && + Objects.equals(properties, patent.properties); + } + + @Override + public int hashCode() { + return Objects.hash(bomRef, patentNumber, applicationNumber, jurisdiction, priorityApplication, + publicationNumber, title, patentAbstract, filingDate, grantDate, expirationDate, + patentLegalStatus, patentAssignee, externalReferences, properties); + } +} 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..36336f6bb3 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/PatentAssertion.java @@ -0,0 +1,178 @@ +/* + * 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 assertion represents a claim about patent ownership or licensing. + * + * @since 10.0.0 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(Include.NON_NULL) +@JsonPropertyOrder({"bomRef", "assertionType", "patents", "patentFamilies", "properties"}) +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 OrganizationalChoice asserter; + + @JacksonXmlElementWrapper(localName = "patents") + @JacksonXmlProperty(localName = "patent") + private List patents; + + @JacksonXmlElementWrapper(localName = "patentFamilies") + @JacksonXmlProperty(localName = "patentFamily") + private List patentFamilies; + + private String notes; + + @JacksonXmlElementWrapper(localName = "properties") + @JacksonXmlProperty(localName = "property") + private List properties; + + public String getBomRef() { + return bomRef; + } + + public void setBomRef(String bomRef) { + this.bomRef = bomRef; + } + + public AssertionType getAssertionType() { + return assertionType; + } + + public void setAssertionType(AssertionType assertionType) { + this.assertionType = assertionType; + } + + public OrganizationalChoice getAsserter() { + return asserter; + } + + public void setAsserter(OrganizationalChoice asserter) { + this.asserter = asserter; + } + + public List getPatents() { + return patents; + } + + public void setPatents(List patents) { + this.patents = patents; + } + + public List getPatentFamilies() { + return patentFamilies; + } + + public void setPatentFamilies(List patentFamilies) { + this.patentFamilies = patentFamilies; + } + + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + + 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 PatentAssertion)) return false; + PatentAssertion that = (PatentAssertion) o; + return Objects.equals(bomRef, that.bomRef) && + assertionType == that.assertionType && + Objects.equals(asserter, that.asserter) && + Objects.equals(patents, that.patents) && + Objects.equals(patentFamilies, that.patentFamilies) && + Objects.equals(notes, that.notes) && + Objects.equals(properties, that.properties); + } + + @Override + public int hashCode() { + return Objects.hash(bomRef, assertionType, asserter, patents, patentFamilies, notes, properties); + } +} 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..fbb454c75a --- /dev/null +++ b/src/main/java/org/cyclonedx/model/PatentFamily.java @@ -0,0 +1,103 @@ +/* + * 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", "patents", "properties"}) +public class PatentFamily extends ExtensibleElement { + + @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") + @JsonProperty("bom-ref") + private String bomRef; + + private String familyId; + + @JacksonXmlElementWrapper(localName = "patents") + @JacksonXmlProperty(localName = "patent") + private List patents; + + @JacksonXmlElementWrapper(localName = "properties") + @JacksonXmlProperty(localName = "property") + private List properties; + + 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 List getPatents() { + return patents; + } + + public void setPatents(List patents) { + this.patents = patents; + } + + 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 PatentFamily)) return false; + PatentFamily that = (PatentFamily) o; + return Objects.equals(bomRef, that.bomRef) && + Objects.equals(familyId, that.familyId) && + Objects.equals(patents, that.patents) && + Objects.equals(properties, that.properties); + } + + @Override + public int hashCode() { + return Objects.hash(bomRef, familyId, patents, properties); + } +} 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..62f793f81b --- /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 org.cyclonedx.util.serializer.CustomDateSerializer; + +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; + @JsonSerialize(using = CustomDateSerializer.class) + 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..603d5ef830 100644 --- a/src/main/java/org/cyclonedx/model/Service.java +++ b/src/main/java/org/cyclonedx/model/Service.java @@ -85,6 +85,10 @@ public class Service extends ExtensibleElement { @VersionFilter(Version.VERSION_16) @JsonUnwrapped private Tags tags; + + @VersionFilter(Version.VERSION_17) + private List patentAssertions; + private List services; private ReleaseNotes releaseNotes; @JsonOnly @@ -272,6 +276,16 @@ public void setTags(final Tags tags) { this.tags = tags; } + @JacksonXmlElementWrapper(localName = "patentAssertions") + @JacksonXmlProperty(localName = "patentAssertion") + 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..1400a98c10 --- /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+STRICT") + AMBER_STRICT("AMBER+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/CipherSuite.java b/src/main/java/org/cyclonedx/model/component/crypto/CipherSuite.java index d66f0b5fb2..a082b82d7d 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,26 @@ public void setIdentifiers(final List identifiers) { this.identifiers = identifiers; } + @JacksonXmlElementWrapper(localName = "tlsGroups") + @JacksonXmlProperty(localName = "tlsGroup") + public List getTlsGroups() { + return tlsGroups; + } + + public void setTlsGroups(final List tlsGroups) { + this.tlsGroups = tlsGroups; + } + + @JacksonXmlElementWrapper(localName = "tlsSignatureSchemes") + @JacksonXmlProperty(localName = "tlsSignatureScheme") + 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 +87,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/license/ExpressionDetail.java b/src/main/java/org/cyclonedx/model/license/ExpressionDetail.java new file mode 100644 index 0000000000..5d54e59d23 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/license/ExpressionDetail.java @@ -0,0 +1,98 @@ +/* + * 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 { + + 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..eadc4f6dda --- /dev/null +++ b/src/main/java/org/cyclonedx/model/license/ExpressionDetailed.java @@ -0,0 +1,131 @@ +/* + * 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.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 { + + private String expression; + + @JacksonXmlElementWrapper(localName = "expressionDetails") + @JacksonXmlProperty(localName = "expressionDetail") + 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); + } +} From d4922b64011c69d79d9413dcc8671a1f149becff Mon Sep 17 00:00:00 2001 From: Alex Alzate Date: Sat, 7 Feb 2026 10:52:30 -0500 Subject: [PATCH 2/8] Update classes --- src/main/java/org/cyclonedx/model/Bom.java | 12 ---- .../java/org/cyclonedx/model/Citation.java | 50 +++++++++++----- .../java/org/cyclonedx/model/Component.java | 3 + .../cyclonedx/model/ExternalReference.java | 24 +++++++- .../org/cyclonedx/model/LicenseChoice.java | 1 + .../java/org/cyclonedx/model/Metadata.java | 13 ++-- src/main/java/org/cyclonedx/model/Patent.java | 35 ++++------- .../org/cyclonedx/model/PatentAssertion.java | 59 ++++++------------- .../org/cyclonedx/model/PatentFamily.java | 47 +++++++++------ .../java/org/cyclonedx/model/Service.java | 1 + .../cyclonedx/model/TlpClassification.java | 4 +- .../model/component/crypto/CipherSuite.java | 6 +- .../model/definition/Definition.java | 40 ++++++++++++- 13 files changed, 169 insertions(+), 126 deletions(-) diff --git a/src/main/java/org/cyclonedx/model/Bom.java b/src/main/java/org/cyclonedx/model/Bom.java index f9a77aa740..38eee02157 100644 --- a/src/main/java/org/cyclonedx/model/Bom.java +++ b/src/main/java/org/cyclonedx/model/Bom.java @@ -59,7 +59,6 @@ "formulation", "declarations", "definitions", - "distributionConstraints", "citations", "signature" }) @@ -102,9 +101,6 @@ public class Bom extends ExtensibleElement { @VersionFilter(Version.VERSION_15) private List annotations; - @VersionFilter(Version.VERSION_17) - private DistributionConstraints distributionConstraints; - @VersionFilter(Version.VERSION_17) private List citations; @@ -247,14 +243,6 @@ public void setAnnotations(List annotations) { this.annotations = annotations; } - public DistributionConstraints getDistributionConstraints() { - return distributionConstraints; - } - - public void setDistributionConstraints(DistributionConstraints distributionConstraints) { - this.distributionConstraints = distributionConstraints; - } - @JacksonXmlElementWrapper(localName = "citations") @JacksonXmlProperty(localName = "citation") @VersionFilter(Version.VERSION_17) diff --git a/src/main/java/org/cyclonedx/model/Citation.java b/src/main/java/org/cyclonedx/model/Citation.java index 51c8b8a1d0..4127904d63 100644 --- a/src/main/java/org/cyclonedx/model/Citation.java +++ b/src/main/java/org/cyclonedx/model/Citation.java @@ -39,7 +39,7 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) -@JsonPropertyOrder({"bomRef", "pointers", "expressions", "source", "timestamp"}) +@JsonPropertyOrder({"bomRef", "pointers", "expressions", "timestamp", "attributedTo", "process", "note"}) public class Citation extends ExtensibleElement { @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") @@ -54,11 +54,15 @@ public class Citation extends ExtensibleElement { @JacksonXmlProperty(localName = "expression") private List expressions; - private OrganizationalChoice source; - @JsonSerialize(using = CustomDateSerializer.class) private Date timestamp; + private String attributedTo; + + private String process; + + private String note; + public String getBomRef() { return bomRef; } @@ -83,14 +87,6 @@ public void setExpressions(List expressions) { this.expressions = expressions; } - public OrganizationalChoice getSource() { - return source; - } - - public void setSource(OrganizationalChoice source) { - this.source = source; - } - public Date getTimestamp() { return timestamp; } @@ -99,6 +95,30 @@ 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; @@ -107,12 +127,14 @@ public boolean equals(Object o) { return Objects.equals(bomRef, citation.bomRef) && Objects.equals(pointers, citation.pointers) && Objects.equals(expressions, citation.expressions) && - Objects.equals(source, citation.source) && - Objects.equals(timestamp, citation.timestamp); + 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, source, timestamp); + 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 2cecbbf87d..7d1b32bd8c 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -327,6 +327,7 @@ public void setVersion(String version) { this.version = version; } + @VersionFilter(Version.VERSION_17) public String getVersionRange() { return versionRange; } @@ -335,6 +336,7 @@ public void setVersionRange(String versionRange) { this.versionRange = versionRange; } + @VersionFilter(Version.VERSION_17) public Boolean getIsExternal() { return isExternal; } @@ -595,6 +597,7 @@ public void setProvides(final List provides) { @JacksonXmlElementWrapper(localName = "patentAssertions") @JacksonXmlProperty(localName = "patentAssertion") + @VersionFilter(Version.VERSION_17) public List getPatentAssertions() { return patentAssertions; } diff --git a/src/main/java/org/cyclonedx/model/ExternalReference.java b/src/main/java/org/cyclonedx/model/ExternalReference.java index 57b20945e7..0bfe5d2c10 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 { @@ -133,6 +133,9 @@ public enum Type { @VersionFilter(Version.VERSION_17) @JsonProperty("citation") CITATION("citation"), + @VersionFilter(Version.VERSION_15) + @JsonProperty("poam") + POAM("poam"), @JsonProperty("other") OTHER("other"); @@ -164,6 +167,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; } @@ -206,6 +212,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; @@ -214,11 +231,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 9d4ac8c3a9..d0d905ca42 100644 --- a/src/main/java/org/cyclonedx/model/LicenseChoice.java +++ b/src/main/java/org/cyclonedx/model/LicenseChoice.java @@ -75,6 +75,7 @@ public void setExpression(Expression expression) { this.expressionDetailed = null; } + @VersionFilter(Version.VERSION_17) public ExpressionDetailed getExpressionDetailed() { return expressionDetailed; } diff --git a/src/main/java/org/cyclonedx/model/Metadata.java b/src/main/java/org/cyclonedx/model/Metadata.java index b02ed92077..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", "classifications" + "properties", "distributionConstraints" }) @JsonDeserialize(using = MetadataDeserializer.class) public class Metadata @@ -93,7 +93,7 @@ public class Metadata private List properties; @VersionFilter(Version.VERSION_17) - private Classifications classifications; + private DistributionConstraints distributionConstraints; public Date getTimestamp() { return timestamp; @@ -202,12 +202,13 @@ public void addProperty(Property property) { this.properties.add(property); } - public Classifications getClassifications() { - return classifications; + @VersionFilter(Version.VERSION_17) + public DistributionConstraints getDistributionConstraints() { + return distributionConstraints; } - public void setClassifications(Classifications classifications) { - this.classifications = classifications; + public void setDistributionConstraints(DistributionConstraints distributionConstraints) { + this.distributionConstraints = distributionConstraints; } public Lifecycles getLifecycles() { diff --git a/src/main/java/org/cyclonedx/model/Patent.java b/src/main/java/org/cyclonedx/model/Patent.java index 80b37c7796..2afd081ab8 100644 --- a/src/main/java/org/cyclonedx/model/Patent.java +++ b/src/main/java/org/cyclonedx/model/Patent.java @@ -41,8 +41,8 @@ @JsonInclude(Include.NON_NULL) @JsonPropertyOrder({ "bomRef", "patentNumber", "applicationNumber", "jurisdiction", "priorityApplication", - "publicationNumber", "title", "abstract", "filingDate", "grantDate", "expirationDate", - "patentLegalStatus", "patentAssignee", "externalReferences", "properties" + "publicationNumber", "title", "abstract", "filingDate", "grantDate", "patentExpirationDate", + "patentLegalStatus", "patentAssignee", "externalReferences" }) public class Patent extends ExtensibleElement { @@ -117,7 +117,7 @@ public static PatentLegalStatus fromValue(String value) { private Date grantDate; @JsonSerialize(using = CustomDateSerializer.class) - private Date expirationDate; + private Date patentExpirationDate; private PatentLegalStatus patentLegalStatus; @@ -129,10 +129,6 @@ public static PatentLegalStatus fromValue(String value) { @JacksonXmlProperty(localName = "reference") private List externalReferences; - @JacksonXmlElementWrapper(localName = "properties") - @JacksonXmlProperty(localName = "property") - private List properties; - public String getBomRef() { return bomRef; } @@ -213,12 +209,12 @@ public void setGrantDate(Date grantDate) { this.grantDate = grantDate; } - public Date getExpirationDate() { - return expirationDate; + public Date getPatentExpirationDate() { + return patentExpirationDate; } - public void setExpirationDate(Date expirationDate) { - this.expirationDate = expirationDate; + public void setPatentExpirationDate(Date patentExpirationDate) { + this.patentExpirationDate = patentExpirationDate; } public PatentLegalStatus getPatentLegalStatus() { @@ -245,14 +241,6 @@ public void setExternalReferences(List externalReferences) { this.externalReferences = externalReferences; } - public List getProperties() { - return properties; - } - - public void setProperties(List properties) { - this.properties = properties; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -268,17 +256,16 @@ public boolean equals(Object o) { Objects.equals(patentAbstract, patent.patentAbstract) && Objects.equals(filingDate, patent.filingDate) && Objects.equals(grantDate, patent.grantDate) && - Objects.equals(expirationDate, patent.expirationDate) && + Objects.equals(patentExpirationDate, patent.patentExpirationDate) && patentLegalStatus == patent.patentLegalStatus && Objects.equals(patentAssignee, patent.patentAssignee) && - Objects.equals(externalReferences, patent.externalReferences) && - Objects.equals(properties, patent.properties); + Objects.equals(externalReferences, patent.externalReferences); } @Override public int hashCode() { return Objects.hash(bomRef, patentNumber, applicationNumber, jurisdiction, priorityApplication, - publicationNumber, title, patentAbstract, filingDate, grantDate, expirationDate, - patentLegalStatus, patentAssignee, externalReferences, properties); + 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 index 36336f6bb3..de2e7ff2ce 100644 --- a/src/main/java/org/cyclonedx/model/PatentAssertion.java +++ b/src/main/java/org/cyclonedx/model/PatentAssertion.java @@ -36,7 +36,7 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) -@JsonPropertyOrder({"bomRef", "assertionType", "patents", "patentFamilies", "properties"}) +@JsonPropertyOrder({"bomRef", "assertionType", "patentRefs", "asserter", "notes"}) public class PatentAssertion extends ExtensibleElement { public enum AssertionType { @@ -85,22 +85,12 @@ public static AssertionType fromValue(String value) { private AssertionType assertionType; - private OrganizationalChoice asserter; - - @JacksonXmlElementWrapper(localName = "patents") - @JacksonXmlProperty(localName = "patent") - private List patents; + private List patentRefs; - @JacksonXmlElementWrapper(localName = "patentFamilies") - @JacksonXmlProperty(localName = "patentFamily") - private List patentFamilies; + private OrganizationalChoice asserter; private String notes; - @JacksonXmlElementWrapper(localName = "properties") - @JacksonXmlProperty(localName = "property") - private List properties; - public String getBomRef() { return bomRef; } @@ -117,28 +107,23 @@ public void setAssertionType(AssertionType assertionType) { this.assertionType = assertionType; } - public OrganizationalChoice getAsserter() { - return asserter; - } - - public void setAsserter(OrganizationalChoice asserter) { - this.asserter = asserter; - } - - public List getPatents() { - return patents; + @JacksonXmlElementWrapper(localName = "patentRefs") + @JacksonXmlProperty(localName = "bom-ref") + @JsonProperty("patentRefs") + public List getPatentRefs() { + return patentRefs; } - public void setPatents(List patents) { - this.patents = patents; + public void setPatentRefs(List patentRefs) { + this.patentRefs = patentRefs; } - public List getPatentFamilies() { - return patentFamilies; + public OrganizationalChoice getAsserter() { + return asserter; } - public void setPatentFamilies(List patentFamilies) { - this.patentFamilies = patentFamilies; + public void setAsserter(OrganizationalChoice asserter) { + this.asserter = asserter; } public String getNotes() { @@ -149,14 +134,6 @@ public void setNotes(String notes) { this.notes = notes; } - public List getProperties() { - return properties; - } - - public void setProperties(List properties) { - this.properties = properties; - } - @Override public boolean equals(Object o) { if (this == o) return true; @@ -164,15 +141,13 @@ public boolean equals(Object o) { 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(patents, that.patents) && - Objects.equals(patentFamilies, that.patentFamilies) && - Objects.equals(notes, that.notes) && - Objects.equals(properties, that.properties); + Objects.equals(notes, that.notes); } @Override public int hashCode() { - return Objects.hash(bomRef, assertionType, asserter, patents, patentFamilies, notes, properties); + 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 index fbb454c75a..74e4073bba 100644 --- a/src/main/java/org/cyclonedx/model/PatentFamily.java +++ b/src/main/java/org/cyclonedx/model/PatentFamily.java @@ -36,7 +36,7 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) -@JsonPropertyOrder({"bomRef", "familyId", "patents", "properties"}) +@JsonPropertyOrder({"bomRef", "familyId", "priorityApplication", "members", "externalReferences"}) public class PatentFamily extends ExtensibleElement { @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") @@ -45,13 +45,15 @@ public class PatentFamily extends ExtensibleElement { private String familyId; - @JacksonXmlElementWrapper(localName = "patents") - @JacksonXmlProperty(localName = "patent") - private List patents; + private PriorityApplication priorityApplication; - @JacksonXmlElementWrapper(localName = "properties") - @JacksonXmlProperty(localName = "property") - private List properties; + @JacksonXmlElementWrapper(localName = "members") + @JacksonXmlProperty(localName = "ref") + private List members; + + @JacksonXmlElementWrapper(localName = "externalReferences") + @JacksonXmlProperty(localName = "reference") + private List externalReferences; public String getBomRef() { return bomRef; @@ -69,20 +71,28 @@ public void setFamilyId(String familyId) { this.familyId = familyId; } - public List getPatents() { - return patents; + public PriorityApplication getPriorityApplication() { + return priorityApplication; + } + + public void setPriorityApplication(PriorityApplication priorityApplication) { + this.priorityApplication = priorityApplication; + } + + public List getMembers() { + return members; } - public void setPatents(List patents) { - this.patents = patents; + public void setMembers(List members) { + this.members = members; } - public List getProperties() { - return properties; + public List getExternalReferences() { + return externalReferences; } - public void setProperties(List properties) { - this.properties = properties; + public void setExternalReferences(List externalReferences) { + this.externalReferences = externalReferences; } @Override @@ -92,12 +102,13 @@ public boolean equals(Object o) { PatentFamily that = (PatentFamily) o; return Objects.equals(bomRef, that.bomRef) && Objects.equals(familyId, that.familyId) && - Objects.equals(patents, that.patents) && - Objects.equals(properties, that.properties); + Objects.equals(priorityApplication, that.priorityApplication) && + Objects.equals(members, that.members) && + Objects.equals(externalReferences, that.externalReferences); } @Override public int hashCode() { - return Objects.hash(bomRef, familyId, patents, properties); + return Objects.hash(bomRef, familyId, priorityApplication, members, externalReferences); } } diff --git a/src/main/java/org/cyclonedx/model/Service.java b/src/main/java/org/cyclonedx/model/Service.java index 603d5ef830..a0220756f8 100644 --- a/src/main/java/org/cyclonedx/model/Service.java +++ b/src/main/java/org/cyclonedx/model/Service.java @@ -278,6 +278,7 @@ public void setTags(final Tags tags) { @JacksonXmlElementWrapper(localName = "patentAssertions") @JacksonXmlProperty(localName = "patentAssertion") + @VersionFilter(Version.VERSION_17) public List getPatentAssertions() { return patentAssertions; } diff --git a/src/main/java/org/cyclonedx/model/TlpClassification.java b/src/main/java/org/cyclonedx/model/TlpClassification.java index 1400a98c10..baab38e011 100644 --- a/src/main/java/org/cyclonedx/model/TlpClassification.java +++ b/src/main/java/org/cyclonedx/model/TlpClassification.java @@ -32,8 +32,8 @@ public enum TlpClassification { GREEN("GREEN"), @JsonProperty("AMBER") AMBER("AMBER"), - @JsonProperty("AMBER+STRICT") - AMBER_STRICT("AMBER+STRICT"), + @JsonProperty("AMBER_AND_STRICT") + AMBER_AND_STRICT("AMBER_AND_STRICT"), @JsonProperty("RED") RED("RED"); 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 a082b82d7d..d950c1c9d9 100644 --- a/src/main/java/org/cyclonedx/model/component/crypto/CipherSuite.java +++ b/src/main/java/org/cyclonedx/model/component/crypto/CipherSuite.java @@ -58,7 +58,8 @@ public void setIdentifiers(final List identifiers) { } @JacksonXmlElementWrapper(localName = "tlsGroups") - @JacksonXmlProperty(localName = "tlsGroup") + @JacksonXmlProperty(localName = "group") + @VersionFilter(Version.VERSION_17) public List getTlsGroups() { return tlsGroups; } @@ -68,7 +69,8 @@ public void setTlsGroups(final List tlsGroups) { } @JacksonXmlElementWrapper(localName = "tlsSignatureSchemes") - @JacksonXmlProperty(localName = "tlsSignatureScheme") + @JacksonXmlProperty(localName = "signatureScheme") + @VersionFilter(Version.VERSION_17) public List getTlsSignatureSchemes() { return tlsSignatureSchemes; } diff --git a/src/main/java/org/cyclonedx/model/definition/Definition.java b/src/main/java/org/cyclonedx/model/definition/Definition.java index 18e357ee56..972bcf6580 100644 --- a/src/main/java/org/cyclonedx/model/definition/Definition.java +++ b/src/main/java/org/cyclonedx/model/definition/Definition.java @@ -8,16 +8,26 @@ 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.Patent; +import org.cyclonedx.model.PatentFamily; +import org.cyclonedx.model.VersionFilter; @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyOrder({ - "standards" + "standards", "patents", "patentFamilies" }) public class Definition { private List standards; + @VersionFilter(Version.VERSION_17) + private List patents; + + @VersionFilter(Version.VERSION_17) + private List patentFamilies; + @JacksonXmlElementWrapper(localName = "standards") @JacksonXmlProperty(localName = "standard") public List getStandards() { @@ -28,6 +38,28 @@ 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; + } + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "patentFamily") + @VersionFilter(Version.VERSION_17) + public List getPatentFamilies() { + return patentFamilies; + } + + public void setPatentFamilies(final List patentFamilies) { + this.patentFamilies = patentFamilies; + } + @Override public boolean equals(final Object object) { if (this == object) { @@ -37,11 +69,13 @@ 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) && + Objects.equals(patentFamilies, that.patentFamilies); } @Override public int hashCode() { - return Objects.hashCode(standards); + return Objects.hash(standards, patents, patentFamilies); } } From c8f9056a57894fc79ef7d32cd9105869ce2cd4d7 Mon Sep 17 00:00:00 2001 From: Alexander Alzate Date: Sat, 9 May 2026 08:52:09 -0500 Subject: [PATCH 3/8] Dev 1.7 Expression Detail (#787) * Support for license Details * Fix tests --- .../java/org/cyclonedx/model/Component.java | 42 ++- .../org/cyclonedx/model/LicenseChoice.java | 193 +++++++++++--- .../java/org/cyclonedx/model/LicenseItem.java | 166 ++++++++++++ .../cyclonedx/model/OrganizationalChoice.java | 3 + .../model/license/ExpressionDetail.java | 2 + .../model/license/ExpressionDetailed.java | 7 +- .../LicenseChoiceDeserializer.java | 166 ++++++++++++ .../OrganizationalChoiceDeserializer.java | 66 ++--- .../OrganizationalEntityDeserializer.java | 19 +- .../serializer/LicenseChoiceSerializer.java | 229 ++++++++++------ .../org/cyclonedx/BomJsonGeneratorTest.java | 98 +++++++ .../org/cyclonedx/BomXmlGeneratorTest.java | 98 +++++++ .../org/cyclonedx/parsers/JsonParserTest.java | 240 +++++++++++++++++ .../org/cyclonedx/parsers/XmlParserTest.java | 244 ++++++++++++++++++ 14 files changed, 1413 insertions(+), 160 deletions(-) create mode 100644 src/main/java/org/cyclonedx/model/LicenseItem.java create mode 100644 src/main/java/org/cyclonedx/util/deserializer/LicenseChoiceDeserializer.java diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index 7d1b32bd8c..afe73e5ef8 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; @@ -380,11 +381,12 @@ public void addHash(Hash hash) { } @JsonDeserialize(using = LicenseDeserializer.class) + @JacksonXmlElementWrapper (useWrapping = false) public LicenseChoice getLicenses() { return licenses; } - @JacksonXmlElementWrapper (useWrapping = false) + @JsonDeserialize(using = LicenseChoiceDeserializer.class) public void setLicenses(LicenseChoice licenses) { this.licenses = licenses; } @@ -644,6 +646,44 @@ 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, diff --git a/src/main/java/org/cyclonedx/model/LicenseChoice.java b/src/main/java/org/cyclonedx/model/LicenseChoice.java index d0d905ca42..a019e4768b 100644 --- a/src/main/java/org/cyclonedx/model/LicenseChoice.java +++ b/src/main/java/org/cyclonedx/model/LicenseChoice.java @@ -21,69 +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.model.license.ExpressionDetailed; -import org.cyclonedx.util.deserializer.LicenseDeserializer; +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; - @VersionFilter(Version.VERSION_17) - @JacksonXmlProperty(localName = "expression-detailed") - private ExpressionDetailed expressionDetailed; + /** + * 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; + } - @JacksonXmlProperty(localName = "license") - public List getLicenses() { - return license; + public void setItems(List items) { + this.items = items; } - public void setLicenses(List licenses) { - this.license = licenses; - this.expression = null; - this.expressionDetailed = null; + /** + * 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; - this.expressionDetailed = 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; - this.expressionDetailed = null; + if (expression != null) { + this.items = new ArrayList<>(); + this.items.add(LicenseItem.ofExpression(expression)); + } else { + this.items = null; + } } - @VersionFilter(Version.VERSION_17) + + /** + * Returns the first ExpressionDetailed item. + * Note: This is part of the 1.7 API, not deprecated. + */ + @JsonIgnore public ExpressionDetailed getExpressionDetailed() { - return expressionDetailed; + 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) { - this.expressionDetailed = expressionDetailed; - this.license = null; - this.expression = null; + 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 @@ -91,13 +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) && - Objects.equals(expressionDetailed, that.expressionDetailed); + return Objects.equals(items, that.items); } @Override public int hashCode() { - return Objects.hash(license, expression, expressionDetailed); + 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..2bdd4efb21 --- /dev/null +++ b/src/main/java/org/cyclonedx/model/LicenseItem.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.model; + +import java.util.Objects; + +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; + 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/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/license/ExpressionDetail.java b/src/main/java/org/cyclonedx/model/license/ExpressionDetail.java index 5d54e59d23..c86cd23843 100644 --- a/src/main/java/org/cyclonedx/model/license/ExpressionDetail.java +++ b/src/main/java/org/cyclonedx/model/license/ExpressionDetail.java @@ -38,6 +38,8 @@ @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") diff --git a/src/main/java/org/cyclonedx/model/license/ExpressionDetailed.java b/src/main/java/org/cyclonedx/model/license/ExpressionDetailed.java index eadc4f6dda..8584cc353d 100644 --- a/src/main/java/org/cyclonedx/model/license/ExpressionDetailed.java +++ b/src/main/java/org/cyclonedx/model/license/ExpressionDetailed.java @@ -18,6 +18,7 @@ */ 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; @@ -44,10 +45,12 @@ }) public class ExpressionDetailed extends ExtensibleElement { + @JacksonXmlProperty(isAttribute = true) private String expression; - @JacksonXmlElementWrapper(localName = "expressionDetails") - @JacksonXmlProperty(localName = "expressionDetail") + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "details") + @JsonFormat(with = JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY) private List expressionDetails; @JacksonXmlProperty(isAttribute = true) 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/OrganizationalChoiceDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/OrganizationalChoiceDeserializer.java index 451409a501..f5e734e3a6 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/OrganizationalChoiceDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/OrganizationalChoiceDeserializer.java @@ -21,57 +21,47 @@ 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 both: + * 1. Object format: {"individual": {...}} or {"organization": {...}} + * 2. String format: "bom-ref" (reference to an organization defined elsewhere) + */ +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 - deserialize normally + 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()); - } - - 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); - } + 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); } - 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/serializer/LicenseChoiceSerializer.java b/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java index 5438d9110b..0861f6bbbe 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,69 @@ public void serialize( private void serializeXml(ToXmlGenerator toXmlGenerator, LicenseChoice lc, final SerializerProvider provider) throws IOException { - if (CollectionUtils.isNotEmpty(lc.getLicenses())) { - toXmlGenerator.writeStartObject(); - toXmlGenerator.writeFieldName("license"); + if (CollectionUtils.isEmpty(lc.getItems())) { 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(); - } toXmlGenerator.writeEndArray(); + return; + } + + 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) { + 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(); + + for (Property property : l.getProperties()) { + toXmlGenerator.writeObjectField("property", property); + } 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 +151,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 +169,87 @@ 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 { + if (CollectionUtils.isEmpty(licenseChoice.getItems())) { gen.writeStartArray(); gen.writeEndArray(); - } + return; + } + + gen.writeStartArray(); + for (LicenseItem item : licenseChoice.getItems()) { + gen.writeStartObject(); + if (item.getLicense() != null) { + provider.defaultSerializeField("license", item.getLicense(), gen); + } else if (item.getExpression() != null) { + serializeExpressionToJson(item.getExpression(), gen); + } else if (item.getExpressionDetailed() != null) { + 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(); + if (version.getVersion() < 1.7) { + return; // ExpressionDetailed is only for 1.7+ } - gen.writeEndArray(); + + 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); + } + + // 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 +257,33 @@ 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 { + if (version.getVersion() < 1.7) { + return; // ExpressionDetailed is only for 1.7+ + } + + // 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/test/java/org/cyclonedx/BomJsonGeneratorTest.java b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java index 14f2f4add4..ad42c76c87 100644 --- a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java @@ -680,6 +680,104 @@ 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)); + } + 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..3699464c5c 100644 --- a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java @@ -869,6 +869,104 @@ 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)); + } + 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/parsers/JsonParserTest.java b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java index d0efdbbea1..56e8e490ea 100644 --- a/src/test/java/org/cyclonedx/parsers/JsonParserTest.java +++ b/src/test/java/org/cyclonedx/parsers/JsonParserTest.java @@ -63,6 +63,8 @@ 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.junit.jupiter.api.Test; import java.io.File; import java.util.ArrayList; @@ -301,6 +303,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"); diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java index 0074cdbd32..5198cdc59e 100644 --- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java +++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java @@ -65,6 +65,8 @@ 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.junit.jupiter.api.Test; import java.io.File; @@ -450,6 +452,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"); From 085ba8917f44a3bf464999877f7c99e132862e81 Mon Sep 17 00:00:00 2001 From: Alexander Alzate Date: Sat, 9 May 2026 09:03:36 -0500 Subject: [PATCH 4/8] Support for IKEV2 in 1.7 (#825) * Fix License issue serialization * Add Ike2 Proper Support * Add headers --- .../java/org/cyclonedx/model/Component.java | 3 + .../crypto/AbstractIkeV2Transform.java | 85 +++++++++++++++++++ .../model/component/crypto/IkeV2Auth.java | 46 ++++++++++ .../model/component/crypto/IkeV2Enc.java | 78 +++++++++++++++++ .../model/component/crypto/IkeV2Integ.java | 46 ++++++++++ .../model/component/crypto/IkeV2Ke.java | 78 +++++++++++++++++ .../model/component/crypto/IkeV2Prf.java | 46 ++++++++++ .../component/crypto/Ikev2TransformTypes.java | 55 ++++++++---- .../serializer/IkeV2TransformSerializer.java | 63 ++++++++++++++ .../serializer/LicenseChoiceSerializer.java | 12 --- .../org/cyclonedx/BomXmlGeneratorTest.java | 2 +- 11 files changed, 485 insertions(+), 29 deletions(-) create mode 100644 src/main/java/org/cyclonedx/model/component/crypto/AbstractIkeV2Transform.java create mode 100644 src/main/java/org/cyclonedx/model/component/crypto/IkeV2Auth.java create mode 100644 src/main/java/org/cyclonedx/model/component/crypto/IkeV2Enc.java create mode 100644 src/main/java/org/cyclonedx/model/component/crypto/IkeV2Integ.java create mode 100644 src/main/java/org/cyclonedx/model/component/crypto/IkeV2Ke.java create mode 100644 src/main/java/org/cyclonedx/model/component/crypto/IkeV2Prf.java create mode 100644 src/main/java/org/cyclonedx/util/serializer/IkeV2TransformSerializer.java diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index afe73e5ef8..248796b051 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -383,6 +383,9 @@ 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; } 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/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/util/serializer/IkeV2TransformSerializer.java b/src/main/java/org/cyclonedx/util/serializer/IkeV2TransformSerializer.java new file mode 100644 index 0000000000..cbbbd2a2a4 --- /dev/null +++ b/src/main/java/org/cyclonedx/util/serializer/IkeV2TransformSerializer.java @@ -0,0 +1,63 @@ +/* + * 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.model.component.crypto.AbstractIkeV2Transform; +import org.cyclonedx.model.component.crypto.IkeV2Enc; +import org.cyclonedx.model.component.crypto.IkeV2Ke; + +import java.io.IOException; + +public class IkeV2TransformSerializer extends JsonSerializer { + + @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) { + gen.writeNumberField("group", ke.getGroup()); + } + } else { + if (value.getName() != null) { + gen.writeStringField("name", value.getName()); + } + if (value instanceof IkeV2Enc) { + IkeV2Enc enc = (IkeV2Enc) value; + if (enc.getKeyLength() != null) { + gen.writeNumberField("keyLength", enc.getKeyLength()); + } + } + } + if (value.getAlgorithm() != null) { + gen.writeStringField("algorithm", value.getAlgorithm()); + } + gen.writeEndObject(); + } +} diff --git a/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java b/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java index 0861f6bbbe..ddeb7a0126 100644 --- a/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java @@ -80,12 +80,6 @@ public void serialize( private void serializeXml(ToXmlGenerator toXmlGenerator, LicenseChoice lc, final SerializerProvider provider) throws IOException { - if (CollectionUtils.isEmpty(lc.getItems())) { - toXmlGenerator.writeStartArray(); - toXmlGenerator.writeEndArray(); - return; - } - toXmlGenerator.writeStartObject(); for (LicenseItem item : lc.getItems()) { @@ -169,12 +163,6 @@ private void serializeJson( final LicenseChoice licenseChoice, final JsonGenerator gen, final SerializerProvider provider) throws IOException { - if (CollectionUtils.isEmpty(licenseChoice.getItems())) { - gen.writeStartArray(); - gen.writeEndArray(); - return; - } - gen.writeStartArray(); for (LicenseItem item : licenseChoice.getItems()) { gen.writeStartObject(); diff --git a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java index 3699464c5c..11697ba862 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 From fa6c7f4fdc4a499e433c6f1a6cfa051ef3108710 Mon Sep 17 00:00:00 2001 From: Alexander Alzate Date: Sat, 9 May 2026 09:16:55 -0500 Subject: [PATCH 5/8] Support patents and version-aware serialization (#827) Add polymorphic patent support and make serialization version-aware. Introduces PatentItem model plus PatentItemDeserializer, PatentsDeserializer and PatentAssertionDeserializer/Serializer to handle mixed Patent/PatentFamily entries and XML/JSON differences; updates Definition to use the polymorphic list and provides helpers for legacy access. Refactors many serializers (EnvironmentVars, InputType, ExternalReference, Hash, IkeV2Transform, etc.) and CustomSerializerModifier to honor @VersionFilter and a Version parameter, filters enum/field serialization by target BOM version, and normalizes date formatting. Also enhances OrganizationalChoice deserialization, adds properties handling to ExternalReferencesDeserializer, and small model tweaks (Component, Composition, Service, LicenseItem, PriorityApplication, FormulationCommon, Level) to align with newer schema versions. --- .../generators/AbstractBomGenerator.java | 6 +- .../java/org/cyclonedx/model/Component.java | 14 +- .../java/org/cyclonedx/model/Composition.java | 4 + .../cyclonedx/model/ExternalReference.java | 24 ++ .../java/org/cyclonedx/model/LicenseItem.java | 2 + src/main/java/org/cyclonedx/model/Patent.java | 12 +- .../org/cyclonedx/model/PatentAssertion.java | 14 +- .../java/org/cyclonedx/model/PatentItem.java | 77 +++++ .../cyclonedx/model/PriorityApplication.java | 4 +- .../java/org/cyclonedx/model/Service.java | 1 + .../model/definition/Definition.java | 68 +++- .../org/cyclonedx/model/definition/Level.java | 1 - .../model/formulation/FormulationCommon.java | 4 +- .../ExternalReferencesDeserializer.java | 8 + .../deserializer/MetadataDeserializer.java | 6 + .../OrganizationalChoiceDeserializer.java | 34 +- .../PatentAssertionDeserializer.java | 104 ++++++ .../deserializer/PatentItemDeserializer.java | 33 ++ .../deserializer/PatentsDeserializer.java | 96 ++++++ .../serializer/CustomSerializerModifier.java | 33 +- .../serializer/EnvironmentVarsSerializer.java | 10 +- .../ExternalReferenceSerializer.java | 29 +- .../util/serializer/HashSerializer.java | 19 +- .../serializer/IkeV2TransformSerializer.java | 21 +- .../util/serializer/InputTypeSerializer.java | 34 +- .../serializer/LicenseChoiceSerializer.java | 20 +- .../util/serializer/LifecycleSerializer.java | 22 +- .../util/serializer/MetadataSerializer.java | 2 +- .../OrganizationalChoiceSerializer.java | 120 +++++++ .../util/serializer/OutputTypeSerializer.java | 62 ++-- .../serializer/PatentAssertionSerializer.java | 135 ++++++++ .../util/serializer/PatentItemSerializer.java | 67 ++++ .../util/serializer/SerializerUtils.java | 105 ++++++- .../org/cyclonedx/BomJsonGeneratorTest.java | 174 ++++++++++ .../org/cyclonedx/BomXmlGeneratorTest.java | 174 ++++++++++ .../org/cyclonedx/VersionFilteringTest.java | 297 ++++++++++++++++++ .../org/cyclonedx/parsers/JsonParserTest.java | 195 ++++++++++++ .../org/cyclonedx/parsers/XmlParserTest.java | 176 +++++++++++ .../resources/1.7/valid-citations-1.7.json | 2 +- 39 files changed, 2073 insertions(+), 136 deletions(-) create mode 100644 src/main/java/org/cyclonedx/model/PatentItem.java create mode 100644 src/main/java/org/cyclonedx/util/deserializer/PatentAssertionDeserializer.java create mode 100644 src/main/java/org/cyclonedx/util/deserializer/PatentItemDeserializer.java create mode 100644 src/main/java/org/cyclonedx/util/deserializer/PatentsDeserializer.java create mode 100644 src/main/java/org/cyclonedx/util/serializer/OrganizationalChoiceSerializer.java create mode 100644 src/main/java/org/cyclonedx/util/serializer/PatentAssertionSerializer.java create mode 100644 src/main/java/org/cyclonedx/util/serializer/PatentItemSerializer.java create mode 100644 src/test/java/org/cyclonedx/VersionFilteringTest.java 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/Component.java b/src/main/java/org/cyclonedx/model/Component.java index 248796b051..78736d5f68 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -54,7 +54,9 @@ @JsonPropertyOrder( { "type", + "mime-type", "bom-ref", + "isExternal", "supplier", "manufacturer", "authors", @@ -63,11 +65,13 @@ "group", "name", "version", + "versionRange", "description", "scope", "hashes", "licenses", "copyright", + "patentAssertions", "cpe", "purl", "omniborId", @@ -83,6 +87,7 @@ "modelCard", "data", "cryptoProperties", + "tags", "signature", "provides" }) @@ -95,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) @@ -133,6 +145,7 @@ public enum Scope { REQUIRED("required"), @JsonProperty("optional") OPTIONAL("optional"), + @VersionFilter(Version.VERSION_12) @JsonProperty("excluded") EXCLUDED("excluded"); @@ -171,7 +184,6 @@ public String getScopeName() { @VersionFilter(Version.VERSION_12) private String author; - @VersionFilter(Version.VERSION_11) private String publisher; private String group; private String name; 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/ExternalReference.java b/src/main/java/org/cyclonedx/model/ExternalReference.java index 0bfe5d2c10..6ce1eb9810 100644 --- a/src/main/java/org/cyclonedx/model/ExternalReference.java +++ b/src/main/java/org/cyclonedx/model/ExternalReference.java @@ -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"), + @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) diff --git a/src/main/java/org/cyclonedx/model/LicenseItem.java b/src/main/java/org/cyclonedx/model/LicenseItem.java index 2bdd4efb21..b1b49a4ce8 100644 --- a/src/main/java/org/cyclonedx/model/LicenseItem.java +++ b/src/main/java/org/cyclonedx/model/LicenseItem.java @@ -20,6 +20,7 @@ import java.util.Objects; +import org.cyclonedx.Version; import org.cyclonedx.model.license.Expression; import org.cyclonedx.model.license.ExpressionDetailed; @@ -38,6 +39,7 @@ public class LicenseItem { private License license; private Expression expression; + @VersionFilter(Version.VERSION_17) private ExpressionDetailed expressionDetailed; /** diff --git a/src/main/java/org/cyclonedx/model/Patent.java b/src/main/java/org/cyclonedx/model/Patent.java index 2afd081ab8..20c0523fe7 100644 --- a/src/main/java/org/cyclonedx/model/Patent.java +++ b/src/main/java/org/cyclonedx/model/Patent.java @@ -23,10 +23,11 @@ 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.CustomDateSerializer; +import org.cyclonedx.util.serializer.OrganizationalChoiceSerializer; import java.util.Date; import java.util.List; @@ -110,19 +111,20 @@ public static PatentLegalStatus fromValue(String value) { @JsonProperty("abstract") private String patentAbstract; - @JsonSerialize(using = CustomDateSerializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC") private Date filingDate; - @JsonSerialize(using = CustomDateSerializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC") private Date grantDate; - @JsonSerialize(using = CustomDateSerializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC") private Date patentExpirationDate; private PatentLegalStatus patentLegalStatus; - @JacksonXmlElementWrapper(localName = "patentAssignee") + @JacksonXmlElementWrapper(useWrapping = false) @JacksonXmlProperty(localName = "patentAssignee") + @JsonSerialize(contentUsing = OrganizationalChoiceSerializer.class) private List patentAssignee; @JacksonXmlElementWrapper(localName = "externalReferences") diff --git a/src/main/java/org/cyclonedx/model/PatentAssertion.java b/src/main/java/org/cyclonedx/model/PatentAssertion.java index de2e7ff2ce..01a41bd4f0 100644 --- a/src/main/java/org/cyclonedx/model/PatentAssertion.java +++ b/src/main/java/org/cyclonedx/model/PatentAssertion.java @@ -23,8 +23,12 @@ 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; @@ -36,7 +40,9 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) -@JsonPropertyOrder({"bomRef", "assertionType", "patentRefs", "asserter", "notes"}) +@JsonPropertyOrder({"bom-ref", "assertionType", "patentRefs", "asserter", "notes"}) +@JsonDeserialize(using = PatentAssertionDeserializer.class) +@JsonSerialize(using = PatentAssertionSerializer.class) public class PatentAssertion extends ExtensibleElement { public enum AssertionType { @@ -96,7 +102,11 @@ public String getBomRef() { } public void setBomRef(String bomRef) { - this.bomRef = 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() { 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 index 62f793f81b..fe3b3bd038 100644 --- a/src/main/java/org/cyclonedx/model/PriorityApplication.java +++ b/src/main/java/org/cyclonedx/model/PriorityApplication.java @@ -23,7 +23,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonPropertyOrder; import com.fasterxml.jackson.databind.annotation.JsonSerialize; -import org.cyclonedx.util.serializer.CustomDateSerializer; +import com.fasterxml.jackson.annotation.JsonFormat; import java.util.Date; import java.util.Objects; @@ -40,7 +40,7 @@ public class PriorityApplication { private String applicationNumber; private String jurisdiction; - @JsonSerialize(using = CustomDateSerializer.class) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd", timezone = "UTC") private Date filingDate; public String getApplicationNumber() { diff --git a/src/main/java/org/cyclonedx/model/Service.java b/src/main/java/org/cyclonedx/model/Service.java index a0220756f8..eedf466a74 100644 --- a/src/main/java/org/cyclonedx/model/Service.java +++ b/src/main/java/org/cyclonedx/model/Service.java @@ -90,6 +90,7 @@ public class Service extends ExtensibleElement { private List patentAssertions; private List services; + @VersionFilter(Version.VERSION_14) private ReleaseNotes releaseNotes; @JsonOnly @VersionFilter(Version.VERSION_14) diff --git a/src/main/java/org/cyclonedx/model/definition/Definition.java b/src/main/java/org/cyclonedx/model/definition/Definition.java index 972bcf6580..88eb9a55c1 100644 --- a/src/main/java/org/cyclonedx/model/definition/Definition.java +++ b/src/main/java/org/cyclonedx/model/definition/Definition.java @@ -1,32 +1,36 @@ 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", "patents", "patentFamilies" + "standards", "patents" }) public class Definition { private List standards; @VersionFilter(Version.VERSION_17) - private List patents; - - @VersionFilter(Version.VERSION_17) - private List patentFamilies; + @JsonDeserialize(using = PatentsDeserializer.class) + private List patents; @JacksonXmlElementWrapper(localName = "standards") @JacksonXmlProperty(localName = "standard") @@ -41,23 +45,54 @@ public void setStandards(final List standards) { @JacksonXmlElementWrapper(localName = "patents") @JacksonXmlProperty(localName = "patent") @VersionFilter(Version.VERSION_17) - public List getPatents() { + public List getPatents() { return patents; } - public void setPatents(final List patents) { + public void setPatents(final List patents) { this.patents = patents; } - @JacksonXmlElementWrapper(useWrapping = false) - @JacksonXmlProperty(localName = "patentFamily") - @VersionFilter(Version.VERSION_17) - public List getPatentFamilies() { - return patentFamilies; + /** + * @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 setPatentFamilies(final List patentFamilies) { - this.patentFamilies = patentFamilies; + 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 @@ -70,12 +105,11 @@ public boolean equals(final Object object) { } Definition that = (Definition) object; return Objects.equals(standards, that.standards) && - Objects.equals(patents, that.patents) && - Objects.equals(patentFamilies, that.patentFamilies); + Objects.equals(patents, that.patents); } @Override public int hashCode() { - return Objects.hash(standards, patents, patentFamilies); + 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/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/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 f5e734e3a6..b07b357de2 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/OrganizationalChoiceDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/OrganizationalChoiceDeserializer.java @@ -30,9 +30,11 @@ import org.cyclonedx.model.OrganizationalEntity; /** - * Deserializer for OrganizationalChoice that handles both: - * 1. Object format: {"individual": {...}} or {"organization": {...}} + * 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 { @@ -50,10 +52,11 @@ public OrganizationalChoice deserialize(JsonParser p, DeserializationContext ctx return choice; } - // Object format - deserialize normally + // Object format JsonNode node = p.getCodec().readTree(p); OrganizationalChoice choice = new OrganizationalChoice(); + // Wrapped format (Licensing): {"individual": {...}} or {"organization": {...}} if (node.has("individual")) { OrganizationalContact individual = p.getCodec().treeToValue(node.get("individual"), OrganizationalContact.class); choice.setIndividual(individual); @@ -61,6 +64,31 @@ public OrganizationalChoice deserialize(JsonParser p, DeserializationContext ctx OrganizationalEntity organization = p.getCodec().treeToValue(node.get("organization"), OrganizationalEntity.class); choice.setOrganization(organization); } + // 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 choice; } 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 index cbbbd2a2a4..3d54ec1d3d 100644 --- a/src/main/java/org/cyclonedx/util/serializer/IkeV2TransformSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/IkeV2TransformSerializer.java @@ -21,14 +21,27 @@ 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 @@ -41,21 +54,21 @@ public void serialize(AbstractIkeV2Transform value, JsonGenerator gen, Serialize gen.writeStartObject(); if (value instanceof IkeV2Ke) { IkeV2Ke ke = (IkeV2Ke) value; - if (ke.getGroup() != null) { + if (ke.getGroup() != null && shouldSerializeField(ke, version, "group")) { gen.writeNumberField("group", ke.getGroup()); } } else { - if (value.getName() != null) { + 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) { + if (enc.getKeyLength() != null && shouldSerializeField(enc, version, "keyLength")) { gen.writeNumberField("keyLength", enc.getKeyLength()); } } } - if (value.getAlgorithm() != null) { + 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 ddeb7a0126..7277a52361 100644 --- a/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java +++ b/src/main/java/org/cyclonedx/util/serializer/LicenseChoiceSerializer.java @@ -87,7 +87,7 @@ private void serializeXml(ToXmlGenerator toXmlGenerator, LicenseChoice lc, final serializeLicenseToXml(toXmlGenerator, item.getLicense(), provider); } else if (item.getExpression() != null) { serializeExpressionToXml(toXmlGenerator, item.getExpression()); - } else if (item.getExpressionDetailed() != null) { + } else if (item.getExpressionDetailed() != null && shouldSerializeField(item, version, "expressionDetailed")) { serializeExpressionDetailedToXml(toXmlGenerator, item.getExpressionDetailed(), provider); } } @@ -165,15 +165,19 @@ private void serializeJson( { gen.writeStartArray(); for (LicenseItem item : licenseChoice.getItems()) { - gen.writeStartObject(); 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); - } else if (item.getExpressionDetailed() != null) { + gen.writeEndObject(); + } else if (item.getExpressionDetailed() != null && shouldSerializeField(item, version, "expressionDetailed")) { + gen.writeStartObject(); serializeExpressionDetailedToJson(item.getExpressionDetailed(), gen, provider); + gen.writeEndObject(); } - gen.writeEndObject(); } gen.writeEndArray(); } @@ -196,10 +200,6 @@ private void serializeExpressionDetailedToXml( final SerializerProvider provider) throws IOException { - if (version.getVersion() < 1.7) { - return; // ExpressionDetailed is only for 1.7+ - } - toXmlGenerator.writeFieldName("expression-detailed"); toXmlGenerator.writeStartObject(); @@ -250,10 +250,6 @@ private void serializeExpressionToJson(final Expression expression, final JsonGe private void serializeExpressionDetailedToJson( final ExpressionDetailed expressionDetailed, final JsonGenerator gen, final SerializerProvider provider) throws IOException { - if (version.getVersion() < 1.7) { - return; // ExpressionDetailed is only for 1.7+ - } - // Flatten the expressionDetailed fields into the license item object if (StringUtils.isNotBlank(expressionDetailed.getBomRef())) { gen.writeStringField("bom-ref", expressionDetailed.getBomRef()); 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/test/java/org/cyclonedx/BomJsonGeneratorTest.java b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java index ad42c76c87..4afd27e134 100644 --- a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java @@ -778,6 +778,180 @@ public void schema17_testLicenseDeclaredConcludedMix_xml() throws Exception { 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)); + } + 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 11697ba862..7c494c49ca 100644 --- a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java @@ -967,6 +967,180 @@ public void schema17_testLicenseDeclaredConcludedMix_xml() throws Exception { 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)); + } + 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 56e8e490ea..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; @@ -65,6 +66,11 @@ 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; @@ -841,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 5198cdc59e..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; @@ -67,6 +68,11 @@ 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; @@ -995,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/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", From 07548e100c0cb1517dff5c8208d3ab8ba3e3bf2e Mon Sep 17 00:00:00 2001 From: Alexander Alzate Date: Sat, 9 May 2026 09:41:19 -0500 Subject: [PATCH 6/8] Dev 1.7 fix model card (#829) * Use 'model-card' for MODEL_CARD external reference Update ExternalReference.MODEL_CARD to use kebab-case: change @JsonProperty and enum value from "model_card" to "model-card". This aligns the serialized name with the expected CycloneDX 1.5 naming while retaining the VersionFilter(Version.VERSION_15) annotation. * Adjust Citation & Component; delete Classifications Fix serialization and equality logic in model classes: change Citation @JsonPropertyOrder to use "bom-ref" to match the XML attribute, remove the now-unused Classifications class, and update Component.equals()/hashCode() to include newly added fields (isExternal, versionRange, patentAssertions, tags) so equality and hashing account for them. --- .../java/org/cyclonedx/model/Citation.java | 2 +- .../org/cyclonedx/model/Classifications.java | 60 ------------------- .../java/org/cyclonedx/model/Component.java | 9 ++- .../cyclonedx/model/ExternalReference.java | 4 +- 4 files changed, 10 insertions(+), 65 deletions(-) delete mode 100644 src/main/java/org/cyclonedx/model/Classifications.java diff --git a/src/main/java/org/cyclonedx/model/Citation.java b/src/main/java/org/cyclonedx/model/Citation.java index 4127904d63..5ebebfe597 100644 --- a/src/main/java/org/cyclonedx/model/Citation.java +++ b/src/main/java/org/cyclonedx/model/Citation.java @@ -39,7 +39,7 @@ */ @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(Include.NON_NULL) -@JsonPropertyOrder({"bomRef", "pointers", "expressions", "timestamp", "attributedTo", "process", "note"}) +@JsonPropertyOrder({"bom-ref", "pointers", "expressions", "timestamp", "attributedTo", "process", "note"}) public class Citation extends ExtensibleElement { @JacksonXmlProperty(isAttribute = true, localName = "bom-ref") diff --git a/src/main/java/org/cyclonedx/model/Classifications.java b/src/main/java/org/cyclonedx/model/Classifications.java deleted file mode 100644 index 04423cc603..0000000000 --- a/src/main/java/org/cyclonedx/model/Classifications.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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; - -/** - * Data sharing and distribution classifications. - * - * @since 10.0.0 - */ -@JsonIgnoreProperties(ignoreUnknown = true) -@JsonInclude(Include.NON_NULL) -@JsonPropertyOrder({"tlp"}) -public class Classifications { - - 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 Classifications)) return false; - Classifications that = (Classifications) o; - return tlp == that.tlp; - } - - @Override - public int hashCode() { - return Objects.hash(tlp); - } -} diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index 78736d5f68..c450edcd71 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -702,7 +702,8 @@ private void validateVersionRangeRequirements() { @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 @@ -733,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/ExternalReference.java b/src/main/java/org/cyclonedx/model/ExternalReference.java index 6ce1eb9810..9aa0802020 100644 --- a/src/main/java/org/cyclonedx/model/ExternalReference.java +++ b/src/main/java/org/cyclonedx/model/ExternalReference.java @@ -77,8 +77,8 @@ public enum Type { @JsonProperty("security-contact") SECURITY_CONTACT("security-contact"), @VersionFilter(Version.VERSION_15) - @JsonProperty("model_card") - MODEL_CARD("model_card"), + @JsonProperty("model-card") + MODEL_CARD("model-card"), @VersionFilter(Version.VERSION_15) @JsonProperty("attestation") ATTESTATION("attestation"), From 21688901ddc712862fb324bfebefbf8f488d9858 Mon Sep 17 00:00:00 2001 From: Alexander Alzate Date: Sat, 9 May 2026 09:41:44 -0500 Subject: [PATCH 7/8] Add related cryptographic assets and schema (#828) Introduce support for related cryptographic assets across crypto models and add a cryptography definitions schema. Changes include: - Add new RelatedCryptographicAsset model with type and ref, equals/hashCode. - Extend AlgorithmProperties, CertificateProperties, ProtocolProperties, and RelatedCryptoMaterialProperties to include List relatedCryptographicAssets with XML wrapper annotations, getters/setters, and include in equals/hashCode. - Annotate new fields with @VersionFilter(Version.VERSION_17) and add necessary imports. - Add cryptography-defs.schema.json resource containing algorithm family and elliptic curve metadata. - Register the new schema in CycloneDxSchema offlineMappings so it can be resolved at runtime. These changes enable expressing relationships between crypto objects and external cryptographic assets and provide a formal schema for algorithm/curve definitions. --- .../java/org/cyclonedx/CycloneDxSchema.java | 2 + .../component/crypto/AlgorithmProperties.java | 51 +- .../crypto/CertificateProperties.java | 26 +- .../component/crypto/ProtocolProperties.java | 22 +- .../RelatedCryptoMaterialProperties.java | 25 +- .../crypto/RelatedCryptographicAsset.java | 48 ++ .../resources/cryptography-defs.schema.json | 592 ++++++++++++++++++ 7 files changed, 753 insertions(+), 13 deletions(-) create mode 100644 src/main/java/org/cyclonedx/model/component/crypto/RelatedCryptographicAsset.java create mode 100644 src/main/resources/cryptography-defs.schema.json diff --git a/src/main/java/org/cyclonedx/CycloneDxSchema.java b/src/main/java/org/cyclonedx/CycloneDxSchema.java index b279e022f4..1ebe729ed4 100644 --- a/src/main/java/org/cyclonedx/CycloneDxSchema.java +++ b/src/main/java/org/cyclonedx/CycloneDxSchema.java @@ -108,6 +108,8 @@ public JsonSchema getJsonSchema(Version schemaVersion, final ObjectMapper mapper 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); 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..9f116b60b9 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; @@ -22,6 +24,8 @@ @JsonPropertyOrder({ "primitive", "parameterSetIdentifier", + "algorithmFamily", + "ellipticCurve", "curve", "executionEnvironment", "implementationPlatform", @@ -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/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/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 From 67234a10a23d9023943ed0d85259d4f12eeb988b Mon Sep 17 00:00:00 2001 From: Alexander Alzate Date: Sat, 9 May 2026 10:18:55 -0500 Subject: [PATCH 8/8] Add CycloneDX 1.7 crypto tests; reorder JSON props (#830) Add comprehensive CycloneDX 1.7 cryptography unit tests for both JSON and XML generators (updates to BomJsonGeneratorTest and BomXmlGeneratorTest). Update schema verification to recognize -1.7 fixtures in JsonSchemaVerificationTest and XmlSchemaVerificationTest. Adjust AlgorithmProperties@JsonPropertyOrder to change the ordering of parameterSetIdentifier, curve, and ellipticCurve to match the 1.7 schema expectations. --- .../component/crypto/AlgorithmProperties.java | 4 +- .../org/cyclonedx/BomJsonGeneratorTest.java | 74 +++++++++++++++++++ .../org/cyclonedx/BomXmlGeneratorTest.java | 74 +++++++++++++++++++ .../schema/JsonSchemaVerificationTest.java | 3 + .../schema/XmlSchemaVerificationTest.java | 3 + 5 files changed, 156 insertions(+), 2 deletions(-) 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 9f116b60b9..99192c4704 100644 --- a/src/main/java/org/cyclonedx/model/component/crypto/AlgorithmProperties.java +++ b/src/main/java/org/cyclonedx/model/component/crypto/AlgorithmProperties.java @@ -23,10 +23,10 @@ @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyOrder({ "primitive", - "parameterSetIdentifier", "algorithmFamily", - "ellipticCurve", + "parameterSetIdentifier", "curve", + "ellipticCurve", "executionEnvironment", "implementationPlatform", "certificationLevel", diff --git a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java index 4afd27e134..77310c3cd1 100644 --- a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java @@ -952,6 +952,80 @@ public void schema17_testMetadataDistribution_xml() throws Exception { 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 7c494c49ca..406f5d44c1 100644 --- a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java @@ -1141,6 +1141,80 @@ public void schema17_testMetadataDistribution_xml() throws Exception { 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/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; }