Repository: CycloneDX/cyclonedx-core-java (java)

Issue:
Unable to deserialize bom with pedigree
Dependency-Track user attempting to upload a BOM with pedigree, was unable to do so. The sample BOM was pulled directly from the CycloneDX website and is a valid example. The following exception was thrown:

```
org.cyclonedx.exception.ParseException: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `org.cyclonedx.model.Component$Type` from String "enhancement": not one of the values accepted for Enum class: [application, container, device, library, file, framework, operating-system, firmware]
 at [Source: (byte[])"<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3"
     serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"
     version="1">
    <components>
        <component type="library">
            <group>com.acme</group>
            <name>sample-library</name>
            <version>1.0.0</version>
            <pedigree>
                <ancestors>
                    <!-- The component from which com.acme's modified
                    version of sample-libr"[truncated 4119 bytes]; line: 47, column: 29] (through reference chain: org.cyclonedx.model.Bom["components"]->java.util.ArrayList[1]->org.cyclonedx.model.Component["type"])
	at org.cyclonedx.parsers.XmlParser.parse(XmlParser.java:89)
	at org.dependencytrack.tasks.BomUploadProcessingTask.inform(BomUploadProcessingTask.java:89)
	at alpine.event.framework.BaseEventService.lambda$publish$0(BaseEventService.java:99)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `org.cyclonedx.model.Component$Type` from String "enhancement": not one of the values accepted for Enum class: [application, container, device, library, file, framework, operating-system, firmware]
 at [Source: (byte[])"<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.3"
     serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"
     version="1">
    <components>
        <component type="library">
            <group>com.acme</group>
            <name>sample-library</name>
            <version>1.0.0</version>
            <pedigree>
                <ancestors>
                    <!-- The component from which com.acme's modified
                    version of sample-libr"[truncated 4119 bytes]; line: 47, column: 29] (through reference chain: org.cyclonedx.model.Bom["components"]->java.util.ArrayList[1]->org.cyclonedx.model.Component["type"])
	at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
	at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1851)
	at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:1079)
	at com.fasterxml.jackson.databind.deser.std.EnumDeserializer._deserializeAltString(EnumDeserializer.java:327)
	at com.fasterxml.jackson.databind.deser.std.EnumDeserializer._fromString(EnumDeserializer.java:214)
	at com.fasterxml.jackson.databind.deser.std.EnumDeserializer.deserialize(EnumDeserializer.java:188)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:324)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:225)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:197)
	at com.fasterxml.jackson.dataformat.xml.deser.WrapperHandlingDeserializer.deserialize(WrapperHandlingDeserializer.java:121)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:347)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
	at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
	at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:324)
	at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:187)
	at com.fasterxml.jackson.dataformat.xml.deser.WrapperHandlingDeserializer.deserialize(WrapperHandlingDeserializer.java:121)
	at com.fasterxml.jackson.dataformat.xml.deser.XmlDeserializationContext.readRootValue(XmlDeserializationContext.java:91)
	at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4593)
	at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3609)
	at org.cyclonedx.parsers.XmlParser.parse(XmlParser.java:87)
```


Need to rework unit tests to account for all valid examples in the specification repo. This library should be able to deserialize every single valid example without errors.

Pull request description:
Implement Patches for Pedigree
This PR adds patches in, and adds a test to ensures they are parsed appropriately (test is fairly bare bones, but can be improved).

This uses the example XML from `cyclonedx.org` for the unit test, and honestly, that's great :)

Fixes #104 

