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