Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.freenow.sauron.plugins;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
import com.freenow.sauron.model.DataSet;
Expand All @@ -21,6 +23,7 @@
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;

@Extension
@Slf4j
Expand Down Expand Up @@ -88,7 +91,24 @@ private List<Component> parseCycloneDxXml(Path bom) throws IOException
private List<Component> parseCycloneDxJson(Path bom) throws IOException
{
ObjectMapper oMapper = new ObjectMapper();
var bomContent = oMapper.readValue(bom.toFile(), Bom.class);
return Optional.ofNullable(bomContent.getComponents()).orElse(Collections.emptyList());
JsonNode bomNode = oMapper.readTree(bom.toFile());

/*
* The npm BOM generator may produce an invalid serialNumber, which can cause validation issues in DependencyTrack.
* https://github.com/DependencyTrack/dependency-track/blob/fa1eb0bb4c1ecf87d231a21e077055acb6b8b59d/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java#L90
* which returns error like this
* {"status":400,"title":"The uploaded BOM is invalid","detail":"Schema validation failed","errors":["$.serialNumber: does not match the regex pattern ^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"]}
* This code replaces the invalid serialNumber with a valid v4 UUID to ensure compatibility.
*/
if (bomNode.has("serialNumber") && bomNode.get("serialNumber").asText().contains("***"))
{
log.debug("Replacing invalid serialNumber in {} for project: {}", bom.getFileName(), bom.getParent());
((ObjectNode) bomNode).put("serialNumber", "urn:uuid:" + UUID.randomUUID());
oMapper.writeValue(bom.toFile(), bomNode);
}

Bom bomObject = oMapper.treeToValue(bomNode, Bom.class);

return Optional.ofNullable(bomObject.getComponents()).orElse(Collections.emptyList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import com.freenow.sauron.model.DataSet;
import com.freenow.sauron.properties.PluginsConfigurationProperties;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import org.cyclonedx.model.Component;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
Expand All @@ -14,6 +18,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
Expand Down Expand Up @@ -220,8 +225,7 @@ public void testDependencyCheckerNodeJs() throws IOException, URISyntaxException
))
);
}



@Test
public void testDependencyCheckerNodeJsYarnNotSupported() throws IOException, URISyntaxException
{
Expand Down Expand Up @@ -440,4 +444,102 @@ private PluginsConfigurationProperties pluginConfigurationProperties()

return properties;
}

@Test
public void testParseCycloneDxJsonWithInvalidSerialNumber() throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException
{
// Given
String invalidBomContent = "{\n" +
" \"bomFormat\": \"CycloneDX\",\n" +
" \"specVersion\": \"1.4\",\n" +
" \"serialNumber\": \"urn:uuid:***\",\n" +
" \"version\": 1,\n" +
" \"components\": [\n" +
" {\n" +
" \"type\": \"library\",\n" +
" \"name\": \"react\",\n" +
" \"version\": \"18.2.0\"\n" +
" }\n" +
" ]\n" +
"}";

Path bomJson = tempFolder.getRoot().toPath().resolve("bom.json");
Files.write(bomJson, invalidBomContent.getBytes(StandardCharsets.UTF_8));

// When
List<Component> components = invokeParseCycloneDxJson(plugin, bomJson);

// Then
assertNotNull(components);
assertEquals(1, components.size());
assertEquals("react", components.get(0).getName());
assertEquals("18.2.0", components.get(0).getVersion());

String sanitizedBomContent = new String(Files.readAllBytes(bomJson), StandardCharsets.UTF_8);
assertFalse("The serialNumber should have been sanitized", sanitizedBomContent.contains("***"));
}

@Test
public void testParseCycloneDxJsonWithValidBom() throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException
{
// Given
String validBomContent = "{\n" +
" \"bomFormat\": \"CycloneDX\",\n" +
" \"specVersion\": \"1.4\",\n" +
" \"serialNumber\": \"urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79\",\n" +
" \"version\": 1,\n" +
" \"components\": [\n" +
" {\n" +
" \"type\": \"library\",\n" +
" \"name\": \"express\",\n" +
" \"version\": \"4.18.2\"\n" +
" }\n" +
" ]\n" +
"}";

Path bomJson = tempFolder.getRoot().toPath().resolve("bom.json");
Files.write(bomJson, validBomContent.getBytes(StandardCharsets.UTF_8));

// When
List<Component> components = invokeParseCycloneDxJson(plugin, bomJson);

// Then
assertNotNull(components);
assertEquals(1, components.size());
assertEquals("express", components.get(0).getName());
assertEquals("4.18.2", components.get(0).getVersion());

String bomContent = new String(Files.readAllBytes(bomJson), StandardCharsets.UTF_8);
assertEquals("The BOM file should not be modified", validBomContent, bomContent);
}

@Test
public void testParseCycloneDxJsonWithNoComponents() throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException
{
// Given
String bomWithNoComponents = "{\n" +
" \"bomFormat\": \"CycloneDX\",\n" +
" \"specVersion\": \"1.4\",\n" +
" \"serialNumber\": \"urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79\",\n" +
" \"version\": 1,\n" +
" \"components\": []\n" +
"}";

Path bomJson = tempFolder.getRoot().toPath().resolve("bom.json");
Files.write(bomJson, bomWithNoComponents.getBytes(StandardCharsets.UTF_8));

// When
List<Component> components = invokeParseCycloneDxJson(plugin, bomJson);

// Then
assertNotNull(components);
assertTrue(components.isEmpty());
}

@SuppressWarnings("unchecked")
private List<Component> invokeParseCycloneDxJson(DependencyChecker plugin, Path bom) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method method = DependencyChecker.class.getDeclaredMethod("parseCycloneDxJson", Path.class);
method.setAccessible(true);
return (List<Component>) method.invoke(plugin, bom);
}
}
2 changes: 1 addition & 1 deletion sauron-service/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ services:
target: /root/.ssh
read_only: true
ports:
- "8080:8080"
- "8080:8080"