All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatusHistoryEntry Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2021-2024 the original author or authors.
 *
 * 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.
 */
package com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus;

import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.analysis.utils.TimeUtils;
import com.metaeffekt.artifact.enrichment.vulnerability.VulnerabilityStatusPostProcessor;
import com.metaeffekt.mirror.contents.vulnerability.Vulnerability;
import lombok.Getter;
import lombok.Setter;
import org.json.JSONArray;
import org.json.JSONObject;
import org.metaeffekt.core.inventory.processor.model.VulnerabilityMetaData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;

@Getter
@Setter
public class VulnerabilityStatusHistoryEntry implements Comparable, Cloneable {

    private final static Logger LOG = LoggerFactory.getLogger(VulnerabilityStatusHistoryEntry.class);

    private String status, rationale, risk, measures, author;
    private Date date;
    private String[] includes, excludes;
    private Double score;
    private int priority = 0;

    /**
     * The active flag is being set by the {@link VulnerabilityStatus#getLabelFilteredStatusHistory(String[])} method
     * when filtering the active status history entries.
     */
    private boolean active = true;
    private VulnerabilityStatus.Scope scope = VulnerabilityStatus.Scope.ARTIFACT;

    public VulnerabilityStatusHistoryEntry() {
    }

    public VulnerabilityStatusHistoryEntry(String status, String rationale, String risk, String measures, String author, String date, Double score, String[] includes, String[] excludes) {
        setStatus(status);
        this.rationale = rationale;
        this.risk = risk;
        this.measures = measures;
        this.author = author;
        this.score = score;
        this.includes = includes;
        this.excludes = excludes;

        setDate(date);
    }

    public VulnerabilityStatusHistoryEntry(String status, String rationale, String risk, String measures, String author, String date, double score, String[] includes, String[] excludes, boolean active, VulnerabilityStatus.Scope scope) {
        this(status, rationale, risk, measures, author, date, score, includes, excludes);
        this.active = active;
        this.scope = scope;
    }

    public static VulnerabilityStatusHistoryEntry fromMap(Map values) {
        final VulnerabilityStatusHistoryEntry parsedEntry = new VulnerabilityStatusHistoryEntry();

        if (values.containsKey("status")) {
            parsedEntry.setStatus(values.get("status").toString());
        }

        if (values.containsKey("rationale")) {
            parsedEntry.rationale = values.get("rationale").toString();
        }
        if (values.containsKey("risk")) {
            parsedEntry.risk = values.get("risk").toString();
        }
        if (values.containsKey("measures")) {
            parsedEntry.measures = values.get("measures").toString();
        }

        if (values.containsKey("statusScore")) {
            parsedEntry.score = Double.parseDouble(values.get("statusScore").toString());
        } else if (values.containsKey("score")) {
            parsedEntry.score = Double.parseDouble(values.get("score").toString());
        } else {
            parsedEntry.score = null;
        }

        if (values.containsKey("author")) {
            parsedEntry.author = values.get("author").toString();
        }

        if (values.containsKey("date")) {
            if (values.get("date") instanceof Date) {
                parsedEntry.date = (Date) values.get("date");
            } else {
                parsedEntry.setDate(values.get("date").toString());
            }
        }

        if (values.containsKey("labels") && values.get("labels") instanceof Map) {
            Map labels = (Map) values.get("labels");
            if (labels.containsKey("includes")) {
                parsedEntry.includes = extractStringArray(labels.get("includes"));
            }
            if (labels.containsKey("excludes")) {
                parsedEntry.excludes = extractStringArray(labels.get("excludes"));
            }
        }

        if (values.containsKey("active")) {
            parsedEntry.active = Boolean.parseBoolean(values.get("active").toString());
        }

        if (values.containsKey("scope")) {
            parsedEntry.scope = VulnerabilityStatus.Scope.valueOf(values.get("scope").toString());
        }

        if (values.containsKey("priority")) {
            parsedEntry.priority = Integer.parseInt(values.get("priority").toString());
        }

        return parsedEntry;
    }

    public static String[] extractStringArray(Object l) {
        if (l instanceof String) {
            if (l.toString().isEmpty()) return null;
            return l.toString().split(", ?");
        } else if (l instanceof ArrayList) {
            return ((ArrayList) l).toArray(new String[0]);
        }
        return null;
    }

