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