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.analyzer;
19  
20  import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
21  import java.io.FileFilter;
22  import java.util.ArrayList;
23  import java.util.Collections;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.ListIterator;
27  import java.util.Set;
28  import java.util.regex.Matcher;
29  import java.util.regex.Pattern;
30  import javax.annotation.concurrent.ThreadSafe;
31  import org.owasp.dependencycheck.Engine;
32  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
33  import org.owasp.dependencycheck.dependency.Dependency;
34  import org.owasp.dependencycheck.dependency.Evidence;
35  import org.owasp.dependencycheck.dependency.EvidenceType;
36  import org.owasp.dependencycheck.dependency.naming.CpeIdentifier;
37  import org.owasp.dependencycheck.dependency.naming.Identifier;
38  import org.owasp.dependencycheck.utils.FileFilterBuilder;
39  import org.owasp.dependencycheck.utils.Settings;
40  import org.slf4j.Logger;
41  import org.slf4j.LoggerFactory;
42  import us.springett.parsers.cpe.Cpe;
43  import us.springett.parsers.cpe.CpeBuilder;
44  import us.springett.parsers.cpe.exceptions.CpeValidationException;
45  import us.springett.parsers.cpe.values.Part;
46  
47  /**
48   * This analyzer attempts to remove some well known false positives -
49   * specifically regarding the java runtime.
50   *
51   * @author Jeremy Long
52   */
53  @ThreadSafe
54  public class FalsePositiveAnalyzer extends AbstractAnalyzer {
55  
56      /**
57       * The Logger.
58       */
59      private static final Logger LOGGER = LoggerFactory.getLogger(FalsePositiveAnalyzer.class);
60      /**
61       * The file filter used to find DLL and EXE.
62       */
63      private static final FileFilter DLL_EXE_FILTER = FileFilterBuilder.newInstance().addExtensions("dll", "exe").build();
64      /**
65       * Regex to identify core java libraries and a few other commonly
66       * misidentified ones.
67       */
68      public static final Pattern CORE_JAVA = Pattern.compile("^cpe:/a:(sun|oracle|ibm):(j2[ems]e|"
69              + "java(_platform_micro_edition|_runtime_environment|_se|virtual_machine|se_development_kit|fx)?|"
70              + "jdk|jre|jsse)($|:.*)");
71      /**
72       * Regex to identify core jsf libraries.
73       */
74      public static final Pattern CORE_JAVA_JSF = Pattern.compile("^cpe:/a:(sun|oracle|ibm):jsf($|:.*)");
75      /**
76       * Regex to identify core java library files. This is currently incomplete.
77       */
78      public static final Pattern CORE_FILES = Pattern.compile("(^|/)((alt[-])?rt|jsse|jfxrt|jfr|jce|javaws|deploy|charsets)\\.jar$");
79      /**
80       * Regex to identify core jsf java library files. This is currently
81       * incomplete.
82       */
83      public static final Pattern CORE_JSF_FILES = Pattern.compile("(^|/)jsf[-][^/]*\\.jar$");
84  
85      //<editor-fold defaultstate="collapsed" desc="All standard implementation details of Analyzer">
86      /**
87       * The name of the analyzer.
88       */
89      private static final String ANALYZER_NAME = "False Positive Analyzer";
90      /**
91       * The phase that this analyzer is intended to run in.
92       */
93      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.POST_IDENTIFIER_ANALYSIS;
94  
95      /**
96       * Returns the name of the analyzer.
97       *
98       * @return the name of the analyzer.
99       */
100     @Override
101     public String getName() {
102         return ANALYZER_NAME;
103     }
104 
105     /**
106      * Returns the phase that the analyzer is intended to run in.
107      *
108      * @return the phase that the analyzer is intended to run in.
109      */
110     @Override
111     public AnalysisPhase getAnalysisPhase() {
112         return ANALYSIS_PHASE;
113     }
114 
115     /**
116      * <p>
117      * Returns the setting key to determine if the analyzer is enabled.</p>
118      *
119      * @return the key for the analyzer's enabled property
120      */
121     @Override
122     protected String getAnalyzerEnabledSettingKey() {
123         return Settings.KEYS.ANALYZER_FALSE_POSITIVE_ENABLED;
124     }
125     //</editor-fold>
126 
127     /**
128      * Analyzes the dependencies and removes bad/incorrect CPE associations
129      * based on various heuristics.
130      *
131      * @param dependency the dependency to analyze.
132      * @param engine the engine that is scanning the dependencies
133      * @throws AnalysisException is thrown if there is an error reading the JAR
134      * file.
135      */
136     @Override
137     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
138         removeJreEntries(dependency);
139         removeBadMatches(dependency);
140         removeWrongVersionMatches(dependency);
141         removeSpuriousCPE(dependency);
142         removeDuplicativeEntriesFromJar(dependency, engine);
143         addFalseNegativeCPEs(dependency);
144     }
145 
146     /**
147      * <p>
148      * Intended to remove spurious CPE entries. By spurious we mean duplicate,
149      * less specific CPE entries.</p>
150      * <p>
151      * Example:</p>
152      * <code>
153      * cpe:/a:some-vendor:some-product
154      * cpe:/a:some-vendor:some-product:1.5
155      * cpe:/a:some-vendor:some-product:1.5.2
156      * </code>
157      * <p>
158      * Should be trimmed to:</p>
159      * <code>
160      * cpe:/a:some-vendor:some-product:1.5.2
161      * </code>
162      *
163      * @param dependency the dependency being analyzed
164      */
165     //CSOFF: NestedIfDepth
166     @SuppressWarnings("null")
167     @SuppressFBWarnings(justification = "null checks are working correctly to prevent NPE", value = {"NP_NULL_ON_SOME_PATH_MIGHT_BE_INFEASIBLE"})
168     private void removeSpuriousCPE(Dependency dependency) {
169         final List<Identifier> ids = new ArrayList<>(dependency.getVulnerableSoftwareIdentifiers());
170         Collections.sort(ids);
171         final ListIterator<Identifier> mainItr = ids.listIterator();
172         while (mainItr.hasNext()) {
173             final Identifier temp = mainItr.next();
174             if (temp instanceof CpeIdentifier) {
175                 final CpeIdentifier currentId = (CpeIdentifier) temp;
176                 final Cpe currentCpe = currentId.getCpe();
177                 final ListIterator<Identifier> subItr = ids.listIterator(mainItr.nextIndex());
178                 while (subItr.hasNext()) {
179                     final Identifier nextId = subItr.next();
180                     if (nextId instanceof CpeIdentifier) {
181                         final CpeIdentifier nextCpeId = (CpeIdentifier) nextId;
182                         final Cpe nextCpe = nextCpeId.getCpe();
183                         //TODO fix the version problem below
184                         if (currentCpe.getVendor().equals(nextCpe.getVendor())) {
185                             if (currentCpe.getProduct().equals(nextCpe.getProduct())) {
186                                 // see if one is contained in the other.. remove the contained one from dependency.getIdentifier
187                                 final String currentVersion = currentCpe.getVersion();
188                                 final String nextVersion = nextCpe.getVersion();
189                                 if (currentVersion == null && nextVersion == null) {
190                                     //how did we get here?
191                                     LOGGER.debug("currentVersion and nextVersion are both null?");
192                                 } else if (currentVersion == null && nextVersion != null) {
193                                     dependency.removeVulnerableSoftwareIdentifier(currentId);
194                                 } else if (nextVersion == null && currentVersion != null) {
195                                     dependency.removeVulnerableSoftwareIdentifier(nextId);
196                                 } else if (currentVersion.length() < nextVersion.length()) {
197                                     if (nextVersion.startsWith(currentVersion) || "-".equals(currentVersion)) {
198                                         dependency.removeVulnerableSoftwareIdentifier(currentId);
199                                     }
200                                 } else if (currentVersion.startsWith(nextVersion) || "-".equals(nextVersion)) {
201                                     dependency.removeVulnerableSoftwareIdentifier(nextId);
202                                 }
203                             }
204                         }
205                     }
206                 }
207             }
208         }
209     }
210     //CSON: NestedIfDepth
211 
212     /**
213      * Removes any CPE entries for the JDK/JRE unless the filename ends with
214      * rt.jar
215      *
216      * @param dependency the dependency to remove JRE CPEs from
217      */
218     private void removeJreEntries(Dependency dependency) {
219         final Set<Identifier> removalSet = new HashSet<>();
220         dependency.getVulnerableSoftwareIdentifiers().forEach(i -> {
221             final Matcher coreCPE = CORE_JAVA.matcher(i.getValue());
222             final Matcher coreFiles = CORE_FILES.matcher(dependency.getFileName());
223             final Matcher coreJsfCPE = CORE_JAVA_JSF.matcher(i.getValue());
224             final Matcher coreJsfFiles = CORE_JSF_FILES.matcher(dependency.getFileName());
225             if ((coreCPE.matches() && !coreFiles.matches())
226                     || (coreJsfCPE.matches() && !coreJsfFiles.matches())) {
227                 removalSet.add(i);
228             }
229 
230         });
231         removalSet.forEach(dependency::removeVulnerableSoftwareIdentifier);
232     }
233 
234     /**
235      * Removes bad CPE matches for a dependency. Unfortunately, right now these
236      * are hard-coded patches for specific problems identified when testing this
237      * on a LARGE volume of jar files.
238      *
239      * @param dependency the dependency to analyze
240      */
241     protected void removeBadMatches(Dependency dependency) {
242 
243         final Set<Identifier> toRemove = new HashSet<>();
244         /* TODO - can we utilize the pom's groupid and artifactId to filter??? most of
245          * these are due to low quality data.  Other idea would be to say any CPE
246          * found based on LOW confidence evidence should have a different CPE type? (this
247          * might be a better solution then just removing the URL for "best-guess" matches).
248          */
249         //Set<Evidence> groupId = dependency.getVendorEvidence().getEvidence("pom", "groupid");
250         //Set<Evidence> artifactId = dependency.getVendorEvidence().getEvidence("pom", "artifactid");
251         for (Identifier i : dependency.getVulnerableSoftwareIdentifiers()) {
252             //TODO move this startsWith expression to the base suppression file
253             if (i instanceof CpeIdentifier) {
254                 final CpeIdentifier cpeId = (CpeIdentifier) i;
255                 final Cpe cpe = cpeId.getCpe();
256                 if ((cpe.getProduct().matches(".*c\\+\\+.*")
257                         || ("file".equals(cpe.getVendor()) && "file".equals(cpe.getProduct()))
258                         || ("mozilla".equals(cpe.getVendor()) && "mozilla".equals(cpe.getProduct()))
259                         || ("cvs".equals(cpe.getVendor()) && "cvs".equals(cpe.getProduct()))
260                         || ("ftp".equals(cpe.getVendor()) && "ftp".equals(cpe.getProduct()))
261                         || ("tcp".equals(cpe.getVendor()) && "tcp".equals(cpe.getProduct()))
262                         || ("ssh".equals(cpe.getVendor()) && "ssh".equals(cpe.getProduct()))
263                         || ("lookup".equals(cpe.getVendor()) && "lookup".equals(cpe.getProduct())))
264                         && (dependency.getFileName().toLowerCase().endsWith(".jar")
265                         || dependency.getFileName().toLowerCase().endsWith("pom.xml")
266                         || dependency.getFileName().toLowerCase().endsWith(".dll")
267                         || dependency.getFileName().toLowerCase().endsWith(".exe")
268                         || dependency.getFileName().toLowerCase().endsWith(".nuspec")
269                         || dependency.getFileName().toLowerCase().endsWith(".zip")
270                         || dependency.getFileName().toLowerCase().endsWith(".sar")
271                         || dependency.getFileName().toLowerCase().endsWith(".apk")
272                         || dependency.getFileName().toLowerCase().endsWith(".tar")
273                         || dependency.getFileName().toLowerCase().endsWith(".gz")
274                         || dependency.getFileName().toLowerCase().endsWith(".tgz")
275                         || dependency.getFileName().toLowerCase().endsWith(".rpm")
276                         || dependency.getFileName().toLowerCase().endsWith(".ear")
277                         || dependency.getFileName().toLowerCase().endsWith(".war"))) {
278                     toRemove.add(i);
279                 } else if ((("jquery".equals(cpe.getVendor()) && "jquery".equals(cpe.getProduct()))
280                         || ("prototypejs".equals(cpe.getVendor()) && "prototype".equals(cpe.getProduct()))
281                         || ("yahoo".equals(cpe.getVendor()) && "yui".equals(cpe.getProduct())))
282                         && (dependency.getFileName().toLowerCase().endsWith(".jar")
283                         || dependency.getFileName().toLowerCase().endsWith("pom.xml")
284                         || dependency.getFileName().toLowerCase().endsWith(".dll")
285                         || dependency.getFileName().toLowerCase().endsWith(".exe"))) {
286                     toRemove.add(i);
287                 } else if ((("microsoft".equals(cpe.getVendor()) && "excel".equals(cpe.getProduct()))
288                         || ("microsoft".equals(cpe.getVendor()) && "word".equals(cpe.getProduct()))
289                         || ("microsoft".equals(cpe.getVendor()) && "visio".equals(cpe.getProduct()))
290                         || ("microsoft".equals(cpe.getVendor()) && "powerpoint".equals(cpe.getProduct()))
291                         || ("microsoft".equals(cpe.getVendor()) && "office".equals(cpe.getProduct()))
292                         || ("core_ftp".equals(cpe.getVendor()) && "core_ftp".equals(cpe.getProduct())))
293                         && (dependency.getFileName().toLowerCase().endsWith(".jar")
294                         || dependency.getFileName().toLowerCase().endsWith(".ear")
295                         || dependency.getFileName().toLowerCase().endsWith(".war")
296                         || dependency.getFileName().toLowerCase().endsWith("pom.xml"))) {
297                     toRemove.add(i);
298                 } else if (("apache".equals(cpe.getVendor()) && "maven".equals(cpe.getProduct()))
299                         && !dependency.getFileName().toLowerCase().matches("maven-core-[\\d.]+\\.jar")) {
300                     toRemove.add(i);
301                 } else if (("m-core".equals(cpe.getVendor()) && "m-core".equals(cpe.getProduct()))) {
302                     boolean found = false;
303                     for (Evidence e : dependency.getEvidence(EvidenceType.PRODUCT)) {
304                         if ("m-core".equalsIgnoreCase(e.getValue())) {
305                             found = true;
306                             break;
307                         }
308                     }
309                     if (!found) {
310                         for (Evidence e : dependency.getEvidence(EvidenceType.VENDOR)) {
311                             if ("m-core".equalsIgnoreCase(e.getValue())) {
312                                 found = true;
313                                 break;
314                             }
315                         }
316                     }
317                     if (!found) {
318                         toRemove.add(i);
319                     }
320                 } else if (("jboss".equals(cpe.getVendor()) && "jboss".equals(cpe.getProduct()))
321                         && !dependency.getFileName().toLowerCase().matches("jboss-?[\\d.-]+(GA)?\\.jar")) {
322                     toRemove.add(i);
323                 } else if ("java-websocket_project".equals(cpe.getVendor())
324                         && "java-websocket".equals(cpe.getProduct())) {
325                     boolean found = false;
326                     for (Identifier si : dependency.getSoftwareIdentifiers()) {
327                         if (si.getValue().toLowerCase().contains("org.java-websocket/java-websocket")) {
328                             found = true;
329                             break;
330                         }
331                     }
332                     if (!found) {
333                         toRemove.add(i);
334                     }
335                 }
336             }
337         }
338         toRemove.forEach(dependency::removeVulnerableSoftwareIdentifier);
339     }
340 
341     /**
342      * Removes CPE matches for the wrong version of a dependency. Currently,
343      * this only covers Axis 1 & 2.
344      *
345      * @param dependency the dependency to analyze
346      */
347     private void removeWrongVersionMatches(Dependency dependency) {
348         final Set<Identifier> identifiersToRemove = new HashSet<>();
349         final String fileName = dependency.getFileName();
350         if (fileName != null && fileName.contains("axis2")) {
351             dependency.getVulnerableSoftwareIdentifiers().stream()
352                     .filter((i) -> (i instanceof CpeIdentifier))
353                     .map(i -> (CpeIdentifier) i)
354                     .forEach((i) -> {
355                         final Cpe cpe = i.getCpe();
356                         if ("apache".equals(cpe.getVendor()) && "axis".equals(cpe.getProduct())) {
357                             identifiersToRemove.add(i);
358                         }
359                     });
360         } else if (fileName != null && fileName.contains("axis")) {
361             dependency.getVulnerableSoftwareIdentifiers().stream()
362                     .filter((i) -> (i instanceof CpeIdentifier))
363                     .map(i -> (CpeIdentifier) i)
364                     .forEach((i) -> {
365                         final Cpe cpe = i.getCpe();
366                         if ("apache".equals(cpe.getVendor()) && "axis2".equals(cpe.getProduct())) {
367                             identifiersToRemove.add(i);
368                         }
369                     });
370         }
371         identifiersToRemove.forEach(dependency::removeVulnerableSoftwareIdentifier);
372     }
373 
374     /**
375      * There are some known CPE entries, specifically regarding sun and oracle
376      * products due to the acquisition and changes in product names, that based
377      * on given evidence we can add the related CPE entries to ensure a complete
378      * list of CVE entries.
379      *
380      * @param dependency the dependency being analyzed
381      */
382     @SuppressWarnings("UnnecessaryParentheses")
383     private void addFalseNegativeCPEs(Dependency dependency) {
384         final CpeBuilder builder = new CpeBuilder();
385         //TODO move this to the hint analyzer
386         final List<Identifier> identifiersToAdd = new ArrayList<>();
387         dependency.getVulnerableSoftwareIdentifiers().stream()
388                 .filter((i) -> (i instanceof CpeIdentifier))
389                 .map(i -> (CpeIdentifier) i)
390                 .forEach((i) -> {
391                     final Cpe cpe = i.getCpe();
392                     if ((("oracle".equals(cpe.getVendor())
393                             && ("opensso".equals(cpe.getProduct()) || "opensso_enterprise".equals(cpe.getProduct()))))
394                             || ("sun".equals(cpe.getVendor())
395                             && ("opensso".equals(cpe.getProduct()) || "opensso_enterprise".equals(cpe.getProduct())))) {
396 
397                         try {
398                             final Cpe newCpe1 = builder.part(Part.APPLICATION).vendor("sun")
399                                     .product("opensso_enterprise").version(cpe.getVersion()).build();
400                             final Cpe newCpe2 = builder.part(Part.APPLICATION).vendor("oracle")
401                                     .product("opensso_enterprise").version(cpe.getVersion()).build();
402                             final Cpe newCpe3 = builder.part(Part.APPLICATION).vendor("sun")
403                                     .product("opensso").version(cpe.getVersion()).build();
404                             final Cpe newCpe4 = builder.part(Part.APPLICATION).vendor("oracle")
405                                     .product("opensso").version(cpe.getVersion()).build();
406                             final CpeIdentifier newCpeId1 = new CpeIdentifier(newCpe1, i.getConfidence());
407                             final CpeIdentifier newCpeId2 = new CpeIdentifier(newCpe2, i.getConfidence());
408                             final CpeIdentifier newCpeId3 = new CpeIdentifier(newCpe3, i.getConfidence());
409                             final CpeIdentifier newCpeId4 = new CpeIdentifier(newCpe4, i.getConfidence());
410                             identifiersToAdd.add(newCpeId1);
411                             identifiersToAdd.add(newCpeId2);
412                             identifiersToAdd.add(newCpeId3);
413                             identifiersToAdd.add(newCpeId4);
414 
415                         } catch (CpeValidationException ex) {
416                             LOGGER.warn("Unable to add oracle and sun CPEs", ex);
417                         }
418                     }
419                     if ("apache".equals(cpe.getVendor()) && "santuario_xml_security_for_java".equals(cpe.getProduct())) {
420                         try {
421                             final Cpe newCpe1 = builder.part(Part.APPLICATION).vendor("apache")
422                                     .product("xml_security_for_java").version(cpe.getVersion()).build();
423                             final CpeIdentifier newCpeId1 = new CpeIdentifier(newCpe1, i.getConfidence());
424                             identifiersToAdd.add(newCpeId1);
425                         } catch (CpeValidationException ex) {
426                             LOGGER.warn("Unable to add apache xml_security_for_java CPE", ex);
427                         }
428                     }
429                 });
430         identifiersToAdd.forEach(dependency::addVulnerableSoftwareIdentifier);
431     }
432 
433     /**
434      * Removes duplicate entries identified that are contained within JAR files.
435      * These occasionally crop up due to POM entries or other types of files
436      * (such as DLLs and EXEs) being contained within the JAR.
437      *
438      * @param dependency the dependency that might be a duplicate
439      * @param engine the engine used to scan all dependencies
440      */
441     private synchronized void removeDuplicativeEntriesFromJar(Dependency dependency, Engine engine) {
442         //Believed to be code that should have been removed several versions ago. This logic
443         // incorreclty removes dependencies such as more than half the pom entries in pax-web-jetty-bundle-6.0.7.jar
444 //        if (dependency.getFileName().toLowerCase().endsWith("pom.xml")
445 //                || DLL_EXE_FILTER.accept(dependency.getActualFile())) {
446 //            String parentPath = dependency.getFilePath().toLowerCase();
447 //            if (parentPath.contains(".jar")) {
448 //                parentPath = parentPath.substring(0, parentPath.indexOf(".jar") + 4);
449 //                final Dependency[] dependencies = engine.getDependencies();
450 //                final Dependency parent = findDependency(parentPath, dependencies);
451 //                if (parent != null) {
452 //                    final boolean remove = dependency.getVulnerableSoftwareIdentifiers().stream()
453 //                            .filter((i) -> (i instanceof CpeIdentifier))
454 //                            .map(i -> (CpeIdentifier) i)
455 //                            .anyMatch(i -> parent.getVulnerableSoftwareIdentifiers().stream()
456 //                                    .filter((p) -> (p instanceof CpeIdentifier))
457 //                                    .map(p -> (CpeIdentifier) p)
458 //                                    .anyMatch(p -> !p.equals(i)
459 //                                    && p.getCpe().getPart().equals(i.getCpe().getPart())
460 //                                    && p.getCpe().getVendor().equals(i.getCpe().getVendor())
461 //                                    && p.getCpe().getProduct().equals(i.getCpe().getProduct())));
462 //                    if (remove) {
463 //                        engine.removeDependency(dependency);
464 //                    }
465 //                }
466 //            }
467 //        }
468     }
469 
470     /**
471      * Retrieves a given dependency, based on a given path, from a list of
472      * dependencies.
473      *
474      * @param dependencyPath the path of the dependency to return
475      * @param dependencies the array of dependencies to search
476      * @return the dependency object for the given path, otherwise null
477      */
478     private Dependency findDependency(String dependencyPath, Dependency[] dependencies) {
479         for (Dependency d : dependencies) {
480             if (d.getFilePath().equalsIgnoreCase(dependencyPath)) {
481                 return d;
482             }
483         }
484         return null;
485     }
486 }