View Javadoc
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  public class CPEAnalyzer implements Analyzer {
60  
61      /**
62       * The Logger.
63       */
64      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         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         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         this.open();
127     }
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         cve = new CveDB();
138         cve.open();
139         cpe = CpeMemoryIndex.getInstance();
140         try {
141             LOGGER.info("Creating the CPE Index");
142             final long creationStart = System.currentTimeMillis();
143             cpe.open(cve);
144             LOGGER.info("CPE Index Created ({} ms)", System.currentTimeMillis() - creationStart);
145         } catch (IndexException ex) {
146             LOGGER.debug("IndexException", ex);
147             throw new DatabaseException(ex);
148         }
149     }
150 
151     /**
152      * Closes the data sources.
153      */
154     @Override
155     public void close() {
156         if (cpe != null) {
157             cpe.close();
158             cpe = null;
159         }
160         if (cve != null) {
161             cve.close();
162             cve = null;
163         }
164     }
165 
166     public boolean isOpen() {
167         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         String vendors = "";
182         String products = "";
183         for (Confidence confidence : Confidence.values()) {
184             if (dependency.getVendorEvidence().contains(confidence)) {
185                 vendors = addEvidenceWithoutDuplicateTerms(vendors, dependency.getVendorEvidence(), confidence);
186                 LOGGER.debug("vendor search: {}", vendors);
187             }
188             if (dependency.getProductEvidence().contains(confidence)) {
189                 products = addEvidenceWithoutDuplicateTerms(products, dependency.getProductEvidence(), confidence);
190                 LOGGER.debug("product search: {}", products);
191             }
192             if (!vendors.isEmpty() && !products.isEmpty()) {
193                 final List<IndexEntry> entries = searchCPE(vendors, products, dependency.getProductEvidence().getWeighting(),
194                         dependency.getVendorEvidence().getWeighting());
195                 if (entries == null) {
196                     continue;
197                 }
198                 boolean identifierAdded = false;
199                 for (IndexEntry e : entries) {
200                     LOGGER.debug("Verifying entry: {}", e);
201                     if (verifyEntry(e, dependency)) {
202                         final String vendor = e.getVendor();
203                         final String product = e.getProduct();
204                         LOGGER.debug("identified vendor/product: {}/{}", vendor, product);
205                         identifierAdded |= determineIdentifiers(dependency, vendor, product, confidence);
206                     }
207                 }
208                 if (identifierAdded) {
209                     break;
210                 }
211             }
212         }
213     }
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         final String txt = (text == null) ? "" : text;
227         final StringBuilder sb = new StringBuilder(txt.length() + (20 * ec.size()));
228         sb.append(' ').append(txt).append(' ');
229         for (Evidence e : ec.iterator(confidenceFilter)) {
230             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             if (value.startsWith("http://")) {
235                 value = value.substring(7).replaceAll("\\.", " ");
236             }
237             if (value.startsWith("https://")) {
238                 value = value.substring(8).replaceAll("\\.", " ");
239             }
240             if (sb.indexOf(" " + value + " ") < 0) {
241                 sb.append(value).append(' ');
242             }
243         }
244         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         final List<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         try {
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                     entry.setSearchScore(d.score);
280                     if (!ret.contains(entry)) {
281                         ret.add(entry);
282                     }
283                 }
284             }
285             return ret;
286         } catch (ParseException ex) {
287             LOGGER.warn("An error occured querying the CPE data. See the log for more details.");
288             LOGGER.info("Unable to parse: {}", searchString, ex);
289         } catch (IOException ex) {
290             LOGGER.warn("An error occured reading CPE data. See the log for more details.");
291             LOGGER.info("IO Error with search string: {}", searchString, ex);
292         }
293         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         final String v = vendor; //.replaceAll("[^\\w\\d]", " ");
313         final String p = product; //.replaceAll("[^\\w\\d]", " ");
314         final StringBuilder sb = new StringBuilder(v.length() + p.length()
315                 + Fields.PRODUCT.length() + Fields.VENDOR.length() + STRING_BUILDER_BUFFER);
316 
317         if (!appendWeightedSearch(sb, Fields.PRODUCT, p, productWeightings)) {
318             return null;
319         }
320         sb.append(" AND ");
321         if (!appendWeightedSearch(sb, Fields.VENDOR, v, vendorWeighting)) {
322             return null;
323         }
324         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         sb.append(' ').append(field).append(":( ");
339 
340         final String cleanText = cleanseText(searchText);
341 
342         if (cleanText.isEmpty()) {
343             return false;
344         }
345 
346         if (weightedText == null || weightedText.isEmpty()) {
347             LuceneUtils.appendEscapedLuceneQuery(sb, cleanText);
348         } else {
349             final StringTokenizer tokens = new StringTokenizer(cleanText);
350             while (tokens.hasMoreElements()) {
351                 final String word = tokens.nextToken();
352                 StringBuilder temp = null;
353                 for (String weighted : weightedText) {
354                     final String weightedStr = cleanseText(weighted);
355                     if (equalsIgnoreCaseAndNonAlpha(word, weightedStr)) {
356                         temp = new StringBuilder(word.length() + 2);
357                         LuceneUtils.appendEscapedLuceneQuery(temp, word);
358                         temp.append(WEIGHTING_BOOST);
359                         if (!word.equalsIgnoreCase(weightedStr)) {
360                             temp.append(' ');
361                             LuceneUtils.appendEscapedLuceneQuery(temp, weightedStr);
362                             temp.append(WEIGHTING_BOOST);
363                         }
364                         break;
365                     }
366                 }
367                 sb.append(' ');
368                 if (temp == null) {
369                     LuceneUtils.appendEscapedLuceneQuery(sb, word);
370                 } else {
371                     sb.append(temp);
372                 }
373             }
374         }
375         sb.append(" ) ");
376         return true;
377     }
378 
379     /**
380      * Removes characters from the input text that are not used within the CPE index.
381      *
382      * @param text is the text to remove the characters from.
383      * @return the text having removed some characters.
384      */
385     private String cleanseText(String text) {
386         return text.replaceAll(CLEANSE_CHARACTER_RX, " ");
387     }
388 
389     /**
390      * Compares two strings after lower casing them and removing the non-alpha 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 that the product, vendor, and version information
408      * for the CPE are contained within the dependencies evidence.
409      *
410      * @param entry a CPE entry.
411      * @param dependency the dependency that the CPE entries could be for.
412      * @return whether or not the entry is valid.
413      */
414     private boolean verifyEntry(final IndexEntry entry, final Dependency dependency) {
415         boolean isValid = false;
416 
417         //TODO - does this nullify some of the fuzzy matching that happens in the lucene search?
418         // for instance CPE some-component and in the evidence we have SomeComponent.
419         if (collectionContainsString(dependency.getProductEvidence(), entry.getProduct())
420                 && collectionContainsString(dependency.getVendorEvidence(), entry.getVendor())) {
421             //&& collectionContainsVersion(dependency.getVersionEvidence(), entry.getVersion())
422             isValid = true;
423         }
424         return isValid;
425     }
426 
427     /**
428      * Used to determine if the EvidenceCollection contains a specific string.
429      *
430      * @param ec an EvidenceCollection
431      * @param text the text to search for
432      * @return whether or not the EvidenceCollection contains the string
433      */
434     private boolean collectionContainsString(EvidenceCollection ec, String text) {
435         //TODO - likely need to change the split... not sure if this will work for CPE with special chars
436         if (text == null) {
437             return false;
438         }
439         final String[] words = text.split("[\\s_-]");
440         final List<String> list = new ArrayList<String>();
441         String tempWord = null;
442         for (String word : words) {
443             /*
444              single letter words should be concatenated with the next word.
445              so { "m", "core", "sample" } -> { "mcore", "sample" }
446              */
447             if (tempWord != null) {
448                 list.add(tempWord + word);
449                 tempWord = null;
450             } else if (word.length() <= 2) {
451                 tempWord = word;
452             } else {
453                 list.add(word);
454             }
455         }
456         if (tempWord != null) {
457             if (!list.isEmpty()) {
458                 final String tmp = list.get(list.size() - 1) + tempWord;
459                 list.add(tmp);
460             } else {
461                 list.add(tempWord);
462             }
463         }
464         if (list.isEmpty()) {
465             return false;
466         }
467         boolean contains = true;
468         for (String word : list) {
469             contains &= ec.containsUsedString(word);
470         }
471         return contains;
472     }
473 
474     /**
475      * Analyzes a dependency and attempts to determine if there are any CPE identifiers for this dependency.
476      *
477      * @param dependency The Dependency to analyze.
478      * @param engine The analysis engine
479      * @throws AnalysisException is thrown if there is an issue analyzing the dependency.
480      */
481     @Override
482     public void analyze(Dependency dependency, Engine engine) throws AnalysisException {
483         try {
484             determineCPE(dependency);
485         } catch (CorruptIndexException ex) {
486             throw new AnalysisException("CPE Index is corrupt.", ex);
487         } catch (IOException ex) {
488             throw new AnalysisException("Failure opening the CPE Index.", ex);
489         } catch (ParseException ex) {
490             throw new AnalysisException("Unable to parse the generated Lucene query for this dependency.", ex);
491         }
492     }
493 
494     /**
495      * Retrieves a list of CPE values from the CveDB based on the vendor and product passed in. The list is then validated to find
496      * only CPEs that are valid for the given dependency. It is possible that the CPE identified is a best effort "guess" based on
497      * the vendor, product, and version information.
498      *
499      * @param dependency the Dependency being analyzed
500      * @param vendor the vendor for the CPE being analyzed
501      * @param product the product for the CPE being analyzed
502      * @param currentConfidence the current confidence being used during analysis
503      * @return <code>true</code> if an identifier was added to the dependency; otherwise <code>false</code>
504      * @throws UnsupportedEncodingException is thrown if UTF-8 is not supported
505      */
506     protected boolean determineIdentifiers(Dependency dependency, String vendor, String product,
507             Confidence currentConfidence) throws UnsupportedEncodingException {
508         final Set<VulnerableSoftware> cpes = cve.getCPEs(vendor, product);
509         DependencyVersion bestGuess = new DependencyVersion("-");
510         Confidence bestGuessConf = null;
511         boolean hasBroadMatch = false;
512         final List<IdentifierMatch> collected = new ArrayList<IdentifierMatch>();
513         for (Confidence conf : Confidence.values()) {
514 //            if (conf.compareTo(currentConfidence) > 0) {
515 //                break;
516 //            }
517             for (Evidence evidence : dependency.getVersionEvidence().iterator(conf)) {
518                 final DependencyVersion evVer = DependencyVersionUtil.parseVersion(evidence.getValue());
519                 if (evVer == null) {
520                     continue;
521                 }
522                 for (VulnerableSoftware vs : cpes) {
523                     DependencyVersion dbVer;
524                     if (vs.getUpdate() != null && !vs.getUpdate().isEmpty()) {
525                         dbVer = DependencyVersionUtil.parseVersion(vs.getVersion() + '.' + vs.getUpdate());
526                     } else {
527                         dbVer = DependencyVersionUtil.parseVersion(vs.getVersion());
528                     }
529                     if (dbVer == null) { //special case, no version specified - everything is vulnerable
530                         hasBroadMatch = true;
531                         final String url = String.format(NVD_SEARCH_URL, URLEncoder.encode(vs.getName(), "UTF-8"));
532                         final IdentifierMatch match = new IdentifierMatch("cpe", vs.getName(), url, IdentifierConfidence.BROAD_MATCH, conf);
533                         collected.add(match);
534                     } else if (evVer.equals(dbVer)) { //yeah! exact match
535                         final String url = String.format(NVD_SEARCH_URL, URLEncoder.encode(vs.getName(), "UTF-8"));
536                         final IdentifierMatch match = new IdentifierMatch("cpe", vs.getName(), url, IdentifierConfidence.EXACT_MATCH, conf);
537                         collected.add(match);
538                     } else {
539                         //TODO the following isn't quite right is it? need to think about this guessing game a bit more.
540                         if (evVer.getVersionParts().size() <= dbVer.getVersionParts().size()
541                                 && evVer.matchesAtLeastThreeLevels(dbVer)) {
542                             if (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0) {
543                                 if (bestGuess.getVersionParts().size() < dbVer.getVersionParts().size()) {
544                                     bestGuess = dbVer;
545                                     bestGuessConf = conf;
546                                 }
547                             }
548                         }
549                     }
550                 }
551                 if (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0) {
552                     if (bestGuess.getVersionParts().size() < evVer.getVersionParts().size()) {
553                         bestGuess = evVer;
554                         bestGuessConf = conf;
555                     }
556                 }
557             }
558         }
559         final String cpeName = String.format("cpe:/a:%s:%s:%s", vendor, product, bestGuess.toString());
560         String url = null;
561         if (hasBroadMatch) { //if we have a broad match we can add the URL to the best guess.
562             final String cpeUrlName = String.format("cpe:/a:%s:%s", vendor, product);
563             url = String.format(NVD_SEARCH_URL, URLEncoder.encode(cpeUrlName, "UTF-8"));
564         }
565         if (bestGuessConf == null) {
566             bestGuessConf = Confidence.LOW;
567         }
568         final IdentifierMatch match = new IdentifierMatch("cpe", cpeName, url, IdentifierConfidence.BEST_GUESS, bestGuessConf);
569         collected.add(match);
570 
571         Collections.sort(collected);
572         final IdentifierConfidence bestIdentifierQuality = collected.get(0).getConfidence();
573         final Confidence bestEvidenceQuality = collected.get(0).getEvidenceConfidence();
574         boolean identifierAdded = false;
575         for (IdentifierMatch m : collected) {
576             if (bestIdentifierQuality.equals(m.getConfidence())
577                     && bestEvidenceQuality.equals(m.getEvidenceConfidence())) {
578                 final Identifier i = m.getIdentifier();
579                 if (bestIdentifierQuality == IdentifierConfidence.BEST_GUESS) {
580                     i.setConfidence(Confidence.LOW);
581                 } else {
582                     i.setConfidence(bestEvidenceQuality);
583                 }
584                 dependency.addIdentifier(i);
585                 identifierAdded = true;
586             }
587         }
588         return identifierAdded;
589     }
590 
591     /**
592      * The confidence whether the identifier is an exact match, or a best guess.
593      */
594     private enum IdentifierConfidence {
595 
596         /**
597          * An exact match for the CPE.
598          */
599         EXACT_MATCH,
600         /**
601          * A best guess for the CPE.
602          */
603         BEST_GUESS,
604         /**
605          * The entire vendor/product group must be added (without a guess at version) because there is a CVE with a VS that only
606          * specifies vendor/product.
607          */
608         BROAD_MATCH
609     }
610 
611     /**
612      * A simple object to hold an identifier and carry information about the confidence in the identifier.
613      */
614     private static class IdentifierMatch implements Comparable<IdentifierMatch> {
615 
616         /**
617          * Constructs an IdentifierMatch.
618          *
619          * @param type the type of identifier (such as CPE)
620          * @param value the value of the identifier
621          * @param url the URL of the identifier
622          * @param identifierConfidence the confidence in the identifier: best guess or exact match
623          * @param evidenceConfidence the confidence of the evidence used to find the identifier
624          */
625         IdentifierMatch(String type, String value, String url, IdentifierConfidence identifierConfidence, Confidence evidenceConfidence) {
626             this.identifier = new Identifier(type, value, url);
627             this.confidence = identifierConfidence;
628             this.evidenceConfidence = evidenceConfidence;
629         }
630         //<editor-fold defaultstate="collapsed" desc="Property implementations: evidenceConfidence, confidence, identifier">
631         /**
632          * The confidence in the evidence used to identify this match.
633          */
634         private Confidence evidenceConfidence;
635 
636         /**
637          * Get the value of evidenceConfidence
638          *
639          * @return the value of evidenceConfidence
640          */
641         public Confidence getEvidenceConfidence() {
642             return evidenceConfidence;
643         }
644 
645         /**
646          * Set the value of evidenceConfidence
647          *
648          * @param evidenceConfidence new value of evidenceConfidence
649          */
650         public void setEvidenceConfidence(Confidence evidenceConfidence) {
651             this.evidenceConfidence = evidenceConfidence;
652         }
653         /**
654          * The confidence whether this is an exact match, or a best guess.
655          */
656         private IdentifierConfidence confidence;
657 
658         /**
659          * Get the value of confidence.
660          *
661          * @return the value of confidence
662          */
663         public IdentifierConfidence getConfidence() {
664             return confidence;
665         }
666 
667         /**
668          * Set the value of confidence.
669          *
670          * @param confidence new value of confidence
671          */
672         public void setConfidence(IdentifierConfidence confidence) {
673             this.confidence = confidence;
674         }
675         /**
676          * The CPE identifier.
677          */
678         private Identifier identifier;
679 
680         /**
681          * Get the value of identifier.
682          *
683          * @return the value of identifier
684          */
685         public Identifier getIdentifier() {
686             return identifier;
687         }
688 
689         /**
690          * Set the value of identifier.
691          *
692          * @param identifier new value of identifier
693          */
694         public void setIdentifier(Identifier identifier) {
695             this.identifier = identifier;
696         }
697         //</editor-fold>
698         //<editor-fold defaultstate="collapsed" desc="Standard implementations of toString, hashCode, and equals">
699 
700         /**
701          * Standard toString() implementation.
702          *
703          * @return the string representation of the object
704          */
705         @Override
706         public String toString() {
707             return "IdentifierMatch{" + "evidenceConfidence=" + evidenceConfidence
708                     + ", confidence=" + confidence + ", identifier=" + identifier + '}';
709         }
710 
711         /**
712          * Standard hashCode() implementation.
713          *
714          * @return the hashCode
715          */
716         @Override
717         public int hashCode() {
718             int hash = 5;
719             hash = 97 * hash + (this.evidenceConfidence != null ? this.evidenceConfidence.hashCode() : 0);
720             hash = 97 * hash + (this.confidence != null ? this.confidence.hashCode() : 0);
721             hash = 97 * hash + (this.identifier != null ? this.identifier.hashCode() : 0);
722             return hash;
723         }
724 
725         /**
726          * Standard equals implementation.
727          *
728          * @param obj the object to compare
729          * @return true if the objects are equal, otherwise false
730          */
731         @Override
732         public boolean equals(Object obj) {
733             if (obj == null) {
734                 return false;
735             }
736             if (getClass() != obj.getClass()) {
737                 return false;
738             }
739             final IdentifierMatch other = (IdentifierMatch) obj;
740             if (this.evidenceConfidence != other.evidenceConfidence) {
741                 return false;
742             }
743             if (this.confidence != other.confidence) {
744                 return false;
745             }
746             if (this.identifier != other.identifier && (this.identifier == null || !this.identifier.equals(other.identifier))) {
747                 return false;
748             }
749             return true;
750         }
751         //</editor-fold>
752 
753         /**
754          * Standard implementation of compareTo that compares identifier confidence, evidence confidence, and then the identifier.
755          *
756          * @param o the IdentifierMatch to compare to
757          * @return the natural ordering of IdentifierMatch
758          */
759         @Override
760         public int compareTo(IdentifierMatch o) {
761             int conf = this.confidence.compareTo(o.confidence);
762             if (conf == 0) {
763                 conf = this.evidenceConfidence.compareTo(o.evidenceConfidence);
764                 if (conf == 0) {
765                     conf = identifier.compareTo(o.identifier);
766                 }
767             }
768             return conf;
769         }
770     }
771 }