Skip to content

Commit f1987ee

Browse files
authored
sbom serialNumber validation handled (#195)
1 parent a474866 commit f1987ee

File tree

3 files changed

+127
-5
lines changed

3 files changed

+127
-5
lines changed

plugins/dependency-checker/src/main/java/com/freenow/sauron/plugins/DependencyChecker.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.freenow.sauron.plugins;
22

33
import com.fasterxml.jackson.databind.DeserializationFeature;
4+
import com.fasterxml.jackson.databind.JsonNode;
45
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.node.ObjectNode;
57
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
68
import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
79
import com.freenow.sauron.model.DataSet;
@@ -21,6 +23,7 @@
2123
import java.util.Collections;
2224
import java.util.List;
2325
import java.util.Optional;
26+
import java.util.UUID;
2427

2528
@Extension
2629
@Slf4j
@@ -88,7 +91,24 @@ private List<Component> parseCycloneDxXml(Path bom) throws IOException
8891
private List<Component> parseCycloneDxJson(Path bom) throws IOException
8992
{
9093
ObjectMapper oMapper = new ObjectMapper();
91-
var bomContent = oMapper.readValue(bom.toFile(), Bom.class);
92-
return Optional.ofNullable(bomContent.getComponents()).orElse(Collections.emptyList());
94+
JsonNode bomNode = oMapper.readTree(bom.toFile());
95+
96+
/*
97+
* The npm BOM generator may produce an invalid serialNumber, which can cause validation issues in DependencyTrack.
98+
* https://github.com/DependencyTrack/dependency-track/blob/fa1eb0bb4c1ecf87d231a21e077055acb6b8b59d/src/main/java/org/dependencytrack/parser/cyclonedx/CycloneDxValidator.java#L90
99+
* which returns error like this
100+
* {"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}$"]}
101+
* This code replaces the invalid serialNumber with a valid v4 UUID to ensure compatibility.
102+
*/
103+
if (bomNode.has("serialNumber") && bomNode.get("serialNumber").asText().contains("***"))
104+
{
105+
log.debug("Replacing invalid serialNumber in {} for project: {}", bom.getFileName(), bom.getParent());
106+
((ObjectNode) bomNode).put("serialNumber", "urn:uuid:" + UUID.randomUUID());
107+
oMapper.writeValue(bom.toFile(), bomNode);
108+
}
109+
110+
Bom bomObject = oMapper.treeToValue(bomNode, Bom.class);
111+
112+
return Optional.ofNullable(bomObject.getComponents()).orElse(Collections.emptyList());
93113
}
94114
}

