com.metaeffekt.artifact.enrichment.vulnerability.VulnerabilityStatusEnrichment Maven / Gradle / Ivy
/*
* 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.enrichment.vulnerability;
import com.metaeffekt.artifact.analysis.utils.CustomCollectors;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatus;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatusHistoryEntry;
import com.metaeffekt.artifact.enrichment.InventoryEnricher;
import com.metaeffekt.artifact.enrichment.configurations.VulnerabilityStatusEnrichmentConfiguration;
import com.metaeffekt.mirror.contents.base.DataSourceIndicator;
import com.metaeffekt.mirror.contents.base.VulnerabilityContextInventory;
import com.metaeffekt.mirror.contents.store.VulnerabilityTypeStore;
import com.metaeffekt.mirror.contents.vulnerability.Vulnerability;
import com.metaeffekt.mirror.download.documentation.EnricherMetadata;
import com.metaeffekt.mirror.download.documentation.InventoryEnrichmentPhase;
import org.json.JSONArray;
import org.metaeffekt.core.inventory.processor.model.Inventory;
import org.metaeffekt.core.inventory.processor.model.InventoryInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.util.*;
import java.util.stream.Collectors;
@EnricherMetadata(
name = "Vulnerability Status", phase = InventoryEnrichmentPhase.ASSESSMENTS,
intermediateFileSuffix = "status", mavenPropertyName = "vulnerabilityStatusEnrichment"
)
public class VulnerabilityStatusEnrichment extends InventoryEnricher {
private static final Logger LOG = LoggerFactory.getLogger(VulnerabilityStatusEnrichment.class);
private VulnerabilityStatusEnrichmentConfiguration configuration = new VulnerabilityStatusEnrichmentConfiguration();
public void setConfiguration(VulnerabilityStatusEnrichmentConfiguration configuration) {
this.configuration = configuration;
}
@Override
public VulnerabilityStatusEnrichmentConfiguration getConfiguration() {
return configuration;
}
@Override
protected void performEnrichment(Inventory inventory) {
final boolean logMatchingCriteriaPreviousValue = VulnerabilityStatus.LOG_MATCHING_CRITERIA;
VulnerabilityStatus.LOG_MATCHING_CRITERIA = configuration.isDebugMatchingCriteria();
try {
LOG.info("");
if (!configuration.getStatusFiles().isEmpty()) {
LOG.info("Adding data from status files from:");
configuration.getStatusFiles().stream().map(File::getAbsolutePath).map(f -> " - " + f).forEach(LOG::info);
}
if (!configuration.getAdditionalStatus().isEmpty()) {
LOG.info("Adding data from [{}] additional status entries added programmatically", configuration.getAdditionalStatus().size());
}
final Set anyScopeStatus = configuration.readVulnerabilityStatusEntries(super.getSecurityPolicyConfiguration().getJsonSchemaValidationErrorsHandling());
// log the status files found with metadata
if (anyScopeStatus.isEmpty()) {
LOG.info("No status files found in provided directories");
} else {
final Map> scopeStatusMap = anyScopeStatus.stream()
.collect(Collectors.groupingBy(s -> s.getOriginYamlFile() == null ? "programmatic" : s.getOriginYamlFile().getAbsolutePath()));
for (Map.Entry> entry : scopeStatusMap.entrySet()) {
final List statuses = entry.getValue();
final String filePath = entry.getKey();
LOG.info("Found [{}] ({}) status file{} in file: {}",
statuses.size(),
statuses.stream().map(VulnerabilityStatus::getScope).distinct().map(Enum::toString).collect(Collectors.joining(", ")),
statuses.size() == 1 ? "" : "s", filePath);
}
}
LOG.info("");
final VulnerabilityContextInventory vInventory = VulnerabilityContextInventory.fromInventory(inventory);
final int vulnerabilityCountBefore = vInventory.getVulnerabilities().size();
// filter out all INVENTORY and ARTIFACT scope entries
final List inventoryScopeStatus = anyScopeStatus.stream()
.filter(s -> s.isScope(VulnerabilityStatus.Scope.INVENTORY))
.collect(Collectors.toList());
final List artifactScopeStatus = anyScopeStatus.stream()
.filter(s -> s.isScope(VulnerabilityStatus.Scope.ARTIFACT))
.collect(Collectors.toList());
// do not yet add the vulnerabilities from the status files to the inventory, as INVENTORY scope statuses should not be applied on these
if (!inventoryScopeStatus.isEmpty()) {
LOG.info("Found [{}] status file{} with scope [{}]", inventoryScopeStatus.size(), inventoryScopeStatus.size() == 1 ? "" : "s", VulnerabilityStatus.Scope.INVENTORY);
for (final Vulnerability vulnerability : vInventory.getVulnerabilities()) {
addStatusEntriesForVulnerability(vInventory, inventoryScopeStatus, vulnerability, false);
}
// add information about the inventory scope status files to the inventory info
final InventoryInfo info = vInventory.getInventory().findOrCreateInventoryInfo(InventoryEnricher.INVENTORY_INFO_VULNERABILITY_STATUS_KEY);
final JSONArray statusJson = inventoryScopeStatus.stream()
.map(VulnerabilityStatus::toJson)
.collect(CustomCollectors.toJsonArray());
if (info.has(InventoryEnricher.INVENTORY_INFO_VULNERABILITY_STATUS_INVENTORY_STATUSES_KEY)) {
final JSONArray existingStatusJson = new JSONArray(info.get(InventoryEnricher.INVENTORY_INFO_VULNERABILITY_STATUS_INVENTORY_STATUSES_KEY));
for (int i = 0; i < existingStatusJson.length(); i++) {
statusJson.put(existingStatusJson.get(i));
}
}
info.set(InventoryEnricher.INVENTORY_INFO_VULNERABILITY_STATUS_INVENTORY_STATUSES_KEY, statusJson.toString());
}
// all vulnerabilities from the statuses that are non-existent in the inventory will be added as 'void' status entries later
final Map createdVulnerabilitySources = new HashMap<>();
for (VulnerabilityStatus cveStatusHistory : anyScopeStatus) {
for (String name : cveStatusHistory.getAffectedVulnerabilitiesWithoutWildcards()) {
if (!vInventory.findVulnerabilityByName(name).isPresent()) {
final Vulnerability vulnerability = vInventory.findOrCreateVulnerabilityByName(name);
VulnerabilityTypeStore.get().inferSourceIdentifierFromIdIfAbsent(vulnerability);
createdVulnerabilitySources.put(vulnerability, cveStatusHistory.getOriginYamlFile());
}
}
}
// add a matching source to all vulnerabilities that have been created by status files
for (Map.Entry vulnerabilityFileEntry : createdVulnerabilitySources.entrySet()) {
final Vulnerability vulnerability = vulnerabilityFileEntry.getKey();
final File file = vulnerabilityFileEntry.getValue();
vulnerability.addMatchingSource(DataSourceIndicator.assessmentStatus(file));
}
final int vulnerabilityCountAfter = vInventory.getVulnerabilities().size();
LOG.info("Found [{}] status files with a total of [{}] affected vulnerabilities, applying to an inventory with [{}] vulnerabilities (merged & deduplicated total: [{}])",
anyScopeStatus.size(), vulnerabilityCountAfter - vulnerabilityCountBefore, vulnerabilityCountBefore, vulnerabilityCountAfter);
// iterate over remaining vulnerabilities and apply the ARTIFACT status entries
for (final Vulnerability vulnerability : vInventory.getVulnerabilities()) {
addStatusEntriesForVulnerability(vInventory, artifactScopeStatus, vulnerability, createdVulnerabilitySources.containsKey(vulnerability));
}
vInventory.writeBack(true);
} finally {
VulnerabilityStatus.LOG_MATCHING_CRITERIA = logMatchingCriteriaPreviousValue;
}
}
private void addStatusEntriesForVulnerability(VulnerabilityContextInventory vInventory, Collection allVulnerabilityStatuses,
Vulnerability vulnerability, boolean vulnerabilityHasBeenCreatedByStatus) {
VulnerabilityTypeStore.get().inferSourceIdentifierFromIdIfAbsent(vulnerability);
if (vulnerabilityHasBeenCreatedByStatus) {
vulnerability.addTag("added by status");
LOG.info("Added [{}] to the inventory via status file", vulnerability);
}
// find matching vulnerability status entries and apply their modifiers
final Map> vulnerabilityStatuses = VulnerabilityStatus.findAffectedEntriesRetainMatchingCondition(allVulnerabilityStatuses, vulnerability);
final List highestPriorityStatuses = VulnerabilityStatus.MatchType.findHighestPriority(vulnerabilityStatuses);
modifyVulnerabilityStatusHistoryEntryDateBasedOnMatchType(vulnerabilityStatuses, true);
final List activeLabels = Arrays.asList(configuration.getActiveLabels());
if (!vulnerabilityStatuses.isEmpty()) {
for (Map.Entry> match : vulnerabilityStatuses.entrySet()) {
final VulnerabilityStatus.MatchType matchType = match.getKey();
final List affectedStatusEntries = match.getValue();
final boolean isHighestPriorityStatusEntries = highestPriorityStatuses == affectedStatusEntries;
// apply validation on all statuses first
affectedStatusEntries.forEach(affectedStatusEntry -> affectedStatusEntry.checkValidation(vInventory.getInventory(), vulnerability, configuration.isFailOnValidationErrors()));
// apply status history on all, no matter if highest priority
for (VulnerabilityStatus affectedStatusEntry : affectedStatusEntries) {
if (vulnerabilityHasBeenCreatedByStatus) {
affectedStatusEntry.addHistoryEntry(VulnerabilityStatusHistoryEntry.VOID);
}
affectedStatusEntry.appendStatusHistoryOnlyToVulnerabilityStatus(vulnerability.getOrCreateNewVulnerabilityStatus(), activeLabels);
if (vulnerabilityHasBeenCreatedByStatus) {
affectedStatusEntry.removeHistoryEntry(VulnerabilityStatusHistoryEntry.VOID);
}
}
// and now only on the first highest priority status apply the rest of the information
if (isHighestPriorityStatusEntries) {
if (affectedStatusEntries.size() != 1) {
LOG.warn("Multiple status entries match for [{}] on criteria level [{}], picking arbitrary: [{}]. Please check file(s) [{}]",
vulnerability.getId(),
matchType,
affectedStatusEntries.get(0).getOriginYamlFile().getAbsolutePath(),
affectedStatusEntries.stream().map(VulnerabilityStatus::getOriginYamlFile).filter(Objects::nonNull).map(File::getAbsolutePath).collect(Collectors.joining(", "))
);
if (this.configuration.isFailOnAmbiguousMatchingInformation()) {
throw new IllegalStateException("[failOnAmbiguousMatchingInformation] Ambiguous assessment matching information found for vulnerability [" + vulnerability.getId() + "] on criteria level [" + matchType + "] from files:\n - " +
affectedStatusEntries.stream().map(VulnerabilityStatus::getOriginYamlFile).filter(Objects::nonNull).map(File::getAbsolutePath).collect(Collectors.joining("\n - ")));
}
}
affectedStatusEntries.get(0).appendAllExceptStatusHistoryToVulnerabilityStatus(vulnerability.getOrCreateNewVulnerabilityStatus());
}
}
} else if (vulnerabilityHasBeenCreatedByStatus) {
final VulnerabilityStatus voidStatus = new VulnerabilityStatus();
// mark as void (=> not contained in inventory, added by status file, etc.)
voidStatus.addHistoryEntry(VulnerabilityStatusHistoryEntry.VOID);
voidStatus.appendToVulnerabilityStatus(vulnerability.getOrCreateNewVulnerabilityStatus(), activeLabels);
}
if (vulnerability.getVulnerabilityStatus() != null) {
vulnerability.getVulnerabilityStatus().applyToVulnerability(vulnerability);
}
modifyVulnerabilityStatusHistoryEntryDateBasedOnMatchType(vulnerabilityStatuses, false);
}
/**
* Using this method ensures that the history entries matched by the criteria of higher-priority match types, such
* as vulnerability ids, are moved higher into the list by adding a few milliseconds to their date.
* Call this method once before applying the status history entries to the vulnerability status with the parameter
* {@code add} set to {@code true}, and once after applying the status history entries with the parameter
* {@code add} set to {@code false}.
*
* This mainly has to be done to prevent identical dates in the history entries, since the sorting method
* ({@link VulnerabilityStatusHistoryEntry#compareTo(VulnerabilityStatusHistoryEntry)}) will otherwise sort them
* based on other criteria.
*
* @param vulnerabilityStatuses the map of match types to status entries
* @param add whether to add or subtract the milliseconds
*/
private static void modifyVulnerabilityStatusHistoryEntryDateBasedOnMatchType(Map> vulnerabilityStatuses, boolean add) {
for (Map.Entry> match : vulnerabilityStatuses.entrySet()) {
final VulnerabilityStatus.MatchType matchType = match.getKey();
final int addition = VulnerabilityStatus.MatchType.values().length - matchType.ordinal();
for (VulnerabilityStatus status : match.getValue()) {
for (VulnerabilityStatusHistoryEntry historyEntry : status.getStatusHistory()) {
if (historyEntry.getDate() != null) {
historyEntry.setDate(new Date(historyEntry.getDate().getTime() + (add ? addition : -addition)));
}
}
}
}
}
}