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

com.metaeffekt.artifact.enrichment.vulnerability.VulnerabilityStatusEnrichment 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.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))); } } } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy