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