From 930d590c363750d220fbfc3656423b23e0976579 Mon Sep 17 00:00:00 2001 From: Jacek Rzeniewicz Date: Wed, 20 Nov 2024 18:12:49 +0000 Subject: [PATCH] feat: add support for Ruby Gems --- .../artifactory/snykSecurityPlugin.properties | 6 ++ .../configuration/PluginConfiguration.java | 1 + .../artifactory/ecosystem/Ecosystem.java | 7 +- .../RepositoryMetadataEcosystemResolver.java | 2 +- .../exception/SnykAPIFailureException.java | 3 +- .../artifactory/model/IssueSummary.java | 5 ++ .../artifactory/scanner/ArtifactCache.java | 2 +- .../artifactory/scanner/PackageValidator.java | 4 +- .../artifactory/scanner/ScannerModule.java | 13 ++++ .../artifactory/scanner/ScannerResolver.java | 8 ++- .../artifactory/scanner/SnykDetailsUrl.java | 2 +- .../scanner/TestResultConverter.java | 10 +++ .../scanner/rubygems/RubyGemsPackage.java | 35 ++++++++++ .../scanner/rubygems/RubyGemsScanner.java | 63 ++++++++++++++++++ .../artifactory/ecosystem/EcosystemTest.java | 15 +++-- .../scanner/rubygems/RubyGemsPackageTest.java | 28 ++++++++ .../scanner/rubygems/RubyGemsScannerTest.java | 66 +++++++++++++++++++ .../snyk/sdk/model/purl/IssueAttribute.java | 21 ++++++ .../io/snyk/sdk/model/purl/PurlIssue.java | 11 ++++ .../io/snyk/sdk/model/purl/PurlIssues.java | 15 +++++ 20 files changed, 302 insertions(+), 15 deletions(-) create mode 100644 core/src/main/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsPackage.java create mode 100644 core/src/main/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsScanner.java create mode 100644 core/src/test/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsPackageTest.java create mode 100644 core/src/test/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsScannerTest.java create mode 100644 snyk-sdk/src/main/java/io/snyk/sdk/model/purl/IssueAttribute.java create mode 100644 snyk-sdk/src/main/java/io/snyk/sdk/model/purl/PurlIssue.java create mode 100644 snyk-sdk/src/main/java/io/snyk/sdk/model/purl/PurlIssues.java diff --git a/core/src/main/groovy/io/snyk/plugins/artifactory/snykSecurityPlugin.properties b/core/src/main/groovy/io/snyk/plugins/artifactory/snykSecurityPlugin.properties index 47b9273..ccb0634 100644 --- a/core/src/main/groovy/io/snyk/plugins/artifactory/snykSecurityPlugin.properties +++ b/core/src/main/groovy/io/snyk/plugins/artifactory/snykSecurityPlugin.properties @@ -23,6 +23,7 @@ snyk.api.organization= # Default: https://api.snyk.io/ #snyk.api.url=https://api.snyk.io/ + # Path to an SSL Certificate for Snyk API in PEM format. #snyk.api.sslCertificatePath= @@ -95,3 +96,8 @@ snyk.api.organization= # Accepts: "true", "false" # Default: "false" #snyk.scanner.packageType.pypi=false + +# Scan Ruby Gems repositories. +# Accepts: "true", "false" +# Default: "false" +#snyk.scanner.packageType.gems=false diff --git a/core/src/main/java/io/snyk/plugins/artifactory/configuration/PluginConfiguration.java b/core/src/main/java/io/snyk/plugins/artifactory/configuration/PluginConfiguration.java index 047cd58..d8c8648 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/configuration/PluginConfiguration.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/configuration/PluginConfiguration.java @@ -19,6 +19,7 @@ public enum PluginConfiguration implements Configuration { SCANNER_PACKAGE_TYPE_MAVEN("snyk.scanner.packageType.maven", "true"), SCANNER_PACKAGE_TYPE_NPM("snyk.scanner.packageType.npm", "true"), SCANNER_PACKAGE_TYPE_PYPI("snyk.scanner.packageType.pypi", "false"), + SCANNER_PACKAGE_TYPE_RUBYGEMS("snyk.scanner.packageType.gems", "false"), TEST_CONTINUOUSLY("snyk.scanner.test.continuously","false"), TEST_FREQUENCY_HOURS("snyk.scanner.frequency.hours", "168"), EXTEND_TEST_DEADLINE_HOURS("snyk.scanner.extendTestDeadline.hours", "24"); diff --git a/core/src/main/java/io/snyk/plugins/artifactory/ecosystem/Ecosystem.java b/core/src/main/java/io/snyk/plugins/artifactory/ecosystem/Ecosystem.java index 2f9a4c8..aaad2bf 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/ecosystem/Ecosystem.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/ecosystem/Ecosystem.java @@ -8,10 +8,10 @@ public enum Ecosystem { - MAVEN(PluginConfiguration.SCANNER_PACKAGE_TYPE_MAVEN), NPM(PluginConfiguration.SCANNER_PACKAGE_TYPE_NPM), PYPI(PluginConfiguration.SCANNER_PACKAGE_TYPE_PYPI), + RUBYGEMS(PluginConfiguration.SCANNER_PACKAGE_TYPE_RUBYGEMS), ; private static final Logger LOG = LoggerFactory.getLogger(Ecosystem.class); @@ -26,14 +26,15 @@ public PluginConfiguration getConfigProperty() { return configProperty; } - public static Optional fromPackageType(String artifactoryPackageType) { + public static Optional match(String artifactoryPackageType, String artifactPath) { switch (artifactoryPackageType.toLowerCase()) { case "maven": return Optional.of(MAVEN); case "npm": return Optional.of(NPM); case "pypi": return Optional.of(PYPI); + case "gems": return artifactPath.endsWith(".gem") ? Optional.of(RUBYGEMS) : Optional.empty(); } - LOG.error("Unknown package type: {}", artifactoryPackageType); + LOG.info("Unknown package type: {}", artifactoryPackageType); return Optional.empty(); } } diff --git a/core/src/main/java/io/snyk/plugins/artifactory/ecosystem/RepositoryMetadataEcosystemResolver.java b/core/src/main/java/io/snyk/plugins/artifactory/ecosystem/RepositoryMetadataEcosystemResolver.java index 89cb9e1..d407e2a 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/ecosystem/RepositoryMetadataEcosystemResolver.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/ecosystem/RepositoryMetadataEcosystemResolver.java @@ -32,6 +32,6 @@ public Optional getFor(RepoPath repoPath) { return Optional.empty(); } - return Ecosystem.fromPackageType(packageType); + return Ecosystem.match(packageType, repoPath.getPath()); } } diff --git a/core/src/main/java/io/snyk/plugins/artifactory/exception/SnykAPIFailureException.java b/core/src/main/java/io/snyk/plugins/artifactory/exception/SnykAPIFailureException.java index 16f5059..5c54049 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/exception/SnykAPIFailureException.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/exception/SnykAPIFailureException.java @@ -1,10 +1,9 @@ package io.snyk.plugins.artifactory.exception; import io.snyk.sdk.api.SnykResult; -import io.snyk.sdk.model.TestResult; public class SnykAPIFailureException extends RuntimeException { - public SnykAPIFailureException(SnykResult result) { + public SnykAPIFailureException(SnykResult result) { super("Snyk API request was not successful. (" + result.statusCode + ")"); } diff --git a/core/src/main/java/io/snyk/plugins/artifactory/model/IssueSummary.java b/core/src/main/java/io/snyk/plugins/artifactory/model/IssueSummary.java index 6a4fbdf..129caf3 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/model/IssueSummary.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/model/IssueSummary.java @@ -2,6 +2,7 @@ import io.snyk.sdk.model.Issue; import io.snyk.sdk.model.Severity; +import io.snyk.sdk.model.purl.PurlIssue; import java.util.*; import java.util.regex.Matcher; @@ -22,6 +23,10 @@ private IssueSummary(Map countBySeverity) { this.countBySeverity = countBySeverity; } + public static IssueSummary fromPurlIssues(List issues) { + return IssueSummary.from(issues.stream().map(i -> i.attribute.severity)); + } + public static IssueSummary from(List issues) { return IssueSummary.from(issues.stream().map(i -> i.severity)); } diff --git a/core/src/main/java/io/snyk/plugins/artifactory/scanner/ArtifactCache.java b/core/src/main/java/io/snyk/plugins/artifactory/scanner/ArtifactCache.java index e5d3de0..21c55a8 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/scanner/ArtifactCache.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/scanner/ArtifactCache.java @@ -28,7 +28,7 @@ public ArtifactCache(Duration testFrequency, Duration extendTestDeadline) { public Optional get(ArtifactProperties properties, Supplier> fetch) { Optional artifact = MonitoredArtifact.read(properties); if (artifact.isEmpty()) { - LOG.info("Previous Snyk Test result not available - testing {}", properties.getArtifactPath()); + LOG.info("Previous Snyk Test result not available for {}", properties.getArtifactPath()); return fetchAndStore(properties, fetch); } 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 index 772cbd3..2ff088e 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/scanner/PackageValidator.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/scanner/PackageValidator.java @@ -65,8 +65,8 @@ private void validateIssues(IssueSummary summary, Optional threshold, } LOG.debug("Package has {} with severity {} or higher: {}", issueType, threshold, artifact.getPath()); - throw new CancelException(format("Artifact has %s with severity %s or higher: %s. Details: %s", - issueType, threshold, artifact.getPath(), artifact.getTestResult().getDetailsUrl() + throw new CancelException(format("Artifact has %s with %s severity or higher: %s. Details: %s", + issueType, threshold.get(), artifact.getPath(), artifact.getTestResult().getDetailsUrl() ), 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 69e85c2..8df02e6 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 @@ -46,10 +46,19 @@ public ScannerModule(ConfigurationModule configurationModule, @Nonnull Repositor } public Optional testArtifact(@Nonnull RepoPath repoPath) { + if(skip(repoPath)) { + LOG.debug("No ecosystem matching for {}, skipping.", repoPath); + return Optional.empty(); + } return runTest(repoPath).map(artifact -> artifact.write(properties(repoPath))); } public void filterAccess(@Nonnull RepoPath repoPath) { + if(skip(repoPath)) { + LOG.debug("No ecosystem matching for {}, skipping.", repoPath); + return; + } + resolveArtifact(repoPath) .ifPresentOrElse( this::filter, @@ -95,4 +104,8 @@ private boolean shouldTestContinuously() { private Duration durationHoursProperty(PluginConfiguration property, ConfigurationModule configurationModule) { return Duration.ofHours(Integer.parseInt(configurationModule.getPropertyOrDefault(property))); } + + private boolean skip(RepoPath repoPath) { + return ecosystemResolver.getFor(repoPath).isEmpty(); + } } diff --git a/core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerResolver.java b/core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerResolver.java index 9a5a826..ad83bb3 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerResolver.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerResolver.java @@ -3,6 +3,7 @@ import io.snyk.plugins.artifactory.configuration.ConfigurationModule; import io.snyk.plugins.artifactory.configuration.PluginConfiguration; import io.snyk.plugins.artifactory.ecosystem.Ecosystem; +import io.snyk.plugins.artifactory.scanner.rubygems.RubyGemsScanner; import io.snyk.sdk.api.SnykClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -12,6 +13,8 @@ import java.util.Optional; import java.util.function.Function; +import static io.snyk.plugins.artifactory.configuration.PluginConfiguration.API_ORGANIZATION; + public class ScannerResolver { private static final Logger LOG = LoggerFactory.getLogger(ScannerResolver.class); private final Function getConfig; @@ -44,9 +47,12 @@ public Optional getFor(Ecosystem ecosystem) { } public static ScannerResolver setup(ConfigurationModule configurationModule, SnykClient snykClient) { + String orgId = configurationModule.getProperty(API_ORGANIZATION); return new ScannerResolver(configurationModule::getPropertyOrDefault) .register(Ecosystem.MAVEN, new MavenScanner(configurationModule, snykClient)) .register(Ecosystem.NPM, new NpmScanner(configurationModule, snykClient)) - .register(Ecosystem.PYPI, new PythonScanner(configurationModule, snykClient)); + .register(Ecosystem.PYPI, new PythonScanner(configurationModule, snykClient)) + .register(Ecosystem.RUBYGEMS, new RubyGemsScanner(snykClient, orgId)) + ; } } diff --git a/core/src/main/java/io/snyk/plugins/artifactory/scanner/SnykDetailsUrl.java b/core/src/main/java/io/snyk/plugins/artifactory/scanner/SnykDetailsUrl.java index 5cda8ee..8c39cfb 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/scanner/SnykDetailsUrl.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/scanner/SnykDetailsUrl.java @@ -8,7 +8,7 @@ public class SnykDetailsUrl { - static URI create(String ecosystem, String packageName, String version) { + public static URI create(String ecosystem, String packageName, String version) { return URI.create( String.format("https://security.snyk.io/package/%s/%s/%s", ecosystem, URLEncoder.encode(packageName, UTF_8), diff --git a/core/src/main/java/io/snyk/plugins/artifactory/scanner/TestResultConverter.java b/core/src/main/java/io/snyk/plugins/artifactory/scanner/TestResultConverter.java index 8ee45f9..08d25b2 100644 --- a/core/src/main/java/io/snyk/plugins/artifactory/scanner/TestResultConverter.java +++ b/core/src/main/java/io/snyk/plugins/artifactory/scanner/TestResultConverter.java @@ -2,8 +2,10 @@ import io.snyk.plugins.artifactory.model.IssueSummary; import io.snyk.plugins.artifactory.model.TestResult; +import io.snyk.sdk.model.purl.PurlIssues; import java.net.URI; +import java.util.stream.Stream; public class TestResultConverter { @@ -14,4 +16,12 @@ public static TestResult convert(io.snyk.sdk.model.TestResult result) { URI.create(result.packageDetailsURL) ); } + + public static TestResult convert(PurlIssues issues) { + return new TestResult( + IssueSummary.fromPurlIssues(issues.purlIssues), + IssueSummary.from(Stream.empty()), + URI.create(issues.packageDetailsUrl) + ); + } } diff --git a/core/src/main/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsPackage.java b/core/src/main/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsPackage.java new file mode 100644 index 0000000..49d8944 --- /dev/null +++ b/core/src/main/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsPackage.java @@ -0,0 +1,35 @@ +package io.snyk.plugins.artifactory.scanner.rubygems; + +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class RubyGemsPackage { + private final String name; + private final String version; + + public RubyGemsPackage(String name, String version) { + this.name = name; + this.version = version; + } + + public String getName() { + return name; + } + + public String getVersion() { + return version; + } + + public static Optional parse(String artifactoryPackageName) { + if(artifactoryPackageName == null) { + return Optional.empty(); + } + Pattern pattern = Pattern.compile("(.*)-([^-]+)\\.gem", Pattern.CASE_INSENSITIVE); + Matcher matcher = pattern.matcher(artifactoryPackageName); + if(!matcher.matches()) { + return Optional.empty(); + } + return Optional.of(new RubyGemsPackage(matcher.group(1), matcher.group(2))); + } +} diff --git a/core/src/main/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsScanner.java b/core/src/main/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsScanner.java new file mode 100644 index 0000000..0592041 --- /dev/null +++ b/core/src/main/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsScanner.java @@ -0,0 +1,63 @@ +package io.snyk.plugins.artifactory.scanner.rubygems; + +import io.snyk.plugins.artifactory.exception.CannotScanException; +import io.snyk.plugins.artifactory.exception.SnykAPIFailureException; +import io.snyk.plugins.artifactory.model.TestResult; +import io.snyk.plugins.artifactory.scanner.PackageScanner; +import io.snyk.plugins.artifactory.scanner.SnykDetailsUrl; +import io.snyk.plugins.artifactory.scanner.TestResultConverter; +import io.snyk.sdk.api.SnykClient; +import io.snyk.sdk.api.SnykResult; +import io.snyk.sdk.model.purl.PurlIssues; +import org.artifactory.fs.FileLayoutInfo; +import org.artifactory.repo.RepoPath; +import org.slf4j.Logger; + +import java.net.URLEncoder; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.slf4j.LoggerFactory.getLogger; + +public class RubyGemsScanner implements PackageScanner { + + private static final Logger LOG = getLogger(RubyGemsScanner.class); + private final SnykClient snykClient; + private final String orgId; + + public RubyGemsScanner(SnykClient snykClient, String orgId) { + this.snykClient = snykClient; + this.orgId = orgId; + } + + @Override + public TestResult scan(FileLayoutInfo fileLayoutInfo, RepoPath repoPath) { + RubyGemsPackage pckg = RubyGemsPackage.parse(repoPath.getName()) + .orElseThrow(() -> new CannotScanException("Unexpected Ruby Gems package name: " + repoPath.getName())); + + String purl = "pkg:gem/" + pckg.getName() + "@" + pckg.getVersion(); + + SnykResult result; + try { + LOG.debug("Running Snyk test: {}, name: {}, version: {}", repoPath, pckg.getName(), pckg.getVersion()); + result = snykClient.get(PurlIssues.class, request -> + request + .withPath(String.format("rest/orgs/%s/packages/%s/issues", + URLEncoder.encode(orgId, UTF_8), + URLEncoder.encode(purl, UTF_8)) + ) + .withQueryParam("version", "2024-10-15") + ); + } catch (Exception e) { + throw new SnykAPIFailureException(e); + } + + PurlIssues testResult = result.get().orElseThrow(() -> new SnykAPIFailureException(result)); + testResult.packageDetailsUrl = getModuleDetailsURL(pckg.getName(), pckg.getVersion()); + + return TestResultConverter.convert(testResult); + } + + public static String getModuleDetailsURL(String name, String version) { + return SnykDetailsUrl.create("rubygems", name, version).toString(); + } +} diff --git a/core/src/test/java/io/snyk/plugins/artifactory/ecosystem/EcosystemTest.java b/core/src/test/java/io/snyk/plugins/artifactory/ecosystem/EcosystemTest.java index 1c51873..90da60e 100644 --- a/core/src/test/java/io/snyk/plugins/artifactory/ecosystem/EcosystemTest.java +++ b/core/src/test/java/io/snyk/plugins/artifactory/ecosystem/EcosystemTest.java @@ -2,15 +2,22 @@ import org.junit.jupiter.api.Test; +import static io.snyk.plugins.artifactory.ecosystem.Ecosystem.match; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; class EcosystemTest { @Test void ecosystemByPackageType() { - assertThat(Ecosystem.fromPackageType("maven")).contains(Ecosystem.MAVEN); - assertThat(Ecosystem.fromPackageType("npm")).contains(Ecosystem.NPM); - assertThat(Ecosystem.fromPackageType("pypi")).contains(Ecosystem.PYPI); - assertThat(Ecosystem.fromPackageType("nuget")).isEmpty(); + assertThat(match("maven", "")).contains(Ecosystem.MAVEN); + assertThat(match("npm", "")).contains(Ecosystem.NPM); + assertThat(match("pypi", "")).contains(Ecosystem.PYPI); + assertThat(match("gems", "gems/rack-protection-4.1.1.gem")).contains(Ecosystem.RUBYGEMS); + assertThat(match("nuget", "")).isEmpty(); + } + + @Test + void gems_noMatchWhenNoExtension() { + assertThat(match("gems", "gems/rack-protection")).isEmpty(); } } diff --git a/core/src/test/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsPackageTest.java b/core/src/test/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsPackageTest.java new file mode 100644 index 0000000..00d0c80 --- /dev/null +++ b/core/src/test/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsPackageTest.java @@ -0,0 +1,28 @@ +package io.snyk.plugins.artifactory.scanner.rubygems; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class RubyGemsPackageTest { + @Test + void parse() { + assertThat(RubyGemsPackage.parse("mustermann-3.0.3.gem")).isPresent(); + assertThat(RubyGemsPackage.parse("mustermann-3.0.3.gem").get().getName()).isEqualTo("mustermann"); + assertThat(RubyGemsPackage.parse("mustermann-3.0.3.gem").get().getVersion()).isEqualTo("3.0.3"); + + assertThat(RubyGemsPackage.parse("rack-protection-4.1.1.gem")).isPresent(); + assertThat(RubyGemsPackage.parse("rack-protection-4.1.1.gem").get().getName()).isEqualTo("rack-protection"); + assertThat(RubyGemsPackage.parse("rack-protection-4.1.1.gem").get().getVersion()).isEqualTo("4.1.1"); + } + + @Test + void parse_null() { + assertThat(RubyGemsPackage.parse(null)).isEmpty(); + } + + @Test + void parse_invalidName() { + assertThat(RubyGemsPackage.parse("mustermann")).isEmpty(); + } +} diff --git a/core/src/test/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsScannerTest.java b/core/src/test/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsScannerTest.java new file mode 100644 index 0000000..e89054d --- /dev/null +++ b/core/src/test/java/io/snyk/plugins/artifactory/scanner/rubygems/RubyGemsScannerTest.java @@ -0,0 +1,66 @@ +package io.snyk.plugins.artifactory.scanner.rubygems; + +import io.snyk.plugins.artifactory.exception.CannotScanException; +import io.snyk.plugins.artifactory.model.TestResult; +import io.snyk.plugins.artifactory.util.SnykConfigForTests; +import io.snyk.sdk.SnykConfig; +import io.snyk.sdk.api.SnykClient; +import io.snyk.sdk.model.Severity; +import org.artifactory.fs.FileLayoutInfo; +import org.artifactory.repo.RepoPath; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class RubyGemsScannerTest { + + RubyGemsScanner scanner; + + RepoPath repoPath; + FileLayoutInfo fileLayoutInfo; + + @BeforeEach + void setUp() throws Exception { + String org = System.getenv("TEST_SNYK_ORG"); + Assertions.assertNotNull(org, "must not be null for test"); + + SnykConfig config = SnykConfigForTests.withDefaults(); + + SnykClient snykClient = new SnykClient(config); + scanner = new RubyGemsScanner(snykClient, org); + + repoPath = mock(RepoPath.class); + fileLayoutInfo = mock(FileLayoutInfo.class); + } + + @Test + void whenAValidGemPackage() { + when(repoPath.getName()).thenReturn("sinatra-2.0.0.gem"); + + TestResult result = scanner.scan(fileLayoutInfo, repoPath); + assertThat(result.getVulnSummary().getCountAtOrAbove(Severity.MEDIUM)).isGreaterThanOrEqualTo(5); + assertThat(result.getDetailsUrl().toString()).isEqualTo("https://security.snyk.io/package/rubygems/sinatra/2.0.0"); + } + + @Test + void whenNotAGem() { + when(repoPath.getName()).thenReturn("sinatra"); + + assertThatThrownBy(() -> scanner.scan(fileLayoutInfo, repoPath)) + .isExactlyInstanceOf(CannotScanException.class) + .hasMessageContaining("sinatra"); + } + + @Test + void whenUnexpectedPackageNameStructure() { + when(repoPath.getName()).thenReturn("version.missing.gem"); + + assertThatThrownBy(() -> scanner.scan(fileLayoutInfo, repoPath)) + .isExactlyInstanceOf(CannotScanException.class) + .hasMessageContaining("version.missing.gem"); + } +} diff --git a/snyk-sdk/src/main/java/io/snyk/sdk/model/purl/IssueAttribute.java b/snyk-sdk/src/main/java/io/snyk/sdk/model/purl/IssueAttribute.java new file mode 100644 index 0000000..76e9236 --- /dev/null +++ b/snyk-sdk/src/main/java/io/snyk/sdk/model/purl/IssueAttribute.java @@ -0,0 +1,21 @@ +package io.snyk.sdk.model.purl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.snyk.sdk.model.Severity; + +import java.io.Serializable; + +public class IssueAttribute implements Serializable { + private static final long serialVersionUID = 1L; + + @JsonProperty("key") + public String key; + @JsonProperty("title") + public String title; + @JsonProperty("type") + public String type; + @JsonProperty("description") + public String description; + @JsonProperty("effective_severity_level") + public Severity severity; +} diff --git a/snyk-sdk/src/main/java/io/snyk/sdk/model/purl/PurlIssue.java b/snyk-sdk/src/main/java/io/snyk/sdk/model/purl/PurlIssue.java new file mode 100644 index 0000000..ea42641 --- /dev/null +++ b/snyk-sdk/src/main/java/io/snyk/sdk/model/purl/PurlIssue.java @@ -0,0 +1,11 @@ +package io.snyk.sdk.model.purl; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.io.Serializable; + +public class PurlIssue implements Serializable { + private static final long serialVersionUID = 1L; + + @JsonProperty("attributes") + public IssueAttribute attribute; +} diff --git a/snyk-sdk/src/main/java/io/snyk/sdk/model/purl/PurlIssues.java b/snyk-sdk/src/main/java/io/snyk/sdk/model/purl/PurlIssues.java new file mode 100644 index 0000000..307ef59 --- /dev/null +++ b/snyk-sdk/src/main/java/io/snyk/sdk/model/purl/PurlIssues.java @@ -0,0 +1,15 @@ +package io.snyk.sdk.model.purl; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.List; + +public class PurlIssues implements Serializable { + + private static final long serialVersionUID = 1L; + + @JsonProperty("data") + public List purlIssues; + public String packageDetailsUrl; +}