Skip to content

Commit c1f497d

Browse files
committed
fix(osi): remap vulnerability refs and preserve merged SBOMs
map component identifiers to bom-ref during scan conversion and reattach cycles so CycloneDX vulnerabilities reference components correctly re-serialize merged SBOMs to the requested format, remap relationship targets, and default merge output to CycloneDX JSON raise JSON read limits and harden OSI dependency preparation/configs for larger SBOMs and richer dependency graphs add unit tests covering vulnerability reference remapping
1 parent 2cff35d commit c1f497d

File tree

11 files changed

+836
-113
lines changed

11 files changed

+836
-113
lines changed
Lines changed: 63 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,75 @@
1+
/**
2+
* Copyright 2021 Rochester Institute of Technology (RIT). Developed with
3+
* government support under contract 70RCSA22C00000008 awarded by the United
4+
* States Department of Homeland Security for Cybersecurity and Infrastructure Security Agency.
5+
* <p>
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
* <p>
13+
* The above copyright notice and this permission notice shall be included in
14+
* all copies or substantial portions of the Software.
15+
* <p>
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
125
package org.svip.api.config;
226

3-
import com.fasterxml.jackson.core.JsonFactory;
4-
import com.fasterxml.jackson.core.StreamWriteConstraints;
27+
import com.fasterxml.jackson.core.StreamReadConstraints;
528
import com.fasterxml.jackson.databind.ObjectMapper;
6-
import com.fasterxml.jackson.databind.SerializationFeature;
7-
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
29+
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
830
import org.springframework.context.annotation.Bean;
931
import org.springframework.context.annotation.Configuration;
32+
import org.springframework.context.annotation.Primary;
1033

34+
/**
35+
* Jackson configuration to handle large SBOM files
36+
*
37+
* This configuration increases the maximum allowed string length for JSON parsing
38+
* to handle very large SBOM files (up to 500MB of JSON text).
39+
*/
1140
@Configuration
1241
public class JacksonConfig {
13-
42+
43+
/**
44+
* Configure Jackson to handle larger JSON strings
45+
* Increases the max string length from 100MB to 500MB
46+
*/
47+
@Bean
48+
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
49+
return builder -> {
50+
builder.postConfigurer(objectMapper -> {
51+
objectMapper.getFactory().setStreamReadConstraints(
52+
StreamReadConstraints.builder()
53+
.maxStringLength(500_000_000) // 500MB limit for JSON strings
54+
.build()
55+
);
56+
});
57+
};
58+
}
59+
60+
/**
61+
* Create a primary ObjectMapper bean with increased limits
62+
* This will be used by default throughout the application
63+
*/
1464
@Bean
65+
@Primary
1566
public ObjectMapper objectMapper() {
16-
// Increase max nesting depth for writing deep JSON structures (e.g., VEX)
17-
JsonFactory factory = JsonFactory.builder()
18-
.streamWriteConstraints(StreamWriteConstraints.builder()
19-
.maxNestingDepth(5_000)
20-
.build())
21-
.build();
22-
23-
ObjectMapper mapper = new ObjectMapper(factory);
24-
25-
// Register Java 8 date/time module for LocalDateTime support
26-
mapper.registerModule(new JavaTimeModule());
27-
28-
// Configure to handle circular references globally
29-
mapper.configure(SerializationFeature.FAIL_ON_SELF_REFERENCES, false);
30-
mapper.configure(SerializationFeature.WRITE_SELF_REFERENCES_AS_NULL, true);
31-
32-
// Disable writing dates as timestamps (use ISO-8601 strings instead)
33-
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
34-
67+
ObjectMapper mapper = new ObjectMapper();
68+
mapper.getFactory().setStreamReadConstraints(
69+
StreamReadConstraints.builder()
70+
.maxStringLength(500_000_000) // 500MB limit for JSON strings
71+
.build()
72+
);
3573
return mapper;
3674
}
37-
}
75+
}

api/src/main/java/org/svip/api/controller/OSIController.java

