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

com.metaeffekt.mirror.contents.advisory.MsrcAdvisorEntry 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.mirror.contents.advisory;

import com.metaeffekt.artifact.analysis.utils.CustomCollectors;
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.enrichment.InventoryEnricher;
import com.metaeffekt.mirror.contents.base.CvssConditionAttributes;
import com.metaeffekt.mirror.contents.base.DescriptionParagraph;
import com.metaeffekt.mirror.contents.base.Reference;
import com.metaeffekt.mirror.contents.msrcdata.MsThreat;
import com.metaeffekt.mirror.contents.msrcdata.MsrcRemediation;
import com.metaeffekt.mirror.contents.store.AdvisoryTypeStore;
import com.metaeffekt.mirror.contents.store.VulnerabilityTypeStore;
import org.apache.lucene.document.Document;
import org.apache.lucene.document.Field;
import org.apache.lucene.document.TextField;
import org.json.JSONArray;
import org.json.JSONObject;
import org.metaeffekt.core.inventory.processor.model.AdvisoryMetaData;
import org.metaeffekt.core.inventory.processor.model.Artifact;
import org.metaeffekt.core.inventory.processor.report.model.AdvisoryUtils;
import org.metaeffekt.core.security.cvss.CvssSource;
import org.metaeffekt.core.security.cvss.CvssVector;
import org.metaeffekt.core.security.cvss.KnownCvssEntities;
import org.metaeffekt.core.security.cvss.v3.Cvss3P1;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import java.util.*;
import java.util.stream.Collectors;

public class MsrcAdvisorEntry extends AdvisoryEntry {

    private final static Logger LOG = LoggerFactory.getLogger(MsrcAdvisorEntry.class);

    protected final static Set CONVERSION_KEYS_AMB = new HashSet(AdvisoryEntry.CONVERSION_KEYS_AMB) {{
        add(InventoryAttribute.MS_AFFECTED_PRODUCTS.getKey());
        add(InventoryAttribute.MS_THREATS.getKey());
        add(InventoryAttribute.MS_REMEDIATIONS.getKey());
    }};

    protected final static Set CONVERSION_KEYS_MAP = new HashSet(AdvisoryEntry.CONVERSION_KEYS_MAP) {{
        add("affectedProducts");
        add("msThreats");
        add("msRemediations");
    }};


    protected final Set affectedProducts = new HashSet<>();
    protected final Set msThreats = new HashSet<>();
    protected final Set msrcRemediations = new HashSet<>();

    public MsrcAdvisorEntry() {
        super(AdvisoryTypeStore.MSRC);
    }

    public MsrcAdvisorEntry(String id) {
        super(AdvisoryTypeStore.MSRC, id);
    }

    public void addAffectedProduct(String product) {
        affectedProducts.add(product);
    }

    public void addAffectedProducts(Collection products) {
        affectedProducts.addAll(products);
    }

    public void addMsThreat(MsThreat threat) {
        msThreats.add(threat);
    }

    public void addMsThreats(Collection threats) {
        msThreats.addAll(threats);
    }

    private void addMsThreats(JSONArray jsonArray) {
        for (int i = 0; i < jsonArray.length(); i++) {
            final JSONObject json = jsonArray.getJSONObject(i);
            final MsThreat threat = MsThreat.fromJson(json);
            msThreats.add(threat);
        }
    }

    private void addMsThreats(List> maps) {
        for (Map map : maps) {
            final MsThreat threat = MsThreat.fromMap(map);
            msThreats.add(threat);
        }
    }

    public void addMsRemediation(MsrcRemediation remediation) {
        msrcRemediations.add(remediation);
    }

    public void addMsRemediations(Collection remediations) {
        msrcRemediations.addAll(remediations);
    }

    private void addMsRemediations(JSONArray jsonArray) {
        for (int i = 0; i < jsonArray.length(); i++) {
            final JSONObject json = jsonArray.getJSONObject(i);
            final MsrcRemediation remediation = MsrcRemediation.fromJson(json);
            msrcRemediations.add(remediation);
        }
    }

    private void addMsRemediations(List> maps) {
        for (Map map : maps) {
            final MsrcRemediation remediation = MsrcRemediation.fromMap(map);
            msrcRemediations.add(remediation);
        }
    }

