/*
 * 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.dependency;

import io.github.jeremylong.openvulnerability.client.nvd.CvssV2;
import io.github.jeremylong.openvulnerability.client.nvd.CvssV3;
import io.github.jeremylong.openvulnerability.client.nvd.CvssV4;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.concurrent.NotThreadSafe;

import org.apache.commons.lang3.builder.CompareToBuilder;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.jetbrains.annotations.NotNull;
import org.owasp.dependencycheck.utils.SeverityUtil;

/**
 * Contains the information about a vulnerability.
 *
 * @author Jeremy Long
 */
@NotThreadSafe
public class Vulnerability implements Serializable, Comparable<Vulnerability> {

    /**
     * An enumeration for the source of vulnerability.
     */
    public enum Source {
        /**
         * National Vulnerability Database.
         */
        NVD,
        /**
         * NPM Public Advisory.
         */
        NPM,
        /**
         * RetireJS.
         */
        RETIREJS,
        /**
         * Sonatype OSS Index.
         */
        OSSINDEX,
        /**
         * Vulnerability from Bundle Audit.
         */
        BUNDLEAUDIT,
        /**
         * Vulnerability from Mix Audit.
         */
        MIXAUDIT
    }

    /**
     * The serial version uid.
     */
    private static final long serialVersionUID = 307319490326651053L;

    /**
     * The name of the vulnerability.
     */
    private String name;
    /**
     * the description of the vulnerability.
     */
    private String description;
    /**
     * Data if the vulnerability is a known exploited vulnerability.
     */
    private org.owasp.dependencycheck.data.knownexploited.json.Vulnerability knownExploitedVulnerability;
    /**
     * References for this vulnerability.
     */
    private final Set<Reference> references = Collections.synchronizedSet(new HashSet<>());
    /**
     * A set of vulnerable software.
     */
    private final Set<VulnerableSoftware> vulnerableSoftware = new HashSet<>();
    /**
     * The CWE(s) for the vulnerability.
     */
    private final CweSet cwes = new CweSet();
    /**
     * The severity a {@link Source} has assigned for which a CVSS score is not
     * available. Severity could be anything ranging from 'critical', 'high',
     * 'medium', and 'low', to non-traditional labels like 'major', 'minor', and
     * 'important'.
     */
    private String unscoredSeverity;
    /**
     * The CVSS V2 scoring information.
     */
    private CvssV2 cvssV2;

    /**
     * The CVSS V3 scoring information.
     */
    private CvssV3 cvssV3;

    /**
     * The CVSS V4 scoring information.
     */
    private CvssV4 cvssV4;

    /**
     * The Vulnerable Software that caused this vulnerability to be flagged.
     */
    private VulnerableSoftware matchedVulnerableSoftware;
    /**
     * Notes about the vulnerability. Generally used for suppression
     * information.
     */
    private String notes;

    /**
     * The source that identified the vulnerability.
     */
    private Source source = null;

    /**
     * Default constructor.
     */
    public Vulnerability() {
        //empty
    }

    /**
     * Constructs a new Vulnerability by its name.
     *
     * @param name the name of the vulnerability
     */
    public Vulnerability(String name) {
        this.name = name;
    }

    /**
     * Get the value of name.
     *
     * @return the value of name
     */
    public String getName() {
        return name;
    }

    /**
     * Set the value of name.
     *
     * @param name new value of name
     */
    public void setName(String name) {
        this.name = name;
    }

    /**
     * Get the value of description.
     *
     * @return the value of description
     */
    public String getDescription() {
        return description;
    }

    /**
     * Set the value of description.
     *
     * @param description new value of description
     */
    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * Get the value of references.
     *
     * @return the value of references
     */
    public Set<Reference> getReferences() {
        return references;
    }

    /**
     * Returns the list of references. This is primarily used within the
     * generated reports.
     *
     * @param sorted whether the returned list should be sorted
     * @return the list of references
     */
    public List<Reference> getReferences(boolean sorted) {
        final List<Reference> sortedRefs = new ArrayList<>(this.references);
        if (sorted) {
            Collections.sort(sortedRefs);
        }
        return sortedRefs;
    }

    /**
     * Adds the references to the collection.
     *
     * @param references a collection of references to add
     */
    public void addReferences(Set<Reference> references) {
        this.references.addAll(references);
    }

    /**
     * Adds a reference to the references collection.
     *
     * @param ref a reference for the vulnerability
     */
    public void addReference(Reference ref) {
        this.references.add(ref);
    }

    /**
     * Adds a reference.
     *
     * @param referenceSource the source of the reference
     * @param referenceName the referenceName of the reference
     * @param referenceUrl the url of the reference
     */
    public void addReference(String referenceSource, String referenceName, String referenceUrl) {
        final Reference ref = new Reference();
        ref.setSource(referenceSource);
        ref.setName(referenceName);
        ref.setUrl(referenceUrl);
        this.references.add(ref);
    }

