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