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

Issue:
`ObjectLocator` doesn't work when BOM doesn't have a `metadata.component` node
`ObjectLocator#locate` does not search *any* objects in a given BOM, if the BOM is missing a `metadata.component` node:

https://github.com/CycloneDX/cyclonedx-core-java/blob/e2f7db69fda60632ce759fbf293b8bdf3754cd91/src/main/java/org/cyclonedx/util/ObjectLocator.java#L64-L90

It should be able to search in `components`, `services`, and `vulnerabilities`, even when `metadata.component` is not set.

Pull request:
Fix `ObjectLocator` not working when BOM doesn't have a `metadata.component` node
Fixes #455

Patch:
diff --git a/src/main/java/org/cyclonedx/util/ObjectLocator.java b/src/main/java/org/cyclonedx/util/ObjectLocator.java
index 4c11c08ea..b2f8ab723 100644
--- a/src/main/java/org/cyclonedx/util/ObjectLocator.java
+++ b/src/main/java/org/cyclonedx/util/ObjectLocator.java
@@ -22,6 +22,7 @@ import org.cyclonedx.model.Bom;
 import org.cyclonedx.model.Component;
 import org.cyclonedx.model.Service;
 import org.cyclonedx.model.vulnerability.Vulnerability;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -66,26 +67,33 @@ public class ObjectLocator {
 
         // TODO is BOM-Link
 
-
         if (this.bom.getMetadata() != null && this.bom.getMetadata().getComponent() != null) {
-            Component c = findComponent(Collections.singletonList(this.bom.getMetadata().getComponent()), this.bomRef);
+            final Component c = findComponent(Collections.singletonList(this.bom.getMetadata().getComponent()), this.bomRef);
             if (c != null) {
                 this.isMetadataComponent = true;
                 this.object = c;
-            }
-            c = findComponent(this.bom.getComponents(), this.bomRef);
-            if (c != null) {
-                this.object = c;
-            }
-            final Service s = findService(this.bom.getServices(), this.bomRef);
-            if (s != null) {
-                this.object = s;
-            }
-            final Vulnerability v = findVulnerability(this.bom.getVulnerabilities(), this.bomRef);
-            if (v != null) {
-                this.object = v;
+                return this;
             }
         }
+
+        final Component c = findComponent(this.bom.getComponents(), this.bomRef);
+        if (c != null) {
+            this.object = c;
+            return this;
+        }
+
+        final Service s = findService(this.bom.getServices(), this.bomRef);
+        if (s != null) {
+            this.object = s;
+            return this;
+        }
+
+        final Vulnerability v = findVulnerability(this.bom.getVulnerabilities(), this.bomRef);
+        if (v != null) {
+            this.object = v;
+            return this;
+        }
+
         return this;
     }
 


