Coverage Report - org.owasp.dependencycheck.analyzer.FalsePositiveAnalyzer
 
Classes in this File Line Coverage Branch Coverage Complexity
FalsePositiveAnalyzer
70%
66/94
46%
39/84
5.6
 
 1  
 /*
 2  
  * This file is part of dependency-check-core.
 3  
  *
 4  
  * Dependency-check-core is free software: you can redistribute it and/or modify it
 5  
  * under the terms of the GNU General Public License as published by the Free
 6  
  * Software Foundation, either version 3 of the License, or (at your option) any
 7  
  * later version.
 8  
  *
 9  
  * Dependency-check-core is distributed in the hope that it will be useful, but
 10  
  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  
  * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 12  
  * details.
 13  
  *
 14  
  * You should have received a copy of the GNU General Public License along with
 15  
  * dependency-check-core. If not, see http://www.gnu.org/licenses/.
 16  
  *
 17  
  * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
 18  
  */
 19  
 package org.owasp.dependencycheck.analyzer;
 20  
 
 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.logging.Level;
 30  
 import java.util.logging.Logger;
 31  
 import java.util.regex.Matcher;
 32  
 import java.util.regex.Pattern;
 33  
 import org.owasp.dependencycheck.Engine;
 34  
 import org.owasp.dependencycheck.dependency.Dependency;
 35  
 import org.owasp.dependencycheck.dependency.Identifier;
 36  
 import org.owasp.dependencycheck.dependency.VulnerableSoftware;
 37  
 
 38  
 /**
 39  
  * This analyzer attempts to remove some well known false positives -
 40  
  * specifically regarding the java runtime.
 41  
  *
 42  
  * @author Jeremy Long (jeremy.long@owasp.org)
 43  
  */
 44  7
 public class FalsePositiveAnalyzer extends AbstractAnalyzer {
 45  
 
 46  
     //<editor-fold defaultstate="collapsed" desc="All standard implmentation details of Analyzer">
 47  
     /**
 48  
      * The set of file extensions supported by this analyzer.
 49  
      */
 50  1
     private static final Set<String> EXTENSIONS = null;
 51  
     /**
 52  
      * The name of the analyzer.
 53  
      */
 54  
     private static final String ANALYZER_NAME = "False Positive Analyzer";
 55  
     /**
 56  
      * The phase that this analyzer is intended to run in.
 57  
      */
 58  1
     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.POST_IDENTIFIER_ANALYSIS;
 59  
 
 60  
     /**
 61  
      * Returns a list of file EXTENSIONS supported by this analyzer.
 62  
      *
 63  
      * @return a list of file EXTENSIONS supported by this analyzer.
 64  
      */
 65  
     public Set<String> getSupportedExtensions() {
 66  129
         return EXTENSIONS;
 67  
     }
 68  
 
 69  
     /**
 70  
      * Returns the name of the analyzer.
 71  
      *
 72  
      * @return the name of the analyzer.
 73  
      */
 74  
     public String getName() {
 75  3
         return ANALYZER_NAME;
 76  
     }
 77  
 
 78  
     /**
 79  
      * Returns whether or not this analyzer can process the given extension.
 80  
      *
 81  
      * @param extension the file extension to test for support
 82  
      * @return whether or not the specified file extension is supported by this
 83  
      * analyzer.
 84  
      */
 85  
     public boolean supportsExtension(String extension) {
 86  3
         return true;
 87  
     }
 88  
 
 89  
     /**
 90  
      * Returns the phase that the analyzer is intended to run in.
 91  
      *
 92  
      * @return the phase that the analyzer is intended to run in.
 93  
      */
 94  
     public AnalysisPhase getAnalysisPhase() {
 95  3
         return ANALYSIS_PHASE;
 96  
     }
 97  
     //</editor-fold>
 98  
 
 99  
     /**
 100  
      * Analyzes the dependencies and removes bad/incorrect CPE associations
 101  
      * based on various heuristics.
 102  
      *
 103  
      * @param dependency the dependency to analyze.
 104  
      * @param engine the engine that is scanning the dependencies
 105  
      * @throws AnalysisException is thrown if there is an error reading the JAR
 106  
      * file.
 107  
      */
 108  
     @Override
 109  
     public void analyze(Dependency dependency, Engine engine) throws AnalysisException {
 110  9
         removeJreEntries(dependency);
 111  9
         removeBadMatches(dependency);
 112  9
         removeSpuriousCPE(dependency);
 113  9
         addFalseNegativeCPEs(dependency);
 114  9
     }
 115  
 
 116  
     /**
 117  
      * <p>Intended to remove spurious CPE entries. By spurious we mean
 118  
      * duplicate, less specific CPE entries.</p>
 119  
      * <p>Example:</p>
 120  
      * <code>
 121  
      * cpe:/a:some-vendor:some-product
 122  
      * cpe:/a:some-vendor:some-product:1.5
 123  
      * cpe:/a:some-vendor:some-product:1.5.2
 124  
      * </code>
 125  
      * <p>Should be trimmed to:</p>
 126  
      * <code>
 127  
      * cpe:/a:some-vendor:some-product:1.5.2
 128  
      * </code>
 129  
      *
 130  
      * @param dependency the dependency being analyzed
 131  
      */
 132  
     private void removeSpuriousCPE(Dependency dependency) {
 133  9
         final List<Identifier> ids = new ArrayList<Identifier>();
 134  9
         ids.addAll(dependency.getIdentifiers());
 135  9
         Collections.sort(ids);
 136  9
         final ListIterator<Identifier> mainItr = ids.listIterator();
 137  23
         while (mainItr.hasNext()) {
 138  14
             final Identifier currentId = mainItr.next();
 139  14
             final VulnerableSoftware currentCpe = parseCpe(currentId.getType(), currentId.getValue());
 140  14
             if (currentCpe == null) {
 141  0
                 continue;
 142  
             }
 143  14
             final ListIterator<Identifier> subItr = ids.listIterator(mainItr.nextIndex());
 144  24
             while (subItr.hasNext()) {
 145  10
                 final Identifier nextId = subItr.next();
 146  10
                 final VulnerableSoftware nextCpe = parseCpe(nextId.getType(), nextId.getValue());
 147  10
                 if (nextCpe == null) {
 148  0
                     continue;
 149  
                 }
 150  
                 //TODO fix the version problem below
 151  10
                 if (currentCpe.getVendor().equals(nextCpe.getVendor())) {
 152  2
                     if (currentCpe.getProduct().equals(nextCpe.getProduct())) {
 153  
                         // see if one is contained in the other.. remove the contained one from dependency.getIdentifier
 154  1
                         final String currentVersion = currentCpe.getVersion();
 155  1
                         final String nextVersion = nextCpe.getVersion();
 156  1
                         if (currentVersion == null && nextVersion == null) {
 157  
                             //how did we get here?
 158  0
                             Logger.getLogger(FalsePositiveAnalyzer.class
 159  
                                     .getName()).log(Level.FINE, "currentVersion and nextVersion are both null?");
 160  1
                         } else if (currentVersion == null && nextVersion != null) {
 161  1
                             dependency.getIdentifiers().remove(currentId);
 162  0
                         } else if (nextVersion == null && currentVersion != null) {
 163  0
                             dependency.getIdentifiers().remove(nextId);
 164  0
                         } else if (currentVersion.length() < nextVersion.length()) {
 165  0
                             if (nextVersion.startsWith(currentVersion) || "-".equals(currentVersion)) {
 166  0
                                 dependency.getIdentifiers().remove(currentId);
 167  
                             }
 168  
                         } else {
 169  0
                             if (currentVersion.startsWith(nextVersion) || "-".equals(nextVersion)) {
 170  0
                                 dependency.getIdentifiers().remove(nextId);
 171  
                             }
 172  
                         }
 173  
                     }
 174  
                 }
 175  10
             }
 176  14
         }
 177  9
     }
 178  
     /**
 179  
      * Regex to identify core java libraries and a few other commonly
 180  
      * misidentified ones.
 181  
      */
 182  1
     public static final Pattern CORE_JAVA = Pattern.compile("^cpe:/a:(sun|oracle|ibm):(j2[ems]e|"
 183  
             + "java(_platfrom_micro_edition|_runtime_environment|_se|virtual_machine|se_development_kit|fx)?|"
 184  
             + "jdk|jre|jsf|jsse)($|:.*)");
 185  
     /**
 186  
      * Regex to identify core java library files. This is currently incomplete.
 187  
      */
 188  1
     public static final Pattern CORE_FILES = Pattern.compile("^((alt[-])?rt|jsf[-].*|jsse|jfxrt|jfr|jce|javaws|deploy|charsets)\\.jar$");
 189  
 
 190  
     /**
 191  
      * Removes any CPE entries for the JDK/JRE unless the filename ends with
 192  
      * rt.jar
 193  
      *
 194  
      * @param dependency the dependency to remove JRE CPEs from
 195  
      */
 196  
     private void removeJreEntries(Dependency dependency) {
 197  9
         final Set<Identifier> identifiers = dependency.getIdentifiers();
 198  9
         final Iterator<Identifier> itr = identifiers.iterator();
 199  23
         while (itr.hasNext()) {
 200  14
             final Identifier i = itr.next();
 201  14
             final Matcher coreCPE = CORE_JAVA.matcher(i.getValue());
 202  14
             final Matcher coreFiles = CORE_FILES.matcher(dependency.getFileName());
 203  14
             if (coreCPE.matches() && !coreFiles.matches()) {
 204  0
                 itr.remove();
 205  
             }
 206  
 
 207  
             //replacecd with the regex above.
 208  
             //            if (("cpe:/a:sun:java".equals(i.getValue())
 209  
             //                    || "cpe:/a:oracle:java".equals(i.getValue())
 210  
             //                    || "cpe:/a:ibm:java".equals(i.getValue())
 211  
             //                    || "cpe:/a:sun:j2se".equals(i.getValue())
 212  
             //                    || "cpe:/a:oracle:j2se".equals(i.getValue())
 213  
             //                    || i.getValue().startsWith("cpe:/a:sun:java:")
 214  
             //                    || i.getValue().startsWith("cpe:/a:sun:j2se:")
 215  
             //                    || i.getValue().startsWith("cpe:/a:sun:java:jre")
 216  
             //                    || i.getValue().startsWith("cpe:/a:sun:java:jdk")
 217  
             //                    || i.getValue().startsWith("cpe:/a:sun:java_se")
 218  
             //                    || i.getValue().startsWith("cpe:/a:oracle:java_se")
 219  
             //                    || i.getValue().startsWith("cpe:/a:oracle:java:")
 220  
             //                    || i.getValue().startsWith("cpe:/a:oracle:j2se:")
 221  
             //                    || i.getValue().startsWith("cpe:/a:oracle:jre")
 222  
             //                    || i.getValue().startsWith("cpe:/a:oracle:jdk")
 223  
             //                    || i.getValue().startsWith("cpe:/a:ibm:java:"))
 224  
             //                    && !dependency.getFileName().toLowerCase().endsWith("rt.jar")) {
 225  
             //                itr.remove();
 226  
             //            }
 227  14
         }
 228  9
     }
 229  
 
 230  
     /**
 231  
      * Parses a CPE string into an IndexEntry.
 232  
      *
 233  
      * @param type the type of identifier
 234  
      * @param value the cpe identifier to parse
 235  
      * @return an VulnerableSoftware object constructed from the identifier
 236  
      */
 237  
     private VulnerableSoftware parseCpe(String type, String value) {
 238  24
         if (!"cpe".equals(type)) {
 239  0
             return null;
 240  
         }
 241  24
         final VulnerableSoftware cpe = new VulnerableSoftware();
 242  
         try {
 243  24
             cpe.parseName(value);
 244  0
         } catch (UnsupportedEncodingException ex) {
 245  0
             Logger.getLogger(FalsePositiveAnalyzer.class.getName()).log(Level.FINEST, null, ex);
 246  0
             return null;
 247  24
         }
 248  24
         return cpe;
 249  
     }
 250  
 
 251  
     /**
 252  
      * Removes bad CPE matches for a dependency. Unfortunately, right now these
 253  
      * are hard-coded patches for specific problems identified when testing this
 254  
      * on a LARGE volume of jar files.
 255  
      *
 256  
      * @param dependency the dependency to analyze
 257  
      */
 258  
     private void removeBadMatches(Dependency dependency) {
 259  9
         final Set<Identifier> identifiers = dependency.getIdentifiers();
 260  9
         final Iterator<Identifier> itr = identifiers.iterator();
 261  
 
 262  
         /* TODO - can we utilize the pom's groupid and artifactId to filter??? most of
 263  
          * these are due to low quality data.  Other idea would be to say any CPE
 264  
          * found based on LOW confidence evidence should have a different CPE type? (this
 265  
          * might be a better solution then just removing the URL for "best-guess" matches).
 266  
          */
 267  
 
 268  
         //Set<Evidence> groupId = dependency.getVendorEvidence().getEvidence("pom", "groupid");
 269  
         //Set<Evidence> artifactId = dependency.getVendorEvidence().getEvidence("pom", "artifactid");
 270  
 
 271  23
         while (itr.hasNext()) {
 272  14
             final Identifier i = itr.next();
 273  
             //TODO move this startswith expression to a configuration file?
 274  14
             if ("cpe".equals(i.getType())) {
 275  14
                 if ((i.getValue().matches(".*c\\+\\+.*")
 276  
                         || i.getValue().startsWith("cpe:/a:jquery:jquery")
 277  
                         || i.getValue().startsWith("cpe:/a:prototypejs:prototype")
 278  
                         || i.getValue().startsWith("cpe:/a:yahoo:yui")
 279  
                         || i.getValue().startsWith("cpe:/a:file:file")
 280  
                         || i.getValue().startsWith("cpe:/a:mozilla:mozilla")
 281  
                         || i.getValue().startsWith("cpe:/a:cvs:cvs")
 282  
                         || i.getValue().startsWith("cpe:/a:ftp:ftp")
 283  
                         || i.getValue().startsWith("cpe:/a:ssh:ssh"))
 284  
                         && dependency.getFileName().toLowerCase().endsWith(".jar")) {
 285  0
                     itr.remove();
 286  14
                 } else if (i.getValue().startsWith("cpe:/a:apache:maven")
 287  
                         && !dependency.getFileName().toLowerCase().matches("maven-core-[\\d\\.]+\\.jar")) {
 288  0
                     itr.remove();
 289  
                 }
 290  
             }
 291  14
         }
 292  9
     }
 293  
 
 294  
     /**
 295  
      * There are some known CPE entries, specifically regarding sun and oracle
 296  
      * products due to the acquisition and changes in product names, that based
 297  
      * on given evidence we can add the related CPE entries to ensure a complete
 298  
      * list of CVE entries.
 299  
      *
 300  
      * @param dependency the dependency being analyzed
 301  
      */
 302  
     private void addFalseNegativeCPEs(Dependency dependency) {
 303  9
         final Iterator<Identifier> itr = dependency.getIdentifiers().iterator();
 304  22
         while (itr.hasNext()) {
 305  13
             final Identifier i = itr.next();
 306  13
             if ("cpe".equals(i.getType()) && i.getValue() != null
 307  
                     && (i.getValue().startsWith("cpe:/a:oracle:opensso:")
 308  
                     || i.getValue().startsWith("cpe:/a:oracle:opensso_enterprise:")
 309  
                     || i.getValue().startsWith("cpe:/a:sun:opensso_enterprise:")
 310  
                     || i.getValue().startsWith("cpe:/a:sun:opensso:"))) {
 311  0
                 final String newCpe = String.format("cpe:/a:sun:opensso_enterprise:%s", i.getValue().substring(22));
 312  0
                 final String newCpe2 = String.format("cpe:/a:oracle:opensso_enterprise:%s", i.getValue().substring(22));
 313  0
                 final String newCpe3 = String.format("cpe:/a:sun:opensso:%s", i.getValue().substring(22));
 314  0
                 final String newCpe4 = String.format("cpe:/a:oracle:opensso:%s", i.getValue().substring(22));
 315  
                 try {
 316  0
                     dependency.addIdentifier("cpe",
 317  
                             newCpe,
 318  
                             String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe, "UTF-8")));
 319  0
                     dependency.addIdentifier("cpe",
 320  
                             newCpe2,
 321  
                             String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe2, "UTF-8")));
 322  0
                     dependency.addIdentifier("cpe",
 323  
                             newCpe3,
 324  
                             String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe3, "UTF-8")));
 325  0
                     dependency.addIdentifier("cpe",
 326  
                             newCpe4,
 327  
                             String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe4, "UTF-8")));
 328  0
                 } catch (UnsupportedEncodingException ex) {
 329  0
                     Logger.getLogger(FalsePositiveAnalyzer.class
 330  
                             .getName()).log(Level.FINE, null, ex);
 331  0
                 }
 332  
             }
 333  13
         }
 334  9
     }
 335  
 }