    /**
     * Adds information about known exploited vulnerabilities.
     *
     * @param kev the known exploited vulnerability information
     */
    public void setKnownExploitedVulnerability(org.owasp.dependencycheck.data.knownexploited.json.Vulnerability kev) {
        this.knownExploitedVulnerability = kev;
    }

    /**
     * Get the value of knownExploitedVulnerability.
     *
     * @return the value of knownExploitedVulnerability
     */
    public org.owasp.dependencycheck.data.knownexploited.json.Vulnerability getKnownExploitedVulnerability() {
        return knownExploitedVulnerability;
    }

    /**
     * Get the value of vulnerableSoftware.
     *
     * @return the value of vulnerableSoftware
     */
    public Set<VulnerableSoftware> getVulnerableSoftware() {
        return vulnerableSoftware;
    }

    /**
     * Returns a sorted list of vulnerable software. This is primarily used for
     * display within reports.
     *
     * @param sorted whether or not the list should be sorted
     * @return the list of vulnerable software
     */
    @SuppressWarnings("unchecked")
    public List<VulnerableSoftware> getVulnerableSoftware(boolean sorted) {
        synchronized (vulnerableSoftware) {
            final List<VulnerableSoftware> sortedVulnerableSoftware = new ArrayList<>(this.vulnerableSoftware);
            if (sorted) {
                Collections.sort(sortedVulnerableSoftware);
            }
            return sortedVulnerableSoftware;
        }
    }

    /**
     * Adds the vulnerableSoftware to the collection.
     *
     * @param vulnerableSoftware a collection of vulnerable software
     */
    public void addVulnerableSoftware(Set<VulnerableSoftware> vulnerableSoftware) {
        this.vulnerableSoftware.addAll(vulnerableSoftware);
    }

    /**
     * Adds an entry for vulnerable software.
     *
     * @param software the vulnerable software reference to add
     */
    public void addVulnerableSoftware(VulnerableSoftware software) {
        vulnerableSoftware.add(software);
    }

    /**
     * Get the CVSS V2 scoring information.
     *
     * @return the CVSS V2 scoring information
     */
    public CvssV2 getCvssV2() {
        return cvssV2;
    }

    /**
     * Sets the CVSS V2 scoring information.
     *
     * @param cvssV2 the CVSS V2 scoring information
     */
    public void setCvssV2(CvssV2 cvssV2) {
        this.cvssV2 = cvssV2;
    }

    /**
     * Get the CVSS V3 scoring information.
     *
     * @return the CVSS V3 scoring information
     */
    public CvssV3 getCvssV3() {
        return cvssV3;
    }

    /**
     * Sets the CVSS V3 scoring information.
     *
     * @param cvssV3 the CVSS V3 scoring information
     */
    public void setCvssV3(CvssV3 cvssV3) {
        this.cvssV3 = cvssV3;
    }

    /**
     * Get the CVSS V3 scoring information.
     *
     * @return the CVSS V3 scoring information
     */
    public CvssV4 getCvssV4() {
        return cvssV4;
    }

    /**
     * Sets the CVSS V4 scoring information.
     *
     * @param cvssV4 the CVSS V4 scoring information
     */
    public void setCvssV4(CvssV4 cvssV4) {
        this.cvssV4 = cvssV4;
    }

    /**
     * Get the set of CWEs.
     *
     * @return the set of CWEs
     */
    public CweSet getCwes() {
        return cwes;
    }

    /**
     * Adds a CWE to the set.
     *
     * @param cwe new CWE to add
     */
    public void addCwe(String cwe) {
        this.cwes.addCwe(cwe);
    }

    /**
     * Retrieves the severity a {@link Source} has assigned for which a CVSS
     * score is not available. Severity could be anything ranging from
     * 'critical', 'high', 'medium', and 'low', to non-traditional labels like
     * 'major', 'minor', and 'important'.
     *
     * @return the un-scored severity
     */
    public String getUnscoredSeverity() {
        return unscoredSeverity;
    }

    /**
     * Sets the severity a {@link Source} has assigned for which a CVSS score is
     * not available. Severity could be anything ranging from 'critical',
     * 'high', 'medium', and 'low', to non-traditional labels like 'major',
     * 'minor', and 'important'.
     *
     * @param unscoredSeverity the un-scored severity
     */
    public void setUnscoredSeverity(String unscoredSeverity) {
        this.unscoredSeverity = unscoredSeverity;
    }

    /**
     * Get the value of notes from suppression notes.
     *
     * @return the value of notes
     */
    public String getNotes() {
        return notes;
    }

    /**
     * Set the value of notes.
     *
     * @param notes new value of notes
     */
    public void setNotes(String notes) {
        this.notes = notes;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null || !(obj instanceof Vulnerability)) {
            return false;
        }
        if (this == obj) {
            return true;
        }
        final Vulnerability other = (Vulnerability) obj;
        return new EqualsBuilder()
                .append(name, other.name)
                .isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder(3, 73)
                .append(name)
                .toHashCode();
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder("Vulnerability ");
        sb.append(this.name);
        sb.append("\nReferences:\n");
        for (Reference reference : getReferences(true)) {
            sb.append("=> ");
            sb.append(reference);
            sb.append("\n");
        }
        sb.append("\nSoftware:\n");

