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         final Integer cveValidForHours = cli.getCveValidForHours();
283         final boolean experimentalEnabled = cli.isExperimentalEnabled();
284 
285         if (propertiesFile != null) {
286             try {
287                 Settings.mergeProperties(propertiesFile);
288             } catch (FileNotFoundException ex) {
289                 LOGGER.error("Unable to load properties file '{}'", propertiesFile.getPath());
290                 LOGGER.debug("", ex);
291             } catch (IOException ex) {
292                 LOGGER.error("Unable to find properties file '{}'", propertiesFile.getPath());
293                 LOGGER.debug("", ex);
294             }
295         }
296         // We have to wait until we've merged the properties before attempting to set whether we use
297         // the proxy for Nexus since it could be disabled in the properties, but not explicitly stated
298         // on the command line
299         final boolean nexusUsesProxy = cli.isNexusUsesProxy();
300         if (dataDirectory != null) {
301             Settings.setString(Settings.KEYS.DATA_DIRECTORY, dataDirectory);
302         } else if (System.getProperty("basedir") != null) {
303             final File dataDir = new File(System.getProperty("basedir"), "data");
304             Settings.setString(Settings.KEYS.DATA_DIRECTORY, dataDir.getAbsolutePath());
305         } else {
306             final File jarPath = new File(App.class.getProtectionDomain().getCodeSource().getLocation().getPath());
307             final File base = jarPath.getParentFile();
308             final String sub = Settings.getString(Settings.KEYS.DATA_DIRECTORY);
309             final File dataDir = new File(base, sub);
310             Settings.setString(Settings.KEYS.DATA_DIRECTORY, dataDir.getAbsolutePath());
311         }
312         Settings.setBoolean(Settings.KEYS.AUTO_UPDATE, autoUpdate);
313         Settings.setStringIfNotEmpty(Settings.KEYS.PROXY_SERVER, proxyServer);
314         Settings.setStringIfNotEmpty(Settings.KEYS.PROXY_PORT, proxyPort);
315         Settings.setStringIfNotEmpty(Settings.KEYS.PROXY_USERNAME, proxyUser);
316         Settings.setStringIfNotEmpty(Settings.KEYS.PROXY_PASSWORD, proxyPass);
317         Settings.setStringIfNotEmpty(Settings.KEYS.CONNECTION_TIMEOUT, connectionTimeout);
318         Settings.setStringIfNotEmpty(Settings.KEYS.SUPPRESSION_FILE, suppressionFile);
319         Settings.setIntIfNotNull(Settings.KEYS.CVE_CHECK_VALID_FOR_HOURS, cveValidForHours);
320 
321         //File Type Analyzer Settings
322         Settings.setBoolean(Settings.KEYS.ANALYZER_EXPERIMENTAL_ENABLED, experimentalEnabled);
323         Settings.setBoolean(Settings.KEYS.ANALYZER_JAR_ENABLED, !cli.isJarDisabled());
324         Settings.setBoolean(Settings.KEYS.ANALYZER_ARCHIVE_ENABLED, !cli.isArchiveDisabled());
325         Settings.setBoolean(Settings.KEYS.ANALYZER_PYTHON_DISTRIBUTION_ENABLED, !cli.isPythonDistributionDisabled());
326         Settings.setBoolean(Settings.KEYS.ANALYZER_PYTHON_PACKAGE_ENABLED, !cli.isPythonPackageDisabled());
327         Settings.setBoolean(Settings.KEYS.ANALYZER_AUTOCONF_ENABLED, !cli.isAutoconfDisabled());
328         Settings.setBoolean(Settings.KEYS.ANALYZER_CMAKE_ENABLED, !cli.isCmakeDisabled());
329         Settings.setBoolean(Settings.KEYS.ANALYZER_NUSPEC_ENABLED, !cli.isNuspecDisabled());
330         Settings.setBoolean(Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED, !cli.isAssemblyDisabled());
331         Settings.setBoolean(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_ENABLED, !cli.isBundleAuditDisabled());
332         Settings.setBoolean(Settings.KEYS.ANALYZER_OPENSSL_ENABLED, !cli.isOpenSSLDisabled());
333         Settings.setBoolean(Settings.KEYS.ANALYZER_COMPOSER_LOCK_ENABLED, !cli.isComposerDisabled());
334         Settings.setBoolean(Settings.KEYS.ANALYZER_NODE_PACKAGE_ENABLED, !cli.isNodeJsDisabled());
335         Settings.setBoolean(Settings.KEYS.ANALYZER_RUBY_GEMSPEC_ENABLED, !cli.isRubyGemspecDisabled());
336         Settings.setBoolean(Settings.KEYS.ANALYZER_CENTRAL_ENABLED, !cli.isCentralDisabled());
337         Settings.setBoolean(Settings.KEYS.ANALYZER_NEXUS_ENABLED, !cli.isNexusDisabled());
338 
339         Settings.setStringIfNotEmpty(Settings.KEYS.ANALYZER_BUNDLE_AUDIT_PATH, cli.getPathToBundleAudit());
340         Settings.setStringIfNotEmpty(Settings.KEYS.ANALYZER_NEXUS_URL, nexusUrl);
341         Settings.setBoolean(Settings.KEYS.ANALYZER_NEXUS_USES_PROXY, nexusUsesProxy);
342         Settings.setStringIfNotEmpty(Settings.KEYS.DB_DRIVER_NAME, databaseDriverName);
343         Settings.setStringIfNotEmpty(Settings.KEYS.DB_DRIVER_PATH, databaseDriverPath);
344         Settings.setStringIfNotEmpty(Settings.KEYS.DB_CONNECTION_STRING, connectionString);
345         Settings.setStringIfNotEmpty(Settings.KEYS.DB_USER, databaseUser);
346         Settings.setStringIfNotEmpty(Settings.KEYS.DB_PASSWORD, databasePassword);
347         Settings.setStringIfNotEmpty(Settings.KEYS.ADDITIONAL_ZIP_EXTENSIONS, additionalZipExtensions);
348         Settings.setStringIfNotEmpty(Settings.KEYS.ANALYZER_ASSEMBLY_MONO_PATH, pathToMono);
349         if (cveBase12 != null && !cveBase12.isEmpty()) {
350             Settings.setString(Settings.KEYS.CVE_SCHEMA_1_2, cveBase12);
351             Settings.setString(Settings.KEYS.CVE_SCHEMA_2_0, cveBase20);
352             Settings.setString(Settings.KEYS.CVE_MODIFIED_12_URL, cveMod12);
353             Settings.setString(Settings.KEYS.CVE_MODIFIED_20_URL, cveMod20);
354         }
355     }
356 
357     /**
358      * Creates a file appender and adds it to logback.
359      *
360      * @param verboseLog the path to the verbose log file
361      */
362     private void prepareLogger(String verboseLog) {
363         final StaticLoggerBinder loggerBinder = StaticLoggerBinder.getSingleton();
364         final LoggerContext context = (LoggerContext) loggerBinder.getLoggerFactory();
365 
366         final PatternLayoutEncoder encoder = new PatternLayoutEncoder();
367         encoder.setPattern("%d %C:%L%n%-5level - %msg%n");
368         encoder.setContext(context);
369         encoder.start();
370         final FileAppender fa = new FileAppender();
371         fa.setAppend(true);
372         fa.setEncoder(encoder);
373         fa.setContext(context);
374         fa.setFile(verboseLog);
375         final File f = new File(verboseLog);
376         String name = f.getName();
377         final int i = name.lastIndexOf('.');
378         if (i > 1) {
379             name = name.substring(0, i);
380         }
381         fa.setName(name);
382         fa.start();
383         final ch.qos.logback.classic.Logger rootLogger = context.getLogger(ch.qos.logback.classic.Logger.ROOT_LOGGER_NAME);
384         rootLogger.addAppender(fa);
385     }
386 
387     /**
388      * Takes a path and resolves it to be a canonical &amp; absolute path. The caveats are that this method will take an Ant style
389      * file selector path (../someDir/**\/*.jar) and convert it to an absolute/canonical path (at least to the left of the first *
390      * or ?).
391      *
392      * @param path the path to canonicalize
393      * @return the canonical path
394      */
395     protected String ensureCanonicalPath(String path) {
396         String basePath = null;
397         String wildCards = null;
398         final String file = path.replace('\\', '/');
399         if (file.contains("*") || file.contains("?")) {
400 
401             int pos = getLastFileSeparator(file);
402             if (pos < 0) {
403                 return file;
404             }
405             pos += 1;
406             basePath = file.substring(0, pos);
407             wildCards = file.substring(pos);
408         } else {
409             basePath = file;
410         }
411 
412         File f = new File(basePath);
413         try {
414             f = f.getCanonicalFile();
415             if (wildCards != null) {
416                 f = new File(f, wildCards);
417             }
418         } catch (IOException ex) {
419             LOGGER.warn("Invalid path '{}' was provided.", path);
420             LOGGER.debug("Invalid path provided", ex);
421         }
422         return f.getAbsolutePath().replace('\\', '/');
423     }
424 
425     /**
426      * Returns the position of the last file separator.
427      *
428      * @param file a file path
429      * @return the position of the last file separator
430      */
431     private int getLastFileSeparator(String file) {
432         if (file.contains("*") || file.contains("?")) {
433             int p1 = file.indexOf('*');
434             int p2 = file.indexOf('?');
435             p1 = p1 > 0 ? p1 : file.length();
436             p2 = p2 > 0 ? p2 : file.length();
437             int pos = p1 < p2 ? p1 : p2;
438             pos = file.lastIndexOf('/', pos);
439             return pos;
440         } else {
441             return file.lastIndexOf('/');
442         }
443     }
444 }