Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use Snyk as data provider #860

Merged
merged 12 commits into from
Aug 17, 2022
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.sap.oss.phosphor.fosstars.advice.oss;

import static com.sap.oss.phosphor.fosstars.model.feature.oss.OssFeatures.USES_SNYK;

import com.sap.oss.phosphor.fosstars.advice.Advice;
import com.sap.oss.phosphor.fosstars.advice.oss.OssAdviceContentYamlStorage.OssAdviceContext;
import com.sap.oss.phosphor.fosstars.model.Subject;
import com.sap.oss.phosphor.fosstars.model.Value;
import com.sap.oss.phosphor.fosstars.model.score.oss.SnykDependencyScanScore;
import com.sap.oss.phosphor.fosstars.model.value.ScoreValue;
import java.net.MalformedURLException;
import java.util.Collections;
import java.util.List;
import java.util.Optional;

/**
* An advisor for features related to Snyk.
*/
public class SnykAdvisor extends AbstractOssAdvisor {

/**
* Create a new advisor.
*
* @param contextFactory A factory that provides contexts for advice.
*/
public SnykAdvisor(OssAdviceContextFactory contextFactory) {
super(OssAdviceContentYamlStorage.DEFAULT, contextFactory);
}

@Override
protected List<Advice> adviceFor(
Subject subject, List<Value<?>> usedValues, OssAdviceContext context)
throws MalformedURLException {

Optional<ScoreValue> snykScore = findSubScoreValue(subject, SnykDependencyScanScore.class);

if (!snykScore.isPresent() || snykScore.get().isNotApplicable()) {
return Collections.emptyList();
}

return adviceForBooleanFeature(usedValues, USES_SNYK, subject, context);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
import com.sap.oss.phosphor.fosstars.data.github.UsesOwaspDependencyCheck;
import com.sap.oss.phosphor.fosstars.data.github.UsesSanitizers;
import com.sap.oss.phosphor.fosstars.data.github.UsesSignedCommits;
import com.sap.oss.phosphor.fosstars.data.github.UsesSnyk;
import com.sap.oss.phosphor.fosstars.data.github.VulnerabilityAlertsInfo;
import com.sap.oss.phosphor.fosstars.data.interactive.AskAboutSecurityTeam;
import com.sap.oss.phosphor.fosstars.data.interactive.AskAboutUnpatchedVulnerabilities;
Expand Down Expand Up @@ -210,6 +211,7 @@ public DataProviderSelector(GitHubDataFetcher fetcher, NVD nvd) throws IOExcepti
new LgtmDataProvider(fetcher),
new UsesSignedCommits(fetcher),
new UsesDependabot(fetcher),
new UsesSnyk(fetcher),
new ProgrammingLanguages(fetcher),
new PackageManagement(fetcher),
new UsesNoHttpTool(fetcher),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static com.sap.oss.phosphor.fosstars.model.feature.oss.OssFeatures.PACKAGE_MANAGERS;
import static com.sap.oss.phosphor.fosstars.model.value.Language.C_SHARP;
import static com.sap.oss.phosphor.fosstars.model.value.Language.F_SHARP;
import static com.sap.oss.phosphor.fosstars.model.value.Language.GO;
import static com.sap.oss.phosphor.fosstars.model.value.Language.JAVA;
import static com.sap.oss.phosphor.fosstars.model.value.Language.JAVASCRIPT;
import static com.sap.oss.phosphor.fosstars.model.value.Language.PHP;
Expand All @@ -13,6 +14,7 @@
import static com.sap.oss.phosphor.fosstars.model.value.Language.VISUALBASIC;
import static com.sap.oss.phosphor.fosstars.model.value.PackageManager.COMPOSER;
import static com.sap.oss.phosphor.fosstars.model.value.PackageManager.DOTNET;
import static com.sap.oss.phosphor.fosstars.model.value.PackageManager.GOMODULES;
import static com.sap.oss.phosphor.fosstars.model.value.PackageManager.GRADLE;
import static com.sap.oss.phosphor.fosstars.model.value.PackageManager.MAVEN;
import static com.sap.oss.phosphor.fosstars.model.value.PackageManager.NPM;
Expand Down Expand Up @@ -63,6 +65,7 @@ public class PackageManagement extends CachedSingleFeatureGitHubDataProvider<Pac
register(PYTHON, PIP);
register(RUBY, RUBYGEMS);
register(PHP, COMPOSER);
register(GO, GOMODULES);
}

/**
Expand All @@ -83,6 +86,7 @@ public class PackageManagement extends CachedSingleFeatureGitHubDataProvider<Pac
".vcxproj"::equals, ".fsproj"::equals, "packages.config"::equals);
register(RUBYGEMS, "Gemfile.lock"::equals, "Gemfile"::equals, ".gemspec"::endsWith);
register(COMPOSER, "composer.json"::equals, "composer.lock"::equals);
register(GOMODULES, "go.mod"::equals);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the only form of Go Module possible for Package Management.

Maybe is there a .lock file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to go.mod, the go command maintains a file named go.sum containing the expected cryptographic hashes of the content of specific module versions. Do we need to check this as well?

}

/**
Expand Down
212 changes: 212 additions & 0 deletions src/main/java/com/sap/oss/phosphor/fosstars/data/github/UsesSnyk.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
package com.sap.oss.phosphor.fosstars.data.github;

import static com.sap.oss.phosphor.fosstars.model.feature.oss.OssFeatures.HAS_OPEN_PULL_REQUEST_FROM_SNYK;
import static com.sap.oss.phosphor.fosstars.model.feature.oss.OssFeatures.USES_SNYK;
import static com.sap.oss.phosphor.fosstars.model.other.Utils.setOf;

import com.sap.oss.phosphor.fosstars.model.Feature;
import com.sap.oss.phosphor.fosstars.model.ValueSet;
import com.sap.oss.phosphor.fosstars.model.feature.oss.OssFeatures;
import com.sap.oss.phosphor.fosstars.model.subject.oss.GitHubProject;
import com.sap.oss.phosphor.fosstars.model.value.ValueHashSet;
import java.io.IOException;
import java.nio.file.Path;
import java.time.Duration;
import java.time.Instant;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHPullRequest;
import org.kohsuke.github.GHUser;

/**
* This data provider checks if an open-source project on GitHub uses Snyk, and fills out the {@link
* OssFeatures#USES_SNYK} feature.
*
* <p>First, the provider checks if a repository contains a policy file for Snyk. If the policy file
* exists, then the provider reports that the project uses Snyk. Next, the provider searches for
* commits from Snyk in the commit history. If the commits are found, then the provider also reports
* that the project uses Snyk.
*/
public class UsesSnyk extends GitHubCachingDataProvider {

/**
* A file name containing Snyk policies in a repository.
*
* @see <a href="https://docs.snyk.io/snyk-cli/test-for-vulnerabilities/the-.snyk-file/">The .snyk
* file</a>
*/
private static String SNYK_POLICY_FILE_NAME = ".snyk";

/**
* A minimal number of characters in a config for Snyk.
*/
private static final int ACCEPTABLE_CONFIG_SIZE = 10;

/**
* A location of a Snyk configuration file in a repository.
*
* @see <a href="https://github.com/snyk/actions/tree/master/setup">To setup Snyk actions</a>
*
*/
private static final String [] SNYK_CONFIGS = {
".github/workflows/snyk.yaml",
".github/workflows/snyk.yml"
};

/** Predicate to confirm if there is a file in open-source project with the .snyk extension. */
ManjunathMS35 marked this conversation as resolved.
Show resolved Hide resolved
private static final Predicate<Path> SNYK_FILE_PREDICATE =
path -> path.getFileName().toString().endsWith(SNYK_POLICY_FILE_NAME);

/**
* A pattern to detect commits by Snyk.
*
* @see <a
* href="https://docs.snyk.io/integrations/git-repository-scm-integrations/github-integration#commit-signing/">Snyk
* commit signing</a>
*/
private static final String SNYK_PATTERN = "snyk";

/** Period of time to be checked. */
ManjunathMS35 marked this conversation as resolved.
Show resolved Hide resolved
private static final Duration ONE_YEAR = Duration.ofDays(365);

/**
* Initializes a data provider.
*
* @param fetcher An interface to GitHub.
*/
public UsesSnyk(GitHubDataFetcher fetcher) {
super(fetcher);
}

@Override
public Set<Feature<?>> supportedFeatures() {
return setOf(USES_SNYK, HAS_OPEN_PULL_REQUEST_FROM_SNYK);
}

@Override
protected ValueSet fetchValuesFor(GitHubProject project) throws IOException {
logger.info("Checking how the project uses Snyk ...");

LocalRepository repository = GitHubDataFetcher.localRepositoryFor(project);

return ValueHashSet.from(
USES_SNYK.value(
hasSnykPolicy(repository)
|| hasSnykConfig(repository)
|| hasSnykCommits(repository)),
HAS_OPEN_PULL_REQUEST_FROM_SNYK.value(hasOpenPullRequestFromSnyk(project)));
}

/**
* Checks if a repository has a configuration file for Snyk.
*
* @param repository The repository
* @return True if a config was found, false otherwise.
*/
private boolean hasSnykConfig(LocalRepository repository) throws IOException {
for (String config : SNYK_CONFIGS) {
Optional<String> content = repository.file(config);
if (content.isPresent() && content.get().length() >= ACCEPTABLE_CONFIG_SIZE) {
return true;
}
}

return false;
}

/**
* Checks if a repository has a policy file for Snyk.
*
* @param repository The repository
* @return True if a policy file was found, false otherwise.
*/
private boolean hasSnykPolicy(LocalRepository repository) throws IOException {
List<Path> snykPolicyFilePaths = repository.files(SNYK_FILE_PREDICATE);
return !snykPolicyFilePaths.isEmpty();
}

/**
* Checks whether a project has open pull requests from Snyk.
*
* @param project The project.
* @return True if the project has open pull requests form Snyk.
* @throws IOException If something went wrong.
*/
private boolean hasOpenPullRequestFromSnyk(GitHubProject project) throws IOException {
return fetcher.repositoryFor(project).getPullRequests(GHIssueState.OPEN).stream()
.anyMatch(this::createdBySnyk);
}

/**
* Checks if a pull request was created by Snyk.
*
* @param pullRequest The pull request.
* @return True if the user looks like Snyk, false otherwise.
*/
private boolean createdBySnyk(GHPullRequest pullRequest) {
try {
GHUser user = pullRequest.getUser();
return isSnyk(user.getName()) || isSnyk(user.getLogin());
} catch (IOException e) {
logger.warn("Oops! Could not fetch name or login!", e);
return false;
}
}

/**
* Checks if a repository contains commits from Snyk in the commit history.
*
* @param repository The repository.
* @return True if at least one commit from Snyk was found, false otherwise.
*/
private boolean hasSnykCommits(LocalRepository repository) {
Date date = Date.from(Instant.now().minus(ONE_YEAR));

try {
for (Commit commit : repository.commitsAfter(date)) {
if (isSnyk(commit)) {
return true;
}
}
} catch (IOException e) {
logger.warn("Something went wrong!", e);
}

return false;
}

/**
* Checks if a commit was done by Snyk.
*
* @param commit The commit to be checked.
* @return True if the commit was done by Snyk, false otherwise.
*/
private static boolean isSnyk(Commit commit) {
if (isSnyk(commit.authorName()) || isSnyk(commit.committerName())) {
return true;
}

for (String line : commit.message()) {
if ((line.startsWith("Signed-off-by:") || line.startsWith("Co-authored-by:"))
&& line.contains(SNYK_PATTERN)) {
return true;
}
}

return false;
}

/**
* Checks whether a name looks like Snyk.
*
* @param name The name.
* @return True if the name looks like Snyk, false otherwise.
*/
private static boolean isSnyk(String name) {
return name != null && name.toLowerCase().contains(SNYK_PATTERN);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe most of the methods given here seems to be from UsesDependebot. Could you please provide an abstract class?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

10 changes: 10 additions & 0 deletions src/main/java/com/sap/oss/phosphor/fosstars/model/Score.java
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,14 @@ static double adjust(double value) {
return value;
}

/**
* Get an Interval with the provided range.
*
* @param min An interval start value.
* @param max An interval end value.
* @return Interval with the range provided from min and max param values.
*/
static Interval makeInterval(double min, double max) {
return DoubleInterval.init().from(min).to(max).closed().make();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,28 @@ private OssFeatures() {
public static final BooleanFeature HAS_OPEN_PULL_REQUEST_FROM_DEPENDABOT
= new BooleanFeature("If a project has open pull requests from Dependabot");

/**
* <p>Shows if a project uses Snyk.</p>
* <p><a href="https://snyk.io/">Snyk</a> offers
* i) Static Application Security Testing (SAST) amd
* i) Static Application Security Testing (SAST) amd
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* i) Static Application Security Testing (SAST) amd

* ii) Automatic dependency updates
* In particular for automatic dependency updates,
* when Snyk finds a vulnerability in dependencies,
* it opens a pull request to update the vulnerable dependency to the safe version.</p>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you format this a bit better? It looks very congested.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*<ul>
*   <li></li> 
*</ul>

*/
public static final Feature<Boolean> USES_SNYK
= new BooleanFeature("If a project uses Snyk");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess here indentation seems to be a miss


/**
* Shows if an open source project has open pull requests from Snyk which means that
* there are dependencies with known vulnerabilities.
*
* @see <a href="https://snyk.io/">Snyk</a>
ManjunathMS35 marked this conversation as resolved.
Show resolved Hide resolved
*/
public static final BooleanFeature HAS_OPEN_PULL_REQUEST_FROM_SNYK
= new BooleanFeature("If a project has open pull requests from Snyk");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indentation

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature seems completely useless, where is it used in SnykScore?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good feature to increase the case in Snyk Score


/**
* Shows how many GitHub users starred an open-source project.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ public class DependabotScore extends FeatureBasedScore {
* A score value that is returned if it's likely
* that a project uses the security alerts on GitHub.
*/
private static final double GITHUB_ALERTS_SCORE_VALUE = 6.0;
private static final double GITHUB_ALERTS_SCORE_VALUE = 3.0;

/**
* Initializes a new score.
Expand Down
Loading