CPEAnalyzer.java

/*
 * This file is part of dependency-check-core.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
 */
package org.owasp.dependencycheck.analyzer;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.mutable.MutableInt;
import org.apache.lucene.analysis.CharArraySet;
import org.apache.lucene.document.Document;
import org.apache.lucene.index.CorruptIndexException;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.ScoreDoc;
import org.apache.lucene.search.TopDocs;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.cpe.CpeMemoryIndex;
import org.owasp.dependencycheck.data.cpe.Fields;
import org.owasp.dependencycheck.data.cpe.IndexEntry;
import org.owasp.dependencycheck.data.cpe.IndexException;
import org.owasp.dependencycheck.data.cpe.MemoryIndex;
import org.owasp.dependencycheck.data.lucene.LuceneUtils;
import org.owasp.dependencycheck.data.lucene.SearchFieldAnalyzer;
import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
import org.owasp.dependencycheck.data.nvdcve.CveDB;
import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
import org.owasp.dependencycheck.data.update.cpe.CpePlus;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.Evidence;
import org.owasp.dependencycheck.dependency.EvidenceType;
import org.owasp.dependencycheck.dependency.naming.CpeIdentifier;
import org.owasp.dependencycheck.dependency.naming.Identifier;
import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
import org.owasp.dependencycheck.exception.InitializationException;
import org.owasp.dependencycheck.utils.DependencyVersion;
import org.owasp.dependencycheck.utils.DependencyVersionUtil;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.Cpe;
import us.springett.parsers.cpe.CpeBuilder;
import us.springett.parsers.cpe.exceptions.CpeValidationException;
import us.springett.parsers.cpe.values.Part;

/**
 * CPEAnalyzer is a utility class that takes a project dependency and attempts
 * to discern if there is an associated CPE. It uses the evidence contained
 * within the dependency to search the Lucene index.
 *
 * @author Jeremy Long
 */
@ThreadSafe
public class CPEAnalyzer extends AbstractAnalyzer {

    /**
     * The Logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(CPEAnalyzer.class);
    /**
     * The weighting boost to give terms when constructing the Lucene query.
     */
    private static final int WEIGHTING_BOOST = 1;
    /**
     * A string representation of a regular expression defining characters
     * utilized within the CPE Names. Note, the :/ are included so URLs are
     * passed into the Lucene query so that the specialized tokenizer can parse
     * them.
     */
    private static final String CLEANSE_CHARACTER_RX = "[^A-Za-z0-9 ._:/-]";
    /**
     * A string representation of a regular expression used to remove all but
     * alpha characters.
     */
    private static final String CLEANSE_NONALPHA_RX = "[^A-Za-z]*";
    /**
     * UTF-8 character set name.
     */
    private static final String UTF8 = StandardCharsets.UTF_8.name();
    /**
     * The URL to search the NVD CVE data at NIST. This is used by calling:
     * <pre>String.format(NVD_SEARCH_URL, vendor, product, version);</pre>
     */
    public static final String NVD_SEARCH_URL = "https://nvd.nist.gov/vuln/search/results?form_type=Advanced&"
            + "results_type=overview&search_type=all&cpe_vendor=cpe%%3A%%2F%%3A%1$s&cpe_product=cpe%%3A%%2F%%3A%1$s%%3A%2$s&"
            + "cpe_version=cpe%%3A%%2F%%3A%1$s%%3A%2$s%%3A%3$s";

    /**
     * The URL to search the NVD CVE data at NIST. This is used by calling:
     * <pre>String.format(NVD_SEARCH_URL, vendor, product);</pre>
     */
    public static final String NVD_SEARCH_BROAD_URL = "https://nvd.nist.gov/vuln/search/results?form_type=Advanced&"
            + "results_type=overview&search_type=all&cpe_vendor=cpe%%3A%%2F%%3A%1$s&cpe_product=cpe%%3A%%2F%%3A%1$s%%3A%2$s";
    /**
     * The CPE in memory index.
     */
    private MemoryIndex cpe;
    /**
     * The CVE Database.
     */
    private CveDB cve;
    /**
     * A reference to the ODC engine.
     */
    private Engine engine;
    /**
     * The list of ecosystems to skip during analysis. These are skipped because
     * there is generally a more accurate vulnerability analyzer in the
     * pipeline.
     */
    private List<String> skipEcosystems;
    /**
     * A reference to the ecosystem object; used to obtain the max query results
     * for each ecosystem.
     */
    private Ecosystem ecosystemTools;
    /**
     * A reference to the suppression analyzer; for timing reasons we need to
     * test for suppressions immediately after identifying the match because a
     * higher confidence match on a FP can mask a lower confidence, yet valid
     * match.
     */
    private CpeSuppressionAnalyzer suppression;

    /**
     * Returns the name of this analyzer.
     *
     * @return the name of this analyzer.
     */
    @Override
    public String getName() {
        return "CPE Analyzer";
    }

    /**
     * Returns the analysis phase that this analyzer should run in.
     *
     * @return the analysis phase that this analyzer should run in.
     */
    @Override
    public AnalysisPhase getAnalysisPhase() {
        return AnalysisPhase.IDENTIFIER_ANALYSIS;
    }

    /**
     * Creates the CPE Lucene Index.
     *
     * @param engine a reference to the dependency-check engine
     * @throws InitializationException is thrown if there is an issue opening
     * the index.
     */
    @Override
    public void prepareAnalyzer(Engine engine) throws InitializationException {
        super.prepareAnalyzer(engine);
        this.engine = engine;
        try {
            this.open(engine.getDatabase());
        } catch (IOException ex) {
            LOGGER.debug("Exception initializing the Lucene Index", ex);
            throw new InitializationException("An exception occurred initializing the Lucene Index", ex);
        } catch (DatabaseException ex) {
            LOGGER.debug("Exception accessing the database", ex);
            throw new InitializationException("An exception occurred accessing the database", ex);
        }
        final String[] tmp = engine.getSettings().getArray(Settings.KEYS.ECOSYSTEM_SKIP_CPEANALYZER);
        if (tmp == null) {
            skipEcosystems = new ArrayList<>();
        } else {
            LOGGER.debug("Skipping CPE Analysis for {}", StringUtils.join(tmp, ","));
            skipEcosystems = Arrays.asList(tmp);
        }
        ecosystemTools = new Ecosystem(engine.getSettings());
        suppression = new CpeSuppressionAnalyzer();
        suppression.initialize(engine.getSettings());
        suppression.prepareAnalyzer(engine);
    }

