com.metaeffekt.mirror.contents.base.VulnerabilityContextInventory 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.mirror.contents.base;
import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.InventoryAttribute;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatus;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatusHistoryEntry;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.warnings.InventoryWarnings;
import com.metaeffekt.mirror.contents.advisory.AdvisoryEntry;
import com.metaeffekt.mirror.contents.store.AdvisoryTypeIdentifier;
import com.metaeffekt.mirror.contents.store.AdvisoryTypeStore;
import com.metaeffekt.mirror.contents.store.VulnerabilityTypeStore;
import com.metaeffekt.mirror.contents.vulnerability.Vulnerability;
import lombok.Getter;
import lombok.Setter;
import org.metaeffekt.core.inventory.processor.model.*;
import org.metaeffekt.core.inventory.processor.reader.InventoryReader;
import org.metaeffekt.core.inventory.processor.report.configuration.CentralSecurityPolicyConfiguration;
import org.metaeffekt.core.inventory.processor.writer.InventoryWriter;
import org.metaeffekt.core.security.cvss.CvssVector;
import org.metaeffekt.core.security.cvss.processor.CvssSelectionResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
public class VulnerabilityContextInventory {
private static final Logger LOG = LoggerFactory.getLogger(VulnerabilityContextInventory.class);
@Getter
private final Inventory inventory;
private final String vulnerabilityContextName;
@Getter
private final Set vulnerabilities = new LinkedHashSet<>();
@Getter
private final Set securityAdvisories = new LinkedHashSet<>();
private final Map vulnerabilityByName = new HashMap<>();
private final Map advisoryByName = new HashMap<>();
// data management settings
/**
* Using the attributes {@link VulnerabilityContextInventory#reAssociateAdvisories}
* and {@link VulnerabilityContextInventory#reAssociateVulnerabilities} gives you higher control over the
* re-association process, which automatically manages the cross-references between vulnerabilities and advisories.
* This may be useful when you want to add a large number of vulnerabilities or advisories at once and want to
* avoid the overhead of re-associating them after each addition, like with updating an index of a database.
* By default, the re-association process is enabled. Make sure to call the re-association methods after you
* have added all vulnerabilities or advisories to the inventory.
* This property in particular is used to control the re-association of advisories.
*/
@Setter
private boolean reAssociateAdvisories = true;
/**
* Using the attributes {@link VulnerabilityContextInventory#reAssociateAdvisories}
* and {@link VulnerabilityContextInventory#reAssociateVulnerabilities} gives you higher control over the
* re-association process, which automatically manages the cross-references between vulnerabilities and advisories.
* This may be useful when you want to add a large number of vulnerabilities or advisories at once and want to
* avoid the overhead of re-associating them after each addition, like with updating an index of a database.
* By default, the re-association process is enabled. Make sure to call the re-association methods after you
* have added all vulnerabilities or advisories to the inventory.
* This property in particular is used to control the re-association of vulnerabilities.
*/
@Setter
private boolean reAssociateVulnerabilities = true;
protected VulnerabilityContextInventory(Inventory inventory, String vulnerabilityContextName) {
this.inventory = inventory;
this.vulnerabilityContextName = vulnerabilityContextName;
}
protected VulnerabilityContextInventory(Inventory inventory) {
this(inventory, VulnerabilityMetaData.VULNERABILITY_ASSESSMENT_CONTEXT_DEFAULT);
}
public void pauseReAssociation() {
this.reAssociateAdvisories = false;
this.reAssociateVulnerabilities = false;
}
public void resumeReAssociation() {
this.reAssociateAdvisories = true;
this.reAssociateVulnerabilities = true;
this.reAssociateAdvisories();
this.reAssociateVulnerabilities();
}
public InventoryWarnings getInventoryWarnings() {
return InventoryWarnings.fromInventory(inventory);
}
public Set getShallowCopyVulnerabilities() {
return new LinkedHashSet<>(vulnerabilities);
}
public List getSortedVulnerabilities() {
final List sortedVulnerabilities = new ArrayList<>(vulnerabilities);
sortedVulnerabilities.sort(Comparator.comparing(Vulnerability::getId));
return sortedVulnerabilities;
}
public Set getShallowCopySecurityAdvisories() {
return new LinkedHashSet<>(securityAdvisories);
}
/* EFFECTIVE INFORMATION CALCULATION */
public void calculateEffectiveCvssVectorsForVulnerabilities(CentralSecurityPolicyConfiguration securityPolicy) {
synchronized (this.vulnerabilities) {
for (Vulnerability vulnerability : this.vulnerabilities) {
vulnerability.selectEffectiveCvssVectors(securityPolicy);
}
}
}
public void applyEffectiveVulnerabilityStatus(CentralSecurityPolicyConfiguration securityPolicy) {
synchronized (this.vulnerabilities) {
for (Vulnerability vulnerability : this.vulnerabilities) {
final boolean isInsignificant = isVulnerabilityInsignificant(securityPolicy, vulnerability);
final VulnerabilityStatus vulnerabilityStatus = vulnerability.getOrCreateNewVulnerabilityStatus();
// remove baked information first
vulnerabilityStatus.getStatusHistorySet().removeIf(e -> e.equalsTemplate(VulnerabilityStatusHistoryEntry.IN_REVIEW));
vulnerabilityStatus.getStatusHistorySet().removeIf(e -> e.equalsTemplate(VulnerabilityStatusHistoryEntry.INSIGNIFICANT));
vulnerabilityStatus.reorderChronologically(vulnerability, isInsignificant, securityPolicy.getInsignificantThreshold());
if (vulnerabilityStatus.getStatusHistory().stream().noneMatch(VulnerabilityStatusHistoryEntry::isActive)) {
vulnerabilityStatus.addHistoryEntry(VulnerabilityStatusHistoryEntry.IN_REVIEW);
}
}
}
}
/* COPIED METHODS FROM CentralSecurityPolicyConfiguration USING Vulnerability INSTEAD OF AeaaVulnerability */
public boolean isVulnerabilityInsignificant(CentralSecurityPolicyConfiguration securityPolicy, Vulnerability vulnerability) {
final double insignificantThreshold = securityPolicy.getInsignificantThreshold();
if (insignificantThreshold == -1.0) return true;
final CvssVector vector = vulnerability.getCvssSelectionResult().getSelectedContextIfAvailableOtherwiseInitial();
if (vector == null) {
return false;
} else {
return vector.getOverallScore() < insignificantThreshold;
}
}
public boolean isVulnerabilityIncludedRegardingAdvisoryProviders(CentralSecurityPolicyConfiguration securityPolicy, Vulnerability vulnerability) {
if (CentralSecurityPolicyConfiguration.containsAny(securityPolicy.getIncludeVulnerabilitiesWithAdvisoryProviders())) {
return true;
}
final List> filter = AdvisoryTypeStore.get().fromJsonNamesAndImplementations(securityPolicy.getIncludeVulnerabilitiesWithAdvisoryProviders());
return isVulnerabilityIncludedRegardingAdvisoryProviders(vulnerability, filter);
}
public static boolean isVulnerabilityIncludedRegardingAdvisoryProviders(Vulnerability vulnerability, Collection> filter) {
return vulnerability.getSecurityAdvisories().stream().anyMatch(a -> filter.contains(a.getSourceIdentifier()));
}
public boolean isVulnerabilityAboveIncludeScoreThreshold(CentralSecurityPolicyConfiguration securityPolicy, Vulnerability vulnerability) {
if (securityPolicy.getIncludeScoreThreshold() == -1.0 || securityPolicy.getIncludeScoreThreshold() == Double.MIN_VALUE) {
return true;
}
final CvssVector vector = vulnerability.getCvssSelectionResult().getSelectedByCustomMetric(CvssVector::getOverallScore, CvssSelectionResult.CUSTOM_VECTOR_SCORE_SELECTOR_MAX);
final double score = vector == null ? 0.0 : vector.getOverallScore();
return score >= securityPolicy.getIncludeScoreThreshold();
}
public boolean isVulnerabilityIncludedRegardingAdvisoryReviewStatus(CentralSecurityPolicyConfiguration securityPolicy, Vulnerability vulnerability) {
final List includeVulnerabilitiesWithAdvisoryReviewStatus = securityPolicy.getIncludeVulnerabilitiesWithAdvisoryReviewStatus();
if (CentralSecurityPolicyConfiguration.containsAny(includeVulnerabilitiesWithAdvisoryReviewStatus)) {
return true;
}
return vulnerability.getSecurityAdvisories().stream()
.map(a -> a.getAdditionalAttribute(AdvisoryMetaData.Attribute.REVIEW_STATUS))
.filter(Objects::nonNull)
.anyMatch(includeVulnerabilitiesWithAdvisoryReviewStatus::contains);
}
/* DATA ACCESS */
public void add(Vulnerability vulnerability) {
synchronized (this.vulnerabilities) {
this.vulnerabilities.add(vulnerability);
}
reAssociateVulnerability(vulnerability);
updateVulnerabilityByName(vulnerability);
}
public void add(VulnerabilityMetaData vmd) {
final Vulnerability vulnerability = Vulnerability.fromVulnerabilityMetaData(vmd);
synchronized (this.vulnerabilities) {
this.vulnerabilities.add(vulnerability);
}
reAssociateVulnerability(vulnerability);
updateVulnerabilityByName(vulnerability);
}
public void addAllVulnerabilities(List vulnerabilities) {
synchronized (this.vulnerabilities) {
this.vulnerabilities.addAll(vulnerabilities);
}
this.reAssociateVulnerabilities();
for (Vulnerability vulnerability : vulnerabilities) {
this.updateVulnerabilityByName(vulnerability);
}
}
public void add(AdvisoryEntry securityAdvisory) {
synchronized (this.securityAdvisories) {
this.securityAdvisories.add(securityAdvisory);
}
this.reAssociateAdvisory(securityAdvisory);
this.updateSecurityAdvisoryByName(securityAdvisory);
}
public void add(AdvisoryMetaData amd) {
final AdvisoryEntry securityAdvisory = AdvisoryEntry.fromAdvisoryMetaData(amd);
synchronized (this.securityAdvisories) {
this.securityAdvisories.add(securityAdvisory);
}
this.reAssociateAdvisory(securityAdvisory);
this.updateSecurityAdvisoryByName(securityAdvisory);
}
public void addAllAdvisories(Collection securityAdvisories) {
synchronized (this.securityAdvisories) {
this.securityAdvisories.addAll(securityAdvisories);
}
this.reAssociateAdvisories();
for (AdvisoryEntry securityAdvisory : securityAdvisories) {
this.updateSecurityAdvisoryByName(securityAdvisory);
}
}
public void remove(Vulnerability vulnerability) {
synchronized (this.vulnerabilities) {
this.vulnerabilities.remove(vulnerability);
}
synchronized (this.vulnerabilityByName) {
this.vulnerabilityByName.remove(vulnerability.getId());
}
}
public void remove(VulnerabilityMetaData vulnerability) {
final Vulnerability removed;
synchronized (this.vulnerabilityByName) {
removed = this.vulnerabilityByName.remove(vulnerability.get(VulnerabilityMetaData.Attribute.NAME));
}
synchronized (this.vulnerabilities) {
this.vulnerabilities.remove(removed);
}
}
public void remove(AdvisoryEntry securityAdvisory) {
synchronized (this.securityAdvisories) {
this.securityAdvisories.remove(securityAdvisory);
}
synchronized (this.advisoryByName) {
this.advisoryByName.remove(securityAdvisory.getId());
}
synchronized (this.vulnerabilities) {
this.vulnerabilities.forEach(v -> v.removeSecurityAdvisory(securityAdvisory));
}
}
public void removeIgnoreVulnerabilities(AdvisoryEntry securityAdvisory) {
synchronized (this.securityAdvisories) {
this.securityAdvisories.remove(securityAdvisory);
}
synchronized (this.advisoryByName) {
this.advisoryByName.remove(securityAdvisory.getId());
}
}
public void remove(AdvisoryMetaData securityAdvisory) {
final AdvisoryEntry removed;
synchronized (this.advisoryByName) {
removed = this.advisoryByName.remove(securityAdvisory.get(AdvisoryMetaData.Attribute.NAME));
}
synchronized (this.securityAdvisories) {
this.securityAdvisories.remove(removed);
}
}
public boolean contains(Vulnerability vulnerability) {
return this.vulnerabilities.contains(vulnerability);
}
public boolean contains(AdvisoryEntry securityAdvisory) {
return this.securityAdvisories.contains(securityAdvisory);
}
private void updateVulnerabilityByName(Vulnerability vulnerability) {
if (vulnerability == null) return;
synchronized (vulnerabilityByName) {
vulnerabilityByName.put(vulnerability.getId(), vulnerability);
}
}
private void updateSecurityAdvisoryByName(AdvisoryEntry advisoryEntry) {
if (advisoryEntry == null) return;
synchronized (this.advisoryByName) {
this.advisoryByName.put(advisoryEntry.getId(), advisoryEntry);
}
}
public Optional findVulnerabilityByName(String name) {
if (name == null) return Optional.empty();
final Vulnerability vulnerability = this.vulnerabilityByName.get(name);
if (vulnerability == null) return Optional.empty();
if (!this.vulnerabilities.contains(vulnerability)) {
synchronized (this.vulnerabilities) {
this.vulnerabilities.remove(vulnerability);
}
return Optional.empty();
}
return Optional.of(vulnerability);
}
public Vulnerability findOrCreateVulnerabilityByName(String name) {
return findVulnerabilityByName(name).orElseGet(() -> {
final Vulnerability vulnerability = new Vulnerability(name);
add(vulnerability);
return vulnerability;
});
}
public Vulnerability findOrAppendVulnerabilityByVulnerability(Vulnerability searchVulnerability) {
if (searchVulnerability == null) {
throw new IllegalArgumentException("Query vulnerability must not be null when searching/creating vulnerabilities");
}
if (searchVulnerability.getId() == null) {
throw new IllegalArgumentException("Query vulnerability must have an id when searching/creating vulnerabilities, but got: " + searchVulnerability.toJson());
}
return findVulnerabilityByName(searchVulnerability.getId()).orElseGet(() -> {
add(searchVulnerability);
return searchVulnerability;
});
}
public Vulnerability findOrCreateWithoutAddingVulnerabilityByName(String name) {
return findVulnerabilityByName(name).orElseGet(() -> new Vulnerability(name));
}
public List findVulnerabilitiesWithSecurityAdvisory(AdvisoryEntry securityAdvisory) {
final List vulnerabilitiesWithSecurityAdvisory = new ArrayList<>();
synchronized (this.vulnerabilities) {
for (Vulnerability vulnerability : this.vulnerabilities) {
if (vulnerability.getSecurityAdvisories().contains(securityAdvisory)) {
vulnerabilitiesWithSecurityAdvisory.add(vulnerability);
}
}
}
return vulnerabilitiesWithSecurityAdvisory;
}
public Optional findAdvisoryEntryByName(String name) {
if (name == null) return Optional.empty();
final AdvisoryEntry advisoryEntry = this.advisoryByName.get(name);
if (advisoryEntry == null) return Optional.empty();
if (!this.securityAdvisories.contains(advisoryEntry)) {
synchronized (this.advisoryByName) {
this.advisoryByName.remove(name);
}
synchronized (this.vulnerabilities) {
this.vulnerabilities.forEach(v -> v.removeSecurityAdvisory(advisoryEntry));
}
return Optional.empty();
}
return Optional.of(advisoryEntry);
}
public AdvisoryEntry findOrCreateAdvisoryEntryByName(String name, Function constructor) {
return findAdvisoryEntryByName(name).orElseGet(() -> {
final AdvisoryEntry advisoryEntry = constructor.apply(name);
add(advisoryEntry);
return advisoryEntry;
});
}
public AdvisoryEntry findOrAppendAdvisoryEntryByAdvisoryEntry(AdvisoryEntry searchAdvisoryEntry) {
if (searchAdvisoryEntry == null) {
throw new IllegalArgumentException("Query advisory entry must not be null when searching/creating advisory entries");
}
if (searchAdvisoryEntry.getId() == null) {
throw new IllegalArgumentException("Query advisory entry must have an id when searching/creating advisory entries, but got: " + searchAdvisoryEntry.toJson());
}
return findAdvisoryEntryByName(searchAdvisoryEntry.getId()).orElseGet(() -> {
add(searchAdvisoryEntry);
return searchAdvisoryEntry;
});
}
@Override
public String toString() {
return "VulnerabilityContextInventory@" + Integer.toHexString(hashCode()) +
"[" + inventory.getArtifacts().size() + ", " + vulnerabilities.size() + ", " + securityAdvisories.size() + ']';
}
public void writeBack() {
writeBack(false);
}
public void writeBack(boolean retainUnreferencedVulnerabilities) {
final long startTime = System.currentTimeMillis();
this.reAssociateAdvisories();
// affectsArtifacts is a map of:
// Artifact Field Names --> Artifact --> List of Vulnerability Names
final Map>> affectsArtifacts = new HashMap<>();
// find all artifact - vulnerability / advisory associations
synchronized (this.vulnerabilities) {
for (Vulnerability vulnerability : this.vulnerabilities) {
extractMatchingArtifacts(vulnerability, affectsArtifacts);
}
}
synchronized (this.securityAdvisories) {
for (AdvisoryEntry advisoryEntry : this.securityAdvisories) {
extractMatchingArtifacts(advisoryEntry, affectsArtifacts);
}
}
// collect previously written vulnerability information from artifacts
for (Artifact artifact : inventory.getArtifacts()) {
// add all present vulnerabilities
if (artifact.get(Artifact.Attribute.VULNERABILITY) != null) {
affectsArtifacts
.computeIfAbsent(Artifact.Attribute.VULNERABILITY.getKey(), k -> new HashMap<>())
.computeIfAbsent(artifact, k -> new LinkedHashSet<>())
.addAll(Arrays.asList(artifact.get(Artifact.Attribute.VULNERABILITY).split(", ")));
}
if (artifact.get(InventoryAttribute.ADDON_CVES) != null) {
final List addonCves = Arrays.asList(artifact.get(InventoryAttribute.ADDON_CVES).split(", "));
affectsArtifacts
.computeIfAbsent(Artifact.Attribute.VULNERABILITY.getKey(), k -> new HashMap<>())
.computeIfAbsent(artifact, k -> new LinkedHashSet<>())
.addAll(addonCves);
// register the ADDON_CVES as a source for the vulnerabilities if they do not already contain it
synchronized (this.vulnerabilities) {
for (String addonCve : addonCves) {
final Vulnerability vulnerability = findOrCreateVulnerabilityByName(addonCve);
final boolean exactArtifactAndAddonCvesReasonAlreadyExists = vulnerability.getMatchingSources().stream()
.filter(source -> source.getDataSource() == VulnerabilityTypeStore.CVE)
.filter(source -> source.getMatchReason() instanceof DataSourceIndicator.AnyArtifactOverwriteSourceReason)
.map(source -> (DataSourceIndicator.AnyArtifactOverwriteSourceReason) source.getMatchReason())
.anyMatch(reason -> reason.isArtifact(artifact) && Objects.equals(reason.getSource(), InventoryAttribute.ADDON_CVES.getKey()));
if (!exactArtifactAndAddonCvesReasonAlreadyExists) {
vulnerability.addMatchingSource(DataSourceIndicator.sourcedArtifact(artifact, InventoryAttribute.ADDON_CVES.getKey()));
}
}
}
}
// remove all that are inapplicable or have been fixed on the artifact,
// since certain vulnerabilities should not be matched by the artifact:
// - MS vulnerabilities that have been fixed and their KB-ID has been added to the artifact
// - inapplicable CVEs as specified in the status files
int removeCount = 0;
if (artifact.get(InventoryAttribute.INAPPLICABLE_CVE) != null) {
// the map modified here is sourced from in the affectsArtifacts map, so no need to put it back
final Set vulnList = affectsArtifacts
.computeIfAbsent(Artifact.Attribute.VULNERABILITY.getKey(), k -> new HashMap<>())
.computeIfAbsent(artifact, k -> new LinkedHashSet<>());
final int sizeBefore = vulnList.size();
Arrays.asList(artifact.get(InventoryAttribute.INAPPLICABLE_CVE).split(", ")).forEach(vulnList::remove);
removeCount += sizeBefore - vulnList.size();
}
if (artifact.get(InventoryAttribute.VULNERABILITIES_FIXED_BY_KB) != null) {
// same as above
final Set vulnList = affectsArtifacts
.computeIfAbsent(Artifact.Attribute.VULNERABILITY.getKey(), k -> new HashMap<>())
.computeIfAbsent(artifact, k -> new LinkedHashSet<>());
final int sizeBefore = vulnList.size();
Arrays.asList(artifact.get(InventoryAttribute.VULNERABILITIES_FIXED_BY_KB).split(", ")).forEach(vulnList::remove);
removeCount += sizeBefore - vulnList.size();
}
if (removeCount > 0) {
LOG.warn("Removed [{}] vulnerabilities from artifact [{}:{}:{}] due to inapplicability or fix status",
removeCount, artifact.getId(), artifact.getComponent(), artifact.getVersion());
}
}
// write back all vulnerabilities
synchronized (this.vulnerabilities) {
// write the effective vulnerability status into the vulnerabilities
for (Vulnerability vulnerability : vulnerabilities) {
if (vulnerability.getVulnerabilityStatus() != null) {
vulnerability.getVulnerabilityStatus().applyToVulnerability(vulnerability);
}
}
final List vmds = this.inventory.getVulnerabilityMetaData(vulnerabilityContextName);
vmds.clear();
for (Vulnerability vulnerability : this.vulnerabilities) {
vmds.add(vulnerability.toBaseModel());
}
}
// write back all advisories
synchronized (this.securityAdvisories) {
final List amds = this.inventory.getAdvisoryMetaData();
amds.clear();
for (AdvisoryEntry advisoryEntry : this.securityAdvisories) {
final AdvisoryMetaData constructedAmd = advisoryEntry.toBaseModel();
amds.add(constructedAmd);
}
}
final Set coveredVulnerabilities = new HashSet<>();
// refresh vulnerability references on artifacts
for (Map.Entry>> artifactKeyToArtifactVulnerabilitiesEntry : affectsArtifacts.entrySet()) {
final String artifactKey = artifactKeyToArtifactVulnerabilitiesEntry.getKey();
final Map> artifactVulnerabilitiesMap = artifactKeyToArtifactVulnerabilitiesEntry.getValue();
for (Map.Entry> artifactsVulnerabilities : artifactVulnerabilitiesMap.entrySet()) {
final Artifact artifact = artifactsVulnerabilities.getKey();
final Set vulnerabilities = artifactsVulnerabilities.getValue();
artifact.set(artifactKey, String.join(", ", vulnerabilities));
coveredVulnerabilities.addAll(vulnerabilities);
}
}
if (!retainUnreferencedVulnerabilities) {
// remove all vulnerabilities that are not referenced by any artifact
final Set unreferencedVulnerabilities = new HashSet<>();
for (Vulnerability vulnerability : this.vulnerabilities) {
if (!coveredVulnerabilities.contains(vulnerability.getId())) {
if (vulnerability.getVulnerabilityStatus() != null) {
final boolean containsVoidStatus = vulnerability.getVulnerabilityStatus().getStatusHistory().stream()
.anyMatch(e -> VulnerabilityMetaData.STATUS_VALUE_VOID.equals(e.getStatus()));
if (containsVoidStatus) {
continue;
}
}
unreferencedVulnerabilities.add(vulnerability.getId());
}
}
if (!unreferencedVulnerabilities.isEmpty()) {
LOG.warn("Removed [{}] vulnerabilities from inventory that are not applicable on any artifact: [{}]",
unreferencedVulnerabilities.size(), String.join(", ", unreferencedVulnerabilities));
this.vulnerabilities.removeIf(v -> unreferencedVulnerabilities.contains(v.getId()));
inventory.getVulnerabilityMetaData().removeIf(v -> unreferencedVulnerabilities.contains(v.get(VulnerabilityMetaData.Attribute.NAME)));
}
}
// sort vulnerabilities by name
inventory.getVulnerabilityMetaData()
.sort(Comparator.comparing((VulnerabilityMetaData o) -> o.get(VulnerabilityMetaData.Attribute.NAME) == null ? "" : o.get(VulnerabilityMetaData.Attribute.NAME)).reversed());
LOG.debug("Wrote back [{}] vulnerabilities and [{}] security advisories in [{}] ms",
this.vulnerabilities.size(), this.securityAdvisories.size(), System.currentTimeMillis() - startTime);
}
/**
* Usually does not need to be called, since almost all information is already written into the inventory by the
* {@link VulnerabilityContextInventory#writeBack()} method.
* It requires the {@link VulnerabilityContextInventory#writeBack()} method to have already been called on it.
* This method will add the following information that depends on the security policy to be present:
*
*
* - CVSS Severity information, calculated from the overall scores (initial, context)
*
*
* @param securityPolicy the security policy to use for additional information
*/
public void writeAdditionalInformationBack(CentralSecurityPolicyConfiguration securityPolicy) {
if (securityPolicy == null) {
LOG.error("Security Policy is null. Cannot write additional information back to inventory without a security policy");
return;
}
for (String context : inventory.getVulnerabilityMetaDataContexts()) {
for (VulnerabilityMetaData vmd : inventory.getVulnerabilityMetaData(context)) {
if (vmd.get(VulnerabilityMetaData.Attribute.SCORE_INITIAL_OVERALL) != null) {
try {
final double score = Double.parseDouble(vmd.get(VulnerabilityMetaData.Attribute.SCORE_INITIAL_OVERALL));
final String severity = securityPolicy.getCvssSeverityRanges().getRange(score).getName();
vmd.set(VulnerabilityMetaData.Attribute.SCORE_INITIAL_OVERALL_SEVERITY, severity);
} catch (NumberFormatException e) {
LOG.error("Could not parse CVSS Initial Overall score for vulnerability [{}]: [{}]", vmd.get(VulnerabilityMetaData.Attribute.NAME), vmd.get(VulnerabilityMetaData.Attribute.SCORE_INITIAL_OVERALL));
}
}
if (vmd.get(VulnerabilityMetaData.Attribute.SCORE_CONTEXT_OVERALL) != null) {
try {
final double score = Double.parseDouble(vmd.get(VulnerabilityMetaData.Attribute.SCORE_CONTEXT_OVERALL));
final String severity = securityPolicy.getCvssSeverityRanges().getRange(score).getName();
vmd.set(VulnerabilityMetaData.Attribute.SCORE_CONTEXT_OVERALL_SEVERITY, severity);
} catch (NumberFormatException e) {
LOG.error("Could not parse CVSS Context Overall score for vulnerability [{}]: [{}]", vmd.get(VulnerabilityMetaData.Attribute.NAME), vmd.get(VulnerabilityMetaData.Attribute.SCORE_CONTEXT_OVERALL));
}
}
}
}
}
public void writeToFile(File file) throws IOException {
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
new InventoryWriter().writeInventory(inventory, file);
}
public void extractMatchingArtifacts(MatchableDetailsAmbDataClass, ?> dc, Map>> affectsArtifacts) {
// affectsArtifacts is a map of:
// Artifact Field Names --> Artifact --> Set of Vulnerability Names
final Map> affectedArtifactsForDataClass = dc.getAffectedArtifacts();
synchronized (affectedArtifactsForDataClass) {
for (Map.Entry> keyToArtifacts : affectedArtifactsForDataClass.entrySet()) {
final String key = keyToArtifacts.getKey();
final Set artifacts = keyToArtifacts.getValue();
for (Artifact artifact : artifacts) {
affectsArtifacts
.computeIfAbsent(key, k -> new HashMap<>())
.computeIfAbsent(artifact, k -> new LinkedHashSet<>()).add(dc.getId());
}
}
}
}
public void reAssociateVulnerabilities() {
if (!reAssociateVulnerabilities) {
LOG.debug("Skipping re-association of vulnerabilities due to configuration");
}
synchronized (this.vulnerabilities) {
for (Vulnerability vulnerability : this.vulnerabilities) {
this.reAssociateVulnerability(vulnerability);
}
}
}
protected void reAssociateVulnerability(Vulnerability vulnerability) {
if (!reAssociateVulnerabilities) {
LOG.debug("Skipping re-association of vulnerability [{}] due to configuration", vulnerability.getId());
return;
}
vulnerability.getSecurityAdvisories().clear();
synchronized (vulnerability.getReferencedSecurityAdvisories()) {
for (Map.Entry, Set> refEntry : vulnerability.getReferencedSecurityAdvisories().entrySet()) {
final AdvisoryTypeIdentifier> source = refEntry.getKey();
final Set ids = refEntry.getValue();
ids.stream()
.map(this::findAdvisoryEntryByName)
.filter(Optional::isPresent).map(Optional::get)
.forEach(securityAdvisory -> {
if (securityAdvisory.getSourceIdentifier() != source) {
LOG.warn("Overwriting source for advisory [{}] from [{}] to [{}] due to more detailed information on vulnerability [{}]",
securityAdvisory.getId(), securityAdvisory.getSourceIdentifier(), source, vulnerability.getId());
securityAdvisory.setSourceIdentifier(source);
}
vulnerability.addSecurityAdvisory(securityAdvisory);
});
}
}
}
public void reAssociateAdvisories() {
if (!reAssociateAdvisories) {
LOG.debug("Skipping re-association of advisories due to configuration");
return;
}
final Set unknownAdvisories = new HashSet<>();
synchronized (this.vulnerabilities) {
synchronized (this.securityAdvisories) {
for (Vulnerability vulnerability : this.vulnerabilities) {
for (AdvisoryEntry securityAdvisory : vulnerability.getSecurityAdvisories()) {
if (!this.securityAdvisories.contains(securityAdvisory)) {
unknownAdvisories.add(securityAdvisory);
}
}
}
}
}
if (!unknownAdvisories.isEmpty()) {
this.addAllAdvisories(unknownAdvisories);
}
synchronized (this.securityAdvisories) {
for (AdvisoryEntry advisoryEntry : this.securityAdvisories) {
this.reAssociateAdvisory(advisoryEntry);
}
}
}
protected void reAssociateAdvisory(AdvisoryEntry advisoryEntry) {
if (!reAssociateAdvisories) {
LOG.debug("Skipping re-association of advisory [{}] due to configuration", advisoryEntry.getId());
return;
}
synchronized (this.vulnerabilities) {
// find vulnerabilities that reference this advisory and add it to the list of security advisories
for (Vulnerability vulnerability : this.vulnerabilities) {
if (!vulnerability.getReferencedSecurityAdvisories().isEmpty()) {
final boolean hasMatchingAdvisories = vulnerability.deepCopyReferencedSecurityAdvisories().values().stream()
.flatMap(Collection::stream)
.anyMatch(e -> StringUtils.equals(advisoryEntry.getId(), e));
if (hasMatchingAdvisories) {
vulnerability.addSecurityAdvisory(advisoryEntry);
}
}
}
}
}
public void parse() {
parse(inventory.getArtifacts(), inventory.getVulnerabilityMetaData(vulnerabilityContextName), inventory.getAdvisoryMetaData());
}
protected void parse(List artifacts, List vmds, List amds) {
synchronized (this.vulnerabilities) {
this.vulnerabilities.clear();
this.vulnerabilityByName.clear();
}
synchronized (this.securityAdvisories) {
this.securityAdvisories.clear();
this.advisoryByName.clear();
}
final long startTime = System.currentTimeMillis();
// find all artifacts that reference vulnerabilities and collect them
final Map>> affectsArtifacts = new HashMap<>();
for (Artifact artifact : artifacts) {
extractAffectsArtifactsForKey(artifact, affectsArtifacts, Artifact.Attribute.VULNERABILITY);
extractAffectsArtifactsForKey(artifact, affectsArtifacts, InventoryAttribute.VULNERABILITIES_FIXED_BY_KB);
extractAffectsArtifactsForKey(artifact, affectsArtifacts, InventoryAttribute.INAPPLICABLE_CVE);
extractAffectsArtifactsForKey(artifact, affectsArtifacts, InventoryAttribute.ADDON_CVES);
}
// parse individual vulnerabilities
synchronized (this.vulnerabilities) {
for (VulnerabilityMetaData vmd : vmds) {
final Vulnerability vulnerability = Vulnerability.fromVulnerabilityMetaData(vmd);
this.vulnerabilities.add(vulnerability);
this.vulnerabilityByName.put(vulnerability.getId(), vulnerability);
// add all artifacts that reference this vulnerability
final Map> artifactMap = affectsArtifacts.get(vulnerability.getId());
if (artifactMap != null) {
for (Map.Entry> artifactEntry : artifactMap.entrySet()) {
final String key = artifactEntry.getKey();
final List artifactsForVulnerability = artifactEntry.getValue();
for (Artifact artifact : artifactsForVulnerability) {
vulnerability.manuallyAffectsArtifact(key, artifact);
}
}
}
}
}
// parse individual security advisories
synchronized (this.securityAdvisories) {
for (AdvisoryMetaData amd : amds) {
final AdvisoryEntry advisoryEntry = AdvisoryEntry.fromAdvisoryMetaData(amd);
this.securityAdvisories.add(advisoryEntry);
this.advisoryByName.put(advisoryEntry.getId(), advisoryEntry);
// add all artifacts that reference this advisory
final Map> artifactMap = affectsArtifacts.get(advisoryEntry.getId());
if (artifactMap != null) {
for (Map.Entry> artifactEntry : artifactMap.entrySet()) {
final String key = artifactEntry.getKey();
final List artifactsForVulnerability = artifactEntry.getValue();
for (Artifact artifact : artifactsForVulnerability) {
advisoryEntry.manuallyAffectsArtifact(key, artifact);
}
}
}
}
}
// add advisory references on vulnerabilities where applicable
this.reAssociateVulnerabilities();
LOG.debug("Parsed [{}] vulnerabilities and [{}] security advisories in [{}] ms",
this.vulnerabilities.size(), this.securityAdvisories.size(), System.currentTimeMillis() - startTime);
}
private static void extractAffectsArtifactsForKey(Artifact artifact, Map>> affectsArtifacts, AbstractModelBase.Attribute attribute) {
final String vulnerabilityString = artifact.get(attribute);
if (StringUtils.hasText(vulnerabilityString)) {
final Set vulnerabilityNames = new HashSet<>(Arrays.asList(vulnerabilityString.split(", ")));
for (String name : vulnerabilityNames) {
affectsArtifacts
.computeIfAbsent(name, k -> new HashMap<>())
.computeIfAbsent(attribute.getKey(), k -> new ArrayList<>())
.add(artifact);
}
}
}
public static VulnerabilityContextInventory fromFile(File file) throws IOException {
return fromInventory(new InventoryReader().readInventory(file));
}
public static VulnerabilityContextInventory fromInventory(Inventory inventory) {
final VulnerabilityContextInventory vInventory = new VulnerabilityContextInventory(inventory);
vInventory.parse();
return vInventory;
}
public static VulnerabilityContextInventory fromInventory(Inventory inventory, String vulnerabilityContextName) {
final VulnerabilityContextInventory vInventory = new VulnerabilityContextInventory(inventory, vulnerabilityContextName);
vInventory.parse();
return vInventory;
}
public static Map fromInventories(Map inventories) {
final Map vInventories = new HashMap<>();
for (Map.Entry entry : inventories.entrySet()) {
final String vulnerabilityContextName = entry.getKey();
final Inventory inventory = entry.getValue();
final VulnerabilityContextInventory vInventory = new VulnerabilityContextInventory(inventory);
vInventory.parse();
vInventories.put(vulnerabilityContextName, vInventory);
}
return vInventories;
}
public static VulnerabilityContextInventory empty() {
return new VulnerabilityContextInventory(new Inventory());
}
}