    public VulnerabilityStatusHistoryEntry setStatus(String status) {
        if (status == null) {
            throw new IllegalArgumentException("Status must not be null");
        }

        switch (status) {
            default:
                LOG.warn("Unknown vulnerability assessment status [{}]", status);
            case VulnerabilityMetaData.STATUS_VALUE_NOTAPPLICABLE:
            case VulnerabilityMetaData.STATUS_VALUE_IN_REVIEW:
            case VulnerabilityMetaData.STATUS_VALUE_APPLICABLE:
            case VulnerabilityMetaData.STATUS_VALUE_INSIGNIFICANT:
            case VulnerabilityMetaData.STATUS_VALUE_VOID:
            case "none":
            case "":
                this.status = status;
        }

        return this;
    }

    public void setDate(Date date) {
        this.date = date;
    }

    public void setDate(String date) {
        if (date == null) {
            this.date = null;
            return;
        }

        if (date.startsWith("${")) {
            final Set variables = VulnerabilityStatusPostProcessor.extractVariables(date);
            final String variableToUse;
            if (variables.size() > 1) {
                variableToUse = variables.iterator().next();
                LOG.warn("Multiple variables found in date string [{}], only one is supported at the moment. Pick the first: {}", date, variableToUse);
            } else if (variables.isEmpty()) {
                variableToUse = null;
                LOG.warn("Invalid date variable found in date string [{}], ignoring", date);
            } else {
                variableToUse = variables.iterator().next();
            }
            if (variableToUse != null) {
                final String variableName = variableToUse.substring(2, variableToUse.length() - 1);
                final String[] variableAccessPath = variableName.split("\\.");

                final boolean isCurrentDate = VulnerabilityStatusPostProcessor.isVariableAccessPathPrefix(variableAccessPath, 0, "date", "current");

                if (isCurrentDate) {
                    this.date = new Date();
                    return;
                } else {
                    LOG.warn("Invalid date variable found in date string [{}], ignoring", date);
                }
            }
        }

        final Date parsedDate = TimeUtils.tryParse(date);

        if (parsedDate == null) {
            if (this.date == null && StringUtils.hasText(date)) {
                this.date = new Date();
                LOG.warn("Invalid date string [{}] on status history entry [{}; {}; {}; {}], setting time to now: [{}]", date, status, rationale, risk, measures, this.date);
            } else {
                LOG.warn("Invalid date string [{}] on status history entry [{}; {}; {}; {}]", date, status, rationale, risk, measures);
            }
        } else {
            this.date = parsedDate;
        }
    }

    public VulnerabilityStatusHistoryEntry setScope(VulnerabilityStatus.Scope scope) {
        this.scope = scope;
        return this;
    }

    private String notNull(String s) {
        return s == null ? StringUtils.EMPTY_STRING : s;
    }

    public String getFormattedDate() {
        if (date == null) return null;
        return TimeUtils.formatNormalizedDate(date);
    }

    public VulnerabilityStatusHistoryEntry setRationale(String rationale) {
        this.rationale = rationale;
        return this;
    }

    public String[] getIncludeLabels() {
        return includes;
    }

    public String[] getExcludeLabels() {
        return excludes;
    }

    public JSONObject toJson() {
        JSONObject exportJson = new JSONObject();
        try {
            exportJson.put("status", status);
            exportJson.put("rationale", rationale);
            exportJson.put("risk", risk);
            exportJson.put("measures", measures);
            exportJson.put("author", author);
            exportJson.put("date", getFormattedDate());
            exportJson.put("statusScore", score);
            exportJson.put("priority", priority);
            if (!active) exportJson.put("active", false);
            exportJson.put("labels", new JSONObject().put("includes", includes).put("excludes", excludes));
            if (scope != VulnerabilityStatus.Scope.ARTIFACT) exportJson.put("scope", scope.name());
        } catch (Exception e) {
            LOG.error("Unable to build vulnerability status export JSON object", e);
        }
        return exportJson;
    }

    public boolean isIncluded(String[] features) {
        if (features == null) return true;
        return isIncluded(Arrays.asList(features));
    }