    /**
     * Opens the data source.
     *
     * @param cve a reference to the NVD CVE database
     * @throws IOException when the Lucene directory to be queried does not
     * exist or is corrupt.
     * @throws DatabaseException when the database throws an exception. This
     * usually occurs when the database is in use by another process.
     */
    public void open(CveDB cve) throws IOException, DatabaseException {
        this.cve = cve;
        this.cpe = CpeMemoryIndex.getInstance();
        try {
            final long creationStart = System.currentTimeMillis();
            cpe.open(cve.getVendorProductList(), this.getSettings());
            final long creationSeconds = TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - creationStart);
            LOGGER.info("Created CPE Index ({} seconds)", creationSeconds);
        } catch (IndexException ex) {
            LOGGER.debug("IndexException", ex);
            throw new DatabaseException(ex);
        }
    }

    /**
     * Closes the data sources.
     */
    @Override
    public void closeAnalyzer() {
        if (cpe != null) {
            cpe.close();
            cpe = null;
        }
    }

    /**
     * Searches the data store of CPE entries, trying to identify the CPE for
     * the given dependency based on the evidence contained within. The
     * dependency passed in is updated with any identified CPE values.
     *
     * @param dependency the dependency to search for CPE entries on
     * @throws CorruptIndexException is thrown when the Lucene index is corrupt
     * @throws IOException is thrown when an IOException occurs
     * @throws ParseException is thrown when the Lucene query cannot be parsed
     * @throws AnalysisException thrown if the suppression rules failed
     */
    protected void determineCPE(Dependency dependency) throws CorruptIndexException, IOException, ParseException, AnalysisException {
        boolean identifierAdded;

        final Set<String> majorVersions = dependency.getSoftwareIdentifiers()
                .stream()
                .filter(i -> i instanceof PurlIdentifier)
                .map(i -> {
                    final PurlIdentifier p = (PurlIdentifier) i;
                    final DependencyVersion depVersion = DependencyVersionUtil.parseVersion(p.getVersion(), false);
                    if (depVersion != null) {
                        return depVersion.getVersionParts().get(0);
                    }
                    return null;
                }).collect(Collectors.toSet());

        final Map<String, MutableInt> vendors = new HashMap<>();
        final Map<String, MutableInt> products = new HashMap<>();
        final Set<Integer> previouslyFound = new HashSet<>();

        for (Confidence confidence : Confidence.values()) {
            collectTerms(vendors, dependency.getIterator(EvidenceType.VENDOR, confidence));
            LOGGER.trace("vendor search: {}", vendors);
            collectTerms(products, dependency.getIterator(EvidenceType.PRODUCT, confidence));
            addMajorVersionToTerms(majorVersions, products);
            LOGGER.trace("product search: {}", products);
            if (!vendors.isEmpty() && !products.isEmpty()) {
                final List<IndexEntry> entries = searchCPE(vendors, products,
                        dependency.getVendorWeightings(), dependency.getProductWeightings(),
                        dependency.getEcosystem());
                if (entries == null) {
                    continue;
                }

                identifierAdded = false;
                for (IndexEntry e : entries) {
                    if (previouslyFound.contains(e.getDocumentId()) /*|| (filter > 0 && e.getSearchScore() < filter)*/) {
                        continue;
                    }
                    previouslyFound.add(e.getDocumentId());
                    if (verifyEntry(e, dependency, majorVersions)) {
                        final String vendor = e.getVendor();
                        final String product = e.getProduct();
                        LOGGER.trace("identified vendor/product: {}/{}", vendor, product);
                        identifierAdded |= determineIdentifiers(dependency, vendor, product, confidence);
                    }
                }
                if (identifierAdded) {
                    break;
                }
            }
        }
    }

    /**
     * <p>
     * Returns the text created by concatenating the text and the values from
     * the EvidenceCollection (filtered for a specific confidence). This
     * attempts to prevent duplicate terms from being added.</p>
     * <p>
     * Note, if the evidence is longer then 1000 characters it will be
     * truncated.</p>
     *
     * @param terms the collection of terms
     * @param evidence an iterable set of evidence to concatenate
     */
    @SuppressWarnings("null")

    protected void collectTerms(Map<String, MutableInt> terms, Iterable<Evidence> evidence) {
        for (Evidence e : evidence) {
            String value = cleanseText(e.getValue());
            if (StringUtils.isBlank(value)) {
                continue;
            }
            if (value.length() > 1000) {
                boolean trimmed = false;
                int pos = value.lastIndexOf(" ", 1000);
                if (pos > 0) {
                    value = value.substring(0, pos);
                    trimmed = true;
                } else {
                    pos = value.lastIndexOf(".", 1000);
                }
                if (!trimmed) {
                    if (pos > 0) {
                        value = value.substring(0, pos);
                        trimmed = true;
                    } else {
                        pos = value.lastIndexOf("-", 1000);
                    }
                }
                if (!trimmed) {
                    if (pos > 0) {
                        value = value.substring(0, pos);
                        trimmed = true;
                    } else {
                        pos = value.lastIndexOf("_", 1000);
                    }
                }
                if (!trimmed) {
                    if (pos > 0) {
                        value = value.substring(0, pos);
                        trimmed = true;
                    } else {
                        pos = value.lastIndexOf("/", 1000);
                    }
                }
                if (!trimmed && pos > 0) {
                    value = value.substring(0, pos);
                    trimmed = true;
                }
                if (!trimmed) {
                    value = value.substring(0, 1000);
                }
            }
            addTerm(terms, value);
        }
    }

    private void addMajorVersionToTerms(Set<String> majorVersions, Map<String, MutableInt> products) {
        final Map<String, MutableInt> temp = new HashMap<>();
        products.entrySet().stream()
                .filter(term -> term.getKey() != null)
                .forEach(term -> majorVersions.stream()
                .filter(version -> version != null
                && (!term.getKey().endsWith(version)
                && !Character.isDigit(term.getKey().charAt(term.getKey().length() - 1))
                && !products.containsKey(term.getKey() + version)))
                .forEach(version -> {
                    addTerm(temp, term.getKey() + version);
                }));
        products.entrySet().stream()
                .filter(term -> term.getKey() != null)
                .forEach(term -> majorVersions.stream()
                .filter(Objects::nonNull)
                .map(version -> "v" + version)
                .filter(version -> (!term.getKey().endsWith(version)
                && !Character.isDigit(term.getKey().charAt(term.getKey().length() - 1))
                && !products.containsKey(term.getKey() + version)))
                .forEach(version -> {
                    addTerm(temp, term.getKey() + version);
                }));
        products.putAll(temp);
    }

    /**
     * Adds a term to the map of terms.
     *
     * @param terms the map of terms
     * @param value the value of the term to add
     */
    private void addTerm(Map<String, MutableInt> terms, String value) {
        final MutableInt count = terms.get(value);
        if (count == null) {
            terms.put(value, new MutableInt(1));
        } else {
            count.add(1);
        }
    }

    /**
     * <p>
     * Searches the Lucene CPE index to identify possible CPE entries associated
     * with the supplied vendor, product, and version.</p>
     *
     * <p>
     * If either the vendorWeightings or productWeightings lists have been
     * populated this data is used to add weighting factors to the search.</p>
     *
     * @param vendor the text used to search the vendor field
     * @param product the text used to search the product field
     * @param vendorWeightings a list of strings to use to add weighting factors
     * to the vendor field
     * @param productWeightings Adds a list of strings that will be used to add
     * weighting factors to the product search
     * @param ecosystem the dependency's ecosystem
     * @return a list of possible CPE values
     */
    protected List<IndexEntry> searchCPE(Map<String, MutableInt> vendor, Map<String, MutableInt> product,
            Set<String> vendorWeightings, Set<String> productWeightings, String ecosystem) {

        final int maxQueryResults = ecosystemTools.getLuceneMaxQueryLimitFor(ecosystem);
        final List<IndexEntry> ret = new ArrayList<>(maxQueryResults);

        final String searchString = buildSearch(vendor, product, vendorWeightings, productWeightings);
        if (searchString == null) {
            return ret;
        }
        try {
            final Query query = cpe.parseQuery(searchString);
            final TopDocs docs = cpe.search(query, maxQueryResults);

            for (ScoreDoc d : docs.scoreDocs) {
                //if (d.score >= minLuceneScore) {
                final Document doc = cpe.getDocument(d.doc);
                final IndexEntry entry = new IndexEntry();
                entry.setDocumentId(d.doc);
                entry.setVendor(doc.get(Fields.VENDOR));
                entry.setProduct(doc.get(Fields.PRODUCT));
                entry.setSearchScore(d.score);

//                LOGGER.error("Explanation: ---------------------");
//                LOGGER.error("Explanation: " + entry.getVendor() + " " + entry.getProduct() + " " + entry.getSearchScore());
//                LOGGER.error("Explanation: " + searchString);
//                LOGGER.error("Explanation: " + cpe.explain(query, d.doc));
                if (!ret.contains(entry)) {
                    ret.add(entry);
                }
                //}
            }
            return ret;
        } catch (ParseException ex) {
            LOGGER.warn("An error occurred querying the CPE data. See the log for more details.");
            LOGGER.info("Unable to parse: {}", searchString, ex);
        } catch (IndexException ex) {
            LOGGER.warn("An error occurred resetting the CPE index searcher. See the log for more details.");
            LOGGER.info("Unable to reset the search analyzer", ex);
        } catch (IOException ex) {
            LOGGER.warn("An error occurred reading CPE data. See the log for more details.");
            LOGGER.info("IO Error with search string: {}", searchString, ex);
        }
        return null;
    }

    /**
     * <p>
     * Builds a Lucene search string by properly escaping data and constructing
     * a valid search query.</p>
     *
     * <p>
     * If either the possibleVendor or possibleProducts lists have been
     * populated this data is used to add weighting factors to the search string
     * generated.</p>
     *
     * @param vendor text to search the vendor field
     * @param product text to search the product field
     * @param vendorWeighting a list of strings to apply to the vendor to boost
     * the terms weight
     * @param productWeightings a list of strings to apply to the product to
     * boost the terms weight
     * @return the Lucene query
     */
    protected String buildSearch(Map<String, MutableInt> vendor, Map<String, MutableInt> product,
            Set<String> vendorWeighting, Set<String> productWeightings) {

        final StringBuilder sb = new StringBuilder();

        if (!appendWeightedSearch(sb, Fields.PRODUCT, product, productWeightings)) {
            return null;
        }
        sb.append(" AND ");
        if (!appendWeightedSearch(sb, Fields.VENDOR, vendor, vendorWeighting)) {
            return null;
        }
        return sb.toString();
    }

    /**
     * This method constructs a Lucene query for a given field. The searchText
     * is split into separate words and if the word is within the list of
     * weighted words then an additional weighting is applied to the term as it
     * is appended into the query.
     *
     * @param sb a StringBuilder that the query text will be appended to.
     * @param field the field within the Lucene index that the query is
     * searching.
     * @param terms text used to construct the query.
     * @param weightedText a list of terms that will be considered higher
     * importance when searching.
     * @return if the append was successful.
     */
    @SuppressWarnings("StringSplitter")
    private boolean appendWeightedSearch(StringBuilder sb, String field, Map<String, MutableInt> terms, Set<String> weightedText) {
        if (terms.isEmpty()) {
            return false;
        }
        sb.append(field).append(":(");
        boolean addSpace = false;
        boolean addedTerm = false;

        for (Map.Entry<String, MutableInt> entry : terms.entrySet()) {
            final StringBuilder boostedTerms = new StringBuilder();
            final int weighting = entry.getValue().intValue();
            final String[] text = entry.getKey().split(" ");
            for (String word : text) {
                if (word.isEmpty()) {
                    continue;
                }
                if (addSpace) {
                    sb.append(" ");
                } else {
                    addSpace = true;
                }
                addedTerm = true;
                if (LuceneUtils.isKeyword(word)) {
                    sb.append("\"");
                    LuceneUtils.appendEscapedLuceneQuery(sb, word);
                    sb.append("\"");
                } else {
                    LuceneUtils.appendEscapedLuceneQuery(sb, word);
                }
                final String boostTerm = findBoostTerm(word, weightedText);

                //The weighting is on a full phrase rather then at a term level for vendor or products
                //TODO - should the weighting be at a "word" level as opposed to phrase level? Or combined word and phrase?
                //remember the reason we are counting the frequency of "phrases" as opposed to terms is that
                //we need to keep the correct sequence of terms from the evidence so the term concatenating analyzer
                //works correctly and will causes searches to take spring framework and produce: spring springframework framework
                if (boostTerm != null) {
                    sb.append("^").append(weighting + WEIGHTING_BOOST);
                    if (!boostTerm.equals(word)) {
                        boostedTerms.append(" ");
                        LuceneUtils.appendEscapedLuceneQuery(boostedTerms, boostTerm);
                        boostedTerms.append("^").append(weighting + WEIGHTING_BOOST);
                    }
                } else if (weighting > 1) {
                    sb.append("^").append(weighting);
                }
            }
            if (boostedTerms.length() > 0) {
                sb.append(boostedTerms);
            }
        }
        sb.append(")");
        return addedTerm;
    }

    /**
     * Removes characters from the input text that are not used within the CPE
     * index.
     *
     * @param text is the text to remove the characters from.
     * @return the text having removed some characters.
     */
    private String cleanseText(String text) {
        return text.replaceAll(CLEANSE_CHARACTER_RX, " ");
    }

    /**
     * Searches the collection of boost terms for the given term. The elements
     * are case insensitive matched using only the alpha-numeric contents of the
     * terms; all other characters are removed.
     *
     * @param term the term to search for
     * @param boost the collection of boost terms
     * @return the value identified
     */
    private String findBoostTerm(String term, Set<String> boost) {
        for (String entry : boost) {
            if (equalsIgnoreCaseAndNonAlpha(term, entry)) {
                return entry;
            }
        }
        return null;
    }

    /**
     * Compares two strings after lower casing them and removing the non-alpha
     * characters.
     *
     * @param l string one to compare.
     * @param r string two to compare.
     * @return whether or not the two strings are similar.
     */
    private boolean equalsIgnoreCaseAndNonAlpha(String l, String r) {
        if (l == null || r == null) {
            return false;
        }

        final String left = l.replaceAll(CLEANSE_NONALPHA_RX, "");
        final String right = r.replaceAll(CLEANSE_NONALPHA_RX, "");
        return left.equalsIgnoreCase(right);
    }

    /**
     * Ensures that the CPE Identified matches the dependency. This validates
     * that the product, vendor, and version information for the CPE are
     * contained within the dependencies evidence.
     *
     * @param entry a CPE entry
     * @param dependency the dependency that the CPE entries could be for
     * @param majorVersions the major versions detected for the dependency
     * @return whether or not the entry is valid.
     */
    private boolean verifyEntry(final IndexEntry entry, final Dependency dependency,
            final Set<String> majorVersions) {
        boolean isValid = false;
        //TODO - does this nullify some of the fuzzy matching that happens in the lucene search?
        // for instance CPE some-component and in the evidence we have SomeComponent.

        //TODO - should this have a package manager only flag instead of just looking for NPM
        if (Ecosystem.NODEJS.equals(dependency.getEcosystem())) {
            for (Identifier i : dependency.getSoftwareIdentifiers()) {
                if (i instanceof PurlIdentifier) {
                    final PurlIdentifier p = (PurlIdentifier) i;
                    if (cleanPackageName(p.getName()).equals(cleanPackageName(entry.getProduct()))) {
                        isValid = true;
                    }
                }
            }
        } else if (collectionContainsString(dependency.getEvidence(EvidenceType.VENDOR), entry.getVendor())) {
            if (collectionContainsString(dependency.getEvidence(EvidenceType.PRODUCT), entry.getProduct())) {
                isValid = true;
            } else {
                isValid = majorVersions.stream().filter(version
                        -> version != null && entry.getProduct().endsWith("v" + version) && entry.getProduct().length() > version.length() + 1)
                        .anyMatch(version
                                -> collectionContainsString(dependency.getEvidence(EvidenceType.PRODUCT),
                                entry.getProduct().substring(0, entry.getProduct().length() - version.length() - 1))
                        );
                isValid |= majorVersions.stream().filter(version
                        -> version != null && entry.getProduct().endsWith(version) && entry.getProduct().length() > version.length())
                        .anyMatch(version
                                -> collectionContainsString(dependency.getEvidence(EvidenceType.PRODUCT),
                                entry.getProduct().substring(0, entry.getProduct().length() - version.length()))
                        );
            }
        }
        return isValid;
    }

    /**
     * Only returns alpha numeric characters contained in a given package name.
     *
     * @param name the package name to cleanse
     * @return the cleansed packaage name
     */
    private String cleanPackageName(String name) {
        if (name == null) {
            return "";
        }
        return name.replaceAll("[^a-zA-Z0-9]+", "");
    }

    /**
     * Used to determine if the EvidenceCollection contains a specific string.
     *
     * @param evidence an of evidence object to check
     * @param text the text to search for
     * @return whether or not the EvidenceCollection contains the string
     */
    @SuppressWarnings("StringSplitter")
    private boolean collectionContainsString(Set<Evidence> evidence, String text) {
        //TODO - likely need to change the split... not sure if this will work for CPE with special chars
        if (text == null) {
            return false;
        }
        // Check if we have an exact match
        final String textLC = text.toLowerCase();
        for (Evidence e : evidence) {
            if (e.getValue().toLowerCase().equals(textLC)) {
                return true;
            }
        }

        final String[] words = text.split("[\\s_-]+");
        final List<String> list = new ArrayList<>();
        String tempWord = null;
        final CharArraySet stopWords = SearchFieldAnalyzer.getStopWords();
        for (String word : words) {
            /*
             single letter words should be concatenated with the next word.
             so { "m", "core", "sample" } -> { "mcore", "sample" }
             */
            if (tempWord != null) {
                list.add(tempWord + word);
                tempWord = null;
            } else if (word.length() <= 2) {
                tempWord = word;
            } else {
                if (stopWords.contains(word)) {
                    continue;
                }
                list.add(word);
            }
        }
        if (tempWord != null) {
            if (!list.isEmpty()) {
                final String tmp = list.get(list.size() - 1) + tempWord;
                list.add(tmp);
            } else {
                list.add(tempWord);
            }
        }
        if (list.isEmpty()) {
            return false;
        }
        boolean isValid = true;

        // Prepare the evidence values, e.g. remove the characters we used for splitting
        final List<String> evidenceValues = new ArrayList<>(evidence.size());
        evidence.forEach((e) -> evidenceValues.add(e.getValue().toLowerCase().replaceAll("[\\s_-]+", "")));

        for (String word : list) {
            word = word.toLowerCase();
            boolean found = false;
            for (String e : evidenceValues) {
                if (e.contains(word)) {
                    if ("http".equals(word) && e.contains("http:")) {
                        continue;
                    }
                    found = true;
                    break;
                }
            }
            isValid &= found;
        }
        return isValid;
    }

    /**
     * Analyzes a dependency and attempts to determine if there are any CPE
     * identifiers for this dependency.
     *
     * @param dependency The Dependency to analyze.
     * @param engine The analysis engine
     * @throws AnalysisException is thrown if there is an issue analyzing the
     * dependency.
     */
    @Override
    protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
        if (skipEcosystems.contains(dependency.getEcosystem())) {
            return;
        }
        try {
            determineCPE(dependency);
        } catch (CorruptIndexException ex) {
            throw new AnalysisException("CPE Index is corrupt.", ex);
        } catch (IOException ex) {
            throw new AnalysisException("Failure opening the CPE Index.", ex);
        } catch (ParseException ex) {
            throw new AnalysisException("Unable to parse the generated Lucene query for this dependency.", ex);
        }
    }

    /**
     * Retrieves a list of CPE values from the CveDB based on the vendor and
     * product passed in. The list is then validated to find only CPEs that are
     * valid for the given dependency. It is possible that the CPE identified is
     * a best effort "guess" based on the vendor, product, and version
     * information.
     *
     * @param dependency the Dependency being analyzed
     * @param vendor the vendor for the CPE being analyzed
     * @param product the product for the CPE being analyzed
     * @param currentConfidence the current confidence being used during
     * analysis
     * @return <code>true</code> if an identifier was added to the dependency;
     * otherwise <code>false</code>
     * @throws UnsupportedEncodingException is thrown if UTF-8 is not supported
     * @throws AnalysisException thrown if the suppression rules failed
     */
    @SuppressWarnings("StringSplitter")
    protected boolean determineIdentifiers(Dependency dependency, String vendor, String product,
            Confidence currentConfidence) throws UnsupportedEncodingException, AnalysisException {

        final CpeBuilder cpeBuilder = new CpeBuilder();

        final Set<CpePlus> cpePlusEntries = cve.getCPEs(vendor, product);
        final Set<Cpe> cpes = filterEcosystem(dependency.getEcosystem(), cpePlusEntries);
        if (cpes == null || cpes.isEmpty()) {
            return false;
        }

        DependencyVersion bestGuess;
        if ("Golang".equals(dependency.getEcosystem()) && dependency.getVersion() == null) {
            bestGuess = new DependencyVersion("*");
        } else {
            bestGuess = new DependencyVersion("-");
        }
        String bestGuessUpdate = null;
        Confidence bestGuessConf = null;
        String bestGuessURL = null;
        final Set<IdentifierMatch> collected = new HashSet<>();

        considerDependencyVersion(dependency, vendor, product, currentConfidence, collected);

        //TODO the following algorithm incorrectly identifies things as a lower version
        // if there lower confidence evidence when the current (highest) version number
        // is newer then anything in the NVD.
        for (Confidence conf : Confidence.values()) {
            for (Evidence evidence : dependency.getIterator(EvidenceType.VERSION, conf)) {
                final DependencyVersion evVer = DependencyVersionUtil.parseVersion(evidence.getValue(), true);
                if (evVer == null) {
                    continue;
                }
                DependencyVersion evBaseVer = null;
                String evBaseVerUpdate = null;
                final int idx = evVer.getVersionParts().size() - 1;
                if (evVer.getVersionParts().get(idx)
                        .matches("^(v|release|final|snapshot|beta|alpha|u|rc|m|20\\d\\d).*$")) {
                    //store the update version
                    final String checkUpdate = evVer.getVersionParts().get(idx);
                    if (checkUpdate.matches("^(v|release|final|snapshot|beta|alpha|u|rc|m|20\\d\\d).*$")) {
                        evBaseVerUpdate = checkUpdate;
                        evBaseVer = new DependencyVersion();
                        evBaseVer.setVersionParts(evVer.getVersionParts().subList(0, idx));
                    }
                }
                //TODO - review and update for new JSON data
                for (Cpe vs : cpes) {
                    final DependencyVersion dbVer = DependencyVersionUtil.parseVersion(vs.getVersion());
                    DependencyVersion dbVerUpdate = dbVer;
                    if (vs.getUpdate() != null && !vs.getUpdate().isEmpty() && !vs.getUpdate().startsWith("*") && !vs.getUpdate().startsWith("-")) {
                        dbVerUpdate = DependencyVersionUtil.parseVersion(vs.getVersion() + '.' + vs.getUpdate(), true);
                    }
                    if (dbVer == null) { //special case, no version specified - everything is vulnerable
                        final String url = String.format(NVD_SEARCH_BROAD_URL, URLEncoder.encode(vs.getVendor(), UTF8),
                                URLEncoder.encode(vs.getProduct(), UTF8));
                        final IdentifierMatch match = new IdentifierMatch(vs, url, IdentifierConfidence.BROAD_MATCH, conf);
                        collected.add(match);
                    } else if (evVer.equals(dbVer)) {
                        addExactMatch(vs, evBaseVerUpdate, conf, collected);
                    } else if (evBaseVer != null && evBaseVer.equals(dbVer)
                            && (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0)) {
                        bestGuessConf = conf;
                        bestGuess = dbVer;
                        bestGuessUpdate = evBaseVerUpdate;
                        bestGuessURL = String.format(NVD_SEARCH_URL, URLEncoder.encode(vs.getVendor(), UTF8),
                                URLEncoder.encode(vs.getProduct(), UTF8), URLEncoder.encode(vs.getVersion(), UTF8));
                    } else if (dbVerUpdate != null && evVer.getVersionParts().size() <= dbVerUpdate.getVersionParts().size()
                            && evVer.matchesAtLeastThreeLevels(dbVerUpdate)) {
                        if (bestGuessConf == null || bestGuessConf.compareTo(conf) > 0) {
                            if (bestGuess.getVersionParts().size() < dbVer.getVersionParts().size()) {
                                bestGuess = dbVer;
                                bestGuessUpdate = evBaseVerUpdate;
                                bestGuessConf = conf;
                            }
                        }
                    }
                }
                if ((bestGuessConf == null || bestGuessConf.compareTo(conf) > 0)
                        && bestGuess.getVersionParts().size() < evVer.getVersionParts().size()) {
                    bestGuess = evVer;
                    bestGuessUpdate = evBaseVerUpdate;
                    bestGuessConf = conf;
                }
            }
        }

        cpeBuilder.part(Part.APPLICATION).vendor(vendor).product(product);
        final int idx = bestGuess.getVersionParts().size() - 1;
        if (bestGuess.getVersionParts().get(idx)
                .matches("^(v|release|final|snapshot|beta|alpha|u|rc|m|20\\d\\d).*$")) {
            cpeBuilder.version(StringUtils.join(bestGuess.getVersionParts().subList(0, idx), "."));
            //when written - no update versions in the NVD start with v### - they all strip the v off
            if (bestGuess.getVersionParts().get(idx).matches("^v\\d.*$")) {
                cpeBuilder.update(bestGuess.getVersionParts().get(idx).substring(1));
            } else {
                cpeBuilder.update(bestGuess.getVersionParts().get(idx));
            }
        } else {
            cpeBuilder.version(bestGuess.toString());
            if (bestGuessUpdate != null) {
                cpeBuilder.update(bestGuessUpdate);
            }
        }
        final Cpe guessCpe;

        try {
            guessCpe = cpeBuilder.build();
        } catch (CpeValidationException ex) {
            throw new AnalysisException(String.format("Unable to create a CPE for %s:%s:%s", vendor, product, bestGuess));
        }
        if (!"-".equals(guessCpe.getVersion())) {
            String url = null;
            if (bestGuessURL != null) {
                url = bestGuessURL;
            }
            if (bestGuessConf == null) {
                bestGuessConf = Confidence.LOW;
            }
            final IdentifierMatch match = new IdentifierMatch(guessCpe, url, IdentifierConfidence.BEST_GUESS, bestGuessConf);

            collected.add(match);
        }
        boolean identifierAdded = false;
        if (!collected.isEmpty()) {
            final List<IdentifierMatch> items = new ArrayList<>(collected);

            Collections.sort(items);
            final IdentifierConfidence bestIdentifierQuality = items.get(0).getIdentifierConfidence();
            final Confidence bestEvidenceQuality = items.get(0).getEvidenceConfidence();
            boolean addedNonGuess = false;
            final Confidence prevAddedConfidence = dependency.getVulnerableSoftwareIdentifiers().stream().map(Identifier::getConfidence)
                    .min(Comparator.comparing(Confidence::ordinal))
                    .orElse(Confidence.LOW);

            for (IdentifierMatch m : items) {
                if (bestIdentifierQuality.equals(m.getIdentifierConfidence())
                        && bestEvidenceQuality.equals(m.getEvidenceConfidence())) {
                    final CpeIdentifier i = m.getIdentifier();
                    if (bestIdentifierQuality == IdentifierConfidence.BEST_GUESS) {
                        if (addedNonGuess) {
                            continue;
                        }
                        i.setConfidence(Confidence.LOW);
                    } else {
                        i.setConfidence(bestEvidenceQuality);
                    }
                    if (prevAddedConfidence.compareTo(i.getConfidence()) < 0) {
                        continue;
                    }

                    //TODO - while this gets the job down it is slow; consider refactoring
                    dependency.addVulnerableSoftwareIdentifier(i);
                    suppression.analyze(dependency, engine);
                    if (dependency.getVulnerableSoftwareIdentifiers().contains(i)) {
                        identifierAdded = true;
                        if (!addedNonGuess && bestIdentifierQuality != IdentifierConfidence.BEST_GUESS) {
                            addedNonGuess = true;
                        }
                    }
                }
            }
        }
        return identifierAdded;
    }

    /**
     * Adds a new CPE to the identifier match collection.
     *
     * @param vs a reference to the vulnerable software
     * @param updateVersion the update version
     * @param conf the current confidence
     * @param collected a reference to the collected identifiers
     * @throws UnsupportedEncodingException thrown if UTF-8 is not supported
     */
    private void addExactMatch(Cpe vs, String updateVersion, Confidence conf,
            final Set<IdentifierMatch> collected) throws UnsupportedEncodingException {

        final CpeBuilder cpeBuilder = new CpeBuilder();
        final String url = String.format(NVD_SEARCH_URL, URLEncoder.encode(vs.getVendor(), UTF8),
                URLEncoder.encode(vs.getProduct(), UTF8), URLEncoder.encode(vs.getVersion(), UTF8));
        Cpe useCpe;
        if (updateVersion != null && "*".equals(vs.getUpdate())) {
            try {
                useCpe = cpeBuilder.part(vs.getPart()).wfVendor(vs.getWellFormedVendor())
                        .wfProduct(vs.getWellFormedProduct()).wfVersion(vs.getWellFormedVersion())
                        .wfEdition(vs.getWellFormedEdition()).wfLanguage(vs.getWellFormedLanguage())
                        .wfOther(vs.getWellFormedOther()).wfSwEdition(vs.getWellFormedSwEdition())
                        .update(updateVersion).build();
            } catch (CpeValidationException ex) {
                LOGGER.debug("Error building cpe with update:" + updateVersion, ex);
                useCpe = vs;
            }
        } else {
            useCpe = vs;
        }
        final IdentifierMatch match = new IdentifierMatch(useCpe, url, IdentifierConfidence.EXACT_MATCH, conf);
        collected.add(match);
    }

    /**
     * Evaluates whether or not to use the `version` of the dependency instead
     * of the version evidence. The dependency should not always be used as it
     * can cause FP.
     *
     * @param dependency the dependency being analyzed
     * @param product the product name
     * @param vendor the vendor name
     * @param confidence the current confidence level
     * @param collected a reference to the identifiers matched
     * @throws AnalysisException thrown if aliens attacked and valid input could
     * not be used to construct a CPE
     * @throws UnsupportedEncodingException thrown if run on a system that
     * doesn't support UTF-8
     */
    private void considerDependencyVersion(Dependency dependency,
            String vendor, String product, Confidence confidence,
            final Set<IdentifierMatch> collected)
            throws AnalysisException, UnsupportedEncodingException {

        if (dependency.getVersion() != null && !dependency.getVersion().isEmpty()) {
            final CpeBuilder cpeBuilder = new CpeBuilder();
            boolean useDependencyVersion = true;
            final CharArraySet stopWords = SearchFieldAnalyzer.getStopWords();
            if (dependency.getName() != null && !dependency.getName().isEmpty()) {
                final String name = dependency.getName();
                for (String word : product.split("[^a-zA-Z0-9]")) {
                    useDependencyVersion &= name.contains(word) || stopWords.contains(word)
                            || wordMatchesEcosystem(dependency.getEcosystem(), word);
                }
            }

            if (useDependencyVersion) {
                //TODO - we need to filter this so that we only use this if something in the
                //dependency.getName() matches the vendor/product in some way
                final DependencyVersion depVersion = new DependencyVersion(dependency.getVersion());
                if (depVersion.getVersionParts().size() > 0) {
                    cpeBuilder.part(Part.APPLICATION).vendor(vendor).product(product);
                    addVersionAndUpdate(depVersion, cpeBuilder);
                    try {
                        final Cpe depCpe = cpeBuilder.build();
                        final String url = String.format(NVD_SEARCH_URL, URLEncoder.encode(vendor, UTF8),
                                URLEncoder.encode(product, UTF8), URLEncoder.encode(depCpe.getVersion(), UTF8));
                        final IdentifierMatch match = new IdentifierMatch(depCpe, url, IdentifierConfidence.EXACT_MATCH, confidence);
                        collected.add(match);
                    } catch (CpeValidationException ex) {
                        throw new AnalysisException(String.format("Unable to create a CPE for %s:%s:%s", vendor, product, depVersion));
                    }
                }
            }
        }
    }

    /**
     * If a CPE product word represents the ecosystem of a dependency it is not required
     * to appear in the dependencyName to still consider the CPE product a match.
     *
     * @param ecosystem The ecosystem of the dependency
     * @param word       The word from the CPE product to check
     * @return {@code true} when the CPE product word is known to match the ecosystem of the dependency
     * @implNote This method is not intended to cover every possible case where the ecosystem is represented by the word. It is a
     * best-effort attempt to prevent {@link #considerDependencyVersion(Dependency, String, String, Confidence, Set)}
     * from not taking an exact-match versioned CPE into account because the ecosystem-related word does not appear in
     * the dependencyName. It helps prevent false-positive cases like https://github.com/jeremylong/DependencyCheck/issues/5545
     * @see #considerDependencyVersion(Dependency, String, String, Confidence, Set)
     */
    private boolean wordMatchesEcosystem(@Nullable String ecosystem, String word) {
        if (Ecosystem.JAVA.equalsIgnoreCase(word)) {
            return Ecosystem.JAVA.equals(ecosystem);
        }
        return false;
    }

    /**
     * <p>
     * Returns the setting key to determine if the analyzer is enabled.</p>
     *
     * @return the key for the analyzer's enabled property
     */
    @Override
    protected String getAnalyzerEnabledSettingKey() {
        return Settings.KEYS.ANALYZER_CPE_ENABLED;
    }

    /**
     * Filters the given list of CPE Entries (plus ecosystem) for the given
     * dependencies ecosystem.
     *
     * @param ecosystem the dependencies ecosystem
     * @param entries the CPE Entries (plus ecosystem)
     * @return the filtered list of CPE entries
     */
    private Set<Cpe> filterEcosystem(String ecosystem, Set<CpePlus> entries) {
        if (entries == null || entries.isEmpty()) {
            return null;
        }
        if (ecosystem != null) {
            return entries.stream().filter(c
                    -> c.getEcosystem() == null
                    || c.getEcosystem().equals(ecosystem)
                    //some ios CVE/CPEs are listed under native
                    || (Ecosystem.IOS.equals(ecosystem) && Ecosystem.NATIVE.equals(c.getEcosystem())))
                    .map(CpePlus::getCpe)
                    .collect(Collectors.toSet());
        }
        return entries.stream()
                .map(CpePlus::getCpe)
                .collect(Collectors.toSet());
    }

    /**
     * Add the given version to the CpeBuilder - this method attempts to parse
     * out the update from the version and correctly set the value in the CPE.
     *
     * @param depVersion the version to add
     * @param cpeBuilder a reference to the CPE Builder
     */
    private void addVersionAndUpdate(DependencyVersion depVersion, final CpeBuilder cpeBuilder) {
        final int idx = depVersion.getVersionParts().size() - 1;
        if (idx > 0 && depVersion.getVersionParts().get(idx)
                .matches("^(v|final|release|snapshot|r|b|beta|a|alpha|u|rc|sp|dev|revision|service|build|pre|p|patch|update|m|20\\d\\d).*$")) {
            cpeBuilder.version(StringUtils.join(depVersion.getVersionParts().subList(0, idx), "."));
            //when written - no update versions in the NVD start with v### - they all strip the v off
            if (depVersion.getVersionParts().get(idx).matches("^v\\d.*$")) {
                cpeBuilder.update(depVersion.getVersionParts().get(idx).substring(1));
            } else {
                cpeBuilder.update(depVersion.getVersionParts().get(idx));
            }
        } else {
            cpeBuilder.version(depVersion.toString());
        }
    }

    /**
     * The confidence whether the identifier is an exact match, or a best guess.
     */
    private enum IdentifierConfidence {

        /**
         * An exact match for the CPE.
         */
        EXACT_MATCH,
        /**
         * A best guess for the CPE.
         */
        BEST_GUESS,
        /**
         * The entire vendor/product group must be added (without a guess at
         * version) because there is a CVE with a VS that only specifies
         * vendor/product.
         */
        BROAD_MATCH
    }

    /**
     * A simple object to hold an identifier and carry information about the
     * confidence in the identifier.
     */
    private static class IdentifierMatch implements Comparable<IdentifierMatch> {

        /**
         * The confidence whether this is an exact match, or a best guess.
         */
        private IdentifierConfidence identifierConfidence;
        /**
         * The CPE identifier.
         */
        private CpeIdentifier identifier;

        /**
         * Constructs an IdentifierMatch.
         *
         * @param cpe the CPE value for the match
         * @param url the URL of the identifier
         * @param identifierConfidence the confidence in the identifier: best
         * guess or exact match
         * @param evidenceConfidence the confidence of the evidence used to find
         * the identifier
         */
        IdentifierMatch(Cpe cpe, String url, IdentifierConfidence identifierConfidence, Confidence evidenceConfidence) {
            this.identifier = new CpeIdentifier(cpe, url, evidenceConfidence);
            this.identifierConfidence = identifierConfidence;
        }

        //<editor-fold defaultstate="collapsed" desc="Property implementations: evidenceConfidence, confidence, identifier">
        /**
         * Get the value of evidenceConfidence
         *
         * @return the value of evidenceConfidence
         */
        public Confidence getEvidenceConfidence() {
            return this.identifier.getConfidence();
        }

        /**
         * Set the value of evidenceConfidence
         *
         * @param evidenceConfidence new value of evidenceConfidence
         */
        public void setEvidenceConfidence(Confidence evidenceConfidence) {
            this.identifier.setConfidence(evidenceConfidence);
        }

        /**
         * Get the value of confidence.
         *
         * @return the value of confidence
         */
        public IdentifierConfidence getIdentifierConfidence() {
            return identifierConfidence;
        }

        /**
         * Set the value of confidence.
         *
         * @param confidence new value of confidence
         */
        public void setIdentifierConfidence(IdentifierConfidence confidence) {
            this.identifierConfidence = confidence;
        }

        /**
         * Get the value of identifier.
         *
         * @return the value of identifier
         */
        public CpeIdentifier getIdentifier() {
            return identifier;
        }

        /**
         * Set the value of identifier.
         *
         * @param identifier new value of identifier
         */
        public void setIdentifier(CpeIdentifier identifier) {
            this.identifier = identifier;
        }
        //</editor-fold>
        //<editor-fold defaultstate="collapsed" desc="Standard implementations of toString, hashCode, and equals">

        /**
         * Standard toString() implementation.
         *
         * @return the string representation of the object
         */
        @Override
        public String toString() {
            return "IdentifierMatch{ IdentifierConfidence=" + identifierConfidence + ", identifier=" + identifier + '}';
        }

        /**
         * Standard hashCode() implementation.
         *
         * @return the hashCode
         */
        @Override
        public int hashCode() {
            return new HashCodeBuilder(115, 303)
                    .append(identifierConfidence)
                    .append(identifier)
                    .toHashCode();
        }

        /**
         * Standard equals implementation.
         *
         * @param obj the object to compare
         * @return true if the objects are equal, otherwise false
         */
        @Override
        public boolean equals(Object obj) {
            if (obj == null || !(obj instanceof IdentifierMatch)) {
                return false;
            }
            if (this == obj) {
                return true;
            }
            final IdentifierMatch other = (IdentifierMatch) obj;
            return new EqualsBuilder()
                    .append(identifierConfidence, other.identifierConfidence)
                    .append(identifier, other.identifier)
                    .build();
        }
        //</editor-fold>

        /**
         * Standard implementation of compareTo that compares identifier
         * confidence, evidence confidence, and then the identifier.
         *
         * @param o the IdentifierMatch to compare to
         * @return the natural ordering of IdentifierMatch
         */
        @Override
        public int compareTo(@NotNull IdentifierMatch o) {
            return new CompareToBuilder()
                    .append(identifierConfidence, o.identifierConfidence)
                    .append(identifier, o.identifier)
                    .toComparison();
        }
    }

    /**
     * Command line tool for querying the Lucene CPE Index.
     *
     * @param args not used
     */
    @SuppressWarnings("InfiniteLoopStatement")
    public static void main(String[] args) {
        final Settings props = new Settings();
        try (Engine en = new Engine(Engine.Mode.EVIDENCE_PROCESSING, props)) {
            en.openDatabase(false, false);
            final CPEAnalyzer analyzer = new CPEAnalyzer();
            analyzer.initialize(props);
            analyzer.prepareAnalyzer(en);
            LOGGER.error("test");
            System.out.println("Memory index query for ODC");
            try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))) {
                while (true) {

                    final Map<String, MutableInt> vendor = new HashMap<>();
                    final Map<String, MutableInt> product = new HashMap<>();
                    System.out.print("Vendor: ");
                    String[] parts = br.readLine().split(" ");
                    for (String term : parts) {
                        final MutableInt count = vendor.get(term);
                        if (count == null) {
                            vendor.put(term, new MutableInt(0));
                        } else {
                            count.add(1);
                        }
                    }
                    System.out.print("Product: ");
                    parts = br.readLine().split(" ");
                    for (String term : parts) {
                        final MutableInt count = product.get(term);
                        if (count == null) {
                            product.put(term, new MutableInt(0));
                        } else {
                            count.add(1);
                        }
                    }
                    final List<IndexEntry> list = analyzer.searchCPE(vendor, product, new HashSet<>(), new HashSet<>(), "default");
                    if (list == null || list.isEmpty()) {
                        System.out.println("No results found");
                    } else {
                        list.forEach((e) -> System.out.printf("%s:%s (%f)%n", e.getVendor(), e.getProduct(),
                                e.getSearchScore()));
                    }
                    System.out.println();
                    System.out.println();
                }
            }
        } catch (InitializationException | IOException ex) {
            System.err.println("Lucene ODC search tool failed:");
            System.err.println(ex.getMessage());
        }
    }

    /**
     * Sets the reference to the CveDB.
     *
     * @param cveDb the CveDB
     */
    protected void setCveDB(CveDB cveDb) {
        this.cve = cveDb;
    }

    /**
     * returns a reference to the CveDB.
     *
     * @return a reference to the CveDB
     */
    protected CveDB getCveDB() {
        return this.cve;
    }

    /**
     * Sets the MemoryIndex.
     *
     * @param idx the memory index
     */
    protected void setMemoryIndex(MemoryIndex idx) {
        cpe = idx;
    }

    /**
     * Returns the memory index.
     *
     * @return the memory index
     */
    protected MemoryIndex getMemoryIndex() {
        return cpe;
    }

    /**
     * Sets the CPE Suppression Analyzer.
     *
     * @param suppression the CPE Suppression Analyzer
     */
    protected void setCpeSuppressionAnalyzer(CpeSuppressionAnalyzer suppression) {
        this.suppression = suppression;
    }
}