diff --git a/pom.xml b/pom.xml index 413af479c3..cee76b4cf2 100644 --- a/pom.xml +++ b/pom.xml @@ -92,11 +92,11 @@ 8.11.2 1.4.1 3.2.0 - 3.14.1 - 1.0.1 2.0.1 6.5.0 1.1.1 + 2.1.1 + 4.5.14 2.0.6 11.2.3.jre17 @@ -227,31 +227,33 @@ pebble ${lib.pebble.version} - + - us.springett - vulndb-data-mirror - ${lib.vulndb-data-mirror.version} - - - - io.github.openunirest - open-unirest-java - - + org.apache.httpcomponents + httpclient + ${lib.httpclient.version} - + - com.konghq - unirest-java - ${lib.unirest.version} + oauth.signpost + signpost-core + ${lib.signpost-core.version} + compile + + + org.apache.httpcomponents + httpmime + ${lib.httpclient.version} + + + com.fasterxml.woodstox woodstox-core ${lib.woodstox.version} - + org.apache.maven maven-artifact @@ -347,7 +349,16 @@ mockito-core ${lib.mockito.version} test + + + com.github.tomakehurst + wiremock-jre8 + 2.35.0 + test + + + com.github.stefanbirkner system-rules diff --git a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java index 720aaad9e0..aa11b90e4b 100644 --- a/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java +++ b/src/main/java/org/dependencytrack/integrations/defectdojo/DefectDojoClient.java @@ -19,15 +19,25 @@ package org.dependencytrack.integrations.defectdojo; import alpine.common.logging.Logger; -import kong.unirest.UnirestInstance; -import kong.unirest.HttpRequestWithBody; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.InputStreamBody; +import org.apache.http.entity.mime.content.StringBody; +import org.apache.http.util.EntityUtils; +import org.dependencytrack.common.HttpClientPool; +import org.json.JSONArray; +import org.json.JSONObject; +import java.io.IOException; import java.io.InputStream; +import java.net.URISyntaxException; import java.net.URL; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -40,86 +50,98 @@ public class DefectDojoClient { private final DefectDojoUploader uploader; private final URL baseURL; - public DefectDojoClient(final DefectDojoUploader uploader, final URL baseURL) { + public DefectDojoClient(final DefectDojoUploader uploader, final URL baseURL) { this.uploader = uploader; this.baseURL = baseURL; } public void uploadDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson) { LOGGER.debug("Uploading Dependency-Track findings to DefectDojo"); - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - final HttpRequestWithBody request = ui.post(baseURL + "/api/v2/import-scan/"); + HttpPost request = new HttpPost(baseURL + "/api/v2/import-scan/"); + InputStreamBody inputStreamBody = new InputStreamBody(findingsJson, ContentType.APPLICATION_OCTET_STREAM, "findings.json"); + request.addHeader("accept", "application/json"); + request.addHeader("Authorization", "Token " + token); + HttpEntity data = MultipartEntityBuilder.create().setMode(HttpMultipartMode.BROWSER_COMPATIBLE) + .addPart("file", inputStreamBody) + .addPart("engagement", new StringBody(engagementId, ContentType.MULTIPART_FORM_DATA)) + .addPart("scan_type", new StringBody("Dependency Track Finding Packaging Format (FPF) Export", ContentType.MULTIPART_FORM_DATA)) + .addPart("verified", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) + .addPart("active", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) + .addPart("minimum_severity", new StringBody("Info", ContentType.MULTIPART_FORM_DATA)) + .addPart("close_old_findings", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) + .addPart("push_to_jira", new StringBody("push_to_jira", ContentType.MULTIPART_FORM_DATA)) + .addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA)) + .build(); + request.setEntity(data); - final HttpResponse response = request - .header("accept", "application/json") - .header("Authorization", "Token " + token) - .field("file", findingsJson, "findings.json") - .field("engagement", engagementId) - .field("scan_type", "Dependency Track Finding Packaging Format (FPF) Export") - .field("verified", "true") - .field("active", "true") - .field("minimum_severity", "Info") - .field("close_old_findings", "true") - .field("push_to_jira", "false") - .field("scan_date", DATE_FORMAT.format(new Date())) - .asString(); - if (response.getStatus() == 201) { - LOGGER.debug("Successfully uploaded findings to DefectDojo"); - } else { - LOGGER.warn("DefectDojo Client did not receive expected response while attempting to upload " - + "Dependency-Track findings. HTTP response code: " - + response.getStatus() + " - " + response.getStatusText()); - uploader.handleUnexpectedHttpResponse(LOGGER, request.getUrl(), response.getStatus(), response.getStatusText()); + + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) { + LOGGER.debug("Successfully uploaded findings to DefectDojo"); + } else { + uploader.handleUnexpectedHttpResponse(LOGGER, request.getURI().toString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); + } + } catch (IOException ex) { + uploader.handleException(LOGGER, ex); } } // Pulling DefectDojo 'tests' API endpoint with engagementID filter on, and retrieve a list of existing tests public ArrayList getDojoTestIds(final String token, final String eid) { LOGGER.debug("Pulling DefectDojo Tests API ..."); - String tests_uri = "/api/v2/tests/"; - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); + String testsUri = "/api/v2/tests/"; LOGGER.debug("Make the first pagination call"); - HttpResponse response = ui.get(baseURL + tests_uri + "?limit=100&engagement=" + eid) - .header("accept", "application/json") - .header("Authorization", "Token " + token) - .asJson(); - if (response.getStatus() == 200) { - if (response.getBody() != null && response.getBody().getObject() != null) { - JsonNode root = response.getBody(); - JSONObject dojoObj = root.getObject(); - JSONArray dojoArray = dojoObj.getJSONArray("results"); - ArrayList dojoTests = jsonToList(dojoArray); - String nextUrl = ""; - while (dojoObj.get("next") != null ) { - nextUrl = dojoObj.get("next").toString(); - LOGGER.debug("Making the subsequent pagination call on " + nextUrl); - response = ui.get(nextUrl) - .header("accept", "application/json") - .header("Authorization", "Token " + token) - .asJson(); - nextUrl = dojoObj.get("next").toString(); - root = response.getBody(); - dojoObj = root.getObject(); - dojoArray = dojoObj.getJSONArray("results"); - dojoTests.addAll(jsonToList(dojoArray)); + try { + URIBuilder uriBuilder = new URIBuilder(baseURL + testsUri); + uriBuilder.addParameter("limit", "100"); + uriBuilder.addParameter("engagement", eid); + HttpGet request = new HttpGet(uriBuilder.build().toString()); + request.addHeader("accept", "application/json"); + request.addHeader("Authorization", "Token " + token); + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if (response.getEntity() != null) { + String stringResponse = EntityUtils.toString(response.getEntity()); + JSONObject dojoObj = new JSONObject(stringResponse); + JSONArray dojoArray = dojoObj.getJSONArray("results"); + ArrayList dojoTests = jsonToList(dojoArray); + String nextUrl = ""; + while (dojoObj.get("next") != null) { + nextUrl = dojoObj.get("next").toString(); + LOGGER.debug("Making the subsequent pagination call on " + nextUrl); + uriBuilder = new URIBuilder(nextUrl); + request = new HttpGet(uriBuilder.build().toString()); + request.addHeader("accept", "application/json"); + request.addHeader("Authorization", "Token " + token); + try (CloseableHttpResponse response1 = HttpClientPool.getClient().execute(request)) { + nextUrl = dojoObj.get("next").toString(); + stringResponse = EntityUtils.toString(response1.getEntity()); + } + dojoObj = new JSONObject(stringResponse); + dojoArray = dojoObj.getJSONArray("results"); + dojoTests.addAll(jsonToList(dojoArray)); + } + LOGGER.debug("Successfully retrieved the test list "); + return dojoTests; + } + } else { + LOGGER.warn("DefectDojo Client did not receive expected response while attempting to retrieve tests list " + + response.getStatusLine().getStatusCode() + " - " + response.getStatusLine().getReasonPhrase()); } - LOGGER.debug("Successfully retrieved the test list "); - return dojoTests; } - } else { - LOGGER.warn("DefectDojo Client did not receive expected response while attempting to retrieve tests list " - + response.getStatus() + " - " + response.getBody()); + } catch (IOException | URISyntaxException ex) { + uploader.handleException(LOGGER, ex); } - return null; + return new ArrayList<>(); } // Given the engagement id and scan type, search for existing test id - public String getDojoTestId(final String engagementID, final ArrayList dojoTests) { + public String getDojoTestId(final String engagementID, final ArrayList dojoTests) { for (int i = 0; i < dojoTests.size(); i++) { String s = dojoTests.get(i).toString(); JSONObject dojoTest = new JSONObject(s); - if (dojoTest.get("engagement").toString().equals(engagementID) && - dojoTest.get("scan_type").toString().equals("Dependency Track Finding Packaging Format (FPF) Export")) { + if (dojoTest.get("engagement").toString().equals(engagementID) && + dojoTest.get("scan_type").toString().equals("Dependency Track Finding Packaging Format (FPF) Export")) { return dojoTest.get("id").toString(); } } @@ -140,32 +162,34 @@ public ArrayList jsonToList(final JSONArray jsonArray) { /* * A Reimport will reuse (overwrite) the existing test, instead of create a new test. * The Successfully reimport will also increase the reimport counter by 1. - */ + */ public void reimportDependencyTrackFindings(final String token, final String engagementId, final InputStream findingsJson, final String testId) { LOGGER.debug("Re-reimport Dependency-Track findings to DefectDojo per Engagement"); - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - final HttpRequestWithBody request = ui.post(baseURL + "/api/v2/reimport-scan/"); - final HttpResponse response = request - .header("accept", "application/json") - .header("Authorization", "Token " + token) - .field("file", findingsJson, "findings.json") - .field("engagement", engagementId) - .field("scan_type", "Dependency Track Finding Packaging Format (FPF) Export") - .field("verified", "true") - .field("active", "true") - .field("minimum_severity", "Info") - .field("close_old_findings", "true") - .field("push_to_jira", "false") - .field("test", testId) - .field("scan_date", DATE_FORMAT.format(new Date())) - .asString(); - if (response.getStatus() == 201) { - LOGGER.debug("Successfully reimport findings to DefectDojo"); - } else { - LOGGER.warn("DefectDojo Client did not receive expected response while attempting to reimport" - + "Dependency-Track findings. HTTP response code: " - + response.getStatus() + " - " + response.getStatusText()); - uploader.handleUnexpectedHttpResponse(LOGGER, request.getUrl(), response.getStatus(), response.getBody()); + HttpPost request = new HttpPost(baseURL + "/api/v2/reimport-scan/"); + request.addHeader("accept", "application/json"); + request.addHeader("Authorization", "Token " + token); + InputStreamBody inputStreamBody = new InputStreamBody(findingsJson, ContentType.APPLICATION_OCTET_STREAM, "findings.json"); + HttpEntity fileData = MultipartEntityBuilder.create().setMode(HttpMultipartMode.BROWSER_COMPATIBLE) + .addPart("file", inputStreamBody) + .addPart("engagement", new StringBody(engagementId, ContentType.MULTIPART_FORM_DATA)) + .addPart("scan_type", new StringBody("Dependency Track Finding Packaging Format (FPF) Export", ContentType.MULTIPART_FORM_DATA)) + .addPart("verified", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) + .addPart("active", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) + .addPart("minimum_severity", new StringBody("Info", ContentType.MULTIPART_FORM_DATA)) + .addPart("close_old_findings", new StringBody("true", ContentType.MULTIPART_FORM_DATA)) + .addPart("push_to_jira", new StringBody("push_to_jira", ContentType.MULTIPART_FORM_DATA)) + .addPart("test", new StringBody(testId, ContentType.MULTIPART_FORM_DATA)) + .addPart("scan_date", new StringBody(DATE_FORMAT.format(new Date()), ContentType.MULTIPART_FORM_DATA)) + .build(); + request.setEntity(fileData); + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) { + LOGGER.debug("Successfully reimport findings to DefectDojo"); + } else { + uploader.handleUnexpectedHttpResponse(LOGGER, request.getURI().toString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); + } + } catch (IOException ex) { + uploader.handleException(LOGGER, ex); } } diff --git a/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscClient.java b/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscClient.java index 035763e3ce..939e8f176e 100644 --- a/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscClient.java +++ b/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscClient.java @@ -19,18 +19,25 @@ package org.dependencytrack.integrations.fortifyssc; import alpine.common.logging.Logger; -import kong.unirest.HttpRequestWithBody; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONObject; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.util.EntityUtils; +import org.dependencytrack.common.HttpClientPool; +import org.json.JSONObject; +import java.io.IOException; import java.io.InputStream; +import java.net.URISyntaxException; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Base64; -import java.util.HashMap; public class FortifySscClient { @@ -45,49 +52,50 @@ public FortifySscClient(final FortifySscUploader uploader, final URL baseURL) { public String generateOneTimeUploadToken(final String citoken) { LOGGER.debug("Generating one-time upload token"); - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); + var request = new HttpPost(baseURL + "/api/v1/fileTokens"); final JSONObject payload = new JSONObject().put("fileTokenType", "UPLOAD"); - final HttpRequestWithBody request = ui.post(baseURL + "/api/v1/fileTokens"); - final HttpResponse response = request - .header("Content-Type", "application/json") - .header("Authorization", "FortifyToken " + Base64.getEncoder().encodeToString(citoken.getBytes(StandardCharsets.UTF_8))) - .body(payload) - .asJson(); - if (response.getStatus() == 201) { - if (response.getBody() != null) { - final JSONObject root = response.getBody().getObject(); - LOGGER.debug("One-time upload token retrieved"); - return root.getJSONObject("data").getString("token"); + request.addHeader("Content-Type", "application/json"); + request.addHeader("Authorization", "FortifyToken " + Base64.getEncoder().encodeToString(citoken.getBytes(StandardCharsets.UTF_8))); + try { + request.setEntity(new StringEntity(payload.toString())); + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_CREATED) { + if (response.getEntity() != null) { + String responseString = EntityUtils.toString(response.getEntity()); + final JSONObject root = new JSONObject(responseString); + LOGGER.debug("One-time upload token retrieved"); + return root.getJSONObject("data").getString("token"); + } + } else { + uploader.handleUnexpectedHttpResponse(LOGGER, request.getURI().toString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); + } } - } else { - LOGGER.warn("Fortify SSC Client did not receive expected response while attempting to generate a " - + "one-time-use fileupload token. HTTP response code: " - + response.getStatus() + " - " + response.getStatusText()); - uploader.handleUnexpectedHttpResponse(LOGGER, request.getUrl(), response.getStatus(), response.getStatusText()); + } catch (IOException ex) { + uploader.handleException(LOGGER, ex); } return null; } public void uploadDependencyTrackFindings(final String token, final String applicationVersion, final InputStream findingsJson) { - LOGGER.debug("Uploading Dependency-Track findings to Fortify SSC"); - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - final HashMap params = new HashMap<>(); - params.put("engineType", "DEPENDENCY_TRACK"); - params.put("mat", token); - params.put("entityId", applicationVersion); - final HttpRequestWithBody request = ui.post(baseURL + "/upload/resultFileUpload.html"); - final HttpResponse response = request - .header("accept", "application/xml") - .queryString(params) - .field("files[]", findingsJson, "findings.json") - .asString(); - if (response.getStatus() == 200) { - LOGGER.debug("Successfully uploaded findings to Fortify SSC"); - } else { - LOGGER.warn("Fortify SSC Client did not receive expected response while attempting to upload " - + "Dependency-Track findings. HTTP response code: " - + response.getStatus() + " - " + response.getStatusText()); - uploader.handleUnexpectedHttpResponse(LOGGER, request.getUrl(), response.getStatus(), response.getStatusText()); + try { + LOGGER.debug("Uploading Dependency-Track findings to Fortify SSC"); + var builder = new URIBuilder(baseURL + "/upload/resultFileUpload.html"); + builder.setParameter("engineType", "DEPENDENCY_TRACK").setParameter("mat", token).setParameter("entityId", applicationVersion); + HttpPost request = new HttpPost(builder.build()); + request.addHeader("accept", "application/xml"); + HttpEntity data = MultipartEntityBuilder.create().setMode(HttpMultipartMode.BROWSER_COMPATIBLE) + .addBinaryBody("files[]", findingsJson, ContentType.APPLICATION_OCTET_STREAM, "findings.json") + .build(); + request.setEntity(data); + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + LOGGER.debug("Successfully uploaded findings to Fortify SSC"); + } else { + uploader.handleUnexpectedHttpResponse(LOGGER, request.getURI().toString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); + } + } + } catch (URISyntaxException | IOException ex) { + uploader.handleException(LOGGER, ex); } } -} +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscUploader.java b/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscUploader.java index 366a5ee3de..ea5f8c33bc 100644 --- a/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscUploader.java +++ b/src/main/java/org/dependencytrack/integrations/fortifyssc/FortifySscUploader.java @@ -35,8 +35,9 @@ import java.util.List; import static org.dependencytrack.model.ConfigPropertyConstants.FORTIFY_SSC_ENABLED; -import static org.dependencytrack.model.ConfigPropertyConstants.FORTIFY_SSC_TOKEN; + import static org.dependencytrack.model.ConfigPropertyConstants.FORTIFY_SSC_URL; +import static org.dependencytrack.model.ConfigPropertyConstants.FORTIFY_SSC_TOKEN; public class FortifySscUploader extends AbstractIntegrationPoint implements ProjectFindingUploader { diff --git a/src/main/java/org/dependencytrack/integrations/kenna/KennaSecurityUploader.java b/src/main/java/org/dependencytrack/integrations/kenna/KennaSecurityUploader.java index 966423b8b6..4e164f1ba6 100644 --- a/src/main/java/org/dependencytrack/integrations/kenna/KennaSecurityUploader.java +++ b/src/main/java/org/dependencytrack/integrations/kenna/KennaSecurityUploader.java @@ -21,20 +21,29 @@ import alpine.common.logging.Logger; import alpine.model.ConfigProperty; import alpine.security.crypto.DataEncryption; -import kong.unirest.ContentType; -import kong.unirest.HttpRequestWithBody; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONObject; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.dependencytrack.common.HttpClientPool; import org.dependencytrack.integrations.AbstractIntegrationPoint; import org.dependencytrack.integrations.PortfolioFindingUploader; import org.dependencytrack.model.Project; import org.dependencytrack.model.ProjectProperty; +import org.json.JSONObject; import java.io.ByteArrayInputStream; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; import static org.dependencytrack.model.ConfigPropertyConstants.KENNA_CONNECTOR_ID; import static org.dependencytrack.model.ConfigPropertyConstants.KENNA_ENABLED; @@ -74,7 +83,7 @@ public boolean isEnabled() { public InputStream process() { LOGGER.debug("Processing..."); final KennaDataTransformer kdi = new KennaDataTransformer(qm); - for (final Project project: qm.getAllProjects()) { + for (final Project project : qm.getAllProjects()) { final ProjectProperty externalId = qm.getProjectProperty(project, KENNA_ENABLED.getGroupName(), ASSET_EXTID_PROPERTY); if (externalId != null && externalId.getPropertyValue() != null) { LOGGER.debug("Transforming findings for project: " + project.getUuid() + " to KDI format"); @@ -89,27 +98,29 @@ public void upload(final InputStream payload) { LOGGER.debug("Uploading payload to KennaSecurity"); final ConfigProperty tokenProperty = qm.getConfigProperty(KENNA_TOKEN.getGroupName(), KENNA_TOKEN.getPropertyName()); try { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); final String token = DataEncryption.decryptAsString(tokenProperty.getPropertyValue()); - final HttpRequestWithBody request = ui.post(String.format(CONNECTOR_UPLOAD_URL, connectorId)); - final HttpResponse response = request - .header("X-Risk-Token", token) - .header("accept", "application/json") - .field("file", payload, ContentType.APPLICATION_JSON, "findings.json") - .field("run", "true") - .asJson(); - if (response.getStatus() == 200 && response.getBody() != null) { - final JSONObject root = response.getBody().getObject(); - if (root.getString("success").equals("true")) { - LOGGER.debug("Successfully uploaded KDI"); - return; + HttpPost request = new HttpPost(String.format(CONNECTOR_UPLOAD_URL, connectorId)); + request.addHeader("X-Risk-Token", token); + request.addHeader("accept", "application/json"); + List nameValuePairList = new ArrayList<>(); + nameValuePairList.add(new BasicNameValuePair("run", "true")); + request.setEntity(new UrlEncodedFormEntity(nameValuePairList, StandardCharsets.UTF_8)); + HttpEntity data = MultipartEntityBuilder.create().setMode(HttpMultipartMode.BROWSER_COMPATIBLE) + .addBinaryBody("file", payload, ContentType.APPLICATION_JSON, "findings.json") + .build(); + request.setEntity(data); + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK && response.getEntity() != null) { + String responseString = EntityUtils.toString(response.getEntity()); + final JSONObject root = new JSONObject(responseString); + if (root.getString("success").equals("true")) { + LOGGER.debug("Successfully uploaded KDI"); + return; + } + LOGGER.warn("An unexpected response was received uploading findings to Kenna Security"); + } else { + handleUnexpectedHttpResponse(LOGGER, request.getURI().toString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); } - LOGGER.warn("An unexpected response was received uploading findings to Kenna Security"); - } else { - LOGGER.warn("Kenna uploader did not receive expected response while attempting to upload " - + "Dependency-Track findings. HTTP response code: " - + response.getStatus() + " - " + response.getStatusText()); - handleUnexpectedHttpResponse(LOGGER, request.getUrl(), response.getStatus(), response.getStatusText()); } } catch (Exception e) { LOGGER.error("An error occurred attempting to upload findings to Kenna Security", e); diff --git a/src/main/java/org/dependencytrack/notification/publisher/AbstractWebhookPublisher.java b/src/main/java/org/dependencytrack/notification/publisher/AbstractWebhookPublisher.java index b865e156f8..be80253cd5 100644 --- a/src/main/java/org/dependencytrack/notification/publisher/AbstractWebhookPublisher.java +++ b/src/main/java/org/dependencytrack/notification/publisher/AbstractWebhookPublisher.java @@ -18,22 +18,24 @@ */ package org.dependencytrack.notification.publisher; -import alpine.common.logging.Logger; import alpine.notification.Notification; import io.pebbletemplates.pebble.template.PebbleTemplate; -import kong.unirest.Header; -import kong.unirest.Headers; -import kong.unirest.UnirestInstance; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; +import org.dependencytrack.common.HttpClientPool; import org.dependencytrack.exception.PublisherException; +import org.dependencytrack.util.HttpUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import javax.json.JsonObject; -import java.util.stream.Collectors; +import java.io.IOException; public abstract class AbstractWebhookPublisher implements Publisher { - public void publish(final String publisherName, final PebbleTemplate template, final Notification notification, final JsonObject config) { - final Logger logger = Logger.getLogger(this.getClass()); + final Logger logger = LoggerFactory.getLogger(getClass()); logger.debug("Preparing to publish " + publisherName + " notification"); if (config == null) { logger.warn("No configuration found. Skipping notification."); @@ -46,11 +48,9 @@ public void publish(final String publisherName, final PebbleTemplate template, f return; } final String mimeType = getTemplateMimeType(config); - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - - final var headers = new Headers(); - headers.add("content-type", mimeType); - headers.add("accept", mimeType); + var request = new HttpPost(destination); + request.addHeader("content-type", mimeType); + request.addHeader("accept", mimeType); final BasicAuthCredentials credentials; try { credentials = getBasicAuthCredentials(); @@ -59,20 +59,21 @@ public void publish(final String publisherName, final PebbleTemplate template, f return; } if (credentials != null) { - headers.setBasicAuth(credentials.user(), credentials.password()); + request.addHeader("Authorization", HttpUtil.basicAuthHeaderValue(credentials.user(), credentials.password())); } - final var response = ui.post(destination) - .headers(headers.all().stream().collect(Collectors.toMap(Header::getName, Header::getValue))) - .body(content) - .asString(); - - if (!response.isSuccess()) { - logger.error("An error was encountered publishing notification to " + publisherName); - logger.error("HTTP Status : " + response.getStatus() + " " + response.getStatusText()); - logger.error("Destination: " + destination); - logger.error("Response: " + response.getBody()); - logger.debug(content); + try { + request.setEntity(new StringEntity(content)); + try (final CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + if (response.getStatusLine().getStatusCode() < 200 || response.getStatusLine().getStatusCode() >= 300) { + logger.error("An error was encountered publishing notification to " + publisherName + + "with HTTP Status : " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase() + + " Destination: " + destination + " Response: " + EntityUtils.toString(response.getEntity())); + logger.debug(content); + } + } + } catch (IOException ex) { + handleRequestException(logger, ex); } } @@ -86,4 +87,8 @@ protected BasicAuthCredentials getBasicAuthCredentials() { protected record BasicAuthCredentials(String user, String password) { } + + protected void handleRequestException(final Logger logger, final Exception e) { + logger.error("Request failure", e); + } } diff --git a/src/main/java/org/dependencytrack/parser/github/graphql/GitHubSecurityAdvisoryParser.java b/src/main/java/org/dependencytrack/parser/github/graphql/GitHubSecurityAdvisoryParser.java index a06ce36b82..7e7854bd13 100644 --- a/src/main/java/org/dependencytrack/parser/github/graphql/GitHubSecurityAdvisoryParser.java +++ b/src/main/java/org/dependencytrack/parser/github/graphql/GitHubSecurityAdvisoryParser.java @@ -18,8 +18,8 @@ */ package org.dependencytrack.parser.github.graphql; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; +import org.json.JSONArray; +import org.json.JSONObject; import org.apache.commons.lang3.tuple.Pair; import org.dependencytrack.parser.github.graphql.model.GitHubSecurityAdvisory; import org.dependencytrack.parser.github.graphql.model.GitHubVulnerability; diff --git a/src/main/java/org/dependencytrack/parser/ossindex/OssIndexParser.java b/src/main/java/org/dependencytrack/parser/ossindex/OssIndexParser.java index 4e729b9934..cbe419f14b 100644 --- a/src/main/java/org/dependencytrack/parser/ossindex/OssIndexParser.java +++ b/src/main/java/org/dependencytrack/parser/ossindex/OssIndexParser.java @@ -19,11 +19,10 @@ package org.dependencytrack.parser.ossindex; import alpine.common.logging.Logger; -import kong.unirest.JsonNode; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; import org.dependencytrack.parser.ossindex.model.ComponentReport; import org.dependencytrack.parser.ossindex.model.ComponentReportVulnerability; +import org.json.JSONArray; +import org.json.JSONObject; import java.util.ArrayList; import java.util.List; @@ -41,15 +40,15 @@ public class OssIndexParser { /** * Parses the JSON response from Sonatype OSS Index * - * @param jsonNode the JSON node to parse + * @param responseString the response as a String to parse * @return an ComponentReport object */ - public List parse(final JsonNode jsonNode) { - LOGGER.debug("Parsing JSON node"); + public List parse(final String responseString) { + LOGGER.debug("Parsing JSON response"); + JSONArray arr = new JSONArray(responseString); final List componentReports = new ArrayList<>(); - final JSONArray resultArray = jsonNode.getArray(); - for (int i = 0; i < resultArray.length(); i++) { - final JSONObject object = resultArray.getJSONObject(i); + for (int i = 0; i < arr.length(); i++) { + final JSONObject object = arr.getJSONObject(i); final ComponentReport componentReport = parse(object); componentReports.add(componentReport); } diff --git a/src/main/java/org/dependencytrack/parser/osv/OsvAdvisoryParser.java b/src/main/java/org/dependencytrack/parser/osv/OsvAdvisoryParser.java index 2846b9ac4c..4f66d45d5c 100644 --- a/src/main/java/org/dependencytrack/parser/osv/OsvAdvisoryParser.java +++ b/src/main/java/org/dependencytrack/parser/osv/OsvAdvisoryParser.java @@ -18,8 +18,8 @@ */ package org.dependencytrack.parser.osv; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; +import org.json.JSONArray; +import org.json.JSONObject; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.model.Severity; import org.dependencytrack.parser.osv.model.OsvAdvisory; diff --git a/src/main/java/org/dependencytrack/parser/snyk/SnykParser.java b/src/main/java/org/dependencytrack/parser/snyk/SnykParser.java index 4edfea2bef..37a7fc3e01 100644 --- a/src/main/java/org/dependencytrack/parser/snyk/SnykParser.java +++ b/src/main/java/org/dependencytrack/parser/snyk/SnykParser.java @@ -22,8 +22,8 @@ import alpine.model.ConfigProperty; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; +import org.json.JSONArray; +import org.json.JSONObject; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Cwe; import org.dependencytrack.model.Severity; diff --git a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java index 8d5a260ecb..cc9a723b39 100644 --- a/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/ModelConverter.java @@ -23,12 +23,14 @@ import org.dependencytrack.model.Cwe; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.parser.common.resolver.CweResolver; +import org.dependencytrack.parser.vulndb.model.Author; +import org.dependencytrack.parser.vulndb.model.CvssV2Metric; +import org.dependencytrack.parser.vulndb.model.CvssV3Metric; +import org.dependencytrack.parser.vulndb.model.ExternalReference; import org.dependencytrack.persistence.QueryManager; import us.springett.cvss.CvssV2; import us.springett.cvss.CvssV3; import us.springett.cvss.Score; -import us.springett.vulndbdatamirror.parser.model.CvssV2Metric; -import us.springett.vulndbdatamirror.parser.model.CvssV3Metric; import java.math.BigDecimal; import java.time.OffsetDateTime; @@ -47,43 +49,45 @@ public final class ModelConverter { /** * Private constructor. */ - private ModelConverter() { } + private ModelConverter() { + } /** * Helper method that converts an VulnDB vulnerability object to a Dependency-Track vulnerability object. + * * @param vulnDbVuln the VulnDB vulnerability to convert * @return a Dependency-Track Vulnerability object */ - public static Vulnerability convert(final QueryManager qm, final us.springett.vulndbdatamirror.parser.model.Vulnerability vulnDbVuln) { + public static Vulnerability convert(final QueryManager qm, final org.dependencytrack.parser.vulndb.model.Vulnerability vulnDbVuln) { final org.dependencytrack.model.Vulnerability vuln = new org.dependencytrack.model.Vulnerability(); vuln.setSource(org.dependencytrack.model.Vulnerability.Source.VULNDB); - vuln.setVulnId(sanitize(String.valueOf(vulnDbVuln.getId()))); - vuln.setTitle(sanitize(vulnDbVuln.getTitle())); + vuln.setVulnId(sanitize(String.valueOf(vulnDbVuln.id()))); + vuln.setTitle(sanitize(vulnDbVuln.title())); /* Description */ final StringBuilder description = new StringBuilder(); - if (vulnDbVuln.getDescription() != null) { - description.append(sanitize(vulnDbVuln.getDescription())); + if (vulnDbVuln.description() != null) { + description.append(sanitize(vulnDbVuln.description())); } - if (vulnDbVuln.getTechnicalDescription() != null) { - description.append(" ").append(sanitize(vulnDbVuln.getTechnicalDescription())); + if (vulnDbVuln.technicalDescription() != null) { + description.append(" ").append(sanitize(vulnDbVuln.technicalDescription())); } - if (vulnDbVuln.getSolution() != null) { - description.append(" ").append(sanitize(vulnDbVuln.getSolution())); + if (vulnDbVuln.solution() != null) { + description.append(" ").append(sanitize(vulnDbVuln.solution())); } - if (vulnDbVuln.getManualNotes() != null) { - description.append(" ").append(sanitize(vulnDbVuln.getManualNotes())); + if (vulnDbVuln.manualNotes() != null) { + description.append(" ").append(sanitize(vulnDbVuln.manualNotes())); } vuln.setDescription(description.toString()); /* Dates */ - if (StringUtils.isNotBlank(vulnDbVuln.getDisclosureDate())) { - final OffsetDateTime odt = OffsetDateTime.parse(vulnDbVuln.getDisclosureDate()); + if (StringUtils.isNotBlank(vulnDbVuln.disclosureDate())) { + final OffsetDateTime odt = OffsetDateTime.parse(vulnDbVuln.disclosureDate()); vuln.setCreated(Date.from(odt.toInstant())); } - if (StringUtils.isNotBlank(vulnDbVuln.getDisclosureDate())) { - final OffsetDateTime odt = OffsetDateTime.parse(vulnDbVuln.getDisclosureDate()); + if (StringUtils.isNotBlank(vulnDbVuln.disclosureDate())) { + final OffsetDateTime odt = OffsetDateTime.parse(vulnDbVuln.disclosureDate()); vuln.setPublished(Date.from(odt.toInstant())); } /* @@ -96,9 +100,9 @@ public static Vulnerability convert(final QueryManager qm, final us.springett.vu /* References */ final StringBuilder references = new StringBuilder(); - for (final us.springett.vulndbdatamirror.parser.model.ExternalReference reference : vulnDbVuln.getExtReferences()) { - final String sType = sanitize(reference.getType()); - final String sValue = sanitize(reference.getValue()); + for (final ExternalReference reference : vulnDbVuln.extReferences()) { + final String sType = sanitize(reference.type()); + final String sValue = sanitize(reference.value()); // Convert reference to Markdown format if (sValue != null && sValue.startsWith("http")) { references.append("* [").append(sValue).append("](").append(sValue).append(")\n"); @@ -111,9 +115,9 @@ public static Vulnerability convert(final QueryManager qm, final us.springett.vu /* Credits */ final StringBuilder credits = new StringBuilder(); - for (final us.springett.vulndbdatamirror.parser.model.Author author : vulnDbVuln.getAuthors()) { - final String name = sanitize(author.getName()); - final String company = sanitize(author.getCompany()); + for (final Author author : vulnDbVuln.authors()) { + final String name = sanitize(author.name()); + final String company = sanitize(author.company()); if (name != null && company != null) { credits.append(name).append(" (").append(company).append(")").append(", "); } else { @@ -131,33 +135,33 @@ public static Vulnerability convert(final QueryManager qm, final us.springett.vu } CvssV2 cvssV2; - for (final CvssV2Metric metric : vulnDbVuln.getCvssV2Metrics()) { - cvssV2 = metric.toNormalizedMetric(); + for (final CvssV2Metric metric : vulnDbVuln.cvssV2Metrics()) { + cvssV2 = toNormalizedMetric(metric); final Score score = cvssV2.calculateScore(); vuln.setCvssV2Vector(cvssV2.getVector()); vuln.setCvssV2BaseScore(BigDecimal.valueOf(score.getBaseScore())); vuln.setCvssV2ImpactSubScore(BigDecimal.valueOf(score.getImpactSubScore())); vuln.setCvssV2ExploitabilitySubScore(BigDecimal.valueOf(score.getExploitabilitySubScore())); - if (metric.getCveId() != null) { + if (metric.cveId() != null) { break; // Always prefer use of the NVD scoring, if available } } CvssV3 cvssV3; - for (final CvssV3Metric metric : vulnDbVuln.getCvssV3Metrics()) { - cvssV3 = metric.toNormalizedMetric(); + for (final CvssV3Metric metric : vulnDbVuln.cvssV3Metrics()) { + cvssV3 = toNormalizedMetric(metric); final Score score = cvssV3.calculateScore(); vuln.setCvssV3Vector(cvssV3.getVector()); vuln.setCvssV3BaseScore(BigDecimal.valueOf(score.getBaseScore())); vuln.setCvssV3ImpactSubScore(BigDecimal.valueOf(score.getImpactSubScore())); vuln.setCvssV3ExploitabilitySubScore(BigDecimal.valueOf(score.getExploitabilitySubScore())); - if (metric.getCveId() != null) { + if (metric.cveId() != null) { break; // Always prefer use of the NVD scoring, if available } } - if (vulnDbVuln.getNvdAdditionalInfo() != null) { - final String cweString = vulnDbVuln.getNvdAdditionalInfo().getCweId(); + if (vulnDbVuln.nvdAdditionalInfo() != null) { + final String cweString = vulnDbVuln.nvdAdditionalInfo().cweId(); if (cweString != null && cweString.startsWith("CWE-")) { final Cwe cwe = CweResolver.getInstance().resolve(qm, cweString); if (cwe != null) { @@ -192,4 +196,55 @@ private static String sanitize(final String input) { .replaceAll("[\\u0080-\\u009F]", "") // (C1 Control Characters) ); } + + public static CvssV2 toNormalizedMetric(CvssV2Metric metric) { + CvssV2 cvss = new CvssV2(); + if (!"ADJACENT_NETWORK" .equals(metric.accessVector()) && !"ADJACENT" .equals(metric.accessVector())) { + if ("LOCAL" .equals(metric.accessVector())) { + cvss.attackVector(CvssV2.AttackVector.LOCAL); + } else if ("NETWORK" .equals(metric.accessVector())) { + cvss.attackVector(CvssV2.AttackVector.NETWORK); + } + } else { + cvss.attackVector(CvssV2.AttackVector.ADJACENT); + } + + if ("SINGLE_INSTANCE" .equals(metric.authentication())) { + cvss.authentication(CvssV2.Authentication.SINGLE); + } else if ("MULTIPLE_INSTANCES" .equals(metric.authentication())) { + cvss.authentication(CvssV2.Authentication.MULTIPLE); + } else if ("NONE" .equals(metric.authentication())) { + cvss.authentication(CvssV2.Authentication.NONE); + } + + cvss.attackComplexity(CvssV2.AttackComplexity.valueOf(metric.accessComplexity())); + cvss.confidentiality(CvssV2.CIA.valueOf(metric.confidentialityImpact())); + cvss.integrity(CvssV2.CIA.valueOf(metric.integrityImpact())); + cvss.availability(CvssV2.CIA.valueOf(metric.availabilityImpact())); + return cvss; + } + + public static CvssV3 toNormalizedMetric(CvssV3Metric metric) { + CvssV3 cvss = new CvssV3(); + if (!"ADJACENT_NETWORK" .equals(metric.attackVector()) && !"ADJACENT" .equals(metric.attackVector())) { + if ("LOCAL" .equals(metric.attackVector())) { + cvss.attackVector(CvssV3.AttackVector.LOCAL); + } else if ("NETWORK" .equals(metric.attackVector())) { + cvss.attackVector(CvssV3.AttackVector.NETWORK); + } else if ("PHYSICAL" .equals(metric.attackVector())) { + cvss.attackVector(CvssV3.AttackVector.PHYSICAL); + } + } else { + cvss.attackVector(CvssV3.AttackVector.ADJACENT); + } + + cvss.attackComplexity(CvssV3.AttackComplexity.valueOf(metric.attackComplexity())); + cvss.privilegesRequired(CvssV3.PrivilegesRequired.valueOf(metric.privilegesRequired())); + cvss.userInteraction(CvssV3.UserInteraction.valueOf(metric.userInteraction())); + cvss.scope(CvssV3.Scope.valueOf(metric.scope())); + cvss.confidentiality(CvssV3.CIA.valueOf(metric.confidentialityImpact())); + cvss.integrity(CvssV3.CIA.valueOf(metric.integrityImpact())); + cvss.availability(CvssV3.CIA.valueOf(metric.availabilityImpact())); + return cvss; + } } diff --git a/src/main/java/org/dependencytrack/parser/vulndb/VulnDbClient.java b/src/main/java/org/dependencytrack/parser/vulndb/VulnDbClient.java new file mode 100644 index 0000000000..f371f13fb8 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/VulnDbClient.java @@ -0,0 +1,118 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb; + +import oauth.signpost.OAuthConsumer; +import oauth.signpost.basic.DefaultOAuthConsumer; +import oauth.signpost.exception.OAuthCommunicationException; +import oauth.signpost.exception.OAuthExpectationFailedException; +import oauth.signpost.exception.OAuthMessageSignerException; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.util.EntityUtils; +import org.dependencytrack.common.HttpClientPool; +import org.dependencytrack.parser.vulndb.model.Results; +import org.dependencytrack.parser.vulndb.model.Vulnerability; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/* + * Util class needed by VulnDBAnalysis Task to get vulnerabilities by the cpe provided. The result obtained from the api + * call are parsed and processed before being returned . Class brought over from the vulndb-data-mirror repo: + * ... and refactored to use the apache http client + * instead of the Unirest client it was using in the source repo. + */ +public class VulnDbClient { + + private final String consumerKey; + private final String consumerSecret; + + private final String apiBaseUrl; + + + public VulnDbClient(String consumerKey, String consumerSecret, String apiBaseUrl) { + this.consumerKey = consumerKey; + this.consumerSecret = consumerSecret; + this.apiBaseUrl = apiBaseUrl; + } + + private static final Logger LOGGER = LoggerFactory.getLogger(VulnDbClient.class); + + public Results getVulnerabilitiesByCpe(String cpe, int size, int page) throws IOException, OAuthMessageSignerException, OAuthExpectationFailedException, URISyntaxException, OAuthCommunicationException { + String encodedCpe = cpe; + + try { + encodedCpe = URLEncoder.encode(cpe, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException var6) { + LOGGER.warn("An error occurred while URL encoding a CPE", var6); + throw new UnsupportedEncodingException(); + } + + return this.getResults(apiBaseUrl + "/api/v1/vulnerabilities/find_by_cpe?&cpe=" + encodedCpe, Vulnerability.class, size, page); + } + + private Results getResults(String url, Class clazz, int size, int page) throws IOException, + OAuthMessageSignerException, OAuthExpectationFailedException, URISyntaxException, + OAuthCommunicationException { + String modifiedUrl = url.contains("?") ? url + "&" : url + "?"; + try (CloseableHttpResponse response = this.makeRequest(modifiedUrl + "size=" + size + "&page=" + page)) { + VulnDbParser vulnDbParser = new VulnDbParser(); + Results results; + if (response != null) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + String responseString = EntityUtils.toString(response.getEntity()); + var jsonObject = new JSONObject(responseString); + results = vulnDbParser.parse(jsonObject, clazz); + return results; + } else { + results = new Results(); + results.setErrorCondition("An unexpected response was returned from VulnDB. Request unsuccessful: " + response.getStatusLine().getStatusCode() + " - " + response.getStatusLine().getReasonPhrase()); + this.logHttpResponseError(response); + return results; + } + } else { + results = new Results(); + results.setErrorCondition("No response was returned from VulnDB. No further information is available."); + return results; + } + } + } + + private CloseableHttpResponse makeRequest(String url) throws OAuthMessageSignerException, OAuthExpectationFailedException, IOException, URISyntaxException, OAuthCommunicationException { + OAuthConsumer consumer = new DefaultOAuthConsumer(this.consumerKey, this.consumerSecret); + String signed = consumer.sign(url); + URIBuilder uriBuilder = new URIBuilder(signed); + HttpGet request = new HttpGet(uriBuilder.build().toString()); + request.addHeader("X-User-Agent", "Dependency Track (https://github.com/DependencyTrack/dependency-track)"); + return HttpClientPool.getClient().execute(request); + } + + private void logHttpResponseError(CloseableHttpResponse response) { + LOGGER.error("Response was not successful: " + response.getStatusLine().getStatusCode() + " - " + response.getStatusLine().getReasonPhrase()); + } +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/VulnDbParser.java b/src/main/java/org/dependencytrack/parser/vulndb/VulnDbParser.java new file mode 100644 index 0000000000..0db26820a2 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/VulnDbParser.java @@ -0,0 +1,354 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb; + +import org.apache.commons.lang3.StringUtils; +import org.dependencytrack.parser.vulndb.model.ApiObject; +import org.dependencytrack.parser.vulndb.model.Author; +import org.dependencytrack.parser.vulndb.model.Classification; +import org.dependencytrack.parser.vulndb.model.Cpe; +import org.dependencytrack.parser.vulndb.model.CvssV2Metric; +import org.dependencytrack.parser.vulndb.model.CvssV3Metric; +import org.dependencytrack.parser.vulndb.model.ExternalReference; +import org.dependencytrack.parser.vulndb.model.ExternalText; +import org.dependencytrack.parser.vulndb.model.NvdAdditionalInfo; +import org.dependencytrack.parser.vulndb.model.Product; +import org.dependencytrack.parser.vulndb.model.Results; +import org.dependencytrack.parser.vulndb.model.Status; +import org.dependencytrack.parser.vulndb.model.Vendor; +import org.dependencytrack.parser.vulndb.model.Version; +import org.dependencytrack.parser.vulndb.model.Vulnerability; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +/* + * Model class needed by VulnDBAnalysis task. Class brought over from the vulndb-data-mirror repo: + * ... + */ +public class VulnDbParser { + private static final Logger LOGGER = LoggerFactory.getLogger(VulnDbParser.class); + + public VulnDbParser() { + } + + public Status parseStatus(JSONObject root) { + LOGGER.debug("Parsing JSON node"); + Status status = new Status(root.optString("organization_name"), root.optString("user_name_requesting"), + root.optString("user_email_address_requesting"), + root.optString("subscription_end_date"), + root.optString("number_of_api_calls_allowed_per_month"), + root.optString("number_of_api_calls_made_this_month"), + root.optString("vulndb_statistics"), + root.toString() + ); + return status; + } + + public Results parse(Object jsonNode, Class apiObject) { + LOGGER.debug("Parsing JSON node"); + + final Results results = new Results<>(); + JSONObject root; + root = (JSONObject) jsonNode; + results.setPage(root.getInt("current_page")); + results.setTotal(root.getInt("total_entries")); + results.setRawResults(jsonNode.toString()); + final JSONArray rso = root.getJSONArray("results"); + + if (Product.class == apiObject) { + results.setResults(parseProducts(rso)); + } else if (Vendor.class == apiObject) { + results.setResults(parseVendors(rso)); + } else if (Version.class == apiObject) { + results.setResults(parseVersions(rso)); + } else if (Vulnerability.class == apiObject) { + results.setResults(parseVulnerabilities(rso)); + } + return results; + } + + public Results parse(String jsonData, Class apiObject) { + Object result = null; + try { + result = new JSONObject(jsonData); + } catch (JSONException ex) { + result = new JSONArray(jsonData); + } + if (result instanceof JSONObject) { + return this.parse((JSONObject) result, apiObject); + } else { + return this.parse((JSONArray) result, apiObject); + } + } + + public Results parse(File file, Class apiObject) throws IOException { + String jsonData = Files.readString(Paths.get(file.toURI()), Charset.defaultCharset()); + Object result = null; + try { + result = new JSONObject(jsonData); + } catch (JSONException ex) { + result = new JSONArray(jsonData); + } + if (result instanceof JSONObject) { + return this.parse((JSONObject) result, apiObject); + } else { + return this.parse((JSONArray) result, apiObject); + } + } + + private List parseCpes(JSONArray rso) { + List cpes = null; + if (rso != null) { + cpes = new ArrayList(); + + for (int i = 0; i < rso.length(); ++i) { + JSONObject object = rso.getJSONObject(i); + Cpe cpe = new Cpe(StringUtils.trimToNull(object.optString("cpe", (String) null)), StringUtils.trimToNull(object.optString("type", (String) null))); + cpes.add(cpe); + } + } + + return cpes; + } + + private List parseProducts(JSONArray rso) { + List products = null; + if (rso != null) { + products = new ArrayList(); + + for (int i = 0; i < rso.length(); ++i) { + JSONObject object = rso.getJSONObject(i); + Product product = new Product(object.getInt("id"), + StringUtils.trimToNull(object.optString("name", (String) null)), + this.parseVersions(object.optJSONArray("versions"))); + products.add(product); + } + } + + return products; + } + + private List parseVendors(JSONArray rso) { + List vendors = null; + if (rso != null) { + vendors = new ArrayList(); + + for (int i = 0; i < rso.length(); ++i) { + JSONObject object = rso.getJSONObject(i); + if (object.has("vendor")) { + JSONObject childObject = object.getJSONObject("vendor"); + Vendor vendor = this.parseVendor(childObject); + vendors.add(vendor); + } else { + Vendor vendor = this.parseVendor(object); + vendors.add(vendor); + } + } + } + + return vendors; + } + + private Vendor parseVendor(JSONObject object) { + Vendor vendor = new Vendor(object.getInt("id"), + StringUtils.trimToNull(object.optString("name", (String) null)), + StringUtils.trimToNull(object.optString("short_name", (String) null)), + StringUtils.trimToNull(object.optString("vendor_url", (String) null)), + this.parseProducts(object.optJSONArray("products"))); + return vendor; + } + + private List parseVersions(JSONArray rso) { + List versions = null; + if (rso != null) { + versions = new ArrayList(); + + for (int i = 0; i < rso.length(); ++i) { + JSONObject object = rso.getJSONObject(i); + Version version = new Version(object.getInt("id"), + StringUtils.trimToNull(object.optString("name", (String) null)), + object.optBoolean("affected", false), + this.parseCpes(object.optJSONArray("cpe"))); + versions.add(version); + } + } + + return versions; + } + + private List parseVulnerabilities(JSONArray rso) { + List vulnerabilities = null; + if (rso != null) { + vulnerabilities = new ArrayList(); + + for (int i = 0; i < rso.length(); ++i) { + JSONObject object = rso.getJSONObject(i); + JSONArray classifications = object.optJSONArray("classifications"); + List classificationList = new ArrayList<>(); + if (classifications != null) { + for (int j = 0; j < classifications.length(); ++j) { + JSONObject jso = classifications.getJSONObject(j); + Classification classification = new Classification(jso.getInt("id"), StringUtils.trimToNull(jso.optString("name", (String) null)), StringUtils.trimToNull(jso.optString("longname", (String) null)), StringUtils.trimToNull(jso.optString("description", (String) null)), + StringUtils.trimToNull(jso.optString("mediumtext", (String) null))); + classificationList.add(classification); + } + } + + JSONArray authors = object.optJSONArray("authors"); + List authorList = new ArrayList<>(); + if (authors != null) { + for (int j = 0; j < authors.length(); ++j) { + JSONObject jso = authors.getJSONObject(j); + Author author = new Author(jso.getInt("id"), StringUtils.trimToNull(jso.optString("name", (String) null)), StringUtils.trimToNull(jso.optString("company", (String) null)), + StringUtils.trimToNull(jso.optString("email", (String) null)), + StringUtils.trimToNull(jso.optString("company_url", (String) null)), + StringUtils.trimToNull(jso.optString("country", (String) null))); + authorList.add(author); + } + } + + JSONArray extRefs = object.optJSONArray("ext_references"); + List externalReferenceList = new ArrayList<>(); + if (extRefs != null) { + for (int j = 0; j < extRefs.length(); ++j) { + JSONObject jso = extRefs.getJSONObject(j); + ExternalReference externalReference = new ExternalReference(StringUtils.trimToNull(jso.optString("type", (String) null)), + StringUtils.trimToNull(jso.optString("value", (String) null))); + externalReferenceList.add(externalReference); + } + } + + JSONArray extTexts = object.optJSONArray("ext_texts"); + List externalTextList = new ArrayList<>(); + if (extTexts != null) { + for (int j = 0; j < extTexts.length(); ++j) { + JSONObject jso = extTexts.getJSONObject(j); + ExternalText externalText = new ExternalText(StringUtils.trimToNull(jso.optString("type", (String) null)), + StringUtils.trimToNull(jso.optString("value", (String) null))); + externalTextList.add(externalText); + } + } + + JSONArray cvssv2Metrics = object.optJSONArray("cvss_metrics"); + List cvssV2MetricList = new ArrayList<>(); + if (cvssv2Metrics != null) { + for (int j = 0; j < cvssv2Metrics.length(); ++j) { + JSONObject jso = cvssv2Metrics.getJSONObject(j); + CvssV2Metric metric = new CvssV2Metric(jso.getInt("id"), + StringUtils.trimToNull(jso.optString("access_complexity", (String) null)), + StringUtils.trimToNull(jso.optString("cve_id", (String) null)), + StringUtils.trimToNull(jso.optString("source", (String) null)), + StringUtils.trimToNull(jso.optString("availability_impact", (String) null)), + StringUtils.trimToNull(jso.optString("confidentiality_impact", (String) null)), + StringUtils.trimToNull(jso.optString("authentication", (String) null)), + jso.optBigDecimal("calculated_cvss_base_score", (BigDecimal) null), + StringUtils.trimToNull(jso.optString("generated_on", (String) null)), + jso.optBigDecimal("score", (BigDecimal) null), + StringUtils.trimToNull(jso.optString("access_vector", (String) null)), + StringUtils.trimToNull(jso.optString("integrity_impact", (String) null))); + cvssV2MetricList.add(metric); + } + } + + JSONArray cvssv3Metrics = object.optJSONArray("cvss_version_three_metrics"); + List cvssV3MetricList = new ArrayList<>(); + if (cvssv3Metrics != null) { + for (int j = 0; j < cvssv3Metrics.length(); ++j) { + JSONObject jso = cvssv3Metrics.getJSONObject(j); + CvssV3Metric metric = new CvssV3Metric(jso.getInt("id"), + StringUtils.trimToNull(jso.optString("attack_complexity", (String) null)), + jso.optString("scope", (String) null), + jso.optString("attack_vector", (String) null), + StringUtils.trimToNull(jso.optString("availability_impact", (String) null)), + jso.optBigDecimal("score", (BigDecimal) null), + StringUtils.trimToNull(jso.optString("privileges_required", (String) null)), + StringUtils.trimToNull(jso.optString("user_interaction", (String) null)), + StringUtils.trimToNull(jso.optString("cve_id", (String) null)), + StringUtils.trimToNull(jso.optString("source", (String) null)), + StringUtils.trimToNull(jso.optString("confidentiality_impact", (String) null)), + jso.optBigDecimal("calculated_cvss_base_score", (BigDecimal) null), + StringUtils.trimToNull(jso.optString("generated_on", (String) null)), + StringUtils.trimToNull(jso.optString("integrity_impact", (String) null)) + ); + cvssV3MetricList.add(metric); + } + } + + JSONArray nvdInfo = object.optJSONArray("nvd_additional_information"); + // List nvdAdditionalInfos = new ArrayList<>(); + NvdAdditionalInfo nvdAdditionalInfo = null; + if (nvdInfo != null) { +// for (int j = 0; j < nvdInfo.length(); ++j) { +// JSONObject jso = nvdInfo.getJSONObject(j); +// NvdAdditionalInfo nvdAdditionalInfo = new NvdAdditionalInfo(StringUtils.trimToNull(jso.optString("summary", (String) null)), +// StringUtils.trimToNull(jso.optString("cwe_id", (String) null)), +// StringUtils.trimToNull(jso.optString("cve_id", (String) null))); +// nvdAdditionalInfos.add(nvdAdditionalInfo); +// } + nvdAdditionalInfo = new NvdAdditionalInfo(StringUtils.trimToNull(nvdInfo.getJSONObject(nvdInfo.length()-1).optString("summary", (String) null)), + StringUtils.trimToNull(nvdInfo.getJSONObject(nvdInfo.length()-1).optString("cwe_id", (String) null)), + StringUtils.trimToNull(nvdInfo.getJSONObject(nvdInfo.length()-1).optString("cve_id", (String) null))); + + } + + JSONArray vendors = object.optJSONArray("vendors"); + Vulnerability vulnerability = new Vulnerability(object.getInt("vulndb_id"), + StringUtils.trimToNull(object.optString("title", (String) null)), + StringUtils.trimToNull(object.optString("disclosure_date", (String) null)), + StringUtils.trimToNull(object.optString("discovery_date", (String) null)), + StringUtils.trimToNull(object.optString("exploit_publish_date", (String) null)), + StringUtils.trimToNull(object.optString("keywords", (String) null)), + StringUtils.trimToNull(object.optString("short_description", (String) null)), + StringUtils.trimToNull(object.optString("description", (String) null)), + StringUtils.trimToNull(object.optString("solution", (String) null)), + StringUtils.trimToNull(object.optString("manual_notes", (String) null)), + StringUtils.trimToNull(object.optString("t_description", (String) null)), + StringUtils.trimToNull(object.optString("solution_date", (String) null)), + StringUtils.trimToNull(object.optString("vendor_informed_date", (String) null)), + StringUtils.trimToNull(object.optString("vendor_ack_date", (String) null)), + StringUtils.trimToNull(object.optString("third_party_solution_date", (String) null)), + classificationList, + authorList, + externalReferenceList, + externalTextList, + this.parseVendors(vendors), + cvssV2MetricList, + cvssV3MetricList, + nvdAdditionalInfo + ); + vulnerabilities.add(vulnerability); + } + } + + return vulnerabilities; + } +} + diff --git a/src/main/java/org/dependencytrack/common/UnirestFactory.java b/src/main/java/org/dependencytrack/parser/vulndb/model/ApiObject.java similarity index 58% rename from src/main/java/org/dependencytrack/common/UnirestFactory.java rename to src/main/java/org/dependencytrack/parser/vulndb/model/ApiObject.java index f658a87be9..d4b0e9a581 100644 --- a/src/main/java/org/dependencytrack/common/UnirestFactory.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/ApiObject.java @@ -16,22 +16,15 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) Steve Springett. All Rights Reserved. */ -package org.dependencytrack.common; +package org.dependencytrack.parser.vulndb.model; -import kong.unirest.Unirest; -import kong.unirest.UnirestInstance; - -public final class UnirestFactory { - - private static final UnirestInstance UNIREST_INSTANCE = Unirest.primaryInstance(); - static { - UNIREST_INSTANCE.config().httpClient(ManagedHttpClientFactory.newManagedHttpClient().getHttpClient()); - } - - private UnirestFactory() { - } +/** + * This interface defines the top-level (and queryable) objects that + * VulnDB supports. + * + * + */ +public interface ApiObject { + int id(); - public static UnirestInstance getUnirestInstance() { - return UNIREST_INSTANCE; - } } diff --git a/src/test/java/org/dependencytrack/common/UnirestFactoryTest.java b/src/main/java/org/dependencytrack/parser/vulndb/model/Author.java similarity index 53% rename from src/test/java/org/dependencytrack/common/UnirestFactoryTest.java rename to src/main/java/org/dependencytrack/parser/vulndb/model/Author.java index 7825388dfb..4d2499cfe7 100644 --- a/src/test/java/org/dependencytrack/common/UnirestFactoryTest.java +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/Author.java @@ -16,24 +16,12 @@ * SPDX-License-Identifier: Apache-2.0 * Copyright (c) Steve Springett. All Rights Reserved. */ -package org.dependencytrack.common; +package org.dependencytrack.parser.vulndb.model; -import kong.unirest.UnirestInstance; -import org.junit.Assert; -import org.junit.Test; - -public class UnirestFactoryTest { - - @Test - public void instanceTest() { - UnirestInstance ui1 = UnirestFactory.getUnirestInstance(); - UnirestInstance ui2 = UnirestFactory.getUnirestInstance(); - Assert.assertSame(ui1, ui2); - } - - @Test - public void httpClientTest() { - UnirestInstance ui = UnirestFactory.getUnirestInstance(); - Assert.assertNotSame(ui.config().getClient().getClient(), ManagedHttpClientFactory.newManagedHttpClient()); - } +/** + * The response from VulnDB Vulnerability API will respond with 0 or more authors. + * This record defines the Author objects returned. + *Record created to replace the model class defined here: ... + */ +public record Author(int id, String name, String company, String email, String companyUrl, String country) { } diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/Classification.java b/src/main/java/org/dependencytrack/parser/vulndb/model/Classification.java new file mode 100644 index 0000000000..32064bc066 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/Classification.java @@ -0,0 +1,27 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +/** + * The response from VulnDB Vulnerability API will respond with 0 or more classifications. + * This class defines the Classification objects returned. + * Record created to replace the model class defined here: ... + */ +public record Classification(int id, String name, String longname, String description, String mediumtext) { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/Cpe.java b/src/main/java/org/dependencytrack/parser/vulndb/model/Cpe.java new file mode 100644 index 0000000000..564eda17fe --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/Cpe.java @@ -0,0 +1,26 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +/** + * Defines an optional CPE returned in a response. + * Record created to replace the model class defined here: ... + */ +public record Cpe(String cpe, String type) { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV2Metric.java b/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV2Metric.java new file mode 100644 index 0000000000..f1713a6177 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV2Metric.java @@ -0,0 +1,33 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +import java.math.BigDecimal; + +/** + * The response from VulnDB Vulnerability API will respond with 0 or more CVSS Metrics. + * This record defines the CvssV2Metric objects returned. + * Record created to replace the model class defined here: ... + */ +public record CvssV2Metric(int id, String accessComplexity, String cveId, String source, String availabilityImpact, + String confidentialityImpact, + String authentication, BigDecimal calculatedCvssBaseScore, String generatedOn, + BigDecimal score, String accessVector, + String integrityImpact) { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV3Metric.java b/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV3Metric.java new file mode 100644 index 0000000000..d069365974 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/CvssV3Metric.java @@ -0,0 +1,42 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +import java.math.BigDecimal; + +/** + * The response from VulnDB Vulnerability API will respond with 0 or more CVSS Metrics. + * This record defines the CvssV3Metric objects returned. + * Record created to replace the model class defined here: ... + */ +public record CvssV3Metric(int id, + String attackComplexity, String scope, + String attackVector, + String availabilityImpact, + BigDecimal score, + String privilegesRequired, + String userInteraction, + String source, + String cveId, + String confidentialityImpact, + BigDecimal calculatedCvssBaseScore, + String generatedOn, + String integrityImpact +) { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/ExternalReference.java b/src/main/java/org/dependencytrack/parser/vulndb/model/ExternalReference.java new file mode 100644 index 0000000000..56bd948aad --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/ExternalReference.java @@ -0,0 +1,27 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +/** + * The response from VulnDB Vulnerability API will respond with 0 or more external + * references. This record defines the ExternalReference objects returned. + * Record created to replace the model class defined here: ... + */ +public record ExternalReference(String type, String value) { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/ExternalText.java b/src/main/java/org/dependencytrack/parser/vulndb/model/ExternalText.java new file mode 100644 index 0000000000..0f1cccb5ad --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/ExternalText.java @@ -0,0 +1,27 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +/** + * The response from VulnDB Vulnerability API will respond with 0 or more external + * texts. This record defines the ExternalText objects returned. + * Record created to replace the model class defined here: ... + */ +public record ExternalText(String type, String value) { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/NvdAdditionalInfo.java b/src/main/java/org/dependencytrack/parser/vulndb/model/NvdAdditionalInfo.java new file mode 100644 index 0000000000..1a85494506 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/NvdAdditionalInfo.java @@ -0,0 +1,27 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +/** + * The response from VulnDB Vulnerability API will respond with 0 or more nvd_additional_information. + * This record defines the NvdAdditionalInfo objects returned. + * Record created to replace the model class defined here: ... + */ +public record NvdAdditionalInfo(String summary, String cweId, String cveId) { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/Product.java b/src/main/java/org/dependencytrack/parser/vulndb/model/Product.java new file mode 100644 index 0000000000..6152aed203 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/Product.java @@ -0,0 +1,29 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +import java.util.List; + +/** + * The response from VulnDB Product API will respond with 0 or more products. + * This record defines the Product objects returned. + * Record created to replace the model class defined here: ... + */ +public record Product(int id, String name, List versions) implements ApiObject{ +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/Results.java b/src/main/java/org/dependencytrack/parser/vulndb/model/Results.java new file mode 100644 index 0000000000..d2146d14d1 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/Results.java @@ -0,0 +1,87 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Defines a top-level Results object containing a list of + * possible results and count/page data. + * + * @author Steve Springett + */ +public class Results { + private int page; + private int total; + private List results = new ArrayList(); + private String rawResults; + private String errorCondition; + + public Results() { + } + + public int getPage() { + return this.page; + } + + public void setPage(int page) { + this.page = page; + } + + public int getTotal() { + return this.total; + } + + public void setTotal(int total) { + this.total = total; + } + + public List getResults() { + return this.results; + } + + public void setResults(List objects) { + this.results = objects; + } + + public void add(T object) { + this.results.add(object); + } + + public String getRawResults() { + return this.rawResults; + } + + public void setRawResults(String rawResults) { + this.rawResults = rawResults; + } + + public boolean isSuccessful() { + return this.errorCondition == null; + } + + public String getErrorCondition() { + return this.errorCondition; + } + + public void setErrorCondition(String errorCondition) { + this.errorCondition = errorCondition; + } +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/Status.java b/src/main/java/org/dependencytrack/parser/vulndb/model/Status.java new file mode 100644 index 0000000000..3de8e81920 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/Status.java @@ -0,0 +1,29 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +/** + * Defines a top-level Status object. + * Record created to replace the model class defined here: ... + */ +public record Status(String organizationName, String userNameRequesting, String userEmailRequesting, + String subscriptionEndDate, + String apiCallsAllowedPerMonth, String apiCallsMadeThisMonth, String vulnDbStatistics, + String rawStatus) { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/Vendor.java b/src/main/java/org/dependencytrack/parser/vulndb/model/Vendor.java new file mode 100644 index 0000000000..9e4a8014ad --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/Vendor.java @@ -0,0 +1,30 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +import java.util.List; + +/** + * The response from VulnDB Vendor API will respond with 0 or more vendors. + * This record defines the Vendor objects returned. + * Record created to replace the model class defined here: ... + */ +public record Vendor(int id, String name, String shortName, String vendorUrl, + List products) implements ApiObject { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/Version.java b/src/main/java/org/dependencytrack/parser/vulndb/model/Version.java new file mode 100644 index 0000000000..497ebe3b32 --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/Version.java @@ -0,0 +1,29 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +import java.util.List; + +/** + * The response from VulnDB Version API will respond with 0 or more versions. + * This record defines the Version objects returned. + * Record created to replace the model class defined here: ... + */ +public record Version(int id, String name, boolean affected, List cpes) implements ApiObject { +} diff --git a/src/main/java/org/dependencytrack/parser/vulndb/model/Vulnerability.java b/src/main/java/org/dependencytrack/parser/vulndb/model/Vulnerability.java new file mode 100644 index 0000000000..295389db9e --- /dev/null +++ b/src/main/java/org/dependencytrack/parser/vulndb/model/Vulnerability.java @@ -0,0 +1,51 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.parser.vulndb.model; + +import java.util.List; + +/** + * The response from VulnDB Vulnerability API will respond with 0 or more vulnerabilities. + * This record defines the Vulnerability objects returned. + * Record created to replace the model class defined here: ... + */ +public record Vulnerability(int id, + String title, + String disclosureDate, + String discoveryDate, + String exploitPublishDate, + String keywords, + String shortDescription, + String description, + String solution, + String manualNotes, + String technicalDescription, + String solutionDate, + String vendorInformedDate, + String vendorAckDate, + String thirdPartySolutionDate, + List classifications, + List authors, + List extReferences, + List extTexts, + List vendors, + List cvssV2Metrics, + List cvssV3Metrics, + NvdAdditionalInfo nvdAdditionalInfo) implements ApiObject { +} diff --git a/src/main/java/org/dependencytrack/persistence/CweImporter.java b/src/main/java/org/dependencytrack/persistence/CweImporter.java index 63a1ad8f3e..d89d0339f8 100644 --- a/src/main/java/org/dependencytrack/persistence/CweImporter.java +++ b/src/main/java/org/dependencytrack/persistence/CweImporter.java @@ -66,7 +66,7 @@ public void processCweDefinitions() throws ParserConfigurationException, SAXExce final DocumentBuilder builder = factory.newDocumentBuilder(); final Document doc = builder.parse(is); - final XPathFactory xPathfactory = XPathFactory.newInstance(); + final var xPathfactory = XPathFactory.newInstance(); final XPath xpath = xPathfactory.newXPath(); final XPathExpression expr1 = xpath.compile("/Weakness_Catalog/Categories/Category"); diff --git a/src/main/java/org/dependencytrack/policy/ComponentHashPolicyEvaluator.java b/src/main/java/org/dependencytrack/policy/ComponentHashPolicyEvaluator.java index 716fa2fb26..70fc3c9887 100644 --- a/src/main/java/org/dependencytrack/policy/ComponentHashPolicyEvaluator.java +++ b/src/main/java/org/dependencytrack/policy/ComponentHashPolicyEvaluator.java @@ -21,8 +21,10 @@ import alpine.common.logging.Logger; import org.apache.commons.lang3.StringUtils; import org.cyclonedx.model.Hash; -import org.dependencytrack.model.Component; + import org.dependencytrack.model.Policy; +import org.dependencytrack.model.Component; + import org.dependencytrack.model.PolicyCondition; import org.json.JSONObject; diff --git a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java index cd4293c1b0..76d725d5fa 100644 --- a/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java +++ b/src/main/java/org/dependencytrack/resources/v1/NotificationPublisherResource.java @@ -24,11 +24,13 @@ import alpine.notification.NotificationLevel; import alpine.server.auth.PermissionRequired; import alpine.server.resources.AlpineResource; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import io.swagger.annotations.ApiParam; + import io.swagger.annotations.ApiResponse; +import io.swagger.annotations.ApiOperation; import io.swagger.annotations.ApiResponses; +import io.swagger.annotations.ApiParam; +import io.swagger.annotations.Api; + import io.swagger.annotations.Authorization; import org.dependencytrack.auth.Permissions; import org.dependencytrack.model.ConfigPropertyConstants; @@ -45,6 +47,8 @@ import javax.json.Json; import javax.json.JsonObject; import javax.validation.Validator; + + import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.FormParam; @@ -54,6 +58,7 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; + import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import java.io.IOException; diff --git a/src/main/java/org/dependencytrack/search/FuzzyVulnerableSoftwareSearchManager.java b/src/main/java/org/dependencytrack/search/FuzzyVulnerableSoftwareSearchManager.java index f1671200e2..dcd2bcd6e1 100644 --- a/src/main/java/org/dependencytrack/search/FuzzyVulnerableSoftwareSearchManager.java +++ b/src/main/java/org/dependencytrack/search/FuzzyVulnerableSoftwareSearchManager.java @@ -45,6 +45,7 @@ import us.springett.parsers.cpe.values.Part; import java.io.IOException; + import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; diff --git a/src/main/java/org/dependencytrack/tasks/EpssMirrorTask.java b/src/main/java/org/dependencytrack/tasks/EpssMirrorTask.java index 4968bfdf73..8c49fbac65 100644 --- a/src/main/java/org/dependencytrack/tasks/EpssMirrorTask.java +++ b/src/main/java/org/dependencytrack/tasks/EpssMirrorTask.java @@ -26,6 +26,7 @@ import alpine.notification.Notification; import alpine.notification.NotificationLevel; import org.apache.commons.io.FileUtils; +import org.apache.http.HttpStatus; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -146,7 +147,7 @@ private void doDownload(final String urlString) { final StatusLine status = response.getStatusLine(); final long end = System.currentTimeMillis(); metricDownloadTime += end - start; - if (status.getStatusCode() == 200) { + if (status.getStatusCode() == HttpStatus.SC_OK) { LOGGER.info("Downloading..."); try (InputStream in = response.getEntity().getContent()) { file = new File(outputDir, filename); diff --git a/src/main/java/org/dependencytrack/tasks/GitHubAdvisoryMirrorTask.java b/src/main/java/org/dependencytrack/tasks/GitHubAdvisoryMirrorTask.java index 85fb9c2625..118bcad5c2 100644 --- a/src/main/java/org/dependencytrack/tasks/GitHubAdvisoryMirrorTask.java +++ b/src/main/java/org/dependencytrack/tasks/GitHubAdvisoryMirrorTask.java @@ -29,12 +29,13 @@ import com.github.packageurl.PackageURLBuilder; import io.pebbletemplates.pebble.PebbleEngine; import io.pebbletemplates.pebble.template.PebbleTemplate; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONObject; import org.apache.commons.lang3.tuple.Pair; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; +import org.dependencytrack.common.HttpClientPool; import org.dependencytrack.event.GitHubAdvisoryMirrorEvent; import org.dependencytrack.event.IndexEvent; import org.dependencytrack.model.Cwe; @@ -51,6 +52,7 @@ import org.dependencytrack.parser.github.graphql.model.GitHubVulnerability; import org.dependencytrack.parser.github.graphql.model.PageableList; import org.dependencytrack.persistence.QueryManager; +import org.json.JSONObject; import java.io.IOException; import java.io.StringWriter; @@ -80,7 +82,7 @@ public class GitHubAdvisoryMirrorTask implements LoggableSubscriber { public GitHubAdvisoryMirrorTask() { try (final QueryManager qm = new QueryManager()) { final ConfigProperty enabled = qm.getConfigProperty(VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ENABLED.getGroupName(), VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ENABLED.getPropertyName()); - this.isEnabled = enabled != null && Boolean.valueOf(enabled.getPropertyValue()); + this.isEnabled = enabled != null && Boolean.parseBoolean(enabled.getPropertyValue()); final ConfigProperty accessToken = qm.getConfigProperty(VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ACCESS_TOKEN.getGroupName(), VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ACCESS_TOKEN.getPropertyName()); if (accessToken != null) { @@ -97,7 +99,11 @@ public void inform(final Event e) { if (this.accessToken != null) { final long start = System.currentTimeMillis(); LOGGER.info("Starting GitHub Advisory mirroring task"); - retrieveAdvisories(null); + try { + retrieveAdvisories(null); + } catch (IOException ex) { + handleRequestException(LOGGER, ex); + } final long end = System.currentTimeMillis(); LOGGER.info("GitHub Advisory mirroring complete"); LOGGER.info("Time spent (total): " + (end - start) + "ms"); @@ -123,68 +129,74 @@ private String generateQueryTemplate(final String advisoriesEndCursor) { } } - private void retrieveAdvisories(final String advisoriesEndCursor) { + private void retrieveAdvisories(final String advisoriesEndCursor) throws IOException { final String queryTemplate = generateQueryTemplate(advisoriesEndCursor); - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - final HttpResponse response = ui.post(GITHUB_GRAPHQL_URL) - .header("Authorization", "bearer " + accessToken) - .header("content-type", "application/json") - .header("accept", "application/json") - .body(new JSONObject().put("query", queryTemplate)) - .asJson(); - if (response.getStatus() < 200 || response.getStatus() > 299) { - LOGGER.error("An error was encountered retrieving advisories"); - LOGGER.error("HTTP Status : " + response.getStatus() + " " + response.getStatusText()); - LOGGER.debug(queryTemplate); - mirroredWithoutErrors = false; - } else { - GitHubSecurityAdvisoryParser parser = new GitHubSecurityAdvisoryParser(); - final PageableList pageableList = parser.parse(response.getBody().getObject()); - updateDatasource(pageableList.getAdvisories()); - if (pageableList.isHasNextPage()) { - retrieveAdvisories(pageableList.getEndCursor()); + HttpPost request = new HttpPost(GITHUB_GRAPHQL_URL); + request.addHeader("Authorization", "bearer " + accessToken); + request.addHeader("content-type", "application/json"); + request.addHeader("accept", "application/json"); + var jsonBody = new JSONObject(); + jsonBody.put("query", queryTemplate); + var stringEntity = new StringEntity(jsonBody.toString()); + request.setEntity(stringEntity); + try (CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + if (response.getStatusLine().getStatusCode() < HttpStatus.SC_OK || response.getStatusLine().getStatusCode() >= HttpStatus.SC_MULTIPLE_CHOICES) { + LOGGER.error("An error was encountered retrieving advisories with HTTP Status : " + response.getStatusLine().getStatusCode() + " " + response.getStatusLine().getReasonPhrase()); + LOGGER.debug(queryTemplate); + mirroredWithoutErrors = false; + } else { + var parser = new GitHubSecurityAdvisoryParser(); + String responseString = EntityUtils.toString(response.getEntity()); + var jsonObject = new JSONObject(responseString); + final PageableList pageableList = parser.parse(jsonObject); + updateDatasource(pageableList.getAdvisories()); + if (pageableList.isHasNextPage()) { + retrieveAdvisories(pageableList.getEndCursor()); + } + } + + if (mirroredWithoutErrors) { + Notification.dispatch(new Notification() + .scope(NotificationScope.SYSTEM) + .group(NotificationGroup.DATASOURCE_MIRRORING) + .title(NotificationConstants.Title.GITHUB_ADVISORY_MIRROR) + .content("Mirroring of GitHub Advisories completed successfully") + .level(NotificationLevel.INFORMATIONAL) + ); + } else { + Notification.dispatch(new Notification() + .scope(NotificationScope.SYSTEM) + .group(NotificationGroup.DATASOURCE_MIRRORING) + .title(NotificationConstants.Title.GITHUB_ADVISORY_MIRROR) + .content("An error occurred mirroring the contents of GitHub Advisories. Check log for details.") + .level(NotificationLevel.ERROR) + ); } - } - if (mirroredWithoutErrors) { - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.DATASOURCE_MIRRORING) - .title(NotificationConstants.Title.GITHUB_ADVISORY_MIRROR) - .content("Mirroring of GitHub Advisories completed successfully") - .level(NotificationLevel.INFORMATIONAL) - ); - } else { - Notification.dispatch(new Notification() - .scope(NotificationScope.SYSTEM) - .group(NotificationGroup.DATASOURCE_MIRRORING) - .title(NotificationConstants.Title.GITHUB_ADVISORY_MIRROR) - .content("An error occurred mirroring the contents of GitHub Advisories. Check log for details.") - .level(NotificationLevel.ERROR) - ); } } /** * Synchronizes the advisories that were downloaded with the internal Dependency-Track database. + * * @param advisories the results to synchronize */ void updateDatasource(final List advisories) { LOGGER.debug("Updating datasource with GitHub advisories"); try (QueryManager qm = new QueryManager()) { - for (final GitHubSecurityAdvisory advisory: advisories) { + for (final GitHubSecurityAdvisory advisory : advisories) { LOGGER.debug("Synchronizing GitHub advisory: " + advisory.getGhsaId()); final Vulnerability mappedVulnerability = mapAdvisoryToVulnerability(qm, advisory); final List vsListOld = qm.detach(qm.getVulnerableSoftwareByVulnId(mappedVulnerability.getSource(), mappedVulnerability.getVulnId())); final Vulnerability synchronizedVulnerability = qm.synchronizeVulnerability(mappedVulnerability, false); List vsList = new ArrayList<>(); - for (GitHubVulnerability ghvuln: advisory.getVulnerabilities()) { + for (GitHubVulnerability ghvuln : advisory.getVulnerabilities()) { final VulnerableSoftware vs = mapVulnerabilityToVulnerableSoftware(qm, ghvuln, advisory); if (vs != null) { vsList.add(vs); } - for (Pair identifier: advisory.getIdentifiers()) { + for (Pair identifier : advisory.getIdentifiers()) { if (identifier != null && identifier.getLeft() != null - && "CVE".equalsIgnoreCase(identifier.getLeft()) && identifier.getLeft().startsWith("CVE")) { + && "CVE" .equalsIgnoreCase(identifier.getLeft()) && identifier.getLeft().startsWith("CVE")) { LOGGER.debug("Updating vulnerability alias for " + advisory.getGhsaId()); final VulnerabilityAlias alias = new VulnerabilityAlias(); alias.setGhsaId(advisory.getGhsaId()); @@ -206,6 +218,7 @@ void updateDatasource(final List advisories) { /** * Helper method that maps an GitHub SecurityAdvisory object to a Dependency-Track vulnerability object. + * * @param advisory the GitHub SecurityAdvisory to map * @return a Dependency-Track Vulnerability object */ @@ -230,7 +243,7 @@ private Vulnerability mapAdvisoryToVulnerability(final QueryManager qm, final Gi //vuln.setVulnerableVersions(advisory.getVulnerableVersions()); //vuln.setPatchedVersions(advisory.getPatchedVersions()); if (advisory.getCwes() != null) { - for (int i=0; i=")) { versionStartIncluding = part.replace(">=", "").trim(); } else if (part.startsWith(">")) { @@ -314,15 +328,24 @@ private VulnerableSoftware mapVulnerabilityToVulnerableSoftware(final QueryManag private String mapGitHubEcosystemToPurlType(final String ecosystem) { switch (ecosystem.toUpperCase()) { - case "MAVEN": return PackageURL.StandardTypes.MAVEN; - case "RUST": return PackageURL.StandardTypes.CARGO; - case "PIP": return PackageURL.StandardTypes.PYPI; - case "RUBYGEMS": return PackageURL.StandardTypes.GEM; - case "GO": return PackageURL.StandardTypes.GOLANG; - case "NPM": return PackageURL.StandardTypes.NPM; - case "COMPOSER": return PackageURL.StandardTypes.COMPOSER; - case "NUGET": return PackageURL.StandardTypes.NUGET; - default: return null; + case "MAVEN": + return PackageURL.StandardTypes.MAVEN; + case "RUST": + return PackageURL.StandardTypes.CARGO; + case "PIP": + return PackageURL.StandardTypes.PYPI; + case "RUBYGEMS": + return PackageURL.StandardTypes.GEM; + case "GO": + return PackageURL.StandardTypes.GOLANG; + case "NPM": + return PackageURL.StandardTypes.NPM; + case "COMPOSER": + return PackageURL.StandardTypes.COMPOSER; + case "NUGET": + return PackageURL.StandardTypes.NUGET; + default: + return null; } } @@ -346,4 +369,14 @@ private PackageURL generatePurlFromGitHubVulnerability(final GitHubVulnerability return null; } + protected void handleRequestException(final Logger logger, final Exception e) { + logger.error("Request failure", e); + Notification.dispatch(new Notification() + .scope(NotificationScope.SYSTEM) + .group(NotificationGroup.ANALYZER) + .title(NotificationConstants.Title.ANALYZER_ERROR) + .content("An error occurred while communicating with a vulnerability intelligence source. Check log for details. " + e.getMessage()) + .level(NotificationLevel.ERROR) + ); + } } diff --git a/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java b/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java index 2d8ac0d329..42af52ffe0 100644 --- a/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java +++ b/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java @@ -27,6 +27,7 @@ import alpine.notification.NotificationLevel; import org.apache.commons.io.FileUtils; import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -223,7 +224,7 @@ private void doDownload(final String urlString, final ResourceType resourceType) final StatusLine status = response.getStatusLine(); final long end = System.currentTimeMillis(); metricDownloadTime += end - start; - if (status.getStatusCode() == 200) { + if (status.getStatusCode() == HttpStatus.SC_OK) { LOGGER.info("Downloading..."); try (InputStream in = response.getEntity().getContent()) { File temp = File.createTempFile(filename, null); diff --git a/src/main/java/org/dependencytrack/tasks/OsvDownloadTask.java b/src/main/java/org/dependencytrack/tasks/OsvDownloadTask.java index bfda8d533e..1d2be7353f 100644 --- a/src/main/java/org/dependencytrack/tasks/OsvDownloadTask.java +++ b/src/main/java/org/dependencytrack/tasks/OsvDownloadTask.java @@ -24,7 +24,8 @@ import alpine.model.ConfigProperty; import com.github.packageurl.MalformedPackageURLException; import com.github.packageurl.PackageURL; -import kong.unirest.json.JSONObject; +import org.apache.http.HttpStatus; +import org.json.JSONObject; import org.apache.http.StatusLine; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; @@ -108,7 +109,7 @@ public void inform(Event e) { HttpUriRequest request = new HttpGet(url); try (final CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { final StatusLine status = response.getStatusLine(); - if (status.getStatusCode() == 200) { + if (status.getStatusCode() == HttpStatus.SC_OK) { try (InputStream in = response.getEntity().getContent(); ZipInputStream zipInput = new ZipInputStream(in)) { unzipFolder(zipInput); @@ -352,7 +353,7 @@ public List getEcosystems() { HttpUriRequest request = new HttpGet(url); try (final CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { final StatusLine status = response.getStatusLine(); - if (status.getStatusCode() == 200) { + if (status.getStatusCode() == HttpStatus.SC_OK) { try (InputStream in = response.getEntity().getContent(); Scanner scanner = new Scanner(in, StandardCharsets.UTF_8)) { while (scanner.hasNextLine()) { diff --git a/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java b/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java index 86c6c217f7..0e0486eef3 100644 --- a/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java +++ b/src/main/java/org/dependencytrack/tasks/VulnDbSyncTask.java @@ -32,17 +32,16 @@ import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; import org.dependencytrack.parser.vulndb.ModelConverter; +import org.dependencytrack.parser.vulndb.VulnDbParser; +import org.dependencytrack.parser.vulndb.model.Product; +import org.dependencytrack.parser.vulndb.model.Results; +import org.dependencytrack.parser.vulndb.model.Vendor; +import org.dependencytrack.parser.vulndb.model.Version; import org.dependencytrack.persistence.QueryManager; import us.springett.parsers.cpe.Cpe; import us.springett.parsers.cpe.CpeParser; import us.springett.parsers.cpe.exceptions.CpeEncodingException; import us.springett.parsers.cpe.exceptions.CpeParsingException; -import us.springett.vulndbdatamirror.parser.VulnDbParser; -import us.springett.vulndbdatamirror.parser.model.CPE; -import us.springett.vulndbdatamirror.parser.model.Product; -import us.springett.vulndbdatamirror.parser.model.Results; -import us.springett.vulndbdatamirror.parser.model.Vendor; -import us.springett.vulndbdatamirror.parser.model.Version; import java.io.File; import java.io.IOException; @@ -54,7 +53,7 @@ * Subscriber task that performs synchronization with VulnDB mirrored data. * This task relies on an existing mirror generated from vulndb-data-mirror. The mirror must exist * in a 'vulndb' subdirectory of the Dependency-Track data directory. i.e. ~/dependency-track/vulndb - * + *

