View Javadoc
1   /*
2    * This file is part of dependency-check-core.
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 org.owasp.dependencycheck.analyzer.AnalysisPhase;
21  import org.owasp.dependencycheck.analyzer.Analyzer;
22  import org.owasp.dependencycheck.analyzer.AnalyzerService;
23  import org.owasp.dependencycheck.analyzer.FileTypeAnalyzer;
24  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
25  import org.owasp.dependencycheck.data.nvdcve.ConnectionFactory;
26  import org.owasp.dependencycheck.data.nvdcve.CveDB;
27  import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
28  import org.owasp.dependencycheck.data.update.CachedWebDataSource;
29  import org.owasp.dependencycheck.data.update.UpdateService;
30  import org.owasp.dependencycheck.data.update.exception.UpdateException;
31  import org.owasp.dependencycheck.dependency.Dependency;
32  import org.owasp.dependencycheck.exception.NoDataException;
33  import org.owasp.dependencycheck.exception.ExceptionCollection;
34  import org.owasp.dependencycheck.exception.InitializationException;
35  import org.owasp.dependencycheck.utils.InvalidSettingException;
36  import org.owasp.dependencycheck.utils.Settings;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import java.io.File;
41  import java.io.FileFilter;
42  import java.util.ArrayList;
43  import java.util.Collection;
44  import java.util.EnumMap;
45  import java.util.HashSet;
46  import java.util.Iterator;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.Set;
50  
51  /**
52   * Scans files, directories, etc. for Dependencies. Analyzers are loaded and
53   * used to process the files found by the scan, if a file is encountered and an
54   * Analyzer is associated with the file type then the file is turned into a
55   * dependency.
56   *
57   * @author Jeremy Long
58   */
59  public class Engine implements FileFilter {
60  
61      /**
62       * The list of dependencies.
63       */
64      private List<Dependency> dependencies = new ArrayList<Dependency>();
65      /**
66       * A Map of analyzers grouped by Analysis phase.
67       */
68      private final Map<AnalysisPhase, List<Analyzer>> analyzers = new EnumMap<AnalysisPhase, List<Analyzer>>(AnalysisPhase.class);
69  
70      /**
71       * A Map of analyzers grouped by Analysis phase.
72       */
73      private final Set<FileTypeAnalyzer> fileTypeAnalyzers = new HashSet<FileTypeAnalyzer>();
74  
75      /**
76       * The ClassLoader to use when dynamically loading Analyzer and Update
77       * services.
78       */
79      private ClassLoader serviceClassLoader = Thread.currentThread().getContextClassLoader();
80      /**
81       * The Logger for use throughout the class.
82       */
83      private static final Logger LOGGER = LoggerFactory.getLogger(Engine.class);
84  
85      /**
86       * Creates a new Engine.
87       *
88       * @throws DatabaseException thrown if there is an error connecting to the
89       * database
90       */
91      public Engine() throws DatabaseException {
92          initializeEngine();
93      }
94  
95      /**
96       * Creates a new Engine.
97       *
98       * @param serviceClassLoader a reference the class loader being used
99       * @throws DatabaseException thrown if there is an error connecting to the
100      * database
101      */
102     public Engine(ClassLoader serviceClassLoader) throws DatabaseException {
103         this.serviceClassLoader = serviceClassLoader;
104         initializeEngine();
105     }
106 
107     /**
108      * Creates a new Engine using the specified classloader to dynamically load
109      * Analyzer and Update services.
110      *
111      * @throws DatabaseException thrown if there is an error connecting to the
112      * database
113      */
114     protected final void initializeEngine() throws DatabaseException {
115         ConnectionFactory.initialize();
116         loadAnalyzers();
117     }
118 
119     /**
120      * Properly cleans up resources allocated during analysis.
121      */
122     public void cleanup() {
123         ConnectionFactory.cleanup();
124     }
125 
126     /**
127      * Loads the analyzers specified in the configuration file (or system
128      * properties).
129      */
130     private void loadAnalyzers() {
131         if (!analyzers.isEmpty()) {
132             return;
133         }
134         for (AnalysisPhase phase : AnalysisPhase.values()) {
135             analyzers.put(phase, new ArrayList<Analyzer>());
136         }
137 
138         final AnalyzerService service = new AnalyzerService(serviceClassLoader);
139         final List<Analyzer> iterator = service.getAnalyzers();
140         for (Analyzer a : iterator) {
141             analyzers.get(a.getAnalysisPhase()).add(a);
142             if (a instanceof FileTypeAnalyzer) {
143                 this.fileTypeAnalyzers.add((FileTypeAnalyzer) a);
144             }
145         }
146     }
147 
148     /**
149      * Get the List of the analyzers for a specific phase of analysis.
150      *
151      * @param phase the phase to get the configured analyzers.
152      * @return the analyzers loaded
153      */
154     public List<Analyzer> getAnalyzers(AnalysisPhase phase) {
155         return analyzers.get(phase);
156     }
157 
158     /**
159      * Get the dependencies identified.
160      *
161      * @return the dependencies identified
162      */
163     public List<Dependency> getDependencies() {
164         return dependencies;
165     }
166 
167     /**
168      * Sets the dependencies.
169      *
170      * @param dependencies the dependencies
171      */
172     public void setDependencies(List<Dependency> dependencies) {
173         this.dependencies = dependencies;
174     }
175 
176     /**
177      * Scans an array of files or directories. If a directory is specified, it
178      * will be scanned recursively. Any dependencies identified are added to the
179      * dependency collection.
180      *
181      * @param paths an array of paths to files or directories to be analyzed
182      * @return the list of dependencies scanned
183      * @since v0.3.2.5
184      */
185     public List<Dependency> scan(String[] paths) {
186         final List<Dependency> deps = new ArrayList<Dependency>();
187         for (String path : paths) {
188             final List<Dependency> d = scan(path);
189             if (d != null) {
190                 deps.addAll(d);
191             }
192         }
193         return deps;
194     }
195 
196     /**
197      * Scans a given file or directory. If a directory is specified, it will be
198      * scanned recursively. Any dependencies identified are added to the
199      * dependency collection.
200      *
201      * @param path the path to a file or directory to be analyzed
202      * @return the list of dependencies scanned
203      */
204     public List<Dependency> scan(String path) {
205         final File file = new File(path);
206         return scan(file);
207     }
208 
209     /**
210      * Scans an array of files or directories. If a directory is specified, it
211      * will be scanned recursively. Any dependencies identified are added to the
212      * dependency collection.
213      *
214      * @param files an array of paths to files or directories to be analyzed.
215      * @return the list of dependencies
216      * @since v0.3.2.5
217      */
218     public List<Dependency> scan(File[] files) {
219         final List<Dependency> deps = new ArrayList<Dependency>();
220         for (File file : files) {
221             final List<Dependency> d = scan(file);
222             if (d != null) {
223                 deps.addAll(d);
224             }
225         }
226         return deps;
227     }
228 
229     /**
230      * Scans a collection of files or directories. If a directory is specified,
231      * it will be scanned recursively. Any dependencies identified are added to
232      * the dependency collection.
233      *
234      * @param files a set of paths to files or directories to be analyzed
235      * @return the list of dependencies scanned
236      * @since v0.3.2.5
237      */
238     public List<Dependency> scan(Collection<File> files) {
239         final List<Dependency> deps = new ArrayList<Dependency>();
240         for (File file : files) {
241             final List<Dependency> d = scan(file);
242             if (d != null) {
243                 deps.addAll(d);
244             }
245         }
246         return deps;
247     }
248 
249     /**
250      * Scans a given file or directory. If a directory is specified, it will be
251      * scanned recursively. Any dependencies identified are added to the
252      * dependency collection.
253      *
254      * @param file the path to a file or directory to be analyzed
255      * @return the list of dependencies scanned
256      * @since v0.3.2.4
257      */
258     public List<Dependency> scan(File file) {
259         if (file.exists()) {
260             if (file.isDirectory()) {
261                 return scanDirectory(file);
262             } else {
263                 final Dependency d = scanFile(file);
264                 if (d != null) {
265                     final List<Dependency> deps = new ArrayList<Dependency>();
266                     deps.add(d);
267                     return deps;
268                 }
269             }
270         }
271         return null;
272     }
273 
274     /**
275      * Recursively scans files and directories. Any dependencies identified are
276      * added to the dependency collection.
277      *
278      * @param dir the directory to scan
279      * @return the list of Dependency objects scanned
280      */
281     protected List<Dependency> scanDirectory(File dir) {
282         final File[] files = dir.listFiles();
283         final List<Dependency> deps = new ArrayList<Dependency>();
284         if (files != null) {
285             for (File f : files) {
286                 if (f.isDirectory()) {
287                     final List<Dependency> d = scanDirectory(f);
288                     if (d != null) {
289                         deps.addAll(d);
290                     }
291                 } else {
292                     final Dependency d = scanFile(f);
293                     deps.add(d);
294                 }
295             }
296         }
297         return deps;
298     }
299 
300     /**
301      * Scans a specified file. If a dependency is identified it is added to the
302      * dependency collection.
303      *
304      * @param file The file to scan
305      * @return the scanned dependency
306      */
307     protected Dependency scanFile(File file) {
308         Dependency dependency = null;
309         if (file.isFile()) {
310             if (accept(file)) {
311                 dependency = new Dependency(file);
312                 dependencies.add(dependency);
313             }
314         } else {
315             LOGGER.debug("Path passed to scanFile(File) is not a file: {}. Skipping the file.", file);
316         }
317         return dependency;
318     }
319 
320     /**
321      * Runs the analyzers against all of the dependencies. Since the mutable
322      * dependencies list is exposed via {@link #getDependencies()}, this method
323      * iterates over a copy of the dependencies list. Thus, the potential for
324      * {@link java.util.ConcurrentModificationException}s is avoided, and
325      * analyzers may safely add or remove entries from the dependencies list.
326      *
327      * Every effort is made to complete analysis on the dependencies. In some
328      * cases an exception will occur with part of the analysis being performed
329      * which may not affect the entire analysis. If an exception occurs it will
330      * be included in the thrown exception collection.
331      *
332      * @throws ExceptionCollection a collections of any exceptions that occurred
333      * during analysis
334      */
335     public void analyzeDependencies() throws ExceptionCollection {
336         final List<Throwable> exceptions = new ArrayList<Throwable>();
337         boolean autoUpdate = true;
338         try {
339             autoUpdate = Settings.getBoolean(Settings.KEYS.AUTO_UPDATE);
340         } catch (InvalidSettingException ex) {
341             LOGGER.debug("Invalid setting for auto-update; using true.");
342             exceptions.add(ex);
343         }
344         if (autoUpdate) {
345             try {
346                 doUpdates();
347             } catch (UpdateException ex) {
348                 exceptions.add(ex);
349                 LOGGER.warn("Unable to update Cached Web DataSource, using local "
350                         + "data instead. Results may not include recent vulnerabilities.");
351                 LOGGER.debug("Update Error", ex);
352             }
353         }
354 
355         //need to ensure that data exists
356         try {
357             ensureDataExists();
358         } catch (NoDataException ex) {
359             LOGGER.error("{}\n\nUnable to continue dependency-check analysis.", ex.getMessage());
360             LOGGER.debug("", ex);
361             exceptions.add(ex);
362             throw new ExceptionCollection("Unable to continue dependency-check analysis.", exceptions, true);
363         } catch (DatabaseException ex) {
364             LOGGER.error("{}\n\nUnable to continue dependency-check analysis.", ex.getMessage());
365             LOGGER.debug("", ex);
366             exceptions.add(ex);
367             throw new ExceptionCollection("Unable to connect to the dependency-check database", exceptions, true);
368         }
369 
370         LOGGER.debug("\n----------------------------------------------------\nBEGIN ANALYSIS\n----------------------------------------------------");
371         LOGGER.info("Analysis Started");
372         final long analysisStart = System.currentTimeMillis();
373 
374         // analysis phases
375         for (AnalysisPhase phase : AnalysisPhase.values()) {
376             final List<Analyzer> analyzerList = analyzers.get(phase);
377 
378             for (Analyzer a : analyzerList) {
379                 try {
380                     a = initializeAnalyzer(a);
381                 } catch (InitializationException ex) {
382                     exceptions.add(ex);
383                     continue;
384                 }
385 
386                 /* need to create a copy of the collection because some of the
387                  * analyzers may modify it. This prevents ConcurrentModificationExceptions.
388                  * This is okay for adds/deletes because it happens per analyzer.
389                  */
390                 LOGGER.debug("Begin Analyzer '{}'", a.getName());
391                 final Set<Dependency> dependencySet = new HashSet<Dependency>(dependencies);
392                 for (Dependency d : dependencySet) {
393                     boolean shouldAnalyze = true;
394                     if (a instanceof FileTypeAnalyzer) {
395                         final FileTypeAnalyzer fAnalyzer = (FileTypeAnalyzer) a;
396                         shouldAnalyze = fAnalyzer.accept(d.getActualFile());
397                     }
398                     if (shouldAnalyze) {
399                         LOGGER.debug("Begin Analysis of '{}'", d.getActualFilePath());
400                         try {
401                             a.analyze(d, this);
402                         } catch (AnalysisException ex) {
403                             LOGGER.warn("An error occurred while analyzing '{}'.", d.getActualFilePath());
404                             LOGGER.debug("", ex);
405                             exceptions.add(ex);
406                         } catch (Throwable ex) {
407                             //final AnalysisException ax = new AnalysisException(axMsg, ex);
408                             LOGGER.warn("An unexpected error occurred during analysis of '{}'", d.getActualFilePath());
409                             LOGGER.debug("", ex);
410                             exceptions.add(ex);
411                         }
412                     }
413                 }
414             }
415         }
416         for (AnalysisPhase phase : AnalysisPhase.values()) {
417             final List<Analyzer> analyzerList = analyzers.get(phase);
418 
419             for (Analyzer a : analyzerList) {
420                 closeAnalyzer(a);
421             }
422         }
423 
424         LOGGER.debug("\n----------------------------------------------------\nEND ANALYSIS\n----------------------------------------------------");
425         LOGGER.info("Analysis Complete ({} ms)", System.currentTimeMillis() - analysisStart);
426         if (exceptions.size() > 0) {
427             throw new ExceptionCollection("One or more exceptions occured during dependency-check analysis", exceptions);
428         }
429     }
430 
431     /**
432      * Initializes the given analyzer.
433      *
434      * @param analyzer the analyzer to initialize
435      * @return the initialized analyzer
436      * @throws InitializationException thrown when there is a problem
437      * initializing the analyzer
438      */
439     protected Analyzer initializeAnalyzer(Analyzer analyzer) throws InitializationException {
440         try {
441             LOGGER.debug("Initializing {}", analyzer.getName());
442             analyzer.initialize();
443         } catch (InitializationException ex) {
444             LOGGER.error("Exception occurred initializing {}.", analyzer.getName());
445             LOGGER.debug("", ex);
446             try {
447                 analyzer.close();
448             } catch (Throwable ex1) {
449                 LOGGER.trace("", ex1);
450             }
451             throw ex;
452         } catch (Throwable ex) {
453             LOGGER.error("Unexpected exception occurred initializing {}.", analyzer.getName());
454             LOGGER.debug("", ex);
455             try {
456                 analyzer.close();
457             } catch (Throwable ex1) {
458                 LOGGER.trace("", ex1);
459             }
460             throw new InitializationException("Unexpected Exception", ex);
461         }
462         return analyzer;
463     }
464 
465     /**
466      * Closes the given analyzer.
467      *
468      * @param analyzer the analyzer to close
469      */
470     protected void closeAnalyzer(Analyzer analyzer) {
471         LOGGER.debug("Closing Analyzer '{}'", analyzer.getName());
472         try {
473             analyzer.close();
474         } catch (Throwable ex) {
475             LOGGER.trace("", ex);
476         }
477     }
478 
479     /**
480      * Cycles through the cached web data sources and calls update on all of
481      * them.
482      *
483      * @throws UpdateException thrown if the operation fails
484      */
485     public void doUpdates() throws UpdateException {
486         LOGGER.info("Checking for updates");
487         final long updateStart = System.currentTimeMillis();
488         final UpdateService service = new UpdateService(serviceClassLoader);
489         final Iterator<CachedWebDataSource> iterator = service.getDataSources();
490         while (iterator.hasNext()) {
491             final CachedWebDataSource source = iterator.next();
492             source.update();
493         }
494         LOGGER.info("Check for updates complete ({} ms)", System.currentTimeMillis() - updateStart);
495     }
496 
497     /**
498      * Returns a full list of all of the analyzers. This is useful for reporting
499      * which analyzers where used.
500      *
501      * @return a list of Analyzers
502      */
503     public List<Analyzer> getAnalyzers() {
504         final List<Analyzer> ret = new ArrayList<Analyzer>();
505         for (AnalysisPhase phase : AnalysisPhase.values()) {
506             final List<Analyzer> analyzerList = analyzers.get(phase);
507             ret.addAll(analyzerList);
508         }
509         return ret;
510     }
511 
512     /**
513      * Checks all analyzers to see if an extension is supported.
514      *
515      * @param file a file extension
516      * @return true or false depending on whether or not the file extension is
517      * supported
518      */
519     @Override
520     public boolean accept(File file) {
521         if (file == null) {
522             return false;
523         }
524         boolean scan = false;
525         for (FileTypeAnalyzer a : this.fileTypeAnalyzers) {
526             /* note, we can't break early on this loop as the analyzers need to know if
527              they have files to work on prior to initialization */
528             scan |= a.accept(file);
529         }
530         return scan;
531     }
532 
533     /**
534      * Returns the set of file type analyzers.
535      *
536      * @return the set of file type analyzers
537      */
538     public Set<FileTypeAnalyzer> getFileTypeAnalyzers() {
539         return this.fileTypeAnalyzers;
540     }
541 
542     /**
543      * Checks the CPE Index to ensure documents exists. If none exist a
544      * NoDataException is thrown.
545      *
546      * @throws NoDataException thrown if no data exists in the CPE Index
547      * @throws DatabaseException thrown if there is an exception opening the
548      * database
549      */
550     private void ensureDataExists() throws NoDataException, DatabaseException {
551         final CveDB cve = new CveDB();
552         try {
553             cve.open();
554             if (!cve.dataExists()) {
555                 throw new NoDataException("No documents exist");
556             }
557         } catch (DatabaseException ex) {
558             throw new NoDataException(ex.getMessage(), ex);
559         } finally {
560             cve.close();
561         }
562     }
563 }