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