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