        for (VulnerableSoftware software : getVulnerableSoftware(true)) {
            sb.append("=> ");
            sb.append(software);
            sb.append("\n");
        }
        return sb.toString();
    }

    /**
     * Compares two vulnerabilities.<br>
     * Natural order of vulnerabilities is defined as decreasing in severity and
     * alphabetically by name for equal severity. This way the most severe
     * issues are listed first in a sorted list.
     * <br>
     * This uses a
     * {@link #bestEffortSeverityLevelForSorting() best-effort ordering} for
     * severity as the variety of sources do not guarantee a consistent
     * availability of standardized severity scores. The bestEffort severity
     * level estimation will use CVSSv3 baseScore for comparison when available
     * on both sides. If any of the vulnerabilities does not have a CVSSv3 score
     * the sort order may be off, but it will be consistent.
     * <br>
     * The ranking (high to low) of severity can be informally represented as      {@code &lt;CVSSv3 critical> >> &lt;Unscored recognized critical> >>
     *     &lt;Unscored unrecognized (assumed Critical)> >> &lt;Score-based comparison for high-or-lower scoring severities with
     *     recognized unscored severities taking the lower bound of the comparable CVSSv3 range>
     * }
     *
     * @param o a vulnerability to be compared
     * @return a negative integer, zero, or a positive integer as this object is
     * less than , equal to, or greater than the specified vulnerability
     * @see #bestEffortSeverityLevelForSorting()
     */
    @Override
    public int compareTo(@NotNull Vulnerability o) {
        return new CompareToBuilder()
                .append(o.bestEffortSeverityLevelForSorting(), this.bestEffortSeverityLevelForSorting())
                .append(this.name, o.name)
                .toComparison();
    }

    /**
     * Compute a best-effort score for the severity of a vulnerability for the
     * purpose of sorting.
     * <br>
     * Note that CVSSv2 and CVSSv3 scores are essentially uncomparable. For the
     * purpose of sorting we nevertheless treat them comparable, with an
     * exception for the 9.0-10.0 range. For that entire range CVSSv3 is scoring
     * more severe than CVSSv2, so that the 'CRITICAL' severity is retained to
     * be reported as the highest severity after sorting on descending severity.
     * <br>
     * For vulnerabilities not scored with a CVSS score we estimate a score from
     * the severity text. For textual severities assumed or semantically
     * confirmed to be of a critical nature we assign a value in between the
     * highest CVSSv2 HIGH and the lowest CVSSv3 CRITICAL severity level.
     *
     * @see SeverityUtil#estimatedSortAdjustedCVSSv3(String)
     * @see SeverityUtil#sortAdjustedCVSSv3BaseScore(float)
     * @return A float value that allows for best-effort sorting on
     * vulnerability severity
     */
    private Double bestEffortSeverityLevelForSorting() {
        if (this.cvssV4 != null) {
            return SeverityUtil.sortAdjustedCVSSv3BaseScore(this.cvssV4.getCvssData().getBaseScore());
        }
        if (this.cvssV3 != null) {
            return SeverityUtil.sortAdjustedCVSSv3BaseScore(this.cvssV3.getCvssData().getBaseScore());
        }
        if (this.cvssV2 != null) {
            return this.cvssV2.getCvssData().getBaseScore();
        }
        return SeverityUtil.estimatedSortAdjustedCVSSv3(this.unscoredSeverity);
    }

    /**
     * The report text to use for highest severity when this issue is ranked
     * highest.
     *
     * @return The string to display in the report, clarifying for unrecognized
     * unscored severities that critical is assumed.
     */
    public String getHighestSeverityText() {
        if (this.cvssV4 != null) {
            return this.cvssV4.getCvssData().getBaseSeverity().value().toUpperCase();
        }
        if (this.cvssV3 != null) {
            return this.cvssV3.getCvssData().getBaseSeverity().value().toUpperCase();
        }
        if (this.cvssV2 != null) {
            return this.cvssV2.getCvssData().getBaseSeverity().toUpperCase();
        }
        return SeverityUtil.unscoredToSeveritytext(this.unscoredSeverity).toUpperCase();
    }

    /**
     * Sets the CPE that caused this vulnerability to be flagged.
     *
     * @param software a Vulnerable Software identifier
     */
    public void setMatchedVulnerableSoftware(VulnerableSoftware software) {
        matchedVulnerableSoftware = software;
    }

    /**
     * Get the value of matchedVulnerableSoftware.
     *
     * @return the value of matchedVulnerableSoftware
     */
    public VulnerableSoftware getMatchedVulnerableSoftware() {
        return matchedVulnerableSoftware;
    }

    /**
     * Returns the source that identified the vulnerability.
     *
     * @return the source
     */
    public Source getSource() {
        return source;
    }

    /**
     * Sets the source that identified the vulnerability.
     *
     * @param source the source
     */
    public void setSource(Source source) {
        this.source = source;
    }
}
