
com.sourceclear.plugins.ScanMavenMojo Maven / Gradle / Ivy
Show all versions of srcclr-maven-plugin Show documentation
/*
* © Copyright 2015 - SourceClear Inc
*/
package com.sourceclear.plugins;
import com.google.common.annotations.VisibleForTesting;
import com.sourceclear.util.config.EnvironmentProvider;
import com.sourceclear.util.config.EnvironmentProviderImpl;
import com.sourceclear.util.config.FailureLevel;
import com.srcclr.sdk.Directives;
import com.srcclr.sdk.LibraryGraph;
import com.srcclr.sdk.LibraryGraphContainer;
import com.srcclr.sdk.build.MavenComponentGraphBuilder;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ScopeArtifactFilter;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.InstantiationStrategy;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.apache.maven.project.MavenProject;
import org.apache.maven.settings.Settings;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
import org.apache.maven.shared.dependency.graph.DependencyNode;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
/**
* The scan goal runs the SRC:CLR scan on the project described by the Maven pom.xml and all submodules.
*
* Upon completion, a list of found vulnerable components will be reported on the console and, if upload is true,
* will be uploaded to the SRC:CLR platform.
*/
@Mojo(
name = "scan",
requiresDependencyCollection = ResolutionScope.TEST, // Because the maven test scope is the superset of all scopes
instantiationStrategy = InstantiationStrategy.SINGLETON)
public class ScanMavenMojo extends AbstractMojo {
private static final String SCOPE_ENV_VAR = "SRCCLR_SCAN_SCOPE";
private static final String DIRECTIVES_FILE_NAME = "srcclr.yml";
private static final Path DIRECTIVES_FILE = Paths.get(DIRECTIVES_FILE_NAME);
private static final String DIRECTIVES_SCOPE_KEY = "scope";
private static final String DEFAULT_SCOPE = Artifact.SCOPE_COMPILE;
@Parameter(defaultValue = "${session}", required = true, readonly = true)
private MavenSession session;
/**
* The settings, from some settings.xml file. We use this to grab proxy information.
*/
@Parameter(defaultValue = "${settings}", readonly = true, required = true)
public Settings settings;
/**
* The name that you would like your project to be called on the SRC:CLR platform.
*
* If not specified, and if a projectID is not provided, a name will be constructed from repo and filesystem information.
*/
@Parameter(property = "projectName")
private String projectName;
/**
* The URL you are using for the SRC:CLR api.
*
* You shouldn't need to use this unless directed to by SRC:CLR support.
*/
@Parameter(property = "apiURL")
protected URI apiURL;
/**
* The apiToken property is used to provide your personal token to the SRC:CLR platform for authentication.
*
* The scan will not finish successfully if this is not provided or if it is incorrect.
*/
@Parameter(property = "apiToken")
protected String apiToken;
/**
* If you know which platform project you would like to associate this scan with, that can be specified by the projectID property.
*/
@Parameter(property = "projectID")
private Long userProjectID;
/**
* Indicates whether the report from SRC:CLR should be uploaded to the platform. If false, the results will only be displayed on the console.
*/
@Parameter(property = "upload", defaultValue = "true")
private boolean upload;
/**
* By default, this plugin only shows components with vulnerabilities in console output. Setting verbose to true causes all components to be listed.
*/
@Parameter(property = "verbose", defaultValue = "false")
private boolean verbose;
/**
* If this is set to true, the plugin will not grab configuration information from environment variables or the
* SRC:CLR directory, relying only on Maven parameters. Used to isolate the Maven plugin from global configuration
* settings.
*/
@Parameter(property = "useOnlyMavenParams", defaultValue = "false")
private boolean useOnlyMavenParams;
@Parameter(property = "srcclr.maven.skip", defaultValue = "false")
private boolean skip;
/**
* If set to true, we will not attempt to contact Platform and acquire licensing information.
*/
@Parameter(property = "srcclr.license.provided", defaultValue = "false")
private boolean licenseProvided;
@Parameter(property = "headlessOutputFile")
private File headlessOutputFile;
/**
* The workspaceSlug if this is an org-level agent.
*/
@Parameter(property = "workspaceSlug")
protected String workspaceSlug;
/**
* Allows you to specify the strength of evidence of a vulnerability at which you want the SRC:CLR scan to fail with an exception
*
* By default, the SRC:CLR scan will fail only if it can show a chain of methods calling a known-vulnerable method
* (the METHOD failureThreshold). If you specify COMPONENT, the scan will fail the build on the discovery of any
* vulnerable component, whether or not a call chain to it can be found. Specifying NEVER will make the scan never
* fail.
*/
@Parameter(property = "failureThreshold")
private FailureLevel failureThreshold;
/**
* Allows users to specify the scope of the scan when building the dependency graph. Possible values are:
* - compile
* - test
* - import
* - runtime
* - system
* - provided
*/
@Parameter(property = "scope")
private String scopeParameter;
@Component(hint = "default")
private DependencyGraphBuilder dependencyGraphBuilder;
EnvironmentProvider environmentProvider = new EnvironmentProviderImpl();
LibraryGraphContainer.Builder projectDependencyTreesBuilder = new LibraryGraphContainer.Builder();
/**
* The final Project the current run will visit, which is obviously the current one for single-module projects,
* but will vary wildly in the case of multi-module ones.
*/
MavenProject lastProject;
private String pluginVersion;
protected File getPathToTop() {
if (session != null) {
return session.getTopLevelProject().getFile().getParentFile();
} else {
getLog().warn("Couldn't figure out the top level of the Maven project.");
return new File(".");
}
}
/**
* Some setup that needs to be done once on this singleton Maven mojo.
*
* @throws MojoFailureException Upon various configure errors, see the handleCOnfig documentation.
*/
@VisibleForTesting
void initialSetup() throws MojoFailureException {
pluginVersion = ((PluginDescriptor) getPluginContext().get("pluginDescriptor")).getVersion();
// Safe so long as we keep things sequential, would have to change for parallel builds.
if (lastProject == null) {
List sortedProjects = session.getProjectDependencyGraph().getSortedProjects();
lastProject = sortedProjects.get(sortedProjects.size() - 1);
}
}
/**
* Gets the dependency graph for a single Maven project (top level or submodule) as a SRC:CLR DependencyGraph object.
*
* @return The dependency graph for the current project
* @throws MojoExecutionException if we run into an unexpected value for Maven's dependency graph (null) or we run
* into a problem translating the dependency graph.
*/
private LibraryGraph getProjectDependencyGraph() throws MojoExecutionException, MojoFailureException {
try {
MavenProject currentProject = session.getCurrentProject();
Path pathToCurrPom = currentProject.getFile().toPath();
String relativePathToCurrPom = (Paths.get(getPathToTop().toString())).relativize(pathToCurrPom).toString();
MavenComponentGraphBuilder graphTranslator = new MavenComponentGraphBuilder();
DependencyNode mavenDependencyTree = dependencyGraphBuilder.buildDependencyGraph(currentProject, new ScopeArtifactFilter(getScope()));
return graphTranslator.buildGraph(mavenDependencyTree, relativePathToCurrPom);
} catch (DependencyGraphBuilderException e) {
throw new MojoExecutionException("Encountered problem running the SRC:CLR maven plugin: " + e.getMessage());
}
}
/**
* The top-level plugin entry point.
*
* @throws MojoExecutionException If an unexpected failure occurs
* @throws MojoFailureException On user error or the discovery of a vulnerability of confidence level at or above the
* failureThreshold
*/
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (skip) {
getLog().info("Skipping the SRC:CLR Maven Plugin because srcclr.maven.skip is set");
return;
}
initialSetup();
final Config config = Config.builder()
.withPluginVersion(pluginVersion)
.withApiToken(apiToken)
.withApiURL(apiURL)
.withFailureThreshold(failureThreshold)
.withLogger(getLog())
.withLicenseProvided(licenseProvided)
.withProjectName(projectName)
.withPathToTop(getPathToTop())
.withSettings(settings)
.withUpload(upload)
.withUseOnlyMavenParams(useOnlyMavenParams)
.withUserProjectID(userProjectID)
.withHeadlessOutputFile(headlessOutputFile)
.withVerbose(verbose)
.withWorkspaceSlug(workspaceSlug)
.build();
final Lifecycle lifecycle = determineLifecycle(config);
lifecycle.configure(environmentProvider);
projectDependencyTreesBuilder.withGraph(getProjectDependencyGraph());
// This works as long as we're doing this sequentially, but would have to change for parallel builds.
if (lastProject.equals(session.getCurrentProject())) {
LibraryGraphContainer depTrees = projectDependencyTreesBuilder.build();
lifecycle.execute(depTrees);
}
}
static Lifecycle determineLifecycle(Config config) {
if (config.getHeadlessOutputFile().isPresent()) {
return new HeadlessLifecycle(config.getHeadlessOutputFile().get());
} else {
return new DefaultLifecycle(config);
}
}
/**
* Returns the scope that is set by the user. There are three possible ways to set the scope:
*
* - the SRCCLR_SCAN_SCOPE environment variable
* - the maven "scope" parameter
* - the scope key in the srcclr.yml directives file
*
* The order that is presented above is also the order of precedence that determines the scope value. That is, if the
* user supplies the scope with multiple ways, like for example, it's supplied by both the environment variable and
* the maven parameter, the value read from the environment variable would be chosen and the parameter will be
* ignored.
*
* If no scope is set, it defaults to {@code Artifact.SCOPE_COMPILE}.
* @return the scope that is set by the user
*/
private String getScope() throws MojoFailureException {
final String scope;
final String envVarScope = environmentProvider.getenv(SCOPE_ENV_VAR);
if (envVarScope != null) {
scope = envVarScope;
} else if (this.scopeParameter != null) {
scope = this.scopeParameter;
} else if (Files.exists(DIRECTIVES_FILE)) {
scope = getScopeFromDirectivesFile(DIRECTIVES_FILE);
} else {
scope = DEFAULT_SCOPE;
}
return toArtifactScopeValue(scope);
}
/**
* Returns the scope that is declared in the directivesFile. If it is not specified, it returns the default scope.
*
* @param directivesFile the file to read the scope from
* @return the scope in the directivesFile
*/
static String getScopeFromDirectivesFile(Path directivesFile) throws MojoFailureException {
final String scope;
try (InputStream is = Files.newInputStream(directivesFile)) {
final Map directives = Directives.parseDirectives(is);
String scopeValue = (String) directives.get(DIRECTIVES_SCOPE_KEY);
if (scopeValue != null) {
scope = scopeValue;
} else {
scope = DEFAULT_SCOPE;
}
} catch (IOException e) {
throw new MojoFailureException(e.getMessage());
}
return scope;
}
/**
* Returns the default scope if scope is not one of the values declared in {@code Artifact}. This ensures that we
* always pass the right scope value to {@code ArtifactFilter}.
*
* @param scope the scope parameter
* @return one of the scopes declared in {@code Artifact}
*/
static String toArtifactScopeValue(String scope) {
switch (scope) {
case Artifact.SCOPE_COMPILE:
case Artifact.SCOPE_TEST:
case Artifact.SCOPE_IMPORT:
case Artifact.SCOPE_RUNTIME:
case Artifact.SCOPE_PROVIDED:
case Artifact.SCOPE_SYSTEM:
return scope;
default:
return DEFAULT_SCOPE;
}
}
}