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