    public Set getAffectedProducts() {
        return affectedProducts;
    }

    public Set getMsThreats() {
        return msThreats;
    }

    public Set getMsRemediations() {
        return msrcRemediations;
    }

    public Set getMsRemediationsByAffectedProduct(String productId) {
        return msrcRemediations.stream()
                .filter(r -> r.getAffectedProductIds().contains(productId))
                .collect(Collectors.toSet());
    }

    public Set getMsRemediationsByDescriptionEquals(String description) {
        if (description.charAt(0) == 'K' && description.charAt(1) == 'B') {
            LOG.warn("MSRC remediation description starts with KB, which is most likely a mistake: {}", description);
        }
        return msrcRemediations.stream()
                .filter(r -> Objects.equals(description, r.getDescription()))
                .collect(Collectors.toSet());
    }

    public String getCveFromId() {
        if (StringUtils.isEmpty(super.id) || super.id.contains("ADV")) return null;
        return super.id.replace("MSRC-", "");
    }

    @Override
    public String getUrl() {
        return "https://msrc.microsoft.com/update-guide/en-US/vulnerability/" + id;
    }

    @Override
    public String getType() {
        return AdvisoryUtils.normalizeType("alert");
    }

    /* TYPE CONVERSION METHODS */

    @Override
    protected Set conversionKeysAmb() {
        return CONVERSION_KEYS_AMB;
    }

    @Override
    protected Set conversionKeysMap() {
        return CONVERSION_KEYS_MAP;
    }

    @Override
    public CertSeiAdvisorEntry constructDataClass() {
        return new CertSeiAdvisorEntry();
    }

    public static MsrcAdvisorEntry fromAdvisoryMetaData(AdvisoryMetaData amd) {
        return AdvisoryEntry.fromAdvisoryMetaData(amd, MsrcAdvisorEntry::new);
    }

    public static MsrcAdvisorEntry fromInputMap(Map map) {
        return AdvisoryEntry.fromInputMap(map, MsrcAdvisorEntry::new);
    }

    public static MsrcAdvisorEntry fromJson(JSONObject json) {
        return AdvisoryEntry.fromJson(json, MsrcAdvisorEntry::new);
    }

    public static MsrcAdvisorEntry fromDocument(Document document) {
        return AdvisoryEntry.fromDocument(document, MsrcAdvisorEntry::new);
    }

    @Override
    public void appendFromDataClass(AdvisoryEntry dataClass) {
        super.appendFromDataClass(dataClass);

        if (!(dataClass instanceof MsrcAdvisorEntry)) {
            return;
        }

        final MsrcAdvisorEntry msrcAdvisorEntry = (MsrcAdvisorEntry) dataClass;

        this.addAffectedProducts(msrcAdvisorEntry.getAffectedProducts());
        this.addMsThreats(msrcAdvisorEntry.getMsThreats());
        this.addMsRemediations(msrcAdvisorEntry.getMsRemediations());
    }

    @Override
    public void appendFromBaseModel(AdvisoryMetaData amd) {
        super.appendFromBaseModel(amd);

        if (amd.get(InventoryAttribute.MS_AFFECTED_PRODUCTS) != null) {
            this.addAffectedProducts(Arrays.stream(amd.get(InventoryAttribute.MS_AFFECTED_PRODUCTS).split(", ")).collect(Collectors.toSet()));
        }
        if (amd.get(InventoryAttribute.MS_THREATS) != null) {
            this.addMsThreats(new JSONArray(amd.get(InventoryAttribute.MS_THREATS)));
        }
        if (amd.get(InventoryAttribute.MS_REMEDIATIONS) != null) {
            this.addMsRemediations(new JSONArray(amd.get(InventoryAttribute.MS_REMEDIATIONS)));
        }
    }

    @Override
    public void appendToBaseModel(AdvisoryMetaData amd) {
        super.appendToBaseModel(amd);

        amd.set(InventoryAttribute.MS_AFFECTED_PRODUCTS, String.join(", ", affectedProducts));
        amd.set(InventoryAttribute.MS_THREATS, new JSONArray(msThreats.stream().map(MsThreat::toJson).collect(Collectors.toList())).toString());
        amd.set(InventoryAttribute.MS_REMEDIATIONS, new JSONArray(msrcRemediations.stream().map(MsrcRemediation::toJson).collect(Collectors.toList())).toString());
    }

