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

com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatusConverter 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.FileUtils;
import com.metaeffekt.artifact.analysis.utils.SnakeYamlParser;
import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.analysis.utils.TimeUtils;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.InventoryAttribute;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.filter.FilterAttribute;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.validation.VulnerabilityStatusValidation;
import org.apache.commons.io.filefilter.DirectoryFileFilter;
import org.apache.commons.io.filefilter.TrueFileFilter;
import org.json.JSONArray;
import org.json.JSONObject;
import org.metaeffekt.core.inventory.processor.model.VulnerabilityMetaData;
import org.metaeffekt.core.inventory.processor.report.configuration.CentralSecurityPolicyConfiguration;
import org.metaeffekt.core.inventory.processor.report.configuration.CentralSecurityPolicyConfiguration.JsonSchemaValidationErrorsHandling;
import org.metaeffekt.core.security.cvss.CvssVector;
import org.metaeffekt.core.security.cvss.v2.Cvss2;
import org.metaeffekt.core.security.cvss.v3.Cvss3P1;
import org.metaeffekt.core.security.cvss.v4P0.Cvss4P0;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.DumperOptions;
import org.yaml.snakeyaml.Yaml;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * Due to the large amount of conversion methods, this class has been separated from the
 * {@link VulnerabilityStatus} class.
