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