* https://github.com/stevespringett/vulndb-data-mirror * * @author Steve Springett @@ -85,7 +84,7 @@ public void inform(final Event e) { LOGGER.info("Parsing: " + file.getName()); final VulnDbParser parser = new VulnDbParser(); try { - final Results results = parser.parse(file, us.springett.vulndbdatamirror.parser.model.Vulnerability.class); + final Results results = parser.parse(file, org.dependencytrack.parser.vulndb.model.Vulnerability.class); updateDatasource(results); } catch (IOException ex) { LOGGER.error("An error occurred while parsing VulnDB payload: " + file.getName(), ex); @@ -116,14 +115,15 @@ public void inform(final Event e) { /** * Synchronizes the VulnDB vulnerabilities with the internal Dependency-Track database. + * * @param results the results to synchronize */ private void updateDatasource(final Results results) { LOGGER.info("Updating datasource with VulnDB vulnerabilities"); try (QueryManager qm = new QueryManager()) { - for (final Object o: results.getResults()) { - if (o instanceof us.springett.vulndbdatamirror.parser.model.Vulnerability) { - final us.springett.vulndbdatamirror.parser.model.Vulnerability vulnDbVuln = (us.springett.vulndbdatamirror.parser.model.Vulnerability)o; + for (final Object o : results.getResults()) { + if (o instanceof org.dependencytrack.parser.vulndb.model.Vulnerability) { + final org.dependencytrack.parser.vulndb.model.Vulnerability vulnDbVuln = (org.dependencytrack.parser.vulndb.model.Vulnerability) o; final org.dependencytrack.model.Vulnerability vulnerability = ModelConverter.convert(qm, vulnDbVuln); final Vulnerability synchronizeVulnerability = qm.synchronizeVulnerability(vulnerability, false); final List vsListOld = qm.detach(qm.getVulnerableSoftwareByVulnId(synchronizeVulnerability.getSource(), synchronizeVulnerability.getVulnId())); @@ -138,28 +138,28 @@ private void updateDatasource(final Results results) { } public static List parseCpes(final QueryManager qm, final Vulnerability vulnerability, - final us.springett.vulndbdatamirror.parser.model.Vulnerability vulnDbVuln) { + final org.dependencytrack.parser.vulndb.model.Vulnerability vulnDbVuln) { // cpe:2.3:a:belavier_commerce:abantecart:1.2.8:*:*:*:*:*:*:* final List vsList = new ArrayList<>(); - if (vulnDbVuln.getVendors() != null) { - for (Vendor vendor: vulnDbVuln.getVendors()) { - if (vendor.getProducts() != null) { - for (Product product: vendor.getProducts()) { - if (product.getVersions() != null) { - for (Version version: product.getVersions()) { + if (vulnDbVuln.vendors() != null) { + for (Vendor vendor : vulnDbVuln.vendors()) { + if (vendor.products() != null) { + for (Product product : vendor.products()) { + if (product.versions() != null) { + for (Version version : product.versions()) { if (version != null) { - if (version.isAffected()) { - if (version.getCpes() != null) { - for (CPE cpeObject : version.getCpes()) { + if (version.affected()) { + if (version.cpes() != null) { + for (org.dependencytrack.parser.vulndb.model.Cpe cpeObject : version.cpes()) { try { - final Cpe cpe = CpeParser.parse(cpeObject.getCpe(), true); + final Cpe cpe = CpeParser.parse(cpeObject.cpe(), true); final VulnerableSoftware vs = generateVulnerableSoftware(qm, cpe, vulnerability); if (vs != null) { vsList.add(vs); } } catch (CpeParsingException e) { // Normally, this would be logged to error, however, VulnDB contains a lot of invalid CPEs - LOGGER.debug("An error occurred parsing " + cpeObject.getCpe(), e); + LOGGER.debug("An error occurred parsing " + cpeObject.cpe(), e); } } } @@ -190,7 +190,6 @@ private static VulnerableSoftware generateVulnerableSoftware(final QueryManager vs.setVersionStartExcluding(null); vs.setVersionStartIncluding(null); vs = qm.persist(vs); - //Event.dispatch(new IndexEvent(IndexEvent.Action.CREATE, qm.detach(VulnerableSoftware.class, vs.getId()))); return vs; } catch (CpeParsingException | CpeEncodingException e) { LOGGER.warn("An error occurred while parsing: " + cpe.toCpe23FS() + " - The CPE is invalid and will be discarded."); diff --git a/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java index c601a8fe22..9afadbe8c6 100644 --- a/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/VulnerabilityAnalysisTask.java @@ -26,21 +26,21 @@ import org.dependencytrack.event.OssIndexAnalysisEvent; import org.dependencytrack.event.PortfolioVulnerabilityAnalysisEvent; import org.dependencytrack.event.ProjectMetricsUpdateEvent; +import org.dependencytrack.event.SnykAnalysisEvent; import org.dependencytrack.event.VulnDbAnalysisEvent; import org.dependencytrack.event.VulnerabilityAnalysisEvent; -import org.dependencytrack.event.SnykAnalysisEvent; -import org.dependencytrack.model.VulnerabilityAnalysisLevel; import org.dependencytrack.model.Component; import org.dependencytrack.model.Project; +import org.dependencytrack.model.VulnerabilityAnalysisLevel; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.policy.PolicyEngine; +import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.dependencytrack.tasks.scanners.CacheableScanTask; import org.dependencytrack.tasks.scanners.InternalAnalysisTask; import org.dependencytrack.tasks.scanners.OssIndexAnalysisTask; import org.dependencytrack.tasks.scanners.ScanTask; -import org.dependencytrack.tasks.scanners.VulnDbAnalysisTask; -import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.dependencytrack.tasks.scanners.SnykAnalysisTask; +import org.dependencytrack.tasks.scanners.VulnDbAnalysisTask; import java.time.Duration; import java.time.Instant; @@ -88,7 +88,7 @@ public void inform(final Event e) { .stream() .map(Project::getUuid) .collect(Collectors.toList()); - for (final UUID projectUuid: projectUuids) { + for (final UUID projectUuid : projectUuids) { final Project project = qm.getObjectByUuid(Project.class, projectUuid); if (project == null) continue; final List components = qm.getAllComponents(project); @@ -142,7 +142,7 @@ private void performPolicyEvaluation(Project project, List components private void inspectComponentReadiness(final Component component, final ScanTask scanTask, final List candidates) { if (scanTask.isCapable(component)) { if (scanTask.getClass().isAssignableFrom(CacheableScanTask.class)) { - final CacheableScanTask cacheableScanTask = (CacheableScanTask)scanTask; + final CacheableScanTask cacheableScanTask = (CacheableScanTask) scanTask; if (cacheableScanTask.shouldAnalyze(component.getPurl())) { candidates.add(component); } else { @@ -158,7 +158,7 @@ private void performAnalysis(final Subscriber scanTask, final VulnerabilityAnaly final AnalyzerIdentity analyzerIdentity, final Event eventType) { Instant start = Instant.now(); event.setVulnerabilityAnalysisLevel(VulnerabilityAnalysisLevel.PERIODIC_ANALYSIS); - if(eventType instanceof VulnerabilityAnalysisEvent) { + if (eventType instanceof VulnerabilityAnalysisEvent) { event.setVulnerabilityAnalysisLevel(VulnerabilityAnalysisLevel.BOM_UPLOAD_ANALYSIS); } if (CollectionUtils.isNotEmpty(event.getComponents())) { @@ -175,7 +175,7 @@ private void performAnalysis(final Subscriber scanTask, final VulnerabilityAnaly event.getComponents().forEach(c -> c.setCacheResult(null)); Instant end = Instant.now(); Duration timeElapsed = Duration.between(start, end); - LOGGER.debug("Time taken by perform analysis task by "+analyzerIdentity.name()+" : "+ timeElapsed.toMillis() +" milliseconds"); + LOGGER.debug("Time taken by perform analysis task by " + analyzerIdentity.name() + " : " + timeElapsed.toMillis() + " milliseconds"); } } } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/AbstractMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/AbstractMetaAnalyzer.java index bca39977dd..5e0cee6ec2 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/AbstractMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/AbstractMetaAnalyzer.java @@ -22,10 +22,19 @@ import alpine.notification.Notification; import alpine.notification.NotificationLevel; import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.utils.URIBuilder; +import org.dependencytrack.common.HttpClientPool; import org.dependencytrack.model.Component; import org.dependencytrack.notification.NotificationConstants; import org.dependencytrack.notification.NotificationGroup; import org.dependencytrack.notification.NotificationScope; +import org.dependencytrack.util.HttpUtil; + +import java.io.IOException; +import java.net.URISyntaxException; /** * Base abstract class that all IMetaAnalyzer implementations should likely extend. @@ -40,7 +49,6 @@ public abstract class AbstractMetaAnalyzer implements IMetaAnalyzer { protected String username; protected String password; - /** * {@inheritDoc} */ @@ -84,4 +92,20 @@ protected void handleRequestException(final Logger logger, final Exception e) { ); } + protected CloseableHttpResponse processHttpRequest(String url) throws IOException { + final Logger logger = Logger.getLogger(getClass()); + try { + URIBuilder uriBuilder = new URIBuilder(url); + final HttpUriRequest request = new HttpGet(uriBuilder.build().toString()); + request.addHeader("accept", "application/json"); + if (username != null || password != null) { + request.addHeader("Authorization", HttpUtil.basicAuthHeaderValue(username, password)); + } + return HttpClientPool.getClient().execute(request); + }catch (URISyntaxException ex){ + handleRequestException(logger, ex); + return null; + } + } + } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/CargoMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/CargoMetaAnalyzer.java index 43381f6c48..d1a9f80a79 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/CargoMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/CargoMetaAnalyzer.java @@ -20,19 +20,18 @@ import alpine.common.logging.Logger; import com.github.packageurl.PackageURL; -import kong.unirest.GetRequest; -import kong.unirest.HttpRequest; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestException; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; -import org.dependencytrack.common.UnirestFactory; +import org.json.JSONArray; +import org.json.JSONObject; +import org.apache.http.HttpEntity; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.util.DateUtil; +import java.io.IOException; + /** * An IMetaAnalyzer implementation that supports Cargo via crates.io compatible repos * @@ -67,35 +66,30 @@ public RepositoryType supportedRepositoryType() { * {@inheritDoc} */ public MetaModel analyze(final Component component) { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); final MetaModel meta = new MetaModel(component); if (component.getPurl() != null) { final String url = String.format(baseUrl + API_URL, component.getPurl().getName()); - try { - final HttpRequest request = ui.get(url) - .header("accept", "application/json"); - if (username != null || password != null) { - request.basicAuth(username, password); - } - final HttpResponse response = request.asJson(); - - if (response.getStatus() == 200) { - if (response.getBody() != null && response.getBody().getObject() != null) { - final JSONObject crate = response.getBody().getObject().optJSONObject("crate"); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + final HttpEntity entity = response.getEntity(); + if (entity != null) { + String responseString = EntityUtils.toString(entity); + var jsonObject = new JSONObject(responseString); + final JSONObject crate = jsonObject.optJSONObject("crate"); if (crate != null) { final String latest = crate.getString("newest_version"); meta.setLatestVersion(latest); } - final JSONArray versions = response.getBody().getObject().optJSONArray("versions"); + final JSONArray versions = jsonObject.optJSONArray("versions"); if (versions != null) { - for (int i=0; i request = ui.get(url) - .header("accept", "application/json"); - if (username != null || password != null) { - request.basicAuth(username, password); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { + handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component); + return meta; } - final HttpResponse response = request.asJson(); - - if (response.getStatus() != 200) { - handleUnexpectedHttpResponse(LOGGER, url, response.getStatus(), response.getStatusText(), component); + if (response.getEntity().getContent() == null) { return meta; } - - if (response.getBody() == null || response.getBody().getObject() == null) { + String jsonString = EntityUtils.toString(response.getEntity()); + if (jsonString.equalsIgnoreCase("")) { return meta; } - + if (jsonString.equalsIgnoreCase("{}")) { + return meta; + } + JSONObject jsonObject = new JSONObject(jsonString); final String expectedResponsePackage = component.getPurl().getNamespace() + "/" + component.getPurl().getName(); - final JSONObject responsePackages = response - .getBody() - .getObject() + final JSONObject responsePackages = jsonObject .getJSONObject("packages"); if (!responsePackages.has(expectedResponsePackage)) { // the package no longer exists - like this one: https://repo.packagist.org/p/magento/adobe-ims.json @@ -121,8 +114,7 @@ public MetaModel analyze(final Component component) { final String version_normalized = composerPackage.getJSONObject(key).getString("version_normalized"); ComparableVersion currentComparableVersion = new ComparableVersion(version_normalized); - if ( currentComparableVersion.compareTo(latestVersion) < 0) - { + if (currentComparableVersion.compareTo(latestVersion) < 0) { // smaller version can be skipped return; } @@ -138,10 +130,11 @@ public MetaModel analyze(final Component component) { LOGGER.warn("An error occurred while parsing upload time", e); } }); - } catch (UnirestException e) { - handleRequestException(LOGGER, e); + } catch (IOException ex) { + handleRequestException(LOGGER, ex); } + return meta; } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/GemMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/GemMetaAnalyzer.java index 3747e29ebd..eeb9cdf59d 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/GemMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/GemMetaAnalyzer.java @@ -20,15 +20,14 @@ import alpine.common.logging.Logger; import com.github.packageurl.PackageURL; -import kong.unirest.GetRequest; -import kong.unirest.HttpRequest; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestException; -import kong.unirest.UnirestInstance; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; +import org.json.JSONObject; + +import java.io.IOException; /** * An IMetaAnalyzer implementation that supports Ruby Gems. @@ -64,29 +63,24 @@ public RepositoryType supportedRepositoryType() { * {@inheritDoc} */ public MetaModel analyze(final Component component) { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); final MetaModel meta = new MetaModel(component); if (component.getPurl() != null) { final String url = String.format(baseUrl + API_URL, component.getPurl().getName()); - try { - final HttpRequest request = ui.get(url) - .header("accept", "application/json"); - if (username != null || password != null) { - request.basicAuth(username, password); - } - final HttpResponse response = request.asJson(); - - if (response.getStatus() == 200) { - if (response.getBody() != null && response.getBody().getObject() != null) { - final String latest = response.getBody().getObject().getString("version"); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK){ + if(response.getEntity()!=null){ + String responseString = EntityUtils.toString(response.getEntity()); + var jsonObject = new JSONObject(responseString); + final String latest = jsonObject.getString("version"); meta.setLatestVersion(latest); } } else { - handleUnexpectedHttpResponse(LOGGER, url, response.getStatus(), response.getStatusText(), component); + handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component); } - } catch (UnirestException e) { - handleRequestException(LOGGER, e); + }catch (IOException ex){ + handleRequestException(LOGGER, ex); } + } return meta; } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/GoModulesMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/GoModulesMetaAnalyzer.java index 7bbdf55ca9..d6f8eaaa8d 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/GoModulesMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/GoModulesMetaAnalyzer.java @@ -20,18 +20,15 @@ import alpine.common.logging.Logger; import com.github.packageurl.PackageURL; -import kong.unirest.GetRequest; -import kong.unirest.HttpRequest; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestException; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONObject; import org.apache.commons.lang3.StringUtils; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; +import org.json.JSONObject; +import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -66,21 +63,13 @@ public MetaModel analyze(final Component component) { if (component.getPurl() == null || component.getPurl().getNamespace() == null) { return meta; } - - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); final String url = String.format(baseUrl + API_URL, caseEncode(component.getPurl().getNamespace()), caseEncode(component.getPurl().getName())); - try { - final HttpRequest request = ui.get(url) - .header("accept", "application/json"); - if (username != null || password != null) { - request.basicAuth(username, password); - } - final HttpResponse response = request.asJson(); - - if (response.getStatus() == 200) { - if (response.getBody() != null && response.getBody().getObject() != null) { - final JSONObject responseJson = response.getBody().getObject(); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if (response.getEntity()!=null) { + String responseString = EntityUtils.toString(response.getEntity()); + final var responseJson = new JSONObject(responseString); meta.setLatestVersion(responseJson.getString("Version")); // Module versions are prefixed with "v" in the Go ecosystem. @@ -99,9 +88,9 @@ public MetaModel analyze(final Component component) { } } } else { - handleUnexpectedHttpResponse(LOGGER, url, response.getStatus(), response.getStatusText(), component); + handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component); } - } catch (UnirestException | ParseException e) { + } catch (IOException | ParseException e) { handleRequestException(LOGGER, e); } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/HexMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/HexMetaAnalyzer.java index 0d5b23dd6b..a3a0f747ed 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/HexMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/HexMetaAnalyzer.java @@ -20,18 +20,15 @@ import alpine.common.logging.Logger; import com.github.packageurl.PackageURL; -import kong.unirest.GetRequest; -import kong.unirest.HttpRequest; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestException; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; +import org.json.JSONArray; +import org.json.JSONObject; +import java.io.IOException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -71,7 +68,6 @@ public RepositoryType supportedRepositoryType() { * {@inheritDoc} */ public MetaModel analyze(final Component component) { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); final MetaModel meta = new MetaModel(component); if (component.getPurl() != null) { @@ -83,17 +79,12 @@ public MetaModel analyze(final Component component) { } final String url = String.format(baseUrl + API_URL, packageName); - try { - final HttpRequest request = ui.get(url) - .header("accept", "application/json"); - if (username != null || password != null) { - request.basicAuth(username, password); - } - final HttpResponse response = request.asJson(); - - if (response.getStatus() == 200) { - if (response.getBody() != null && response.getBody().getObject() != null) { - final JSONArray releasesArray = response.getBody().getObject().getJSONArray("releases"); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if (response.getEntity()!=null) { + String responseString = EntityUtils.toString(response.getEntity()); + var jsonObject = new JSONObject(responseString); + final JSONArray releasesArray = jsonObject.getJSONArray("releases"); if (releasesArray.length() > 0) { // The first one in the array is always the latest version final JSONObject release = releasesArray.getJSONObject(0); @@ -112,9 +103,9 @@ public MetaModel analyze(final Component component) { } } } else { - handleUnexpectedHttpResponse(LOGGER, url, response.getStatus(), response.getStatusText(), component); + handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component); } - } catch (UnirestException e) { + } catch (IOException e) { handleRequestException(LOGGER, e); } } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/MavenMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/MavenMetaAnalyzer.java index f3629c618e..dd2c2193e1 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/MavenMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/MavenMetaAnalyzer.java @@ -21,15 +21,11 @@ import alpine.common.logging.Logger; import com.github.packageurl.PackageURL; import org.apache.http.HttpEntity; -import org.apache.http.StatusLine; +import org.apache.http.HttpStatus; import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpUriRequest; -import org.dependencytrack.common.HttpClientPool; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; import org.dependencytrack.util.DateUtil; -import org.dependencytrack.util.HttpUtil; import org.dependencytrack.util.XmlUtil; import org.w3c.dom.Document; import org.xml.sax.SAXException; @@ -81,44 +77,36 @@ public MetaModel analyze(final Component component) { if (component.getPurl() != null) { final String mavenGavUrl = component.getPurl().getNamespace().replaceAll("\\.", "/") + "/" + component.getPurl().getName(); final String url = String.format(baseUrl + REPO_METADATA_URL, mavenGavUrl); - try { - final HttpUriRequest request = new HttpGet(url); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + final HttpEntity entity = response.getEntity(); + if (entity != null) { + try (InputStream in = entity.getContent()) { + final Document document = XmlUtil.buildSecureDocumentBuilder().parse(in); + final var xpathFactory = XPathFactory.newInstance(); + final XPath xpath = xpathFactory.newXPath(); - if (username != null || password != null) { - request.setHeader("Authorization", HttpUtil.basicAuthHeaderValue(username, password)); - } - - try (final CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { - final StatusLine status = response.getStatusLine(); - if (status.getStatusCode() == 200) { - final HttpEntity entity = response.getEntity(); - if (entity != null) { - try (InputStream in = entity.getContent()) { - final Document document = XmlUtil.buildSecureDocumentBuilder().parse(in); - final XPathFactory xpathFactory = XPathFactory.newInstance(); - final XPath xpath = xpathFactory.newXPath(); + final XPathExpression releaseExpression = xpath.compile("/metadata/versioning/release"); + final XPathExpression latestExpression = xpath.compile("/metadata/versioning/latest"); + final var release = (String) releaseExpression.evaluate(document, XPathConstants.STRING); + final String latest = (String) latestExpression.evaluate(document, XPathConstants.STRING); - final XPathExpression releaseExpression = xpath.compile("/metadata/versioning/release"); - final XPathExpression latestExpression = xpath.compile("/metadata/versioning/latest"); - final String release = (String) releaseExpression.evaluate(document, XPathConstants.STRING); - final String latest = (String) latestExpression.evaluate(document, XPathConstants.STRING); + final XPathExpression lastUpdatedExpression = xpath.compile("/metadata/versioning/lastUpdated"); + final var lastUpdated = (String) lastUpdatedExpression.evaluate(document, XPathConstants.STRING); - final XPathExpression lastUpdatedExpression = xpath.compile("/metadata/versioning/lastUpdated"); - final String lastUpdated = (String) lastUpdatedExpression.evaluate(document, XPathConstants.STRING); - - meta.setLatestVersion(release != null ? release: latest); - if (lastUpdated != null) { - meta.setPublishedTimestamp(DateUtil.parseDate(lastUpdated)); - } + meta.setLatestVersion(release != null ? release : latest); + if (lastUpdated != null) { + meta.setPublishedTimestamp(DateUtil.parseDate(lastUpdated)); } } - } else { - handleUnexpectedHttpResponse(LOGGER, url, status.getStatusCode(), status.getReasonPhrase(), component); } + } else { + handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component); } } catch (IOException | ParserConfigurationException | SAXException | XPathExpressionException e) { handleRequestException(LOGGER, e); } + } return meta; } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NpmMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NpmMetaAnalyzer.java index 03ccf30bfc..0186f8af68 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/NpmMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/NpmMetaAnalyzer.java @@ -20,15 +20,14 @@ import alpine.common.logging.Logger; import com.github.packageurl.PackageURL; -import kong.unirest.GetRequest; -import kong.unirest.HttpRequest; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestException; -import kong.unirest.UnirestInstance; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; +import org.json.JSONObject; + +import java.io.IOException; /** * An IMetaAnalyzer implementation that supports NPM. @@ -64,7 +63,6 @@ public RepositoryType supportedRepositoryType() { * {@inheritDoc} */ public MetaModel analyze(final Component component) { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); final MetaModel meta = new MetaModel(component); if (component.getPurl() != null) { @@ -76,25 +74,20 @@ public MetaModel analyze(final Component component) { } final String url = String.format(baseUrl + API_URL, packageName); - try { - final HttpRequest request = ui.get(url) - .header("accept", "application/json"); - if (username != null || password != null) { - request.basicAuth(username, password); - } - final HttpResponse response = request.asJson(); - - if (response.getStatus() == 200) { - if (response.getBody() != null && response.getBody().getObject() != null) { - final String latest = response.getBody().getObject().optString("latest"); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if (response.getEntity()!=null) { + String responseString = EntityUtils.toString(response.getEntity()); + var jsonObject = new JSONObject(responseString); + final String latest = jsonObject.optString("latest"); if (latest != null) { meta.setLatestVersion(latest); } } } else { - handleUnexpectedHttpResponse(LOGGER, url, response.getStatus(), response.getStatusText(), component); + handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component); } - } catch (UnirestException e) { + } catch (IOException e) { handleRequestException(LOGGER, e); } } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzer.java index bd0333dc59..dba7b99ddd 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzer.java @@ -20,19 +20,16 @@ import alpine.common.logging.Logger; import com.github.packageurl.PackageURL; -import kong.unirest.GetRequest; -import kong.unirest.HttpRequest; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestException; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; import org.apache.maven.artifact.versioning.ComparableVersion; -import org.dependencytrack.common.UnirestFactory; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; +import org.json.JSONArray; +import org.json.JSONObject; +import java.io.IOException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -107,19 +104,20 @@ public MetaModel analyze(final Component component) { private boolean performVersionCheck(final MetaModel meta, final Component component) { final String url = String.format(versionQueryUrl, component.getPurl().getName().toLowerCase()); - try { - final HttpResponse response = httpGet(url); - if (response.getStatus() == 200) { - if (response.getBody() != null && response.getBody().getObject() != null) { - final JSONArray versions = response.getBody().getObject().getJSONArray("versions"); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if (response.getEntity() != null) { + String responseString = EntityUtils.toString(response.getEntity()); + var jsonObject = new JSONObject(responseString); + final JSONArray versions = jsonObject.getJSONArray("versions"); final String latest = findLatestVersion(versions); // get the last version in the array meta.setLatestVersion(latest); } return true; } else { - handleUnexpectedHttpResponse(LOGGER, url, response.getStatus(), response.getStatusText(), component); + handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component); } - } catch (UnirestException e) { + } catch (IOException e) { handleRequestException(LOGGER, e); } return false; @@ -142,33 +140,25 @@ private String findLatestVersion(JSONArray versions) { return latestVersion.toString(); } - private HttpResponse httpGet(String url) { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - final HttpRequest request = ui.get(url).header("accept", "application/json"); - - if (username != null || password != null) { - request.basicAuth(username, password); - } - - return request.asJson(); - } - private boolean performLastPublishedCheck(final MetaModel meta, final Component component) { final String url = String.format(registrationUrl, component.getPurl().getName().toLowerCase(), meta.getLatestVersion()); - try { - final HttpResponse response = httpGet(url); - if (response.getStatus() == 200) { - if (response.getBody() != null && response.getBody().getObject() != null) { - final String updateTime = response.getBody().getObject().optString("published", null); - if (updateTime != null) { - meta.setPublishedTimestamp(parseUpdateTime(updateTime)); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if (response.getEntity() != null) { + String stringResponse = EntityUtils.toString(response.getEntity()); + if (!stringResponse.equalsIgnoreCase("") && !stringResponse.equalsIgnoreCase("{}")) { + JSONObject jsonResponse = new JSONObject(stringResponse); + final String updateTime = jsonResponse.optString("published", null); + if (updateTime != null) { + meta.setPublishedTimestamp(parseUpdateTime(updateTime)); + } + return true; } } - return true; } else { - handleUnexpectedHttpResponse(LOGGER, url, response.getStatus(), response.getStatusText(), component); + handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component); } - } catch (UnirestException e) { + } catch (IOException e) { handleRequestException(LOGGER, e); } return false; @@ -177,17 +167,22 @@ private boolean performLastPublishedCheck(final MetaModel meta, final Component private void initializeEndpoints() { final String url = baseUrl + INDEX_URL; try { - final HttpResponse response = httpGet(url); - if (response.getStatus() == 200 && response.getBody() != null && response.getBody().getObject() != null) { - final JSONArray resources = response.getBody().getObject().getJSONArray("resources"); - final JSONObject packageBaseResource = findResourceByType(resources, "PackageBaseAddress"); - final JSONObject registrationsBaseResource = findResourceByType(resources, "RegistrationsBaseUrl"); - if (packageBaseResource != null && registrationsBaseResource != null) { - versionQueryUrl = packageBaseResource.getString("@id") + "%s/index.json"; - registrationUrl = registrationsBaseResource.getString("@id") + "%s/%s.json"; + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if(response.getEntity()!=null){ + String responseString = EntityUtils.toString(response.getEntity()); + JSONObject responseJson = new JSONObject(responseString); + final JSONArray resources = responseJson.getJSONArray("resources"); + final JSONObject packageBaseResource = findResourceByType(resources, "PackageBaseAddress"); + final JSONObject registrationsBaseResource = findResourceByType(resources, "RegistrationsBaseUrl"); + if (packageBaseResource != null && registrationsBaseResource != null) { + versionQueryUrl = packageBaseResource.getString("@id") + "%s/index.json"; + registrationUrl = registrationsBaseResource.getString("@id") + "%s/%s.json"; + } + } } } - } catch (UnirestException e) { + } catch (IOException e) { handleRequestException(LOGGER, e); } } diff --git a/src/main/java/org/dependencytrack/tasks/repositories/PypiMetaAnalyzer.java b/src/main/java/org/dependencytrack/tasks/repositories/PypiMetaAnalyzer.java index bddda44aad..484571bd52 100644 --- a/src/main/java/org/dependencytrack/tasks/repositories/PypiMetaAnalyzer.java +++ b/src/main/java/org/dependencytrack/tasks/repositories/PypiMetaAnalyzer.java @@ -20,18 +20,15 @@ import alpine.common.logging.Logger; import com.github.packageurl.PackageURL; -import kong.unirest.GetRequest; -import kong.unirest.HttpRequest; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestException; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; -import org.dependencytrack.common.UnirestFactory; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; import org.dependencytrack.model.Component; import org.dependencytrack.model.RepositoryType; +import org.json.JSONArray; +import org.json.JSONObject; +import java.io.IOException; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; @@ -71,25 +68,19 @@ public RepositoryType supportedRepositoryType() { * {@inheritDoc} */ public MetaModel analyze(final Component component) { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); final MetaModel meta = new MetaModel(component); if (component.getPurl() != null) { final String url = String.format(baseUrl + API_URL, component.getPurl().getName()); - try { - final HttpRequest request = ui.get(url) - .header("accept", "application/json"); - if (username != null || password != null) { - request.basicAuth(username, password); - } - final HttpResponse response = request.asJson(); - - if (response.getStatus() == 200) { - if (response.getBody() != null && response.getBody().getObject() != null) { - final JSONObject info = response.getBody().getObject().getJSONObject("info"); + try (final CloseableHttpResponse response = processHttpRequest(url)) { + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + if (response.getEntity() != null) { + String stringResponse = EntityUtils.toString(response.getEntity()); + JSONObject jsonObject = new JSONObject(stringResponse); + final JSONObject info = jsonObject.getJSONObject("info"); final String latest = info.optString("version", null); if (latest != null) { meta.setLatestVersion(latest); - final JSONObject releases = response.getBody().getObject().getJSONObject("releases"); + final JSONObject releases = jsonObject.getJSONObject("releases"); final JSONArray latestArray = releases.getJSONArray(latest); if (latestArray.length() > 0) { final JSONObject release = latestArray.getJSONObject(0); @@ -107,12 +98,12 @@ public MetaModel analyze(final Component component) { } } } else { - handleUnexpectedHttpResponse(LOGGER, url, response.getStatus(), response.getStatusText(), component); + handleUnexpectedHttpResponse(LOGGER, url, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase(), component); } - } catch (UnirestException e) { + } catch (IOException e) { handleRequestException(LOGGER, e); } } return meta; } -} +} \ No newline at end of file diff --git a/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java b/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java index 65c9156025..bf8ca516a2 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/BaseComponentAnalyzerTask.java @@ -147,7 +147,7 @@ protected void handleUnexpectedHttpResponse(final Logger logger, String url, fin ); } - protected void handleRequestException(final Logger logger, final Exception e) { + protected void handleRequestException(final Logger logger, final Throwable e) { logger.error("Request failure", e); Notification.dispatch(new Notification() .scope(NotificationScope.SYSTEM) diff --git a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java index c21709670d..2f10f1dd3e 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java @@ -33,17 +33,17 @@ import io.github.resilience4j.retry.Retry; import io.github.resilience4j.retry.RetryConfig; import io.github.resilience4j.retry.RetryRegistry; -import kong.unirest.HttpRequestWithBody; -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestException; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONObject; import org.apache.commons.collections4.CollectionUtils; +import org.apache.http.HttpEntity; import org.apache.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.util.EntityUtils; import org.dependencytrack.common.ConfigKey; +import org.dependencytrack.common.HttpClientPool; import org.dependencytrack.common.ManagedHttpClientFactory; -import org.dependencytrack.common.UnirestFactory; import org.dependencytrack.event.OssIndexAnalysisEvent; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; @@ -56,12 +56,15 @@ import org.dependencytrack.parser.ossindex.model.ComponentReport; import org.dependencytrack.parser.ossindex.model.ComponentReportVulnerability; import org.dependencytrack.persistence.QueryManager; +import org.dependencytrack.util.HttpUtil; import org.dependencytrack.util.NotificationUtil; +import org.json.JSONObject; import us.springett.cvss.Cvss; import us.springett.cvss.CvssV2; import us.springett.cvss.CvssV3; import us.springett.cvss.Score; +import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -110,7 +113,7 @@ public class OssIndexAnalysisTask extends BaseComponentAnalyzerTask implements C public AnalyzerIdentity getAnalyzerIdentity() { return AnalyzerIdentity.OSSINDEX_ANALYZER; } - + /** * {@inheritDoc} */ @@ -141,7 +144,7 @@ public void inform(final Event e) { } } } - final OssIndexAnalysisEvent event = (OssIndexAnalysisEvent)e; + final var event = (OssIndexAnalysisEvent) e; LOGGER.info("Starting Sonatype OSS Index analysis task"); vulnerabilityAnalysisLevel = event.getVulnerabilityAnalysisLevel(); if (event.getComponents().size() > 0) { @@ -184,6 +187,7 @@ public void applyAnalysisFromCache(final Component component) { /** * Analyzes a list of Components. + * * @param components a list of Components */ public void analyze(final List components) { @@ -202,11 +206,13 @@ public void analyze(final List components) { final JSONObject json = new JSONObject(); json.put("coordinates", coordinates); try { - final List report = ossIndexRetryer.executeSupplier(() -> submit(json)); + final List report = ossIndexRetryer.executeCheckedSupplier(() -> submit(json)); processResults(report, paginatedList); - } catch (UnirestException e) { - handleRequestException(LOGGER, e); + } catch (Throwable ex) { + handleRequestException(LOGGER, ex); + return; } + LOGGER.info("Analyzing " + coordinates.size() + " component(s)"); } paginatedComponents.nextPage(); @@ -218,15 +224,16 @@ public void analyze(final List components) { * HTTP POST is used and PackageURL is specified that contains qualifiers (and possibly a subpath). * Therefore, this method will return a String representation of a PackageURL without qualifier * or subpath. - * + *

