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
Expand Up @@ -26,6 +26,7 @@
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.ossindex.OssIndexClientProvider;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.Reference;
import org.owasp.dependencycheck.dependency.Vulnerability;
import org.owasp.dependencycheck.dependency.VulnerableSoftware;
import org.owasp.dependencycheck.dependency.VulnerableSoftwareBuilder;
Expand All @@ -51,10 +52,12 @@

import javax.annotation.Nullable;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
Expand Down Expand Up @@ -329,7 +332,8 @@ void enrich(final Dependency dependency) {
}

// expose the URL to the package details for report generation
id.setUrl(report.getReference().toString());
Optional.ofNullable(report.getReference())
.ifPresent(ref -> id.setUrl(ref.toString()));

report.getVulnerabilities().stream()
.map((vuln) -> transform(report, vuln))
Expand Down Expand Up @@ -425,11 +429,15 @@ private Vulnerability transform(final ComponentReport report, final ComponentRep
result.setUnscoredSeverity(Double.toString(cvssScore));
}
// generate a reference to the vulnerability details on OSS Index
result.addReference(REFERENCE_TYPE, source.getTitle(), source.getReference().toString());
Optional.ofNullable(source.getReference())
.ifPresent(ref -> result.addReference(REFERENCE_TYPE, source.getTitle(), ref.toString()));

// generate references to other references reported by OSS Index
source.getExternalReferences().forEach(externalReference
-> result.addReference("OSSIndex", externalReference.toString(), externalReference.toString()));
source.getExternalReferences().stream()
.filter(Objects::nonNull)
.map(URI::toString)
.map(ref -> new Reference(REFERENCE_TYPE, ref, ref))
.forEach(result::addReference);

// attach vulnerable software details as best we can
final PackageUrl purl = report.getCoordinates();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package org.owasp.dependencycheck.data.ossindex;

import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.TypeAdapter;
import com.google.gson.internal.bind.TypeAdapters;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.sonatype.goodies.packageurl.PackageUrl;
import org.sonatype.ossindex.service.client.marshal.Marshaller;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.lang.reflect.Type;
import java.net.URI;