plugins/dependency-checker/src/test/java/com/freenow/sauron/plugins/DependencyCheckerTest.java

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
import com.freenow.sauron.model.DataSet;
44
import com.freenow.sauron.properties.PluginsConfigurationProperties;
55
import com.github.tomakehurst.wiremock.junit.WireMockRule;
6+
import java.lang.reflect.InvocationTargetException;
7+
import java.lang.reflect.Method;
8+
import java.nio.charset.StandardCharsets;
9+
import org.cyclonedx.model.Component;
610
import org.junit.Before;
711
import org.junit.Rule;
812
import org.junit.Test;
@@ -14,6 +18,7 @@
1418
import java.nio.file.Path;
1519
import java.nio.file.Paths;
1620
import java.util.HashMap;
21+
import java.util.List;
1722
import java.util.Map;
1823
import java.util.Objects;
1924
import java.util.Optional;
@@ -220,8 +225,7 @@ public void testDependencyCheckerNodeJs() throws IOException, URISyntaxException
220225
))
221226
);
222227
}
223-
224-
228+
225229
@Test
226230
public void testDependencyCheckerNodeJsYarnNotSupported() throws IOException, URISyntaxException
227231
{
@@ -440,4 +444,102 @@ private PluginsConfigurationProperties pluginConfigurationProperties()
440444

441445
return properties;
442446
}
447+
448+
@Test
449+
public void testParseCycloneDxJsonWithInvalidSerialNumber() throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException
450+
{
451+
// Given
452+
String invalidBomContent = "{\n" +
453+
" \"bomFormat\": \"CycloneDX\",\n" +
454+
" \"specVersion\": \"1.4\",\n" +
455+
" \"serialNumber\": \"urn:uuid:***\",\n" +
456+
" \"version\": 1,\n" +
457+
" \"components\": [\n" +
458+
" {\n" +
459+
" \"type\": \"library\",\n" +
460+
" \"name\": \"react\",\n" +
461+
" \"version\": \"18.2.0\"\n" +
462+
" }\n" +
463+
" ]\n" +
464+
"}";
465+
466+
Path bomJson = tempFolder.getRoot().toPath().resolve("bom.json");
467+
Files.write(bomJson, invalidBomContent.getBytes(StandardCharsets.UTF_8));
468+
469+
// When
470+
List<Component> components = invokeParseCycloneDxJson(plugin, bomJson);
471+
472+
// Then
473+
assertNotNull(components);
474+
assertEquals(1, components.size());
475+
assertEquals("react", components.get(0).getName());
476+
assertEquals("18.2.0", components.get(0).getVersion());
477+
478+
String sanitizedBomContent = new String(Files.readAllBytes(bomJson), StandardCharsets.UTF_8);
479+
assertFalse("The serialNumber should have been sanitized", sanitizedBomContent.contains("***"));
480+
}
481+
482+
@Test
483+
public void testParseCycloneDxJsonWithValidBom() throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException
484+
{
485+
// Given
486+
String validBomContent = "{\n" +
487+
" \"bomFormat\": \"CycloneDX\",\n" +
488+
" \"specVersion\": \"1.4\",\n" +
489+
" \"serialNumber\": \"urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79\",\n" +
490+
" \"version\": 1,\n" +
491+
" \"components\": [\n" +
492+
" {\n" +
493+
" \"type\": \"library\",\n" +
494+
" \"name\": \"express\",\n" +
495+
" \"version\": \"4.18.2\"\n" +
496+
" }\n" +
497+
" ]\n" +
498+
"}";
499+
500+
Path bomJson = tempFolder.getRoot().toPath().resolve("bom.json");
501+
Files.write(bomJson, validBomContent.getBytes(StandardCharsets.UTF_8));
502+
503+
// When
504+
List<Component> components = invokeParseCycloneDxJson(plugin, bomJson);
505+
506+
// Then
507+
assertNotNull(components);
508+
assertEquals(1, components.size());
509+
assertEquals("express", components.get(0).getName());
510+
assertEquals("4.18.2", components.get(0).getVersion());
511+
512+
String bomContent = new String(Files.readAllBytes(bomJson), StandardCharsets.UTF_8);
513+
assertEquals("The BOM file should not be modified", validBomContent, bomContent);
514+
}
515+
516+
@Test
517+
public void testParseCycloneDxJsonWithNoComponents() throws IOException, NoSuchMethodException, InvocationTargetException, IllegalAccessException
518+
{
519+
// Given
520+
String bomWithNoComponents = "{\n" +
521+
" \"bomFormat\": \"CycloneDX\",\n" +
522+
" \"specVersion\": \"1.4\",\n" +
523+
" \"serialNumber\": \"urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79\",\n" +
524+
" \"version\": 1,\n" +
525+
" \"components\": []\n" +
526+
"}";
527+
528+
Path bomJson = tempFolder.getRoot().toPath().resolve("bom.json");
529+
Files.write(bomJson, bomWithNoComponents.getBytes(StandardCharsets.UTF_8));
530+
531+
// When
532+
List<Component> components = invokeParseCycloneDxJson(plugin, bomJson);
533+
534+
// Then
535+
assertNotNull(components);
536+
assertTrue(components.isEmpty());
537+
}
538+
539+
@SuppressWarnings("unchecked")
540+
private List<Component> invokeParseCycloneDxJson(DependencyChecker plugin, Path bom) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
541+
Method method = DependencyChecker.class.getDeclaredMethod("parseCycloneDxJson", Path.class);
542+
method.setAccessible(true);
543+
return (List<Component>) method.invoke(plugin, bom);
544+
}
443545
}

sauron-service/docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,4 @@ services:
7070
target: /root/.ssh
7171
read_only: true
7272
ports:
73-
- "8080:8080"
73+
- "8080:8080"

0 commit comments

Comments
 (0)