Lines changed: 60 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
import org.svip.serializers.SerializerFactory;
4545
import org.svip.serializers.exceptions.DeserializerException;
4646
import org.svip.serializers.exceptions.SerializerException;
47+
import org.svip.serializers.serializer.Serializer;
48+
import org.svip.sbom.model.interfaces.generics.SBOM;
49+
import org.svip.sbom.model.objects.SVIPSBOM;
4750

4851
import java.io.IOException;
4952
import java.net.URISyntaxException;
@@ -305,33 +308,57 @@ public ResponseEntity<?> generateWithOSI(@RequestParam("projectName") String pro
305308
mergedID = uploaded.get(0).getId();
306309
}
307310

308-
// Convert
311+
// Re-serialize to requested format (preserves relationships better than conversion)
309312
Long convertedID;
310313
try {
311-
LOGGER.info("POST /svip/generators/osi - Converting SBOM to {} {}", schema, format);
312-
convertedID = sbomService.convert(mergedID, schema, format, true);
313-
LOGGER.info("POST /svip/generators/osi - Successfully converted SBOM to id {}", convertedID);
314-
} catch (DeserializerException | JsonProcessingException | SerializerException | SBOMBuilderException |
315-
ConversionException e) {
316-
// Failed to convert
317-
LOGGER.error("POST /svip/generators/osi - Failed to convert - " + e.getMessage());
314+
SBOMFile mergedSbom = sbomService.getSBOMFile(mergedID);
315+
316+
// Check if already in target schema/format
317+
if (matchesRequestedFormat(mergedSbom, schema, format)) {
318+
LOGGER.info("POST /svip/generators/osi - Merged SBOM already in {} {}, using as-is", schema, format);
319+
convertedID = mergedID;
320+
} else {
321+
// Re-serialize instead of convert to preserve relationship structure
322+
LOGGER.info("POST /svip/generators/osi - Re-serializing merged SBOM to {} {}", schema, format);
323+
SBOM sbomObject = mergedSbom.toSBOMObject();
324+
Serializer serializer = SerializerFactory.createSerializer(schema, format, true);
325+
serializer.setPrettyPrinting(true);
326+
String reserializedContent = serializer.writeToString((SVIPSBOM) sbomObject);
327+
328+
String newName = (sbomObject.getName() != null ? sbomObject.getName() : "merged-sbom") + "-" + schema + "-" + format;
329+
UploadSBOMFileInput input = new UploadSBOMFileInput(newName, reserializedContent);
330+
SBOMFile reserialized = input.toSBOMFile();
331+
332+
// Delete old merged SBOM and save new one
333+
sbomService.deleteSBOMFile(mergedSbom);
334+
sbomService.upload(reserialized);
335+
convertedID = reserialized.getId();
336+
LOGGER.info("POST /svip/generators/osi - Successfully re-serialized to id {}", convertedID);
337+
}
338+
} catch (Exception e) {
339+
// Failed to re-serialize
340+
LOGGER.error("POST /svip/generators/osi - Failed to re-serialize - " + e.getMessage());
318341
return new ResponseEntity<>(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
319342
}
320343

321344
// RE-ADD VULNERABILITIES AFTER MERGE (merge strips them)
322345
if (!allVulnerabilities.isEmpty() && vulnerabilityScanService.isEnabled()) {
323346
try {
324-
LOGGER.info("POST /svip/generators/osi - Re-adding {} vulnerabilities to merged SBOM", allVulnerabilities.size());
325347
SBOMFile mergedSbom = sbomService.getSBOMFile(convertedID);
326348
if (mergedSbom != null) {
327-
JsonNode sbomJson = new ObjectMapper().readTree(mergedSbom.getContent());
328-
349+
ObjectMapper mapper = new ObjectMapper();
350+
JsonNode sbomJson = mapper.readTree(mergedSbom.getContent());
351+
List<JsonNode> remappedVulnerabilities = vulnerabilityScanService.remapVulnerabilityReferences(sbomJson, allVulnerabilities);
352+
allVulnerabilities = new ArrayList<>(remappedVulnerabilities);
353+
354+
LOGGER.info("POST /svip/generators/osi - Re-adding {} vulnerabilities to merged SBOM", allVulnerabilities.size());
355+
329356
@SuppressWarnings("unchecked")
330-
Map<String, Object> sbomMap = new ObjectMapper().convertValue(sbomJson, Map.class);
357+
Map<String, Object> sbomMap = mapper.convertValue(sbomJson, Map.class);
331358
sbomMap.put("vulnerabilities", allVulnerabilities);
332359

333-
String enrichedContent = new ObjectMapper().writerWithDefaultPrettyPrinter()
334-
.writeValueAsString(sbomMap);
360+
String enrichedContent = mapper.writerWithDefaultPrettyPrinter()
361+
.writeValueAsString(sbomMap);
335362

336363
// Update with enriched content
337364
mergedSbom.setContent(enrichedContent);
@@ -382,4 +409,23 @@ public ResponseEntity<?> generateWithOSI(@RequestParam("projectName") String pro
382409
return new ResponseEntity<>(convertedID, HttpStatus.OK);
383410
}
384411

412+
private boolean matchesRequestedFormat(SBOMFile file, SerializerFactory.Schema schema, SerializerFactory.Format format) {
413+
SBOMFile.Schema expectedSchema = switch (schema) {
414+
case CDX14 -> SBOMFile.Schema.CYCLONEDX_14;
415+
case SPDX23 -> SBOMFile.Schema.SPDX_23;
416+
default -> null;
417+
};
418+
419+
SBOMFile.FileType expectedType = switch (format) {
420+
case JSON -> SBOMFile.FileType.JSON;
421+
case XML -> SBOMFile.FileType.XML;
422+
case TAGVALUE -> SBOMFile.FileType.TAG_VALUE;
423+
};
424+
425+
return expectedSchema != null
426+
&& expectedType != null
427+
&& file.getSchema() == expectedSchema
428+
&& file.getFileType() == expectedType;
429+
}
430+
385431
}

api/src/main/java/org/svip/api/services/OSIService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public HashMap<String, String> generateSBOMs(List<String> toolNames) throws IOEx
140140
// Convert osi json string into map. Increase max string length to support large base64 bodies
141141
JsonFactory factory = JsonFactory.builder()
142142
.streamReadConstraints(StreamReadConstraints.builder()
143-
.maxStringLength(100_000_000) // 100MB, higher than default 20MB
143+
.maxStringLength(500_000_000) // 500MB, increased from 100MB to handle very large SBOMs
144144
.build())
145145
.build();
146146
ObjectMapper mapper = new ObjectMapper(factory);

api/src/main/java/org/svip/api/services/SBOMFileService.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -259,17 +259,15 @@ public Long merge(Long[] ids) throws Exception {
259259
throw new Exception("Error merging SBOMs: " + e.getMessage());
260260
}
261261

262-
SerializerFactory.Schema schema = SerializerFactory.Schema.SPDX23;
262+
SerializerFactory.Schema schema = SerializerFactory.Schema.CDX14;
263263

264264
// serialize merged SBOM
265-
Serializer s = SerializerFactory.createSerializer(schema, SerializerFactory.Format.TAGVALUE, // todo default to
266-
// SPDX JSON for
267-
// now?
268-
true);
265+
Serializer s = SerializerFactory.createSerializer(schema, SerializerFactory.Format.JSON, true);
269266
s.setPrettyPrinting(true);
270267
String contents;
271268
try {
272-
contents = s.writeToString((SVIPSBOM) merged);
269+
org.svip.sbom.model.interfaces.generics.SBOM mergedForSchema = Conversion.convert(merged, SerializerFactory.Schema.SVIP, schema);
270+
contents = s.writeToString((SVIPSBOM) mergedForSchema);
273271
} catch (JsonProcessingException | ClassCastException e) {
274272
throw new Exception("Error deserializing merged SBOM: " + e.getMessage());
275273
}

0 commit comments

Comments
 (0)