Skip to content

Commit

Permalink
Merge pull request #105 from snyk/chore/extract-package-validator
Browse files Browse the repository at this point in the history
chore: extract Package Validator [OSM-2240]
  • Loading branch information
jacek-rzrz authored Oct 30, 2024
2 parents a1f2b8d + 02b0f98 commit d64cf76
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 d64cf76

Please sign in to comment.