Coverage Report - org.owasp.dependencycheck.analyzer.CPEAnalyzer
 
Classes in this File Line Coverage Branch Coverage Complexity
CPEAnalyzer
80%
193/240
76%
102/134
4.433
CPEAnalyzer$IdentifierConfidence
100%
4/4
N/A
4.433
CPEAnalyzer$IdentifierMatch
36%
14/38
0%
0/20
4.433
 
 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.IOException;
 21  
 import java.io.UnsupportedEncodingException;
 22  
 import java.net.URLEncoder;
 23  
 import java.util.ArrayList;
 24  
 import java.util.Collections;
 25  
 import java.util.List;
 26  
 import java.util.Set;
 27  
 import java.util.StringTokenizer;
 28  
 import java.util.concurrent.TimeUnit;
 29  
 import org.apache.commons.lang3.builder.CompareToBuilder;
 30  
 import org.apache.lucene.document.Document;
 31  
 import org.apache.lucene.index.CorruptIndexException;
 32  
 import org.apache.lucene.queryparser.classic.ParseException;
 33  
 import org.apache.lucene.search.ScoreDoc;
 34  
 import org.apache.lucene.search.TopDocs;
 35  
 import org.owasp.dependencycheck.Engine;
 36  
 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
 37  
 import org.owasp.dependencycheck.data.cpe.CpeMemoryIndex;
 38  
 import org.owasp.dependencycheck.data.cpe.Fields;
 39  
 import org.owasp.dependencycheck.data.cpe.IndexEntry;
 40  
 import org.owasp.dependencycheck.data.cpe.IndexException;
 41  
 import org.owasp.dependencycheck.data.lucene.LuceneUtils;
 42  
 import org.owasp.dependencycheck.data.nvdcve.CveDB;
 43  
 import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
 44  
 import org.owasp.dependencycheck.dependency.Confidence;
 45  
 import org.owasp.dependencycheck.dependency.Dependency;
 46  
 import org.owasp.dependencycheck.dependency.Evidence;
 47  
 import org.owasp.dependencycheck.dependency.EvidenceCollection;
 48  
 import org.owasp.dependencycheck.dependency.Identifier;
 49  
 import org.owasp.dependencycheck.dependency.VulnerableSoftware;
 50  
 import org.owasp.dependencycheck.exception.InitializationException;
 51  
 import org.owasp.dependencycheck.utils.DependencyVersion;
 52  
 import org.owasp.dependencycheck.utils.DependencyVersionUtil;
 53  
 import org.owasp.dependencycheck.utils.Settings;
 54  
 import org.slf4j.Logger;
 55  
 import org.slf4j.LoggerFactory;
 56  
 
 57  
 /**
 58  
  * CPEAnalyzer is a utility class that takes a project dependency and attempts
 59  
  * to discern if there is an associated CPE. It uses the evidence contained
 60  
  * within the dependency to search the Lucene index.
 61  
  *
 62  
  * @author Jeremy Long
 63  
  */
 64  8
 public class CPEAnalyzer extends AbstractAnalyzer {
 65  
 
 66  
     /**
 67  
      * The Logger.
 68  
      */
 69  1
     private static final Logger LOGGER = LoggerFactory.getLogger(CPEAnalyzer.class);
 70  
     /**
 71  
      * The maximum number of query results to return.
 72  
      */
 73  
     static final int MAX_QUERY_RESULTS = 25;
 74  
     /**
 75  
      * The weighting boost to give terms when constructing the Lucene query.
 76  
      */
 77  
     static final String WEIGHTING_BOOST = "^5";
 78  
     /**
 79  
      * A string representation of a regular expression defining characters
 80  
      * utilized within the CPE Names.
 81  
      */
 82  
     static final String CLEANSE_CHARACTER_RX = "[^A-Za-z0-9 ._-]";
 83  
     /**
 84  
      * A string representation of a regular expression used to remove all but
 85  
      * alpha characters.
 86  
      */
 87  
     static final String CLEANSE_NONALPHA_RX = "[^A-Za-z]*";
 88  
     /**
 89  
      * The additional size to add to a new StringBuilder to account for extra
 90  
      * data that will be written into the string.
 91  
      */
 92  
     static final int STRING_BUILDER_BUFFER = 20;
 93  
     /**
 94  
      * The CPE in memory index.
 95  
      */
 96  
     private CpeMemoryIndex cpe;
 97  
     /**
 98  
      * The CVE Database.
 99  
      */
 100  
     private CveDB cve;
 101  
 
 102  
     /**
 103  
      * The URL to perform a search of the NVD CVE data at NIST.
 104  
      */
 105  
     public static final String NVD_SEARCH_URL = "https://web.nvd.nist.gov/view/vuln/search-results?adv_search=true&cves=on&cpe_version=%s";
 106  
 
 107  
     /**
 108  
      * Returns the name of this analyzer.
 109  
      *
 110  
      * @return the name of this analyzer.
 111  
      */
 112  
     @Override
 113  
     public String getName() {
 114  26
         return "CPE Analyzer";
 115  
     }
 116  
 
 117  
     /**
 118  
      * Returns the analysis phase that this analyzer should run in.
 119  
      *
 120  
      * @return the analysis phase that this analyzer should run in.
 121  
      */
 122  
     @Override
 123  
     public AnalysisPhase getAnalysisPhase() {
 124  6
         return AnalysisPhase.IDENTIFIER_ANALYSIS;
 125  
     }
 126  
     /**
 127  
      * The default is to support parallel processing.
 128  
      * @return false
 129  
      */
 130  
     @Override
 131  
     public boolean supportsParallelProcessing() {
 132  2
         return false;
 133  
     }
 134  
     /**
 135  
      * Creates the CPE Lucene Index.
 136  
      *
 137  
      * @throws InitializationException is thrown if there is an issue opening
 138  
      * the index.
 139  
      */
 140  
     @Override
 141  
     public void initializeAnalyzer() throws InitializationException {
 142  
         try {
 143  2
             this.open();
 144  0
         } catch (IOException ex) {
 145  0
             LOGGER.debug("Exception initializing the Lucene Index", ex);
 146  0
             throw new InitializationException("An exception occurred initializing the Lucene Index", ex);
 147  0
         } catch (DatabaseException ex) {
 148  0
             LOGGER.debug("Exception accessing the database", ex);
 149  0
             throw new InitializationException("An exception occurred accessing the database", ex);
 150  2
         }
 151  2
     }
 152  
 
 153  
     /**
 154  
      * Opens the data source.
 155  
      *
 156  
      * @throws IOException when the Lucene directory to be queried does not
 157  
      * exist or is corrupt.
 158  
      * @throws DatabaseException when the database throws an exception. This
 159  
      * usually occurs when the database is in use by another process.
 160  
      */
 161  
     public void open() throws IOException, DatabaseException {
 162  2
         if (!isOpen()) {
 163  2
             cve = new CveDB();
 164  2
             cve.open();
 165  2
             cpe = CpeMemoryIndex.getInstance();
 166  
             try {
 167  2
                 final long creationStart = System.currentTimeMillis();
 168  2
                 cpe.open(cve);
 169  2
                 final long creationSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - creationStart);
 170  2
                 LOGGER.info("Created CPE Index ({} seconds)", creationSeconds);
 171  0
             } catch (IndexException ex) {
 172  0
                 LOGGER.debug("IndexException", ex);
 173  0
                 throw new DatabaseException(ex);
 174  2
             }
 175  
         }
 176  2
     }
 177  
 
 178  
     /**
 179  
      * Closes the data sources.
 180  
      */
 181  
     @Override
 182  
     public void closeAnalyzer() {
 183  2
         if (cpe != null) {
 184  2
             cpe.close();
 185  2
             cpe = null;
 186  
         }
 187  2
         if (cve != null) {
 188  2
             cve.close();
 189  2
             cve = null;
 190  
         }
 191  2
     }
 192  
 
 193  
     public boolean isOpen() {
 194  2
         return cpe != null && cpe.isOpen();
 195  
     }
 196  
 
 197  
     /**
 198  
      * Searches the data store of CPE entries, trying to identify the CPE for
 199  
      * the given dependency based on the evidence contained within. The
 200  
      * dependency passed in is updated with any identified CPE values.
 201  
      *
 202  
      * @param dependency the dependency to search for CPE entries on.
 203  
      * @throws CorruptIndexException is thrown when the Lucene index is corrupt.
 204  
      * @throws IOException is thrown when an IOException occurs.
 205  
      * @throws ParseException is thrown when the Lucene query cannot be parsed.
 206  
      */
 207  
     protected void determineCPE(Dependency dependency) throws CorruptIndexException, IOException, ParseException {
 208  
         //TODO test dojo-war against this. we shold get dojo-toolkit:dojo-toolkit AND dojo-toolkit:toolkit
 209  4
         String vendors = "";
 210  4
         String products = "";
 211  17
         for (Confidence confidence : Confidence.values()) {
 212  14
             if (dependency.getVendorEvidence().contains(confidence)) {
 213  9
                 vendors = addEvidenceWithoutDuplicateTerms(vendors, dependency.getVendorEvidence(), confidence);
 214  9
                 LOGGER.debug("vendor search: {}", vendors);
 215  
             }
 216  14
             if (dependency.getProductEvidence().contains(confidence)) {
 217  8
                 products = addEvidenceWithoutDuplicateTerms(products, dependency.getProductEvidence(), confidence);
 218  8
                 LOGGER.debug("product search: {}", products);
 219  
             }
 220  14
             if (!vendors.isEmpty() && !products.isEmpty()) {
 221  22
                 final List<IndexEntry> entries = searchCPE(vendors, products, dependency.getVendorEvidence().getWeighting(),
 222  11
                         dependency.getProductEvidence().getWeighting());
 223  11
                 if (entries == null) {
 224  0
                     continue;
 225  
                 }
 226  11
                 boolean identifierAdded = false;
 227  11
                 for (IndexEntry e : entries) {
 228  21
                     LOGGER.debug("Verifying entry: {}", e);
 229  21
                     if (verifyEntry(e, dependency)) {
 230  4
                         final String vendor = e.getVendor();
 231  4
                         final String product = e.getProduct();
 232  4
                         LOGGER.debug("identified vendor/product: {}/{}", vendor, product);
 233  4
                         identifierAdded |= determineIdentifiers(dependency, vendor, product, confidence);
 234  
                     }
 235  21
                 }
 236  11
                 if (identifierAdded) {
 237  1
                     break;
 238  
                 }
 239  
             }
 240  
         }
 241  4
     }
 242  
 
 243  
     /**
 244  
      * Returns the text created by concatenating the text and the values from
 245  
      * the EvidenceCollection (filtered for a specific confidence). This
 246  
      * attempts to prevent duplicate terms from being added.<br/<br/> Note, if
 247  
      * the evidence is longer then 200 characters it will be truncated.
 248  
      *
 249  
      * @param text the base text.
 250  
      * @param ec an EvidenceCollection
 251  
      * @param confidenceFilter a Confidence level to filter the evidence by.
 252  
      * @return the new evidence text
 253  
      */
 254  
     private String addEvidenceWithoutDuplicateTerms(final String text, final EvidenceCollection ec, Confidence confidenceFilter) {
 255  17
         final String txt = (text == null) ? "" : text;
 256  17
         final StringBuilder sb = new StringBuilder(txt.length() + (20 * ec.size()));
 257  17
         sb.append(' ').append(txt).append(' ');
 258  17
         for (Evidence e : ec.iterator(confidenceFilter)) {
 259  36
             String value = e.getValue();
 260  
 
 261  
             //hack to get around the fact that lucene does a really good job of recognizing domains and not
 262  
             // splitting them. TODO - put together a better lucene analyzer specific to the domain.
 263  36
             if (value.startsWith("http://")) {
 264  2
                 value = value.substring(7).replaceAll("\\.", " ");
 265  
             }
 266  36
             if (value.startsWith("https://")) {
 267  1
                 value = value.substring(8).replaceAll("\\.", " ");
 268  
             }
 269  36
             if (sb.indexOf(" " + value + " ") < 0) {
 270  34
                 sb.append(value).append(' ');
 271  
             }
 272  36
         }
 273  17
         return sb.toString().trim();
 274  
     }
 275  
 
 276  
     /**
 277  
      * <p>
 278  
      * Searches the Lucene CPE index to identify possible CPE entries associated
 279  
      * with the supplied vendor, product, and version.</p>
 280  
      *
 281  
      * <p>
 282  
      * If either the vendorWeightings or productWeightings lists have been
 283  
      * populated this data is used to add weighting factors to the search.</p>
 284  
      *
 285  
      * @param vendor the text used to search the vendor field
 286  
      * @param product the text used to search the product field
 287  
      * @param vendorWeightings a list of strings to use to add weighting factors
 288  
      * to the vendor field
 289  
      * @param productWeightings Adds a list of strings that will be used to add
 290  
      * weighting factors to the product search
 291  
      * @return a list of possible CPE values
 292  
      */
 293  
     protected List<IndexEntry> searchCPE(String vendor, String product,
 294  
             Set<String> vendorWeightings, Set<String> productWeightings) {
 295  
 
 296  11
         final List<IndexEntry> ret = new ArrayList<IndexEntry>(MAX_QUERY_RESULTS);
 297  
 
 298  11
         final String searchString = buildSearch(vendor, product, vendorWeightings, productWeightings);
 299  11
         if (searchString == null) {
 300  0
             return ret;
 301  
         }
 302  
         try {
 303  11
             final TopDocs docs = cpe.search(searchString, MAX_QUERY_RESULTS);
 304  76
             for (ScoreDoc d : docs.scoreDocs) {
 305  65
                 if (d.score >= 0.08) {
 306  21
                     final Document doc = cpe.getDocument(d.doc);
 307  21
                     final IndexEntry entry = new IndexEntry();
 308  21
                     entry.setVendor(doc.get(Fields.VENDOR));
 309  21
                     entry.setProduct(doc.get(Fields.PRODUCT));
 310  21
                     entry.setSearchScore(d.score);
 311  21
                     if (!ret.contains(entry)) {
 312  21
                         ret.add(entry);
 313  
                     }
 314  
                 }
 315  
             }
 316  11
             return ret;
 317  0
         } catch (ParseException ex) {
 318  0
             LOGGER.warn("An error occurred querying the CPE data. See the log for more details.");
 319  0
             LOGGER.info("Unable to parse: {}", searchString, ex);
 320  0
         } catch (IOException ex) {
 321  0
             LOGGER.warn("An error occurred reading CPE data. See the log for more details.");
 322  0
             LOGGER.info("IO Error with search string: {}", searchString, ex);
 323  0
         }
 324  0
         return null;
 325  
     }
 326  
 
 327  
     /**
 328  
      * <p>
 329  
      * Builds a Lucene search string by properly escaping data and constructing
 330  
      * a valid search query.</p>
 331  
      *
 332  
      * <p>
 333  
      * If either the possibleVendor or possibleProducts lists have been
 334  
      * populated this data is used to add weighting factors to the search string
 335  
      * generated.</p>
 336  
      *
 337  
      * @param vendor text to search the vendor field
 338  
      * @param product text to search the product field
 339  
      * @param vendorWeighting a list of strings to apply to the vendor to boost
 340  
      * the terms weight
 341  
      * @param productWeightings a list of strings to apply to the product to
 342  
      * boost the terms weight
 343  
      * @return the Lucene query
 344  
      */
 345  
     protected String buildSearch(String vendor, String product,
 346  
             Set<String> vendorWeighting, Set<String> productWeightings) {
 347  11
         final String v = vendor; //.replaceAll("[^\\w\\d]", " ");
 348  11
         final String p = product; //.replaceAll("[^\\w\\d]", " ");
 349  11
         final StringBuilder sb = new StringBuilder(v.length() + p.length()
 350  11
                 + Fields.PRODUCT.length() + Fields.VENDOR.length() + STRING_BUILDER_BUFFER);
 351  
 
 352  11
         if (!appendWeightedSearch(sb, Fields.PRODUCT, p, productWeightings)) {
 353  0
             return null;
 354  
         }
 355  11
         sb.append(" AND ");
 356  11
         if (!appendWeightedSearch(sb, Fields.VENDOR, v, vendorWeighting)) {
 357  0
             return null;
 358  
         }
 359  11
         return sb.toString();
 360  
     }
 361  
 
 362  
     /**
 363  
      * This method constructs a Lucene query for a given field. The searchText
 364  
      * is split into separate words and if the word is within the list of
 365  
      * weighted words then an additional weighting is applied to the term as it
 366  
      * is appended into the query.
 367  
      *
 368  
      * @param sb a StringBuilder that the query text will be appended to.
 369  
      * @param field the field within the Lucene index that the query is
 370  
      * searching.
 371  
      * @param searchText text used to construct the query.
 372  
      * @param weightedText a list of terms that will be considered higher
 373  
      * importance when searching.
 374  
      * @return if the append was successful.
 375  
      */
 376  
     private boolean appendWeightedSearch(StringBuilder sb, String field, String searchText, Set<String> weightedText) {
 377  22
         sb.append(' ').append(field).append(":( ");
 378  
 
 379  22
         final String cleanText = cleanseText(searchText);
 380  
 
 381  22
         if (cleanText.isEmpty()) {
 382  0
             return false;
 383  
         }
 384  
 
 385  22
         if (weightedText == null || weightedText.isEmpty()) {
 386  14
             LuceneUtils.appendEscapedLuceneQuery(sb, cleanText);
 387  
         } else {
 388  8
             final StringTokenizer tokens = new StringTokenizer(cleanText);
 389  103
             while (tokens.hasMoreElements()) {
 390  95
                 final String word = tokens.nextToken();
 391  95
                 StringBuilder temp = null;
 392  95
                 for (String weighted : weightedText) {
 393  225
                     final String weightedStr = cleanseText(weighted);
 394  225
                     if (equalsIgnoreCaseAndNonAlpha(word, weightedStr)) {
 395  7
                         temp = new StringBuilder(word.length() + 2);
 396  7
                         LuceneUtils.appendEscapedLuceneQuery(temp, word);
 397  7
                         temp.append(WEIGHTING_BOOST);
 398  7
                         if (!word.equalsIgnoreCase(weightedStr)) {
 399  0
                             temp.append(' ');
 400  0
                             LuceneUtils.appendEscapedLuceneQuery(temp, weightedStr);
 401  0
                             temp.append(WEIGHTING_BOOST);
 402  
                         }
 403  
                         break;
 404  
                     }
 405  218
                 }
 406  95
                 sb.append(' ');
 407  95
                 if (temp == null) {
 408  88
                     LuceneUtils.appendEscapedLuceneQuery(sb, word);
 409  
                 } else {
 410  7
                     sb.append(temp);
 411  
                 }
 412  95
             }
 413  
         }
 414  22
         sb.append(" ) ");
 415  22
         return true;
 416  
     }
 417  
 
 418  
     /**
 419  
      * Removes characters from the input text that are not used within the CPE
 420  
      * index.
 421  
      *
 422  
      * @param text is the text to remove the characters from.
 423  
      * @return the text having removed some characters.
 424  
      */
 425  
     private String cleanseText(String text) {
 426  247
         return text.replaceAll(CLEANSE_CHARACTER_RX, " ");
 427  
     }
 428  
 
 429  
     /**
 430  
      * Compares two strings after lower casing them and removing the non-alpha
 431  
      * characters.
 432  
      *
 433  
      * @param l string one to compare.
 434  
      * @param r string two to compare.
 435  
      * @return whether or not the two strings are similar.
 436  
      */
 437  
     private boolean equalsIgnoreCaseAndNonAlpha(String l, String r) {
 438  225
         if (l == null || r == null) {
 439  0
             return false;
 440  
         }
 441  
 
 442  225
         final String left = l.replaceAll(CLEANSE_NONALPHA_RX, "");
 443  225
         final String right = r.replaceAll(CLEANSE_NONALPHA_RX, "");
 444  225
         return left.equalsIgnoreCase(right);
 445  
     }
 446  
 
 447  
     /**
 448  
      * Ensures that the CPE Identified matches the dependency. This validates
 449  
      * that the product, vendor, and version information for the CPE are
 450  
      * contained within the dependencies evidence.
 451  
      *
 452  
      * @param entry a CPE entry.
 453  
      * @param dependency the dependency that the CPE entries could be for.
 454  
      * @return whether or not the entry is valid.
 455  
      */
 456  
     private boolean verifyEntry(final IndexEntry entry, final Dependency dependency) {
 457  21
         boolean isValid = false;
 458  
 
 459  
         //TODO - does this nullify some of the fuzzy matching that happens in the lucene search?
 460  
         // for instance CPE some-component and in the evidence we have SomeComponent.
 461  21
         if (collectionContainsString(dependency.getProductEvidence(), entry.getProduct())
 462  4
                 && collectionContainsString(dependency.getVendorEvidence(), entry.getVendor())) {
 463  
             //&& collectionContainsVersion(dependency.getVersionEvidence(), entry.getVersion())
 464  4
             isValid = true;
 465  
         }
 466  21
         return isValid;
 467  
     }
 468  
 
 469  
     /**
 470  
      * Used to determine if the EvidenceCollection contains a specific string.
 471  
      *
 472  
      * @param ec an EvidenceCollection
 473  
      * @param text the text to search for
 474  
      * @return whether or not the EvidenceCollection contains the string
 475  
      */
 476  
     private boolean collectionContainsString(EvidenceCollection ec, String text) {
 477  
         //TODO - likely need to change the split... not sure if this will work for CPE with special chars
 478  25
         if (text == null) {
 479  0
             return false;
 480  
         }
 481  25
         final String[] words = text.split("[\\s_-]");
 482  25
         final List<String> list = new ArrayList<String>();
 483  25
         String tempWord = null;
 484  88
         for (String word : words) {
 485  
             /*
 486  
              single letter words should be concatenated with the next word.
 487  
              so { "m", "core", "sample" } -> { "mcore", "sample" }
 488  
              */
 489  63
             if (tempWord != null) {
 490  2
                 list.add(tempWord + word);
 491  2
                 tempWord = null;
 492  61
             } else if (word.length() <= 2) {
 493  2
                 tempWord = word;
 494  
             } else {
 495  59
                 list.add(word);
 496  
             }
 497  
         }
 498  25
         if (tempWord != null) {
 499  0
             if (!list.isEmpty()) {
 500  0
                 final String tmp = list.get(list.size() - 1) + tempWord;
 501  0
                 list.add(tmp);
 502  0
             } else {
 503  0
                 list.add(tempWord);
 504  
             }
 505  
         }
 506  25
         if (list.isEmpty()) {
 507  0
             return false;
 508  
         }
 509  25
         boolean contains = true;
 510  25
         for (String word : list) {
 511  61
             contains &= ec.containsUsedString(word);
 512  61
         }
 513  25
         return contains;
 514  
     }
 515  
 
 516  
     /**
 517  
      * Analyzes a dependency and attempts to determine if there are any CPE
 518  
      * identifiers for this dependency.
 519  
      *
 520  
      * @param dependency The Dependency to analyze.
 521  
      * @param engine The analysis engine
 522  
      * @throws AnalysisException is thrown if there is an issue analyzing the
 523  
      * dependency.
 524  
      */
 525  
     @Override
 526  
     protected synchronized void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
 527  
         try {
 528  4
             determineCPE(dependency);
 529  0
         } catch (CorruptIndexException ex) {
 530  0
             throw new AnalysisException("CPE Index is corrupt.", ex);
 531  0
         } catch (IOException ex) {
 532  0
             throw new AnalysisException("Failure opening the CPE Index.", ex);
 533  0
         } catch (ParseException ex) {
 534  0
             throw new AnalysisException("Unable to parse the generated Lucene query for this dependency.", ex);
 535  4
         }
 536  4
     }
 537  
 
 538  
     /**
 539  
      * Retrieves a list of CPE values from the CveDB based on the vendor and
 540  
      * product passed in. The list is then validated to find only CPEs that are
 541  
      * valid for the given dependency. It is possible that the CPE identified is
 542  
      * a best effort "guess" based on the vendor, product, and version
 543  
      * information.
 544  
      *
 545  
      * @param dependency the Dependency being analyzed
 546  
      * @param vendor the vendor for the CPE being analyzed
 547  
      * @param product the product for the CPE being analyzed
 548  
      * @param currentConfidence the current confidence being used during
 549  
      * analysis
 550  
      * @return <code>true</code> if an identifier was added to the dependency;
 551  
      * otherwise <code>false</code>
 552  
      * @throws UnsupportedEncodingException is thrown if UTF-8 is not supported
 553  
      */
 554  
     protected boolean determineIdentifiers(Dependency dependency, String vendor, String product,
 555  
             Confidence currentConfidence) throws UnsupportedEncodingException {
 556  4
         final Set<VulnerableSoftware> cpes = cve.getCPEs(vendor, product);
 557  4
         DependencyVersion bestGuess = new DependencyVersion("-");
 558  4
         Confidence bestGuessConf = null;
 559  4
         boolean hasBroadMatch = false;
 560  4
         final List<IdentifierMatch> collected = new ArrayList<IdentifierMatch>();
 561  
 
 562  
         //TODO the following algorithm incorrectly identifies things as a lower version
 563  
         // if there lower confidence evidence when the current (highest) version number
 564  
         // is newer then anything in the NVD.
 565  20
         for (Confidence conf : Confidence.values()) {
 566  16
             for (Evidence evidence : dependency.getVersionEvidence().iterator(conf)) {
 567  16
                 final DependencyVersion evVer = DependencyVersionUtil.parseVersion(evidence.getValue());
 568  16
                 if (evVer == null) {
 569  4
                     continue;
 570  
                 }
 571  12
                 for (VulnerableSoftware vs : cpes) {
 572  
                     DependencyVersion dbVer;
 573  444
                     if (vs.getUpdate() != null && !vs.getUpdate().isEmpty()) {
 574  96
                         dbVer = DependencyVersionUtil.parseVersion(vs.getVersion() + '.' + vs.getUpdate());
 575  
                     } else {
 576  348
                         dbVer = DependencyVersionUtil.parseVersion(vs.getVersion());
 577  
                     }
 578  444
                     if (dbVer == null) { //special case, no version specified - everything is vulnerable
 579  0
                         hasBroadMatch = true;
 580  0
                         final String url = String.format(NVD_SEARCH_URL, URLEncoder.encode(vs.getName(), "UTF-8"));
 581  0
                         final IdentifierMatch match = new IdentifierMatch("cpe", vs.getName(), url, IdentifierConfidence.BROAD_MATCH, conf);
 582  0
                         collected.add(match);
 583  0
                     } else if (evVer.equals(dbVer)) { //yeah! exact match
 584  6
                         final String url = String.format(NVD_SEARCH_URL, URLEncoder.encode(vs.getName(), "UTF-8"));
 585  6
                         final IdentifierMatch match = new IdentifierMatch("cpe", vs.getName(), url, IdentifierConfidence.EXACT_MATCH, conf);
 586  6
                         collected.add(match);
 587  
 
 588  
                         //TODO the following isn't quite right is it? need to think about this guessing game a bit more.
 589  6
                     } else if (evVer.getVersionParts().size() <= dbVer.getVersionParts().size()
 590  432
                             && evVer.matchesAtLeastThreeLevels(dbVer)) {
 591  48
                         if (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0) {
 592  2
                             if (bestGuess.getVersionParts().size() < dbVer.getVersionParts().size()) {
 593  2
                                 bestGuess = dbVer;
 594  2
                                 bestGuessConf = conf;
 595  
                             }
 596  
                         }
 597  
                     }
 598  444
                 }
 599  12
                 if (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0) {
 600  2
                     if (bestGuess.getVersionParts().size() < evVer.getVersionParts().size()) {
 601  2
                         bestGuess = evVer;
 602  2
                         bestGuessConf = conf;
 603  
                     }
 604  
                 }
 605  12
             }
 606  
         }
 607  4
         final String cpeName = String.format("cpe:/a:%s:%s:%s", vendor, product, bestGuess.toString());
 608  4
         String url = null;
 609  4
         if (hasBroadMatch) { //if we have a broad match we can add the URL to the best guess.
 610  0
             final String cpeUrlName = String.format("cpe:/a:%s:%s", vendor, product);
 611  0
             url = String.format(NVD_SEARCH_URL, URLEncoder.encode(cpeUrlName, "UTF-8"));
 612  
         }
 613  4
         if (bestGuessConf == null) {
 614  0
             bestGuessConf = Confidence.LOW;
 615  
         }
 616  4
         final IdentifierMatch match = new IdentifierMatch("cpe", cpeName, url, IdentifierConfidence.BEST_GUESS, bestGuessConf);
 617  4
         collected.add(match);
 618  
 
 619  4
         Collections.sort(collected);
 620  4
         final IdentifierConfidence bestIdentifierQuality = collected.get(0).getConfidence();
 621  4
         final Confidence bestEvidenceQuality = collected.get(0).getEvidenceConfidence();
 622  4
         boolean identifierAdded = false;
 623  4
         for (IdentifierMatch m : collected) {
 624  10
             if (bestIdentifierQuality.equals(m.getConfidence())
 625  8
                     && bestEvidenceQuality.equals(m.getEvidenceConfidence())) {
 626  4
                 final Identifier i = m.getIdentifier();
 627  4
                 if (bestIdentifierQuality == IdentifierConfidence.BEST_GUESS) {
 628  2
                     i.setConfidence(Confidence.LOW);
 629  
                 } else {
 630  2
                     i.setConfidence(bestEvidenceQuality);
 631  
                 }
 632  4
                 dependency.addIdentifier(i);
 633  4
                 identifierAdded = true;
 634  
             }
 635  10
         }
 636  4
         return identifierAdded;
 637  
     }
 638  
 
 639  
     /**
 640  
      * <p>
 641  
      * Returns the setting key to determine if the analyzer is enabled.</p>
 642  
      *
 643  
      * @return the key for the analyzer's enabled property
 644  
      */
 645  
     @Override
 646  
     protected String getAnalyzerEnabledSettingKey() {
 647  2
         return Settings.KEYS.ANALYZER_CPE_ENABLED;
 648  
     }
 649  
 
 650  
     /**
 651  
      * The confidence whether the identifier is an exact match, or a best guess.
 652  
      */
 653  4
     private enum IdentifierConfidence {
 654  
 
 655  
         /**
 656  
          * An exact match for the CPE.
 657  
          */
 658  1
         EXACT_MATCH,
 659  
         /**
 660  
          * A best guess for the CPE.
 661  
          */
 662  1
         BEST_GUESS,
 663  
         /**
 664  
          * The entire vendor/product group must be added (without a guess at
 665  
          * version) because there is a CVE with a VS that only specifies
 666  
          * vendor/product.
 667  
          */
 668  1
         BROAD_MATCH
 669  
     }
 670  
 
 671  
     /**
 672  
      * A simple object to hold an identifier and carry information about the
 673  
      * confidence in the identifier.
 674  
      */
 675  6
     private static class IdentifierMatch implements Comparable<IdentifierMatch> {
 676  
 
 677  
         /**
 678  
          * Constructs an IdentifierMatch.
 679  
          *
 680  
          * @param type the type of identifier (such as CPE)
 681  
          * @param value the value of the identifier
 682  
          * @param url the URL of the identifier
 683  
          * @param identifierConfidence the confidence in the identifier: best
 684  
          * guess or exact match
 685  
          * @param evidenceConfidence the confidence of the evidence used to find
 686  
          * the identifier
 687  
          */
 688  10
         IdentifierMatch(String type, String value, String url, IdentifierConfidence identifierConfidence, Confidence evidenceConfidence) {
 689  10
             this.identifier = new Identifier(type, value, url);
 690  10
             this.confidence = identifierConfidence;
 691  10
             this.evidenceConfidence = evidenceConfidence;
 692  10
         }
 693  
         //<editor-fold defaultstate="collapsed" desc="Property implementations: evidenceConfidence, confidence, identifier">
 694  
         /**
 695  
          * The confidence in the evidence used to identify this match.
 696  
          */
 697  
         private Confidence evidenceConfidence;
 698  
 
 699  
         /**
 700  
          * Get the value of evidenceConfidence
 701  
          *
 702  
          * @return the value of evidenceConfidence
 703  
          */
 704  
         public Confidence getEvidenceConfidence() {
 705  12
             return evidenceConfidence;
 706  
         }
 707  
 
 708  
         /**
 709  
          * Set the value of evidenceConfidence
 710  
          *
 711  
          * @param evidenceConfidence new value of evidenceConfidence
 712  
          */
 713  
         public void setEvidenceConfidence(Confidence evidenceConfidence) {
 714  0
             this.evidenceConfidence = evidenceConfidence;
 715  0
         }
 716  
         /**
 717  
          * The confidence whether this is an exact match, or a best guess.
 718  
          */
 719  
         private IdentifierConfidence confidence;
 720  
 
 721  
         /**
 722  
          * Get the value of confidence.
 723  
          *
 724  
          * @return the value of confidence
 725  
          */
 726  
         public IdentifierConfidence getConfidence() {
 727  14
             return confidence;
 728  
         }
 729  
 
 730  
         /**
 731  
          * Set the value of confidence.
 732  
          *
 733  
          * @param confidence new value of confidence
 734  
          */
 735  
         public void setConfidence(IdentifierConfidence confidence) {
 736  0
             this.confidence = confidence;
 737  0
         }
 738  
         /**
 739  
          * The CPE identifier.
 740  
          */
 741  
         private Identifier identifier;
 742  
 
 743  
         /**
 744  
          * Get the value of identifier.
 745  
          *
 746  
          * @return the value of identifier
 747  
          */
 748  
         public Identifier getIdentifier() {
 749  4
             return identifier;
 750  
         }
 751  
 
 752  
         /**
 753  
          * Set the value of identifier.
 754  
          *
 755  
          * @param identifier new value of identifier
 756  
          */
 757  
         public void setIdentifier(Identifier identifier) {
 758  0
             this.identifier = identifier;
 759  0
         }
 760  
         //</editor-fold>
 761  
         //<editor-fold defaultstate="collapsed" desc="Standard implementations of toString, hashCode, and equals">
 762  
 
 763  
         /**
 764  
          * Standard toString() implementation.
 765  
          *
 766  
          * @return the string representation of the object
 767  
          */
 768  
         @Override
 769  
         public String toString() {
 770  0
             return "IdentifierMatch{" + "evidenceConfidence=" + evidenceConfidence
 771  
                     + ", confidence=" + confidence + ", identifier=" + identifier + '}';
 772  
         }
 773  
 
 774  
         /**
 775  
          * Standard hashCode() implementation.
 776  
          *
 777  
          * @return the hashCode
 778  
          */
 779  
         @Override
 780  
         public int hashCode() {
 781  0
             int hash = 5;
 782  0
             hash = 97 * hash + (this.evidenceConfidence != null ? this.evidenceConfidence.hashCode() : 0);
 783  0
             hash = 97 * hash + (this.confidence != null ? this.confidence.hashCode() : 0);
 784  0
             hash = 97 * hash + (this.identifier != null ? this.identifier.hashCode() : 0);
 785  0
             return hash;
 786  
         }
 787  
 
 788  
         /**
 789  
          * Standard equals implementation.
 790  
          *
 791  
          * @param obj the object to compare
 792  
          * @return true if the objects are equal, otherwise false
 793  
          */
 794  
         @Override
 795  
         public boolean equals(Object obj) {
 796  0
             if (obj == null) {
 797  0
                 return false;
 798  
             }
 799  0
             if (getClass() != obj.getClass()) {
 800  0
                 return false;
 801  
             }
 802  0
             final IdentifierMatch other = (IdentifierMatch) obj;
 803  0
             if (this.evidenceConfidence != other.evidenceConfidence) {
 804  0
                 return false;
 805  
             }
 806  0
             if (this.confidence != other.confidence) {
 807  0
                 return false;
 808  
             }
 809  0
             if (this.identifier != other.identifier && (this.identifier == null || !this.identifier.equals(other.identifier))) {
 810  0
                 return false;
 811  
             }
 812  0
             return true;
 813  
         }
 814  
         //</editor-fold>
 815  
 
 816  
         /**
 817  
          * Standard implementation of compareTo that compares identifier
 818  
          * confidence, evidence confidence, and then the identifier.
 819  
          *
 820  
          * @param o the IdentifierMatch to compare to
 821  
          * @return the natural ordering of IdentifierMatch
 822  
          */
 823  
         @Override
 824  
         public int compareTo(IdentifierMatch o) {
 825  12
             return new CompareToBuilder()
 826  6
                     .append(confidence, o.confidence)
 827  6
                     .append(evidenceConfidence, o.evidenceConfidence)
 828  6
                     .append(identifier, o.identifier)
 829  6
                     .toComparison();
 830  
         }
 831  
     }
 832  
 }