Skip to content

Commit

Permalink
feat: add support for CocoaPods and Nuget ecosystems
Browse files Browse the repository at this point in the history
Similar to the Gems scanner, CocoaPods and Nuget are powered by Snyk's
PURL test API. Disabled by default so these new ecosystems won't be
scanned unless explicitly opted-in.
  • Loading branch information
jacek-rzrz committed Nov 26, 2024
1 parent 5b7b921 commit 324d70c
Show file tree
Hide file tree
Showing 17 changed files with 443 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public enum PluginConfiguration implements Configuration {
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"),
SCANNER_PACKAGE_TYPE_NUGET("snyk.scanner.packageType.nuget", "false"),
SCANNER_PACKAGE_TYPE_COCOAPODS("snyk.scanner.packageType.cocoapods", "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");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public enum Ecosystem {
NPM(PluginConfiguration.SCANNER_PACKAGE_TYPE_NPM),
PYPI(PluginConfiguration.SCANNER_PACKAGE_TYPE_PYPI),
RUBYGEMS(PluginConfiguration.SCANNER_PACKAGE_TYPE_RUBYGEMS),
NUGET(PluginConfiguration.SCANNER_PACKAGE_TYPE_NUGET),
COCOAPODS(PluginConfiguration.SCANNER_PACKAGE_TYPE_COCOAPODS),
;

private static final Logger LOG = LoggerFactory.getLogger(Ecosystem.class);
Expand All @@ -28,13 +30,25 @@ public PluginConfiguration getConfigProperty() {

public static Optional<Ecosystem> 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();
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();
case "nuget":
return artifactPath.endsWith(".nupkg") ? Optional.of(NUGET) : Optional.empty();
case "cocoapods":
return matchesCocoapods(artifactPath) ? Optional.of(COCOAPODS) : Optional.empty();
}

LOG.info("Unknown package type: {}", artifactoryPackageType);
return Optional.empty();
}

private static boolean matchesCocoapods(String artifactPath) {
return artifactPath.endsWith(".tar.gz") || artifactPath.endsWith(".zip");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public ZonedDateTime getTimestamp() {
}

public void write(ArtifactProperties properties) {
LOG.info("Writing Snyk properties for package {}", detailsUrl);
LOG.info("Writing Snyk properties for package {} - artifactory path {}", detailsUrl, properties.getArtifactPath());
properties.set(TEST_TIMESTAMP, timestamp.toString());
properties.set(ISSUE_VULNERABILITIES, vulnSummary.toString());
properties.set(ISSUE_LICENSES, licenseSummary.toString());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
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.cocoapods.CocoapodsScanner;
import io.snyk.plugins.artifactory.scanner.nuget.NugetScanner;
import io.snyk.plugins.artifactory.scanner.purl.PurlScanner;
import io.snyk.plugins.artifactory.scanner.rubygems.RubyGemsScanner;
import io.snyk.sdk.api.SnykClient;
import org.slf4j.Logger;
Expand Down Expand Up @@ -48,11 +51,14 @@ public Optional<PackageScanner> getFor(Ecosystem ecosystem) {

public static ScannerResolver setup(ConfigurationModule configurationModule, SnykClient snykClient) {
String orgId = configurationModule.getProperty(API_ORGANIZATION);
PurlScanner purlScanner = new PurlScanner(snykClient, orgId);
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.RUBYGEMS, new RubyGemsScanner(snykClient, orgId))
.register(Ecosystem.RUBYGEMS, new RubyGemsScanner(purlScanner))
.register(Ecosystem.NUGET, new NugetScanner(purlScanner))
.register(Ecosystem.COCOAPODS, new CocoapodsScanner(purlScanner))
;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.snyk.plugins.artifactory.scanner.cocoapods;

import org.slf4j.Logger;

import java.util.Optional;

import static org.slf4j.LoggerFactory.getLogger;

public class CocoapodsPackage {
private static final Logger LOG = getLogger(CocoapodsPackage.class);
private final String name;
private final String version;

public CocoapodsPackage(String name, String version) {
this.name = name;
this.version = version;
}

public String getName() {
return name;
}

public String getVersion() {
return version;
}

public static Optional<CocoapodsPackage> parse(
String artifactoryPackageName
) {
if (artifactoryPackageName == null) {
LOG.warn("Unexpected package name: null");
return Optional.empty();
}

String[] nameVersion = artifactoryPackageName.replace(".tar.gz", "")
.replaceFirst("(?s)-(?!.*?-)", "!")
.split("!");

if (nameVersion.length != 2) {
LOG.warn("Unexpected Cocoapods package name: {}", artifactoryPackageName);
return Optional.empty();
}

return Optional.of(new CocoapodsPackage(nameVersion[0], nameVersion[1]));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package io.snyk.plugins.artifactory.scanner.cocoapods;

import io.snyk.plugins.artifactory.exception.CannotScanException;
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.purl.PurlScanner;
import org.artifactory.fs.FileLayoutInfo;
import org.artifactory.repo.RepoPath;
import org.slf4j.Logger;

import static org.slf4j.LoggerFactory.getLogger;

public class CocoapodsScanner implements PackageScanner {

private static final Logger LOG = getLogger(CocoapodsScanner.class);
private final PurlScanner purlScanner;

public CocoapodsScanner(PurlScanner purlScanner) {
this.purlScanner = purlScanner;
}

@Override
public TestResult scan(FileLayoutInfo fileLayoutInfo, RepoPath repoPath) {
LOG.debug("Cocoapods: repoPath.getName() {}", repoPath.getName());

CocoapodsPackage pckg = CocoapodsPackage.parse(repoPath.getName())
.orElseThrow(() -> new CannotScanException("Unexpected Cocoapods package name" + repoPath.getName()));

String purl = "pkg:cocoapods/" + pckg.getName() + "@" + pckg.getVersion();

String packageDetailsUrl = getModuleDetailsURL(pckg.getName(), pckg.getVersion());

return purlScanner.scan(purl, packageDetailsUrl);
}

public static String getModuleDetailsURL(String name, String version) {
return SnykDetailsUrl.create("cocoapods", name, version).toString();
}
}

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


import org.slf4j.Logger;

import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.slf4j.LoggerFactory.getLogger;

public class NugetPackage {
private static final Logger LOG = getLogger(NugetPackage.class);
private final String name;
private final String version;

public NugetPackage(String name, String version) {
this.name = name;
this.version = version;
}

public String getName() {
return name;
}

public String getVersion() {
return version;
}

public static Optional<NugetPackage> parse(String artifactoryPackageName) {
if (artifactoryPackageName == null) {
LOG.warn("Unexpected Nuget package name: null");
return Optional.empty();
}

Pattern pattern = Pattern.compile("\\.([0-9]+\\..*)\\.nupkg");
Matcher matcher = pattern.matcher(artifactoryPackageName);
if (!matcher.find()) {
LOG.warn("Unexpected Nuget package name: {}", artifactoryPackageName);
return Optional.empty();
}
String name = artifactoryPackageName.substring(0, matcher.start());
String version = matcher.group(1);

return Optional.of(new NugetPackage(name, version));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.snyk.plugins.artifactory.scanner.nuget;

import io.snyk.plugins.artifactory.exception.CannotScanException;
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.purl.PurlScanner;
import org.artifactory.fs.FileLayoutInfo;
import org.artifactory.repo.RepoPath;

public class NugetScanner implements PackageScanner {

private final PurlScanner purlScanner;

public NugetScanner(PurlScanner purlScanner) {
this.purlScanner = purlScanner;
}

@Override
public TestResult scan(FileLayoutInfo fileLayoutInfo, RepoPath repoPath) {
NugetPackage pckg = NugetPackage.parse(repoPath.getName())
.orElseThrow(() -> new CannotScanException("Unexpected Nuget package name: " + repoPath.getName()));

String purl = "pkg:nuget/" + pckg.getName() + "@" + pckg.getVersion();

String packageDetailsUrl = getModuleDetailsURL(pckg.getName(), pckg.getVersion());

return purlScanner.scan(purl, packageDetailsUrl);
}

public static String getModuleDetailsURL(String name, String version) {
return SnykDetailsUrl.create("nuget", name, version).toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package io.snyk.plugins.artifactory.scanner.purl;

import io.snyk.plugins.artifactory.exception.SnykAPIFailureException;
import io.snyk.plugins.artifactory.model.TestResult;
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.slf4j.Logger;

import java.net.URLEncoder;

import static java.nio.charset.StandardCharsets.UTF_8;
import static org.slf4j.LoggerFactory.getLogger;

public class PurlScanner {

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

private final SnykClient snykClient;
private final String orgId;

public PurlScanner(SnykClient snykClient, String orgId) {
this.snykClient = snykClient;
this.orgId = orgId;
}

public TestResult scan(String purl, String packageDetailsUrl) {
SnykResult<PurlIssues> result;
try {
LOG.debug("Running Snyk test: {}", packageDetailsUrl);
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 = packageDetailsUrl;

return TestResultConverter.convert(testResult);
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package io.snyk.plugins.artifactory.scanner.rubygems;

import org.slf4j.Logger;

import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static org.slf4j.LoggerFactory.getLogger;

public class RubyGemsPackage {
private static final Logger LOG = getLogger(RubyGemsPackage.class);
private final String name;
private final String version;

Expand All @@ -23,11 +28,13 @@ public String getVersion() {

public static Optional<RubyGemsPackage> parse(String artifactoryPackageName) {
if(artifactoryPackageName == null) {
LOG.warn("Unexpected Gems package name: null");
return Optional.empty();
}
Pattern pattern = Pattern.compile("(.*)-([^-]+)\\.gem", Pattern.CASE_INSENSITIVE);
Matcher matcher = pattern.matcher(artifactoryPackageName);
if(!matcher.matches()) {
LOG.warn("Unexpected Gems package name: {}", artifactoryPackageName);
return Optional.empty();
}
return Optional.of(new RubyGemsPackage(matcher.group(1), matcher.group(2)));
Expand Down
Loading

0 comments on commit 324d70c

Please sign in to comment.