Patch:
diff --git a/src/main/java/org/cyclonedx/model/Diff.java b/src/main/java/org/cyclonedx/model/Diff.java
new file mode 100644
index 000000000..0c13860bb
--- /dev/null
+++ b/src/main/java/org/cyclonedx/model/Diff.java
@@ -0,0 +1,41 @@
+/*
+ * 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) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.model;
+
+public class Diff
+{
+  private AttachmentText text;
+  private String url;
+
+  public AttachmentText getText() {
+    return text;
+  }
+
+  public void setText(final AttachmentText text) {
+    this.text = text;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public void setUrl(final String url) {
+    this.url = url;
+  }
+}
diff --git a/src/main/java/org/cyclonedx/model/Issue.java b/src/main/java/org/cyclonedx/model/Issue.java
new file mode 100644
index 000000000..394ba7d9f
--- /dev/null
+++ b/src/main/java/org/cyclonedx/model/Issue.java
@@ -0,0 +1,101 @@
+/*
+ * 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) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+import org.cyclonedx.model.Source;
+
+@JsonPropertyOrder({"id", "name", "description", "source", "reference"})
+public class Issue
+{
+
+  public enum Type {
+    @JsonProperty("enhancement")
+    ENHANCEMENT("enhancement"),
+    @JsonProperty("security")
+    SECURITY("security"),
+    @JsonProperty("defect")
+    DEFECT("defect");
+
+    private final String name;
+
+    public String getTypeName() {
+      return this.name;
+    }
+
+    Type(String name) {
+      this.name = name;
+    }
+  }
+
+  private String id;
+
+  private String name;
+
+  private String description;
+
+  private Source source;
+
+  private ExternalReference references;
+
+  @JacksonXmlProperty(isAttribute = true)
+  private Type type;
+
+  public String getId() {
+    return id;
+  }
+
+  public void setId(final String id) {
+    this.id = id;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(final String name) {
+    this.name = name;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public void setDescription(final String description) {
+    this.description = description;
+  }
+
+  public Source getSource() {
+    return source;
+  }
+
+  public void setSource(final Source source) {
+    this.source = source;
+  }
+
+  public ExternalReference getReferences() {
+    return references;
+  }
+
+  public void setReferences(final ExternalReference references) {
+    this.references = references;
+  }
+}
diff --git a/src/main/java/org/cyclonedx/model/Patch.java b/src/main/java/org/cyclonedx/model/Patch.java
new file mode 100644
index 000000000..00eab51bb
--- /dev/null
+++ b/src/main/java/org/cyclonedx/model/Patch.java
@@ -0,0 +1,73 @@
+/*
+ * 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) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.model;
+
+import java.util.List;
+
+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;
+
+@SuppressWarnings("unused")
+@JsonPropertyOrder({"diff", "resolves"})
+public class Patch {
+
+  public enum Type {
+    @JsonProperty("backport")
+    BACKPORT("backport"),
+    @JsonProperty("unofficial")
+    UNOFFICIAL("unofficial");
+
+    private final String name;
+
+    public String getTypeName() {
+      return this.name;
+    }
+
+    Type(final String name) {
+      this.name = name;
+    }
+  }
+
+  private Diff diff;
+
+  private List<Issue> resolves;
+
+  @JacksonXmlElementWrapper(localName = "resolves")
+  @JacksonXmlProperty(localName = "issue")
+  public List<Issue> getResolves() {
+    return resolves;
+  }
+
+  public void setResolves(final List<Issue> resolves) {
+    this.resolves = resolves;
+  }
+
+  @JacksonXmlProperty(isAttribute = true, localName = "type")
+  private Type type;
+
+  public Diff getDiff() {
+    return diff;
+  }
+
+  public void setDiff(final Diff diff) {
+    this.diff = diff;
+  }
+}
diff --git a/src/main/java/org/cyclonedx/model/Pedigree.java b/src/main/java/org/cyclonedx/model/Pedigree.java
index b50223da5..1a66a0813 100644
--- a/src/main/java/org/cyclonedx/model/Pedigree.java
+++ b/src/main/java/org/cyclonedx/model/Pedigree.java
@@ -28,7 +28,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 import org.cyclonedx.util.ComponentWrapperDeserializer;
 
 @SuppressWarnings("unused")
-@JsonPropertyOrder({"ancestors", "descendants", "variants", "commits", "notes"})
+@JsonPropertyOrder({"ancestors", "descendants", "variants", "commits", "patches", "notes"})
 public class Pedigree extends ExtensibleElement {
 
     @JsonDeserialize(using = ComponentWrapperDeserializer.class)
@@ -40,6 +40,7 @@ public class Pedigree extends ExtensibleElement {
     @JsonDeserialize(using = ComponentWrapperDeserializer.class)
     private Variants variants;
     private List<Commit> commits;
+    private List<Patch> patches;
     private String notes;
 
     public Ancestors getAncestors() {
@@ -73,6 +74,16 @@ public class Pedigree extends ExtensibleElement {
         this.commits = commits;
     }
 
+    @JacksonXmlElementWrapper(localName = "patches")
+    @JacksonXmlProperty(localName = "patch")
+    public List<Patch> getPatches() {
+        return patches;
+    }
+
+    public void setPatches(List<Patch> patches) {
+        this.patches = patches;
+    }
+
     public String getNotes() {
         return notes;
     }
diff --git a/src/main/java/org/cyclonedx/model/Source.java b/src/main/java/org/cyclonedx/model/Source.java
new file mode 100644
index 000000000..41cece15a
--- /dev/null
+++ b/src/main/java/org/cyclonedx/model/Source.java
@@ -0,0 +1,45 @@
+/*
+ * 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) Steve Springett. All Rights Reserved.
+ */
+package org.cyclonedx.model;
+
+import java.net.URL;
+
+import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
+
+public class Source {
+  @JacksonXmlProperty(localName = "name", isAttribute = true)
+  private String name;
+  private URL url;
+
+  public URL getUrl() {
+    return url;
+  }
+
+  public void setUrl(final URL url) {
+    this.url = url;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(final String name) {
+    this.name = name;
+  }
+}
diff --git a/src/main/java/org/cyclonedx/model/vulnerability/Vulnerability10.java b/src/main/java/org/cyclonedx/model/vulnerability/Vulnerability10.java
index 036b4e73e..cd62a19a9 100644
--- a/src/main/java/org/cyclonedx/model/vulnerability/Vulnerability10.java
+++ b/src/main/java/org/cyclonedx/model/vulnerability/Vulnerability10.java
@@ -18,7 +18,6 @@
  */
 package org.cyclonedx.model.vulnerability;
 
-import java.net.URL;
 import java.util.List;
 import java.io.Serializable;
 
@@ -27,6 +26,7 @@ import com.fasterxml.jackson.annotation.JsonRootName;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
 import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
 import org.cyclonedx.model.ExtensibleType;
+import org.cyclonedx.model.Source;
 
 @JsonRootName(Vulnerability10.NAME)
 public class Vulnerability10
@@ -88,6 +88,7 @@ public class Vulnerability10
       for (Severity s : Severity.values()) {
         if (s.name.equals(text)) {
           return s;
+
         }
       }
       return null;
@@ -197,28 +198,6 @@ public class Vulnerability10
     this.id = id;
   }
 