    public boolean isIncluded(Collection features) {
        if (features == null) return true;

        // at least one 'include' labels must be included
        if (includes != null && includes.length > 0) {
            boolean included = false;
            for (String include : includes) {
                if (features.contains(include)) {
                    included = true;
                    break;
                }
            }
            if (!included) return false;
        }

        // none of the 'exclude' labels may be included
        if (excludes != null) {
            for (String exclude : excludes) {
                if (features.contains(exclude)) {
                    return false;
                }
            }
        }

        return true;
    }

    private  boolean arrayContains(T[] arr, T value) {
        if (value == null || arr == null) return false;
        return Arrays.asList(arr).contains(value);
    }

    private String getStatusNotNull() {
        return notNull(getStatus());
    }

    private String getAuthorNotNull() {
        return notNull(getAuthor());
    }

    @Override
    public int compareTo(VulnerabilityStatusHistoryEntry o) {
        if (scope == VulnerabilityStatus.Scope.INVENTORY && o.scope != VulnerabilityStatus.Scope.INVENTORY) {
            return 1;
        } else if (scope != VulnerabilityStatus.Scope.INVENTORY && o.scope == VulnerabilityStatus.Scope.INVENTORY) {
            return -1;
        }

        if (!active && o.active) return 1;
        else if (active && !o.active) return -1;

        if (status == null && o.status != null) return 1;
        else if (status != null && o.status == null) return -1;
        else if (status == null) return 0;

        if (priority != o.priority) return Integer.compare(o.priority, priority);

        if (date == null && o.date == null) {
            return Comparator.comparing(VulnerabilityStatusHistoryEntry::getStatusNotNull)
                    .thenComparing(VulnerabilityStatusHistoryEntry::getAuthorNotNull)
                    .compare(this, o);
        }
        if (date == null) return 1;
        if (o.date == null) return -1;

        final int dateCompareResult = -date.compareTo(o.date);
        if (dateCompareResult != 0) return dateCompareResult;

        if (author == null && o.author != null) return 1;
        else if (author != null && o.author == null) return -1;
        else if (author == null) return 1;

        final int authorCompareResult = author.compareTo(o.author);
        if (authorCompareResult != 0) return authorCompareResult;

        final int statusCompareResult = status.compareTo(o.status);
        if (statusCompareResult != 0) return statusCompareResult;

        return 0;
    }

    public final static VulnerabilityStatusHistoryEntry INSIGNIFICANT =
            new VulnerabilityStatusHistoryEntry(VulnerabilityMetaData.STATUS_VALUE_INSIGNIFICANT,
                    "Vulnerability severity score is below insignificant threshold.",
                    "", "", "",
                    TimeUtils.formatNormalizedDate(new Date(System.currentTimeMillis())),
                    null, null, null);

    public final static VulnerabilityStatusHistoryEntry VOID =
            new VulnerabilityStatusHistoryEntry(VulnerabilityMetaData.STATUS_VALUE_VOID,
                    "The component affected by this vulnerability is not included in the current asset version.",
                    "", "", "",
                    TimeUtils.formatNormalizedDate(new Date(System.currentTimeMillis())),
                    0.0, null, null);

    public final static VulnerabilityStatusHistoryEntry IN_REVIEW =
            new VulnerabilityStatusHistoryEntry(VulnerabilityMetaData.STATUS_VALUE_IN_REVIEW,
                    "The vulnerability has automatically been marked as in review.",
                    "", "", "",
                    TimeUtils.formatNormalizedDate(new Date(System.currentTimeMillis())),
                    null, null, null);

    @Override
    public String toString() {
        return "VulnerabilityStatusHistoryEntry{" +
                "status='" + status + '\'' +
                ", rationale='" + rationale + '\'' +
                ", risk='" + risk + '\'' +
                ", measures='" + measures + '\'' +
                ", author='" + author + '\'' +
                ", date='" + date + '\'' +
                ", includes=" + Arrays.toString(includes) +
                ", excludes=" + Arrays.toString(excludes) +
                ", score=" + score +
                ", active=" + active +
                ", scope=" + scope +
                ", priority=" + priority +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final VulnerabilityStatusHistoryEntry that = (VulnerabilityStatusHistoryEntry) o;

        // Using Double.compare() only if both scores are non-null
        if (score == null && that.score != null || score != null && that.score == null) return false;
        if (score != null && Double.compare(that.score, score) != 0) return false;

        // Using Objects.equals() for null-safe comparison of other fields
        if (!Objects.equals(status, that.status)) return false;
        if (!Objects.equals(rationale, that.rationale)) return false;
        if (!Objects.equals(risk, that.risk)) return false;
        if (!Objects.equals(measures, that.measures)) return false;
        if (!Objects.equals(author, that.author)) return false;
        if (!Objects.equals(date, that.date)) return false;
        if (priority != that.priority) return false;

        // Using Arrays.equals() only if both arrays are non-null
        if (includes == null && that.includes != null || includes != null && that.includes == null) return false;
        if (includes != null && !Arrays.equals(includes, that.includes)) return false;

        if (excludes == null && that.excludes != null || excludes != null && that.excludes == null) return false;
        if (excludes != null && !Arrays.equals(excludes, that.excludes)) return false;

        return true;
    }

