Skip to content

Commit

Permalink
Merge pull request #3661 from nscuro/ossindex-api-token-fallback
Browse files Browse the repository at this point in the history
Fall back to no authentication when OSS Index API token decryption fails
  • Loading branch information
nscuro authored May 1, 2024
2 parents 664ee25 + 21af7b2 commit 70a6017
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 32 deletions.
2 changes: 2 additions & 0 deletions docs/_posts/2024-xx-xx-v4.11.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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/[email protected]");
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/[email protected]"
]
}
""")));
}

@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/[email protected]");
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/[email protected]"
]
}
""")));
}

}

0 comments on commit 70a6017

Please sign in to comment.