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