diff --git a/src/main/java/org/cyclonedx/model/Pedigree.java b/src/main/java/org/cyclonedx/model/Pedigree.java index 6d1af51240..1c25e54847 100644 --- a/src/main/java/org/cyclonedx/model/Pedigree.java +++ b/src/main/java/org/cyclonedx/model/Pedigree.java @@ -27,6 +27,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper; import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty; import org.cyclonedx.Version; +import org.cyclonedx.util.deserializer.CommitListDeserializer; import org.cyclonedx.util.deserializer.ComponentWrapperDeserializer; @SuppressWarnings("unused") @@ -44,6 +45,7 @@ public class Pedigree extends ExtensibleElement { @JsonDeserialize(using = ComponentWrapperDeserializer.class) private Variants variants; + @JsonDeserialize(using = CommitListDeserializer.class) private List commits; @VersionFilter(Version.VERSION_12) diff --git a/src/main/java/org/cyclonedx/util/deserializer/CommitListDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/CommitListDeserializer.java new file mode 100644 index 0000000000..4dfbd38cb2 --- /dev/null +++ b/src/main/java/org/cyclonedx/util/deserializer/CommitListDeserializer.java @@ -0,0 +1,67 @@ +/* + * 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.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.dataformat.xml.deser.FromXmlParser; +import org.cyclonedx.model.Commit; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class CommitListDeserializer + extends JsonDeserializer> +{ + @Override + public List deserialize(JsonParser parser, DeserializationContext ctxt) + throws IOException + { + if (parser instanceof FromXmlParser) { + return Arrays.asList(parser.readValueAs(Commit[].class)); + } + + JsonToken token = parser.currentToken(); + if (token == JsonToken.START_ARRAY) { + return Arrays.asList(parser.readValueAs(Commit[].class)); + } else if (token == JsonToken.START_OBJECT) { + // XML-via-JsonNode path (e.g. ToolInformationDeserializer): wrapper element + // ... becomes {"commit": {...}} or {"commit": [{...},...]} + ObjectNode node = parser.readValueAs(ObjectNode.class); + if (node.has("commit")) { + JsonNode commitNode = node.get("commit"); + try (JsonParser cp = commitNode.traverse(parser.getCodec())) { + cp.nextToken(); + if (commitNode.isArray()) { + return Arrays.asList(cp.readValueAs(Commit[].class)); + } else { + return Collections.singletonList(cp.readValueAs(Commit.class)); + } + } + } + } + return Collections.emptyList(); + } +} \ No newline at end of file diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java index 0074cdbd32..0ba90c5045 100644 --- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java +++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java @@ -21,14 +21,10 @@ import org.apache.commons.io.IOUtils; import org.cyclonedx.Version; import org.cyclonedx.exception.ParseException; -import org.cyclonedx.model.Bom; -import org.cyclonedx.model.Component; +import org.cyclonedx.generators.BomGeneratorFactory; +import org.cyclonedx.generators.xml.BomXmlGenerator; +import org.cyclonedx.model.*; import org.cyclonedx.model.Component.Type; -import org.cyclonedx.model.Dependency; -import org.cyclonedx.model.ExternalReference; -import org.cyclonedx.model.LicenseChoice; -import org.cyclonedx.model.OrganizationalEntity; -import org.cyclonedx.model.Pedigree; import org.cyclonedx.model.attestation.Assessor; import org.cyclonedx.model.attestation.Attestation; import org.cyclonedx.model.attestation.AttestationMap; @@ -65,14 +61,13 @@ import org.cyclonedx.model.definition.Standard; import org.cyclonedx.model.license.Acknowledgement; import org.cyclonedx.model.license.Expression; +import org.cyclonedx.model.metadata.ToolInformation; import org.junit.jupiter.api.Test; import java.io.File; import java.io.InputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; +import java.nio.charset.StandardCharsets; +import java.util.*; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -765,4 +760,62 @@ void validateShouldNotBeVulnerableToXxe() throws Exception { .contains("not allowed due to restriction set by the accessExternalDTD property")); } + @Test + public void testMetadataToolComponentPedigreeWithSingleCommit() throws Exception { + final String expectedUid = "abc123def456abc123def456abc123def456abc1"; + + Commit commit = new Commit(); + commit.setUid(expectedUid); + + List commits = parseToolComponentPedigreeCommits(Collections.singletonList(commit)); + + assertEquals(1, commits.size()); + assertEquals(expectedUid, commits.get(0).getUid()); + } + + @Test + public void testMetadataToolComponentPedigreeWithMultipleCommits() throws Exception { + final String uid1 = "abc123def456abc123def456abc123def456abc1"; + final String uid2 = "def456abc123def456abc123def456abc123def4"; + + Commit commit1 = new Commit(); + commit1.setUid(uid1); + Commit commit2 = new Commit(); + commit2.setUid(uid2); + + List commits = parseToolComponentPedigreeCommits(Arrays.asList(commit1, commit2)); + + assertEquals(2, commits.size()); + assertEquals(uid1, commits.get(0).getUid()); + assertEquals(uid2, commits.get(1).getUid()); + } + + private List parseToolComponentPedigreeCommits(List commits) throws Exception { + Pedigree pedigree = new Pedigree(); + pedigree.setCommits(commits); + + Component toolComponent = new Component(); + toolComponent.setType(Component.Type.APPLICATION); + toolComponent.setName("my-build-tool"); + toolComponent.setPedigree(pedigree); + + ToolInformation toolInformation = new ToolInformation(); + toolInformation.setComponents(Collections.singletonList(toolComponent)); + + Metadata metadata = new Metadata(); + metadata.setToolChoice(toolInformation); + + Bom inputBom = new Bom(); + inputBom.setMetadata(metadata); + + BomXmlGenerator generator = BomGeneratorFactory.createXml(Version.VERSION_16, inputBom); + byte[] xml = generator.toXmlString().getBytes(StandardCharsets.UTF_8); + + return new XmlParser().parse(xml) + .getMetadata() + .getToolChoice() + .getComponents().get(0) + .getPedigree() + .getCommits(); + } }