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