diff --git a/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java b/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java index fd6e3abc8..bd655f800 100644 --- a/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java +++ b/src/main/java/org/cyclonedx/generators/AbstractBomGenerator.java @@ -5,6 +5,7 @@ import org.cyclonedx.CycloneDxSchema; import org.cyclonedx.Version; import org.cyclonedx.model.Bom; +import org.cyclonedx.util.serializer.EvidenceSerializer; import org.cyclonedx.util.serializer.InputTypeSerializer; import org.cyclonedx.util.serializer.LifecycleSerializer; import org.cyclonedx.util.serializer.MetadataSerializer; @@ -48,5 +49,9 @@ protected void setupObjectMapper(boolean isXml) { SimpleModule outputTypeModule = new SimpleModule(); outputTypeModule.addSerializer(new OutputTypeSerializer(isXml)); mapper.registerModule(outputTypeModule); + + SimpleModule evidenceModule = new SimpleModule(); + evidenceModule.addSerializer(new EvidenceSerializer(isXml, getSchemaVersion())); + mapper.registerModule(evidenceModule); } } diff --git a/src/main/java/org/cyclonedx/model/Bom.java b/src/main/java/org/cyclonedx/model/Bom.java index 6c1607fd2..cb170bc48 100644 --- a/src/main/java/org/cyclonedx/model/Bom.java +++ b/src/main/java/org/cyclonedx/model/Bom.java @@ -63,35 +63,35 @@ public class Bom extends ExtensibleElement { @JacksonXmlProperty(isAttribute = true) private String xmlns; - @VersionFilter(value = Version.VERSION_12) + @VersionFilter(Version.VERSION_12) private Metadata metadata; private List components; - @VersionFilter(value = Version.VERSION_12) + @VersionFilter(Version.VERSION_12) private List services; - @VersionFilter(value = Version.VERSION_11) + @VersionFilter(Version.VERSION_11) private DependencyList dependencies; - @VersionFilter(value = Version.VERSION_11) + @VersionFilter(Version.VERSION_11) @JsonDeserialize(using = ExternalReferencesDeserializer.class) private List externalReferences; - @VersionFilter(value = Version.VERSION_13) + @VersionFilter(Version.VERSION_13) private List compositions; - @VersionFilter(value = Version.VERSION_15) + @VersionFilter(Version.VERSION_15) private List formulation; - @VersionFilter(value = Version.VERSION_14) + @VersionFilter(Version.VERSION_14) @JsonDeserialize(using = VulnerabilityDeserializer.class) private List vulnerabilities; - @VersionFilter(value = Version.VERSION_15) + @VersionFilter(Version.VERSION_15) private List annotations; - @VersionFilter(value = Version.VERSION_13) + @VersionFilter(Version.VERSION_13) private List properties; @JacksonXmlProperty(isAttribute = true) @@ -107,7 +107,7 @@ public class Bom extends ExtensibleElement { private String bomFormat; @JsonOnly - @VersionFilter(value = Version.VERSION_14) + @VersionFilter(Version.VERSION_14) private Signature signature; public Metadata getMetadata() { diff --git a/src/main/java/org/cyclonedx/model/Component.java b/src/main/java/org/cyclonedx/model/Component.java index f914d3fdd..5774b64bb 100644 --- a/src/main/java/org/cyclonedx/model/Component.java +++ b/src/main/java/org/cyclonedx/model/Component.java @@ -100,7 +100,7 @@ public enum Type { MACHINE_LEARNING_MODEL("machine-learning-model"), @JsonProperty("data") DATA("data"), - @VersionFilter(value = Version.VERSION_16) + @VersionFilter(Version.VERSION_16) @JsonProperty("cryptographic-asset") CRYPTOGRAPHIC_ASSET("cryptographic-asset"); @@ -185,11 +185,11 @@ public String getScopeName() { @JsonProperty("data") private ComponentData data; - @VersionFilter(value = Version.VERSION_16) + @VersionFilter(Version.VERSION_16) @JsonProperty("cryptoProperties") private CryptoProperties cryptoProperties; - @VersionFilter(value = Version.VERSION_16) + @VersionFilter(Version.VERSION_16) @JsonProperty("provides") private List provides; diff --git a/src/main/java/org/cyclonedx/model/Copyright.java b/src/main/java/org/cyclonedx/model/Copyright.java index 0066414a6..c86c9ef15 100644 --- a/src/main/java/org/cyclonedx/model/Copyright.java +++ b/src/main/java/org/cyclonedx/model/Copyright.java @@ -18,6 +18,13 @@ */ package org.cyclonedx.model; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@JacksonXmlRootElement(localName = "copyright") public class Copyright { private String text; diff --git a/src/main/java/org/cyclonedx/model/Evidence.java b/src/main/java/org/cyclonedx/model/Evidence.java index 67f011a1e..743ed4073 100644 --- a/src/main/java/org/cyclonedx/model/Evidence.java +++ b/src/main/java/org/cyclonedx/model/Evidence.java @@ -29,6 +29,7 @@ import org.cyclonedx.model.component.evidence.Callstack; import org.cyclonedx.model.component.evidence.Identity; import org.cyclonedx.model.component.evidence.Occurrence; +import org.cyclonedx.util.deserializer.IdentityDeserializer; import org.cyclonedx.util.deserializer.LicenseDeserializer; import java.util.ArrayList; @@ -45,8 +46,8 @@ public class Evidence private List copyright; - @VersionFilter(Version.VERSION_15) - private Identity identity; + @VersionFilter(Version.VERSION_16) + private List identities; @VersionFilter(Version.VERSION_15) private List occurrences; @@ -66,6 +67,8 @@ public void setLicenseChoice(LicenseChoice licenseChoice) { } @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "copyright") + @JsonProperty("copyright") public List getCopyright() { return copyright; } @@ -81,14 +84,6 @@ public void addCopyright(Copyright copyright) { this.copyright.add(copyright); } - public Identity getIdentity() { - return identity; - } - - public void setIdentity(final Identity identity) { - this.identity = identity; - } - @JsonProperty("occurrences") @JacksonXmlElementWrapper(localName = "occurrences") @JacksonXmlProperty(localName = "occurrence") @@ -107,4 +102,16 @@ public Callstack getCallstack() { public void setCallstack(final Callstack callstack) { this.callstack = callstack; } + + @JacksonXmlElementWrapper(useWrapping = false) + @JacksonXmlProperty(localName = "identity") + @JsonProperty("identity") + @JsonDeserialize(using = IdentityDeserializer.class) + public List getIdentities() { + return identities; + } + + public void setIdentities(final List identities) { + this.identities = identities; + } } diff --git a/src/main/java/org/cyclonedx/model/component/evidence/Identity.java b/src/main/java/org/cyclonedx/model/component/evidence/Identity.java index 5052c1fdf..f071d4311 100644 --- a/src/main/java/org/cyclonedx/model/component/evidence/Identity.java +++ b/src/main/java/org/cyclonedx/model/component/evidence/Identity.java @@ -18,16 +18,16 @@ @JsonPropertyOrder({"field", "confidence", "concludedValue", "methods", "tools"}) public class Identity extends ExtensibleElement { - public Field field; + private Field field; - public Double confidence; + private Double confidence; @VersionFilter(Version.VERSION_16) - public String concludedValue; + private String concludedValue; - public List methods; + private List methods; - public List tools; + private List tools; public enum Field { @JsonProperty("group") diff --git a/src/main/java/org/cyclonedx/model/component/evidence/Method.java b/src/main/java/org/cyclonedx/model/component/evidence/Method.java index c6c362677..039843236 100644 --- a/src/main/java/org/cyclonedx/model/component/evidence/Method.java +++ b/src/main/java/org/cyclonedx/model/component/evidence/Method.java @@ -4,15 +4,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonPropertyOrder; -import org.cyclonedx.model.ExtensibleElement; +import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_EMPTY) @JsonPropertyOrder({"technique", "confidence", "value"}) +@JacksonXmlRootElement(localName = "method") public class Method - extends ExtensibleElement { - private Technique technique; private Double confidence; diff --git a/src/main/java/org/cyclonedx/model/component/evidence/Occurrence.java b/src/main/java/org/cyclonedx/model/component/evidence/Occurrence.java index c7ab8f4b8..72b87e5da 100644 --- a/src/main/java/org/cyclonedx/model/component/evidence/Occurrence.java +++ b/src/main/java/org/cyclonedx/model/component/evidence/Occurrence.java @@ -18,16 +18,16 @@ public class Occurrence extends ExtensibleElement private String location; - @VersionFilter(value = Version.VERSION_16) + @VersionFilter(Version.VERSION_16) private Integer line; - @VersionFilter(value = Version.VERSION_16) + @VersionFilter(Version.VERSION_16) private Integer offset; - @VersionFilter(value = Version.VERSION_16) + @VersionFilter(Version.VERSION_16) private Integer symbol; - @VersionFilter(value = Version.VERSION_16) + @VersionFilter(Version.VERSION_16) private String additionalContext; public String getBomRef() { diff --git a/src/main/java/org/cyclonedx/util/deserializer/IdentityDeserializer.java b/src/main/java/org/cyclonedx/util/deserializer/IdentityDeserializer.java new file mode 100644 index 000000000..961078cf3 --- /dev/null +++ b/src/main/java/org/cyclonedx/util/deserializer/IdentityDeserializer.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.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.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import org.cyclonedx.model.BomReference; +import org.cyclonedx.model.component.evidence.Identity; +import org.cyclonedx.model.component.evidence.Identity.Field; +import org.cyclonedx.model.component.evidence.Method; + +public class IdentityDeserializer + extends JsonDeserializer> { + + + private final ObjectMapper mapper = new ObjectMapper(); + @Override + public List deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + + List identities = new ArrayList<>(); + + if(node.has("identity")) { + node = node.get("identity"); + } + + if (node.isArray()) { + // If the node is an array, deserialize each element individually + for (JsonNode identityNode : node) { + Identity singleIdentity = deserializeSingleIdentity(identityNode); + identities.add(singleIdentity); + } + } else { + // If the node is a single object, deserialize it as a single Identity + identities.add(deserializeSingleIdentity(node)); + } + return identities; + } + + private Identity deserializeSingleIdentity(JsonNode node) { + Identity identity = new Identity(); + + if (node.has("field")) { + Field field = mapper.convertValue(node.get("field"), Field.class); + identity.setField(field); + } + + if (node.has("confidence")) { + Double confidence = node.get("confidence").asDouble(); + identity.setConfidence(confidence); + } + + if (node.has("concludedValue")) { + String concludedValue = node.get("concludedValue").asText(); + identity.setConcludedValue(concludedValue); + } + + if (node.has("methods")) { + JsonNode methodsNode = node.get("methods"); + + if(methodsNode.has("method")) { + methodsNode = methodsNode.get("method"); + } + + ArrayNode nodes = (methodsNode.isArray() ? (ArrayNode) methodsNode : new ArrayNode(null).add(methodsNode)); + + List methods = new ArrayList<>(); + for (JsonNode resolvesNode : nodes) { + Method method = mapper.convertValue(resolvesNode, Method.class); + methods.add(method); + } + identity.setMethods(methods); + } + + if (node.has("tools")) { + JsonNode toolsNode = node.get("tools"); + + if(toolsNode.has("tool")) { + toolsNode = toolsNode.get("tool"); + } + + ArrayNode nodes = (toolsNode.isArray() ? (ArrayNode) toolsNode : new ArrayNode(null).add(toolsNode)); + + List tools = new ArrayList<>(); + for (JsonNode resolvesNode : nodes) { + BomReference tool = mapper.convertValue(resolvesNode, BomReference.class); + tools.add(tool); + } + + identity.setTools(tools); + } + return identity; + } +} \ No newline at end of file diff --git a/src/main/java/org/cyclonedx/util/serializer/EvidenceSerializer.java b/src/main/java/org/cyclonedx/util/serializer/EvidenceSerializer.java new file mode 100644 index 000000000..34fe88e8a --- /dev/null +++ b/src/main/java/org/cyclonedx/util/serializer/EvidenceSerializer.java @@ -0,0 +1,82 @@ +package org.cyclonedx.util.serializer; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +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.Copyright; +import org.cyclonedx.model.Evidence; + +public class EvidenceSerializer + extends StdSerializer +{ + private final boolean isXml; + + private final Version version; + + public EvidenceSerializer(boolean isXml, Version version) { + this(null, isXml, version); + } + + public EvidenceSerializer(Class t, boolean isXml, Version version) { + super(t); + this.isXml = isXml; + this.version = version; + } + + @Override + public void serialize(Evidence value, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + if (isXml && jsonGenerator instanceof ToXmlGenerator) { + ToXmlGenerator xmlGenerator = (ToXmlGenerator) jsonGenerator; + serializeJson(xmlGenerator, value, serializerProvider); + } else { + serializeJson(jsonGenerator, value, serializerProvider); + } + } + + private void serializeJson(final JsonGenerator gen, final Evidence evidence, SerializerProvider serializerProvider) throws IOException { + gen.writeStartObject(); + if (CollectionUtils.isNotEmpty(evidence.getIdentities())) { + if(version.getVersion()>=Version.VERSION_16.getVersion()) { + gen.writeObjectField("identity", evidence.getIdentities()); + } else { + gen.writeObjectField("identity", evidence.getIdentities().get(0)); + } + } + + if(CollectionUtils.isNotEmpty(evidence.getOccurrences())) { + gen.writeObjectField("occurrences", evidence.getOccurrences()); + } + + if(evidence.getCallstack()!=null) { + gen.writeObjectField("callstack", evidence.getCallstack()); + } + + if(evidence.getLicenseChoice() != null) { + gen.writeFieldName("licenses"); + new LicenseChoiceSerializer().serialize(evidence.getLicenseChoice(), gen, serializerProvider); + } + + if(CollectionUtils.isNotEmpty(evidence.getCopyright())) { + gen.writeFieldName("copyright"); + gen.writeStartArray(); + for (Copyright item : evidence.getCopyright()) { + gen.writeStartObject(); + gen.writeStringField("text", item.getText()); + gen.writeEndObject(); + } + gen.writeEndArray(); + } + gen.writeEndObject(); + } + + @Override + public Class handledType() { + return Evidence.class; + } +} diff --git a/src/test/java/org/cyclonedx/parsers/AbstractParserTest.java b/src/test/java/org/cyclonedx/parsers/AbstractParserTest.java index df9c1a957..279e923d8 100644 --- a/src/test/java/org/cyclonedx/parsers/AbstractParserTest.java +++ b/src/test/java/org/cyclonedx/parsers/AbstractParserTest.java @@ -666,10 +666,10 @@ private void assertEvidence(final Evidence evidence, final Version version) { if(version== Version.VERSION_15) { assertCallStack(evidence.getCallstack()); assertOccurrences(evidence.getOccurrences()); - assertIdentity(evidence.getIdentity()); + assertIdentity(evidence.getIdentities().get(0)); } else { assertNull(evidence.getCallstack()); - assertNull(evidence.getIdentity()); + assertNull(evidence.getIdentities()); assertNull(evidence.getOccurrences()); } }