View Javadoc

    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  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      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      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          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          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          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          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         removeJreEntries(dependency);
    111         removeBadMatches(dependency);
    112         removeWrongVersionMatches(dependency);
    113         removeSpuriousCPE(dependency);
    114         addFalseNegativeCPEs(dependency);
    115     }
    116
    117     /**
    118      * <p>Intended to remove spurious CPE entries. By spurious we mean
    119      * duplicate, less specific CPE entries.</p>
    120      * <p>Example:</p>
    121      * <code>
    122      * cpe:/a:some-vendor:some-product
    123      * cpe:/a:some-vendor:some-product:1.5
    124      * cpe:/a:some-vendor:some-product:1.5.2
    125      * </code>
    126      * <p>Should be trimmed to:</p>
    127      * <code>
    128      * cpe:/a:some-vendor:some-product:1.5.2
    129      * </code>
    130      *
    131      * @param dependency the dependency being analyzed
    132      */
    133     @SuppressWarnings("null")
    134     private void removeSpuriousCPE(Dependency dependency) {
    135         final List<Identifier> ids = new ArrayList<Identifier>();
    136         ids.addAll(dependency.getIdentifiers());
    137         Collections.sort(ids);
    138         final ListIterator<Identifier> mainItr = ids.listIterator();
    139         while (mainItr.hasNext()) {
    140             final Identifier currentId = mainItr.next();
    141             final VulnerableSoftware currentCpe = parseCpe(currentId.getType(), currentId.getValue());
    142             if (currentCpe == null) {
    143                 continue;
    144             }
    145             final ListIterator<Identifier> subItr = ids.listIterator(mainItr.nextIndex());
    146             while (subItr.hasNext()) {
    147                 final Identifier nextId = subItr.next();
    148                 final VulnerableSoftware nextCpe = parseCpe(nextId.getType(), nextId.getValue());
    149                 if (nextCpe == null) {
    150                     continue;
    151                 }
    152                 //TODO fix the version problem below
    153                 if (currentCpe.getVendor().equals(nextCpe.getVendor())) {
    154                     if (currentCpe.getProduct().equals(nextCpe.getProduct())) {
    155                         // see if one is contained in the other.. remove the contained one from dependency.getIdentifier
    156                         final String currentVersion = currentCpe.getVersion();
    157                         final String nextVersion = nextCpe.getVersion();
    158                         if (currentVersion == null && nextVersion == null) {
    159                             //how did we get here?
    160                             Logger.getLogger(FalsePositiveAnalyzer.class
    161                                     .getName()).log(Level.FINE, "currentVersion and nextVersion are both null?");
    162                         } else if (currentVersion == null && nextVersion != null) {
    163                             dependency.getIdentifiers().remove(currentId);
    164                         } else if (nextVersion == null && currentVersion != null) {
    165                             dependency.getIdentifiers().remove(nextId);
    166                         } else if (currentVersion.length() < nextVersion.length()) {
    167                             if (nextVersion.startsWith(currentVersion) || "-".equals(currentVersion)) {
    168                                 dependency.getIdentifiers().remove(currentId);
    169                             }
    170                         } else {
    171                             if (currentVersion.startsWith(nextVersion) || "-".equals(nextVersion)) {
    172                                 dependency.getIdentifiers().remove(nextId);
    173                             }
    174                         }
    175                     }
    176                 }
    177             }
    178         }
    179     }
    180     /**
    181      * Regex to identify core java libraries and a few other commonly
    182      * misidentified ones.
    183      */
    184     public static final Pattern CORE_JAVA = Pattern.compile("^cpe:/a:(sun|oracle|ibm):(j2[ems]e|"
    185             + "java(_platfrom_micro_edition|_runtime_environment|_se|virtual_machine|se_development_kit|fx)?|"
    186             + "jdk|jre|jsf|jsse)($|:.*)");
    187     /**
    188      * Regex to identify core java library files. This is currently incomplete.
    189      */
    190     public static final Pattern CORE_FILES = Pattern.compile("^((alt[-])?rt|jsf[-].*|jsse|jfxrt|jfr|jce|javaws|deploy|charsets)\\.jar$");
    191
    192     /**
    193      * Removes any CPE entries for the JDK/JRE unless the filename ends with
    194      * rt.jar
    195      *
    196      * @param dependency the dependency to remove JRE CPEs from
    197      */
    198     private void removeJreEntries(Dependency dependency) {
    199         final Set<Identifier> identifiers = dependency.getIdentifiers();
    200         final Iterator<Identifier> itr = identifiers.iterator();
    201         while (itr.hasNext()) {
    202             final Identifier i = itr.next();
    203             final Matcher coreCPE = CORE_JAVA.matcher(i.getValue());
    204             final Matcher coreFiles = CORE_FILES.matcher(dependency.getFileName());
    205             if (coreCPE.matches() && !coreFiles.matches()) {
    206                 itr.remove();
    207             }
    208
    209             //replacecd with the regex above.
    210             //            if (("cpe:/a:sun:java".equals(i.getValue())
    211             //                    || "cpe:/a:oracle:java".equals(i.getValue())
    212             //                    || "cpe:/a:ibm:java".equals(i.getValue())
    213             //                    || "cpe:/a:sun:j2se".equals(i.getValue())
    214             //                    || "cpe:/a:oracle:j2se".equals(i.getValue())
    215             //                    || i.getValue().startsWith("cpe:/a:sun:java:")
    216             //                    || i.getValue().startsWith("cpe:/a:sun:j2se:")
    217             //                    || i.getValue().startsWith("cpe:/a:sun:java:jre")
    218             //                    || i.getValue().startsWith("cpe:/a:sun:java:jdk")
    219             //                    || i.getValue().startsWith("cpe:/a:sun:java_se")
    220             //                    || i.getValue().startsWith("cpe:/a:oracle:java_se")
    221             //                    || i.getValue().startsWith("cpe:/a:oracle:java:")
    222             //                    || i.getValue().startsWith("cpe:/a:oracle:j2se:")
    223             //                    || i.getValue().startsWith("cpe:/a:oracle:jre")
    224             //                    || i.getValue().startsWith("cpe:/a:oracle:jdk")
    225             //                    || i.getValue().startsWith("cpe:/a:ibm:java:"))
    226             //                    && !dependency.getFileName().toLowerCase().endsWith("rt.jar")) {
    227             //                itr.remove();
    228             //            }
    229         }
    230     }
    231
    232     /**
    233      * Parses a CPE string into an IndexEntry.
    234      *
    235      * @param type the type of identifier
    236      * @param value the cpe identifier to parse
    237      * @return an VulnerableSoftware object constructed from the identifier
    238      */
    239     private VulnerableSoftware parseCpe(String type, String value) {
    240         if (!"cpe".equals(type)) {
    241             return null;
    242         }
    243         final VulnerableSoftware cpe = new VulnerableSoftware();
    244         try {
    245             cpe.parseName(value);
    246         } catch (UnsupportedEncodingException ex) {
    247             Logger.getLogger(FalsePositiveAnalyzer.class.getName()).log(Level.FINEST, null, ex);
    248             return null;
    249         }
    250         return cpe;
    251     }
    252
    253     /**
    254      * Removes bad CPE matches for a dependency. Unfortunately, right now these
    255      * are hard-coded patches for specific problems identified when testing this
    256      * on a LARGE volume of jar files.
    257      *
    258      * @param dependency the dependency to analyze
    259      */
    260     private void removeBadMatches(Dependency dependency) {
    261         final Set<Identifier> identifiers = dependency.getIdentifiers();
    262         final Iterator<Identifier> itr = identifiers.iterator();
    263
    264         /* TODO - can we utilize the pom's groupid and artifactId to filter??? most of
    265          * these are due to low quality data.  Other idea would be to say any CPE
    266          * found based on LOW confidence evidence should have a different CPE type? (this
    267          * might be a better solution then just removing the URL for "best-guess" matches).
    268          */
    269
    270         //Set<Evidence> groupId = dependency.getVendorEvidence().getEvidence("pom", "groupid");
    271         //Set<Evidence> artifactId = dependency.getVendorEvidence().getEvidence("pom", "artifactid");
    272
    273         while (itr.hasNext()) {
    274             final Identifier i = itr.next();
    275             //TODO move this startswith expression to a configuration file?
    276             if ("cpe".equals(i.getType())) {
    277                 if ((i.getValue().matches(".*c\\+\\+.*")
    278                         || i.getValue().startsWith("cpe:/a:jquery:jquery")
    279                         || i.getValue().startsWith("cpe:/a:prototypejs:prototype")
    280                         || i.getValue().startsWith("cpe:/a:yahoo:yui")
    281                         || i.getValue().startsWith("cpe:/a:file:file")
    282                         || i.getValue().startsWith("cpe:/a:mozilla:mozilla")
    283                         || i.getValue().startsWith("cpe:/a:cvs:cvs")
    284                         || i.getValue().startsWith("cpe:/a:ftp:ftp")
    285                         || i.getValue().startsWith("cpe:/a:ssh:ssh"))
    286                         && dependency.getFileName().toLowerCase().endsWith(".jar")) {
    287                     itr.remove();
    288                 } else if (i.getValue().startsWith("cpe:/a:apache:maven")
    289                         && !dependency.getFileName().toLowerCase().matches("maven-core-[\\d\\.]+\\.jar")) {
    290                     itr.remove();
    291                 }
    292             }
    293         }
    294     }
    295
    296     /**
    297      * Removes CPE matches for the wrong version of a dependency. Currently,
    298      * this only covers Axis 1 & 2.
    299      *
    300      * @param dependency the dependency to analyze
    301      */
    302     private void removeWrongVersionMatches(Dependency dependency) {
    303         final Set<Identifier> identifiers = dependency.getIdentifiers();
    304         final Iterator<Identifier> itr = identifiers.iterator();
    305
    306         final String fileName = dependency.getFileName();
    307         if (fileName != null && fileName.contains("axis2")) {
    308             while (itr.hasNext()) {
    309                 final Identifier i = itr.next();
    310                 if ("cpe".equals(i.getType())) {
    311                     final String cpe = i.getValue();
    312                     if (cpe != null && (cpe.startsWith("cpe:/a:apache:axis:") || "cpe:/a:apache:axis".equals(cpe))) {
    313                         itr.remove();
    314                     }
    315                 }
    316             }
    317         } else if (fileName != null && fileName.contains("axis")) {
    318             while (itr.hasNext()) {
    319                 final Identifier i = itr.next();
    320                 if ("cpe".equals(i.getType())) {
    321                     final String cpe = i.getValue();
    322                     if (cpe != null && (cpe.startsWith("cpe:/a:apache:axis2:") || "cpe:/a:apache:axis2".equals(cpe))) {
    323                         itr.remove();
    324                     }
    325                 }
    326             }
    327         }
    328     }
    329
    330     /**
    331      * There are some known CPE entries, specifically regarding sun and oracle
    332      * products due to the acquisition and changes in product names, that based
    333      * on given evidence we can add the related CPE entries to ensure a complete
    334      * list of CVE entries.
    335      *
    336      * @param dependency the dependency being analyzed
    337      */
    338     private void addFalseNegativeCPEs(Dependency dependency) {
    339         final Iterator<Identifier> itr = dependency.getIdentifiers().iterator();
    340         while (itr.hasNext()) {
    341             final Identifier i = itr.next();
    342             if ("cpe".equals(i.getType()) && i.getValue() != null
    343                     && (i.getValue().startsWith("cpe:/a:oracle:opensso:")
    344                     || i.getValue().startsWith("cpe:/a:oracle:opensso_enterprise:")
    345                     || i.getValue().startsWith("cpe:/a:sun:opensso_enterprise:")
    346                     || i.getValue().startsWith("cpe:/a:sun:opensso:"))) {
    347                 final String newCpe = String.format("cpe:/a:sun:opensso_enterprise:%s", i.getValue().substring(22));
    348                 final String newCpe2 = String.format("cpe:/a:oracle:opensso_enterprise:%s", i.getValue().substring(22));
    349                 final String newCpe3 = String.format("cpe:/a:sun:opensso:%s", i.getValue().substring(22));
    350                 final String newCpe4 = String.format("cpe:/a:oracle:opensso:%s", i.getValue().substring(22));
    351                 try {
    352                     dependency.addIdentifier("cpe",
    353                             newCpe,
    354                             String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe, "UTF-8")));
    355                     dependency.addIdentifier("cpe",
    356                             newCpe2,
    357                             String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe2, "UTF-8")));
    358                     dependency.addIdentifier("cpe",
    359                             newCpe3,
    360                             String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe3, "UTF-8")));
    361                     dependency.addIdentifier("cpe",
    362                             newCpe4,
    363                             String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(newCpe4, "UTF-8")));
    364                 } catch (UnsupportedEncodingException ex) {
    365                     Logger.getLogger(FalsePositiveAnalyzer.class
    366                             .getName()).log(Level.FINE, null, ex);
    367                 }
    368             }
    369         }
    370     }
    371 }