View Javadoc

    1   /*
    2    * This file is part of dependency-check-core.
    3    *
    4    * Dependency-check-core is free software: you can redistribute it and/or modify it
    5    * under the terms of the GNU General Public License as published by the Free
    6    * Software Foundation, either version 3 of the License, or (at your option) any
    7    * later version.
    8    *
    9    * Dependency-check-core is distributed in the hope that it will be useful, but
    10   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    11   * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
    12   * details.
    13   *
    14   * You should have received a copy of the GNU General Public License along with
    15   * dependency-check-core. If not, see http://www.gnu.org/licenses/.
    16   *
    17   * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
    18   */
    19  package org.owasp.dependencycheck.analyzer;
    20
    21  import java.io.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  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         cpe = new CpeIndexReader();
    104         cpe.open();
    105         cve = new CveDB();
    106         try {
    107             cve.open();
    108         } catch (SQLException ex) {
    109             Logger.getLogger(CPEAnalyzer.class.getName()).log(Level.FINE, null, ex);
    110             throw new DatabaseException("Unable to open the cve db", ex);
    111         } catch (ClassNotFoundException ex) {
    112             Logger.getLogger(CPEAnalyzer.class.getName()).log(Level.FINE, null, ex);
    113             throw new DatabaseException("Unable to open the cve db", ex);
    114         }
    115     }
    116
    117     /**
    118      * Closes the data source.
    119      */
    120     @Override
    121     public void close() {
    122         if (cpe != null) {
    123             cpe.close();
    124         }
    125         if (cve != null) {
    126             cve.close();
    127         }
    128     }
    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         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         super.finalize();
    147         if (isOpen()) {
    148             close();
    149         }
    150     }
    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         Confidence vendorConf = Confidence.HIGHEST;
    164         Confidence productConf = Confidence.HIGHEST;
    165
    166         String vendors = addEvidenceWithoutDuplicateTerms("", dependency.getVendorEvidence(), vendorConf);
    167         String products = addEvidenceWithoutDuplicateTerms("", dependency.getProductEvidence(), productConf);
    168
    169         int ctr = 0;
    170         do {
    171             if (!vendors.isEmpty() && !products.isEmpty()) {
    172                 final List<IndexEntry> entries = searchCPE(vendors, products, dependency.getProductEvidence().getWeighting(),
    173                         dependency.getVendorEvidence().getWeighting());
    174
    175                 for (IndexEntry e : entries) {
    176                     if (verifyEntry(e, dependency)) {
    177                         final String vendor = e.getVendor();
    178                         final String product = e.getProduct();
    179                         determineIdentifiers(dependency, vendor, product);
    180                     }
    181                 }
    182             }
    183             vendorConf = reduceConfidence(vendorConf);
    184             if (dependency.getVendorEvidence().contains(vendorConf)) {
    185                 vendors = addEvidenceWithoutDuplicateTerms(vendors, dependency.getVendorEvidence(), vendorConf);
    186             }
    187             productConf = reduceConfidence(productConf);
    188             if (dependency.getProductEvidence().contains(productConf)) {
    189                 products = addEvidenceWithoutDuplicateTerms(products, dependency.getProductEvidence(), productConf);
    190             }
    191         } while ((++ctr) < 4);
    192     }
    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         final String txt = (text == null) ? "" : text;
    207         final StringBuilder sb = new StringBuilder(txt.length() + (20 * ec.size()));
    208         sb.append(' ').append(txt).append(' ');
    209         for (Evidence e : ec.iterator(confidenceFilter)) {
    210             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             if (value.startsWith("http://")) {
    215                 value = value.substring(7).replaceAll("\\.", " ");
    216             }
    217             if (value.startsWith("https://")) {
    218                 value = value.substring(8).replaceAll("\\.", " ");
    219             }
    220             if (sb.indexOf(" " + value + " ") < 0) {
    221                 sb.append(value).append(' ');
    222             }
    223         }
    224         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         if (c == Confidence.HIGHEST) {
    236             return Confidence.HIGH;
    237         } else if (c == Confidence.HIGH) {
    238             return Confidence.MEDIUM;
    239         } else {
    240             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         final ArrayList<IndexEntry> ret = new ArrayList<IndexEntry>(MAX_QUERY_RESULTS);
    266
    267         final String searchString = buildSearch(vendor, product, vendorWeightings, productWeightings);
    268         if (searchString == null) {
    269             return ret;
    270         }
    271
    272         final TopDocs docs = cpe.search(searchString, MAX_QUERY_RESULTS);
    273         for (ScoreDoc d : docs.scoreDocs) {
    274             if (d.score >= 0.08) {
    275                 final Document doc = cpe.getDocument(d.doc);
    276                 final IndexEntry entry = new IndexEntry();
    277                 entry.setVendor(doc.get(Fields.VENDOR));
    278                 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                 entry.setSearchScore(d.score);
    287                 if (!ret.contains(entry)) {
    288                     ret.add(entry);
    289                 }
    290             }
    291         }
    292         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         final String v = vendor; //.replaceAll("[^\\w\\d]", " ");
    314         final String p = product; //.replaceAll("[^\\w\\d]", " ");
    315         final StringBuilder sb = new StringBuilder(v.length() + p.length()
    316                 + Fields.PRODUCT.length() + Fields.VENDOR.length() + STRING_BUILDER_BUFFER);
    317
    318         if (!appendWeightedSearch(sb, Fields.PRODUCT, p, productWeightings)) {
    319             return null;
    320         }
    321         sb.append(" AND ");
    322         if (!appendWeightedSearch(sb, Fields.VENDOR, v, vendorWeighting)) {
    323             return null;
    324         }
    325         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         sb.append(" ").append(field).append(":( ");
    344
    345         final String cleanText = cleanseText(searchText);
    346
    347         if ("".equals(cleanText)) {
    348             return false;
    349         }
    350
    351         if (weightedText == null || weightedText.isEmpty()) {
    352             LuceneUtils.appendEscapedLuceneQuery(sb, cleanText);
    353         } else {
    354             final StringTokenizer tokens = new StringTokenizer(cleanText);
    355             while (tokens.hasMoreElements()) {
    356                 final String word = tokens.nextToken();
    357                 String temp = null;
    358                 for (String weighted : weightedText) {
    359                     final String weightedStr = cleanseText(weighted);
    360                     if (equalsIgnoreCaseAndNonAlpha(word, weightedStr)) {
    361                         temp = LuceneUtils.escapeLuceneQuery(word) + WEIGHTING_BOOST;
    362                         if (!word.equalsIgnoreCase(weightedStr)) {
    363                             temp += " " + LuceneUtils.escapeLuceneQuery(weightedStr) + WEIGHTING_BOOST;
    364                         }
    365                     }
    366                 }
    367                 if (temp == null) {
    368                     temp = LuceneUtils.escapeLuceneQuery(word);
    369                 }
    370                 sb.append(" ").append(temp);
    371             }
    372         }
    373         sb.append(" ) ");
    374         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         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         if (l == null || r == null) {
    398             return false;
    399         }
    400
    401         final String left = l.replaceAll(CLEANSE_NONALPHA_RX, "");
    402         final String right = r.replaceAll(CLEANSE_NONALPHA_RX, "");
    403         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         boolean isValid = false;
    417
    418         if (collectionContainsString(dependency.getProductEvidence(), entry.getProduct())
    419                 && collectionContainsString(dependency.getVendorEvidence(), entry.getVendor())) {
    420             //&& collectionContainsVersion(dependency.getVersionEvidence(), entry.getVersion())
    421             isValid = true;
    422         }
    423         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         if (text == null) {
    448             return false;
    449         }
    450         final String[] words = text.split("[\\s_-]");
    451         final List<String> list = new ArrayList<String>();
    452         String tempWord = null;
    453         for (String word : words) {
    454             //single letter words should be concatonated with the next word.
    455             // so { "m", "core", "sample" } -> { "mcore", "sample" }
    456             if (tempWord != null) {
    457                 list.add(tempWord + word);
    458                 tempWord = null;
    459             } else if (word.length() <= 2) {
    460                 tempWord = word;
    461             } else {
    462                 list.add(word);
    463             }
    464         }
    465         if (tempWord != null && !list.isEmpty()) {
    466             final String tmp = list.get(list.size() - 1) + tempWord;
    467             list.add(tmp);
    468         }
    469         boolean contains = true;
    470         for (String word : list) {
    471             contains &= ec.containsUsedString(word);
    472         }
    473         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             determineCPE(dependency);
    489         } catch (CorruptIndexException ex) {
    490             throw new AnalysisException("CPE Index is corrupt.", ex);
    491         } catch (IOException ex) {
    492             throw new AnalysisException("Failure opening the CPE Index.", ex);
    493         } catch (ParseException ex) {
    494             throw new AnalysisException("Unable to parse the generated Lucene query for this dependency.", ex);
    495         }
    496     }
    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         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         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         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         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         this.open();
    547     }
    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         final Set<VulnerableSoftware> cpes = cve.getCPEs(vendor, product);
    563         DependencyVersion bestGuess = new DependencyVersion("-");
    564         Confidence bestGuessConf = null;
    565         final List<IdentifierMatch> collected = new ArrayList<IdentifierMatch>();
    566         for (Confidence conf : Confidence.values()) {
    567             for (Evidence evidence : dependency.getVersionEvidence().iterator(conf)) {
    568                 final DependencyVersion evVer = DependencyVersionUtil.parseVersion(evidence.getValue());
    569                 if (evVer == null) {
    570                     continue;
    571                 }
    572                 for (VulnerableSoftware vs : cpes) {
    573                     DependencyVersion dbVer;
    574                     if (vs.getRevision() != null && !vs.getRevision().isEmpty()) {
    575                         dbVer = DependencyVersionUtil.parseVersion(vs.getVersion() + "." + vs.getRevision());
    576                     } else {
    577                         dbVer = DependencyVersionUtil.parseVersion(vs.getVersion());
    578                     }
    579                     if (dbVer == null //special case, no version specified - everything is vulnerable
    580                             || evVer.equals(dbVer)) { //woot exect match
    581                         final String url = String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(vs.getName(), "UTF-8"));
    582                         final IdentifierMatch match = new IdentifierMatch("cpe", vs.getName(), url, IdentifierConfidence.EXACT_MATCH, conf);
    583                         collected.add(match);
    584                     } else {
    585                         //TODO the following isn't quite right is it? need to think about this guessing game a bit more.
    586                         if (evVer.getVersionParts().size() <= dbVer.getVersionParts().size()
    587                                 && evVer.matchesAtLeastThreeLevels(dbVer)) {
    588                             if (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0) {
    589                                 if (bestGuess.getVersionParts().size() < dbVer.getVersionParts().size()) {
    590                                     bestGuess = dbVer;
    591                                     bestGuessConf = conf;
    592                                 }
    593                             }
    594                         }
    595                     }
    596                 }
    597                 if (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0) {
    598                     if (bestGuess.getVersionParts().size() < evVer.getVersionParts().size()) {
    599                         bestGuess = evVer;
    600                         bestGuessConf = conf;
    601                     }
    602                 }
    603             }
    604         }
    605         final String cpeName = String.format("cpe:/a:%s:%s:%s", vendor, product, bestGuess.toString());
    606         final String url = null; //String.format("http://web.nvd.nist.gov/view/vuln/search?cpe=%s", URLEncoder.encode(cpeName, "UTF-8"));
    607         if (bestGuessConf == null) {
    608             bestGuessConf = Confidence.LOW;
    609         }
    610         final IdentifierMatch match = new IdentifierMatch("cpe", cpeName, url, IdentifierConfidence.BEST_GUESS, bestGuessConf);
    611         collected.add(match);
    612
    613         Collections.sort(collected);
    614         final IdentifierConfidence bestIdentifierQuality = collected.get(0).getConfidence();
    615         final Confidence bestEvidenceQuality = collected.get(0).getEvidenceConfidence();
    616         for (IdentifierMatch m : collected) {
    617             if (bestIdentifierQuality.equals(m.getConfidence())
    618                     && bestEvidenceQuality.equals(m.getEvidenceConfidence())) {
    619                 dependency.addIdentifier(m.getIdentifier());
    620             }
    621         }
    622     }
    623
    624     /**
    625      * The confidence whether the identifier is an exact match, or a best guess.
    626      */
    627     private enum IdentifierConfidence {
    628
    629         /**
    630          * An exact match for the CPE.
    631          */
    632         EXACT_MATCH,
    633         /**
    634          * A best guess for the CPE.
    635          */
    636         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     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         IdentifierMatch(String type, String value, String url, IdentifierConfidence identifierConfidence, Confidence evidenceConfidence) {
    657             this.identifier = new Identifier(type, value, url);
    658             this.confidence = identifierConfidence;
    659             this.evidenceConfidence = evidenceConfidence;
    660         }
    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             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             this.evidenceConfidence = evidenceConfidence;
    683         }
    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             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             this.confidence = confidence;
    705         }
    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             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             this.identifier = identifier;
    727         }
    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             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             int hash = 5;
    750             hash = 97 * hash + (this.evidenceConfidence != null ? this.evidenceConfidence.hashCode() : 0);
    751             hash = 97 * hash + (this.confidence != null ? this.confidence.hashCode() : 0);
    752             hash = 97 * hash + (this.identifier != null ? this.identifier.hashCode() : 0);
    753             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             if (obj == null) {
    765                 return false;
    766             }
    767             if (getClass() != obj.getClass()) {
    768                 return false;
    769             }
    770             final IdentifierMatch other = (IdentifierMatch) obj;
    771             if (this.evidenceConfidence != other.evidenceConfidence) {
    772                 return false;
    773             }
    774             if (this.confidence != other.confidence) {
    775                 return false;
    776             }
    777             if (this.identifier != other.identifier && (this.identifier == null || !this.identifier.equals(other.identifier))) {
    778                 return false;
    779             }
    780             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             int conf = this.confidence.compareTo(o.confidence);
    794             if (conf == 0) {
    795                 conf = this.evidenceConfidence.compareTo(o.evidenceConfidence);
    796                 if (conf == 0) {
    797                     conf = identifier.compareTo(o.identifier);
    798                 }
    799             }
    800             return conf;
    801         }
    802     }
    803 }