* It contains methods to convert the status data from YAML files or {@link VulnerabilityMetaData} to * {@link VulnerabilityStatus} instances and vice versa. */ public class VulnerabilityStatusConverter { private final static Logger LOG = LoggerFactory.getLogger(VulnerabilityStatusConverter.class); /** * Restores a VulnerabilityStatus from a VulnerabilityMetaData instance that has status data applied to it.
* The vulnerability status is stored in several fields: *
    *
  • * {@link InventoryAttribute#STATUS_HISTORY} contains a JSON Array of JSON Objects that * represent the individual {@link VulnerabilityStatusHistoryEntry} instances. *
  • *
  • {@link VulnerabilityMetaData.Attribute#NAME} contains the name of the vulnerability, which is affected.
  • *
  • * {@link InventoryAttribute#REVIEWED_ADVISORIES} contains a JSON Array of JSON Objects that * each represent a reviewed advisory (aka cert) entry with an optional comment. *
  • *
  • * {@link InventoryAttribute#STATUS_ACCEPTED} contains an author with an optional date * in parenthesis behind it. (Author (2022-01-10 01:00:00)) *
  • *
  • * {@link InventoryAttribute#STATUS_REPORTED} contains an author with an optional date * in parenthesis behind it. (Author (2022-01-10 01:00:00)) *
  • *
  • {@link InventoryAttribute#STATUS_TITLE} the title of the status entry.
  • *
* * @param vmd The VulnerabilityMetaData instance to restore the status from. * @return The restored VulnerabilityStatus instance. */ @Deprecated public static VulnerabilityStatus fromVulnerabilityMetaData(VulnerabilityMetaData vmd) { final VulnerabilityStatus parsedStatus = new VulnerabilityStatus(); final String statusHistoryString = vmd.get(InventoryAttribute.STATUS_HISTORY.getKey()); if (statusHistoryString != null && statusHistoryString.startsWith("[")) { final JSONArray historyJson = new JSONArray(statusHistoryString); final List entries = VulnerabilityStatusHistoryEntry.parseEntries(historyJson); parsedStatus.addHistoryEntries(entries); } if (StringUtils.hasText(vmd.get(VulnerabilityMetaData.Attribute.NAME))) { parsedStatus.addAffectedVulnerability(vmd.get(VulnerabilityMetaData.Attribute.NAME)); } final String reviewedAdvisoriesString = vmd.get(InventoryAttribute.REVIEWED_ADVISORIES.getKey()); if (reviewedAdvisoriesString != null && reviewedAdvisoriesString.startsWith("[")) { final JSONArray reviewedJson = new JSONArray(reviewedAdvisoriesString); final List entries = VulnerabilityStatusReviewedEntry.fromMultipleFormattedStringOrMapEntries(reviewedJson.toList()); parsedStatus.addReviewedAdvisoryEntries(entries); } final Pattern valueWithOptionalParenthesisPattern = Pattern.compile("^([^(]+)(?: \\(([^)]+)\\))?$"); final String acceptedBy = vmd.get(InventoryAttribute.STATUS_ACCEPTED.getKey()); final String reportedBy = vmd.get(InventoryAttribute.STATUS_REPORTED.getKey()); if (StringUtils.hasText(acceptedBy)) { final Matcher matcher = valueWithOptionalParenthesisPattern.matcher(acceptedBy); if (matcher.matches()) { parsedStatus.setAcceptedBy(matcher.group(1)); parsedStatus.setAcceptedDate(matcher.groupCount() == 2 ? matcher.group(2) : null); } } if (StringUtils.hasText(reportedBy)) { final Matcher matcher = valueWithOptionalParenthesisPattern.matcher(reportedBy); if (matcher.matches()) { parsedStatus.setReportedBy(matcher.group(1)); parsedStatus.setReportedDate(matcher.groupCount() == 2 ? matcher.group(2) : null); } } final String statusTitle = vmd.get(InventoryAttribute.STATUS_TITLE.getKey()); parsedStatus.setTitle(StringUtils.hasText(statusTitle) ? statusTitle : null); return parsedStatus; } public static VulnerabilityStatus fromLegacyFormatFromVulnerabilityMetaData(VulnerabilityMetaData vulnerabilityMetaData) { VulnerabilityStatus status = new VulnerabilityStatus(); status.addAffectedVulnerability(vulnerabilityMetaData.get(VulnerabilityMetaData.Attribute.NAME)); status.addHistoryEntry(new VulnerabilityStatusHistoryEntry( vulnerabilityMetaData.get(VulnerabilityMetaData.Attribute.STATUS), vulnerabilityMetaData.get(VulnerabilityMetaData.Attribute.RATIONALE), vulnerabilityMetaData.get(VulnerabilityMetaData.Attribute.RISK), vulnerabilityMetaData.get(InventoryAttribute.MEASURES.getKey()), "Reference Inventory", new SimpleDateFormat("yyyy-MM-dd").format(new Date()), 0.0, null, null)); return status; } public static Set fromStatusFileOrDirectory(File fileOrDir) { return fromStatusFileOrDirectory(fileOrDir, CentralSecurityPolicyConfiguration.JSON_SCHEMA_VALIDATION_ERRORS_DEFAULT); } /** * Extracts all YAML files from the given directory or if file is already a File uses this file.
* For every file: Attempts to parse the file and creates a {@link VulnerabilityStatus} instance using the * {@link VulnerabilityStatusConverter#fromYaml(LinkedHashMap)} method. * * @param fileOrDir The File or Directory to parse the YAML file(s) from. * @param jsonSchemaValidationErrorsHandling Schema validation mode. * * @return The parsed YAML files * @throws RuntimeException If the status file is malformed. */ public static Set fromStatusFileOrDirectory(File fileOrDir, JsonSchemaValidationErrorsHandling jsonSchemaValidationErrorsHandling) { final Set parsedStatuses = new HashSet<>(); for (File file : extractVulnerabilityStatusFilesFromDirectory(fileOrDir)) { LOG.debug("Parsing vulnerability status from YAML file {}.", file.getAbsolutePath()); try { VulnerabilityStatus.assertVulnerabilityStatusFileValid(file, jsonSchemaValidationErrorsHandling); } catch (Exception e) { throw new RuntimeException("Failed to parse vulnerability status file from " + file.getAbsolutePath(), e); } try { final Object yamlRoot = SnakeYamlParser.parseYaml(SnakeYamlParser.createNoTimestampYaml(), file); if (yamlRoot instanceof LinkedHashMap) { final LinkedHashMap yamlRootMap = (LinkedHashMap) yamlRoot; final VulnerabilityStatus parsedStatus = fromYaml(yamlRootMap); parsedStatus.originYamlFile = file; parsedStatuses.add(parsedStatus); } else if (yamlRoot instanceof List) { final List> yamlRootList = (List>) yamlRoot; for (final LinkedHashMap yamlRootMap : yamlRootList) { final VulnerabilityStatus parsedStatus = fromYaml(yamlRootMap); parsedStatus.originYamlFile = file; parsedStatuses.add(parsedStatus); } } } catch (FileNotFoundException fileNotFoundException) { throw new RuntimeException("Failed to read status file " + file.getAbsolutePath(), fileNotFoundException); } catch (Exception exception) { throw new RuntimeException("Failed to parse status file, even though validation passed previously from " + file.getAbsolutePath(), exception); } } return parsedStatuses; } public static Set extractVulnerabilityStatusFilesFromDirectory(File cveStatusDir) { final Set files = new HashSet<>(); if (cveStatusDir.exists() && cveStatusDir.isDirectory()) { for (final File file : FileUtils.listFiles(cveStatusDir, TrueFileFilter.INSTANCE, DirectoryFileFilter.DIRECTORY)) { if (file.isFile() && file.getName().endsWith(".yaml")) { files.add(file); } } } else if (cveStatusDir.exists() && cveStatusDir.isFile()) { files.add(cveStatusDir); } else { LOG.warn("Status file directory does not exist: [{}]", cveStatusDir.getAbsolutePath()); } return files; } public static VulnerabilityStatus fromYaml(LinkedHashMap yamlRoot) { final VulnerabilityStatus parsedStatus = new VulnerabilityStatus(); // create VulnerabilityStatusHistoryEntry objects from the history list if (validateEntryType(yamlRoot, "history", ArrayList.class)) { ArrayList steps = (ArrayList) yamlRoot.get("history"); for (Object step : steps) { if (step instanceof LinkedHashMap) { VulnerabilityStatusHistoryEntry entry = VulnerabilityStatusHistoryEntry.fromMap((LinkedHashMap) step); parsedStatus.addHistoryEntry(entry); } } } // find what Advisor entries have already been reviewed if (validateEntryType(yamlRoot, "reviewed", ArrayList.class)) { final ArrayList steps = (ArrayList) yamlRoot.get("reviewed"); final List reviewedEntries = VulnerabilityStatusReviewedEntry.fromMultipleFormattedStringOrMapEntries(steps); parsedStatus.addReviewedAdvisoryEntries(reviewedEntries); } // affected CVEs and CPEs if (validateEntryType(yamlRoot, "affects", LinkedHashMap.class)) { LinkedHashMap> affectedIds = (LinkedHashMap>) yamlRoot.get("affects"); if (validateEntryType(affectedIds, "cve", ArrayList.class)) { affectedIds.get("cve").forEach(parsedStatus::addAffectedVulnerability); } if (validateEntryType(affectedIds, "cpe", ArrayList.class)) { affectedIds.get("cpe").forEach(parsedStatus::addAffectedCpe); } if (validateEntryType(affectedIds, "cwe", ArrayList.class)) { affectedIds.get("cwe").forEach(parsedStatus::addAffectedCwe); } if (validateEntryType(affectedIds, "condition", String.class)) { final FilterAttribute conditionAttribute = FilterAttribute.fromString(String.valueOf(affectedIds.get("condition"))); parsedStatus.addAffectedVulnerabilitiesFilter(conditionAttribute); } } // get the accepted/reported by information if (validateEntryType(yamlRoot, "accepted", LinkedHashMap.class)) { LinkedHashMap accepted = (LinkedHashMap) yamlRoot.get("accepted"); if (validateEntryType(accepted, "by", String.class)) { parsedStatus.setAcceptedBy(accepted.get("by").toString()); } if (validateEntryType(accepted, "date", Date.class, String.class)) { if (accepted.get("date") instanceof String) { final Date parsed = TimeUtils.tryParse(accepted.get("date").toString()); parsedStatus.setAcceptedDate(TimeUtils.formatNormalizedDate(parsed)); } else { parsedStatus.setAcceptedDate(TimeUtils.formatNormalizedDate((Date) accepted.get("date"))); } } } if (validateEntryType(yamlRoot, "reported", LinkedHashMap.class)) { LinkedHashMap reported = (LinkedHashMap) yamlRoot.get("reported"); if (reported.containsKey("by")) { parsedStatus.setReportedBy(reported.get("by").toString()); } if (validateEntryType(reported, "date", Date.class, String.class)) { if (reported.get("date") instanceof String) { final Date parsed = TimeUtils.tryParse(reported.get("date").toString()); parsedStatus.setReportedDate(TimeUtils.formatNormalizedDate(parsed)); } else { parsedStatus.setReportedDate(TimeUtils.formatNormalizedDate((Date) reported.get("date"))); } } } if (validateEntryType(yamlRoot, "cvssV2", String.class, LinkedHashMap.class)) { if (yamlRoot.get("cvssV2") instanceof LinkedHashMap) { final Map cvssV2Map = (LinkedHashMap) yamlRoot.get("cvssV2"); if (validateEntryType(cvssV2Map, "all", String.class)) { parsedStatus.setCvss2(new Cvss2(cvssV2Map.get("all").toString())); } if (validateEntryType(cvssV2Map, "higher", String.class)) { parsedStatus.setCvss2Higher(new Cvss2(cvssV2Map.get("higher").toString())); } if (validateEntryType(cvssV2Map, "lower", String.class)) { parsedStatus.setCvss2Lower(new Cvss2(cvssV2Map.get("lower").toString())); } } else { parsedStatus.setCvss2(new Cvss2(yamlRoot.get("cvssV2").toString())); } } if (validateEntryType(yamlRoot, "cvssV3", String.class, LinkedHashMap.class)) { if (yamlRoot.get("cvssV3") instanceof LinkedHashMap) { final Map cvssV3Map = (LinkedHashMap) yamlRoot.get("cvssV3"); if (validateEntryType(cvssV3Map, "all", String.class)) { parsedStatus.setCvss3P1(new Cvss3P1(cvssV3Map.get("all").toString())); } if (validateEntryType(cvssV3Map, "higher", String.class)) { parsedStatus.setCvss3P1Higher(new Cvss3P1(cvssV3Map.get("higher").toString())); } if (validateEntryType(cvssV3Map, "lower", String.class)) { parsedStatus.setCvss3P1Lower(new Cvss3P1(cvssV3Map.get("lower").toString())); } } else { parsedStatus.setCvss3P1(new Cvss3P1(yamlRoot.get("cvssV3").toString())); } } if (validateEntryType(yamlRoot, "cvssV4", String.class, LinkedHashMap.class)) { if (yamlRoot.get("cvssV4") instanceof LinkedHashMap) { final Map cvssV4Map = (LinkedHashMap) yamlRoot.get("cvssV4"); if (validateEntryType(cvssV4Map, "all", String.class)) { parsedStatus.setCvss4(new Cvss4P0(cvssV4Map.get("all").toString())); } if (validateEntryType(cvssV4Map, "higher", String.class)) { parsedStatus.setCvss4Higher(new Cvss4P0(cvssV4Map.get("higher").toString())); } if (validateEntryType(cvssV4Map, "lower", String.class)) { parsedStatus.setCvss4Lower(new Cvss4P0(cvssV4Map.get("lower").toString())); } } else { parsedStatus.setCvss4(new Cvss4P0(yamlRoot.get("cvssV4").toString())); } } if (validateEntryType(yamlRoot, "title", String.class)) parsedStatus.setTitle(yamlRoot.get("title").toString()); if (validateEntryType(yamlRoot, "scope", String.class)) { parsedStatus.setScope(VulnerabilityStatus.Scope.fromString(yamlRoot.get("scope").toString())); for (VulnerabilityStatusHistoryEntry entry : parsedStatus.getStatusHistory()) { entry.setScope(parsedStatus.getScope()); } } if (validateEntryType(yamlRoot, "validation", LinkedHashMap.class)) { final Map validationMap = (Map) yamlRoot.get("validation"); parsedStatus.setValidation(VulnerabilityStatusValidation.fromYamlMap(validationMap)); } return parsedStatus; } private static boolean validateEntryType(Map map, String key, Class... anyOfType) { if (map == null || !map.containsKey(key)) { return false; } boolean noneMatches = Arrays.stream(anyOfType).noneMatch(allowedClass -> (map.get(key).getClass().equals(allowedClass))); if (noneMatches) { final List expectedType = Arrays.stream(anyOfType).map(Class::getSimpleName).collect(Collectors.toList()); final String effectiveType = map.get(key).getClass().getSimpleName(); throw new IllegalArgumentException(String.format("Expected %s on element [%s] but got [%s]", expectedType, key, effectiveType)); } return true; } public static List fromJson(final JSONArray json) { final List statusList = new ArrayList<>(); for (int i = 0; i < json.length(); i++) { final JSONObject statusJson = json.getJSONObject(i); statusList.add(fromJson(statusJson)); } return statusList; } public static VulnerabilityStatus fromJson(final JSONObject json) { final VulnerabilityStatus status = new VulnerabilityStatus(); status.appendFromJson(json); return status; } /** * Generate a yaml file containing the status data. * * @param status The status to export. * @param file The file to write the data to. * @throws IOException If the writer was unable to write the file. */ public static void exportYaml(VulnerabilityStatus status, File file) throws IOException { final Map rootMap = new LinkedHashMap<>(); exportCvssInformationToYaml(rootMap, status.getCvss2(), status.getCvss2Higher(), status.getCvss2Lower(), "cvssV2"); exportCvssInformationToYaml(rootMap, status.getCvss3P1(), status.getCvss3P1Higher(), status.getCvss3P1Lower(), "cvssV3"); exportCvssInformationToYaml(rootMap, status.getCvss4(), status.getCvss4Higher(), status.getCvss4Lower(), "cvssV4"); if (status.getTitle() != null) rootMap.put("title", status.getTitle()); LinkedHashMap> affectedIds = new LinkedHashMap<>(); if (!status.getAffectedVulnerabilities().isEmpty()) affectedIds.put("cve", new ArrayList<>(status.getAffectedVulnerabilities())); if (!status.getAffectedCpe().isEmpty()) affectedIds.put("cpe", new ArrayList<>(status.getAffectedCpe())); if (!status.getAffectedCwe().isEmpty()) affectedIds.put("cwe", new ArrayList<>(status.getAffectedCwe())); rootMap.put("affects", affectedIds); Set reviewedEntries = new HashSet<>(); for (VulnerabilityStatusReviewedEntry entry : status.getReviewedAdvisories()) { reviewedEntries.add(entry.toString()); } if (!reviewedEntries.isEmpty()) rootMap.put("reviewed", reviewedEntries); LinkedHashMap reportedByMap = new LinkedHashMap<>(); if (status.getReportedDate() != null) reportedByMap.put("date", status.getReportedDate()); if (status.getReportedBy() != null) reportedByMap.put("by", status.getReportedBy()); if (!reportedByMap.isEmpty()) rootMap.put("reported", reportedByMap); LinkedHashMap acceptedByMap = new LinkedHashMap<>(); if (status.getAcceptedDate() != null) acceptedByMap.put("date", status.getAcceptedDate()); if (status.getAcceptedBy() != null) acceptedByMap.put("by", status.getAcceptedBy()); if (acceptedByMap.size() > 0) rootMap.put("accepted", acceptedByMap); if (status.getScope() != null) rootMap.put("scope", status.getScope()); List history = new ArrayList<>(); for (VulnerabilityStatusHistoryEntry historyEntry : status.getStatusHistory()) { LinkedHashMap historyEntryYaml = new LinkedHashMap<>(); if (historyEntry.getStatus() != null) historyEntryYaml.put("status", historyEntry.getStatus()); if (historyEntry.getAuthor() != null) historyEntryYaml.put("author", historyEntry.getAuthor()); if (historyEntry.getDate() != null) historyEntryYaml.put("date", historyEntry.getDate()); if (historyEntry.getRationale() != null) historyEntryYaml.put("rationale", historyEntry.getRationale()); if (historyEntry.getRisk() != null) historyEntryYaml.put("risk", historyEntry.getRisk()); if (historyEntry.getScore() != -1) historyEntryYaml.put("score", historyEntry.getScore()); history.add(historyEntryYaml); } if (history.size() > 0) rootMap.put("history", history); DumperOptions dumperOptions = new DumperOptions(); dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); FileWriter writer = new FileWriter(file); new Yaml(dumperOptions).dump(rootMap, writer); } private static void exportCvssInformationToYaml(Map rootMap, CvssVector vectorBase, CvssVector vectorHigher, CvssVector vectorLower, String cvssVersion) { final Map cvss2Map = new LinkedHashMap<>(); if (vectorBase != null) cvss2Map.put("all", vectorBase.toString()); if (vectorHigher != null) cvss2Map.put("higher", vectorHigher.toString()); if (vectorLower != null) cvss2Map.put("lower", vectorLower.toString()); if (!cvss2Map.isEmpty()) rootMap.put(cvssVersion, cvss2Map); } }