* Additionally, as of October 2021, versions prefixed with "v" (as commonly done in the Go and PHP ecosystems) * are triggering a bug in OSS Index that causes all vulnerabilities for the given component to be returned, * not just the ones for the requested version: https://github.com/OSSIndex/vulns/issues/129#issuecomment-740666614 * As a result, this method will remove "v" prefixes from versions. - * + *

* This method should be removed at a future date when OSSIndex resolves the issues. - * + *

* TODO: Delete this method and workaround for OSSIndex bugs once Sonatype resolves them. + * * @since 3.4.0 */ @Deprecated @@ -248,29 +255,33 @@ private static String minimizePurl(final PackageURL purl) { /** * Submits the payload to the Sonatype OSS Index service */ - private List submit(final JSONObject payload) throws UnirestException { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - final HttpRequestWithBody request = ui.post(API_BASE_URL) - .header(HttpHeaders.ACCEPT, "application/json") - .header(HttpHeaders.CONTENT_TYPE, "application/json") - .header(HttpHeaders.USER_AGENT, ManagedHttpClientFactory.getUserAgent()); + private List submit(final JSONObject payload) throws IOException { + HttpPost request = new HttpPost(API_BASE_URL); + request.addHeader(HttpHeaders.ACCEPT, "application/json"); + request.addHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + request.addHeader(HttpHeaders.USER_AGENT, ManagedHttpClientFactory.getUserAgent()); + request.setEntity(new StringEntity(payload.toString())); if (apiUsername != null && apiToken != null) { - request.basicAuth(apiUsername, apiToken); + request.addHeader("Authorization", HttpUtil.basicAuthHeaderValue(apiUsername, apiToken)); } - final HttpResponse jsonResponse = request.body(payload).asJson(); - if (jsonResponse.getStatus() == 200) { - final OssIndexParser parser = new OssIndexParser(); - return parser.parse(jsonResponse.getBody()); - } else { - handleUnexpectedHttpResponse(LOGGER, API_BASE_URL, jsonResponse.getStatus(), jsonResponse.getStatusText()); + try (final CloseableHttpResponse response = HttpClientPool.getClient().execute(request)) { + HttpEntity responseEntity = response.getEntity(); + String responseString = EntityUtils.toString(responseEntity); + if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) { + final OssIndexParser parser = new OssIndexParser(); + return parser.parse(responseString); + } else { + handleUnexpectedHttpResponse(LOGGER, API_BASE_URL, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); + } } return new ArrayList<>(); + } private void processResults(final List report, final List componentsScanned) { try (QueryManager qm = new QueryManager()) { - for (final ComponentReport componentReport: report) { - for (final Component c: componentsScanned) { + for (final ComponentReport componentReport : report) { + for (final Component c : componentsScanned) { //final String componentPurl = component.getPurl().canonicalize(); // todo: put this back when minimizePurl() is removed final String componentPurl = minimizePurl(c.getPurl()); final PackageURL sonatypePurl = oldPurlResolver(componentReport.getCoordinates()); @@ -282,7 +293,7 @@ private void processResults(final List report, final List>custom() + final RetryRegistry retryRegistry = RetryRegistry.of(RetryConfig.custom() .intervalFunction(ofExponentialBackoff( Duration.ofSeconds(Config.getInstance().getPropertyAsInt(ConfigKey.SNYK_RETRY_EXPONENTIAL_BACKOFF_INITIAL_DURATION_SECONDS)), Config.getInstance().getPropertyAsInt(ConfigKey.SNYK_RETRY_EXPONENTIAL_BACKOFF_MULTIPLIER), @@ -108,12 +111,12 @@ public class SnykAnalysisTask extends BaseComponentAnalyzerTask implements Cache )) .maxAttempts(Config.getInstance().getPropertyAsInt(ConfigKey.SNYK_RETRY_MAX_ATTEMPTS)) .retryOnException(exception -> false) - .retryOnResult(response -> HttpStatus.TOO_MANY_REQUESTS == response.getStatus()) + .retryOnResult(response -> 429 == response.getStatusLine().getStatusCode()) .build()); RETRY = retryRegistry.retry("snyk-api"); RETRY.getEventPublisher() - .onRetry(event -> LOGGER.debug("Will execute retry #%d in %s".formatted(event.getNumberOfRetryAttempts(), event.getWaitInterval()))) - .onError(event -> LOGGER.error("Retry failed after %d attempts: %s".formatted(event.getNumberOfRetryAttempts(), event.getLastThrowable()))); + .onRetry(event -> LOGGER.debug("Will execute retry #%d in %s" .formatted(event.getNumberOfRetryAttempts(), event.getWaitInterval()))) + .onError(event -> LOGGER.error("Retry failed after %d attempts: %s" .formatted(event.getNumberOfRetryAttempts(), event.getLastThrowable()))); TaggedRetryMetrics.ofRetryRegistry(retryRegistry) .bindTo(Metrics.getRegistry()); @@ -238,7 +241,7 @@ public void analyze(final List components) { countDownLatch.countDown(); if (exception != null) { - LOGGER.error("An unexpected error occurred while analyzing %s".formatted(component), exception); + LOGGER.error("An unexpected error occurred while analyzing %s" .formatted(component), exception); } }); } @@ -267,7 +270,7 @@ public void analyze(final List components) { .scope(NotificationScope.SYSTEM) .level(NotificationLevel.WARNING) .group(NotificationGroup.ANALYZER) - .title("Snyk API version %s is deprecated".formatted(apiVersion)) + .title("Snyk API version %s is deprecated" .formatted(apiVersion)) .content(message)); } } @@ -294,28 +297,42 @@ public void applyAnalysisFromCache(final Component component) { private void analyzeComponent(final Component component) { final String encodedPurl = URLEncoder.encode(component.getPurl().getCoordinates(), StandardCharsets.UTF_8); - final String requestUrl = "%s/rest/orgs/%s/packages/%s/issues?version=%s".formatted(apiBaseUrl, apiOrgId, encodedPurl, apiVersion); - final GetRequest request = UnirestFactory.getUnirestInstance().get(requestUrl) - .header(HttpHeaders.USER_AGENT, ManagedHttpClientFactory.getUserAgent()) - .header(HttpHeaders.AUTHORIZATION, "token " + apiTokenSupplier.get()) - .header(HttpHeaders.ACCEPT, "application/vnd.api+json"); - - final HttpResponse response = RETRY.executeSupplier(request::asJson); - apiVersionSunset = StringUtils.trimToNull(response.getHeaders().getFirst("Sunset")); - if (response.isSuccess()) { - handle(component, response.getBody().getObject()); - } else if (response.getBody() != null) { - final List errors = new SnykParser().parseErrors(response.getBody().getObject()); - if (!errors.isEmpty()) { - LOGGER.error("Analysis of component %s failed with HTTP status %d: \n%s" - .formatted(component.getPurl(), response.getStatus(), errors.stream() - .map(error -> " - %s: %s (%s)".formatted(error.title(), error.detail(), error.code())) - .collect(Collectors.joining("\n")))); - } else { - handleUnexpectedHttpResponse(LOGGER, request.getUrl(), response.getStatus(), response.getStatusText()); + final String requestUrl = "%s/rest/orgs/%s/packages/%s/issues?version=%s" .formatted(apiBaseUrl, apiOrgId, encodedPurl, apiVersion); + try { + URIBuilder uriBuilder = new URIBuilder(requestUrl); + final HttpUriRequest request = new HttpGet(uriBuilder.build().toString()); + request.setHeader(HttpHeaders.USER_AGENT, ManagedHttpClientFactory.getUserAgent()); + request.setHeader(HttpHeaders.AUTHORIZATION, "token " + apiTokenSupplier.get()); + request.setHeader(HttpHeaders.ACCEPT, "application/vnd.api+json"); + try (final CloseableHttpResponse response = RETRY.executeCheckedSupplier(() -> HttpClientPool.getClient().execute(request))) { + Header header = response.getFirstHeader("Sunset"); + if (header != null) { + apiVersionSunset = StringUtils.trimToNull(header.getValue()); + } else { + apiVersionSunset = null; + } + if (response.getStatusLine().getStatusCode() >= HttpStatus.SC_OK && response.getStatusLine().getStatusCode() < HttpStatus.SC_MULTIPLE_CHOICES) { + String responseString = EntityUtils.toString(response.getEntity()); + JSONObject responseJson = new JSONObject(responseString); + handle(component, responseJson); + } else if (response.getEntity() != null) { + String responseString = EntityUtils.toString(response.getEntity()); + JSONObject responseJson = new JSONObject(responseString); + final List errors = new SnykParser().parseErrors(responseJson); + if (!errors.isEmpty()) { + LOGGER.error("Analysis of component %s failed with HTTP status %d: \n%s" + .formatted(component.getPurl(), response.getStatusLine().getStatusCode(), errors.stream() + .map(error -> " - %s: %s (%s)" .formatted(error.title(), error.detail(), error.code())) + .collect(Collectors.joining("\n")))); + } else { + handleUnexpectedHttpResponse(LOGGER, request.getURI().toString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); + } + } else { + handleUnexpectedHttpResponse(LOGGER, request.getURI().toString(), response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); + } } - } else { - handleUnexpectedHttpResponse(LOGGER, request.getUrl(), response.getStatus(), response.getStatusText()); + } catch (Throwable ex) { + handleRequestException(LOGGER, ex); } } @@ -372,7 +389,7 @@ private Optional getApiBaseUrl() { private Supplier createTokenSupplier(final String tokenValue) { final String[] tokens = tokenValue.split(";"); if (tokens.length > 1) { - LOGGER.debug("Will use %d tokens in round robin".formatted(tokens.length)); + LOGGER.debug("Will use %d tokens in round robin" .formatted(tokens.length)); final var roundRobinAccessor = new RoundRobinAccessor<>(List.of(tokens)); return roundRobinAccessor::get; } diff --git a/src/main/java/org/dependencytrack/tasks/scanners/VulnDbAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/VulnDbAnalysisTask.java index 130a1fc2f0..9294eff845 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/VulnDbAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/VulnDbAnalysisTask.java @@ -23,18 +23,22 @@ import alpine.event.framework.Subscriber; import alpine.model.ConfigProperty; import alpine.security.crypto.DataEncryption; -import org.dependencytrack.common.UnirestFactory; +import oauth.signpost.exception.OAuthCommunicationException; +import oauth.signpost.exception.OAuthExpectationFailedException; +import oauth.signpost.exception.OAuthMessageSignerException; import org.dependencytrack.event.VulnDbAnalysisEvent; import org.dependencytrack.model.Component; import org.dependencytrack.model.ConfigPropertyConstants; import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerabilityAnalysisLevel; import org.dependencytrack.parser.vulndb.ModelConverter; +import org.dependencytrack.parser.vulndb.VulnDbClient; +import org.dependencytrack.parser.vulndb.model.Results; import org.dependencytrack.persistence.QueryManager; import org.dependencytrack.util.NotificationUtil; -import us.springett.vulndbdatamirror.client.VulnDbApi; -import us.springett.vulndbdatamirror.parser.model.Results; +import java.io.IOException; +import java.net.URISyntaxException; import java.util.List; /** @@ -45,17 +49,27 @@ */ public class VulnDbAnalysisTask extends BaseComponentAnalyzerTask implements Subscriber { + private static final Logger LOGGER = Logger.getLogger(VulnDbAnalysisTask.class); - private static final String TARGET_HOST = "https://vulndb.cyberriskanalytics.com/"; private static final int PAGE_SIZE = 100; private VulnerabilityAnalysisLevel vulnerabilityAnalysisLevel; private String apiConsumerKey; private String apiConsumerSecret; + private String apiBaseUrl; + public AnalyzerIdentity getAnalyzerIdentity() { return AnalyzerIdentity.VULNDB_ANALYZER; } + public VulnDbAnalysisTask(String apiBaseUrl) { + this.apiBaseUrl = apiBaseUrl; + } + + public VulnDbAnalysisTask() { + this("https://vulndb.cyberriskanalytics.com"); + } + /** * {@inheritDoc} */ @@ -73,6 +87,11 @@ public void inform(final Event e) { ConfigPropertyConstants.SCANNER_VULNDB_OAUTH1_CONSUMER_SECRET.getGroupName(), ConfigPropertyConstants.SCANNER_VULNDB_OAUTH1_CONSUMER_SECRET.getPropertyName() ); + if (this.apiBaseUrl == null) { + LOGGER.warn("No API base URL provided; Skipping"); + return; + } + if (apiConsumerKey == null || apiConsumerKey.getPropertyValue() == null) { LOGGER.warn("An OAuth 1.0a consumer key has not been specified for use with VulnDB. Skipping"); return; @@ -89,10 +108,10 @@ public void inform(final Event e) { return; } } - final VulnDbAnalysisEvent event = (VulnDbAnalysisEvent)e; + final var event = (VulnDbAnalysisEvent) e; vulnerabilityAnalysisLevel = event.getVulnerabilityAnalysisLevel(); LOGGER.info("Starting VulnDB analysis task"); - if (event.getComponents().size() > 0) { + if (!event.getComponents().isEmpty()) { analyze(event.getComponents()); } LOGGER.info("VulnDB analysis complete"); @@ -111,26 +130,35 @@ public boolean isCapable(final Component component) { /** * Analyzes a list of Components. + * * @param components a list of Components */ public void analyze(final List components) { - final VulnDbApi api = new VulnDbApi(this.apiConsumerKey, this.apiConsumerSecret, UnirestFactory.getUnirestInstance()); - for (final Component component: components) { - if(isCacheCurrent(Vulnerability.Source.VULNDB, TARGET_HOST, component.getCpe())){ - applyAnalysisFromCache(Vulnerability.Source.VULNDB, TARGET_HOST, component.getCpe(),component, AnalyzerIdentity.VULNDB_ANALYZER, vulnerabilityAnalysisLevel); - } - else if (!component.isInternal() && isCapable(component) - && !isCacheCurrent(Vulnerability.Source.VULNDB, TARGET_HOST, component.getCpe())) { - int page = 1; - boolean more = true; - while (more) { - final Results results = api.getVulnerabilitiesByCpe(component.getCpe(), PAGE_SIZE, page); - if (results.isSuccessful()) { - more = processResults(results, component); - page++; - } else { - LOGGER.error(results.getErrorCondition()); - return; + final var api = new VulnDbClient(this.apiConsumerKey, this.apiConsumerSecret, this.apiBaseUrl); + for (final Component component : components) { + if (isCacheCurrent(Vulnerability.Source.VULNDB, apiBaseUrl, component.getCpe())) { + applyAnalysisFromCache(Vulnerability.Source.VULNDB, apiBaseUrl, component.getCpe(), component, AnalyzerIdentity.VULNDB_ANALYZER, vulnerabilityAnalysisLevel); + } else if (!component.isInternal() && isCapable(component) + && !isCacheCurrent(Vulnerability.Source.VULNDB, apiBaseUrl, component.getCpe())) { + if (!component.isInternal() && isCapable(component) + && !isCacheCurrent(Vulnerability.Source.VULNDB, apiBaseUrl, component.getCpe())) { + int page = 1; + boolean more = true; + while (more) { + try { + final Results results = api.getVulnerabilitiesByCpe(component.getCpe(), PAGE_SIZE, page); + if (results.isSuccessful()) { + more = processResults(results, component); + page++; + } else { + LOGGER.warn(results.getErrorCondition()); + handleRequestException(LOGGER, new Exception(results.getErrorCondition())); + return; + } + } catch (IOException | OAuthMessageSignerException | OAuthExpectationFailedException | + URISyntaxException | OAuthCommunicationException ex) { + handleRequestException(LOGGER, ex); + } } } } @@ -141,8 +169,8 @@ else if (!component.isInternal() && isCapable(component) private boolean processResults(final Results results, final Component component) { try (final QueryManager qm = new QueryManager()) { final Component vulnerableComponent = qm.getObjectByUuid(Component.class, component.getUuid()); // Refresh component and attach to current pm. - for (us.springett.vulndbdatamirror.parser.model.Vulnerability vulnDbVuln : (List) results.getResults()) { - Vulnerability vulnerability = qm.getVulnerabilityByVulnId(Vulnerability.Source.VULNDB, String.valueOf(vulnDbVuln.getId())); + for (org.dependencytrack.parser.vulndb.model.Vulnerability vulnDbVuln : (List) results.getResults()) { + Vulnerability vulnerability = qm.getVulnerabilityByVulnId(Vulnerability.Source.VULNDB, String.valueOf(vulnDbVuln.id())); if (vulnerability == null) { vulnerability = qm.createVulnerability(ModelConverter.convert(qm, vulnDbVuln), false); } else { @@ -152,8 +180,9 @@ private boolean processResults(final Results results, final Component component) qm.addVulnerability(vulnerability, vulnerableComponent, this.getAnalyzerIdentity()); addVulnerabilityToCache(vulnerableComponent, vulnerability); } - updateAnalysisCacheStats(qm, Vulnerability.Source.VULNDB, TARGET_HOST, vulnerableComponent.getCpe(), vulnerableComponent.getCacheResult()); + updateAnalysisCacheStats(qm, Vulnerability.Source.VULNDB, apiBaseUrl, vulnerableComponent.getCpe(), vulnerableComponent.getCacheResult()); return results.getPage() * PAGE_SIZE < results.getTotal(); } } + } diff --git a/src/main/java/org/dependencytrack/util/XmlUtil.java b/src/main/java/org/dependencytrack/util/XmlUtil.java index df10739290..f7aff024f8 100644 --- a/src/main/java/org/dependencytrack/util/XmlUtil.java +++ b/src/main/java/org/dependencytrack/util/XmlUtil.java @@ -31,9 +31,11 @@ import java.io.InputStream; import static org.apache.xerces.jaxp.JAXPConstants.JAXP_SCHEMA_LANGUAGE; + import static org.apache.xerces.jaxp.JAXPConstants.JAXP_SCHEMA_SOURCE; import static org.apache.xerces.jaxp.JAXPConstants.W3C_XML_SCHEMA; + public final class XmlUtil { private XmlUtil() { } diff --git a/src/test/java/org/dependencytrack/ResourceTest.java b/src/test/java/org/dependencytrack/ResourceTest.java index f9761c4d54..9d7ce0d1d1 100644 --- a/src/test/java/org/dependencytrack/ResourceTest.java +++ b/src/test/java/org/dependencytrack/ResourceTest.java @@ -29,10 +29,7 @@ import org.dependencytrack.persistence.QueryManager; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.grizzly.connector.GrizzlyConnectorProvider; -import org.glassfish.jersey.test.DeploymentContext; import org.glassfish.jersey.test.JerseyTest; -import org.glassfish.jersey.test.grizzly.GrizzlyWebTestContainerFactory; -import org.glassfish.jersey.test.spi.TestContainer; import org.glassfish.jersey.test.spi.TestContainerFactory; import org.junit.After; import org.junit.Before; diff --git a/src/test/java/org/dependencytrack/auth/PermissionsTest.java b/src/test/java/org/dependencytrack/auth/PermissionsTest.java index 997174970b..5d7408b6e1 100644 --- a/src/test/java/org/dependencytrack/auth/PermissionsTest.java +++ b/src/test/java/org/dependencytrack/auth/PermissionsTest.java @@ -21,8 +21,18 @@ import org.junit.Assert; import org.junit.Test; -import static org.dependencytrack.auth.Permissions.Constants.*; - +import static org.dependencytrack.auth.Permissions.Constants.PORTFOLIO_MANAGEMENT; +import static org.dependencytrack.auth.Permissions.Constants.BOM_UPLOAD; +import static org.dependencytrack.auth.Permissions.Constants.VIEW_PORTFOLIO; +import static org.dependencytrack.auth.Permissions.Constants.VIEW_VULNERABILITY; +import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_ANALYSIS; +import static org.dependencytrack.auth.Permissions.Constants.VIEW_POLICY_VIOLATION; +import static org.dependencytrack.auth.Permissions.Constants.VULNERABILITY_MANAGEMENT; +import static org.dependencytrack.auth.Permissions.Constants.POLICY_VIOLATION_ANALYSIS; +import static org.dependencytrack.auth.Permissions.Constants.ACCESS_MANAGEMENT; +import static org.dependencytrack.auth.Permissions.Constants.SYSTEM_CONFIGURATION; +import static org.dependencytrack.auth.Permissions.Constants.PROJECT_CREATION_UPLOAD; +import static org.dependencytrack.auth.Permissions.Constants.POLICY_MANAGEMENT; public class PermissionsTest { @Test diff --git a/src/test/java/org/dependencytrack/integration/ApiClient.java b/src/test/java/org/dependencytrack/integration/ApiClient.java deleted file mode 100644 index d0d7d35a97..0000000000 --- a/src/test/java/org/dependencytrack/integration/ApiClient.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * 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) Steve Springett. All Rights Reserved. - */ -package org.dependencytrack.integration; - -import kong.unirest.HttpResponse; -import kong.unirest.JsonNode; -import kong.unirest.UnirestException; -import kong.unirest.UnirestInstance; -import kong.unirest.json.JSONObject; -import org.apache.commons.io.FileUtils; -import org.dependencytrack.common.UnirestFactory; - -import java.io.File; -import java.io.IOException; -import java.util.Base64; -import java.util.UUID; - -public class ApiClient { - - private String baseUrl; - private String apiKey; - - public ApiClient(String baseUrl, String apiKey) { - this.baseUrl = baseUrl; - this.apiKey = apiKey; - } - - public UUID createProject(String name, String version) throws UnirestException { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - final HttpResponse response = ui.put(baseUrl + "/api/v1/project") - .header("Content-Type", "application/json") - .header("X-API-Key", apiKey) - .body(new JSONObject() - .put("name", name) - .put("version", version) - ) - .asJson(); - if (response.getStatus() == 201) { - return UUID.fromString(response.getBody().getObject().getString("uuid")); - } - System.out.println("Error creating project " + name + " status: " + response.getStatus()); - return null; - } - - public boolean uploadBom(UUID uuid, File bom) throws IOException, UnirestException { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - final HttpResponse response = ui.put(baseUrl + "/api/v1/bom") - .header("Content-Type", "application/json") - .header("X-API-Key", apiKey) - .body(new JSONObject() - .put("project", uuid.toString()) - .put("bom", Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(bom))) - ) - .asJson(); - return (response.getStatus() == 200); - } - - public boolean uploadScan(UUID uuid, File scan) throws IOException, UnirestException { - final UnirestInstance ui = UnirestFactory.getUnirestInstance(); - final HttpResponse response = ui.put(baseUrl + "/api/v1/scan") - .header("Content-Type", "application/json") - .header("X-API-Key", apiKey) - .body(new JSONObject() - .put("project", uuid.toString()) - .put("scan", Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(scan))) - ) - .asJson(); - return (response.getStatus() == 200); - } -} diff --git a/src/test/java/org/dependencytrack/integration/PopulateData.java b/src/test/java/org/dependencytrack/integration/PopulateData.java deleted file mode 100644 index d27b06e418..0000000000 --- a/src/test/java/org/dependencytrack/integration/PopulateData.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * This file is part of Dependency-Track. - * - * 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) Steve Springett. All Rights Reserved. - */ -package org.dependencytrack.integration; - -import org.junit.Test; - -import java.io.File; -import java.util.UUID; - -public class PopulateData { - - private static final String BASE_URL = "http://localhost:8080"; - private static final String API_KEY = "hETzpWanQkXV6KsJsfPuFoNBRZdiiDyY"; - - @Test - public void doit() throws Exception { - ApiClient api = new ApiClient(BASE_URL, API_KEY); - UUID uuid = api.createProject("SonarQube", "5.6"); - - File file = new File(this.getClass().getResource("/integration/sonarqube-6.5.spdx").getFile()); - - if (file.exists()) { - System.out.println("Found It"); - api.uploadBom(uuid, file); - } - - } - - - public static void main(String[] args) throws Exception { - final PopulateData populateData = new PopulateData(); - populateData.doit(); - } -} diff --git a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java index de604eb990..590854e299 100644 --- a/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java +++ b/src/test/java/org/dependencytrack/integrations/defectdojo/DefectDojoClientTest.java @@ -18,116 +18,60 @@ */ package org.dependencytrack.integrations.defectdojo; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.matching.EqualToPattern; import org.apache.commons.io.input.NullInputStream; import org.apache.http.HttpHeaders; -import org.junit.After; -import org.junit.AfterClass; -import org.junit.Before; -import org.junit.BeforeClass; +import org.apache.http.entity.ContentType; import org.junit.Rule; import org.junit.Test; -import org.junit.contrib.java.lang.system.EnvironmentVariables; -import org.junit.rules.ExpectedException; -import org.mockserver.client.MockServerClient; -import org.mockserver.integration.ClientAndServer; -import org.mockserver.verify.VerificationTimes; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.net.URL; -import static org.mockserver.integration.ClientAndServer.startClientAndServer; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; - public class DefectDojoClientTest { - private static ClientAndServer mockServer; - private static MockServerClient testClient; - - @Rule - public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); - @Rule - public ExpectedException thrown = ExpectedException.none(); + public WireMockRule wireMockRule = new WireMockRule(); - @Before - public void before() { - environmentVariables.set("http_proxy", "http://127.0.0.1:1080"); - testClient = new MockServerClient("localhost", 1080); - } - - @After - public void after() { - testClient.clear( - request() - .withPath("/defectdojo/api/v2/import-scan/") - ); - testClient.clear( - request() - .withPath("/defectdojo/api/v2/reimport-scan/") - ); - } - - @BeforeClass - public static void beforeClass() { - mockServer = startClientAndServer(1080); - } - - @AfterClass - public static void afterClass() { - mockServer.stop(); - } @Test public void testUploadFindingsPositiveCase() throws Exception { + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/defectdojo/api/v2/import-scan/")) + .withMultipartRequestBody(WireMock.aMultipart().withName("engagement"). + withBody(WireMock.equalTo("12345")))); + InputStream stream = new ByteArrayInputStream("test input" .getBytes()); String token = "db975c97-98b1-4988-8d6a-9c3e044dfff3"; String engagementId = "12345"; - testClient.when( - request() - .withMethod("POST") - .withHeader(HttpHeaders.AUTHORIZATION, "Token " + token) - .withPath("/defectdojo/api/v2/import-scan/") - ) - .respond( - response() - .withStatusCode(201) - .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") - ); DefectDojoUploader uploader = new DefectDojoUploader(); - DefectDojoClient client = new DefectDojoClient(uploader, new URL("https://localhost/defectdojo")); - client.uploadDependencyTrackFindings(token, engagementId, new NullInputStream(0)); - testClient.verify( - request() - .withMethod("POST") - .withPath("/defectdojo/api/v2/import-scan/"), - VerificationTimes.exactly(1) - ); + DefectDojoClient client = new DefectDojoClient(uploader, new URL(wireMockRule.baseUrl() + "/defectdojo")); + client.uploadDependencyTrackFindings(token, engagementId, stream); + + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/defectdojo/api/v2/import-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart().withName("engagement"). + withBody(WireMock.equalTo("12345") + )).withAnyRequestBodyPart(WireMock.aMultipart().withName("file") + .withBody(WireMock.equalTo("test input")).withHeader("Content-Type", WireMock.equalTo(ContentType.APPLICATION_OCTET_STREAM.getMimeType())))); } + @Test public void testUploadFindingsNegativeCase() throws Exception { String token = "db975c97-98b1-4988-8d6a-9c3e044dfff2"; String engagementId = ""; - testClient.when( - request() - .withMethod("POST") - .withHeader(HttpHeaders.AUTHORIZATION, "Token " + token) - .withPath("/defectdojo/api/v2/import-scan/") - ) - .respond( - response() - .withStatusCode(400) - .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") - ); + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/defectdojo/api/v2/import-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token " + token)) + .withMultipartRequestBody(WireMock.aMultipart().withName("engagement"). + withBody(WireMock.equalTo(""))).willReturn(WireMock.aResponse().withStatus(400).withHeader(HttpHeaders.CONTENT_TYPE, "application/json"))); DefectDojoUploader uploader = new DefectDojoUploader(); - DefectDojoClient client = new DefectDojoClient(uploader, new URL("https://localhost/defectdojo")); + DefectDojoClient client = new DefectDojoClient(uploader, new URL(wireMockRule.baseUrl() + "/defectdojo")); client.uploadDependencyTrackFindings(token, engagementId, new NullInputStream(16)); - testClient.verify( - request() - .withMethod("POST") - .withHeader(HttpHeaders.AUTHORIZATION, "Token " + token) - .withPath("/defectdojo/api/v2/import-scan/"), - VerificationTimes.exactly(1) - ); + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/defectdojo/api/v2/import-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart().withName("engagement"). + withBody(WireMock.equalTo("") + ))); } @Test @@ -135,26 +79,17 @@ public void testReimportFindingsPositiveCase() throws Exception { String token = "db975c97-98b1-4988-8d6a-9c3e044dfff3"; String testId = "15"; String engagementId = "67890"; - testClient.when( - request() - .withMethod("POST") - .withHeader(HttpHeaders.AUTHORIZATION, "Token " + token) - .withPath("/defectdojo/api/v2/reimport-scan/") - ) - .respond( - response() - .withStatusCode(201) - .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") - ); + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/defectdojo/api/v2/reimport-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token " + token)) + .withMultipartRequestBody(WireMock.aMultipart().withName("engagement"). + withBody(WireMock.equalTo(engagementId))).willReturn(WireMock.aResponse().withStatus(201).withHeader(HttpHeaders.CONTENT_TYPE, "application/json"))); DefectDojoUploader uploader = new DefectDojoUploader(); - DefectDojoClient client = new DefectDojoClient(uploader, new URL("https://localhost/defectdojo")); + DefectDojoClient client = new DefectDojoClient(uploader, new URL(wireMockRule.baseUrl() + "/defectdojo")); client.reimportDependencyTrackFindings(token, engagementId, new NullInputStream(0), testId); - testClient.verify( - request() - .withMethod("POST") - .withPath("/defectdojo/api/v2/reimport-scan/"), - VerificationTimes.exactly(1) - ); + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/defectdojo/api/v2/reimport-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart().withName("engagement"). + withBody(WireMock.equalTo(engagementId) + ))); } @Test @@ -162,26 +97,16 @@ public void testReimportFindingsNegativeCase() throws Exception { String token = "db975c97-98b1-4988-8d6a-9c3e044dfff2"; String testId = "14"; String engagementId = ""; - testClient.when( - request() - .withMethod("POST") - .withHeader(HttpHeaders.AUTHORIZATION, "Token " + token) - .withPath("/defectdojo/api/v2/reimport-scan/") - ) - .respond( - response() - .withStatusCode(400) - .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") - ); + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/defectdojo/api/v2/reimport-scan/")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("Token " + token)) + .withMultipartRequestBody(WireMock.aMultipart().withName("engagement"). + withBody(WireMock.equalTo(""))).willReturn(WireMock.aResponse().withStatus(400).withHeader(HttpHeaders.CONTENT_TYPE, "application/json"))); DefectDojoUploader uploader = new DefectDojoUploader(); - DefectDojoClient client = new DefectDojoClient(uploader, new URL("https://localhost/defectdojo")); + DefectDojoClient client = new DefectDojoClient(uploader, new URL(wireMockRule.baseUrl() + "/defectdojo")); client.reimportDependencyTrackFindings(token, engagementId, new NullInputStream(16), testId); - testClient.verify( - request() - .withMethod("POST") - .withHeader(HttpHeaders.AUTHORIZATION, "Token " + token) - .withPath("/defectdojo/api/v2/reimport-scan/"), - VerificationTimes.exactly(1) - ); + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/defectdojo/api/v2/reimport-scan/")) + .withAnyRequestBodyPart(WireMock.aMultipart().withName("engagement"). + withBody(WireMock.equalTo(engagementId) + ))); } } diff --git a/src/test/java/org/dependencytrack/integrations/fortifyssc/FortifySscClientTest.java b/src/test/java/org/dependencytrack/integrations/fortifyssc/FortifySscClientTest.java index 0e1db80ee2..b67f2d71b9 100644 --- a/src/test/java/org/dependencytrack/integrations/fortifyssc/FortifySscClientTest.java +++ b/src/test/java/org/dependencytrack/integrations/fortifyssc/FortifySscClientTest.java @@ -18,90 +18,48 @@ */ package org.dependencytrack.integrations.fortifyssc; -import org.apache.commons.io.input.NullInputStream; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.junit.WireMockRule; +import com.github.tomakehurst.wiremock.matching.EqualToPattern; import org.apache.http.HttpHeaders; -import org.junit.AfterClass; +import org.apache.http.entity.ContentType; import org.junit.Assert; -import org.junit.Before; -import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; -import org.junit.contrib.java.lang.system.EnvironmentVariables; -import org.junit.rules.ExpectedException; -import org.mockserver.client.MockServerClient; -import org.mockserver.integration.ClientAndServer; +import java.io.ByteArrayInputStream; +import java.io.InputStream; import java.net.URL; import java.nio.charset.StandardCharsets; import java.util.Base64; -import static org.mockserver.integration.ClientAndServer.startClientAndServer; -import static org.mockserver.model.HttpRequest.request; -import static org.mockserver.model.HttpResponse.response; - public class FortifySscClientTest { - private static ClientAndServer mockServer; - - @Rule - public final EnvironmentVariables environmentVariables = new EnvironmentVariables(); - @Rule - public ExpectedException thrown = ExpectedException.none(); + public WireMockRule wireMockRule = new WireMockRule(); - @Before - public void before() { - environmentVariables.set("http_proxy", "http://127.0.0.1:1080"); - } - - @BeforeClass - public static void beforeClass() { - mockServer = startClientAndServer(1080); - } - - @AfterClass - public static void after() { - mockServer.stop(); - } @Test public void testOneTimeTokenPositiveCase() throws Exception { - new MockServerClient("localhost", 1080) - .when( - request() - .withMethod("POST") - .withHeader(HttpHeaders.AUTHORIZATION, "FortifyToken " + Base64.getEncoder().encodeToString("2d5e4a06-945e-405f-a3c2-112bb3053453".getBytes(StandardCharsets.UTF_8))) - .withPath("/ssc/api/v1/fileTokens") - .withBody("{\"fileTokenType\":\"UPLOAD\"}") - ) - .respond( - response() - .withStatusCode(201) - .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") - .withBody("{ \"data\": { \"token\": \"db975c97-98b1-4988-8d6a-9c3e044dfff3\" }}") - ); + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/ssc/api/v1/fileTokens")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("FortifyToken " + Base64.getEncoder().encodeToString("2d5e4a06-945e-405f-a3c2-112bb3053453" .getBytes(StandardCharsets.UTF_8)))) + .withRequestBody(WireMock.equalToJson("{\"fileTokenType\":\"UPLOAD\"}")) + .willReturn(WireMock.aResponse().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") + .withBody("{ \"data\": { \"token\": \"db975c97-98b1-4988-8d6a-9c3e044dfff3\" }}").withStatus(201))); FortifySscUploader uploader = new FortifySscUploader(); - FortifySscClient client = new FortifySscClient(uploader, new URL("https://localhost/ssc")); + FortifySscClient client = new FortifySscClient(uploader, new URL(wireMockRule.baseUrl() + "/ssc")); String token = client.generateOneTimeUploadToken("2d5e4a06-945e-405f-a3c2-112bb3053453"); Assert.assertEquals("db975c97-98b1-4988-8d6a-9c3e044dfff3", token); } @Test public void testOneTimeTokenInvalidCredentials() throws Exception { - new MockServerClient("localhost", 1080) - .when( - request() - .withMethod("POST") - .withHeader(HttpHeaders.AUTHORIZATION, "FortifyToken " + Base64.getEncoder().encodeToString("wrong".getBytes(StandardCharsets.UTF_8))) - .withPath("/ssc/api/v1/fileTokens") - .withBody("{\"fileTokenType\":\"UPLOAD\"}") - ) - .respond( - response() - .withStatusCode(401) - ); + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/ssc/api/v1/fileTokens")) + .withHeader(HttpHeaders.AUTHORIZATION, new EqualToPattern("FortifyToken " + Base64.getEncoder().encodeToString("wrong" .getBytes(StandardCharsets.UTF_8)))) + .withRequestBody(WireMock.equalToJson("{\"fileTokenType\":\"UPLOAD\"}")) + .willReturn(WireMock.aResponse().withHeader(HttpHeaders.CONTENT_TYPE, "application/json").withStatus(401))); FortifySscUploader uploader = new FortifySscUploader(); - FortifySscClient client = new FortifySscClient(uploader, new URL("https://localhost/ssc")); + FortifySscClient client = new FortifySscClient(uploader, new URL(wireMockRule.baseUrl() + "/ssc")); String token = client.generateOneTimeUploadToken("wrong"); Assert.assertNull(token); } @@ -110,47 +68,47 @@ public void testOneTimeTokenInvalidCredentials() throws Exception { public void testUploadFindingsPositiveCase() throws Exception { String token = "db975c97-98b1-4988-8d6a-9c3e044dfff3"; String applicationVersion = "12345"; - new MockServerClient("localhost", 1080) - .when( - request() - .withMethod("POST") - .withHeader(HttpHeaders.ACCEPT, "application/xml") - .withPath("/ssc/upload/resultFileUpload.html?mat=" + token + "&engineType=DEPENDENCY_TRACK&entityId=" + applicationVersion) - .withQueryStringParameter("engineType", "DEPENDENCY_TRACK") - .withQueryStringParameter("mat", token) - .withQueryStringParameter("entityId", applicationVersion) - ) - .respond( - response() - .withStatusCode(200) - .withHeader(HttpHeaders.CONTENT_TYPE, "application/xml") - ); + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/ssc/upload/resultFileUpload.html")) + .withHeader(HttpHeaders.ACCEPT, new EqualToPattern("application/xml")) + .withQueryParam("engineType", new EqualToPattern("DEPENDENCY_TRACK")) + .withQueryParam("mat", new EqualToPattern(token)) + .withQueryParam("entityId", new EqualToPattern(applicationVersion)) + .willReturn(WireMock.aResponse().withHeader(HttpHeaders.CONTENT_TYPE, "application/xml").withStatus(200))); FortifySscUploader uploader = new FortifySscUploader(); - FortifySscClient client = new FortifySscClient(uploader, new URL("https://localhost/ssc")); - client.uploadDependencyTrackFindings(token, applicationVersion, new NullInputStream(0)); + FortifySscClient client = new FortifySscClient(uploader, new URL(wireMockRule.baseUrl() + "/ssc")); + InputStream stream = new ByteArrayInputStream("test input" .getBytes()); + client.uploadDependencyTrackFindings(token, applicationVersion, stream); + + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/ssc/upload/resultFileUpload.html")) + .withQueryParam("engineType", new EqualToPattern("DEPENDENCY_TRACK")) + .withQueryParam("mat", new EqualToPattern(token)) + .withQueryParam("entityId", new EqualToPattern(applicationVersion)) + .withAnyRequestBodyPart(WireMock.aMultipart().withName("files[]") + .withBody(WireMock.equalTo("test input")).withHeader("Content-Type", WireMock.equalTo(ContentType.APPLICATION_OCTET_STREAM.getMimeType())))); } @Test public void testUploadFindingsNegativeCase() throws Exception { String token = "db975c97-98b1-4988-8d6a-9c3e044dfff3"; String applicationVersion = ""; - new MockServerClient("localhost", 1080) - .when( - request() - .withMethod("POST") - .withHeader(HttpHeaders.ACCEPT, "application/xml") - .withPath("/ssc/upload/resultFileUpload.html?mat=" + token + "&engineType=DEPENDENCY_TRACK&entityId=" + applicationVersion) - .withQueryStringParameter("engineType", "DEPENDENCY_TRACK") - .withQueryStringParameter("mat", token) - .withQueryStringParameter("entityId", applicationVersion) - ) - .respond( - response() - .withStatusCode(400) - .withHeader(HttpHeaders.CONTENT_TYPE, "application/xml") - ); + + WireMock.stubFor(WireMock.post(WireMock.urlPathEqualTo("/ssc/upload/resultFileUpload.html")) + .withHeader(HttpHeaders.ACCEPT, new EqualToPattern("application/xml")) + .withQueryParam("engineType", new EqualToPattern("DEPENDENCY_TRACK")) + .withQueryParam("mat", new EqualToPattern(token)) + .withQueryParam("entityId", new EqualToPattern(applicationVersion)) + .willReturn(WireMock.aResponse().withHeader(HttpHeaders.CONTENT_TYPE, "application/xml").withStatus(400))); FortifySscUploader uploader = new FortifySscUploader(); - FortifySscClient client = new FortifySscClient(uploader, new URL("https://localhost/ssc")); - client.uploadDependencyTrackFindings(token, applicationVersion, new NullInputStream(16)); + FortifySscClient client = new FortifySscClient(uploader, new URL(wireMockRule.baseUrl() + "/ssc")); + InputStream stream = new ByteArrayInputStream("test input" .getBytes()); + client.uploadDependencyTrackFindings(token, applicationVersion, stream); + + WireMock.verify(WireMock.postRequestedFor(WireMock.urlPathEqualTo("/ssc/upload/resultFileUpload.html")) + .withQueryParam("engineType", new EqualToPattern("DEPENDENCY_TRACK")) + .withQueryParam("mat", new EqualToPattern(token)) + .withQueryParam("entityId", new EqualToPattern(applicationVersion)) + .withAnyRequestBodyPart(WireMock.aMultipart().withName("files[]") + .withBody(WireMock.equalTo("test input")).withHeader("Content-Type", WireMock.equalTo(ContentType.APPLICATION_OCTET_STREAM.getMimeType())))); + } } diff --git a/src/test/java/org/dependencytrack/parser/osv/OsvAdvisoryParserTest.java b/src/test/java/org/dependencytrack/parser/osv/OsvAdvisoryParserTest.java index d33dc3fe66..4ba6b8ba4f 100644 --- a/src/test/java/org/dependencytrack/parser/osv/OsvAdvisoryParserTest.java +++ b/src/test/java/org/dependencytrack/parser/osv/OsvAdvisoryParserTest.java @@ -1,7 +1,7 @@ package org.dependencytrack.parser.osv; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; +import org.json.JSONArray; +import org.json.JSONObject; import org.dependencytrack.parser.osv.model.OsvAdvisory; import org.dependencytrack.parser.osv.model.OsvAffectedPackage; import org.junit.Assert; diff --git a/src/test/java/org/dependencytrack/parser/snyk/SnykParserTest.java b/src/test/java/org/dependencytrack/parser/snyk/SnykParserTest.java index 5d855dc4e9..104dccbcec 100644 --- a/src/test/java/org/dependencytrack/parser/snyk/SnykParserTest.java +++ b/src/test/java/org/dependencytrack/parser/snyk/SnykParserTest.java @@ -19,8 +19,8 @@ package org.dependencytrack.parser.snyk; import alpine.model.IConfigProperty; -import kong.unirest.json.JSONArray; -import kong.unirest.json.JSONObject; +import org.json.JSONArray; +import org.json.JSONObject; import org.dependencytrack.PersistenceCapableTest; import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.parser.snyk.model.SnykError; diff --git a/src/test/java/org/dependencytrack/persistence/ProjectQueryFilterBuilderTest.java b/src/test/java/org/dependencytrack/persistence/ProjectQueryFilterBuilderTest.java index b21a161229..0a8180b382 100644 --- a/src/test/java/org/dependencytrack/persistence/ProjectQueryFilterBuilderTest.java +++ b/src/test/java/org/dependencytrack/persistence/ProjectQueryFilterBuilderTest.java @@ -4,7 +4,9 @@ import java.util.Map; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; public class ProjectQueryFilterBuilderTest { diff --git a/src/test/java/org/dependencytrack/policy/CwePolicyEvaluatorTest.java b/src/test/java/org/dependencytrack/policy/CwePolicyEvaluatorTest.java index 1754bb4670..7c2db0c5f1 100644 --- a/src/test/java/org/dependencytrack/policy/CwePolicyEvaluatorTest.java +++ b/src/test/java/org/dependencytrack/policy/CwePolicyEvaluatorTest.java @@ -19,7 +19,11 @@ package org.dependencytrack.policy; import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.model.*; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Policy; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.PolicyCondition; +import org.dependencytrack.model.Vulnerability; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/dependencytrack/policy/PolicyEngineTest.java b/src/test/java/org/dependencytrack/policy/PolicyEngineTest.java index 69935b26b9..d4f4850121 100644 --- a/src/test/java/org/dependencytrack/policy/PolicyEngineTest.java +++ b/src/test/java/org/dependencytrack/policy/PolicyEngineTest.java @@ -19,7 +19,16 @@ package org.dependencytrack.policy; import org.dependencytrack.PersistenceCapableTest; -import org.dependencytrack.model.*; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.License; +import org.dependencytrack.model.LicenseGroup; +import org.dependencytrack.model.Policy; +import org.dependencytrack.model.PolicyCondition; +import org.dependencytrack.model.PolicyViolation; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Tag; +import org.dependencytrack.model.Vulnerability; import org.dependencytrack.tasks.scanners.AnalyzerIdentity; import org.junit.Assert; import org.junit.Test; @@ -28,6 +37,7 @@ import java.util.Collections; import java.util.List; import java.util.UUID; + public class PolicyEngineTest extends PersistenceCapableTest { @Test diff --git a/src/test/java/org/dependencytrack/search/FuzzyVulnerableSoftwareSearchManagerTest.java b/src/test/java/org/dependencytrack/search/FuzzyVulnerableSoftwareSearchManagerTest.java index ffba4925d2..8c38d900a3 100644 --- a/src/test/java/org/dependencytrack/search/FuzzyVulnerableSoftwareSearchManagerTest.java +++ b/src/test/java/org/dependencytrack/search/FuzzyVulnerableSoftwareSearchManagerTest.java @@ -3,10 +3,13 @@ import alpine.Config; import org.apache.commons.io.FileUtils; import org.dependencytrack.model.Component; -import org.dependencytrack.model.Vulnerability; import org.dependencytrack.model.VulnerableSoftware; import org.dependencytrack.persistence.QueryManager; -import org.junit.*; +import org.junit.Before; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Ignore; +import org.junit.Test; import us.springett.parsers.cpe.Cpe; import us.springett.parsers.cpe.CpeParser; import us.springett.parsers.cpe.exceptions.CpeEncodingException; @@ -20,9 +23,14 @@ import java.util.UUID; import java.util.regex.Pattern; -import static org.mockito.Mockito.*; - -import static org.junit.Assert.*; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class FuzzyVulnerableSoftwareSearchManagerTest { private static final File INDEX_DIRECTORY; diff --git a/src/test/java/org/dependencytrack/tasks/OsvDownloadTaskTest.java b/src/test/java/org/dependencytrack/tasks/OsvDownloadTaskTest.java index a3fcbc8630..e54a4f6269 100644 --- a/src/test/java/org/dependencytrack/tasks/OsvDownloadTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/OsvDownloadTaskTest.java @@ -18,7 +18,7 @@ import alpine.model.ConfigProperty; import alpine.model.IConfigProperty; import com.github.packageurl.PackageURL; -import kong.unirest.json.JSONObject; +import org.json.JSONObject; import org.apache.commons.lang3.StringUtils; import org.dependencytrack.PersistenceCapableTest; import org.dependencytrack.model.AffectedVersionAttribution; diff --git a/src/test/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzerTest.java b/src/test/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzerTest.java index 4082d52dbc..df1870781b 100644 --- a/src/test/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzerTest.java +++ b/src/test/java/org/dependencytrack/tasks/repositories/NugetMetaAnalyzerTest.java @@ -79,10 +79,10 @@ public void testAnalyzerWithPrivatePackageRepository() throws Exception { .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") .withBody(mockIndexResponse) ); + String encodedBasicHeader = "Basic OnBhc3N3b3Jk"; - String encodedBasicHeader = "Basic bnVsbDpwYXNzd29yZA=="; String mockVersionResponse = readResourceFileToString("/unit/tasks/repositories/https---localhost-1080-v3-flat2" + - "-nunitprivate-index.json"); + "-nunitprivate-index.json"); new MockServerClient("localhost", mockServer.getPort()) .when( request() @@ -112,18 +112,15 @@ public void testAnalyzerWithPrivatePackageRepository() throws Exception { .withHeader(HttpHeaders.CONTENT_TYPE, "application/json") .withBody(mockRegistrationResponse) ); - Component component = new Component(); component.setPurl(new PackageURL("pkg:nuget/NUnitPrivate@5.0.1")); NugetMetaAnalyzer analyzer = new NugetMetaAnalyzer(); analyzer.setRepositoryUsernameAndPassword(null, "password"); analyzer.setRepositoryBaseUrl("http://localhost:1080"); MetaModel metaModel = analyzer.analyze(component); - Assert.assertEquals("5.0.2", metaModel.getLatestVersion()); Assert.assertNotNull(metaModel.getPublishedTimestamp()); } - private String readResourceFileToString(String fileName) throws Exception { return Files.readString(Paths.get(getClass().getResource(fileName).toURI())); } diff --git a/src/test/java/org/dependencytrack/tasks/scanners/SnykAnalysisTaskTest.java b/src/test/java/org/dependencytrack/tasks/scanners/SnykAnalysisTaskTest.java index ab03102770..781cc53f6e 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/SnykAnalysisTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/SnykAnalysisTaskTest.java @@ -262,7 +262,7 @@ public void testAnalyzeWithRateLimiting() { } ], "links": { - "self": "/orgs/fd53e445-dc38-4b25-9c8a-5f68ed79f537/packages/pkg%3Amaven%2Fcom.fasterxml.woodstox%2Fwoodstox-core%405.0.0/issues?version=2022-11-14&limit=1000&offset=0" + "self": "/orgs/fd53e445-dc38-4b25-9c8a-5f68ed79f537/packages/pkg%3Amaven%2Fcom.fasterxml.woodstox%2Fwoodstox-core%405.0.0/issues?version=2023-01-04&limit=1000&offset=0" }, "meta": { "package": { @@ -354,7 +354,7 @@ public void testAnalyzeWithNoIssues() { }, "data": [], "links": { - "self": "/orgs/da563045-a462-421a-ae47-53239fe46612/packages/pkg%3Amaven%2Fcom.fasterxml.woodstox%2Fwoodstox-core%406.4.0/issues?version=2022-11-14&limit=1000&offset=0" + "self": "/orgs/da563045-a462-421a-ae47-53239fe46612/packages/pkg%3Amaven%2Fcom.fasterxml.woodstox%2Fwoodstox-core%406.4.0/issues?version=2023-01-04&limit=1000&offset=0" }, "meta": { "package": { @@ -563,7 +563,7 @@ public void testAnalyzeWithDeprecatedApiVersion() throws Exception { }, "data": [], "links": { - "self": "/orgs/da563045-a462-421a-ae47-53239fe46612/packages/pkg%3Amaven%2Fcom.fasterxml.woodstox%2Fwoodstox-core%406.4.0/issues?version=2022-11-14&limit=1000&offset=0" + "self": "/orgs/da563045-a462-421a-ae47-53239fe46612/packages/pkg%3Amaven%2Fcom.fasterxml.woodstox%2Fwoodstox-core%406.4.0/issues?version=2023-01-04&limit=1000&offset=0" }, "meta": { "package": { diff --git a/src/test/java/org/dependencytrack/tasks/scanners/VulnDBAnalysisTaskTest.java b/src/test/java/org/dependencytrack/tasks/scanners/VulnDBAnalysisTaskTest.java new file mode 100644 index 0000000000..6ba0985abb --- /dev/null +++ b/src/test/java/org/dependencytrack/tasks/scanners/VulnDBAnalysisTaskTest.java @@ -0,0 +1,338 @@ +/* + * This file is part of Dependency-Track. + * + * 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) Steve Springett. All Rights Reserved. + */ +package org.dependencytrack.tasks.scanners; + +import alpine.model.IConfigProperty; +import alpine.notification.Notification; +import alpine.notification.NotificationService; +import alpine.notification.Subscriber; +import alpine.notification.Subscription; +import alpine.security.crypto.DataEncryption; +import org.apache.http.HttpHeaders; +import org.assertj.core.api.SoftAssertions; +import org.dependencytrack.PersistenceCapableTest; +import org.dependencytrack.event.VulnDbAnalysisEvent; +import org.dependencytrack.model.Component; +import org.dependencytrack.model.ComponentAnalysisCache; +import org.dependencytrack.model.Project; +import org.dependencytrack.model.Severity; +import org.dependencytrack.model.Vulnerability; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.mockserver.integration.ClientAndServer; +import org.mockserver.model.Header; + +import javax.jdo.Query; +import javax.json.Json; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_VULNDB_ENABLED; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_VULNDB_OAUTH1_CONSUMER_KEY; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_VULNDB_OAUTH1_CONSUMER_SECRET; +import static org.mockserver.model.HttpRequest.request; +import static org.mockserver.model.HttpResponse.response; + +public class VulnDBAnalysisTaskTest extends PersistenceCapableTest { + + private static ClientAndServer mockServer; + + @BeforeClass + public static void beforeClass() { + NotificationService.getInstance().subscribe(new Subscription(NotificationSubscriber.class)); + mockServer = ClientAndServer.startClientAndServer(1080); + } + + @Before + public void setUp() throws Exception { + qm.createConfigProperty(SCANNER_VULNDB_ENABLED.getGroupName(), + SCANNER_VULNDB_ENABLED.getPropertyName(), + "true", + IConfigProperty.PropertyType.BOOLEAN, + "vulndb"); + qm.createConfigProperty(SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD.getGroupName(), + SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD.getPropertyName(), + "86400", + IConfigProperty.PropertyType.STRING, + "cache"); + qm.createConfigProperty(SCANNER_VULNDB_OAUTH1_CONSUMER_KEY.getGroupName(), + SCANNER_VULNDB_OAUTH1_CONSUMER_KEY.getPropertyName(), + DataEncryption.encryptAsString("secret"), + IConfigProperty.PropertyType.STRING, + "secret"); + qm.createConfigProperty(SCANNER_VULNDB_OAUTH1_CONSUMER_SECRET.getGroupName(), + SCANNER_VULNDB_OAUTH1_CONSUMER_SECRET.getPropertyName(), + DataEncryption.encryptAsString("secret"), + IConfigProperty.PropertyType.STRING, + "secret"); + } + + @After + public void tearDown() { + mockServer.reset(); + NOTIFICATIONS.clear(); + } + + @AfterClass + public static void afterClass() { + mockServer.stop(); + NotificationService.getInstance().unsubscribe(new Subscription(NotificationSubscriber.class)); + } + + @Test + public void testIsCapable() { + final var asserts = new SoftAssertions(); + + for (final Map.Entry test : Map.of( + "cpe:2.3:a:apache:log4j:2.0:-:*:*:*:*:*:*", true, + "cpe:2.3:h:siemens:sppa-t3000_ses3000:-:*:*:*:*:*:*:*", true + ).entrySet()) { + final var component = new Component(); + component.setCpe(test.getKey()); + asserts.assertThat(new VulnDbAnalysisTask("http://localhost:1080").isCapable(component)).isEqualTo(test.getValue()); + } + + asserts.assertAll(); + } + + @Test + public void testAnalyzeWithOneIssue() { + mockServer + .when(request() + .withMethod("GET") + .withPath("/api/v1/vulnerabilities/find_by_cpe") + .withHeader(new Header("X-User-Agent", "Dependency Track (https://github.com/DependencyTrack/dependency-track)")) + .withQueryStringParameter("cpe", "cpe:2.3:h:siemens:sppa-t3000_ses3000:-:*:*:*:*:*:*:*")) + .respond(response() + .withStatusCode(200) + .withHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.api+json") + .withBody(""" + { + "current_page": 1, + "total_entries": 1, + "results": [ + { + "vulndb_id": 1, + "title": "test title", + "classifications": [ + { + "id": 1, + "name": "test vulnerability", + "longname": "test vulnerability 1 1", + "description": "test test", + "mediumtext": "some text" + } + ], + "authors": [ + { + "id": 23, + "name": "test author", + "company": "test company" + } + ], + "ext_references": [ + { + "type": "external test reference", + "value": "external test reference value" + } + ], + "ext_texts": [ + { + "type": "external test texts", + "value": "external test texts value" + } + ], + "cvss_metrics": [ + + ], + "cvss_version_three_metrics": [ + + ], + "nvd_additional_information": [ + { + "summary": "test summary", + "cwe_id": "test1", + "cve_id": "test4" + } + ], + "vendors": [ + { + "vendor": { + "id": 1, + "name": "vendor one test", + "short_name": "test", + "vendor_url": "http://test.com", + "products": [ + { + "id": 45, + "name": "test product name", + "versions": [ + { + "id": 2, + "name": "version 2", + "affected": false, + "cpe": [ + { + "cpe": "test cpe", + "type": "test type" + } + ] + } + ] + } + ] + } + } + ] + } + ] + } + """)); + + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, null, false); + + var component = new Component(); + component.setProject(project); + component.setGroup("com.fasterxml.woodstox"); + component.setName("woodstox-core"); + component.setVersion("6.4.0"); + component.setCpe("cpe:2.3:h:siemens:sppa-t3000_ses3000:-:*:*:*:*:*:*:*"); + component = qm.createComponent(component, false); + + new VulnDbAnalysisTask("http://localhost:1080").inform(new VulnDbAnalysisEvent(component)); + + final List vulnerabilities = qm.getAllVulnerabilities(component); + + assertThat(vulnerabilities).hasSize(1); + + final Query cacheQuery = qm.getPersistenceManager().newQuery(ComponentAnalysisCache.class); + final List cacheEntries = cacheQuery.executeList(); + assertThat(cacheEntries).hasSize(1); + + final ComponentAnalysisCache cacheEntry = cacheEntries.get(0); + assertThat(cacheEntry.getTarget()).isEqualTo("cpe:2.3:h:siemens:sppa-t3000_ses3000:-:*:*:*:*:*:*:*"); + List result = new ArrayList(); + result.add(1); + assertThat(cacheEntry.getResult()) + .containsEntry("vulnIds", Json.createArrayBuilder(result).build()); + } + + @Test + public void testAnalyzeWithNoIssue() { + mockServer + .when(request() + .withMethod("GET") + .withPath("/api/v1/vulnerabilities/find_by_cpe") + .withHeader(new Header("X-User-Agent", "Dependency Track (https://github.com/DependencyTrack/dependency-track)")) + .withQueryStringParameter("cpe", "cpe:2.3:h:siemens:sppa-t3000_ses3000:-:*:*:*:*:*:*:*")) + .respond(response() + .withStatusCode(200) + .withHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.api+json") + .withBody(""" + { + "current_page": 1, + "total_entries": 1, + "results": [] + } + """)); + + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, null, false); + + var component = new Component(); + component.setProject(project); + component.setGroup("com.fasterxml.woodstox"); + component.setName("woodstox-core"); + component.setVersion("6.4.0"); + component.setCpe("cpe:2.3:h:siemens:sppa-t3000_ses3000:-:*:*:*:*:*:*:*"); + component = qm.createComponent(component, false); + + new VulnDbAnalysisTask("http://localhost:1080").inform(new VulnDbAnalysisEvent(component)); + + final List vulnerabilities = qm.getAllVulnerabilities(component); + + assertThat(vulnerabilities).hasSize(0); + + final Query cacheQuery = qm.getPersistenceManager().newQuery(ComponentAnalysisCache.class); + final List cacheEntries = cacheQuery.executeList(); + assertThat(cacheEntries).hasSize(1); + + final ComponentAnalysisCache cacheEntry = cacheEntries.get(0); + assertThat(cacheEntry.getTarget()).isEqualTo("cpe:2.3:h:siemens:sppa-t3000_ses3000:-:*:*:*:*:*:*:*"); + assertThat(cacheEntry.getResult()) + .isNull(); + } + + @Test + public void testAnalyzeWithCurrentCache() { + var vuln = new Vulnerability(); + vuln.setVulnId("VULNDB-001"); + vuln.setSource(Vulnerability.Source.VULNDB); + vuln.setSeverity(Severity.HIGH); + vuln = qm.createVulnerability(vuln, false); + + qm.updateComponentAnalysisCache(ComponentAnalysisCache.CacheType.VULNERABILITY, "http://localhost:1080", + Vulnerability.Source.VULNDB.name(), "cpe:2.3:h:siemens:sppa-t3000_ses3000:-:*:*:*:*:*:*:*", new Date(), + Json.createObjectBuilder() + .add("vulnIds", Json.createArrayBuilder().add(vuln.getId())) + .build()); + + var project = new Project(); + project.setName("acme-app"); + project = qm.createProject(project, null, false); + + var component = new Component(); + component.setProject(project); + component.setGroup("com.fasterxml.woodstox"); + component.setName("woodstox-core"); + component.setVersion("5.0.0"); + component.setCpe("cpe:2.3:h:siemens:sppa-t3000_ses3000:-:*:*:*:*:*:*:*"); + component = qm.createComponent(component, false); + + new VulnDbAnalysisTask("http://localhost:1080").inform(new VulnDbAnalysisEvent(component)); + + final List vulnerabilities = qm.getAllVulnerabilities(component); + assertThat(vulnerabilities).hasSize(1); + + mockServer.verifyZeroInteractions(); + } + + private static final ConcurrentLinkedQueue NOTIFICATIONS = new ConcurrentLinkedQueue<>(); + + public static class NotificationSubscriber implements Subscriber { + + @Override + public void inform(final Notification notification) { + NOTIFICATIONS.add(notification); + } + + } + +} \ No newline at end of file