    @Override
    public void appendFromMap(Map map) {
        super.appendFromMap(map);

        try {
            if (map.containsKey("affectedProducts")) {
                this.addAffectedProducts(((List) map.get("affectedProducts")).stream().map(Object::toString).collect(Collectors.toSet()));
            }
            if (map.containsKey("msThreats")) {
                this.addMsThreats(((List>) map.get("msThreats")));
            }
            if (map.containsKey("msRemediations")) {
                this.addMsRemediations(((List>) map.get("msRemediations")));
            }
        } catch (Exception e) {
            LOG.error("Error parsing MSRC Advisor entry from map:\n" + map, e);
        }
    }

    @Override
    public void appendToJson(JSONObject json) {
        super.appendToJson(json);

        json.put("affectedProducts", affectedProducts.stream().collect(CustomCollectors.toJsonArray()));
        json.put("msThreats", msThreats.stream().map(MsThreat::toJson).collect(CustomCollectors.toJsonArray()));
        json.put("msRemediations", msrcRemediations.stream().map(MsrcRemediation::toJson).collect(CustomCollectors.toJsonArray()));
    }

    @Override
    public void appendFromDocument(Document document) {
        super.appendFromDocument(document);

        this.addAffectedProducts(new JSONArray(document.get("affectedProducts")).toList().stream().map(Object::toString).collect(Collectors.toSet()));
        this.addMsThreats(new JSONArray(document.get("msThreats")));
        this.addMsRemediations(new JSONArray(document.get("msRemediations")));
    }

    @Override
    public void appendToDocument(Document document) {
        super.appendToDocument(document);

        document.add(new TextField("affectedProducts", affectedProducts.stream().collect(CustomCollectors.toJsonArray()).toString(), Field.Store.YES));
        document.add(new TextField("msThreats", msThreats.stream().map(MsThreat::toJson).collect(CustomCollectors.toJsonArray()).toString(), Field.Store.YES));
        document.add(new TextField("msRemediations", msrcRemediations.stream().map(MsrcRemediation::toJson).collect(CustomCollectors.toJsonArray()).toString(), Field.Store.YES));
    }

    /* PARSING OF MSRC ADVISORIES */

    public static List fromDownloadXml(org.w3c.dom.Document document) {
        final List entries = new ArrayList<>();

        final NodeList vulnerabilityElements = document.getElementsByTagName("vuln:Vulnerability");

        for (int i = 0; i < vulnerabilityElements.getLength(); i++) {
            final Element vulnerabilityElement = (Element) vulnerabilityElements.item(i);

            final MsrcAdvisorEntry entry = fromDownloadXmlVulnerabilityElement(vulnerabilityElement);

            entries.add(entry);
        }

        return entries;
    }

