com.sourceclear.plugins.CaptureMavenMojo Maven / Gradle / Ivy
Show all versions of srcclr-maven-plugin Show documentation
/*
* Copyright 2001-2005 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.sourceclear.plugins;
import com.google.common.collect.ImmutableSet;
import com.sourceclear.api.client.Client;
import com.sourceclear.api.client.SourceClearClient;
import com.sourceclear.api.data.artifact.LibraryArtifactApiModel;
import com.sourceclear.api.data.artifact.LibraryMatchWithArtifactsApiModel;
import com.sourceclear.api.data.evidence.Evidence;
import com.sourceclear.api.data.evidence.LanguageType;
import com.sourceclear.api.data.match.MatchQuery;
import com.sourceclear.api.data.match.MatchResponse;
import com.sourceclear.api.data.methods.MethodCallChains;
import com.sourceclear.api.data.methods.VulnerableMethodUpload;
import com.sourceclear.engine.common.ClassFileVisitor;
import com.sourceclear.engine.common.DependencyGraph;
import com.sourceclear.engine.common.logging.NoopLogStream;
import com.sourceclear.engine.component.Utils;
import com.sourceclear.engine.methods.ClassMethodsEngine;
import com.sourceclear.engine.methods.VulnerableMethodsCollator;
import com.sourceclear.plugins.config.ConfigService;
import com.sourceclear.plugins.config.ConfigServiceImpl;
import com.sourceclear.plugins.config.ConsoleConfig;
import com.sourceclear.util.io.GitUtils;
import com.sourceclear.util.io.renderers.ComponentRenderer;
import com.sourceclear.util.io.renderers.ScanReport;
import com.sourceclear.util.io.renderers.SummaryRenderer;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import jdk.nashorn.internal.ir.annotations.Immutable;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.filter.ArtifactFilter;
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.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.project.ProjectBuildingRequest;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilder;
import org.apache.maven.shared.dependency.graph.DependencyGraphBuilderException;
import org.apache.maven.shared.dependency.graph.DependencyNode;
/**
* 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 shouldUpload is true,
* will be uploaded to the SRC:CLR platform.
*/
@Mojo(
name = "scan",
requiresDependencyCollection = ResolutionScope.COMPILE,
requiresProject = true,
instantiationStrategy=InstantiationStrategy.SINGLETON,
threadSafe = false) // I know this is the default, but since I'm doing a lazy init on a singleton, make sure people know it.
public class CaptureMavenMojo
extends AbstractMojo {
@Parameter(defaultValue = "${session}", required = true, readonly = true)
private MavenSession session;
/**
* This token is used to grant you authority to write to a repo owned by an organization on the SRC:CLR platform.
*
* If not specified, any uploads will be done to your personal environment.
*/
@Parameter(property = "orgToken")
protected String orgToken;
/**
* The name of your organization on the SRC:CLR platform
*/
@Parameter(property="orgName")
private String orgName;
/**
* 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 userToken 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 = "userToken")
protected String userToken;
/**
* 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 = "shouldUpload", defaultValue="true")
private boolean shouldUpload;
/**
* 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;
public static final URI DEFAULT_API_URL = URI.create("https://api.srcclr.com");
enum FailureLevel {
COMPONENT,
METHOD,
NEVER
}
/**
* 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", defaultValue="METHOD")
private FailureLevel failureThreshold;
@org.apache.maven.plugins.annotations.Component(hint = "default")
private DependencyGraphBuilder dependencyGraphBuilder;
@Immutable // This class is stateless, hence why we have a singleton instance that is immutable
static final ClassMethodsEngine METHODS_ENGINE = new ClassMethodsEngine();
// To time how long our scan takes.
long scanStart;
class NoTestArtifacts implements ArtifactFilter {
@Override
public boolean include(Artifact artifact) {
return !artifact.getScope().equals("test");
}
}
ArrayList projectDependencyTrees = new ArrayList<>();
MavenProject lastProject = null;
/**
* Handles some configuration issues. Essentially, looks to see if we have certain maven parameters. If the
* parameters aren't present, we dig around in some SRC:CLR config files and environment variables to attempt to fill
* in those parameters.
* @throws MojoFailureException when no userToken is specified, when the SRC:CLR platform URI is malformed, etc.
*/
protected void handleConfig() throws MojoFailureException {
// If these are all non-null, no need to look in the config files. Maven properties take precedence.
if(StringUtils.isNotBlank(userToken) && StringUtils.isNotBlank(orgToken) && (apiURL != null)) {
return;
}
ConfigService configService = new ConfigServiceImpl();
if(StringUtils.isBlank(userToken)) {
try {
userToken = configService.getSourceClearClientToken();
} catch(Exception e) {
getLog().debug("Error in retrieving client token", e);
}
if(StringUtils.isBlank(userToken)) {
throw new MojoFailureException("The userToken parameter is required. Specify it with -DuserToken or by setting userToken in a SRC:CLR configuration file.");
}
}
if(StringUtils.isBlank(orgToken)) {
try {
orgToken = configService.getSourceClearOrgToken();
} catch(Exception e) {
getLog().debug("Error in retrieving org token", e);
}
if(StringUtils.isBlank(orgToken)) {
orgToken = null;
}
}
if(apiURL == null) {
ConsoleConfig config = null;
try {
config = configService.getConfiguration();
} catch(Exception e) {
getLog().debug("Error in retrieving URL string: ", e);
}
String apiURLString = null;
if(config != null) {
String configApiString = config.getApiUrl();
if(StringUtils.isNotBlank(configApiString)) {
apiURLString = configApiString;
}
}
if(StringUtils.isNotBlank(apiURLString)) {
try {
apiURL = new URI(apiURLString);
} catch (Exception e) {
throw new MojoFailureException("Failure in trying to use specified apiURL:", e);
}
}
}
if(apiURL == null) {
apiURL = DEFAULT_API_URL;
}
}
/**
* Some setup that needs to be done once on this singleton Maven mojo.
* @throws MojoFailureException Upon various configure errors, see the handleCOnfig documentation.
*/
private void initialSetup() throws MojoFailureException {
// Safe so long as we keep things sequential, would have to change for parallel builds.
if (lastProject == null) {
scanStart = System.currentTimeMillis();
handleConfig();
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 DependencyGraph getProjectDependencyGraph() throws MojoExecutionException {
Path pathToTop = getPathToTop();
try {
ProjectBuildingRequest buildRequest = session.getProjectBuildingRequest();
MavenProject currentProject = session.getCurrentProject();
buildRequest.setProject(currentProject);
Path pathToCurrPom = currentProject.getFile().toPath();
Path relativePathToCurrPom = pathToTop.relativize(pathToCurrPom);
DependencyGraphTranslator graphTranslator = new DependencyGraphTranslator(relativePathToCurrPom.toString());
DependencyNode mavenDependencyTree =
dependencyGraphBuilder.buildDependencyGraph(buildRequest, new NoTestArtifacts());
return graphTranslator.getSrcclrDependencyGraph(mavenDependencyTree);
} catch (DependencyGraphBuilderException e) {
throw new MojoExecutionException("Encountered problem running the SRC:CLR maven plugin", e);
}
}
/**
* Grabs the path to the top-level Maven project's directory
* @return the path to the top level Maven project's directory
*/
private Path getPathToTop() {
return Paths.get(session.getTopLevelProject().getFile().getParent());
}
/**
* After collecting evidence describing the artifacts used in this Maven project, this function builds a MatchQuery
* to send up to the SRC:CLR platform to find which ones contain vulnerabilities.
* @param evidence The evidence collected from artifact dependency graphs
* @param doVulnMethods Whether we are going to attempt vulnerable method analysis
* @return A MatchQuery suitible for sending to the SRC:CLR platform to find vulnerabilities.
*/
private MatchQuery buildMatchQuery(ImmutableSet evidence, boolean doVulnMethods) {
String gitBranch = null;
String gitCommitHash = null;
String repoUrl = null;
File projectDir = session.getTopLevelProject().getBasedir();
Path pathToTop = getPathToTop();
try {
gitBranch = GitUtils.getBranch(projectDir);
gitCommitHash = GitUtils.getCommitHash(projectDir);
repoUrl = GitUtils.getRepoUrl(projectDir);
if(repoUrl == null) {
repoUrl = pathToTop.toFile().getCanonicalPath();
}
} catch (Exception e) {
getLog().error("\n ** A problem was encountered in trying to find repo naming information. ", e);
}
MatchQuery.Builder matchQueryBuilder =
new MatchQuery.Builder()
.withProjectId(userProjectID)
.withScanId(generateScanId())
.withEvidence(new ArrayList<>(evidence))
.withBranch(gitBranch)
.withCommitHash(gitCommitHash)
.withRepoUrl(repoUrl)
.withProjectName(projectName)
.withOrganization(orgName)
.persist(shouldUpload)
.requestVulnMethods(doVulnMethods);
return matchQueryBuilder.build();
}
/**
* Builds a SourceClearClient for communicating with the SRC:CLR platform
* @return a client object
*/
private Client buildClient() {
SourceClearClient.Builder clientBuilder = new SourceClearClient.Builder()
.withAuthToken(userToken)
.withOrgToken(orgToken)
.withBaseURI(apiURL);
return clientBuilder.build();
}
/**
* Builds a ScanReport object, which indicates what vulnerabilities we found and where. This will be rendered to the
* console in a few different rendering formats.
* @param evidence Information about the artifacts found in the dependency trees
* @param vulnMethods The list of vulnerable methods found
* @param response The response from the SRC:CLR platform from the MatchQuery
* @return A ScanReport indicating the results of this scan.
*/
private ScanReport buildReport(
ImmutableSet evidence,
Map> vulnMethods,
MatchResponse response) {
ScanReport.Builder reportBuilder = new ScanReport.Builder()
.withScanPath(getPathToTop().toString())
.withDuration((System.currentTimeMillis() - scanStart) / 1000)
.withEvidence(evidence)
.withMatchResponse(response)
.withUpload(shouldUpload);
if (vulnMethods != null) {
reportBuilder.withCallChains(vulnMethods);
}
return reportBuilder.build();
}
/**
* Print the results of the scan to the console in various formats.
* @param report The ScanReport generated from this scan.
*/
private void renderReport(ScanReport report) {
SummaryRenderer sumRenderer = new SummaryRenderer();
ComponentRenderer compRenderer= new ComponentRenderer(verbose);
compRenderer.accept(report);
sumRenderer.accept(report);
}
/**
* After all subprojects have finished building their dependency graphs, this method consumes that information, scans
* for vulnerabilities, and reports to the console and (depending on shouldUpload) the SRC:CLR platform
* @throws MojoFailureException If vulnerabilities with a confidence level equal to or greater than than the
* failureThreshold are found, we throw a MojoFailureException.
* @throws MojoExecutionException If we experience unexpected network issues while talking to the SRC:CLR platform
*/
private void consumeAndReport() throws MojoFailureException, MojoExecutionException {
ImmutableSet.Builder builder = new ImmutableSet.Builder<>();
builder.addAll(
Utils.fromDependencyGraph(
LanguageType.JAVA,
projectDependencyTrees.toArray(new DependencyGraph[projectDependencyTrees.size()])));
ImmutableSet evidence = builder.build();
String pathToTopStr = getPathToTop().toString();
boolean doVulnMethods = isMethodsSupported(pathToTopStr);
if(!doVulnMethods) {
System.err.printf("Couldn't find any built class files in %s, skipping vulnerable methods check.\n", pathToTopStr);
}
Client client = buildClient();
long matchingStart = System.currentTimeMillis();
MatchResponse response;
try {
response = client.match(buildMatchQuery(evidence, doVulnMethods));
} catch (IOException e) {
throw new MojoExecutionException("Encountered problem running the SRC:CLR maven plugin", e);
}
Long trueProjectID = (userProjectID == null) ? response.getProjectId() : userProjectID;
if (doVulnMethods && (trueProjectID == null)) {
System.err.println("No projectID found, skipping vulnerable method upload.");
doVulnMethods = false;
}
System.out.println(); // flush from the print "API..." upstairs
System.out.printf(" -> matched in %s seconds.%n%n", (System.currentTimeMillis() - matchingStart) / 1000);
Map> vulnMethods = null;
if(doVulnMethods) {
vulnMethods = doVulnMethodsScan(evidence, response.getComponents(), client, trueProjectID);
} else {
System.out.println("Skipping vulnerable methods scan.");
}
ScanReport report = buildReport(evidence, vulnMethods, response);
renderReport(report);
maybeFailOnVulns(report.getVulnerableMethods(), report.getVulnerableComponents());
}
/**
* Perform the vulnerable method scan, and (depending on shouldUpload) send this to the SRC:CLR platform.
* @param evidence Evidence harvested from the artifact dependency graphs
* @param components Components created from the artifact dependency graphs
* @param client The client used to communicate with the SRC:CLR platform
* @param trueProjectID The projectID, either specified by the user or given to us in the response from the name.
* @return A map describing vulnerable method callchains.
*/
private Map> doVulnMethodsScan(
ImmutableSet evidence,
List components,
Client client,
Long trueProjectID) {
Map> vulnMethods = null;
final VulnerableMethodsCollator collator =
new VulnerableMethodsCollator(evidence, components, METHODS_ENGINE, new NoopLogStream());
Path scanPath = getPathToTop();
collator.scanPath(scanPath.toFile());
vulnMethods = collator.getMethodsMap();
for (List vulnMethodList : vulnMethods.values()) {
final VulnerableMethodUpload vmUpload = new VulnerableMethodUpload(vulnMethodList);
boolean result = false;
if (shouldUpload) {
try {
result = client.uploadVulnerableMethodsForProject(trueProjectID, vmUpload);
} catch (IOException e) {
// Report an error in the finally, try to continue.
getLog().error("Encountered error during vulnerable method upload.", e);
} finally {
if (!result) {
System.err.println("Vulnerable methods upload failed.");
}
}
}
}
return vulnMethods;
}
/**
* Fails if vulnerabilities are found with confidence greater than or equal to the failureThreshold
* @param optVulnMethodCount The number of vulnerable method callchains found
* @param vulnComponentCount The number of vulnerable components found
* @throws MojoFailureException If the vulnerability confidence meets or exceeds the threshold.
*/
protected void maybeFailOnVulns(Integer optVulnMethodCount, int vulnComponentCount) throws MojoFailureException {
FailureLevel failureLevel = null;
if((optVulnMethodCount != null) && (optVulnMethodCount > 0)) {
failureLevel = FailureLevel.METHOD;
} else if(vulnComponentCount > 0) {
failureLevel = FailureLevel.COMPONENT;
}
if((failureLevel != null) && (failureLevel.compareTo(failureThreshold) >= 0)) {
throw new MojoFailureException(
"The current scan found vulnerable " +
failureLevel.toString().toLowerCase() +
"s, failing as failureThreshold is set to " +
failureThreshold +
".");
}
}
/**
* 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 {
initialSetup();
projectDependencyTrees.add(getProjectDependencyGraph());
// This works as long as we're doing this sequentially, but would have to change for parallel builds.
if (lastProject.equals(session.getCurrentProject())) {
consumeAndReport();
}
}
/**
* A unique identifier for the current scan.
* @return The unique identifier.
*/
private String generateScanId() {
return String.format("maven-plugin-%s", UUID.randomUUID());
}
//FIXME: The following method is copy-pasted from com.sourceclear.engine.component.natives.parsing.CollectionResult.
// They should share the same code.
/**
* If the project path contains class files (other than the default exclusions),
* we can assume that vulnerable methods scanning is to be supported.
*/
private boolean isMethodsSupported(String projectRoot) {
try {
ClassFileVisitor visitor = new ClassFileVisitor();
Files.walkFileTree(Paths.get(projectRoot), visitor);
return !visitor.getClassFiles().isEmpty();
} catch (Exception ex) {
System.err.println("Unable to determine vulnerable methods support, skipping");
getLog().error("Couldn't scan for class files", ex);
return false;
}
}
}