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