/**
* Unmarshaller which will unmarshal nulls for any URIs which are not strictly valid.
*/
class LenientGsonMarshaller implements Marshaller {
private final Gson lenientGson;

public LenientGsonMarshaller() {
lenientGson = new GsonBuilder()
.registerTypeAdapter(PackageUrl.class, new PackageUrlAdapter())
.registerTypeAdapter(URI.class, new LenientUriAdapter())
.create();
}

@Override
public String marshal(final Object value) throws IOException {
try {
return lenientGson.toJson(value);
} catch (JsonParseException e) {
throw new IOException(e);
}
}

@Override
public void marshal(final Object value, final Writer writer) throws IOException {
try {
lenientGson.toJson(value, writer);
writer.flush();
} catch (JsonParseException e) {
throw new IOException(e);
}
}

@Override
public <T> T unmarshal(String value, Class<T> type) throws IOException {
try {
return lenientGson.fromJson(value, type);
} catch (JsonParseException e) {
throw new IOException(e);
}
}

@Override
public <T> T unmarshal(Reader reader, Class<T> type) throws IOException {
try {
return lenientGson.fromJson(reader, type);
} catch (JsonParseException e) {
throw new IOException(e);
}
}

@Override
public <T> T unmarshal(String value, TypeToken<T> type) throws IOException {
try {
return lenientGson.fromJson(value, type.getType());
} catch (JsonParseException e) {
throw new IOException(e);
}
}

@Override
public <T> T unmarshal(Reader reader, TypeToken<T> type) throws IOException {
try {
return lenientGson.fromJson(reader, type.getType());
} catch (JsonParseException e) {
throw new IOException(e);
}
}

private static class PackageUrlAdapter implements JsonDeserializer<PackageUrl>, JsonSerializer<PackageUrl> {
@Override
public PackageUrl deserialize(final JsonElement element, final Type type, final JsonDeserializationContext context) {
return PackageUrl.parse(element.getAsString());
}

@Override
public JsonElement serialize(final PackageUrl value, final Type type, final JsonSerializationContext context) {
return new JsonPrimitive(value.toString());
}
}

private static class LenientUriAdapter extends TypeAdapter<URI> {
@Override
public URI read(JsonReader in) throws IOException {
try {
return TypeAdapters.URI.read(in);
} catch (JsonIOException e) {
return null;
}
}

@Override
public void write(JsonWriter out, URI value) throws IOException {
TypeAdapters.URI.write(out, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import org.sonatype.ossindex.service.client.OssindexClientConfiguration;
import org.sonatype.ossindex.service.client.cache.DirectoryCache;
import org.sonatype.ossindex.service.client.internal.OssindexClientImpl;
import org.sonatype.ossindex.service.client.marshal.GsonMarshaller;
import org.sonatype.ossindex.service.client.marshal.Marshaller;
import org.sonatype.ossindex.service.client.transport.AuthConfiguration;
import org.sonatype.ossindex.service.client.transport.Transport;
Expand Down Expand Up @@ -110,7 +109,7 @@ public static OssindexClient create(final Settings settings) {

final Transport transport = new ODCConnectionTransport(config, userAgent);

final Marshaller marshaller = new GsonMarshaller();
final Marshaller marshaller = new LenientGsonMarshaller();

return newClientFor(config, transport, marshaller);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
package org.owasp.dependencycheck.data.ossindex;

import com.google.common.reflect.TypeToken;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.sonatype.goodies.packageurl.PackageUrl;
import org.sonatype.ossindex.service.api.componentreport.ComponentReport;
import org.sonatype.ossindex.service.api.componentreport.ComponentReportVulnerability;

import java.io.IOException;
import java.io.StringReader;
import java.net.URI;
import java.util.List;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.nullValue;

class LenientGsonMarshallerTest {

/**
* The same type the OSS Index client uses to unmarshal responses.
*/
private static final TypeToken<List<ComponentReport>> LIST_COMPONENT_REPORT = new TypeToken<>() {};

private static final String PURL = "pkg:maven/com.mysql/mysql-connector-j@26.7.0";
private static final String VALID_URI = "https://example.com/component/" + PURL;
private static final String INVALID_URI = "https://example.com/path with spaces/{and}/braces";

private final LenientGsonMarshaller marshaller = new LenientGsonMarshaller();

private static String reportJson(String reportReference, String vulnReference, String externalReferences) {
return "{"
+ "\"coordinates\":\"" + PURL + "\","
+ "\"description\":\"MySQL Connector/J\","
+ "\"reference\":" + reportReference + ","
+ "\"vulnerabilities\":[{"
+ " \"id\":\"CVE-2026-0001\","
+ " \"displayName\":\"CVE-2026-0001\","
+ " \"title\":\"[CVE-2026-0001] Something\","
+ " \"cvssScore\":7.5,"
+ " \"reference\":" + vulnReference + ","
+ " \"externalReferences\":" + externalReferences
+ "}]"
+ "}";
}

private static String quoted(String value) {
return "\"" + value + "\"";
}

private ComponentReport unmarshalSingleReport(String json) throws IOException {
List<ComponentReport> reports = marshaller.unmarshal("[" + json + "]", LIST_COMPONENT_REPORT);
assertThat(reports, hasSize(1));
return reports.get(0);
}

@Nested
class Uris {

@Test
void validUrisAreParsedAsNormal() throws IOException {
String json = reportJson(quoted(VALID_URI), quoted(VALID_URI + "#vuln"), "[" + quoted(VALID_URI + "#ext") + "]");

ComponentReport report = unmarshalSingleReport(json);

assertThat(report.getReference(), is(URI.create(VALID_URI)));
ComponentReportVulnerability vuln = report.getVulnerabilities().get(0);
assertThat(vuln.getReference(), is(URI.create(VALID_URI + "#vuln")));
assertThat(vuln.getExternalReferences(), contains(URI.create(VALID_URI + "#ext")));
}

@Test
void invalidComponentReferenceBecomesNullAndRestOfReportSurvives() throws IOException {
String json = reportJson(quoted(INVALID_URI), quoted(VALID_URI), "[]");

ComponentReport report = unmarshalSingleReport(json);

assertThat(report.getReference(), nullValue());
assertThat(report.getCoordinates(), is(PackageUrl.parse(PURL)));
assertThat(report.getDescription(), is("MySQL Connector/J"));
assertThat(report.getVulnerabilities().get(0).getReference(), is(URI.create(VALID_URI)));
}

@Test
void invalidVulnerabilityReferenceBecomesNullAndRestOfVulnerabilitySurvives() throws IOException {
String json = reportJson(quoted(VALID_URI), quoted(INVALID_URI), "[]");

ComponentReportVulnerability vuln = unmarshalSingleReport(json).getVulnerabilities().get(0);

assertThat(vuln.getReference(), nullValue());
assertThat(vuln.getId(), is("CVE-2026-0001"));
assertThat(vuln.getCvssScore(), is(7.5f));
}

@Test
void invalidExternalReferenceBecomesNullEntryWhileValidOnesAreKept() throws IOException {
String json = reportJson(quoted(VALID_URI), quoted(VALID_URI),
"[" + quoted(VALID_URI + "#one") + "," + quoted(INVALID_URI) + "," + quoted(VALID_URI + "#two") + "]");

ComponentReportVulnerability vuln = unmarshalSingleReport(json).getVulnerabilities().get(0);

assertThat(vuln.getExternalReferences(), contains(URI.create(VALID_URI + "#one"), null, URI.create(VALID_URI + "#two")));
}

@Test
void jsonNullUrisRemainNull() throws IOException {
String json = reportJson("null", "null", "null");

ComponentReport report = unmarshalSingleReport(json);

assertThat(report.getReference(), nullValue());
assertThat(report.getVulnerabilities().get(0).getReference(), nullValue());
// ComponentReportVulnerability exposes an empty list rather than null for missing external references
assertThat(report.getVulnerabilities().get(0).getExternalReferences(), is(empty()));
}

@Test
void leniencyAppliesToEveryUnmarshalOverload() throws IOException {
String json = "{\"coordinates\":\"" + PURL + "\",\"reference\":" + quoted(INVALID_URI) + "}";
String listJson = "[" + json + "]";

assertThat(marshaller.unmarshal(json, ComponentReport.class).getReference(), nullValue());
assertThat(marshaller.unmarshal(new StringReader(json), ComponentReport.class).getReference(), nullValue());
assertThat(marshaller.unmarshal(listJson, LIST_COMPONENT_REPORT).get(0).getReference(), nullValue());
assertThat(marshaller.unmarshal(new StringReader(listJson), LIST_COMPONENT_REPORT).get(0).getReference(), nullValue());
}

@Test
void urisAreMarshalledAsStrings() throws IOException {
ComponentReport report = new ComponentReport();
report.setReference(URI.create(VALID_URI));

String json = marshaller.marshal(report);

assertThat(json, containsString("\"reference\":\"" + VALID_URI + "\""));
assertThat(marshaller.unmarshal(json, ComponentReport.class).getReference(), is(URI.create(VALID_URI)));
}
}

@Test
void packageUrlRoundTripsThroughMarshalAndUnmarshal() throws IOException {
ComponentReport original = new ComponentReport();
original.setCoordinates(PackageUrl.parse(PURL));

ComponentReport copy = marshaller.unmarshal(marshaller.marshal(original), ComponentReport.class);

assertThat(copy.getCoordinates(), is(original.getCoordinates()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
import org.owasp.dependencycheck.utils.Settings;
import org.sonatype.ossindex.service.client.OssindexClientConfiguration;
import org.sonatype.ossindex.service.client.cache.DirectoryCache;
import org.sonatype.ossindex.service.client.marshal.GsonMarshaller;
import org.sonatype.ossindex.service.client.marshal.Marshaller;
import org.sonatype.ossindex.service.client.transport.Transport;

Expand Down Expand Up @@ -58,7 +57,7 @@ void shouldUseDefaultConfiguration() {
ODCConnectionTransport transport = assertTransportConfigured();
assertThat(transport.getUserAgent().get(), matchesPattern("dependency-check/.+ \\(.*\\)"));

assertThat(marshallerCaptor.getValue(), instanceOf(GsonMarshaller.class));
assertThat(marshallerCaptor.getValue(), instanceOf(LenientGsonMarshaller.class));
}
}

Expand Down
Loading