-  public static class Source {
-    @JacksonXmlProperty(localName = "name", isAttribute = true)
-    private String name;
-    private URL url;
-
-    public URL getUrl() {
-      return url;
-    }
-
-    public void setUrl(final URL url) {
-      this.url = url;
-    }
-
-    public String getName() {
-      return name;
-    }
-
-    public void setName(final String name) {
-      this.name = name;
-    }
-  }
-
   public static class Score {
     private Double base;
     private Double impact;
diff --git a/src/main/java/org/cyclonedx/util/ExtensionDeserializer.java b/src/main/java/org/cyclonedx/util/ExtensionDeserializer.java
index 9266812d7..9bfcc4713 100644
--- a/src/main/java/org/cyclonedx/util/ExtensionDeserializer.java
+++ b/src/main/java/org/cyclonedx/util/ExtensionDeserializer.java
@@ -43,7 +43,7 @@ import org.cyclonedx.model.vulnerability.Vulnerability10.Recommendation;
 import org.cyclonedx.model.vulnerability.Vulnerability10.Score;
 import org.cyclonedx.model.vulnerability.Vulnerability10.ScoreSource;
 import org.cyclonedx.model.vulnerability.Vulnerability10.Severity;
-import org.cyclonedx.model.vulnerability.Vulnerability10.Source;
+import org.cyclonedx.model.Source;
 
 public class ExtensionDeserializer extends StdDeserializer<Extension>
 {
@@ -190,7 +190,7 @@ public class ExtensionDeserializer extends StdDeserializer<Extension>
     return c;
   }
 
-  private Vulnerability10.Source processSource(final JsonNode sourceNode) {
+  private Source processSource(final JsonNode sourceNode) {
     Source source = new Source();
     source.setName(getAsString("name", sourceNode));
     if (sourceNode.get(Vulnerability10.URL) != null) {


Test patch:
diff --git a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
index 326cfcc1a..42227d1e0 100644
--- a/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
+++ b/src/test/java/org/cyclonedx/parsers/XmlParserTest.java
@@ -94,6 +94,22 @@ public class XmlParserTest {
         Assert.assertEquals("sample-library-variant", pedigree.getVariants().getComponents().get(0).getName());
     }
 
+    @Test
+    public void testValid12BomWithPedigreeWithPatches() throws Exception {
+        final File file = new File(this.getClass().getResource("/bom-1.2-pedigree-example.xml").getFile());
+        final XmlParser parser = new XmlParser();
+        final boolean valid = parser.isValid(file, CycloneDxSchema.Version.VERSION_12);
+        Assert.assertTrue(valid);
+
+        final Bom bom = parser.parse(file);
+        testPedigreeFromExample(bom.getComponents().get(0).getPedigree());
+    }
+
+    private void testPedigreeFromExample(final Pedigree pedigree) {
+        Assert.assertEquals(2, pedigree.getPatches().size());
+        Assert.assertEquals(2, pedigree.getPatches().get(1).getResolves().size());
+    }
+
     @Test
     public void testParsedObjects10Bom() throws Exception {
         final byte[] bomBytes = IOUtils.toByteArray(this.getClass().getResourceAsStream("/bom-1.0.xml"));
diff --git a/src/test/resources/bom-1.2-pedigree-example.xml b/src/test/resources/bom-1.2-pedigree-example.xml
new file mode 100644
index 000000000..0235ae23c
--- /dev/null
+++ b/src/test/resources/bom-1.2-pedigree-example.xml
@@ -0,0 +1,89 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<bom xmlns="http://cyclonedx.org/schema/bom/1.2"
+     serialNumber="urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79"
+     version="1">
+  <components>
+    <component type="library">
+      <group>com.acme</group>
+      <name>sample-library</name>
+      <version>1.0.0</version>
+      <pedigree>
+        <ancestors>
+          <component type="library">
+            <group>org.example</group>
+            <name>sample-library</name>
+            <version>1.0.0</version>
+          </component>
+        </ancestors>
+        <commits>
+          <commit>
+            <uid>7638417db6d59f3c431d3e1f261cc637155684cd</uid>
+            <url>https://location/to/7638417db6d59f3c431d3e1f261cc637155684cd</url>
+            <author>
+              <timestamp>2018-11-07T22:01:45Z</timestamp>
+              <name>John Doe</name>
+              <email>john.doe@example.com</email>
+            </author>
+            <committer>
+              <timestamp>2018-11-07T22:01:45Z</timestamp>
+              <name>Jane Doe</name>
+              <email>jane.doe@example.com</email>
+            </committer>
+            <message>Initial commit</message>
+          </commit>
+        </commits>
+        <patches>
+          <patch type="unofficial">
+            <diff>
+              <text content-type="text/plain" encoding="base64">ZXhhbXBsZSBkaWZmIGhlcmU=</text>
+              <url>uri/to/changes.diff</url>
+            </diff>
+            <resolves>
+              <issue type="enhancement">
+                <id>JIRA-17240</id>
+                <description>Great new feature that does something</description>
+                <source>
+                  <name>Acme Org</name>
+                  <url>https://issues.acme.org/17240</url>
+                </source>
+              </issue>
+            </resolves>
+          </patch>
+          <patch type="backport">
+            <diff>
+              <text content-type="text/plain" encoding="base64">ZXhhbXBsZSBkaWZmIGhlcmU=</text>
+              <url>uri/to/changes.diff</url>
+            </diff>
+            <resolves>
+              <issue type="security">
+                <id>CVE-2019-9997</id>
+                <name>CVE-2019-9997</name>
+                <description>Issue description here</description>
+                <source>
+                  <name>NVD</name>
+                  <url>https://nvd.nist.gov/vuln/detail/CVE-2019-9997</url>
+                </source>
+                <references>
+                  <url>http://some/other/site-1</url>
+                  <url>http://some/other/site-2</url>
+                </references>
+              </issue>
+              <issue type="defect">
+                <id>JIRA-874319</id>
+                <description>Description of fix here</description>
+                <source>
+                  <name>Example Org</name>
+                  <url>https://issues.example.org/874319</url>
+                </source>
+                <references>
+                  <url>http://some/other/site-1</url>
+                  <url>http://some/other/site-2</url>
+                </references>
+              </issue>
+            </resolves>
+          </patch>
+        </patches>
+      </pedigree>
+    </component>
+  </components>
+</bom>