Test patch:
diff --git a/src/test/java/org/cyclonedx/util/ObjectLocatorTest.java b/src/test/java/org/cyclonedx/util/ObjectLocatorTest.java
new file mode 100644
index 000000000..2a58850f3
--- /dev/null
+++ b/src/test/java/org/cyclonedx/util/ObjectLocatorTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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;
+
+import org.cyclonedx.model.Bom;
+import org.cyclonedx.model.Component;
+import org.cyclonedx.model.Service;
+import org.cyclonedx.model.vulnerability.Vulnerability;
+import org.cyclonedx.parsers.BomParserFactory;
+import org.cyclonedx.parsers.Parser;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.apache.commons.io.IOUtils.resourceToByteArray;
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ObjectLocatorTest {
+
+    private Bom bom;
+
+    @BeforeEach
+    void beforeEach() throws Exception {
+        final byte[] bomBytes = resourceToByteArray("/bom-object-locator.json");
+        final Parser parser = BomParserFactory.createParser(bomBytes);
+        bom = parser.parse(bomBytes);
+    }
+
+    @Test
+    void shouldLocateMetadataComponent() {
+        final ObjectLocator objectLocator = new ObjectLocator(bom, "0a7ac0b2-0f52-45c6-8f73-4dbcfb25286b").locate();
+        assertThat(objectLocator.found()).isTrue();
+        assertThat(objectLocator.isMetadataComponent()).isTrue();
+        assertThat(objectLocator.isComponent()).isTrue();
+        assertThat(objectLocator.isService()).isFalse();
+        assertThat(objectLocator.isVulnerability()).isFalse();
+        assertThat(objectLocator.getObject()).isInstanceOf(Component.class);
+    }
+
+    @Test
+    void shouldLocateComponent() {
+        final ObjectLocator objectLocator = new ObjectLocator(bom, "ac9c4a17-2bc2-42ef-81af-01a8e363501f").locate();
+        assertThat(objectLocator.found()).isTrue();
+        assertThat(objectLocator.isMetadataComponent()).isFalse();
+        assertThat(objectLocator.isComponent()).isTrue();
+        assertThat(objectLocator.isService()).isFalse();
+        assertThat(objectLocator.isVulnerability()).isFalse();
+        assertThat(objectLocator.getObject()).isInstanceOf(Component.class);
+    }
+
+    @Test // https://github.com/CycloneDX/cyclonedx-core-java/issues/455
+    void shouldLocateComponentWhenMetadataComponentIsNull() {
+        bom.getMetadata().setComponent(null);
+
+        final ObjectLocator objectLocator = new ObjectLocator(bom, "ac9c4a17-2bc2-42ef-81af-01a8e363501f").locate();
+        assertThat(objectLocator.found()).isTrue();
+        assertThat(objectLocator.isMetadataComponent()).isFalse();
+        assertThat(objectLocator.isComponent()).isTrue();
+        assertThat(objectLocator.isService()).isFalse();
+        assertThat(objectLocator.isVulnerability()).isFalse();
+        assertThat(objectLocator.getObject()).isInstanceOf(Component.class);
+    }
+
+    @Test
+    void shouldLocateService() {
+        final ObjectLocator objectLocator = new ObjectLocator(bom, "b2a46a4b-8367-4bae-9820-95557cfe03a8").locate();
+        assertThat(objectLocator.found()).isTrue();
+        assertThat(objectLocator.isMetadataComponent()).isFalse();
+        assertThat(objectLocator.isComponent()).isFalse();
+        assertThat(objectLocator.isService()).isTrue();
+        assertThat(objectLocator.isVulnerability()).isFalse();
+        assertThat(objectLocator.getObject()).isInstanceOf(Service.class);
+    }
+
+    @Test // https://github.com/CycloneDX/cyclonedx-core-java/issues/455
+    void shouldLocateServiceWhenMetadataComponentIsNull() {
+        bom.getMetadata().setComponent(null);
+
+        final ObjectLocator objectLocator = new ObjectLocator(bom, "b2a46a4b-8367-4bae-9820-95557cfe03a8").locate();
+        assertThat(objectLocator.found()).isTrue();
+        assertThat(objectLocator.isMetadataComponent()).isFalse();
+        assertThat(objectLocator.isComponent()).isFalse();
+        assertThat(objectLocator.isService()).isTrue();
+        assertThat(objectLocator.isVulnerability()).isFalse();
+        assertThat(objectLocator.getObject()).isInstanceOf(Service.class);
+    }
+
+    @Test
+    void shouldLocateVulnerability() {
+        final ObjectLocator objectLocator = new ObjectLocator(bom, "6eee14da-8f42-4cc4-bb65-203235f02415").locate();
+        assertThat(objectLocator.found()).isTrue();
+        assertThat(objectLocator.isMetadataComponent()).isFalse();
+        assertThat(objectLocator.isComponent()).isFalse();
+        assertThat(objectLocator.isService()).isFalse();
+        assertThat(objectLocator.isVulnerability()).isTrue();
+        assertThat(objectLocator.getObject()).isInstanceOf(Vulnerability.class);
+    }
+
+    @Test // https://github.com/CycloneDX/cyclonedx-core-java/issues/455
+    void shouldLocateVulnerabilityWhenMetadataComponentIsNull() {
+        bom.getMetadata().setComponent(null);
+
+        final ObjectLocator objectLocator = new ObjectLocator(bom, "6eee14da-8f42-4cc4-bb65-203235f02415").locate();
+        assertThat(objectLocator.found()).isTrue();
+        assertThat(objectLocator.isMetadataComponent()).isFalse();
+        assertThat(objectLocator.isComponent()).isFalse();
+        assertThat(objectLocator.isService()).isFalse();
+        assertThat(objectLocator.isVulnerability()).isTrue();
+        assertThat(objectLocator.getObject()).isInstanceOf(Vulnerability.class);
+    }
+
+    @Test
+    void shouldNotFailWhenUnableToLocate() {
+        final ObjectLocator objectLocator = new ObjectLocator(bom, "doesNotExist").locate();
+        assertThat(objectLocator.found()).isFalse();
+        assertThat(objectLocator.isMetadataComponent()).isFalse();
+        assertThat(objectLocator.isComponent()).isFalse();
+        assertThat(objectLocator.isService()).isFalse();
+        assertThat(objectLocator.isVulnerability()).isFalse();
+        assertThat(objectLocator.getObject()).isNull();
+    }
+
+}
\ No newline at end of file
diff --git a/src/test/resources/bom-object-locator.json b/src/test/resources/bom-object-locator.json
new file mode 100644
index 000000000..2620601c4
--- /dev/null
+++ b/src/test/resources/bom-object-locator.json
@@ -0,0 +1,38 @@
+{
+  "bomFormat": "CycloneDX",
+  "specVersion": "1.6",
+  "serialNumber": "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79",
+  "version": 1,
+  "metadata": {
+    "component": {
+      "bom-ref": "0a7ac0b2-0f52-45c6-8f73-4dbcfb25286b",
+      "type": "application",
+      "name": "Acme Application",
+      "version": "9.1.1"
+    }
+  },
+  "components": [
+    {
+      "bom-ref": "ac9c4a17-2bc2-42ef-81af-01a8e363501f",
+      "type": "library",
+      "name": "tomcat-catalina",
+      "version": "9.0.14"
+    }
+  ],
+  "services": [
+    {
+      "bom-ref": "b2a46a4b-8367-4bae-9820-95557cfe03a8",
+      "name": "Stock ticker service"
+    }
+  ],
+  "vulnerabilities": [
+    {
+      "bom-ref": "6eee14da-8f42-4cc4-bb65-203235f02415",
+      "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-32111",
+      "source": {
+        "name": "Snyk",
+        "url": "https://snyk.io/vuln/SNYK-JAVA-COMFASTERXMLJACKSONCORE-32111"
+      }
+    }
+  ]
+}