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