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