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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
21  import org.apache.commons.jcs3.JCS;
22  import org.jspecify.annotations.NonNull;
23  import org.jspecify.annotations.Nullable;
24  import org.owasp.dependencycheck.analyzer.AnalysisPhase;
25  import org.owasp.dependencycheck.analyzer.Analyzer;
26  import org.owasp.dependencycheck.analyzer.AnalyzerService;
27  import org.owasp.dependencycheck.analyzer.DependencyBundlingAnalyzer;
28  import org.owasp.dependencycheck.analyzer.FileTypeAnalyzer;
29  import org.owasp.dependencycheck.data.nvdcve.CveDB;
30  import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
31  import org.owasp.dependencycheck.data.nvdcve.DatabaseManager;
32  import org.owasp.dependencycheck.data.nvdcve.DatabaseProperties;
33  import org.owasp.dependencycheck.data.update.CachedWebDataSource;
34  import org.owasp.dependencycheck.data.update.UpdateService;
35  import org.owasp.dependencycheck.data.update.exception.UpdateException;
36  import org.owasp.dependencycheck.dependency.Dependency;
37  import org.owasp.dependencycheck.dependency.naming.Identifier;
38  import org.owasp.dependencycheck.exception.ExceptionCollection;
39  import org.owasp.dependencycheck.exception.InitializationException;
40  import org.owasp.dependencycheck.exception.NoDataException;
41  import org.owasp.dependencycheck.exception.ReportException;
42  import org.owasp.dependencycheck.exception.WriteLockException;
43  import org.owasp.dependencycheck.reporting.ReportGenerator;
44  import org.owasp.dependencycheck.utils.FileUtils;
45  import org.owasp.dependencycheck.utils.Settings;
46  import org.owasp.dependencycheck.utils.WriteLock;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  import javax.annotation.concurrent.NotThreadSafe;
51  import java.io.File;
52  import java.io.FileFilter;
53  import java.io.IOException;
54  import java.nio.file.Files;
55  import java.util.ArrayList;
56  import java.util.Arrays;
57  import java.util.Collection;
58  import java.util.Collections;
59  import java.util.EnumMap;
60  import java.util.HashMap;
61  import java.util.HashSet;
62  import java.util.Iterator;
63  import java.util.List;
64  import java.util.Map;
65  import java.util.Objects;
66  import java.util.Set;
67  import java.util.concurrent.CancellationException;
68  import java.util.concurrent.ExecutionException;
69  import java.util.concurrent.ExecutorService;
70  import java.util.concurrent.Executors;
71  import java.util.concurrent.Future;
72  import java.util.concurrent.TimeUnit;
73  
74  import static org.owasp.dependencycheck.analyzer.AnalysisPhase.*;
75  
76  /**
77   * Scans files, directories, etc. for Dependencies. Analyzers are loaded and
78   * used to process the files found by the scan, if a file is encountered and an
79   * Analyzer is associated with the file type then the file is turned into a
80   * dependency.
81   *
82   * @author Jeremy Long
83   */
84  @NotThreadSafe
85  public class Engine implements FileFilter, AutoCloseable {
86  
87      /**
88       * The Logger for use throughout the class.
89       */
90      private static final Logger LOGGER = LoggerFactory.getLogger(Engine.class);
91      /**
92       * The list of dependencies.
93       */
94      private final List<Dependency> dependencies = Collections.synchronizedList(new ArrayList<>());
95      /**
96       * A Map of analyzers grouped by Analysis phase.
97       */
98      private final Map<AnalysisPhase, List<Analyzer>> analyzers = new EnumMap<>(AnalysisPhase.class);
99      /**
100      * A Map of analyzers grouped by Analysis phase.
101      */
102     private final Set<FileTypeAnalyzer> fileTypeAnalyzers = new HashSet<>();
103     /**
104      * The engine execution mode indicating it will either collect evidence or
105      * process evidence or both.
106      */
107     private final Mode mode;
108     /**
109      * The ClassLoader to use when dynamically loading Analyzer and Update
110      * services.
111      */
112     private final ClassLoader serviceClassLoader;
113     /**
114      * The configured settings.
115      */
116     private final Settings settings;
117     /**
118      * A storage location to persist objects throughout the execution of ODC.
119      */
120     private final Map<String, Object> objects = new HashMap<>();
121     /**
122      * The external view of the dependency list.
123      */
124     private Dependency[] dependenciesExternalView = null;
125     /**
126      * A reference to the database.
127      */
128     private CveDB database = null;
129     /**
130      * Creates a new {@link Mode#STANDALONE} Engine.
131      *
132      * @param settings reference to the configured settings
133      */
134     public Engine(@NonNull final Settings settings) {
135         this(Mode.STANDALONE, settings);
136     }
137 
138     /**
139      * Creates a new Engine.
140      *
141      * @param mode the mode of operation
142      * @param settings reference to the configured settings
143      */
144     public Engine(@NonNull final Mode mode, @NonNull final Settings settings) {
145         this(Thread.currentThread().getContextClassLoader(), mode, settings);
146     }
147 
148     /**
149      * Creates a new {@link Mode#STANDALONE} Engine.
150      *
151      * @param serviceClassLoader a reference the class loader being used
152      * @param settings reference to the configured settings
153      */
154     public Engine(@NonNull final ClassLoader serviceClassLoader, @NonNull final Settings settings) {
155         this(serviceClassLoader, Mode.STANDALONE, settings);
156     }
157 
158     /**
159      * Creates a new Engine.
160      *
161      * @param serviceClassLoader a reference the class loader being used
162      * @param mode the mode of the engine
163      * @param settings reference to the configured settings
164      */
165     public Engine(@NonNull final ClassLoader serviceClassLoader, @NonNull final Mode mode, @NonNull final Settings settings) {
166         this.settings = settings;
167         this.serviceClassLoader = serviceClassLoader;
168         this.mode = mode;
169         initializeEngine();
170     }
171 
172     /**
173      * Creates a new Engine using the specified classloader to dynamically load
174      * Analyzer and Update services.
175      *
176      * @throws DatabaseException thrown if there is an error connecting to the
177      * database
178      */
179     protected final void initializeEngine() {
180         loadAnalyzers();
181     }
182 
183     /**
184      * Properly cleans up resources allocated during analysis.
185      */
186     @Override
187     public void close() {
188         if (mode.isDatabaseRequired()) {
189             if (database != null) {
190                 database.close();
191                 database = null;
192             }
193         }
194         JCS.shutdown();
195     }
196 
197     /**
198      * Loads the analyzers specified in the configuration file (or system
199      * properties).
200      */
201     private void loadAnalyzers() {
202         if (!analyzers.isEmpty()) {
203             return;
204         }
205         mode.getPhases().forEach((phase) -> analyzers.put(phase, new ArrayList<>()));
206         final AnalyzerService service = new AnalyzerService(serviceClassLoader, settings);
207         final List<Analyzer> iterator = service.getAnalyzers(mode.getPhases());
208         iterator.forEach((a) -> {
209             a.initialize(this.settings);
210             analyzers.get(a.getAnalysisPhase()).add(a);
211             if (a instanceof FileTypeAnalyzer) {
212                 this.fileTypeAnalyzers.add((FileTypeAnalyzer) a);
213             }
214         });
215     }
216 
217     /**
218      * Get the List of the analyzers for a specific phase of analysis.
219      *
220      * @param phase the phase to get the configured analyzers.
221      * @return the analyzers loaded
222      */
223     public List<Analyzer> getAnalyzers(AnalysisPhase phase) {
224         return analyzers.get(phase);
225     }
226 
227     /**
228      * Adds a dependency. In some cases, when adding a virtual dependency, the
229      * method will identify if the virtual dependency was previously added and
230      * update the existing dependency rather then adding a duplicate.
231      *
232      * @param dependency the dependency to add
233      */
234     public synchronized void addDependency(Dependency dependency) {
235         if (dependency.isVirtual()) {
236             for (Dependency existing : dependencies) {
237                 if (existing.isVirtual()
238                         && existing.getSha256sum() != null
239                         && existing.getSha256sum().equals(dependency.getSha256sum())
240                         && existing.getDisplayFileName() != null
241                         && existing.getDisplayFileName().equals(dependency.getDisplayFileName())
242                         && identifiersMatch(existing.getSoftwareIdentifiers(), dependency.getSoftwareIdentifiers())) {
243                     DependencyBundlingAnalyzer.mergeDependencies(existing, dependency, null);
244                     return;
245                 }
246             }
247         }
248         dependencies.add(dependency);
249         dependenciesExternalView = null;
250     }
251 
252     /**
253      * Sorts the dependency list.
254      */
255     public synchronized void sortDependencies() {
256         //TODO - is this actually necassary????
257 //        Collections.sort(dependencies);
258 //        dependenciesExternalView = null;
259     }
260 
261     /**
262      * Removes the dependency.
263      *
264      * @param dependency the dependency to remove.
265      */
266     public synchronized void removeDependency(@NonNull final Dependency dependency) {
267         dependencies.remove(dependency);
268         dependenciesExternalView = null;
269     }
270 
271     /**
272      * Returns a copy of the dependencies as an array.
273      *
274      * @return the dependencies identified
275      */
276     @SuppressFBWarnings(justification = "This is the intended external view of the dependencies", value = {"EI_EXPOSE_REP"})
277     public synchronized Dependency[] getDependencies() {
278         if (dependenciesExternalView == null) {
279             dependenciesExternalView = dependencies.toArray(new Dependency[0]);
280         }
281         return dependenciesExternalView;
282     }
283 
284     /**
285      * Sets the dependencies.
286      *
287      * @param dependencies the dependencies
288      */
289     public synchronized void setDependencies(@NonNull final List<Dependency> dependencies) {
290         this.dependencies.clear();
291         this.dependencies.addAll(dependencies);
292         dependenciesExternalView = null;
293     }
294 
295     /**
296      * Scans an array of files or directories. If a directory is specified, it
297      * will be scanned recursively. Any dependencies identified are added to the
298      * dependency collection.
299      *
300      * @param paths an array of paths to files or directories to be analyzed
301      * @return the list of dependencies scanned
302      * @since v0.3.2.5
303      */
304     public List<Dependency> scan(@NonNull final String[] paths) {
305         return scan(paths, null);
306     }
307 
308     /**
309      * Scans an array of files or directories. If a directory is specified, it
310      * will be scanned recursively. Any dependencies identified are added to the
311      * dependency collection.
312      *
313      * @param paths an array of paths to files or directories to be analyzed
314      * @param projectReference the name of the project or scope in which the
315      * dependency was identified
316      * @return the list of dependencies scanned
317      * @since v1.4.4
318      */
319     public List<Dependency> scan(@NonNull final String[] paths, @Nullable final String projectReference) {
320         final List<Dependency> deps = new ArrayList<>();
321         for (String path : paths) {
322             final List<Dependency> d = scan(path, projectReference);
323             if (d != null) {
324                 deps.addAll(d);
325             }
326         }
327         return deps;
328     }
329 
330     /**
331      * Scans a given file or directory. If a directory is specified, it will be
332      * scanned recursively. Any dependencies identified are added to the
333      * dependency collection.
334      *
335      * @param path the path to a file or directory to be analyzed
336      * @return the list of dependencies scanned
337      */
338     public List<Dependency> scan(@NonNull final String path) {
339         return scan(path, null);
340     }
341 
342     /**
343      * Scans a given file or directory. If a directory is specified, it will be
344      * scanned recursively. Any dependencies identified are added to the
345      * dependency collection.
346      *
347      * @param path the path to a file or directory to be analyzed
348      * @param projectReference the name of the project or scope in which the
349      * dependency was identified
350      * @return the list of dependencies scanned
351      * @since v1.4.4
352      */
353     public List<Dependency> scan(@NonNull final String path, String projectReference) {
354         final File file = new File(path);
355         return scan(file, projectReference);
356     }
357 
358     /**
359      * Scans an array of files or directories. If a directory is specified, it
360      * will be scanned recursively. Any dependencies identified are added to the
361      * dependency collection.
362      *
363      * @param files an array of paths to files or directories to be analyzed.
364      * @return the list of dependencies
365      * @since v0.3.2.5
366      */
367     public List<Dependency> scan(File[] files) {
368         return scan(files, null);
369     }
370 
371     /**
372      * Scans an array of files or directories. If a directory is specified, it
373      * will be scanned recursively. Any dependencies identified are added to the
374      * dependency collection.
375      *
376      * @param files an array of paths to files or directories to be analyzed.
377      * @param projectReference the name of the project or scope in which the
378      * dependency was identified
379      * @return the list of dependencies
380      * @since v1.4.4
381      */
382     public List<Dependency> scan(File[] files, String projectReference) {
383         final List<Dependency> deps = new ArrayList<>();
384         for (File file : files) {
385             final List<Dependency> d = scan(file, projectReference);
386             if (d != null) {
387                 deps.addAll(d);
388             }
389         }
390         return deps;
391     }
392 
393     /**
394      * Scans a collection of files or directories. If a directory is specified,
395      * it will be scanned recursively. Any dependencies identified are added to
396      * the dependency collection.
397      *
398      * @param files a set of paths to files or directories to be analyzed
399      * @return the list of dependencies scanned
400      * @since v0.3.2.5
401      */
402     public List<Dependency> scan(Collection<File> files) {
403         return scan(files, null);
404     }
405 
406     /**
407      * Scans a collection of files or directories. If a directory is specified,
408      * it will be scanned recursively. Any dependencies identified are added to
409      * the dependency collection.
410      *
411      * @param files a set of paths to files or directories to be analyzed
412      * @param projectReference the name of the project or scope in which the
413      * dependency was identified
414      * @return the list of dependencies scanned
415      * @since v1.4.4
416      */
417     public List<Dependency> scan(Collection<File> files, String projectReference) {
418         final List<Dependency> deps = new ArrayList<>();
419         files.stream().map((file) -> scan(file, projectReference))
420                 .filter(Objects::nonNull)
421                 .forEach(deps::addAll);
422         return deps;
423     }
424 
425     /**
426      * Scans a given file or directory. If a directory is specified, it will be
427      * scanned recursively. Any dependencies identified are added to the
428      * dependency collection.
429      *
430      * @param file the path to a file or directory to be analyzed
431      * @return the list of dependencies scanned
432      * @since v0.3.2.4
433      */
434     public List<Dependency> scan(File file) {
435         return scan(file, null);
436     }
437 
438     /**
439      * Scans a given file or directory. If a directory is specified, it will be
440      * scanned recursively. Any dependencies identified are added to the
441      * dependency collection.
442      *
443      * @param file the path to a file or directory to be analyzed
444      * @param projectReference the name of the project or scope in which the
445      * dependency was identified
446      * @return the list of dependencies scanned
447      * @since v1.4.4
448      */
449     @Nullable
450     public List<Dependency> scan(@NonNull final File file, String projectReference) {
451         if (file.exists()) {
452             if (file.isDirectory()) {
453                 return scanDirectory(file, projectReference);
454             } else {
455                 final Dependency d = scanFile(file, projectReference);
456                 if (d != null) {
457                     final List<Dependency> deps = new ArrayList<>();
458                     deps.add(d);
459                     return deps;
460                 }
461             }
462         }
463         return null;
464     }
465 
466     /**
467      * Recursively scans files and directories. Any dependencies identified are
468      * added to the dependency collection.
469      *
470      * @param dir the directory to scan
471      * @return the list of Dependency objects scanned
472      */
473     protected List<Dependency> scanDirectory(File dir) {
474         return scanDirectory(dir, null);
475     }
476 
477     /**
478      * Recursively scans files and directories. Any dependencies identified are
479      * added to the dependency collection.
480      *
481      * @param dir the directory to scan
482      * @param projectReference the name of the project or scope in which the
483      * dependency was identified
484      * @return the list of Dependency objects scanned
485      * @since v1.4.4
486      */
487     protected List<Dependency> scanDirectory(@NonNull final File dir, @Nullable final String projectReference) {
488         final File[] files = dir.listFiles();
489         final List<Dependency> deps = new ArrayList<>();
490         if (files != null) {
491             for (File f : files) {
492                 if (f.isDirectory()) {
493                     final List<Dependency> d = scanDirectory(f, projectReference);
494                     if (d != null) {
495                         deps.addAll(d);
496                     }
497                 } else {
498                     final Dependency d = scanFile(f, projectReference);
499                     if (d != null) {
500                         deps.add(d);
501                     }
502                 }
503             }
504         }
505         return deps;
506     }
507 
508     /**
509      * Scans a specified file. If a dependency is identified it is added to the
510      * dependency collection.
511      *
512      * @param file The file to scan
513      * @return the scanned dependency
514      */
515     protected Dependency scanFile(@NonNull final File file) {
516         return scanFile(file, null);
517     }
518 
519     //CSOFF: NestedIfDepth
520     /**
521      * Scans a specified file. If a dependency is identified it is added to the
522      * dependency collection.
523      *
524      * @param file The file to scan
525      * @param projectReference the name of the project or scope in which the
526      * dependency was identified
527      * @return the scanned dependency
528      * @since v1.4.4
529      */
530     protected synchronized Dependency scanFile(@NonNull final File file, @Nullable final String projectReference) {
531         Dependency dependency = null;
532         if (file.isFile()) {
533             if (accept(file)) {
534                 dependency = new Dependency(file);
535                 if (projectReference != null) {
536                     dependency.addProjectReference(projectReference);
537                 }
538                 final String sha1 = dependency.getSha1sum();
539                 boolean found = false;
540 
541                 if (sha1 != null) {
542                     for (Dependency existing : dependencies) {
543                         if (sha1.equals(existing.getSha1sum())) {
544                             if (existing.getDisplayFileName().contains(": ")
545                                     || dependency.getDisplayFileName().contains(": ")
546                                     || dependency.getActualFilePath().contains("dctemp")) {
547                                 continue;
548                             }
549                             found = true;
550                             if (projectReference != null) {
551                                 existing.addProjectReference(projectReference);
552                             }
553                             if (existing.getActualFilePath() != null && dependency.getActualFilePath() != null
554                                     && !existing.getActualFilePath().equals(dependency.getActualFilePath())) {
555 
556                                 if (DependencyBundlingAnalyzer.firstPathIsShortest(existing.getFilePath(), dependency.getFilePath())) {
557                                     DependencyBundlingAnalyzer.mergeDependencies(existing, dependency, null);
558 
559                                     //return null;
560                                     return existing;
561                                 } else {
562                                     //Merging dependency<-existing could be complicated. Instead analyze them seperately
563                                     //and possibly merge them at the end.
564                                     found = false;
565                                 }
566 
567                             } else { //somehow we scanned the same file twice?
568                                 //return null;
569                                 return existing;
570                             }
571                             break;
572                         }
573                     }
574                 }
575                 if (!found) {
576                     dependencies.add(dependency);
577                     dependenciesExternalView = null;
578                 }
579             }
580         } else {
581             LOGGER.debug("Path passed to scanFile(File) is not a file that can be scanned by dependency-check: {}. Skipping the file.", file);
582         }
583         return dependency;
584     }
585     //CSON: NestedIfDepth
586 
587     /**
588      * Runs the analyzers against all of the dependencies. Since the mutable
589      * dependencies list is exposed via {@link #getDependencies()}, this method
590      * iterates over a copy of the dependencies list. Thus, the potential for
591      * {@link java.util.ConcurrentModificationException}s is avoided, and
592      * analyzers may safely add or remove entries from the dependencies list.
593      * <p>
594      * Every effort is made to complete analysis on the dependencies. In some
595      * cases an exception will occur with part of the analysis being performed
596      * which may not affect the entire analysis. If an exception occurs it will
597      * be included in the thrown exception collection.
598      *
599      * @throws ExceptionCollection a collections of any exceptions that occurred
600      * during analysis
601      */
602     public void analyzeDependencies() throws ExceptionCollection {
603         final List<Throwable> exceptions = Collections.synchronizedList(new ArrayList<>());
604 
605         initializeAndUpdateDatabase(exceptions);
606 
607         //need to ensure that data exists
608         try {
609             ensureDataExists();
610         } catch (NoDataException ex) {
611             throwFatalExceptionCollection("Unable to continue dependency-check analysis.", ex, exceptions);
612         }
613         LOGGER.info("\n\nDependency-Check is an open source tool performing a best effort analysis of 3rd party dependencies; false positives and "
614                 + "false negatives may exist in the analysis performed by the tool. Use of the tool and the reporting provided constitutes "
615                 + "acceptance for use in an AS IS condition, and there are NO warranties, implied or otherwise, with regard to the analysis "
616                 + "or its use. Any use of the tool and the reporting provided is at the user's risk. In no event shall the copyright holder "
617                 + "or OWASP be held liable for any damages whatsoever arising out of or in connection with the use of this tool, the analysis "
618                 + "performed, or the resulting report.\n\n\n"
619                 + "   About ODC: https://dependency-check.github.io/DependencyCheck/general/internals.html\n"
620                 + "   False Positives: https://dependency-check.github.io/DependencyCheck/general/suppression.html\n"
621                 + "\n");
622         LOGGER.debug("\n----------------------------------------------------\nBEGIN ANALYSIS\n----------------------------------------------------");
623         LOGGER.info("Analysis Started");
624         final long analysisStart = System.currentTimeMillis();
625 
626         // analysis phases
627         for (AnalysisPhase phase : mode.getPhases()) {
628             final List<Analyzer> analyzerList = analyzers.get(phase);
629 
630             for (final Analyzer analyzer : analyzerList) {
631                 final long analyzerStart = System.currentTimeMillis();
632                 try {
633                     initializeAnalyzer(analyzer);
634                 } catch (InitializationException ex) {
635                     exceptions.add(ex);
636                     if (ex.isFatal()) {
637                         continue;
638                     }
639                 }
640 
641                 if (analyzer.isEnabled()) {
642                     executeAnalysisTasks(analyzer, exceptions);
643 
644                     final long analyzerDurationMillis = System.currentTimeMillis() - analyzerStart;
645                     final long analyzerDurationSeconds = TimeUnit.MILLISECONDS.toSeconds(analyzerDurationMillis);
646                     LOGGER.info("Finished {} ({} seconds)", analyzer.getName(), analyzerDurationSeconds);
647                 } else {
648                     LOGGER.debug("Skipping {} (not enabled)", analyzer.getName());
649                 }
650             }
651         }
652         mode.getPhases().stream()
653                 .map(analyzers::get)
654                 .forEach((analyzerList) -> analyzerList.forEach(this::closeAnalyzer));
655 
656         LOGGER.debug("\n----------------------------------------------------\nEND ANALYSIS\n----------------------------------------------------");
657         final long analysisDurationSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - analysisStart);
658         LOGGER.info("Analysis Complete ({} seconds)", analysisDurationSeconds);
659         if (exceptions.size() > 0) {
660             throw new ExceptionCollection(exceptions);
661         }
662     }
663 
664     /**
665      * Performs any necessary updates and initializes the database.
666      *
667      * @param exceptions a collection to store non-fatal exceptions
668      * @throws ExceptionCollection thrown if fatal exceptions occur
669      */
670     private void initializeAndUpdateDatabase(@NonNull final List<Throwable> exceptions) throws ExceptionCollection {
671         if (!mode.isDatabaseRequired()) {
672             return;
673         }
674         final boolean autoUpdate;
675         autoUpdate = settings.getBoolean(Settings.KEYS.AUTO_UPDATE, true);
676         if (autoUpdate) {
677             try {
678                 doUpdates(true);
679             } catch (UpdateException ex) {
680                 exceptions.add(ex);
681                 LOGGER.warn("Unable to update 1 or more Cached Web DataSource, using local "
682                         + "data instead. Results may not include recent vulnerabilities.");
683                 LOGGER.debug("Update Error", ex);
684             } catch (DatabaseException ex) {
685                 throwFatalDatabaseException(ex, exceptions);
686             }
687         } else {
688             try {
689                 if (DatabaseManager.isH2Connection(settings) && !DatabaseManager.h2DataFileExists(settings)) {
690                     throw new ExceptionCollection(new NoDataException("Autoupdate is disabled and the database does not exist"), true);
691                 } else {
692                     openDatabase(true, true);
693                 }
694             } catch (IOException ex) {
695                 throw new ExceptionCollection(new DatabaseException("Autoupdate is disabled and unable to connect to the database"), true);
696             } catch (DatabaseException ex) {
697                 throwFatalDatabaseException(ex, exceptions);
698             }
699         }
700     }
701 
702     /**
703      * Utility method to throw a fatal database exception.
704      *
705      * @param ex the exception that was caught
706      * @param exceptions the exception collection
707      * @throws ExceptionCollection the collection of exceptions is always thrown
708      * as a fatal exception
709      */
710     private void throwFatalDatabaseException(DatabaseException ex, final List<Throwable> exceptions) throws ExceptionCollection {
711         final String msg;
712         if (ex.getMessage().contains("Unable to connect") && DatabaseManager.isH2Connection(settings)) {
713             msg = "Unable to connect to the database - if this error persists it may be "
714                     + "due to a corrupt database. Consider running `purge` to delete the existing database";
715         } else {
716             msg = "Unable to connect to the dependency-check database";
717         }
718         exceptions.add(new DatabaseException(msg, ex));
719         throw new ExceptionCollection(exceptions, true);
720     }
721 
722     /**
723      * Executes executes the analyzer using multiple threads.
724      *
725      * @param exceptions a collection of exceptions that occurred during
726      * analysis
727      * @param analyzer the analyzer to execute
728      * @throws ExceptionCollection thrown if exceptions occurred during analysis
729      */
730     protected void executeAnalysisTasks(@NonNull final Analyzer analyzer, List<Throwable> exceptions) throws ExceptionCollection {
731         LOGGER.debug("Starting {}", analyzer.getName());
732         final List<AnalysisTask> analysisTasks = getAnalysisTasks(analyzer, exceptions);
733         final ExecutorService executorService = getExecutorService(analyzer);
734 
735         try {
736             final int timeout = settings.getInt(Settings.KEYS.ANALYSIS_TIMEOUT, 180);
737             final List<Future<Void>> results = executorService.invokeAll(analysisTasks, timeout, TimeUnit.MINUTES);
738 
739             // ensure there was no exception during execution
740             for (Future<Void> result : results) {
741                 try {
742                     result.get();
743                 } catch (ExecutionException e) {
744                     throwFatalExceptionCollection("Analysis task failed with a fatal exception.", e, exceptions);
745                 } catch (CancellationException e) {
746                     throwFatalExceptionCollection("Analysis task was cancelled.", e, exceptions);
747                 }
748             }
749         } catch (InterruptedException e) {
750             Thread.currentThread().interrupt();
751             throwFatalExceptionCollection("Analysis has been interrupted.", e, exceptions);
752         } finally {
753             executorService.shutdown();
754         }
755     }
756 
757     /**
758      * Returns the analysis tasks for the dependencies.
759      *
760      * @param analyzer the analyzer to create tasks for
761      * @param exceptions the collection of exceptions to collect
762      * @return a collection of analysis tasks
763      */
764     protected synchronized List<AnalysisTask> getAnalysisTasks(Analyzer analyzer, List<Throwable> exceptions) {
765         final List<AnalysisTask> result = new ArrayList<>();
766         dependencies.stream().map((dependency) -> new AnalysisTask(analyzer, dependency, this, exceptions)).forEach(result::add);
767         return result;
768     }
769 
770     /**
771      * Returns the executor service for a given analyzer.
772      *
773      * @param analyzer the analyzer to obtain an executor
774      * @return the executor service
775      */
776     protected ExecutorService getExecutorService(Analyzer analyzer) {
777         if (analyzer.supportsParallelProcessing()) {
778             final int maximumNumberOfThreads = Runtime.getRuntime().availableProcessors();
779             LOGGER.debug("Parallel processing with up to {} threads: {}.", maximumNumberOfThreads, analyzer.getName());
780             return Executors.newFixedThreadPool(maximumNumberOfThreads);
781         } else {
782             LOGGER.debug("Parallel processing is not supported: {}.", analyzer.getName());
783             return Executors.newSingleThreadExecutor();
784         }
785     }
786 
787     /**
788      * Initializes the given analyzer.
789      *
790      * @param analyzer the analyzer to prepare
791      * @throws InitializationException thrown when there is a problem
792      * initializing the analyzer
793      */
794     protected void initializeAnalyzer(@NonNull final Analyzer analyzer) throws InitializationException {
795         try {
796             LOGGER.debug("Initializing {}", analyzer.getName());
797             analyzer.prepare(this);
798         } catch (InitializationException ex) {
799             LOGGER.error("Exception occurred initializing {}.", analyzer.getName());
800             LOGGER.debug("", ex);
801             if (ex.isFatal()) {
802                 try {
803                     analyzer.close();
804                 } catch (Throwable ex1) {
805                     LOGGER.trace("", ex1);
806                 }
807             }
808             throw ex;
809         } catch (Throwable ex) {
810             LOGGER.error("Unexpected exception occurred initializing {}.", analyzer.getName());
811             LOGGER.debug("", ex);
812             try {
813                 analyzer.close();
814             } catch (Throwable ex1) {
815                 LOGGER.trace("", ex1);
816             }
817             throw new InitializationException("Unexpected Exception", ex);
818         }
819     }
820 
821     /**
822      * Closes the given analyzer.
823      *
824      * @param analyzer the analyzer to close
825      */
826     protected void closeAnalyzer(@NonNull final Analyzer analyzer) {
827         LOGGER.debug("Closing Analyzer '{}'", analyzer.getName());
828         try {
829             analyzer.close();
830         } catch (Throwable ex) {
831             LOGGER.trace("", ex);
832         }
833     }
834 
835     /**
836      * Cycles through the cached web data sources and calls update on all of
837      * them.
838      *
839      * @throws UpdateException thrown if the operation fails
840      * @throws DatabaseException if the operation fails due to a local database
841      * failure
842      * @return Whether any updates actually happened
843      */
844     public boolean doUpdates() throws UpdateException, DatabaseException {
845         return doUpdates(false);
846     }
847 
848     /**
849      * Cycles through the cached web data sources and calls update on all of
850      * them.
851      *
852      * @param remainOpen whether or not the database connection should remain
853      * open
854      * @throws UpdateException thrown if the operation fails
855      * @throws DatabaseException if the operation fails due to a local database
856      * failure
857      * @return Whether any updates actually happened
858      */
859     public boolean doUpdates(boolean remainOpen) throws UpdateException, DatabaseException {
860         if (mode.isDatabaseRequired()) {
861             try (WriteLock dblock = new WriteLock(getSettings(), DatabaseManager.isH2Connection(getSettings()))) {
862                 //lock is not needed as we already have the lock held
863                 openDatabase(false, false);
864                 LOGGER.info("Checking for updates");
865                 final long updateStart = System.currentTimeMillis();
866                 final UpdateService service = new UpdateService(serviceClassLoader);
867                 final Iterator<CachedWebDataSource> iterator = service.getDataSources();
868                 boolean dbUpdatesMade = false;
869                 UpdateException updateException = null;
870                 while (iterator.hasNext()) {
871                     try {
872                         final CachedWebDataSource source = iterator.next();
873                         dbUpdatesMade |= source.update(this);
874                     } catch (UpdateException ex) {
875                         updateException = ex;
876                         LOGGER.error(ex.getMessage(), ex);
877                     }
878                 }
879                 if (dbUpdatesMade) {
880                     database.defrag();
881                 }
882                 database.close();
883                 database = null;
884                 LOGGER.info("Check for updates complete ({} ms)", System.currentTimeMillis() - updateStart);
885                 if (remainOpen) {
886                     //lock is not needed as we already have the lock held
887                     openDatabase(true, false);
888                 }
889                 if (updateException != null) {
890                     throw updateException;
891                 }
892 
893                 return dbUpdatesMade;
894             } catch (WriteLockException ex) {
895                 throw new UpdateException("Unable to obtain an exclusive lock on the H2 database to perform updates", ex);
896             }
897         } else {
898             LOGGER.info("Skipping update check in evidence collection mode.");
899             return false;
900         }
901     }
902 
903     /**
904      * Purges the cached web data sources.
905      *
906      * @return <code>true</code> if the purge was successful; otherwise
907      * <code>false</code>
908      */
909     public boolean purge() {
910         boolean result = true;
911         final UpdateService service = new UpdateService(serviceClassLoader);
912         final Iterator<CachedWebDataSource> iterator = service.getDataSources();
913         while (iterator.hasNext()) {
914             result &= iterator.next().purge(this);
915         }
916         try {
917             final File cache = new File(settings.getDataDirectory(), "cache");
918             if (cache.exists()) {
919                 if (FileUtils.delete(cache)) {
920                     LOGGER.info("Cache directory purged");
921                 }
922             }
923         } catch (IOException ex) {
924             throw new RuntimeException(ex);
925         }
926         try {
927             final File cache = new File(settings.getDataDirectory(), "oss_cache");
928             if (cache.exists()) {
929                 if (FileUtils.delete(cache)) {
930                     LOGGER.info("OSS Cache directory purged");
931                 }
932             }
933         } catch (IOException ex) {
934             throw new RuntimeException(ex);
935         }
936 
937         return result;
938     }
939 
940     /**
941      * <p>
942      * This method is only public for unit/integration testing. This method
943      * should not be called by any integration that uses
944      * dependency-check-core.</p>
945      * <p>
946      * Opens the database connection.</p>
947      *
948      * @throws DatabaseException if the database connection could not be created
949      */
950     public void openDatabase() throws DatabaseException {
951         openDatabase(false, true);
952     }
953 
954     /**
955      * <p>
956      * This method is only public for unit/integration testing. This method
957      * should not be called by any integration that uses
958      * dependency-check-core.</p>
959      * <p>
960      * Opens the database connection; if readOnly is true a copy of the database
961      * will be made.</p>
962      *
963      * @param readOnly whether or not the database connection should be readonly
964      * @param lockRequired whether or not a lock needs to be acquired when
965      * opening the database
966      * @throws DatabaseException if the database connection could not be created
967      */
968     @SuppressWarnings("try")
969     public void openDatabase(boolean readOnly, boolean lockRequired) throws DatabaseException {
970         if (mode.isDatabaseRequired() && database == null) {
971             try (WriteLock dblock = new WriteLock(getSettings(), lockRequired && DatabaseManager.isH2Connection(settings))) {
972                 if (readOnly
973                         && DatabaseManager.isH2Connection(settings)
974                         && settings.getString(Settings.KEYS.DB_CONNECTION_STRING).contains("file:%s")) {
975                     final File db = DatabaseManager.getH2DataFile(settings);
976                     if (db.isFile()) {
977                         final File temp = settings.getTempDirectory();
978                         final File tempDB = new File(temp, db.getName());
979                         LOGGER.debug("copying database {} to {}", db.toPath(), temp.toPath());
980                         Files.copy(db.toPath(), tempDB.toPath());
981                         settings.setString(Settings.KEYS.H2_DATA_DIRECTORY, temp.getPath());
982                         final String connStr = settings.getString(Settings.KEYS.DB_CONNECTION_STRING);
983                         if (!connStr.contains("ACCESS_MODE_DATA")) {
984                             settings.setString(Settings.KEYS.DB_CONNECTION_STRING, connStr + "ACCESS_MODE_DATA=r");
985                         }
986                         settings.setBoolean(Settings.KEYS.AUTO_UPDATE, false);
987                         database = new CveDB(settings);
988                     } else {
989                         throw new DatabaseException("Unable to open database - configured database file does not exist: " + db);
990                     }
991                 } else {
992                     database = new CveDB(settings);
993                 }
994             } catch (IOException ex) {
995                 throw new DatabaseException("Unable to open database in read only mode", ex);
996             } catch (WriteLockException ex) {
997                 throw new DatabaseException("Failed to obtain lock - unable to open database", ex);
998             }
999             database.open();
1000         }
1001     }
1002 
1003     /**
1004      * Returns a reference to the database.
1005      *
1006      * @return a reference to the database
1007      */
1008     public CveDB getDatabase() {
1009         return this.database;
1010     }
1011 
1012     /**
1013      * Returns a full list of all of the analyzers. This is useful for reporting
1014      * which analyzers where used.
1015      *
1016      * @return a list of Analyzers
1017      */
1018     @NonNull
1019     public List<Analyzer> getAnalyzers() {
1020         final List<Analyzer> analyzerList = new ArrayList<>();
1021         //insteae of forEach - we can just do a collect
1022         mode.getPhases().stream()
1023                 .map(analyzers::get)
1024                 .forEachOrdered(analyzerList::addAll);
1025         return analyzerList;
1026     }
1027 
1028     /**
1029      * Checks all analyzers to see if an extension is supported.
1030      *
1031      * @param file a file extension
1032      * @return true or false depending on whether or not the file extension is
1033      * supported
1034      */
1035     @Override
1036     public boolean accept(@Nullable final File file) {
1037         if (file == null) {
1038             return false;
1039         }
1040         /* note, we can't break early on this loop as the analyzers need to know if
1041         they have files to work on prior to initialization */
1042         return this.fileTypeAnalyzers.stream().map((a) -> a.accept(file)).reduce(false, (accumulator, result) -> accumulator || result);
1043     }
1044 
1045     /**
1046      * Returns the set of file type analyzers.
1047      *
1048      * @return the set of file type analyzers
1049      */
1050     public Set<FileTypeAnalyzer> getFileTypeAnalyzers() {
1051         return this.fileTypeAnalyzers;
1052     }
1053 
1054     /**
1055      * Returns the configured settings.
1056      *
1057      * @return the configured settings
1058      */
1059     public Settings getSettings() {
1060         return settings;
1061     }
1062 
1063     /**
1064      * Retrieve an object from the objects collection.
1065      *
1066      * @param key the key to retrieve the object
1067      * @return the object
1068      */
1069     public Object getObject(String key) {
1070         return objects.get(key);
1071     }
1072 
1073     /**
1074      * Put an object in the object collection.
1075      *
1076      * @param key the key to store the object
1077      * @param object the object to store
1078      */
1079     public void putObject(String key, Object object) {
1080         objects.put(key, object);
1081     }
1082 
1083     /**
1084      * Verifies if the object exists in the object store.
1085      *
1086      * @param key the key to retrieve the object
1087      * @return <code>true</code> if the object exists; otherwise
1088      * <code>false</code>
1089      */
1090     public boolean hasObject(String key) {
1091         return objects.containsKey(key);
1092     }
1093 
1094     /**
1095      * Removes an object from the object store.
1096      *
1097      * @param key the key to the object
1098      */
1099     public void removeObject(String key) {
1100         objects.remove(key);
1101     }
1102 
1103     /**
1104      * Returns the mode of the engine.
1105      *
1106      * @return the mode of the engine
1107      */
1108     public Mode getMode() {
1109         return mode;
1110     }
1111 
1112     /**
1113      * Adds a file type analyzer. This has been added solely to assist in unit
1114      * testing the Engine.
1115      *
1116      * @param fta the file type analyzer to add
1117      */
1118     protected void addFileTypeAnalyzer(@NonNull final FileTypeAnalyzer fta) {
1119         this.fileTypeAnalyzers.add(fta);
1120     }
1121 
1122     /**
1123      * Checks the CPE Index to ensure documents exists. If none exist a
1124      * NoDataException is thrown.
1125      *
1126      * @throws NoDataException thrown if no data exists in the CPE Index
1127      */
1128     private void ensureDataExists() throws NoDataException {
1129         if (mode.isDatabaseRequired() && (database == null || !database.dataExists())) {
1130             throw new NoDataException("No documents exist");
1131         }
1132     }
1133 
1134     /**
1135      * Constructs and throws a fatal exception collection.
1136      *
1137      * @param message the exception message
1138      * @param throwable the cause
1139      * @param exceptions a collection of exception to include
1140      * @throws ExceptionCollection a collection of exceptions that occurred
1141      * during analysis
1142      */
1143     private void throwFatalExceptionCollection(String message, @NonNull final Throwable throwable,
1144             @NonNull final List<Throwable> exceptions) throws ExceptionCollection {
1145         LOGGER.error(message);
1146         LOGGER.debug("", throwable);
1147         exceptions.add(throwable);
1148         throw new ExceptionCollection(exceptions, true);
1149     }
1150 
1151     //CSOFF: LineLength
1152     /**
1153      * Writes the report to the given output directory.
1154      *
1155      * @param applicationName the name of the application/project
1156      * @param outputDir the path to the output directory (can include the full
1157      * file name if the format is not ALL)
1158      * @param format the report format (see {@link ReportGenerator.Format})
1159      * @param exceptions a collection of exceptions that may have occurred
1160      * during the analysis
1161      * @throws ReportException thrown if there is an error generating the report
1162      */
1163     public void writeReports(String applicationName, File outputDir, String format, ExceptionCollection exceptions) throws ReportException {
1164         writeReports(applicationName, null, null, null, outputDir, format, exceptions);
1165     }
1166     //CSON: LineLength
1167 
1168     /**
1169      * Writes the report to the given output directory.
1170      *
1171      * @param applicationName the name of the application/project
1172      * @param groupId the Maven groupId
1173      * @param artifactId the Maven artifactId
1174      * @param version the Maven version
1175      * @param outputDir the path to the output directory (can include the full
1176      * file name if the format is not ALL)
1177      * @param format the report format  (see {@link ReportGenerator.Format})
1178      * @param exceptions a collection of exceptions that may have occurred
1179      * during the analysis
1180      * @throws ReportException thrown if there is an error generating the report
1181      */
1182     public synchronized void writeReports(String applicationName, @Nullable final String groupId,
1183             @Nullable final String artifactId, @Nullable final String version,
1184             @NonNull final File outputDir, String format, ExceptionCollection exceptions) throws ReportException {
1185         if (mode == Mode.EVIDENCE_COLLECTION) {
1186             throw new UnsupportedOperationException("Cannot generate report in evidence collection mode.");
1187         }
1188         final DatabaseProperties prop = database.getDatabaseProperties();
1189 
1190         final ReportGenerator r = new ReportGenerator(applicationName, groupId, artifactId, version,
1191                 dependencies, getAnalyzers(), prop, settings, exceptions);
1192         try {
1193             r.write(outputDir.getAbsolutePath(), format);
1194         } catch (ReportException ex) {
1195             final String msg = String.format("Error generating the report for %s", applicationName);
1196             LOGGER.debug(msg, ex);
1197             throw new ReportException(msg, ex);
1198         }
1199     }
1200 
1201     private boolean identifiersMatch(Set<Identifier> left, Set<Identifier> right) {
1202         if (left != null && right != null && !left.isEmpty() && left.size() == right.size()) {
1203             int count = 0;
1204             for (Identifier l : left) {
1205                 for (Identifier r : right) {
1206                     if (l.getValue().equals(r.getValue())) {
1207                         count += 1;
1208                         break;
1209                     }
1210                 }
1211             }
1212             return count == left.size();
1213         }
1214         return false;
1215     }
1216 
1217     /**
1218      * {@link Engine} execution modes.
1219      */
1220     public enum Mode {
1221         /**
1222          * In evidence collection mode the {@link Engine} only collects evidence
1223          * from the scan targets, and doesn't require a database.
1224          */
1225         EVIDENCE_COLLECTION(
1226                 false,
1227                 INITIAL,
1228                 PRE_INFORMATION_COLLECTION,
1229                 INFORMATION_COLLECTION,
1230                 INFORMATION_COLLECTION2,
1231                 POST_INFORMATION_COLLECTION1,
1232                 POST_INFORMATION_COLLECTION2,
1233                 POST_INFORMATION_COLLECTION3
1234         ),
1235         /**
1236          * In evidence processing mode the {@link Engine} processes the evidence
1237          * collected using the {@link #EVIDENCE_COLLECTION} mode. Dependencies
1238          * should be injected into the {@link Engine} using
1239          * {@link Engine#setDependencies(List)}.
1240          */
1241         EVIDENCE_PROCESSING(
1242                 true,
1243                 PRE_IDENTIFIER_ANALYSIS,
1244                 IDENTIFIER_ANALYSIS,
1245                 POST_IDENTIFIER_ANALYSIS,
1246                 PRE_FINDING_ANALYSIS,
1247                 FINDING_ANALYSIS,
1248                 POST_FINDING_ANALYSIS,
1249                 FINDING_ANALYSIS_PHASE2,
1250                 FINAL
1251         ),
1252         /**
1253          * In standalone mode the {@link Engine} will collect and process
1254          * evidence in a single execution.
1255          */
1256         STANDALONE(true, AnalysisPhase.values());
1257 
1258         /**
1259          * Whether the database is required in this mode.
1260          */
1261         private final boolean databaseRequired;
1262         /**
1263          * The analysis phases included in the mode.
1264          */
1265         private final List<AnalysisPhase> phases;
1266 
1267         /**
1268          * Constructs a new mode.
1269          *
1270          * @param databaseRequired if the database is required for the mode
1271          * @param phases the analysis phases to include in the mode
1272          */
1273         Mode(boolean databaseRequired, AnalysisPhase... phases) {
1274             this.databaseRequired = databaseRequired;
1275             this.phases = Collections.unmodifiableList(Arrays.asList(phases));
1276         }
1277 
1278         /**
1279          * Returns true if the database is required; otherwise false.
1280          *
1281          * @return whether or not the database is required
1282          */
1283         private boolean isDatabaseRequired() {
1284             return databaseRequired;
1285         }
1286 
1287         /**
1288          * Returns the phases for this mode.
1289          *
1290          * @return the phases for this mode
1291          */
1292         public List<AnalysisPhase> getPhases() {
1293             return phases;
1294         }
1295     }
1296 }