From e51d785e16eacf1ef3c50b039486885332c6b30e Mon Sep 17 00:00:00 2001 From: jjhz Date: Wed, 10 Sep 2025 13:50:30 +1000 Subject: [PATCH 1/4] Fixes XML Serialization: Corrects the XML generation for component authors to produce schema-compliant ... structure, resolving the issue of nested tags. Add backward compatibility to support legecy format: ... Signed-off-by: jjhz --- .../java/org/cyclonedx/model/Component.java | 5 +- .../ComponentAuthorsDeserializer.java | 56 +++++++++++++++ .../org/cyclonedx/BomJsonGeneratorTest.java | 12 ++++ .../org/cyclonedx/BomXmlGeneratorTest.java | 68 ++++++++++++++++++- .../invalid-component-authors-legacy-1.6.xml | 16 +++++ .../1.6/valid-component-authors-1.6.xml | 16 +++++ ...lid-component-authors-json-object-1.6.json | 22 ++++++ 7 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/cyclonedx/util/deserializer/ComponentAuthorsDeserializer.java create mode 100644 src/test/resources/1.6/invalid-component-authors-legacy-1.6.xml create mode 100644 src/test/resources/1.6/valid-component-authors-1.6.xml create mode 100644 src/test/resources/1.6/valid-component-authors-json-object-1.6.json diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index 0ba8fabe1..265f17f08 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -29,6 +29,7 @@ import org.cyclonedx.model.component.crypto.CryptoProperties; import org.cyclonedx.model.component.Tags; import org.cyclonedx.model.component.data.ComponentData; +import org.cyclonedx.util.deserializer.ComponentAuthorsDeserializer; import org.cyclonedx.util.deserializer.ComponentListDeserializer; import org.cyclonedx.util.deserializer.ExternalReferencesDeserializer; import org.cyclonedx.util.deserializer.HashesDeserializer; @@ -223,7 +224,6 @@ public String getScopeName() { private Tags tags; @VersionFilter(Version.VERSION_16) - @JsonProperty("authors") private List authors; @VersionFilter(Version.VERSION_16) @@ -556,10 +556,13 @@ public void setTags(final Tags tags) { this.tags = tags; } + @JacksonXmlElementWrapper(localName = "authors") + @JacksonXmlProperty(localName = "author") public List getAuthors() { return authors; } + @JsonDeserialize(using = ComponentAuthorsDeserializer.class) public void setAuthors(final List authors) { this.authors = authors; } diff --git a/src/main/java/org/cyclonedx/util/deserializer/ComponentAuthorsDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/ComponentAuthorsDeserializer.java new file mode 100644 index 000000000..f980d1564 --- /dev/null +++ b/src/main/java/org/cyclonedx/util/deserializer/ComponentAuthorsDeserializer.java @@ -0,0 +1,56 @@ +/* + * 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 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.dataformat.xml.deser.FromXmlParser; +import org.cyclonedx.model.OrganizationalContact; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class ComponentAuthorsDeserializer extends JsonDeserializer> { + + @Override + public List deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + List contacts = new ArrayList<>(); + if (p instanceof FromXmlParser) { // Handle XML + while (p.nextToken() != JsonToken.END_OBJECT && p.currentToken() != JsonToken.END_OBJECT) { + if (p.currentToken() == JsonToken.FIELD_NAME) { + // Handles both and as the item tag + p.nextToken(); + OrganizationalContact contact = p.readValueAs(OrganizationalContact.class); + contacts.add(contact); + } + } + } else { // Handle JSON + if (p.isExpectedStartArrayToken()) { + while (p.nextToken() != JsonToken.END_ARRAY) { + // Handles case where author is a JSON object + contacts.add(p.readValueAs(OrganizationalContact.class)); + } + } + } + return contacts; + } +} diff --git a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java index 2d9840577..0d88a7333 100644 --- a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java @@ -602,6 +602,18 @@ public void testIssue492() throws Exception { assertTrue(parser.isValid(loadedFile, version)); } + @Test + public void testComponentAuthorsDeserializationJsonObject16() throws Exception { + Bom bom = createCommonJsonBom("/1.6/valid-component-authors-json-object-1.6.json"); + Component component = bom.getComponents().get(0); + assertNotNull(component.getAuthors()); + assertEquals(2, component.getAuthors().size()); + assertEquals("Test Author 1", component.getAuthors().get(0).getName()); + assertEquals("author1@example.com", component.getAuthors().get(0).getEmail()); + assertEquals("Test Author 2", component.getAuthors().get(1).getName()); + assertNull(component.getAuthors().get(1).getEmail()); + } + 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 bf041845b..75dce4b99 100644 --- a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java @@ -46,13 +46,21 @@ import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import org.w3c.dom.Document; +import org.w3c.dom.NodeList; + +import javax.xml.namespace.NamespaceContext; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.xpath.XPath; +import javax.xml.xpath.XPathConstants; +import javax.xml.xpath.XPathFactory; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; -import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.UUID; @@ -786,6 +794,64 @@ public void testIssue408Regression_jsonToXml_externalReferenceBom() throws Excep assertTrue(parser.isValid(loadedFile, version)); } + @Test + public void testComponentAuthorSerializationOutputAsString() throws Exception { + Version version = Version.VERSION_16; + Bom bom = createCommonBomXml("/1.6/valid-component-authors-1.6.xml"); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); + String xmlString = generator.toXmlString(); + File loadedFile = writeToFile(xmlString); + + XmlParser parser = new XmlParser(); + assertTrue(parser.isValid(loadedFile, version)); + + // Verify the xml content + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + Document doc = dbf.newDocumentBuilder() + .parse(new java.io.ByteArrayInputStream(xmlString.getBytes())); + + XPath xpath = XPathFactory.newInstance().newXPath(); + xpath.setNamespaceContext(new NamespaceContext() { + @Override + public String getNamespaceURI(String prefix) { + return "bom".equals(prefix) ? "http://cyclonedx.org/schema/bom/1.6" : null; + } + @Override + public String getPrefix(String namespaceURI) { + return "http://cyclonedx.org/schema/bom/1.6".equals(namespaceURI) ? "bom" : null; + } + @Override + public Iterator getPrefixes(String namespaceURI) { + return Collections.singleton("bom").iterator(); + } + }); + + NodeList authors = (NodeList) xpath.evaluate( + "//bom:component/bom:authors/bom:author", + doc, + XPathConstants.NODESET + ); + assertEquals(2, authors.getLength(), "There should be exactly 2 elements"); + + String author1 = xpath.evaluate("//bom:component/bom:authors/bom:author[1]/bom:name", doc); + String author2 = xpath.evaluate("//bom:component/bom:authors/bom:author[2]/bom:name", doc); + + assertEquals("Test Author 1", author1); + assertEquals("Test Author 2", author2); + } + + @Test + public void testComponentAuthorsDeserializationLegacy() throws Exception { + Bom bom = createCommonBomXml("/1.6/invalid-component-authors-legacy-1.6.xml"); + Component component = bom.getComponents().get(0); + assertNotNull(component.getAuthors()); + assertEquals(2, component.getAuthors().size()); + assertEquals("Test Author 1", component.getAuthors().get(0).getName()); + assertEquals("Test Author 2", component.getAuthors().get(1).getName()); + } + private void assertExternalReferenceInfo(Bom bom) { assertEquals(3, bom.getExternalReferences().size()); assertEquals(3, bom.getComponents().get(0).getExternalReferences().size()); diff --git a/src/test/resources/1.6/invalid-component-authors-legacy-1.6.xml b/src/test/resources/1.6/invalid-component-authors-legacy-1.6.xml new file mode 100644 index 000000000..60e58d178 --- /dev/null +++ b/src/test/resources/1.6/invalid-component-authors-legacy-1.6.xml @@ -0,0 +1,16 @@ + + + + + + + Test Author 1 + + + Test Author 2 + + + Test Component + + + \ No newline at end of file diff --git a/src/test/resources/1.6/valid-component-authors-1.6.xml b/src/test/resources/1.6/valid-component-authors-1.6.xml new file mode 100644 index 000000000..9e937e724 --- /dev/null +++ b/src/test/resources/1.6/valid-component-authors-1.6.xml @@ -0,0 +1,16 @@ + + + + + + + Test Author 1 + + + Test Author 2 + + + Test Component + + + \ No newline at end of file diff --git a/src/test/resources/1.6/valid-component-authors-json-object-1.6.json b/src/test/resources/1.6/valid-component-authors-json-object-1.6.json new file mode 100644 index 000000000..600fd0dce --- /dev/null +++ b/src/test/resources/1.6/valid-component-authors-json-object-1.6.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:e1acbeda-240f-4ab6-bd4e-749ab4183fec", + "version": 1, + "components": [ + { + "type": "application", + "name": "Test Component", + "authors": [ + { + "name": "Test Author 1", + "email": "author1@example.com" + }, + { + "name": "Test Author 2" + } + ] + } + ] +} \ No newline at end of file From 69c020c3858df9d06f56bd6ec4e8add37c65a447 Mon Sep 17 00:00:00 2001 From: jjhz Date: Fri, 12 Sep 2025 16:53:47 +1000 Subject: [PATCH 2/4] Code refactored. Signed-off-by: jjhz --- .../java/org/cyclonedx/model/Component.java | 9 ++- .../ComponentAuthorsDeserializer.java | 19 +++-- .../ComponentAuthorsSerializer.java | 60 ++++++++++++++ .../org/cyclonedx/BomJsonGeneratorTest.java | 78 +++++++++++++++---- .../org/cyclonedx/BomXmlGeneratorTest.java | 58 +++++++++++--- ...id-component-authors-bad-item-name-1.6.xml | 21 +++++ .../invalid-component-authors-legacy-1.6.xml | 31 ++++---- .../1.6/valid-component-authors-1.6.json | 25 ++++++ .../1.6/valid-component-authors-1.6.xml | 31 ++++---- ...lid-component-authors-json-object-1.6.json | 22 ------ 10 files changed, 273 insertions(+), 81 deletions(-) create mode 100644 src/main/java/org/cyclonedx/util/serializer/ComponentAuthorsSerializer.java create mode 100644 src/test/resources/1.6/invalid-component-authors-bad-item-name-1.6.xml create mode 100644 src/test/resources/1.6/valid-component-authors-1.6.json delete mode 100644 src/test/resources/1.6/valid-component-authors-json-object-1.6.json diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index 265f17f08..4b1af3759 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import org.cyclonedx.Version; import org.cyclonedx.model.component.ModelCard; import org.cyclonedx.model.component.crypto.CryptoProperties; @@ -44,6 +45,7 @@ import com.github.packageurl.PackageURL; import org.cyclonedx.util.deserializer.LicenseDeserializer; import org.cyclonedx.util.deserializer.PropertiesDeserializer; +import org.cyclonedx.util.serializer.ComponentAuthorsSerializer; @SuppressWarnings("unused") @JacksonXmlRootElement(localName = "component") @@ -224,6 +226,10 @@ public String getScopeName() { private Tags tags; @VersionFilter(Version.VERSION_16) + @JsonProperty("authors") + @JacksonXmlElementWrapper(localName = "authors") + @JsonSerialize(using = ComponentAuthorsSerializer.class) + @JsonDeserialize(using = ComponentAuthorsDeserializer.class) private List authors; @VersionFilter(Version.VERSION_16) @@ -556,13 +562,10 @@ public void setTags(final Tags tags) { this.tags = tags; } - @JacksonXmlElementWrapper(localName = "authors") - @JacksonXmlProperty(localName = "author") public List getAuthors() { return authors; } - @JsonDeserialize(using = ComponentAuthorsDeserializer.class) public void setAuthors(final List authors) { this.authors = authors; } diff --git a/src/main/java/org/cyclonedx/util/deserializer/ComponentAuthorsDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/ComponentAuthorsDeserializer.java index f980d1564..fb601a055 100644 --- a/src/main/java/org/cyclonedx/util/deserializer/ComponentAuthorsDeserializer.java +++ b/src/main/java/org/cyclonedx/util/deserializer/ComponentAuthorsDeserializer.java @@ -37,10 +37,19 @@ public List deserialize(JsonParser p, DeserializationCont if (p instanceof FromXmlParser) { // Handle XML while (p.nextToken() != JsonToken.END_OBJECT && p.currentToken() != JsonToken.END_OBJECT) { if (p.currentToken() == JsonToken.FIELD_NAME) { - // Handles both and as the item tag - p.nextToken(); - OrganizationalContact contact = p.readValueAs(OrganizationalContact.class); - contacts.add(contact); + String fieldName = p.currentName(); + if ("author".equals(fieldName) || "authors".equals(fieldName)) { + // Handles both and as the item tag + p.nextToken(); + contacts.add(p.readValueAs(OrganizationalContact.class)); + } else { + ctxt.reportInputMismatch( + List.class, + "Unexpected field '%s' in %s", + fieldName, + getClass().getSimpleName() + ); + } } } } else { // Handle JSON @@ -53,4 +62,4 @@ public List deserialize(JsonParser p, DeserializationCont } return contacts; } -} +} \ No newline at end of file diff --git a/src/main/java/org/cyclonedx/util/serializer/ComponentAuthorsSerializer.java b/src/main/java/org/cyclonedx/util/serializer/ComponentAuthorsSerializer.java new file mode 100644 index 000000000..50750b10e --- /dev/null +++ b/src/main/java/org/cyclonedx/util/serializer/ComponentAuthorsSerializer.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.util.serializer; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +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.model.OrganizationalContact; + +import javax.xml.namespace.QName; +import java.io.IOException; +import java.util.List; + +public class ComponentAuthorsSerializer extends StdSerializer> { + + + public ComponentAuthorsSerializer() { + super(List.class, false); + } + + + @Override + public void serialize(List authors, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { + if (jsonGenerator instanceof ToXmlGenerator) { // Handle XML + ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator; + xmlGenerator.writeStartArray(); + + for (OrganizationalContact author : authors) { + xmlGenerator.setNextName(new QName("author")); + xmlGenerator.writeObject(author); + } + + xmlGenerator.writeEndArray(); + } else { // Handle JSON, as default. + JsonSerializer defaultSerializer = + serializerProvider.findValueSerializer(List.class, null); + defaultSerializer.serialize(authors, jsonGenerator, serializerProvider); + } + } + +} diff --git a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java index 0d88a7333..d87e74706 100644 --- a/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomJsonGeneratorTest.java @@ -21,17 +21,14 @@ import com.fasterxml.jackson.databind.JsonNode; import java.nio.charset.StandardCharsets; + +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.io.IOUtils; 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.*; import org.cyclonedx.model.Component.Type; -import org.cyclonedx.model.License; -import org.cyclonedx.model.LicenseChoice; -import org.cyclonedx.model.Metadata; -import org.cyclonedx.model.Service; import org.cyclonedx.model.license.Expression; import org.cyclonedx.parsers.JsonParser; import org.cyclonedx.parsers.XmlParser; @@ -48,6 +45,8 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; import java.util.stream.Stream; import java.util.Objects; @@ -603,15 +602,64 @@ public void testIssue492() throws Exception { } @Test - public void testComponentAuthorsDeserializationJsonObject16() throws Exception { - Bom bom = createCommonJsonBom("/1.6/valid-component-authors-json-object-1.6.json"); - Component component = bom.getComponents().get(0); - assertNotNull(component.getAuthors()); - assertEquals(2, component.getAuthors().size()); - assertEquals("Test Author 1", component.getAuthors().get(0).getName()); - assertEquals("author1@example.com", component.getAuthors().get(0).getEmail()); - assertEquals("Test Author 2", component.getAuthors().get(1).getName()); - assertNull(component.getAuthors().get(1).getEmail()); + public void testComponentAuthorsSerializationAndDeserialization() throws Exception { + Version version = Version.VERSION_16; + Bom bom = createCommonJsonBom("/1.6/valid-component-authors-1.6.json"); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component bomComponent = bom.getComponents().get(0); + assertEquals("Outer Author with String value", bomComponent.getAuthor()); + + List bomAuthors = bomComponent.getAuthors(); + assertNotNull(bomAuthors); + assertEquals(2, bomAuthors.size()); + + OrganizationalContact bomAuthor1 = bomAuthors.get(0); + OrganizationalContact bomAuthor2 = bomAuthors.get(1); + + assertNotNull(bomAuthor1); + assertEquals("Test Author 1", bomAuthor1.getName()); + assertEquals("author1@example.com", bomAuthor1.getEmail()); + assertEquals("123", bomAuthor1.getPhone()); + + assertNotNull(bomAuthor2); + assertEquals("Test Author 2", bomAuthor2.getName()); + assertEquals("author2@example.com", bomAuthor2.getEmail()); + assertEquals("456", bomAuthor2.getPhone()); + + BomJsonGenerator generator = BomGeneratorFactory.createJson(version, bom); + String jsonString = generator.toJsonString(); + + File loadedFile = writeToFile(jsonString); + JsonParser parser = new JsonParser(); + assertTrue(parser.isValid(loadedFile, version)); + + // Verify the json content + ObjectMapper mapper = new ObjectMapper(); + JsonNode rootNode = mapper.readTree(jsonString); + + JsonNode component = rootNode.path("components").get(0); + assertNotNull(component); + + String outerAuthor = component.path("author").asText(); + assertEquals("Outer Author with String value", outerAuthor, "Outer author value mismatch"); + + JsonNode authorsNode = component.path("authors"); + assertTrue(authorsNode.isArray()); + assertEquals(2, authorsNode.size(), "Authors list size mismatch"); + + Iterator elements = authorsNode.elements(); + JsonNode author1 = elements.next(); + assertEquals("Test Author 1", author1.path("name").asText()); + assertEquals("author1@example.com", author1.path("email").asText()); + assertEquals("123", author1.path("phone").asText()); + + JsonNode author2 = elements.next(); + assertEquals("Test Author 2", author2.path("name").asText()); + assertEquals("author2@example.com", author2.path("email").asText()); + assertEquals("456", author2.path("phone").asText()); } private void assertExternalReferenceInfo(Bom bom) { diff --git a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java index 75dce4b99..cb4123b42 100644 --- a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java @@ -45,6 +45,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; import org.w3c.dom.Document; import org.w3c.dom.NodeList; @@ -794,10 +795,37 @@ public void testIssue408Regression_jsonToXml_externalReferenceBom() throws Excep assertTrue(parser.isValid(loadedFile, version)); } - @Test - public void testComponentAuthorSerializationOutputAsString() throws Exception { + @ParameterizedTest + @ValueSource(strings = { + "/1.6/valid-component-authors-1.6.xml", + "/1.6/invalid-component-authors-legacy-1.6.xml" + }) + public void testComponentAuthorsSerializationAndDeserialization(String xmlFilePath) throws Exception { Version version = Version.VERSION_16; - Bom bom = createCommonBomXml("/1.6/valid-component-authors-1.6.xml"); + Bom bom = createCommonBomXml(xmlFilePath); + + assertNotNull(bom.getComponents()); + assertEquals(1, bom.getComponents().size()); + + Component bomComponent = bom.getComponents().get(0); + assertEquals("Outer Author with String value", bomComponent.getAuthor()); + + List bomAuthors = bomComponent.getAuthors(); + assertNotNull(bomAuthors); + assertEquals(2, bomAuthors.size()); + + OrganizationalContact bomAuthor1 = bomAuthors.get(0); + OrganizationalContact bomAuthor2 = bomAuthors.get(1); + + assertNotNull(bomAuthor1); + assertEquals("Test Author 1", bomAuthor1.getName()); + assertEquals("author1@example.com", bomAuthor1.getEmail()); + assertEquals("123", bomAuthor1.getPhone()); + + assertNotNull(bomAuthor2); + assertEquals("Test Author 2", bomAuthor2.getName()); + assertEquals("author2@example.com", bomAuthor2.getEmail()); + assertEquals("456", bomAuthor2.getPhone()); BomXmlGenerator generator = BomGeneratorFactory.createXml(version, bom); String xmlString = generator.toXmlString(); @@ -840,16 +868,26 @@ public Iterator getPrefixes(String namespaceURI) { assertEquals("Test Author 1", author1); assertEquals("Test Author 2", author2); + + String email1 = xpath.evaluate("//bom:component/bom:authors/bom:author[1]/bom:email", doc); + String email2 = xpath.evaluate("//bom:component/bom:authors/bom:author[2]/bom:email", doc); + + assertEquals("author1@example.com", email1); + assertEquals("author2@example.com", email2); + + String phone1 = xpath.evaluate("//bom:component/bom:authors/bom:author[1]/bom:phone", doc); + String phone2 = xpath.evaluate("//bom:component/bom:authors/bom:author[2]/bom:phone", doc); + + assertEquals("123", phone1); + assertEquals("456", phone2); + + String outerAuthorStr = (String) xpath.evaluate("//bom:component/bom:author", doc, XPathConstants.STRING); + assertEquals("Outer Author with String value", outerAuthorStr); } @Test - public void testComponentAuthorsDeserializationLegacy() throws Exception { - Bom bom = createCommonBomXml("/1.6/invalid-component-authors-legacy-1.6.xml"); - Component component = bom.getComponents().get(0); - assertNotNull(component.getAuthors()); - assertEquals(2, component.getAuthors().size()); - assertEquals("Test Author 1", component.getAuthors().get(0).getName()); - assertEquals("Test Author 2", component.getAuthors().get(1).getName()); + public void testComponentAuthorsWithInvalidItemTag(){ + assertThrows(ParseException.class, () -> createCommonBomXml("/1.6/invalid-component-authors-bad-item-name-1.6.xml")); } private void assertExternalReferenceInfo(Bom bom) { diff --git a/src/test/resources/1.6/invalid-component-authors-bad-item-name-1.6.xml b/src/test/resources/1.6/invalid-component-authors-bad-item-name-1.6.xml new file mode 100644 index 000000000..327d4b01b --- /dev/null +++ b/src/test/resources/1.6/invalid-component-authors-bad-item-name-1.6.xml @@ -0,0 +1,21 @@ + + + + + + + Test Author 1 + author1@example.com + 123 + + + Test Author 2 + author2@example.com + 456 + + + Outer Author with String value + Test Component + + + \ No newline at end of file diff --git a/src/test/resources/1.6/invalid-component-authors-legacy-1.6.xml b/src/test/resources/1.6/invalid-component-authors-legacy-1.6.xml index 60e58d178..c32a50a29 100644 --- a/src/test/resources/1.6/invalid-component-authors-legacy-1.6.xml +++ b/src/test/resources/1.6/invalid-component-authors-legacy-1.6.xml @@ -1,16 +1,21 @@ - - - - - Test Author 1 - - - Test Author 2 - - - Test Component - - + + + + + Test Author 1 + author1@example.com + 123 + + + Test Author 2 + author2@example.com + 456 + + + Outer Author with String value + Test Component + + \ No newline at end of file diff --git a/src/test/resources/1.6/valid-component-authors-1.6.json b/src/test/resources/1.6/valid-component-authors-1.6.json new file mode 100644 index 000000000..a375d7318 --- /dev/null +++ b/src/test/resources/1.6/valid-component-authors-1.6.json @@ -0,0 +1,25 @@ +{ + "bomFormat" : "CycloneDX", + "specVersion" : "1.6", + "serialNumber" : "urn:uuid:e1acbeda-240f-4ab6-bd4e-749ab4183fec", + "version" : 1, + "components" : [ + { + "type" : "application", + "authors" : [ + { + "name" : "Test Author 1", + "email" : "author1@example.com", + "phone" : "123" + }, + { + "name" : "Test Author 2", + "email" : "author2@example.com", + "phone" : "456" + } + ], + "author" : "Outer Author with String value", + "name" : "Test Component" + } + ] +} \ No newline at end of file diff --git a/src/test/resources/1.6/valid-component-authors-1.6.xml b/src/test/resources/1.6/valid-component-authors-1.6.xml index 9e937e724..a4d1876a0 100644 --- a/src/test/resources/1.6/valid-component-authors-1.6.xml +++ b/src/test/resources/1.6/valid-component-authors-1.6.xml @@ -1,16 +1,21 @@ - - - - - Test Author 1 - - - Test Author 2 - - - Test Component - - + + + + + Test Author 1 + author1@example.com + 123 + + + Test Author 2 + author2@example.com + 456 + + + Outer Author with String value + Test Component + + \ No newline at end of file diff --git a/src/test/resources/1.6/valid-component-authors-json-object-1.6.json b/src/test/resources/1.6/valid-component-authors-json-object-1.6.json deleted file mode 100644 index 600fd0dce..000000000 --- a/src/test/resources/1.6/valid-component-authors-json-object-1.6.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", - "bomFormat": "CycloneDX", - "specVersion": "1.6", - "serialNumber": "urn:uuid:e1acbeda-240f-4ab6-bd4e-749ab4183fec", - "version": 1, - "components": [ - { - "type": "application", - "name": "Test Component", - "authors": [ - { - "name": "Test Author 1", - "email": "author1@example.com" - }, - { - "name": "Test Author 2" - } - ] - } - ] -} \ No newline at end of file From b52cfd147694f1f58e1e484d82697371477f2130 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Sep 2025 07:01:34 +0000 Subject: [PATCH 3/4] chore(deps): Bump github/codeql-action from 3.30.1 to 3.30.2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3.30.1 to 3.30.2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f1f6e5f6af878fb37288ce1c627459e94dbf7d01...d3678e237b9c32a6c9bffb3315c335f976f3549f) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Signed-off-by: jjhz --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fca83d72b..0f3eee049 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -31,10 +31,10 @@ jobs: if: ${{ github.event_name == 'pull_request' }} # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # tag=v3.29.5 + uses: github/codeql-action/init@d3678e237b9c32a6c9bffb3315c335f976f3549f # tag=v3.29.5 with: languages: java - name: Autobuild - uses: github/codeql-action/autobuild@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # tag=v3.29.5 + uses: github/codeql-action/autobuild@d3678e237b9c32a6c9bffb3315c335f976f3549f # tag=v3.29.5 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # tag=v3.29.5 + uses: github/codeql-action/analyze@d3678e237b9c32a6c9bffb3315c335f976f3549f # tag=v3.29.5 From e173a1abca838129bf87bb868fd629bc2f5a1a1b Mon Sep 17 00:00:00 2001 From: jjhz Date: Fri, 12 Sep 2025 17:58:17 +1000 Subject: [PATCH 4/4] Fix test case for Codacy. Signed-off-by: jjhz --- src/test/java/org/cyclonedx/BomXmlGeneratorTest.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java index cb4123b42..55499c778 100644 --- a/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java +++ b/src/test/java/org/cyclonedx/BomXmlGeneratorTest.java @@ -837,6 +837,10 @@ public void testComponentAuthorsSerializationAndDeserialization(String xmlFilePa // Verify the xml content DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); dbf.setNamespaceAware(true); + dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); + dbf.setFeature("http://xml.org/sax/features/external-general-entities", false); + dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false); + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); Document doc = dbf.newDocumentBuilder() .parse(new java.io.ByteArrayInputStream(xmlString.getBytes()));