View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2019 Matthijs van den Bos. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
21  import java.io.File;
22  import java.io.FileFilter;
23  import java.io.IOException;
24  import org.owasp.dependencycheck.Engine;
25  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
26  import org.owasp.dependencycheck.dependency.Dependency;
27  import org.owasp.dependencycheck.exception.InitializationException;
28  import org.owasp.dependencycheck.utils.FileFilterBuilder;
29  import org.owasp.dependencycheck.utils.Settings;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  import java.util.ArrayList;
34  import java.util.List;
35  import javax.json.stream.JsonParsingException;
36  import org.apache.commons.lang3.StringUtils;
37  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
38  import org.owasp.dependencycheck.processing.GoModProcessor;
39  import org.owasp.dependencycheck.utils.processing.ProcessReader;
40  
41  /**
42   * Go mod dependency analyzer.
43   *
44   * @author Matthijs van den Bos
45   */
46  @Experimental
47  public class GolangModAnalyzer extends AbstractFileTypeAnalyzer {
48  
49      /**
50       * The logger.
51       */
52      private static final Logger LOGGER = LoggerFactory.getLogger(GolangModAnalyzer.class);
53  
54      /**
55       * A descriptor for the type of dependencies processed or added by this
56       * analyzer.
57       */
58      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.GOLANG;
59  
60      /**
61       * The name of the analyzer.
62       */
63      private static final String ANALYZER_NAME = "Golang Mod Analyzer";
64  
65      /**
66       * Lock file name. Please note that go.sum is NOT considered a lock file and
67       * may contain dependencies that are no longer used and dependencies of
68       * dependencies. According to here, go.mod should be used for reproducible
69       * builds:
70       * 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
71       */
72      public static final String GO_MOD = "go.mod";
73  
74      /**
75       * The path to the go executable.
76       */
77      private static String goPath = null;
78      /**
79       * The file filter for Gopkg.lock
80       */
81      private static final FileFilter GO_MOD_FILTER = FileFilterBuilder.newInstance()
82              .addFilenames(GO_MOD)
83              .build();
84  
85      /**
86       * Returns the name of the Golang Mode Analyzer.
87       *
88       * @return the name of the analyzer
89       */
90      @Override
91      public String getName() {
92          return ANALYZER_NAME;
93      }
94  
95      /**
96       * Tell that we are used for information collection.
97       *
98       * @return INFORMATION_COLLECTION
99       */
100     @Override
101     public AnalysisPhase getAnalysisPhase() {
102         return AnalysisPhase.INFORMATION_COLLECTION;
103     }
104 
105     /**
106      * Returns the key name for the analyzers enabled setting.
107      *
108      * @return the key name for the analyzers enabled setting
109      */
110     @Override
111     protected String getAnalyzerEnabledSettingKey() {
112         return Settings.KEYS.ANALYZER_GOLANG_MOD_ENABLED;
113     }
114 
115     /**
116      * Returns the FileFilter
117      *
118      * @return the FileFilter
119      */
120     @Override
121     protected FileFilter getFileFilter() {
122         return GO_MOD_FILTER;
123     }
124 
125     /**
126      * Attempts to determine the path to `go`.
127      *
128      * @return the path to `go`
129      */
130     private String getGo() {
131         synchronized (this) {
132             if (goPath == null) {
133                 final String path = getSettings().getString(Settings.KEYS.ANALYZER_GOLANG_PATH);
134                 if (path == null) {
135                     goPath = "go";
136                 } else {
137                     final File goFile = new File(path);
138                     if (goFile.isFile()) {
139                         goPath = goFile.getAbsolutePath();
140                     } else {
141                         LOGGER.warn("Provided path to `go` executable is invalid. Trying default location. "
142                                 + "If you do want to set it, please set the `{}` property",
143                                 Settings.KEYS.ANALYZER_GOLANG_PATH
144                         );
145                         goPath = "go";
146                     }
147                 }
148             }
149         }
150         return goPath;
151     }
152 
153     /**
154      * Launches `go mod edit` to test if go is installed.
155      *
156      * @param folder the folder location to execute go mode help in
157      * @return a reference to the launched process
158      * @throws AnalysisException thrown if there is an issue launching `go mod
159      * edit`
160      * @throws IOException thrown if there is an error starting `go mod edit`
161      */
162     private Process testGoMod(File folder) throws AnalysisException, IOException {
163         if (!folder.isDirectory()) {
164             throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
165         }
166 
167         final List<String> args = new ArrayList<>();
168         args.add(getGo());
169         args.add("mod");
170         args.add("edit");
171 
172         final ProcessBuilder builder = new ProcessBuilder(args);
173         builder.directory(folder);
174         LOGGER.debug("Launching: {} from {}", args, folder);
175         return builder.start();
176     }
177 
178     /**
179      * Launches `go list -json -m -mod=readonly all` in the given folder.
180      *
181      * @param folder the working folder
182      * @return a reference to the launched process
183      * @throws AnalysisException thrown if there is an issue launching `go mod`
184      */
185     private Process launchGoListReadonly(File folder) throws AnalysisException {
186         if (!folder.isDirectory()) {
187             throw new AnalysisException(String.format("%s should have been a directory.", folder.getAbsolutePath()));
188         }
189 
190         final List<String> args = new ArrayList<>();
191         args.add(getGo());
192         args.add("list");
193         args.add("-json");
194         args.add("-m");
195         args.add("-mod=readonly");
196         args.add("all");
197 
198         final ProcessBuilder builder = new ProcessBuilder(args);
199         builder.directory(folder);
200         try {
201             LOGGER.debug("Launching: {} from {}", args, folder);
202             return builder.start();
203         } catch (IOException ioe) {
204             throw new AnalysisException("go initialization failure; this error can be ignored if you are not analyzing Go. "
205                     + "Otherwise ensure that go is installed and the path to go is correctly specified", ioe);
206         }
207     }
208 
209     /**
210      * Initialize the go mod analyzer; ensures that go is installed and can be
211      * called.
212      *
213      * @param engine a reference to the dependency-check engine
214      * @throws InitializationException never thrown
215      */
216     @SuppressWarnings("fallthrough")
217     @SuppressFBWarnings(justification = "The fallthrough is intentional to avoid code duplication", value = {"SF_SWITCH_NO_DEFAULT"})
218     @Override
219     protected void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
220         setEnabled(false);
221         final File tempDirectory;
222         try {
223             tempDirectory = getSettings().getTempDirectory();
224         } catch (IOException ex) {
225             throw new InitializationException("Unable to create temporary file, the Go Mod Analyzer will be disabled", ex);
226         }
227         try {
228             final Process process = testGoMod(tempDirectory);
229             try (ProcessReader processReader = new ProcessReader(process)) {
230                 processReader.readAll();
231                 final int exitValue = process.waitFor();
232                 final int expectedNoModuleFoundExitValue = 1;
233                 final int possiblyGoTooOldExitValue = 2;
234                 final int goExecutableNotFoundExitValue = 127;
235 
236                 switch (exitValue) {
237                     case expectedNoModuleFoundExitValue:
238                         setEnabled(true);
239                         LOGGER.debug("{} is enabled.", ANALYZER_NAME);
240                         break;
241                     case goExecutableNotFoundExitValue:
242                         throw new InitializationException(String.format("Go executable not found. Disabling %s: %s", ANALYZER_NAME, exitValue));
243                     case possiblyGoTooOldExitValue:
244                         final String error = processReader.getError();
245                         if (!StringUtils.isBlank(error)) {
246                             if (error.contains("unknown subcommand \"mod\"")) {
247                                 LOGGER.warn("Your version of `go` does not support modules. Disabling {}. Error: `{}`", ANALYZER_NAME, error);
248                                 throw new InitializationException("Go version does not support modules.");
249                             }
250                             LOGGER.warn("An error occurred calling `go` - no output could be read. Disabling {}.", ANALYZER_NAME);
251                             throw new InitializationException("Error calling `go` - no output could be read.");
252                         }
253                     // fall through
254                     default:
255                         final String msg = String.format("Unexpected exit code from go process. Disabling %s: %s", ANALYZER_NAME, exitValue);
256                         throw new InitializationException(msg);
257                 }
258             }
259         } catch (AnalysisException ae) {
260             final String msg = String.format("Exception from go process: %s. Disabling %s", ae.getCause(), ANALYZER_NAME);
261             throw new InitializationException(msg, ae);
262         } catch (InterruptedException ex) {
263             final String msg = String.format("Go mod process was interrupted. Disabling %s", ANALYZER_NAME);
264             Thread.currentThread().interrupt();
265             throw new InitializationException(msg);
266         } catch (IOException ex) {
267             throw new RuntimeException(ex);
268         }
269     }
270 
271     /**
272      * Analyzes go packages and adds evidence to the dependency.
273      *
274      * @param dependency the dependency being analyzed
275      * @param engine the engine being used to perform the scan
276      * @throws AnalysisException thrown if there is an unrecoverable error
277      * analyzing the dependency
278      */
279     @Override
280     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
281         //engine.removeDependency(dependency);
282 
283         final int exitValue;
284         final File parentFile = dependency.getActualFile().getParentFile();
285         final Process process = launchGoListReadonly(parentFile);
286         String error = null;
287         try (GoModProcessor processor = new GoModProcessor(dependency, engine);
288                 ProcessReader processReader = new ProcessReader(process, processor)) {
289             processReader.readAll();
290             error = processReader.getError();
291             if (!StringUtils.isBlank(error)) {
292                 LOGGER.warn("While analyzing `{}` `go` generated the following warnings:\n{}", dependency.getFilePath(), error);
293             }
294             exitValue = process.exitValue();
295             if (exitValue < 0 || exitValue > 1) {
296                 final String msg = String.format("Error analyzing '%s'; Unexpected exit code from go process; exit code: %s",
297                         dependency.getFilePath(), exitValue);
298                 throw new AnalysisException(msg);
299             }
300         } catch (InterruptedException ie) {
301             Thread.currentThread().interrupt();
302             throw new AnalysisException("go process interrupted while analyzing '" + dependency.getFilePath() + "'", ie);
303         } catch (IOException ex) {
304             throw new AnalysisException("Error closing the go process while analyzing '" + dependency.getFilePath() + "'", ex);
305         } catch (JsonParsingException ex) {
306             final String msg;
307             if (error != null) {
308                 msg = String.format("Error analyzing '%s'; Unable to process output from `go list -json -m -mod=readonly all`; "
309                         + "the command reported the following errors: %s", dependency.getFilePath(), error);
310             } else {
311                 msg = String.format("Error analyzing '%s'; Unable to process output from `go list -json -m -mod=readonly all`; "
312                         + "please validate that the command runs without errors.", dependency.getFilePath());
313             }
314             throw new AnalysisException(msg, ex);
315         }
316     }
317 }