diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/data/AbstractStaticScanToolsDataProvider.java b/src/main/java/com/sap/oss/phosphor/fosstars/data/AbstractStaticScanToolsDataProvider.java index 38e9f1214..b38aec31a 100644 --- a/src/main/java/com/sap/oss/phosphor/fosstars/data/AbstractStaticScanToolsDataProvider.java +++ b/src/main/java/com/sap/oss/phosphor/fosstars/data/AbstractStaticScanToolsDataProvider.java @@ -1,36 +1,102 @@ package com.sap.oss.phosphor.fosstars.data; +import static com.sap.oss.phosphor.fosstars.github.GitHubVisitor.Location.GITHUB_ACTION; +import static com.sap.oss.phosphor.fosstars.github.GitHubVisitor.Location.INI_CONFIG; +import static com.sap.oss.phosphor.fosstars.github.GitHubVisitor.Location.PRE_COMMIT_HOOK; + import com.sap.oss.phosphor.fosstars.data.github.GitHubCachingDataProvider; import com.sap.oss.phosphor.fosstars.data.github.GitHubDataFetcher; import com.sap.oss.phosphor.fosstars.data.github.LocalRepository; +import com.sap.oss.phosphor.fosstars.github.AbstractGitHubVisitor; +import com.sap.oss.phosphor.fosstars.github.GitHubVisitor; +import com.sap.oss.phosphor.fosstars.github.GitHubVisitor.Location; import com.sap.oss.phosphor.fosstars.model.Feature; +import com.sap.oss.phosphor.fosstars.model.subject.oss.GitHubProject; +import com.sap.oss.phosphor.fosstars.util.Yaml; import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.function.BiPredicate; +import java.util.function.Predicate; +import java.util.stream.Stream; +import org.apache.commons.collections4.IteratorUtils; /** * This is a base class for data providers that would like to check if the project uses a Static * Analysis Scan Tool (SAST) and also determines how the project uses it. */ public abstract class AbstractStaticScanToolsDataProvider extends - GitHubCachingDataProvider implements StaticAnalysisScanTool { + GitHubCachingDataProvider { /** * A directory where GitHub Actions configs are stored. */ private static final String GITHUB_ACTIONS_DIRECTORY = ".github/workflows"; + /** + * A pre-commit hook standard config file path. + * + * @see Pre-commit hook config + * file + */ + private static final String PRE_COMMIT_HOOK_CONFIG = ".pre-commit-config.yaml"; + + /** + * A list of extensions of GitHub Actions configs. + */ + private static final List GITHUB_ACTIONS_CONFIG_EXTENSIONS = + Arrays.asList(".yaml", ".yml"); + /** * A list of extensions of GitHub Actions configs. */ - private static final List GITHUB_ACTIONS_CONFIG_EXTENSIONS - = Arrays.asList(".yaml", ".yml"); + private static final String GITHUB_INI_CONFIG_EXTENSION = ".ini"; + + /** + * A Bi-Predicate to evaluate each context of Map type extracted from the GitHub Action jobs. + */ + private static final BiPredicate, Map>> MATCH_MAP_PREDICATE = + (context, matchers) -> { + for (Map.Entry> entry : matchers.entrySet()) { + final String key = entry.getKey(); + if (context.containsKey(entry.getKey()) + && entry.getValue().test(context.get(key).toString())) { + return true; + } + } + return false; + }; + + /** + * A Bi-Predicate to evaluate each content of String type extracted from the GitHub Action jobs. + */ + private static final BiPredicate>> MATCH_STRING_PREDICATE = + (content, matchers) -> { + for (Predicate entry : matchers.values()) { + if (entry.test(content)) { + return true; + } + } + return false; + }; + + /** + * The list of Pre-defined stops to run the predicate or search for the key pattern in pre-commit + * hook config file. + */ + private static final List PRE_COMMIT_HOOK_CONTEXT = + Arrays.asList("entry", "additional_dependencies", "repo", "rev"); /** * Set of supported features. @@ -54,6 +120,32 @@ public Set> supportedFeatures() { return supportedFeatures; } + /** + * Browse a POM file with a specified visitor. + * + * @param repository The {@link LocalRepository}. + * @param matchers map of {@link Predicate}s to parse for a specific content and match the + * predicate. + * @param visitor The visitor. + * @param A type of the visitor. + * @return The passed visitor. + * @throws IOException if something went wrong. + */ + protected static T browse(LocalRepository repository, + Map> matchers, T visitor) throws IOException { + Objects.requireNonNull(repository, "Oh no! Repository is null!"); + Objects.requireNonNull(matchers, "Oh no! Predicate is null!"); + Objects.requireNonNull(visitor, "On no! Visitor is null!"); + + visitor.visitGitHubAction(repository, matchers, + in(EnumSet.noneOf(Location.class), GITHUB_ACTION)); + visitor.visitPreCommitHook(repository, matchers, + in(EnumSet.noneOf(Location.class), PRE_COMMIT_HOOK)); + visitor.visitIniConfig(repository, matchers, in(EnumSet.noneOf(Location.class), INI_CONFIG)); + // visitor.visitSourceCode(repository, predicate, in(EnumSet.noneOf(Location.class), TYPE_PY)); + return visitor; + } + /** * Looks for GitHub actions in a repository. * @@ -71,6 +163,82 @@ protected static List findGitHubActionsIn(LocalRepository repository) thro return repository.files(path, AbstractStaticScanToolsDataProvider::isGitHubActionConfig); } + /** + * Looks for GitHub ini config files in a repository. + * + * @param repository The repository to be checked. + * @return A list of paths to GitHub Action ini configs. + * @throws IOException If something went wrong. + */ + public static List findIniConfigsIn(LocalRepository repository) throws IOException { + return repository + .files(path -> path.getFileName().toString().endsWith(GITHUB_INI_CONFIG_EXTENSION)); + } + + /** + * Browse through the GitHub actions and find matches with the given predicates. + * + * @param githubAction GitHub Actions. + * @param matchers predicates to match through the given action. + * @return True if one of the predicate finds matches. False otherwise. + */ + private static boolean scanGitHubAction(Map githubAction, + Map> matchers) { + return Optional.ofNullable(githubAction.get("jobs")) + .filter(Map.class::isInstance) + .map(Map.class::cast) + .map(jobs -> jobs.values()) + .filter(Iterable.class::isInstance) + .map(Iterable.class::cast) + .map(jobs -> scanJobs(jobs, matchers)) + .orElse(false); + } + + /** + * Search for the given predicate matches in the given list of action jobs. + * + * @param jobs Iterable list of GitHub action jobs. + * @param matchers predicates to match through the given action. + * @return True if one of the predicate finds matches. False otherwise. + */ + private static boolean scanJobs(Iterable jobs, Map> matchers) { + return IteratorUtils.toList(jobs.iterator()).stream() + .filter(Map.class::isInstance) + .map(Map.class::cast) + .map(job -> job.get("steps")) + .filter(Iterable.class::isInstance) + .map(Iterable.class::cast) + .anyMatch(steps -> scanSteps(steps, matchers)); + } + + /** + * Search for the given predicate matches in the given list of job steps. + * + * @param jobs Iterable list of GitHub job steps. + * @param matchers predicates to match through the given action. + * @return True if one of the predicate finds matches. False otherwise. + */ + private static boolean scanSteps(Iterable steps, Map> matchers) { + return IteratorUtils.toList(steps.iterator()).stream() + .filter(Map.class::isInstance) + .map(Map.class::cast) + .anyMatch(step -> MATCH_MAP_PREDICATE.test(step, matchers)); + } + + /** + * Checks if a GitHub action runs on pull requests. + * + * @param githubAction A config of the action. + * @return True if the action runs on pull requests, false otherwise. + */ + private static boolean runsOnPullRequests(Map githubAction) { + return Optional.ofNullable(githubAction.get("on")) + .filter(Map.class::isInstance) + .map(Map.class::cast) + .map(on -> on.containsKey("pull_request")) + .orElse(false); + } + /** * Checks if a file is a config for a GitHub action. * @@ -82,12 +250,183 @@ private static boolean isGitHubActionConfig(Path path) { .stream().anyMatch(ext -> path.getFileName().toString().endsWith(ext)); } - @Override - public boolean runsOnPullRequests(Map githubAction) { - return Optional.ofNullable(githubAction.get("on")) + /** + * Prepare the map of Pre-Commit Hook config contexts. + * + * @param matcher The predicate matcher to match against each contexts. + * @return Map of Context and the associated Predicates. + */ + protected static Map> preCommitHookContextMap( + Predicate matcher) { + Map> map = new HashMap<>(); + for (String context : PRE_COMMIT_HOOK_CONTEXT) { + map.put(context, matcher); + } + return map; + } + + /** + * Checks if a Static Analysis Tool is configured as a Pre-Commit Hook. + * + * @param config The Pre-Commit Config file. + * @param matchers predicates to match through the given config. + * @return True if one of the predicate finds matches. False otherwise. + */ + private static boolean hasPreCommitHook(Map config, + Map> matchers) { + return Optional.ofNullable(config.get("repos")) + .filter(Iterable.class::isInstance) + .map(Iterable.class::cast) + .map(repos -> scanPreCommitRepos(repos, matchers) || scanPreCommitHooks(repos, matchers)) + .orElse(false); + } + + /** + * Search for the given predicate matches in the given list of repos. + * + * @param repos Iterable list of GitHub repositories config. + * @param matchers predicates to match through the given action. + * @return True if one of the predicate finds matches. False otherwise. + */ + private static boolean scanPreCommitRepos(Iterable repos, + Map> matchers) { + return IteratorUtils.toList(repos.iterator()).stream() .filter(Map.class::isInstance) .map(Map.class::cast) - .map(on -> on.containsKey("pull_request")) - .orElse(false); + .anyMatch(repo -> MATCH_MAP_PREDICATE.test(repo, matchers)); + } + + /** + * Search for the given predicate matches in the given list of repo hooks. + * + * @param repos Iterable list of GitHub repositories hooks config. + * @param matchers predicates to match through the given action. + * @return True if one of the predicate finds matches. False otherwise. + */ + private static boolean scanPreCommitHooks(Iterable repos, + Map> matchers) { + return IteratorUtils.toList(repos.iterator()).stream() + .filter(Map.class::isInstance) + .map(Map.class::cast) + .map(repo -> repo.get("hooks")) + .filter(Iterable.class::isInstance) + .map(Iterable.class::cast) + .anyMatch(hooks -> scanPreCommitHook(hooks, matchers)); + } + + /** + * Search for the given predicate matches in the given list of repo hook. + * + * @param repos Iterable list of GitHub hooks config. + * @param matchers predicates to match through the given action. + * @return True if one of the predicate finds matches. False otherwise. + */ + private static boolean scanPreCommitHook(Iterable hooks, + Map> matchers) { + return IteratorUtils.toList(hooks.iterator()).stream() + .filter(Map.class::isInstance) + .map(Map.class::cast) + .anyMatch(hook -> MATCH_MAP_PREDICATE.test(hook, matchers)); + } + + /** + * Search for the given predicate matches in the given ini config file. + * + * @param repos Iterable list lines from the ini config. + * @param matchers predicates to match through the given action. + * @return True if one of the predicate finds matches. False otherwise. + */ + private static boolean hasIniConfig(Stream lines, + Map> matchers) { + boolean result = lines.anyMatch(line -> MATCH_STRING_PREDICATE.test(line, matchers)); + return result; + } + + /** + * Builds a new set of locations. + * + * @param locations A set of locations to be added to the resulting set. + * @param rest An array of locations to be added to the resulting set. + * @return A set of locations that contains all passed locations. + */ + public static EnumSet in( + EnumSet locations, Location... rest) { + + EnumSet set = EnumSet.copyOf(locations); + set.addAll(Arrays.asList(rest)); + return set; + } + + /** + * Creates a visitor for searching {@link GitHubProject}. + * + * @return {@link Visitor}. + */ + protected static Visitor withVisitor() { + return new Visitor(); + } + + /** + * A visitor for searching a specific config predicate in a {@link GitHubProject}. + */ + public static class Visitor extends AbstractGitHubVisitor { + + /** + * A visitor for searching {@link GitHubProject} if the config to run a specific check tool + * exists. + */ + public boolean runCheck = false; + + /** + * A visitor for searching {@link GitHubProject} if the config to use a specific check tool + * exists. + */ + public boolean usesCheck = false; + + @Override + public void visitPreCommitHook(LocalRepository repository, + Map> matchers, Set locations) throws IOException { + Optional preCommitConfigPath = repository.path(PRE_COMMIT_HOOK_CONFIG); + if (!preCommitConfigPath.isPresent()) { + return; + } + + try (InputStream content = Files.newInputStream(preCommitConfigPath.get())) { + Map config = Yaml.readMap(content); + if (hasPreCommitHook(config, matchers)) { + runCheck = true; + usesCheck = true; + } + } + } + + @Override + public void visitIniConfig(LocalRepository repository, Map> matchers, + Set locations) throws IOException { + for (Path configPath : findIniConfigsIn(repository)) { + try (Stream lines = Files.lines(configPath)) { + if (hasIniConfig(lines, matchers)) { + runCheck = true; + } + } + } + } + + @Override + public void visitGitHubAction(LocalRepository repository, + Map> matchers, Set locations) throws IOException { + for (Path configPath : findGitHubActionsIn(repository)) { + try (InputStream content = Files.newInputStream(configPath)) { + Map githubAction = Yaml.readMap(content); + if (scanGitHubAction(githubAction, matchers)) { + runCheck = true; + if (runsOnPullRequests(githubAction)) { + usesCheck = true; + break; + } + } + } + } + } } } \ No newline at end of file diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/data/DataProviderSelector.java b/src/main/java/com/sap/oss/phosphor/fosstars/data/DataProviderSelector.java index 956f1e586..b44818e34 100644 --- a/src/main/java/com/sap/oss/phosphor/fosstars/data/DataProviderSelector.java +++ b/src/main/java/com/sap/oss/phosphor/fosstars/data/DataProviderSelector.java @@ -43,6 +43,7 @@ import com.sap.oss.phosphor.fosstars.data.github.OwaspSecurityLibraries; import com.sap.oss.phosphor.fosstars.data.github.PackageManagement; import com.sap.oss.phosphor.fosstars.data.github.ProgrammingLanguages; +import com.sap.oss.phosphor.fosstars.data.github.PylintDataProvider; import com.sap.oss.phosphor.fosstars.data.github.ReadmeInfo; import com.sap.oss.phosphor.fosstars.data.github.ReleasesFromGitHub; import com.sap.oss.phosphor.fosstars.data.github.SecurityReviewsFromOpenSSF; @@ -237,6 +238,7 @@ public DataProviderSelector(GitHubDataFetcher fetcher, NVD nvd) throws IOExcepti new VulnerabilitiesFromOwaspDependencyCheck(), new VulnerabilitiesFromNpmAudit(nvd), new HasExecutableBinaries(fetcher), + new PylintDataProvider(fetcher), PROJECT_USAGE_PROVIDER, FUNCTIONALITY_PROVIDER, HANDLING_UNTRUSTED_DATA_LIKELIHOOD_PROVIDER, diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/data/StaticAnalysisScanTool.java b/src/main/java/com/sap/oss/phosphor/fosstars/data/StaticAnalysisScanTool.java deleted file mode 100644 index c73c9bd42..000000000 --- a/src/main/java/com/sap/oss/phosphor/fosstars/data/StaticAnalysisScanTool.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.sap.oss.phosphor.fosstars.data; - -import java.util.Map; - -/** - * This is an interface of an Static Analysis Scan Tool (SAST) check data provider. - */ -public interface StaticAnalysisScanTool { - - /** - * Checks if a GitHub action triggers a SAST. - * - * @param githubAction A config for the action. - * @return True if the action triggers a SAST, false otherwise. - */ - boolean triggersScan(Map githubAction); - - /** - * Checks if a GitHub action runs on pull requests. - * - * @param githubAction A config of the action. - * @return True if the action runs on pull requests, false otherwise. - */ - boolean runsOnPullRequests(Map githubAction); -} diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/data/github/BanditDataProvider.java b/src/main/java/com/sap/oss/phosphor/fosstars/data/github/BanditDataProvider.java index 94e397507..945f62ebf 100644 --- a/src/main/java/com/sap/oss/phosphor/fosstars/data/github/BanditDataProvider.java +++ b/src/main/java/com/sap/oss/phosphor/fosstars/data/github/BanditDataProvider.java @@ -10,31 +10,35 @@ 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 com.sap.oss.phosphor.fosstars.util.Yaml; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; +import java.util.HashMap; import java.util.Map; -import java.util.Optional; +import java.util.function.Predicate; import java.util.regex.Pattern; -import org.apache.commons.collections4.IteratorUtils; /** * The data provider gathers info about how a project uses Bandit for static analysis. In * particular, it tries to fill out the following features: *
    - *
  • {@link OssFeatures#RUNS_BANDIT_SCANS}
  • - *
  • {@link OssFeatures#USES_BANDIT_SCAN_CHECKS}
  • + *
  • {@link OssFeatures#RUNS_BANDIT_SCANS}
  • + *
  • {@link OssFeatures#USES_BANDIT_SCAN_CHECKS}
  • *
*/ public class BanditDataProvider extends AbstractStaticScanToolsDataProvider { /** - * A step in a GitHub action that triggers analysis with Bandit. + * A Predicate to check the any step in a GitHub action that triggers analysis with Bandit. */ - private static final Pattern RUN_STEP_BANDIT_REGEX_PATTERN - = Pattern.compile("^.*bandit .*$", Pattern.DOTALL); + private static final Map> MATCH_BANDIT_PREDICATE = new HashMap<>(); + + static { + { + MATCH_BANDIT_PREDICATE.put("uses", + step -> Pattern.compile(".*bandit.*$", Pattern.DOTALL).matcher(step).matches()); + MATCH_BANDIT_PREDICATE.put("run", + step -> Pattern.compile("^.*bandit .*$", Pattern.DOTALL).matcher(step).matches()); + } + } /** * Initializes a data provider. @@ -51,69 +55,14 @@ protected ValueSet fetchValuesFor(GitHubProject project) throws IOException { LocalRepository repository = GitHubDataFetcher.localRepositoryFor(project); - Value runsBandit = RUNS_BANDIT_SCANS.value(false); - Value usesBanditScanChecks = USES_BANDIT_SCAN_CHECKS.value(false); - // ideally, we're looking for a GitHub action that runs Bandit scan on pull requests // but if we just find an action that runs Bandit scans, that's also fine - for (Path configPath : findGitHubActionsIn(repository)) { - try (InputStream content = Files.newInputStream(configPath)) { - Map githubAction = Yaml.readMap(content); - if (triggersScan(githubAction)) { - runsBandit = RUNS_BANDIT_SCANS.value(true); - if (runsOnPullRequests(githubAction)) { - usesBanditScanChecks = USES_BANDIT_SCAN_CHECKS.value(true); - break; - } - } - } - } + Visitor visitor = withVisitor(); + browse(repository, MATCH_BANDIT_PREDICATE, visitor); - return ValueHashSet.from(runsBandit, usesBanditScanChecks); - } - - @Override - public boolean triggersScan(Map githubAction) { - return Optional.ofNullable(githubAction.get("jobs")) - .filter(Map.class::isInstance) - .map(Map.class::cast) - .map(jobs -> jobs.values()) - .filter(Iterable.class::isInstance) - .map(Iterable.class::cast) - .map(BanditDataProvider::scanJobs) - .orElse(false); - } + Value runsBandit = RUNS_BANDIT_SCANS.value(visitor.runCheck); + Value usesBanditScanChecks = USES_BANDIT_SCAN_CHECKS.value(visitor.usesCheck); - /** - * Checks if any step in a collection of jobs triggers a Bandit scan. - * - * @param jobs The collection of jobs from GitHub action. - * @return True if a step triggers a Bandit scan, false otherwise. - */ - private static boolean scanJobs(Iterable jobs) { - return IteratorUtils.toList(jobs.iterator()).stream() - .filter(Map.class::isInstance) - .map(Map.class::cast) - .map(job -> job.get("steps")) - .filter(Iterable.class::isInstance) - .map(Iterable.class::cast) - .anyMatch(BanditDataProvider::hasBanditRunStep); - } - - /** - * Checks if a collection of steps from a GitHub action contains a step that triggers a Bandit - * scan. - * - * @param steps The steps to be checked. - * @return True if the steps contain a step that triggers a Bandit scan, false otherwise. - */ - private static boolean hasBanditRunStep(Iterable steps) { - return IteratorUtils.toList(steps.iterator()).stream() - .filter(Map.class::isInstance) - .map(Map.class::cast) - .map(step -> step.get("run")) - .filter(String.class::isInstance) - .map(String.class::cast) - .anyMatch(run -> RUN_STEP_BANDIT_REGEX_PATTERN.matcher(run).matches()); + return ValueHashSet.from(runsBandit, usesBanditScanChecks); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/data/github/CodeqlDataProvider.java b/src/main/java/com/sap/oss/phosphor/fosstars/data/github/CodeqlDataProvider.java index 4b43a473e..c872086fc 100644 --- a/src/main/java/com/sap/oss/phosphor/fosstars/data/github/CodeqlDataProvider.java +++ b/src/main/java/com/sap/oss/phosphor/fosstars/data/github/CodeqlDataProvider.java @@ -10,21 +10,17 @@ 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 com.sap.oss.phosphor.fosstars.util.Yaml; import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; +import java.util.HashMap; import java.util.Map; -import java.util.Optional; -import org.apache.commons.collections4.IteratorUtils; +import java.util.function.Predicate; /** - * The data provider gathers info about how a project uses CodeQL for static analysis. - * In particular, it tires to fill out the following features: + * The data provider gathers info about how a project uses CodeQL for static analysis. In + * particular, it tires to fill out the following features: *
    - *
  • {@link OssFeatures#RUNS_CODEQL_SCANS}
  • - *
  • {@link OssFeatures#USES_CODEQL_CHECKS}
  • + *
  • {@link OssFeatures#RUNS_CODEQL_SCANS}
  • + *
  • {@link OssFeatures#USES_CODEQL_CHECKS}
  • *
* * @see LgtmDataProvider @@ -32,9 +28,15 @@ public class CodeqlDataProvider extends AbstractStaticScanToolsDataProvider { /** - * A step in a GitHub action that triggers analysis with CodeQL. + * A predicate to check if any step in GitHub action that triggers analysis with CodeQL. */ - private static final String CODEQL_ANALYZE_STEP_TASK = "github/codeql-action/analyze"; + private static final Map> MATCH_CODEQL_ANALYZE_PREDICATE = + new HashMap<>(); + + static { + MATCH_CODEQL_ANALYZE_PREDICATE.put("uses", + uses -> uses.startsWith("github/codeql-action/analyze")); + } /** * Initializes a data provider. @@ -51,56 +53,12 @@ protected ValueSet fetchValuesFor(GitHubProject project) throws IOException { LocalRepository repository = GitHubDataFetcher.localRepositoryFor(project); - Value runsCodeqlScans = RUNS_CODEQL_SCANS.value(false); - Value usesCodeqlChecks = USES_CODEQL_CHECKS.value(false); - - // ideally, we're looking for a GitHub action that runs CodeQL scan on pull requests - // but if we just find an action that runs CodeQL scans, that's also fine - for (Path configPath : findGitHubActionsIn(repository)) { - try (InputStream content = Files.newInputStream(configPath)) { - Map githubAction = Yaml.readMap(content); - if (triggersScan(githubAction)) { - runsCodeqlScans = RUNS_CODEQL_SCANS.value(true); - if (runsOnPullRequests(githubAction)) { - usesCodeqlChecks = USES_CODEQL_CHECKS.value(true); - break; - } - } - } - } - - return ValueHashSet.from(usesCodeqlChecks, runsCodeqlScans); - } + Visitor visitor = withVisitor(); + browse(repository, MATCH_CODEQL_ANALYZE_PREDICATE, visitor); - @Override - public boolean triggersScan(Map githubAction) { - return Optional.ofNullable(githubAction.get("jobs")) - .filter(Map.class::isInstance) - .map(Map.class::cast) - .map(jobs -> jobs.get("analyze")) - .filter(Map.class::isInstance) - .map(Map.class::cast) - .map(jobs -> jobs.get("steps")) - .filter(Iterable.class::isInstance) - .map(Iterable.class::cast) - .map(CodeqlDataProvider::hasCodeqlAnalyzeStep) - .orElse(false); - } + Value runsCodeqlScans = RUNS_CODEQL_SCANS.value(visitor.runCheck); + Value usesCodeqlChecks = USES_CODEQL_CHECKS.value(visitor.usesCheck); - /** - * Checks if a collection of steps from a GitHub action contains a step that triggers - * a CodeQL scan. - * - * @param steps The steps to be checked. - * @return True if the steps contain a step that triggers a CodeQL scan, false otherwise. - */ - private static boolean hasCodeqlAnalyzeStep(Iterable steps) { - return IteratorUtils.toList(steps.iterator()).stream() - .filter(Map.class::isInstance) - .map(Map.class::cast) - .map(step -> step.get("uses")) - .filter(String.class::isInstance) - .map(String.class::cast) - .anyMatch(uses -> uses.startsWith(CODEQL_ANALYZE_STEP_TASK)); + return ValueHashSet.from(runsCodeqlScans, usesCodeqlChecks); } -} \ No newline at end of file +} diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/data/github/LocalRepository.java b/src/main/java/com/sap/oss/phosphor/fosstars/data/github/LocalRepository.java index f6ab9fcd1..03536f2cb 100644 --- a/src/main/java/com/sap/oss/phosphor/fosstars/data/github/LocalRepository.java +++ b/src/main/java/com/sap/oss/phosphor/fosstars/data/github/LocalRepository.java @@ -234,6 +234,16 @@ public boolean hasFile(String file) { return Files.isRegularFile(info.path().resolve(Paths.get(file))); } + /** + * Resolve the repository path for specified file. + * + * @param file A path to the file. + * @return Optional resolved path within the {@link LocalRepository}. + */ + public Optional path(String file) { + return Optional.ofNullable(info.path().resolve(Paths.get(file))); + } + /** * Returns a stream of a file if it exists. * diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/data/github/PylintDataProvider.java b/src/main/java/com/sap/oss/phosphor/fosstars/data/github/PylintDataProvider.java new file mode 100644 index 000000000..08f599642 --- /dev/null +++ b/src/main/java/com/sap/oss/phosphor/fosstars/data/github/PylintDataProvider.java @@ -0,0 +1,65 @@ +package com.sap.oss.phosphor.fosstars.data.github; + +import static com.sap.oss.phosphor.fosstars.model.feature.oss.OssFeatures.RUNS_PYLINT_SCANS; +import static com.sap.oss.phosphor.fosstars.model.feature.oss.OssFeatures.USES_PYLINT_SCAN_CHECKS; +import static com.sap.oss.phosphor.fosstars.model.other.Utils.setOf; + +import com.sap.oss.phosphor.fosstars.data.AbstractStaticScanToolsDataProvider; +import com.sap.oss.phosphor.fosstars.model.Value; +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.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; +import java.util.regex.Pattern; + +/** + * The data provider gathers info about how a project uses Bandit for static analysis. In + * particular, it tries to fill out the following features: + *
    + *
  • {@link OssFeatures#RUNS_PYLINT_SCANS}
  • + *
  • {@link OssFeatures#USES_PYLINT_SCAN_CHECKS}
  • + *
+ */ +public class PylintDataProvider extends AbstractStaticScanToolsDataProvider { + + /** + * A predicate to check if the any step of a GitHub action that triggers analysis with Pylint. + */ + private static final Map> MATCH_PYLINT_PREDICATE_MAP = new HashMap<>(); + + static { + MATCH_PYLINT_PREDICATE_MAP.put("uses", + step -> Pattern.compile("^.*pylint.*$", Pattern.DOTALL).matcher(step).matches()); + MATCH_PYLINT_PREDICATE_MAP.put("run", + step -> Pattern.compile("^.*pylint .*$", Pattern.DOTALL).matcher(step).matches()); + MATCH_PYLINT_PREDICATE_MAP.putAll(preCommitHookContextMap(hook -> hook.contains("pylint"))); + } + + /** + * Initializes a data provider. + * + * @param fetcher An interface to GitHub. + */ + public PylintDataProvider(GitHubDataFetcher fetcher) { + super(fetcher, setOf(RUNS_PYLINT_SCANS, USES_PYLINT_SCAN_CHECKS)); + } + + @Override + protected ValueSet fetchValuesFor(GitHubProject project) throws IOException { + logger.info("Figuring out how the project uses pylint ..."); + + LocalRepository repository = GitHubDataFetcher.localRepositoryFor(project); + + Visitor visitor = withVisitor(); + browse(repository, MATCH_PYLINT_PREDICATE_MAP, visitor); + + Value runsPylint = RUNS_PYLINT_SCANS.value(visitor.runCheck); + Value usesPylintScanChecks = USES_PYLINT_SCAN_CHECKS.value(visitor.usesCheck); + + return ValueHashSet.from(runsPylint, usesPylintScanChecks); + } +} diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/github/AbstractGitHubVisitor.java b/src/main/java/com/sap/oss/phosphor/fosstars/github/AbstractGitHubVisitor.java new file mode 100644 index 000000000..e77e3b7b9 --- /dev/null +++ b/src/main/java/com/sap/oss/phosphor/fosstars/github/AbstractGitHubVisitor.java @@ -0,0 +1,37 @@ +package com.sap.oss.phosphor.fosstars.github; + +import com.sap.oss.phosphor.fosstars.data.github.LocalRepository; +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +/** + * An implementation of {@link GitHubVisitor} that does nothing. + */ +public abstract class AbstractGitHubVisitor implements GitHubVisitor { + + @Override + public void visitPreCommitHook(LocalRepository repository, + Map> matchers, Set locations) throws IOException { + // do nothing + } + + @Override + public void visitIniConfig(LocalRepository repository, Map> matchers, + Set locations) throws IOException { + // do nothing + } + + @Override + public void visitSourceCode(LocalRepository repository, Map> matchers, + Set locations) throws IOException { + // don nothing + } + + @Override + public void visitGitHubAction(LocalRepository repository, Map> matchers, + Set locations) throws IOException { + // do nothing + } +} diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/github/GitHubVisitor.java b/src/main/java/com/sap/oss/phosphor/fosstars/github/GitHubVisitor.java new file mode 100644 index 000000000..e55a76325 --- /dev/null +++ b/src/main/java/com/sap/oss/phosphor/fosstars/github/GitHubVisitor.java @@ -0,0 +1,69 @@ +package com.sap.oss.phosphor.fosstars.github; + +import com.sap.oss.phosphor.fosstars.data.github.LocalRepository; +import com.sap.oss.phosphor.fosstars.model.subject.oss.GitHubProject; +import java.io.IOException; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +/** + * A visitor for visiting elements in a {@link GitHubProject}. + */ +public interface GitHubVisitor { + + /** + * Known locations of elements in a {@link GitHubProject}. + */ + enum Location { + PRE_COMMIT_HOOK, INI_CONFIG, TYPE_PY, GITHUB_ACTION + } + + /** + * Visit the pre-commit hook config file. + * + * @param repository The {@link LocalRepository}. + * @param matchers map of {@link Predicate}s to parse for a specific content and match the + * predicate. + * @param locations A set of locations that tells where the plugin is located. + * @throws IOException if something went wrong. + */ + void visitPreCommitHook(LocalRepository repository, Map> matchers, + Set locations) throws IOException; + + /** + * Visit the *.ini files and check for the config. + * + * @param repository The {@link LocalRepository}. + * @param matchers map of {@link Predicate}s to parse for a specific content and match the + * predicate. + * @param locations A set of locations that tells where the plugin is located. + * @throws IOException if something went wrong. + */ + void visitIniConfig(LocalRepository repository, Map> matchers, + Set locations) throws IOException; + + /** + * Visit the source code and check for the config. + * + * @param repository The {@link LocalRepository}. + * @param matchers map of {@link Predicate}s to parse for a specific content and match the + * predicate. + * @param locations A set of locations that tells where the plugin is located. + * @throws IOException if something went wrong. + */ + void visitSourceCode(LocalRepository repository, Map> matchers, + Set locations) throws IOException; + + /** + * Visit the GitHub action and check for the config. + * + * @param repository The {@link LocalRepository}. + * @param matchers map of {@link Predicate}s to parse for a specific content and match the + * predicate. + * @param locations A set of locations that tells where the plugin is located. + * @throws IOException if something went wrong. + */ + void visitGitHubAction(LocalRepository repository, Map> matchers, + Set locations) throws IOException; +} diff --git a/src/main/java/com/sap/oss/phosphor/fosstars/model/feature/oss/OssFeatures.java b/src/main/java/com/sap/oss/phosphor/fosstars/model/feature/oss/OssFeatures.java index b3884d6af..582676927 100755 --- a/src/main/java/com/sap/oss/phosphor/fosstars/model/feature/oss/OssFeatures.java +++ b/src/main/java/com/sap/oss/phosphor/fosstars/model/feature/oss/OssFeatures.java @@ -261,6 +261,25 @@ private OssFeatures() { public static final Feature USES_BANDIT_SCAN_CHECKS = new BooleanFeature("If a project runs Bandit scan checks for commits"); + /** + * Shows if an open-source project runs Pylint scans. + * + * @see Trigger + * Pylint code scanning for a repository + */ + public static final Feature RUNS_PYLINT_SCANS = + new BooleanFeature("If a project runs Pylint scans"); + + /** + * Shows if an open-source project runs Pylint checks before commits. + * + * @see Trigger + * Pylint code scanning job before every commit to a repository + */ + public static final Feature USES_PYLINT_SCAN_CHECKS = + new BooleanFeature("If a project runs Pylint scan checks for commits"); + /** * Shows if an open-source project uses LGTM checks for commits. */ diff --git a/src/test/java/com/sap/oss/phosphor/fosstars/data/AbstractStaticScanToolsDataProviderTest.java b/src/test/java/com/sap/oss/phosphor/fosstars/data/AbstractStaticScanToolsDataProviderTest.java index e4c3b4747..78e33b139 100644 --- a/src/test/java/com/sap/oss/phosphor/fosstars/data/AbstractStaticScanToolsDataProviderTest.java +++ b/src/test/java/com/sap/oss/phosphor/fosstars/data/AbstractStaticScanToolsDataProviderTest.java @@ -15,7 +15,6 @@ import com.sap.oss.phosphor.fosstars.model.ValueSet; import com.sap.oss.phosphor.fosstars.model.subject.oss.GitHubProject; import java.io.IOException; -import java.util.Map; import java.util.Set; import org.apache.commons.lang3.NotImplementedException; import org.junit.Test; @@ -50,11 +49,6 @@ public SastDataProvider( super(fetcher, supportedFeatures); } - @Override - public boolean triggersScan(Map githubAction) { - return false; - } - @Override protected ValueSet fetchValuesFor(GitHubProject project) throws IOException { throw new NotImplementedException("The method is not implemented in this test class"); diff --git a/src/test/java/com/sap/oss/phosphor/fosstars/data/github/PylintDataProviderTest.java b/src/test/java/com/sap/oss/phosphor/fosstars/data/github/PylintDataProviderTest.java new file mode 100644 index 000000000..d9f875c78 --- /dev/null +++ b/src/test/java/com/sap/oss/phosphor/fosstars/data/github/PylintDataProviderTest.java @@ -0,0 +1,242 @@ +package com.sap.oss.phosphor.fosstars.data.github; + +import static com.sap.oss.phosphor.fosstars.model.feature.oss.OssFeatures.RUNS_PYLINT_SCANS; +import static com.sap.oss.phosphor.fosstars.model.feature.oss.OssFeatures.USES_PYLINT_SCAN_CHECKS; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.sap.oss.phosphor.fosstars.model.Feature; +import com.sap.oss.phosphor.fosstars.model.Value; +import com.sap.oss.phosphor.fosstars.model.ValueSet; +import com.sap.oss.phosphor.fosstars.model.subject.oss.GitHubProject; +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class PylintDataProviderTest extends TestGitHubDataFetcherHolder { + + private static final GitHubProject PROJECT = new GitHubProject("org", "test"); + + private static final String GITHUB_WORKFLOW_FILENAME = ".github/workflows/pylint.yml"; + + private static final String GITHUB_PRE_COMMIT_HOOK_CONFIG_FILENAME = ".pre-commit-config.yaml"; + + private static final String INI_CONFIG_FILENAME = "pylint.ini"; + + private static Path repositoryDirectory; + + private static LocalRepository localRepository; + + @Before + public void setup() { + try { + repositoryDirectory = Files.createTempDirectory(PylintDataProviderTest.class.getName()); + localRepository = mock(LocalRepository.class); + TestGitHubDataFetcher.addForTesting(PROJECT, localRepository); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Test + public void testNotInteractive() { + assertFalse(new PylintDataProvider(fetcher).interactive()); + } + + @Test + public void testSupportedFeatures() { + Set> features = new PylintDataProvider(fetcher).supportedFeatures(); + assertEquals(2, features.size()); + assertThat(features, hasItem(RUNS_PYLINT_SCANS)); + assertThat(features, hasItem(USES_PYLINT_SCAN_CHECKS)); + } + + @Test + public void testWithPylintRunsAndChecks() throws IOException { + try (InputStream content = getClass().getResourceAsStream("pylint-analysis-with-run.yml")) { + testPylintFilesCheck(GITHUB_WORKFLOW_FILENAME, content, + RUNS_PYLINT_SCANS.value(true), + USES_PYLINT_SCAN_CHECKS.value(true)); + } + } + + @Test + public void testWithPylintRunsAndMultipleJobs() throws IOException { + try (InputStream content = getClass().getResourceAsStream( + "pylint-analysis-with-multiple-jobs.yml")) { + testPylintFilesCheck(GITHUB_WORKFLOW_FILENAME, content, + RUNS_PYLINT_SCANS.value(true), + USES_PYLINT_SCAN_CHECKS.value(false)); + } + } + + @Test + public void testWithNoPylintRunsButInstallPylint() throws IOException { + try (InputStream content = getClass().getResourceAsStream( + "pylint-analysis-with-no-pylint-run.yml")) { + testPylintFilesCheck(GITHUB_WORKFLOW_FILENAME, content, + RUNS_PYLINT_SCANS.value(false), + USES_PYLINT_SCAN_CHECKS.value(false)); + } + } + + @Test + public void testWithNoPylintRunsButInstallsPylintAndUsesPylint() throws IOException { + try (InputStream content = getClass().getResourceAsStream( + "pylint-analysis-with-no-pylint-run-but-uses-pylint.yml")) { + testPylintFilesCheck(GITHUB_WORKFLOW_FILENAME, content, + RUNS_PYLINT_SCANS.value(false), + USES_PYLINT_SCAN_CHECKS.value(false)); + } + } + + @Test + public void testWithPylintInRepo() throws IOException { + try (InputStream content = + getClass().getResourceAsStream("pylint-analysis-with-pylint-in-repo.yml")) { + testPylintPathCheck(GITHUB_PRE_COMMIT_HOOK_CONFIG_FILENAME, content, + RUNS_PYLINT_SCANS.value(true), + USES_PYLINT_SCAN_CHECKS.value(true)); + } + } + + @Test + public void testWithPylintInEntry() throws IOException { + try (InputStream content = + getClass().getResourceAsStream("pylint-analysis-with-pylint-in-entry.yml")) { + testPylintPathCheck(GITHUB_PRE_COMMIT_HOOK_CONFIG_FILENAME, content, + RUNS_PYLINT_SCANS.value(true), + USES_PYLINT_SCAN_CHECKS.value(true)); + } + } + + @Test + public void testWithPylintInRev() throws IOException { + try (InputStream content = + getClass().getResourceAsStream("pylint-analysis-with-pylint-in-rev.yml")) { + testPylintPathCheck(GITHUB_PRE_COMMIT_HOOK_CONFIG_FILENAME, content, + RUNS_PYLINT_SCANS.value(true), + USES_PYLINT_SCAN_CHECKS.value(true)); + } + } + + @Test + public void testWithNoPylintAsPreCommitHookConfig() throws IOException { + try (InputStream content = + getClass().getResourceAsStream("pylint-analysis-no-pylint-hook.yml")) { + testPylintPathCheck(GITHUB_PRE_COMMIT_HOOK_CONFIG_FILENAME, content, + RUNS_PYLINT_SCANS.value(false), + USES_PYLINT_SCAN_CHECKS.value(false)); + } + } + + @Test + public void testWithPylintProspector() throws IOException { + try (InputStream content = + getClass().getResourceAsStream("pylint-analysis-with-prospector.yml")) { + testPylintPathCheck(GITHUB_PRE_COMMIT_HOOK_CONFIG_FILENAME, content, + RUNS_PYLINT_SCANS.value(true), + USES_PYLINT_SCAN_CHECKS.value(true)); + } + } + + @Test + public void testWithPylintIniConfig() throws IOException { + try (InputStream content = getClass().getResourceAsStream("tox.ini")) { + testPylintFileCheck(INI_CONFIG_FILENAME, content, + RUNS_PYLINT_SCANS.value(true), + USES_PYLINT_SCAN_CHECKS.value(false)); + } + } + + @Test + public void testWithNoPylintIniConfig() throws IOException { + try (InputStream content = getClass().getResourceAsStream("tox-no-pylint.ini")) { + testPylintFileCheck(INI_CONFIG_FILENAME, content, + RUNS_PYLINT_SCANS.value(false), + USES_PYLINT_SCAN_CHECKS.value(false)); + } + } + + private void testPylintFilesCheck(String filename, InputStream content, + Value... expectedValues) throws IOException { + Path file = repositoryDirectory.resolve(filename); + Files.createDirectories(file.getParent()); + when(localRepository.hasDirectory(any(Path.class))).thenReturn(true); + IOUtils.copy(content, Files.newOutputStream(file)); + when(localRepository.files(any(), any())).thenReturn(Collections.singletonList(file)); + + PylintDataProvider provider = new PylintDataProvider(fetcher); + ValueSet values = provider.fetchValuesFor(PROJECT); + + assertEquals(2, values.size()); + for (Value expectedValue : expectedValues) { + Optional> something = values.of(expectedValue.feature()); + assertTrue(something.isPresent()); + assertEquals(expectedValue, something.get()); + } + } + + private void testPylintPathCheck(String filename, InputStream content, + Value... expectedValues) throws IOException { + Path file = repositoryDirectory.resolve(filename); + Files.createDirectories(file.getParent()); + when(localRepository.hasDirectory(any(Path.class))).thenReturn(true); + IOUtils.copy(content, Files.newOutputStream(file)); + when(localRepository.path(any())).thenReturn(Optional.of(file)); + + PylintDataProvider provider = new PylintDataProvider(fetcher); + ValueSet values = provider.fetchValuesFor(PROJECT); + + assertEquals(2, values.size()); + for (Value expectedValue : expectedValues) { + Optional> something = values.of(expectedValue.feature()); + assertTrue(something.isPresent()); + assertEquals(expectedValue, something.get()); + } + } + + private void testPylintFileCheck(String filename, InputStream content, + Value... expectedValues) throws IOException { + Path file = repositoryDirectory.resolve(filename); + Files.createDirectories(file.getParent()); + when(localRepository.hasDirectory(any(Path.class))).thenReturn(true); + IOUtils.copy(content, Files.newOutputStream(file)); + when(localRepository.files(any())).thenReturn(Collections.singletonList(file)); + + PylintDataProvider provider = new PylintDataProvider(fetcher); + ValueSet values = provider.fetchValuesFor(PROJECT); + + assertEquals(2, values.size()); + for (Value expectedValue : expectedValues) { + Optional> something = values.of(expectedValue.feature()); + assertTrue(something.isPresent()); + assertEquals(expectedValue, something.get()); + } + } + + @After + public void shutdown() { + try { + FileUtils.forceDeleteOnExit(repositoryDirectory.toFile()); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } +} \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-as-pre-commit-hook.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-as-pre-commit-hook.yml new file mode 100644 index 000000000..974f0da6e --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-as-pre-commit-hook.yml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/pycqa/pylint + rev: pylint-2.6.0 + hooks: + - id: pylint + name: pylint + entry: pylint + language: system + types: [python] + args: + [ + "-rn", # Only display messages + "-sn", # Don't display the score + ] \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-no-pylint-hook.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-no-pylint-hook.yml new file mode 100644 index 000000000..f3a4235b9 --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-no-pylint-hook.yml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/pycqa/test + rev: test-2.6.0 + hooks: + - id: test + name: test + entry: test + language: system + types: [python] + args: + [ + "-rn", # Only display messages + "-sn", # Don't display the score + ] \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-multiple-jobs.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-multiple-jobs.yml new file mode 100644 index 000000000..2b3811b38 --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-multiple-jobs.yml @@ -0,0 +1,25 @@ +name: "Pylint" +on: + push: + branches: [master] + schedule: + - cron: '0 13 * * 3' +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - name: Use Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + architecture: 'x64' + bandit: + steps: + - run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - run: | + mkdir -p reports + pylint -rn -d unused-variable fileName.py \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-no-pylint-run-but-uses-pylint.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-no-pylint-run-but-uses-pylint.yml new file mode 100644 index 000000000..ea1721255 --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-no-pylint-run-but-uses-pylint.yml @@ -0,0 +1,27 @@ +name: "Pylint" +on: + push: + branches: [master] + pull_request: + branches: [ master ] + schedule: + - cron: '0 13 * * 3' +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Use Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + architecture: 'x64' + - name: Install pylint + run: pip install pylint + bandit: + steps: + - run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - run: | + mkdir -p reports \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-no-pylint-run.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-no-pylint-run.yml new file mode 100644 index 000000000..a8711da53 --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-no-pylint-run.yml @@ -0,0 +1,25 @@ +name: "Pylint" +on: + push: + branches: [master] + schedule: + - cron: '0 13 * * 3' +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Use Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + architecture: 'x64' + - name: Install pylint + run: pip install pylint + bandit: + steps: + - run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - run: | + mkdir -p reports \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-prospector.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-prospector.yml new file mode 100644 index 000000000..2f70c436c --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-prospector.yml @@ -0,0 +1,7 @@ +repos: +- repo: https://github.com/PyCQA/prospector + rev: 1.7.5 + hooks: + - id: prospector + additional_dependencies: + - ".[with_pylint,with_bandit]" \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-pylint-in-entry.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-pylint-in-entry.yml new file mode 100644 index 000000000..9a9bae3af --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-pylint-in-entry.yml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/pycqa/test + rev: test-2.6.0 + hooks: + - id: test + name: test + entry: pylint + language: system + types: [python] + args: + [ + "-rn", # Only display messages + "-sn", # Don't display the score + ] \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-pylint-in-repo.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-pylint-in-repo.yml new file mode 100644 index 000000000..044ea0d4e --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-pylint-in-repo.yml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/pycqa/pylint + rev: 2.6.0 + hooks: + - id: test + name: test + entry: test + language: system + types: [python] + args: + [ + "-rn", # Only display messages + "-sn", # Don't display the score + ] \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-pylint-in-rev.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-pylint-in-rev.yml new file mode 100644 index 000000000..e7299ba23 --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-pylint-in-rev.yml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/pycqa/test + rev: pylint-2.6.0 + hooks: + - id: test + name: test + entry: test + language: system + types: [python] + args: + [ + "-rn", # Only display messages + "-sn", # Don't display the score + ] \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-run.yml b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-run.yml new file mode 100644 index 000000000..9ccaf8934 --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/pylint-analysis-with-run.yml @@ -0,0 +1,25 @@ +name: "Pylint" +on: + push: + branches: [master] + pull_request: + branches: [master] + schedule: + - cron: '0 13 * * 3' +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + + - name: Use Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + architecture: 'x64' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + - name: Run Pylint (Python code checker) + run: pylint -r . -f xml -o pylint.xml || true \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/tox-no-pylint.ini b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/tox-no-pylint.ini new file mode 100644 index 000000000..33a1c1686 --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/tox-no-pylint.ini @@ -0,0 +1,94 @@ +[tox] +minversion = 2.4 +envlist = formatting, py37, py38, py39, py310, pypy, benchmark +skip_missing_interpreters = true +requires = pip >=21.3.1 +isolated_build = true + +[testenv:test] +deps = + -r {toxinidir}/requirements_test_min.txt + pre-commit~=2.13 +commands = + pre-commit run test --all-files + +[testenv:formatting] +basepython = python3 +deps = + -r {toxinidir}/requirements_test_min.txt + pre-commit~=2.13 +commands = + pre-commit run --all-files + +[testenv:mypy] +basepython = python3 +deps = + pre-commit~=2.13 +commands = + pre-commit run mypy --all-files + +[testenv] +setenv = + COVERAGE_FILE = {toxinidir}/.coverage.{envname} +deps = + !pypy: -r {toxinidir}/requirements_test.txt + pypy: -r {toxinidir}/requirements_test_min.txt +commands = + ; Run tests, ensuring all benchmark tests do not run + pytest --benchmark-disable {toxinidir}/tests/ {posargs:} + +[testenv:spelling] +deps = + -r {toxinidir}/requirements_test.txt +commands = + pytest {toxinidir}/tests/ {posargs:} -k unittest_spelling + +[testenv:coverage-html] +setenv = + COVERAGE_FILE = {toxinidir}/.coverage +deps = + -r {toxinidir}/requirements_test.txt +skip_install = true +commands = + coverage combine + coverage html --ignore-errors --rcfile={toxinidir}/.coveragerc + +[testenv:docs] +changedir = doc/ +deps = + -r {toxinidir}/doc/requirements.txt +commands = + sphinx-build -W -b html -d _build/doctrees . _build/html + +[testenv:test_doc] +deps = + -r {toxinidir}/requirements_test.txt +commands = + pytest {toxinidir}/doc/test_messages_documentation.py + +[testenv:benchmark] +deps = + -r {toxinidir}/requirements_test.txt + pygal +commands = + ; Run the only the benchmark tests, grouping output and forcing .json output so we + ; can compare benchmark runs + pytest --exitfirst \ + --failed-first \ + --benchmark-only \ + --benchmark-save=batch_files \ + --benchmark-save-data \ + --benchmark-autosave {toxinidir}/tests \ + --benchmark-group-by="group" \ + {posargs:} + +[testenv:profile_against_external] +setenv = + PYTEST_PROFILE_EXTERNAL = 1 +deps = + -r {toxinidir}/requirements_test.txt + gprof2dot +commands = + pytest --exitfirst \ + --profile-svg \ + {toxinidir}/tests/profile/test_profile_against_externals.py \ No newline at end of file diff --git a/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/tox.ini b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/tox.ini new file mode 100644 index 000000000..27f9f3d59 --- /dev/null +++ b/src/test/resources/com/sap/oss/phosphor/fosstars/data/github/tox.ini @@ -0,0 +1,94 @@ +[tox] +minversion = 2.4 +envlist = formatting, py37, py38, py39, py310, pypy, benchmark +skip_missing_interpreters = true +requires = pip >=21.3.1 +isolated_build = true + +[testenv:pylint] +deps = + -r {toxinidir}/requirements_test_min.txt + pre-commit~=2.13 +commands = + pre-commit run pylint --all-files + +[testenv:formatting] +basepython = python3 +deps = + -r {toxinidir}/requirements_test_min.txt + pre-commit~=2.13 +commands = + pre-commit run --all-files + +[testenv:mypy] +basepython = python3 +deps = + pre-commit~=2.13 +commands = + pre-commit run mypy --all-files + +[testenv] +setenv = + COVERAGE_FILE = {toxinidir}/.coverage.{envname} +deps = + !pypy: -r {toxinidir}/requirements_test.txt + pypy: -r {toxinidir}/requirements_test_min.txt +commands = + ; Run tests, ensuring all benchmark tests do not run + pytest --benchmark-disable {toxinidir}/tests/ {posargs:} + +[testenv:spelling] +deps = + -r {toxinidir}/requirements_test.txt +commands = + pytest {toxinidir}/tests/ {posargs:} -k unittest_spelling + +[testenv:coverage-html] +setenv = + COVERAGE_FILE = {toxinidir}/.coverage +deps = + -r {toxinidir}/requirements_test.txt +skip_install = true +commands = + coverage combine + coverage html --ignore-errors --rcfile={toxinidir}/.coveragerc + +[testenv:docs] +changedir = doc/ +deps = + -r {toxinidir}/doc/requirements.txt +commands = + sphinx-build -W -b html -d _build/doctrees . _build/html + +[testenv:test_doc] +deps = + -r {toxinidir}/requirements_test.txt +commands = + pytest {toxinidir}/doc/test_messages_documentation.py + +[testenv:benchmark] +deps = + -r {toxinidir}/requirements_test.txt + pygal +commands = + ; Run the only the benchmark tests, grouping output and forcing .json output so we + ; can compare benchmark runs + pytest --exitfirst \ + --failed-first \ + --benchmark-only \ + --benchmark-save=batch_files \ + --benchmark-save-data \ + --benchmark-autosave {toxinidir}/tests \ + --benchmark-group-by="group" \ + {posargs:} + +[testenv:profile_against_external] +setenv = + PYTEST_PROFILE_EXTERNAL = 1 +deps = + -r {toxinidir}/requirements_test.txt + gprof2dot +commands = + pytest --exitfirst \ + --profile-svg \ + {toxinidir}/tests/profile/test_profile_against_externals.py \ No newline at end of file