GolangModAnalyzer.java
/*
* This file is part of dependency-check-core.
*
* 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.
*
* Copyright (c) 2019 Matthijs van den Bos. All Rights Reserved.
*/
package org.owasp.dependencycheck.analyzer;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.exception.InitializationException;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import javax.json.stream.JsonParsingException;
import org.apache.commons.lang3.StringUtils;
import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
import org.owasp.dependencycheck.processing.GoModProcessor;
import org.owasp.dependencycheck.utils.processing.ProcessReader;
/**
* Go mod dependency analyzer.
*
* @author Matthijs van den Bos
*/
@Experimental
public class GolangModAnalyzer extends AbstractFileTypeAnalyzer {
/**
* The logger.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(GolangModAnalyzer.class);
/**
* A descriptor for the type of dependencies processed or added by this
* analyzer.
*/
public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.GOLANG;
/**
* The name of the analyzer.
*/
private static final String ANALYZER_NAME = "Golang Mod Analyzer";
/**
* Lock file name. Please note that go.sum is NOT considered a lock file and
* may contain dependencies that are no longer used and dependencies of
* dependencies. According to here, go.mod should be used for reproducible
* builds:
* https://github.com/golang/go/wiki/Modules#is-gosum-a-lock-file-why-does-gosum-include-information-for-module-versions-i-am-no-longer-using
*/
public static final String GO_MOD = "go.mod";
/**
* The path to the go executable.
*/
private static String goPath = null;
/**
* The file filter for Gopkg.lock
*/
private static final FileFilter GO_MOD_FILTER = FileFilterBuilder.newInstance()
.addFilenames(GO_MOD)
.build();
/**
* Returns the name of the Golang Mode Analyzer.
*
* @return the name of the analyzer
*/
@Override
public String getName() {
return ANALYZER_NAME;
}
/**
* Tell that we are used for information collection.
*
* @return INFORMATION_COLLECTION
*/
@Override
public AnalysisPhase getAnalysisPhase() {
return AnalysisPhase.INFORMATION_COLLECTION;
}
/**
* Returns the key name for the analyzers enabled setting.
*
* @return the key name for the analyzers enabled setting
*/
@Override
protected String getAnalyzerEnabledSettingKey() {
return Settings.KEYS.ANALYZER_GOLANG_MOD_ENABLED;
}
/**
* Returns the FileFilter
*
* @return the FileFilter
*/
@Override
protected FileFilter getFileFilter() {
return GO_MOD_FILTER;
}
/**
* Attempts to determine the path to `go`.
*
* @return the path to `go`
*/
private String getGo() {
synchronized (this) {
if (goPath == null) {
final String path = getSettings().getString(Settings.KEYS.ANALYZER_GOLANG_PATH);
if (path == null) {
goPath = "go";
} else {
final File goFile = new File(path);
if (goFile.isFile()) {
goPath = goFile.getAbsolutePath();
} else {
LOGGER.warn("Provided path to `go` executable is invalid. Trying default location. "
+ "If you do want to set it, please set the `{}` property",
Settings.KEYS.ANALYZER_GOLANG_PATH
);
goPath = "go";
}
}
}
}
return goPath;
}
/**
* Launches `go mod edit` to test if go is installed.
*
* @param folder the folder location to execute go mode help in
* @return a reference to the launched process
* @throws AnalysisException thrown if there is an issue launching `go mod
* edit`
* @throws IOException thrown if there is an error starting `go mod edit`
*/
private Process testGoMod(File folder) throws AnalysisException, IOException {
if (!folder.isDirectory()) {
throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
}
final List<String> args = new ArrayList<>();
args.add(getGo());
args.add("mod");
args.add("edit");
final ProcessBuilder builder = new ProcessBuilder(args);
builder.directory(folder);
LOGGER.debug("Launching: {} from {}", args, folder);
return builder.start();
}
/**
* Launches `go list -json -m -mod=readonly all` in the given folder.
*
* @param folder the working folder
* @return a reference to the launched process
* @throws AnalysisException thrown if there is an issue launching `go mod`
*/
private Process launchGoListReadonly(File folder) throws AnalysisException {
if (!folder.isDirectory()) {
throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
}
final List<String> args = new ArrayList<>();
args.add(getGo());
args.add("list");
args.add("-json");
args.add("-m");
args.add("-mod=readonly");
args.add("all");
final ProcessBuilder builder = new ProcessBuilder(args);
builder.directory(folder);
try {
LOGGER.debug("Launching: {} from {}", args, folder);
return builder.start();
} catch (IOException ioe) {
throw new AnalysisException("go initialization failure; this error can be ignored if you are not analyzing Go. "
+ "Otherwise ensure that go is installed and the path to go is correctly specified", ioe);
}
}
/**
* Initialize the go mod analyzer; ensures that go is installed and can be
* called.
*
* @param engine a reference to the dependency-check engine
* @throws InitializationException never thrown
*/
@SuppressWarnings("fallthrough")
@SuppressFBWarnings(justification = "The fallthrough is intentional to avoid code duplication", value = {"SF_SWITCH_NO_DEFAULT"})
@Override
protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
setEnabled(false);
final File tempDirectory;
try {
tempDirectory = getSettings().getTempDirectory();
} catch (IOException ex) {
throw new InitializationException("Unable to create temporary file, the Go Mod Analyzer will be disabled", ex);
}
try {
final Process process = testGoMod(tempDirectory);
try (ProcessReader processReader = new ProcessReader(process)) {
processReader.readAll();
final int exitValue = process.waitFor();
final int expectedNoModuleFoundExitValue = 1;
final int possiblyGoTooOldExitValue = 2;
final int goExecutableNotFoundExitValue = 127;
switch (exitValue) {
case expectedNoModuleFoundExitValue:
setEnabled(true);
LOGGER.debug("{} is enabled.", ANALYZER_NAME);
break;
case goExecutableNotFoundExitValue:
throw new InitializationException(String.format("Go executable not found. Disabling %s: %s", ANALYZER_NAME, exitValue));
case possiblyGoTooOldExitValue:
final String error = processReader.getError();
if (!StringUtils.isBlank(error)) {
if (error.contains("unknown subcommand \"mod\"")) {
LOGGER.warn("Your version of `go` does not support modules. Disabling {}. Error: `{}`", ANALYZER_NAME, error);
throw new InitializationException("Go version does not support modules.");
}
LOGGER.warn("An error occurred calling `go` - no output could be read. Disabling {}.", ANALYZER_NAME);
throw new InitializationException("Error calling `go` - no output could be read.");
}
// fall through
default:
final String msg = String.format("Unexpected exit code from go process. Disabling %s: %s", ANALYZER_NAME, exitValue);
throw new InitializationException(msg);
}
}
} catch (AnalysisException ae) {
final String msg = String.format("Exception from go process: %s. Disabling %s", ae.getCause(), ANALYZER_NAME);
throw new InitializationException(msg, ae);
} catch (InterruptedException ex) {
final String msg = String.format("Go mod process was interrupted. Disabling %s", ANALYZER_NAME);
Thread.currentThread().interrupt();
throw new InitializationException(msg);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
/**
* Analyzes go packages and adds evidence to the dependency.
*
* @param dependency the dependency being analyzed
* @param engine the engine being used to perform the scan
* @throws AnalysisException thrown if there is an unrecoverable error
* analyzing the dependency
*/
@Override
protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
//engine.removeDependency(dependency);
final int exitValue;
final File parentFile = dependency.getActualFile().getParentFile();
final Process process = launchGoListReadonly(parentFile);
String error = null;
try (GoModProcessor processor = new GoModProcessor(dependency, engine);
ProcessReader processReader = new ProcessReader(process, processor)) {
processReader.readAll();
error = processReader.getError();
if (!StringUtils.isBlank(error)) {
LOGGER.warn("While analyzing `{}` `go` generated the following warnings:\n{}", dependency.getFilePath(), error);
}
exitValue = process.exitValue();
if (exitValue < 0 || exitValue > 1) {
final String msg = String.format("Error analyzing '%s'; Unexpected exit code from go process; exit code: %s",
dependency.getFilePath(), exitValue);
throw new AnalysisException(msg);
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new AnalysisException("go process interrupted while analyzing '" + dependency.getFilePath() + "'", ie);
} catch (IOException ex) {
throw new AnalysisException("Error closing the go process while analyzing '" + dependency.getFilePath() + "'", ex);
} catch (JsonParsingException ex) {
final String msg;
if (error != null) {
msg = String.format("Error analyzing '%s'; Unable to process output from `go list -json -m -mod=readonly all`; "
+ "the command reported the following errors: %s", dependency.getFilePath(), error);
} else {
msg = String.format("Error analyzing '%s'; Unable to process output from `go list -json -m -mod=readonly all`; "
+ "please validate that the command runs without errors.", dependency.getFilePath());
}
throw new AnalysisException(msg, ex);
}
}
}