View Javadoc
1   /*
2    * This file is part of dependency-check-cli.
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) 2012 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck;
19  
20  import ch.qos.logback.classic.LoggerContext;
21  import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
22  import java.io.File;
23  import java.io.FileNotFoundException;
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Set;
29  import org.apache.commons.cli.ParseException;
30  import org.owasp.dependencycheck.data.nvdcve.CveDB;
31  import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
32  import org.owasp.dependencycheck.data.nvdcve.DatabaseProperties;
33  import org.owasp.dependencycheck.dependency.Dependency;
34  import org.apache.tools.ant.DirectoryScanner;
35  import org.owasp.dependencycheck.reporting.ReportGenerator;
36  import org.owasp.dependencycheck.utils.Settings;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  import ch.qos.logback.core.FileAppender;
40  import org.slf4j.impl.StaticLoggerBinder;
41  
42  /**
43   * The command line interface for the DependencyCheck application.
44   *
45   * @author Jeremy Long
46   */
47  public class App {
48  
49      /**
50       * The logger.
51       */
52      private static final Logger LOGGER = LoggerFactory.getLogger(App.class);
53  
54      /**
55       * The main method for the application.
56       *
57       * @param args the command line arguments
58       */
59      public static void main(String[] args) {
60          try {
61              Settings.initialize();
62              final App app = new App();
63              app.run(args);
64          } finally {
65              Settings.cleanup(true);
66          }
67      }
68  
69      /**
70       * Main CLI entry-point into the application.
71       *
72       * @param args the command line arguments
73       */
74      public void run(String[] args) {
75          final CliParser cli = new CliParser();
76  
77          try {
78              cli.parse(args);
79          } catch (FileNotFoundException ex) {
80              System.err.println(ex.getMessage());
81              cli.printHelp();
82              return;
83          } catch (ParseException ex) {
84              System.err.println(ex.getMessage());
85              cli.printHelp();
86              return;
87          }
88  
89          if (cli.getVerboseLog() != null) {
90              prepareLogger(cli.getVerboseLog());
91          }
92  
93          if (cli.isPurge()) {
94              if (cli.getConnectionString() != null) {
95                  LOGGER.error("Unable to purge the database when using a non-default connection string");
96              } else {
97                  populateSettings(cli);
98                  File db;
99                  try {
100                     db = new File(Settings.getDataDirectory(), "dc.h2.db");
101                     if (db.exists()) {
102                         if (db.delete()) {
103                             LOGGER.info("Database file purged; local copy of the NVD has been removed");
104                         } else {
105                             LOGGER.error("Unable to delete '{}'; please delete the file manually", db.getAbsolutePath());
106                         }
107                     } else {
108                         LOGGER.error("Unable to purge database; the database file does not exists: {}", db.getAbsolutePath());
109                     }
110                 } catch (IOException ex) {
111                     LOGGER.error("Unable to delete the database");
112                 }
113             }
114         } else if (cli.isGetVersion()) {
115             cli.printVersionInfo();
116         } else if (cli.isUpdateOnly()) {
117             populateSettings(cli);
118             runUpdateOnly();
119         } else if (cli.isRunScan()) {
120             populateSettings(cli);
121             try {
122                 runScan(cli.getReportDirectory(), cli.getReportFormat(), cli.getProjectName(), cli.getScanFiles(),
123                         cli.getExcludeList(), cli.getSymLinkDepth());
124             } catch (InvalidScanPathException ex) {
125                 LOGGER.error("An invalid scan path was detected; unable to scan '//*' paths");
126             }
127         } else {
128             cli.printHelp();
129         }
130     }
131 
132     /**
133      * Scans the specified directories and writes the dependency reports to the reportDirectory.
134      *
135      * @param reportDirectory the path to the directory where the reports will be written
136      * @param outputFormat the output format of the report
137      * @param applicationName the application name for the report
138      * @param files the files/directories to scan
139      * @param excludes the patterns for files/directories to exclude
140      * @param symLinkDepth the depth that symbolic links will be followed
141      *
142      * @throws InvalidScanPathException thrown if the path to scan starts with "//"
143      */
144     private void runScan(String reportDirectory, String outputFormat, String applicationName, String[] files,
145             String[] excludes, int symLinkDepth) throws InvalidScanPathException {
146         Engine engine = null;
147         try {
148             engine = new Engine();
149             final List<String> antStylePaths = new ArrayList<String>();
150             for (String file : files) {
151                 final String antPath = ensureCanonicalPath(file);
152                 antStylePaths.add(antPath);
153             }
154 
155             final Set<File> paths = new HashSet<File>();
156             for (String file : antStylePaths) {
157                 LOGGER.debug("Scanning {}", file);
158                 final DirectoryScanner scanner = new DirectoryScanner();
159                 String include = file.replace('\\', '/');
160                 File baseDir;
161 
162                 if (include.startsWith("//")) {
163                     throw new InvalidScanPathException("Unable to scan paths specified by //");
164                 } else {
165                     final int pos = getLastFileSeparator(include);
166                     final String tmpBase = include.substring(0, pos);
167                     final String tmpInclude = include.substring(pos + 1);
168                     if (tmpInclude.indexOf('*') >= 0 || tmpInclude.indexOf('?') >= 0
169                             || (new File(include)).isFile()) {
170                         baseDir = new File(tmpBase);
171                         include = tmpInclude;
172                     } else {
173                         baseDir = new File(tmpBase, tmpInclude);
174                         include = "**/*";
175                     }
176                 }
177                 //LOGGER.debug("baseDir: {}", baseDir);
178                 //LOGGER.debug("include: {}", include);
179                 scanner.setBasedir(baseDir);
180                 final String[] includes = {include};
181                 scanner.setIncludes(includes);
182                 scanner.setMaxLevelsOfSymlinks(symLinkDepth);
183                 if (symLinkDepth <= 0) {
184                     scanner.setFollowSymlinks(false);
185                 }
186                 if (excludes != null && excludes.length > 0) {
187                     scanner.addExcludes(excludes);
188                 }
189                 scanner.scan();
190                 if (scanner.getIncludedFilesCount() > 0) {
191                     for (String s : scanner.getIncludedFiles()) {
192                         final File f = new File(baseDir, s);
193                         LOGGER.debug("Found file {}", f.toString());
194                         paths.add(f);
195                     }
196                 }
197             }
198             engine.scan(paths);
199 
200             engine.analyzeDependencies();
201             final List<Dependency> dependencies = engine.getDependencies();
202             DatabaseProperties prop = null;
203             CveDB cve = null;
204             try {
205                 cve = new CveDB();
206                 cve.open();
207                 prop = cve.getDatabaseProperties();
208             } catch (DatabaseException ex) {
209                 LOGGER.debug("Unable to retrieve DB Properties", ex);
210             } finally {
211                 if (cve != null) {
212                     cve.close();
213                 }
214             }
215             final ReportGenerator report = new ReportGenerator(applicationName, dependencies, engine.getAnalyzers(), prop);
216             try {
217                 report.generateReports(reportDirectory, outputFormat);
218             } catch (IOException ex) {
219                 LOGGER.error("There was an IO error while attempting to generate the report.");
220                 LOGGER.debug("", ex);
221             } catch (Throwable ex) {
222                 LOGGER.error("There was an error while attempting to generate the report.");
223                 LOGGER.debug("", ex);
224             }
225         } catch (DatabaseException ex) {
226             LOGGER.error("Unable to connect to the dependency-check database; analysis has stopped");
227             LOGGER.debug("", ex);
228         } finally {
229             if (engine != null) {
230                 engine.cleanup();
231             }
232         }
233     }
234 
235     /**
236      * Only executes the update phase of dependency-check.
237      */
238     private void runUpdateOnly() {
239         Engine engine = null;
240         try {
241             engine = new Engine();
242             engine.doUpdates();
243         } catch (DatabaseException ex) {
244             LOGGER.error("Unable to connect to the dependency-check database; analysis has stopped");
245             LOGGER.debug("", ex);
246         } finally {
247             if (engine != null) {
248                 engine.cleanup();
249             }
250         }
251     }
252 
253     /**
254      * Updates the global Settings.
255      *
256      * @param cli a reference to the CLI Parser that contains the command line arguments used to set the corresponding settings in
257      * the core engine.
258      */
259     private void populateSettings(CliParser cli) {
260 
261         final boolean autoUpdate = cli.isAutoUpdate();
262         final String connectionTimeout = cli.getConnectionTimeout();
263         final String proxyServer = cli.getProxyServer();
264         final String proxyPort = cli.getProxyPort();
265         final String proxyUser = cli.getProxyUsername();
266         final String proxyPass = cli.getProxyPassword();
267         final String dataDirectory = cli.getDataDirectory();
268         final File propertiesFile = cli.getPropertiesFile();
269         final String suppressionFile = cli.getSuppressionFile();
270         final String nexusUrl = cli.getNexusUrl();
271         final String databaseDriverName = cli.getDatabaseDriverName();
272         final String databaseDriverPath = cli.getDatabaseDriverPath();
273         final String connectionString = cli.getConnectionString();
274         final String databaseUser = cli.getDatabaseUser();
275         final String databasePassword = cli.getDatabasePassword();
276         final String additionalZipExtensions = cli.getAdditionalZipExtensions();
277         final String pathToMono = cli.getPathToMono();
278         final String cveMod12 = cli.getModifiedCve12Url();
279         final String cveMod20 = cli.getModifiedCve20Url();
280         final String cveBase12 = cli.getBaseCve12Url();
281         final String cveBase20 = cli.getBaseCve20Url();
282 
283         if (propertiesFile != null) {
284             try {
285                 Settings.mergeProperties(propertiesFile);
286             } catch (FileNotFoundException ex) {
287                 LOGGER.error("Unable to load properties file '{}'", propertiesFile.getPath());
288                 LOGGER.debug("", ex);
289             } catch (IOException ex) {
290                 LOGGER.error("Unable to find properties file '{}'", propertiesFile.getPath());
291                 LOGGER.debug("", ex);
292             }
293         }
294         // We have to wait until we've merged the properties before attempting to set whether we use
295         // the proxy for Nexus since it could be disabled in the properties, but not explicitly stated
296         // on the command line
297         final boolean nexusUsesProxy = cli.isNexusUsesProxy();
298         if (dataDirectory != null) {
299             Settings.setString(Settings.KEYS.DATA_DIRECTORY, dataDirectory);
300         } else if (System.getProperty("basedir") != null) {
301             final File dataDir = new File(System.getProperty("basedir"), "data");
302             Settings.setString(Settings.KEYS.DATA_DIRECTORY, dataDir.getAbsolutePath());
303         } else {
304             final File jarPath = new File(App.class.getProtectionDomain().getCodeSource().getLocation().getPath());
305             final File base = jarPath.getParentFile();
306             final String sub = Settings.getString(Settings.KEYS.DATA_DIRECTORY);
307             final File dataDir = new File(base, sub);
308             Settings.setString(Settings.KEYS.DATA_DIRECTORY, dataDir.getAbsolutePath());
309         }
310         Settings.setBoolean(Settings.KEYS.AUTO_UPDATE, autoUpdate);
311         if (proxyServer != null && !proxyServer.isEmpty()) {
312             Settings.setString(Settings.KEYS.PROXY_SERVER, proxyServer);
313         }
314         if (proxyPort != null && !proxyPort.isEmpty()) {
315             Settings.setString(Settings.KEYS.PROXY_PORT, proxyPort);
316         }
317         if (proxyUser != null && !proxyUser.isEmpty()) {
318             Settings.setString(Settings.KEYS.PROXY_USERNAME, proxyUser);
319         }
320         if (proxyPass != null && !proxyPass.isEmpty()) {
321             Settings.setString(Settings.KEYS.PROXY_PASSWORD, proxyPass);
322         }
323         if (connectionTimeout != null && !connectionTimeout.isEmpty()) {
324             Settings.setString(Settings.KEYS.CONNECTION_TIMEOUT, connectionTimeout);
325         }
326         if (suppressionFile != null && !suppressionFile.isEmpty()) {
327             Settings.setString(Settings.KEYS.SUPPRESSION_FILE, suppressionFile);
328         }
329 
330         //File Type Analyzer Settings
331         Settings.setBoolean(Settings.KEYS.ANALYZER_JAR_ENABLED, !cli.isJarDisabled());
332         Settings.setBoolean(Settings.KEYS.ANALYZER_ARCHIVE_ENABLED, !cli.isArchiveDisabled());
333         Settings.setBoolean(Settings.KEYS.ANALYZER_PYTHON_DISTRIBUTION_ENABLED, !cli.isPythonDistributionDisabled());
334         Settings.setBoolean(Settings.KEYS.ANALYZER_PYTHON_PACKAGE_ENABLED, !cli.isPythonPackageDisabled());
335         Settings.setBoolean(Settings.KEYS.ANALYZER_AUTOCONF_ENABLED, !cli.isAutoconfDisabled());
336         Settings.setBoolean(Settings.KEYS.ANALYZER_CMAKE_ENABLED, !cli.isCmakeDisabled());
337         Settings.setBoolean(Settings.KEYS.ANALYZER_NUSPEC_ENABLED, !cli.isNuspecDisabled());
338         Settings.setBoolean(Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED, !cli.isAssemblyDisabled());
339         Settings.setBoolean(Settings.KEYS.ANALYZER_OPENSSL_ENABLED, !cli.isOpenSSLDisabled());
340         Settings.setBoolean(Settings.KEYS.ANALYZER_COMPOSER_LOCK_ENABLED, !cli.isComposerDisabled());
341         Settings.setBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED, !cli.isNodeJsDisabled());
342         Settings.setBoolean(Settings.KEYS.ANALYZER_RUBY_GEMSPEC_ENABLED, !cli.isRubyGemspecDisabled());
343 
344         Settings.setBoolean(Settings.KEYS.ANALYZER_CENTRAL_ENABLED, !cli.isCentralDisabled());
345         Settings.setBoolean(Settings.KEYS.ANALYZER_NEXUS_ENABLED, !cli.isNexusDisabled());
346         if (nexusUrl != null && !nexusUrl.isEmpty()) {
347             Settings.setString(Settings.KEYS.ANALYZER_NEXUS_URL, nexusUrl);
348         }
349         Settings.setBoolean(Settings.KEYS.ANALYZER_NEXUS_PROXY, nexusUsesProxy);
350         if (databaseDriverName != null && !databaseDriverName.isEmpty()) {
351             Settings.setString(Settings.KEYS.DB_DRIVER_NAME, databaseDriverName);
352         }
353         if (databaseDriverPath != null && !databaseDriverPath.isEmpty()) {
354             Settings.setString(Settings.KEYS.DB_DRIVER_PATH, databaseDriverPath);
355         }
356         if (connectionString != null && !connectionString.isEmpty()) {
357             Settings.setString(Settings.KEYS.DB_CONNECTION_STRING, connectionString);
358         }
359         if (databaseUser != null && !databaseUser.isEmpty()) {
360             Settings.setString(Settings.KEYS.DB_USER, databaseUser);
361         }
362         if (databasePassword != null && !databasePassword.isEmpty()) {
363             Settings.setString(Settings.KEYS.DB_PASSWORD, databasePassword);
364         }
365         if (additionalZipExtensions != null && !additionalZipExtensions.isEmpty()) {
366             Settings.setString(Settings.KEYS.ADDITIONAL_ZIP_EXTENSIONS, additionalZipExtensions);
367         }
368         if (pathToMono != null && !pathToMono.isEmpty()) {
369             Settings.setString(Settings.KEYS.ANALYZER_ASSEMBLY_MONO_PATH, pathToMono);
370         }
371         if (cveBase12 != null && !cveBase12.isEmpty()) {
372             Settings.setString(Settings.KEYS.CVE_SCHEMA_1_2, cveBase12);
373             Settings.setString(Settings.KEYS.CVE_SCHEMA_2_0, cveBase20);
374             Settings.setString(Settings.KEYS.CVE_MODIFIED_12_URL, cveMod12);
375             Settings.setString(Settings.KEYS.CVE_MODIFIED_20_URL, cveMod20);
376         }
377     }
378 
379     /**
380      * Creates a file appender and adds it to logback.
381      *
382      * @param verboseLog the path to the verbose log file
383      */
384     private void prepareLogger(String verboseLog) {
385         final StaticLoggerBinder loggerBinder = StaticLoggerBinder.getSingleton();
386         final LoggerContext context = (LoggerContext) loggerBinder.getLoggerFactory();
387 
388         final PatternLayoutEncoder encoder = new PatternLayoutEncoder();
389         encoder.setPattern("%d %C:%L%n%-5level - %msg%n");
390         encoder.setContext(context);
391         encoder.start();
392         final FileAppender fa = new FileAppender();
393         fa.setAppend(true);
394         fa.setEncoder(encoder);
395         fa.setContext(context);
396         fa.setFile(verboseLog);
397         final File f = new File(verboseLog);
398         String name = f.getName();
399         final int i = name.lastIndexOf('.');
400         if (i > 1) {
401             name = name.substring(0, i);
402         }
403         fa.setName(name);
404         fa.start();
405         final ch.qos.logback.classic.Logger rootLogger = context.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
406         rootLogger.addAppender(fa);
407     }
408 
409     /**
410      * Takes a path and resolves it to be a canonical &amp; absolute path. The caveats are that this method will take an Ant style
411      * file selector path (../someDir/**\/*.jar) and convert it to an absolute/canonical path (at least to the left of the first *
412      * or ?).
413      *
414      * @param path the path to canonicalize
415      * @return the canonical path
416      */
417     protected String ensureCanonicalPath(String path) {
418         String basePath = null;
419         String wildCards = null;
420         final String file = path.replace('\\', '/');
421         if (file.contains("*") || file.contains("?")) {
422 
423             int pos = getLastFileSeparator(file);
424             if (pos < 0) {
425                 return file;
426             }
427             pos += 1;
428             basePath = file.substring(0, pos);
429             wildCards = file.substring(pos);
430         } else {
431             basePath = file;
432         }
433 
434         File f = new File(basePath);
435         try {
436             f = f.getCanonicalFile();
437             if (wildCards != null) {
438                 f = new File(f, wildCards);
439             }
440         } catch (IOException ex) {
441             LOGGER.warn("Invalid path '{}' was provided.", path);
442             LOGGER.debug("Invalid path provided", ex);
443         }
444         return f.getAbsolutePath().replace('\\', '/');
445     }
446 
447     /**
448      * Returns the position of the last file separator.
449      *
450      * @param file a file path
451      * @return the position of the last file separator
452      */
453     private int getLastFileSeparator(String file) {
454         if (file.contains("*") || file.contains("?")) {
455             int p1 = file.indexOf('*');
456             int p2 = file.indexOf('?');
457             p1 = p1 > 0 ? p1 : file.length();
458             p2 = p2 > 0 ? p2 : file.length();
459             int pos = p1 < p2 ? p1 : p2;
460             pos = file.lastIndexOf('/', pos);
461             return pos;
462         } else {
463             return file.lastIndexOf('/');
464         }
465     }
466 }