diff --git a/docs/_posts/2024-xx-xx-v4.11.0.md b/docs/_posts/2024-xx-xx-v4.11.0.md index c0af5067f3..242b226314 100644 --- a/docs/_posts/2024-xx-xx-v4.11.0.md +++ b/docs/_posts/2024-xx-xx-v4.11.0.md @@ -74,6 +74,7 @@ It is also available through [Artifact Hub](https://artifacthub.io/packages/helm * Gracefully handle unique constraint violations - [apiserver/#3648] * Log debug information upon possible secret key corruption - [apiserver/#3651] * Add support for worker pool drain timeout - [apiserver/#3657] +* Fall back to no authentication when OSS Index API token decryption fails - [apiserver/#3661] * Show component count in projects list - [frontend/#683] * Add current *fail*, *warn*, and *info* values to bottom of policy violation metrics - [frontend/#707] * Remove unused policy violation widget - [frontend/#710] @@ -250,6 +251,7 @@ Special thanks to everyone who contributed code to implement enhancements and fi [apiserver/#3651]: https://github.com/DependencyTrack/dependency-track/pull/3651 [apiserver/#3657]: https://github.com/DependencyTrack/dependency-track/pull/3657 [apiserver/#3659]: https://github.com/DependencyTrack/dependency-track/pull/3659 +[apiserver/#3661]: https://github.com/DependencyTrack/dependency-track/pull/3661 [frontend/#682]: https://github.com/DependencyTrack/frontend/pull/682 [frontend/#683]: https://github.com/DependencyTrack/frontend/pull/683 diff --git a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java index 37fba86f9d..07e9b2f4f4 100644 --- a/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java +++ b/src/main/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTask.java @@ -137,41 +137,45 @@ public AnalyzerIdentity getAnalyzerIdentity() { * {@inheritDoc} */ public void inform(final Event e) { - if (e instanceof OssIndexAnalysisEvent) { - if (!super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED)) { - return; - } - try (QueryManager qm = new QueryManager()) { - final ConfigProperty apiUsernameProperty = qm.getConfigProperty( - ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME.getGroupName(), - ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME.getPropertyName() - ); - final ConfigProperty apiTokenProperty = qm.getConfigProperty( - ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN.getGroupName(), - ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN.getPropertyName() - ); - if (apiUsernameProperty == null || apiUsernameProperty.getPropertyValue() == null - || apiTokenProperty == null || apiTokenProperty.getPropertyValue() == null) { - LOGGER.warn("An API username or token has not been specified for use with OSS Index. Using anonymous access"); - } else { - try { - apiUsername = apiUsernameProperty.getPropertyValue(); - apiToken = DebugDataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); - } catch (Exception ex) { - LOGGER.error("An error occurred decrypting the OSS Index API Token. Skipping", ex); - return; - } + if (!(e instanceof final OssIndexAnalysisEvent event)) { + return; + } + if (!super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED)) { + return; + } + + try (final var qm = new QueryManager()) { + final ConfigProperty apiUsernameProperty = qm.getConfigProperty( + ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME.getGroupName(), + ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME.getPropertyName() + ); + final ConfigProperty apiTokenProperty = qm.getConfigProperty( + ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN.getGroupName(), + ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN.getPropertyName() + ); + if (apiUsernameProperty == null || apiUsernameProperty.getPropertyValue() == null + || apiTokenProperty == null || apiTokenProperty.getPropertyValue() == null) { + LOGGER.warn("An API username or token has not been specified for use with OSS Index. Using anonymous access"); + } else { + try { + apiUsername = apiUsernameProperty.getPropertyValue(); + apiToken = DebugDataEncryption.decryptAsString(apiTokenProperty.getPropertyValue()); + } catch (Exception ex) { + // NB: OSS Index can be used without AuthN, however stricter rate limiting may apply. + // We favour "service degradation" over "service outage" here. Analysis will continue + // to work, although more retries may need to be performed until a new token is supplied. + LOGGER.error("An error occurred decrypting the OSS Index API Token; Continuing without authentication", ex); } - aliasSyncEnabled = super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ALIAS_SYNC_ENABLED); - } - final var event = (OssIndexAnalysisEvent) e; - LOGGER.info("Starting Sonatype OSS Index analysis task"); - vulnerabilityAnalysisLevel = event.getVulnerabilityAnalysisLevel(); - if (event.getComponents().size() > 0) { - analyze(event.getComponents()); } - LOGGER.info("Sonatype OSS Index analysis complete"); + aliasSyncEnabled = super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ALIAS_SYNC_ENABLED); + } + + LOGGER.info("Starting Sonatype OSS Index analysis task"); + vulnerabilityAnalysisLevel = event.getVulnerabilityAnalysisLevel(); + if (!event.getComponents().isEmpty()) { + analyze(event.getComponents()); } + LOGGER.info("Sonatype OSS Index analysis complete"); } /** diff --git a/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java b/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java index bb32c1cda8..414e208c4f 100644 --- a/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java +++ b/src/test/java/org/dependencytrack/tasks/scanners/OssIndexAnalysisTaskTest.java @@ -1,6 +1,8 @@ package org.dependencytrack.tasks.scanners; +import alpine.security.crypto.DataEncryption; import com.github.packageurl.PackageURL; +import com.github.tomakehurst.wiremock.client.BasicCredentials; import com.github.tomakehurst.wiremock.junit.WireMockRule; import org.assertj.core.api.SoftAssertions; import org.dependencytrack.PersistenceCapableTest; @@ -30,6 +32,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNoException; import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_ANALYSIS_CACHE_VALIDITY_PERIOD; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_OSSINDEX_API_TOKEN; +import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_OSSINDEX_API_USERNAME; import static org.dependencytrack.model.ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED; public class OssIndexAnalysisTaskTest extends PersistenceCapableTest { @@ -185,4 +189,104 @@ public void testAnalyzeWithRateLimiting() { """))); } + @Test + public void testAnalyzeWithAuthentication() throws Exception { + qm.createConfigProperty( + SCANNER_OSSINDEX_API_USERNAME.getGroupName(), + SCANNER_OSSINDEX_API_USERNAME.getPropertyName(), + "foo", + SCANNER_OSSINDEX_API_USERNAME.getPropertyType(), + SCANNER_OSSINDEX_API_USERNAME.getDescription() + ); + qm.createConfigProperty( + SCANNER_OSSINDEX_API_TOKEN.getGroupName(), + SCANNER_OSSINDEX_API_TOKEN.getPropertyName(), + DataEncryption.encryptAsString("apiToken"), + SCANNER_OSSINDEX_API_TOKEN.getPropertyType(), + SCANNER_OSSINDEX_API_TOKEN.getDescription() + ); + + wireMock.stubFor(post(urlPathEqualTo("/api/v3/component-report")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/vnd.ossindex.component-report.v1+json") + .withBody("[]"))); + + var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + var component = new Component(); + component.setProject(project); + component.setGroup("com.fasterxml.jackson.core"); + component.setName("jackson-databind"); + component.setVersion("2.13.1"); + component.setPurl("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1"); + qm.persist(component); + + assertThatNoException().isThrownBy(() -> analysisTask.inform(new OssIndexAnalysisEvent(component))); + + wireMock.verify(postRequestedFor(urlPathEqualTo("/api/v3/component-report")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo(ManagedHttpClientFactory.getUserAgent())) + .withBasicAuth(new BasicCredentials("foo", "apiToken")) + .withRequestBody(equalToJson(""" + { + "coordinates": [ + "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1" + ] + } + """))); + } + + @Test + public void testAnalyzeWithApiTokenDecryptionError() { + qm.createConfigProperty( + SCANNER_OSSINDEX_API_USERNAME.getGroupName(), + SCANNER_OSSINDEX_API_USERNAME.getPropertyName(), + "foo", + SCANNER_OSSINDEX_API_USERNAME.getPropertyType(), + SCANNER_OSSINDEX_API_USERNAME.getDescription() + ); + qm.createConfigProperty( + SCANNER_OSSINDEX_API_TOKEN.getGroupName(), + SCANNER_OSSINDEX_API_TOKEN.getPropertyName(), + "notAnEncryptedValue", + SCANNER_OSSINDEX_API_TOKEN.getPropertyType(), + SCANNER_OSSINDEX_API_TOKEN.getDescription() + ); + + wireMock.stubFor(post(urlPathEqualTo("/api/v3/component-report")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/vnd.ossindex.component-report.v1+json") + .withBody("[]"))); + + var project = new Project(); + project.setName("acme-app"); + qm.persist(project); + + var component = new Component(); + component.setProject(project); + component.setGroup("com.fasterxml.jackson.core"); + component.setName("jackson-databind"); + component.setVersion("2.13.1"); + component.setPurl("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1"); + qm.persist(component); + + assertThatNoException().isThrownBy(() -> analysisTask.inform(new OssIndexAnalysisEvent(component))); + + wireMock.verify(postRequestedFor(urlPathEqualTo("/api/v3/component-report")) + .withHeader("Content-Type", equalTo("application/json")) + .withHeader("User-Agent", equalTo(ManagedHttpClientFactory.getUserAgent())) + .withoutHeader("Authorization") + .withRequestBody(equalToJson(""" + { + "coordinates": [ + "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1" + ] + } + """))); + } + } \ No newline at end of file