Skip to content

Commit

Permalink
chore: extract Package Validator [OSM-2240]
Browse files Browse the repository at this point in the history
In preparation for feeding cached test result into the validation step,
extracting the validator to its own class.

This commit does not introduce any behavioural changes, only lays
the ground for the next iteration. In a follow-up step, we will allow
skipping of the test so that gatekeeping is based on cached results.
  • Loading branch information
jacek-rzrz committed Oct 29, 2024
1 parent a1f2b8d commit 02b0f98
Show file tree
Hide file tree
Showing 5 changed files with 235 additions and 141 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Then unpack the release into artifactory's plugins folder:
unzip -o distribution/target/artifactory-snyk-security-plugin-LOCAL-SNAPSHOT.zip -d distribution/docker/etc/artifactory/
```

Set your Snyk org ID and API token inside `distribution/docker/etc/artifactory/snykSecurityPlugin.properties`
Set your Snyk org ID and API token inside `distribution/docker/etc/artifactory/plugins/snykSecurityPlugin.properties`
and restart Artifactory. Check [the logs](http://localhost:8082/ui/admin/artifactory/advanced/system_logs)
to confirm the plugin gets loaded.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package io.snyk.plugins.artifactory.scanner;

import io.snyk.plugins.artifactory.configuration.ConfigurationModule;
import io.snyk.plugins.artifactory.configuration.PluginConfiguration;
import io.snyk.sdk.model.Severity;
import io.snyk.sdk.model.TestResult;
import org.artifactory.exception.CancelException;
import org.artifactory.repo.RepoPath;
import org.artifactory.repo.Repositories;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.snyk.plugins.artifactory.configuration.ArtifactProperty.ISSUE_LICENSES_FORCE_DOWNLOAD;
import static io.snyk.plugins.artifactory.configuration.ArtifactProperty.ISSUE_VULNERABILITIES_FORCE_DOWNLOAD;
import static java.lang.String.format;

public class PackageValidator {

private static final Logger LOG = LoggerFactory.getLogger(PackageValidator.class);

private final ConfigurationModule configurationModule;
private final Repositories repositories;

public PackageValidator(ConfigurationModule configurationModule, Repositories repositories) {
this.configurationModule = configurationModule;
this.repositories = repositories;
}

public void validate(TestResult testResult, RepoPath repoPath) {
validateVulnerabilityIssues(testResult, repoPath);
validateLicenseIssues(testResult, repoPath);
}

private void validateVulnerabilityIssues(TestResult testResult, RepoPath repoPath) {
final String vulnerabilitiesForceDownloadProperty = ISSUE_VULNERABILITIES_FORCE_DOWNLOAD.propertyKey();
final String vulnerabilitiesForceDownload = repositories.getProperty(repoPath, vulnerabilitiesForceDownloadProperty);
final boolean forceDownload = "true".equalsIgnoreCase(vulnerabilitiesForceDownload);
if (forceDownload) {
LOG.debug("Allowing download. Artifact Property \"{}\" is \"true\". {}", vulnerabilitiesForceDownloadProperty, repoPath);
return;
}

Severity vulnerabilityThreshold = Severity.of(configurationModule.getPropertyOrDefault(PluginConfiguration.SCANNER_VULNERABILITY_THRESHOLD));
if (vulnerabilityThreshold == Severity.LOW) {
if (!testResult.issues.vulnerabilities.isEmpty()) {
LOG.debug("Found vulnerabilities in {} returning 403", repoPath);
throw new CancelException(format("Artifact has vulnerabilities. %s", repoPath), 403);
}
} else if (vulnerabilityThreshold == Severity.MEDIUM) {
long count = testResult.issues.vulnerabilities.stream()
.filter(vulnerability -> vulnerability.severity == Severity.MEDIUM || vulnerability.severity == Severity.HIGH || vulnerability.severity == Severity.CRITICAL)
.count();
if (count > 0) {
LOG.debug("Found {} vulnerabilities in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has vulnerabilities with medium, high or critical severity. %s", repoPath), 403);
}
} else if (vulnerabilityThreshold == Severity.HIGH) {
long count = testResult.issues.vulnerabilities.stream()
.filter(vulnerability -> vulnerability.severity == Severity.HIGH || vulnerability.severity == Severity.CRITICAL)
.count();
if (count > 0) {
LOG.debug("Found {}, vulnerabilities in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has vulnerabilities with high or critical severity. %s", repoPath), 403);
}
} else if (vulnerabilityThreshold == Severity.CRITICAL) {
long count = testResult.issues.vulnerabilities.stream()
.filter(vulnerability -> vulnerability.severity == Severity.CRITICAL)
.count();
if (count > 0) {
LOG.debug("Found {} vulnerabilities in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has vulnerabilities with critical severity. %s", repoPath), 403);
}
}
}

private void validateLicenseIssues(TestResult testResult, RepoPath repoPath) {
final String licensesForceDownloadProperty = ISSUE_LICENSES_FORCE_DOWNLOAD.propertyKey();
final String licensesForceDownload = repositories.getProperty(repoPath, licensesForceDownloadProperty);
final boolean forceDownload = "true".equalsIgnoreCase(licensesForceDownload);
if (forceDownload) {
LOG.debug("Allowing download. Artifact Property \"{}\" is \"true\". {}", repoPath, licensesForceDownloadProperty);
return;
}

Severity licensesThreshold = Severity.of(configurationModule.getPropertyOrDefault(PluginConfiguration.SCANNER_LICENSE_THRESHOLD));
if (licensesThreshold == Severity.LOW) {
if (!testResult.issues.licenses.isEmpty()) {
LOG.debug("Found license issues in {} returning 403", repoPath);
throw new CancelException(format("Artifact has license issues. %s", repoPath), 403);
}
} else if (licensesThreshold == Severity.MEDIUM) {
long count = testResult.issues.licenses.stream()
.filter(vulnerability -> vulnerability.severity == Severity.MEDIUM || vulnerability.severity == Severity.HIGH)
.count();
if (count > 0) {
LOG.debug("Found {} license issues in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has license issues with medium or high severity. %s", repoPath), 403);
}
} else if (licensesThreshold == Severity.HIGH) {
long count = testResult.issues.licenses.stream()
.filter(vulnerability -> vulnerability.severity == Severity.HIGH)
.count();
if (count > 0) {
LOG.debug("Found {} license issues in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has license issues with high severity. %s", repoPath), 403);
}
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,14 @@

import io.snyk.plugins.artifactory.configuration.ArtifactProperty;
import io.snyk.plugins.artifactory.configuration.ConfigurationModule;
import io.snyk.plugins.artifactory.configuration.PluginConfiguration;
import io.snyk.plugins.artifactory.exception.CannotScanException;
import io.snyk.sdk.api.v1.SnykClient;
import io.snyk.sdk.model.Issue;
import io.snyk.sdk.model.Severity;
import io.snyk.sdk.model.TestResult;
import org.artifactory.exception.CancelException;
import org.artifactory.fs.FileLayoutInfo;
import org.artifactory.repo.RepoPath;
import org.artifactory.repo.Repositories;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.util.List;
Expand All @@ -26,8 +22,6 @@

public class ScannerModule {

private static final Logger LOG = LoggerFactory.getLogger(ScannerModule.class);

private final ConfigurationModule configurationModule;
private final Repositories repositories;
private final MavenScanner mavenScanner;
Expand All @@ -52,8 +46,9 @@ public void scanArtifact(@Nonnull RepoPath repoPath) {

TestResult testResult = scanner.scan(fileLayoutInfo, repoPath);
updateProperties(repoPath, testResult);
validateVulnerabilityIssues(testResult, repoPath);
validateLicenseIssues(testResult, repoPath);

PackageValidator validator = new PackageValidator(configurationModule, repositories);
validator.validate(testResult, repoPath);
}

protected PackageScanner getScannerForPackageType(String path) {
Expand Down Expand Up @@ -113,79 +108,7 @@ private String getIssuesAsFormattedString(@Nonnull List<? extends Issue> issues)
return format("%d critical, %d high, %d medium, %d low", countCriticalSeverities, countHighSeverities, countMediumSeverities, countLowSeverities);
}

protected void validateVulnerabilityIssues(TestResult testResult, RepoPath repoPath) {
final String vulnerabilitiesForceDownloadProperty = ISSUE_VULNERABILITIES_FORCE_DOWNLOAD.propertyKey();
final String vulnerabilitiesForceDownload = repositories.getProperty(repoPath, vulnerabilitiesForceDownloadProperty);
final boolean forceDownload = "true".equalsIgnoreCase(vulnerabilitiesForceDownload);
if (forceDownload) {
LOG.debug("Allowing download. Artifact Property \"{}\" is \"true\". {}", vulnerabilitiesForceDownloadProperty, repoPath);
return;
}

Severity vulnerabilityThreshold = Severity.of(configurationModule.getPropertyOrDefault(PluginConfiguration.SCANNER_VULNERABILITY_THRESHOLD));
if (vulnerabilityThreshold == Severity.LOW) {
if (!testResult.issues.vulnerabilities.isEmpty()) {
LOG.debug("Found vulnerabilities in {} returning 403", repoPath);
throw new CancelException(format("Artifact has vulnerabilities. %s", repoPath), 403);
}
} else if (vulnerabilityThreshold == Severity.MEDIUM) {
long count = testResult.issues.vulnerabilities.stream()
.filter(vulnerability -> vulnerability.severity == Severity.MEDIUM || vulnerability.severity == Severity.HIGH || vulnerability.severity == Severity.CRITICAL)
.count();
if (count > 0) {
LOG.debug("Found {} vulnerabilities in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has vulnerabilities with medium, high or critical severity. %s", repoPath), 403);
}
} else if (vulnerabilityThreshold == Severity.HIGH) {
long count = testResult.issues.vulnerabilities.stream()
.filter(vulnerability -> vulnerability.severity == Severity.HIGH || vulnerability.severity == Severity.CRITICAL)
.count();
if (count > 0) {
LOG.debug("Found {}, vulnerabilities in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has vulnerabilities with high or critical severity. %s", repoPath), 403);
}
} else if (vulnerabilityThreshold == Severity.CRITICAL) {
long count = testResult.issues.vulnerabilities.stream()
.filter(vulnerability -> vulnerability.severity == Severity.CRITICAL)
.count();
if (count > 0) {
LOG.debug("Found {} vulnerabilities in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has vulnerabilities with critical severity. %s", repoPath), 403);
}
}
}

protected void validateLicenseIssues(TestResult testResult, RepoPath repoPath) {
final String licensesForceDownloadProperty = ISSUE_LICENSES_FORCE_DOWNLOAD.propertyKey();
final String licensesForceDownload = repositories.getProperty(repoPath, licensesForceDownloadProperty);
final boolean forceDownload = "true".equalsIgnoreCase(licensesForceDownload);
if (forceDownload) {
LOG.debug("Allowing download. Artifact Property \"{}\" is \"true\". {}", repoPath, licensesForceDownloadProperty);
return;
}

Severity licensesThreshold = Severity.of(configurationModule.getPropertyOrDefault(PluginConfiguration.SCANNER_LICENSE_THRESHOLD));
if (licensesThreshold == Severity.LOW) {
if (!testResult.issues.licenses.isEmpty()) {
LOG.debug("Found license issues in {} returning 403", repoPath);
throw new CancelException(format("Artifact has license issues. %s", repoPath), 403);
}
} else if (licensesThreshold == Severity.MEDIUM) {
long count = testResult.issues.licenses.stream()
.filter(vulnerability -> vulnerability.severity == Severity.MEDIUM || vulnerability.severity == Severity.HIGH)
.count();
if (count > 0) {
LOG.debug("Found {} license issues in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has license issues with medium or high severity. %s", repoPath), 403);
}
} else if (licensesThreshold == Severity.HIGH) {
long count = testResult.issues.licenses.stream()
.filter(vulnerability -> vulnerability.severity == Severity.HIGH)
.count();
if (count > 0) {
LOG.debug("Found {} license issues in {} returning 403", count, repoPath);
throw new CancelException(format("Artifact has license issues with high severity. %s", repoPath), 403);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package io.snyk.plugins.artifactory.scanner;

import io.snyk.plugins.artifactory.configuration.ConfigurationModule;
import io.snyk.plugins.artifactory.configuration.PluginConfiguration;
import io.snyk.sdk.model.*;
import org.artifactory.exception.CancelException;
import org.artifactory.repo.RepoPath;
import org.artifactory.repo.Repositories;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Properties;
import java.util.stream.Collectors;

import static io.snyk.plugins.artifactory.configuration.ArtifactProperty.ISSUE_LICENSES_FORCE_DOWNLOAD;
import static io.snyk.plugins.artifactory.configuration.ArtifactProperty.ISSUE_VULNERABILITIES_FORCE_DOWNLOAD;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

class PackageValidatorTest {

@Test
void validate_severityBelowThreshold_allowed() {
Repositories repositories = mock(Repositories.class);
RepoPath repoPath = mock(RepoPath.class);

ConfigurationModule configurationModule = pluginConfig(Severity.MEDIUM, Severity.CRITICAL);

PackageValidator validator = new PackageValidator(configurationModule, repositories);
TestResult testResult = getTestResult(List.of(Severity.LOW), List.of(Severity.MEDIUM));

assertDoesNotThrow(() -> validator.validate(testResult, repoPath));
}

@Test
void validate_vulnIssueAboveThreshold_forbidden() {
Repositories repositories = mock(Repositories.class);
RepoPath repoPath = mock(RepoPath.class);

ConfigurationModule configurationModule = pluginConfig(Severity.HIGH, Severity.LOW);

PackageValidator validator = new PackageValidator(configurationModule, repositories);
TestResult testResult = getTestResult(List.of(Severity.HIGH), List.of());

assertThrows(CancelException.class, () -> validator.validate(testResult, repoPath));
}

@Test
void validate_vulnForceDownload_allowed() {
Repositories repositories = mock(Repositories.class);
RepoPath repoPath = mock(RepoPath.class);

when(repositories.getProperty(repoPath, ISSUE_VULNERABILITIES_FORCE_DOWNLOAD.propertyKey())).thenReturn("true");

ConfigurationModule configurationModule = pluginConfig(Severity.HIGH, Severity.LOW);

PackageValidator validator = new PackageValidator(configurationModule, repositories);
TestResult testResult = getTestResult(List.of(Severity.HIGH), List.of());

assertDoesNotThrow(() -> validator.validate(testResult, repoPath));
}

@Test
void validate_licenseIssueAboveThreshold_forbidden() {
Repositories repositories = mock(Repositories.class);
RepoPath repoPath = mock(RepoPath.class);

ConfigurationModule configurationModule = pluginConfig(Severity.LOW, Severity.MEDIUM);

PackageValidator validator = new PackageValidator(configurationModule, repositories);
TestResult testResult = getTestResult(List.of(), List.of(Severity.MEDIUM));

assertThrows(CancelException.class, () -> validator.validate(testResult, repoPath));
}

@Test
void validate_licenseForceDownload_allowed() {
Repositories repositories = mock(Repositories.class);
RepoPath repoPath = mock(RepoPath.class);

when(repositories.getProperty(repoPath, ISSUE_LICENSES_FORCE_DOWNLOAD.propertyKey())).thenReturn("true");

ConfigurationModule configurationModule = pluginConfig(Severity.LOW, Severity.MEDIUM);

PackageValidator validator = new PackageValidator(configurationModule, repositories);
TestResult testResult = getTestResult(List.of(), List.of(Severity.MEDIUM));

assertDoesNotThrow(() -> validator.validate(testResult, repoPath));
}

private static @NotNull ConfigurationModule pluginConfig(Severity vulnThreshold, Severity licenseThreshold) {
Properties properties = new Properties();
properties.setProperty(PluginConfiguration.SCANNER_VULNERABILITY_THRESHOLD.propertyKey(), vulnThreshold.getSeverityLevel());
properties.setProperty(PluginConfiguration.SCANNER_LICENSE_THRESHOLD.propertyKey(), licenseThreshold.getSeverityLevel());
return new ConfigurationModule(properties);
}

private static @NotNull TestResult getTestResult(List<Severity> vulnSeverities, List<Severity> licenseSeverities) {
TestResult testResult = new TestResult();

testResult.issues = new Issues();

testResult.issues.vulnerabilities = vulnSeverities.stream().map(severity -> {
Vulnerability vuln = new Vulnerability();
vuln.severity = severity;
return vuln;
}).collect(Collectors.toList());

testResult.issues.licenses = licenseSeverities.stream().map(severity -> {
Issue issue = new Vulnerability();
issue.severity = severity;
return issue;
}).collect(Collectors.toList());
return testResult;
}


}
Loading

0 comments on commit 02b0f98

Please sign in to comment.