    public boolean equalsTemplate(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        final VulnerabilityStatusHistoryEntry that = (VulnerabilityStatusHistoryEntry) o;

        // Using Double.compare() only if both scores are non-null
        if (score == null && that.score != null || score != null && that.score == null) return false;
        if (score != null && Double.compare(that.score, score) != 0) return false;

        // Using Objects.equals() for null-safe comparison of other fields
        if (!Objects.equals(status, that.status)) return false;

        if (VulnerabilityMetaData.STATUS_VALUE_INSIGNIFICANT.equalsIgnoreCase(status)) {
            if (rationale == null || !rationale.startsWith("Score is below") || that.rationale == null || !that.rationale.startsWith("Score is below")) {
                if (!Objects.equals(rationale, that.rationale)) return false;
            }
        } else {
            if (!Objects.equals(rationale, that.rationale)) return false;
        }

        if (!Objects.equals(risk, that.risk)) return false;
        if (!Objects.equals(measures, that.measures)) return false;
        if (!Objects.equals(author, that.author)) return false;
        if (priority != that.priority) return false;

        // Using Arrays.equals() only if both arrays are non-null
        if (includes == null && that.includes != null || includes != null && that.includes == null) return false;
        if (includes != null && !Arrays.equals(includes, that.includes)) return false;

        if (excludes == null && that.excludes != null || excludes != null && that.excludes == null) return false;
        if (excludes != null && !Arrays.equals(excludes, that.excludes)) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = Objects.hash(status, rationale, risk, measures, author, date, score, priority);
        result = 31 * result + Arrays.hashCode(includes);
        result = 31 * result + Arrays.hashCode(excludes);
        return result;
    }

    public static List parseEntries(JSONArray json) {
        try {
            final List parsed = new ArrayList<>();

            for (int i = 0; i < json.length(); i++) {
                VulnerabilityStatusHistoryEntry entry = VulnerabilityStatusHistoryEntry.fromMap(json.getJSONObject(i).toMap());
                parsed.add(entry);
            }

            return parsed;
        } catch (Exception e) {
            throw new RuntimeException("Unable to parse Vulnerability Status History Entries from JSON Array: " + json, e);
        }
    }

    public static List reorderChronologically(VulnerabilityStatus status, Vulnerability vulnerability, boolean isInsignificant, double insignificantThreshold) {
        if (vulnerability == null || status == null) return Collections.emptyList();

        final List statusHistory = new ArrayList<>(status.getStatusHistory());
        statusHistory.sort(VulnerabilityStatusHistoryEntry::compareTo);

        final boolean hasNoStatus = statusHistory.stream().noneMatch(s -> StringUtils.hasText(s.getStatus()));

        if (hasNoStatus && isInsignificant) {
            final VulnerabilityStatusHistoryEntry insignificantEntry = VulnerabilityStatusHistoryEntry.INSIGNIFICANT.clone();
            if (insignificantEntry.getRationale() != null) {
                insignificantEntry.setRationale(String.format(Locale.GERMANY, insignificantEntry.getRationale(), insignificantThreshold));
            }
            statusHistory.add(0, insignificantEntry);
        }

        return statusHistory;
    }

    @Override
    public VulnerabilityStatusHistoryEntry clone() {
        try {
            VulnerabilityStatusHistoryEntry clone = (VulnerabilityStatusHistoryEntry) super.clone();

            if (excludes != null) clone.excludes = Arrays.copyOf(excludes, excludes.length);
            if (includes != null) clone.includes = Arrays.copyOf(includes, includes.length);

            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy