diff --git a/README.md b/README.md index 1a5b270..de11c9b 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/core/src/main/java/io/snyk/plugins/artifactory/scanner/PackageValidator.java b/core/src/main/java/io/snyk/plugins/artifactory/scanner/PackageValidator.java new file mode 100644 index 0000000..4b4abc0 --- /dev/null +++ b/core/src/main/java/io/snyk/plugins/artifactory/scanner/PackageValidator.java @@ -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); + } + } + } + +} diff --git a/core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerModule.java b/core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerModule.java index dbe01a4..b50c831 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerModule.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerModule.java @@ -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; @@ -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; @@ -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) { @@ -113,79 +108,7 @@ private String getIssuesAsFormattedString(@Nonnull List 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); - } - } - } } diff --git a/core/src/test/java/io/snyk/plugins/artifactory/scanner/PackageValidatorTest.java b/core/src/test/java/io/snyk/plugins/artifactory/scanner/PackageValidatorTest.java new file mode 100644 index 0000000..c8f6cda --- /dev/null +++ b/core/src/test/java/io/snyk/plugins/artifactory/scanner/PackageValidatorTest.java @@ -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 vulnSeverities, List 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; + } + + +} diff --git a/core/src/test/java/io/snyk/plugins/artifactory/scanner/ScannerModuleTest.java b/core/src/test/java/io/snyk/plugins/artifactory/scanner/ScannerModuleTest.java index d54c98b..9c1c3ba 100644 --- a/core/src/test/java/io/snyk/plugins/artifactory/scanner/ScannerModuleTest.java +++ b/core/src/test/java/io/snyk/plugins/artifactory/scanner/ScannerModuleTest.java @@ -146,16 +146,6 @@ void testScanNpmItem_noVulns() throws Exception { assertEquals(0, tr.issues.vulnerabilities.size()); assertEquals("npm", tr.packageManager); assertEquals(testSetup.org, tr.organisation.id); - - verify(spyScanner, times(1)).validateVulnerabilityIssues( - eq(tr), - eq(repoPath) - ); - - verify(spyScanner, times(1)).validateLicenseIssues( - eq(tr), - eq(repoPath) - ); } @Test @@ -187,16 +177,6 @@ void testScanNpmItem_withVulns() throws Exception { assertTrue(tr.issues.vulnerabilities.size() > 0); assertEquals("npm", tr.packageManager); assertEquals(testSetup.org, tr.organisation.id); - - verify(spyScanner, times(1)).validateVulnerabilityIssues( - eq(tr), - eq(repoPath) - ); - - verify(spyScanner, times(0)).validateLicenseIssues( - any(), - any() - ); } @Test @@ -226,16 +206,6 @@ void testScanMavenItem_noVulns() throws Exception { assertEquals(0, tr.issues.vulnerabilities.size()); assertEquals("maven", tr.packageManager); assertEquals(testSetup.org, tr.organisation.id); - - verify(spyScanner, times(1)).validateVulnerabilityIssues( - eq(tr), - eq(repoPath) - ); - - verify(spyScanner, times(1)).validateLicenseIssues( - eq(tr), - eq(repoPath) - ); } @Test @@ -267,16 +237,6 @@ void testScanMavenItem_withVulns() throws Exception { assertTrue(tr.issues.vulnerabilities.size() > 0); assertEquals("maven", tr.packageManager); assertEquals(testSetup.org, tr.organisation.id); - - verify(spyScanner, times(1)).validateVulnerabilityIssues( - eq(tr), - eq(repoPath) - ); - - verify(spyScanner, times(0)).validateLicenseIssues( - any(), - any() - ); } @Test @@ -305,16 +265,6 @@ void testScanPythonItem_noVulns() throws Exception { assertEquals(0, tr.issues.vulnerabilities.size()); assertEquals("pip", tr.packageManager); assertEquals(testSetup.org, tr.organisation.id); - - verify(spyScanner, times(1)).validateVulnerabilityIssues( - eq(tr), - eq(repoPath) - ); - - verify(spyScanner, times(1)).validateLicenseIssues( - eq(tr), - eq(repoPath) - ); } @Test @@ -346,15 +296,5 @@ void testScanPythonItem_withVulns() throws Exception { assertEquals(6, tr.issues.vulnerabilities.size()); assertEquals("pip", tr.packageManager); assertEquals(testSetup.org, tr.organisation.id); - - verify(spyScanner, times(1)).validateVulnerabilityIssues( - eq(tr), - eq(repoPath) - ); - - verify(spyScanner, times(0)).validateLicenseIssues( - any(), - any() - ); } }