    public static MsrcAdvisorEntry fromDownloadXmlVulnerabilityElement(Element vulnerabilityElement) {
        final MsrcAdvisorEntry entry = new MsrcAdvisorEntry();

        final String title = vulnerabilityElement.getElementsByTagName("vuln:Title").item(0).getTextContent();
        entry.setSummary(title);

        final String cve = vulnerabilityElement.getElementsByTagName("vuln:CVE").item(0).getTextContent();
        if (VulnerabilityTypeStore.CVE.patternMatchesId(cve)) {
            entry.addReferencedVulnerability(VulnerabilityTypeStore.CVE, cve);
        }
        final boolean isAdv = cve.contains("ADV");
        String msrcId = cve.replaceAll("(MSRC-)?(CVE|CAN)-?", "")
                .replaceAll(".*?(\\d{1,4})-(\\d+).*", "$1-$2")
                .replace("ADV", "");
        msrcId = isAdv ? "ADV" + msrcId : "MSRC-CVE-" + msrcId;
        entry.setId(msrcId);


        final Element notesElement = (Element) vulnerabilityElement.getElementsByTagName("vuln:Notes").item(0);
        final NodeList noteElements = notesElement.getElementsByTagName("vuln:Note");

        for (int j = 0; j < noteElements.getLength(); j++) {
            final Element noteElement = (Element) noteElements.item(j);

            final String noteText = Arrays.stream(noteElement.getTextContent().split("\n"))
                    .map(e -> e.replaceAll("\\s{2,}", " "))
                    .map(String::trim)
                    .filter(s -> !s.isEmpty())
                    .collect(Collectors.joining("\n"));

            if (StringUtils.hasText(noteText)) {
                final String noteType = noteElement.getAttribute("Type");
                final String noteTitle = noteElement.getAttribute("Title");

                final String noteTitleJoined = noteTitle.equals(noteType) ? noteTitle : noteType + ": " + noteTitle;

                entry.addDescription(DescriptionParagraph.fromTitleAndContent(noteTitleJoined, noteText));
            }
        }


        final Element productStatusesElement = (Element) vulnerabilityElement.getElementsByTagName("vuln:ProductStatuses").item(0);
        final NodeList statusElements = productStatusesElement.getElementsByTagName("vuln:Status");

        for (int j = 0; j < statusElements.getLength(); j++) {
            final Element statusElement = (Element) statusElements.item(j);
            if (statusElement.getAttribute("Type").equals("Known Affected")) {
                final NodeList productIdElements = statusElement.getElementsByTagName("vuln:ProductID");

                for (int k = 0; k < productIdElements.getLength(); k++) {
                    final Element productIdElement = (Element) productIdElements.item(k);
                    final String productId = productIdElement.getTextContent();

                    entry.addAffectedProduct(productId);
                }
            } else {
                LOG.info("Unknown status type: {}", statusElement.getAttribute("Type"));
            }
        }


        final Element threatsElement = (Element) vulnerabilityElement.getElementsByTagName("vuln:Threats").item(0);
        final NodeList threatElements = threatsElement.getElementsByTagName("vuln:Threat");

        for (int j = 0; j < threatElements.getLength(); j++) {
            final Element threatElement = (Element) threatElements.item(j);
            final String threatType = threatElement.getAttribute("Type");
            final String threatDescription = threatElement.getElementsByTagName("vuln:Description").item(0).getTextContent()
                    .replace("\n", " ")
                    .replaceAll(" +", " ")
                    .trim();

            final Node productIdItem = threatElement.getElementsByTagName("vuln:ProductID").item(0);
            final String threatProductId;
            if (productIdItem == null) {
                threatProductId = null;
            } else {
                threatProductId = productIdItem.getTextContent();
            }

            entry.addMsThreat(new MsThreat(threatType, threatProductId, threatDescription));
        }

        for (MsThreat msThreat : entry.getMsThreats()) {
            if (StringUtils.isEmpty(msThreat.getProductId())) {
                entry.setThreat((msThreat.getType() == null ? "" : msThreat.getType() + ": ") + msThreat.getDescription().replaceAll("([;:])(?! )", "$1 "));
            }
        }


        final Element remediationsElement = (Element) vulnerabilityElement.getElementsByTagName("vuln:Remediations").item(0);
        final NodeList remediationElements = remediationsElement.getElementsByTagName("vuln:Remediation");

        for (int i = 0; i < remediationElements.getLength(); i++) {
            final Element remediationElement = (Element) remediationElements.item(i);

            final String remediationType = remediationElement.getAttribute("Type");
            final String remediationDescription = remediationElement.getElementsByTagName("vuln:Description").item(0).getTextContent()
                    .replace("\n", " ")
                    .replaceAll(" +", " ")
                    .trim();
            final String remediationUrl = remediationElement.getElementsByTagName("vuln:URL").item(0) == null ?
                    null : remediationElement.getElementsByTagName("vuln:URL").item(0).getTextContent();
            final String remediationSupercedence = remediationElement.getElementsByTagName("vuln:Supercedence").item(0) == null ?
                    null : remediationElement.getElementsByTagName("vuln:Supercedence").item(0).getTextContent();

            final Set remediationProductIds = new HashSet<>();
            final NodeList remediationProductIdElements = remediationElement.getElementsByTagName("vuln:ProductID");
            for (int j = 0; j < remediationProductIdElements.getLength(); j++) {
                final Element remediationProductIdElement = (Element) remediationProductIdElements.item(j);
                remediationProductIds.add(remediationProductIdElement.getTextContent());
            }

            final String remediationSubType = remediationElement.getElementsByTagName("vuln:SubType").item(0) == null ?
                    null : remediationElement.getElementsByTagName("vuln:SubType").item(0).getTextContent();
            final String remediationFixedBuild = remediationElement.getElementsByTagName("vuln:FixedBuild").item(0) == null ?
                    null : remediationElement.getElementsByTagName("vuln:FixedBuild").item(0).getTextContent();

            final MsrcRemediation remediation = new MsrcRemediation(remediationType, remediationSubType, remediationDescription, StringUtils.isEmpty(remediationUrl) ? null : Reference.fromTitleAndUrl(remediationDescription, remediationUrl), remediationProductIds, remediationFixedBuild, remediationSupercedence);
            entry.addMsRemediation(remediation);
        }


        final Element cvssScoreSetsElement = (Element) vulnerabilityElement.getElementsByTagName("vuln:CVSSScoreSets").item(0);
        final NodeList scoreSetElements = cvssScoreSetsElement.getElementsByTagName("vuln:ScoreSet");

        final Map> identicalProductVectors = new HashMap<>();
        for (int j = 0; j < scoreSetElements.getLength(); j++) {
            final Element scoreSetElement = (Element) scoreSetElements.item(j);

            final String vector = scoreSetElement.getElementsByTagName("vuln:Vector").item(0).getTextContent();
            final String productId = scoreSetElement.getElementsByTagName("vuln:ProductID").item(0).getTextContent();

            identicalProductVectors.computeIfAbsent(new Cvss3P1(vector), k -> new HashSet<>()).add(productId);
        }

        final CvssSource msrcCvss3Source = new CvssSource(KnownCvssEntities.MSRC, Cvss3P1.class);

        for (Map.Entry> cvssProductsEntry : identicalProductVectors.entrySet()) {
            final Cvss3P1 cvssVector = cvssProductsEntry.getKey();
            final Set productIds = cvssProductsEntry.getValue();
            final JSONObject applicabilityCondition = new JSONObject()
                    .put(CvssConditionAttributes.MATCHES_ON_MS_PRODUCT_ID, new JSONArray(productIds));
            final CvssVector effectiveVector = cvssVector.deriveAddSource(msrcCvss3Source);
            effectiveVector.putAllApplicabilityCondition(applicabilityCondition);
            entry.getCvssVectors().addCvssVector(effectiveVector);
        }


        final Element revisionHistoryElement = (Element) vulnerabilityElement.getElementsByTagName("vuln:RevisionHistory").item(0);
        final NodeList revisionElements = revisionHistoryElement.getElementsByTagName("vuln:Revision");

        final List revisionDates = new ArrayList<>();
        for (int j = 0; j < revisionElements.getLength(); j++) {
            final Element revisionElement = (Element) revisionElements.item(j);
            final String dateString = revisionElement.getElementsByTagName("cvrf:Date").item(0).getTextContent();
            final Date date = TimeUtils.tryParse(dateString);

            if (date != null) {
                revisionDates.add(date);
            }
        }

        revisionDates.sort(Comparator.comparing(Date::getTime));
        entry.setCreateDate(!revisionDates.isEmpty() ? revisionDates.get(0) : null);
        entry.setUpdateDate(!revisionDates.isEmpty() ? revisionDates.get(revisionDates.size() - 1) : null);


        for (DescriptionParagraph description : entry.getDescription()) {
            entry.addReferencedSecurityAdvisories(AdvisoryTypeStore.MSRC, AdvisoryTypeStore.MSRC.fromFreeText(description.getContent()));
        }


        if (StringUtils.isEmpty(entry.getId())) {
            LOG.warn("No id found for MSRC entry {}", entry.toJson());
        }

        return entry;
    }

    public static Set getAllMsrcProductIds(Collection artifacts) {
        return artifacts.stream()
                .filter(a -> StringUtils.hasText(a.get(InventoryAttribute.MS_PRODUCT_ID.getKey())))
                .map(a -> a.get(InventoryAttribute.MS_PRODUCT_ID.getKey()))
                .map(InventoryEnricher::splitVulnerabilitiesCsv)
                .flatMap(Collection::stream)
                .collect(Collectors.toSet());
    }
}