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

com.metaeffekt.artifact.enrichment.other.vad.VulnerabilityAssessmentDashboard 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.other.vad;

import com.metaeffekt.artifact.analysis.dashboard.*;
import com.metaeffekt.artifact.analysis.dashboard.elements.ContentCard;
import com.metaeffekt.artifact.analysis.dashboard.elements.TableBuilder;
import com.metaeffekt.artifact.analysis.utils.Vector;
import com.metaeffekt.artifact.analysis.utils.*;
import com.metaeffekt.artifact.analysis.vulnerability.CommonEnumerationUtil;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.InventoryAttribute;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.VersionComparator;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.filter.FilterAttribute;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.score.VulnerabilityPriorityCalculator;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatus;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatusConverter;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatusHistoryEntry;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.vulnerabilitystatus.VulnerabilityStatusReviewedEntry;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.warnings.InventoryWarningEntry;
import com.metaeffekt.artifact.analysis.vulnerability.enrichment.warnings.InventoryWarnings;
import com.metaeffekt.artifact.enrichment.InventoryEnricher;
import com.metaeffekt.artifact.enrichment.configurations.VadDetailLevelConfiguration;
import com.metaeffekt.artifact.enrichment.configurations.VulnerabilityAssessmentDashboardEnrichmentConfiguration;
import com.metaeffekt.artifact.enrichment.other.VulnerabilityOverviewChartGenerator;
import com.metaeffekt.artifact.enrichment.other.timeline.VulnerabilityTimeline;
import com.metaeffekt.artifact.enrichment.other.timeline.VulnerabilityTimeline.TimelineVersion;
import com.metaeffekt.artifact.enrichment.other.timeline.VulnerabilityTimelineGenerator;
import com.metaeffekt.artifact.enrichment.other.timeline.VulnerabilityTimelineGeneratorResult;
import com.metaeffekt.mirror.contents.advisory.*;
import com.metaeffekt.mirror.contents.base.DataSourceIndicator;
import com.metaeffekt.mirror.contents.base.DescriptionParagraph;
import com.metaeffekt.mirror.contents.base.Reference;
import com.metaeffekt.mirror.contents.base.VulnerabilityContextInventory;
import com.metaeffekt.mirror.contents.eol.EolCycle;
import com.metaeffekt.mirror.contents.eol.export.CycleScenarioRating;
import com.metaeffekt.mirror.contents.eol.export.CycleStateScenario;
import com.metaeffekt.mirror.contents.eol.export.ExportedCycleState;
import com.metaeffekt.mirror.contents.kev.KevData;
import com.metaeffekt.mirror.contents.msrcdata.MsThreat;
import com.metaeffekt.mirror.contents.msrcdata.MsrcProduct;
import com.metaeffekt.mirror.contents.msrcdata.MsrcRemediation;
import com.metaeffekt.mirror.contents.store.AdvisoryTypeIdentifier;
import com.metaeffekt.mirror.contents.store.AdvisoryTypeStore;
import com.metaeffekt.mirror.contents.store.ContentIdentifierStore;
import com.metaeffekt.mirror.contents.store.VulnerabilityTypeStore;
import com.metaeffekt.mirror.contents.vulnerability.Vulnerability;
import com.metaeffekt.mirror.contents.vulnerability.VulnerableSoftwareTreeNode;
import com.metaeffekt.mirror.contents.vulnerability.VulnerableSoftwareVersionRangeCpe;
import com.metaeffekt.mirror.download.documentation.EnricherMetadata;
import com.metaeffekt.mirror.download.documentation.InventoryEnrichmentPhase;
import com.metaeffekt.mirror.query.MsrcProductIndexQuery;
import com.metaeffekt.mirror.query.NvdCpeApiIndexQuery;
import com.metaeffekt.mirror.query.NvdCveIndexQuery;
import de.yanwittmann.j2chartjs.chart.Chart;
import de.yanwittmann.j2chartjs.chart.LineChart;
import de.yanwittmann.j2chartjs.chart.RadarChart;
import de.yanwittmann.j2chartjs.data.LineChartData;
import de.yanwittmann.j2chartjs.data.RadarChartData;
import de.yanwittmann.j2chartjs.dataset.LineChartDataset;
import de.yanwittmann.j2chartjs.dataset.RadarChartDataset;
import de.yanwittmann.j2chartjs.options.ChartOptions;
import de.yanwittmann.j2chartjs.options.interaction.InteractionOption;
import de.yanwittmann.j2chartjs.options.plugins.title.TitleOption;
import de.yanwittmann.j2chartjs.options.scale.LinearScaleOption;
import de.yanwittmann.j2chartjs.options.scale.RadialScaleOption;
import de.yanwittmann.j2chartjs.options.scale.RadialScaleTicksOption;
import de.yanwittmann.j2chartjs.options.scale.ScaleTitleOption;
import j2html.TagCreator;
import j2html.tags.ContainerTag;
import j2html.tags.DomContent;
import j2html.tags.UnescapedText;
import j2html.tags.specialized.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.commons.lang3.tuple.Triple;
import org.jfree.chart.ChartColor;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.labels.StandardCategoryToolTipGenerator;
import org.jfree.chart.plot.SpiderWebPlot;
import org.jfree.chart.ui.RectangleInsets;
import org.jfree.data.category.DefaultCategoryDataset;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.metaeffekt.core.inventory.processor.model.*;
import org.metaeffekt.core.inventory.processor.report.configuration.CentralSecurityPolicyConfiguration;
import org.metaeffekt.core.inventory.processor.report.configuration.VulnerabilityPriorityScoreConfiguration;
import org.metaeffekt.core.security.cvss.*;
import org.metaeffekt.core.security.cvss.processor.BakedCvssVectorScores;
import org.metaeffekt.core.security.cvss.processor.CvssSelectionResult;
import org.metaeffekt.core.security.cvss.processor.UniversalCvssCalculatorLinkGenerator;
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.metaeffekt.core.util.ColorScheme;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.Cpe;
import us.springett.parsers.cpe.values.Part;

import java.awt.*;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static j2html.TagCreator.*;

/**
 * This class uses an inventory to generate a 'Vulnerability Assessment Dashboard' as single-page HTML file.
 */
@EnricherMetadata(
        name = "Vulnerability Assessment Dashboard", phase = InventoryEnrichmentPhase.REPORTING,
        intermediateFileSuffix = "vulnerability-assessment-dashboard", mavenPropertyName = "vulnerabilityAssessmentDashboardEnrichment",
        shouldWriteIntermediateInventory = false
)
public class VulnerabilityAssessmentDashboard extends InventoryEnricher {

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

    /**
     * The current dashboard version. Independent value that should be updated with each release.
     */
    public final static String VERSION = "v" + BuildProperties.getVulnerabilityAssessmentDashboardVersion();
    public final static String ARTIFACT_ANALYSIS_VERSION = BuildProperties.getProjectVersion();
    public final static List> SECURITY_ADVISORY_DISPLAY_ORDERING = Arrays.asList(AdvisoryTypeStore.MSRC, AdvisoryTypeStore.GHSA, AdvisoryTypeStore.CERT_EU, AdvisoryTypeStore.CERT_SEI, AdvisoryTypeStore.CERT_FR);

    private final LazySupplier vulnerabilityQuery;
    private final LazySupplier cpeDictionary;
    private final LazySupplier msrcProductIndexQuery;

    @Setter
    private VulnerabilityAssessmentDashboardEnrichmentConfiguration configuration = new VulnerabilityAssessmentDashboardEnrichmentConfiguration();

    private final Random random = new Random();

    public VulnerabilityAssessmentDashboard(File baseMirrorDirectory) {
        this.vulnerabilityQuery = new LazySupplier<>(() -> new NvdCveIndexQuery(baseMirrorDirectory));
        this.cpeDictionary = new LazySupplier<>(() -> new NvdCpeApiIndexQuery(baseMirrorDirectory));
        this.msrcProductIndexQuery = new LazySupplier<>(() -> new MsrcProductIndexQuery(baseMirrorDirectory));
    }

    @Override
    protected void performEnrichment(Inventory inventory) {
        LOG.info("Parsing inventory data");
        final VulnerabilityContextInventory vInventory = VulnerabilityContextInventory.fromInventory(inventory);

        LOG.info("Baking effective CVSS vectors");
        vInventory.calculateEffectiveCvssVectorsForVulnerabilities(super.getSecurityPolicyConfiguration());
        LOG.info("Applying effective vulnerability status");
        vInventory.applyEffectiveVulnerabilityStatus(super.getSecurityPolicyConfiguration());

        LOG.info("Using calculated values for potential vulnerability filtering");
        final List effectiveVulnerabilities = VulnerabilityAssessmentDashboard.getEffectiveVulnerabilitiesAll(vInventory, configuration, super.getSecurityPolicyConfiguration());

        LOG.info("Initializing dashboard");
        final Dashboard dashboard = this.initializeDashboard(vInventory);

        LOG.info("Starting dashboard generation");
        final Map> vulnerabilitiesPerArtifact = Vulnerability.groupVulnerabilitiesByArtifact(effectiveVulnerabilities);
        final Map vulnerabilityDetails = this.determineVulnerabilityDetailLevels(effectiveVulnerabilities);

        this.setFaviconDependingOnVulnerabilityPresence(dashboard, !effectiveVulnerabilities.isEmpty());

        final VulnerabilityTimelineGeneratorResult vulnerabilityTimelines = generateApplicableVulnerabilityTimelines(effectiveVulnerabilities, vulnerabilityDetails);

        LOG.info("Processing vulnerabilities...");

        final Modal inventoryWarningsModal = createInventoryWarningsModal(inventory, vulnerabilityDetails.keySet());
        dashboard.addModal(inventoryWarningsModal);

        LOG.info("");
        for (Vulnerability vulnerability : effectiveVulnerabilities) {
            final VadDetailLevelConfiguration detailLevel = vulnerabilityDetails.get(vulnerability);

            super.executor.submit(() -> {
                LOG.debug("Processing vulnerability: [{}]", vulnerability.getId());
                try {
                    dashboard.addSheet(createVulnerabilitySheet(vulnerability, effectiveVulnerabilities, vulnerabilityTimelines, detailLevel));
                } catch (Exception e) {
                    LOG.error("Failed to create vulnerability sheet for vulnerability: {}", vulnerability.getId(), e);
                    throw new RuntimeException("Failed to create vulnerability sheet for vulnerability: " + vulnerability.getId(), e);
                }
            });
        }

        executor.setSize(1);
        executor.start();
        try {
            executor.join();
        } catch (InterruptedException e) {
            throw new RuntimeException("Failed to wait for all vulnerability sheets to be created", e);
        }

        dashboard.sortSheets(Comparator.comparing(sheet -> {
            final String id = sheet.getId();
            if (id.equalsIgnoreCase("inventory warnings")) {
                return "!!!inventory warnings";
            } else {
                return id;
            }
        }));


        LOG.info("Generating overview charts");
        final VulnerabilityOverviewChartGenerator overviewChartGenerator = new VulnerabilityOverviewChartGenerator(vInventory, super.getSecurityPolicyConfiguration(), effectiveVulnerabilities, vulnerabilitiesPerArtifact);
        final List overviewCharts = overviewChartGenerator.generateOverviewCharts();

        for (GeneratedChart overviewChart : overviewCharts) {
            overviewChart.writeSvgTo(configuration.getSvgDirectory());
        }

        dashboard.addModal(generateOverviewModal(overviewCharts));
        dashboard.addModal(generateAssessmentEditorModal());

        dashboard.addBottomLeftBadge(inventory.getArtifacts().size() + " artifact" + plural(inventory.getArtifacts().size()));
        dashboard.addBottomLeftBadge(effectiveVulnerabilities.size() + " vulnerabilitie" + plural(effectiveVulnerabilities.size()));

        final List priorityScores = effectiveVulnerabilities.stream().map(vulnerability -> vulnerability.calculatePriorityScore(securityPolicyConfiguration)).collect(Collectors.toList());
        final List priorityScoreRanges = priorityScores.stream().filter(VulnerabilityPriorityCalculator.PriorityScoreResult::isElevated).map(score -> securityPolicyConfiguration.getPriorityScoreSeverityRanges().getRange(score.getResultingScore())).collect(Collectors.toList());
        final CvssSeverityRanges.SeverityRange[] priorityScoreSeverityRanges = securityPolicyConfiguration.getPriorityScoreSeverityRanges().getRanges();
        for (int i = 0; i < priorityScoreSeverityRanges.length - 1; i++) {
            final CvssSeverityRanges.SeverityRange severityRange = priorityScoreSeverityRanges[i];
            dashboard.addBottomLeftBadge(span(priorityScoreRanges.stream().filter(range -> range == severityRange).count() + " " + severityRange.getName()), i == 0 ? "danger" : "warning");
        }

        dashboard.addBottomLeftBadge(vInventory.getSecurityAdvisories().size() + " advisorie" + plural(vInventory.getSecurityAdvisories().size()));
        {
            final int ignoredVulnerabilitiesCount = vInventory.getVulnerabilities().size() - effectiveVulnerabilities.size();
            if (ignoredVulnerabilitiesCount > 0) {
                dashboard.addBottomLeftBadge(ignoredVulnerabilitiesCount + " ignored");
            }
        }
        addAdditionalInventoryInformation(inventory, dashboard);

        // FIXME: implement missing behavior
        // checkForFailReasons(statistics);

        try {
            dashboard.generateIntoFile(configuration.getOutputDashboardFile());
            LOG.info("Wrote dashboard to file: {}", configuration.getOutputDashboardFile().getAbsolutePath());
        } catch (IOException e) {
            throw new RuntimeException("Failed to write dashboard to file: " + configuration.getOutputDashboardFile(), e);
        }
    }

    private VulnerabilityTimelineGeneratorResult generateApplicableVulnerabilityTimelines(List effectiveVulnerabilities, Map vulnerabilityDetails) {
        final VulnerabilityTimelineGeneratorResult vulnerabilityTimelines;

        if (configuration.isVulnerabilityTimelinesGlobalEnabled()) {
            LOG.info("Building vulnerability timelines...");
            final long startTime = System.currentTimeMillis();

            final NvdCveIndexQuery vulnerabilityQuery = this.vulnerabilityQuery.get();
            final NvdCpeApiIndexQuery cpeDictionary = this.cpeDictionary.get();

            final VulnerabilityTimelineGenerator vulnerabilityTimelineGenerator = new VulnerabilityTimelineGenerator(vulnerabilityQuery, cpeDictionary, configuration, super.getSecurityPolicyConfiguration());

            for (Vulnerability vulnerability : effectiveVulnerabilities) {
                if (!vulnerabilityDetails.get(vulnerability).isTimeline()) {
                    continue;
                }

                vulnerabilityTimelineGenerator.addRelevantVulnerability(vulnerability.getId());

                final Set> allVendorProducts = new HashSet<>();
                final Map>> vendorProductsPerArtifact = new HashMap<>();

                final List affectedCpes = VulnerableSoftwareTreeNode.getAllCpes(vulnerability.getVulnerableSoftwareConfigurations());
                findVendorProductsOnArtifactsForVulnerabilityTimeline(vulnerability.getAffectedArtifactsByDefaultKey(), affectedCpes, allVendorProducts, vendorProductsPerArtifact);

                for (Pair vp : allVendorProducts) {
                    vulnerabilityTimelineGenerator.addVendorProduct(vp.getLeft(), vp.getRight());
                }
            }


            vulnerabilityTimelines = vulnerabilityTimelineGenerator.generate(super.executor);
            LOG.info("Generated [{}] timelines in [{}]", vulnerabilityTimelines.getTimelines().size(), TimeUtils.formatTimeNoSuffixPrefix(System.currentTimeMillis() - startTime));

        } else {
            vulnerabilityTimelines = new VulnerabilityTimelineGeneratorResult();
        }
        return vulnerabilityTimelines;
    }

    private Modal createInventoryWarningsModal(Inventory inventory, Collection vulnerabilities) {
        final InventoryWarnings inventoryWarnings = new InventoryWarnings(inventory);

        final Modal modal = new Modal();
        modal.setId("Inventory Warnings");
        modal.setTitle("Inventory Warnings");
        modal.setToggleKey("KeyU");
        modal.setShowInSidebar(inventoryWarnings.hasData());
        modal.setSvgIcon(SvgIcon.EXCLAMATION_TRIANGLE_FILL);

        if (!inventoryWarnings.hasData()) {
            modal.with(p("No warnings found."));
            modal.setSize(Modal.Size.NORMAL);
            return modal;
        } else {
            modal.setSize(Modal.Size.ADJUST);
        }

        final List> artifactWarnings = inventoryWarnings.getArtifactWarnings();
        final List> vulnerabilityWarnings = inventoryWarnings.getVulnerabilityWarnings();
        final List sourcelessWarnings = inventoryWarnings.getSourcelessWarnings();

        final Map>> groupedArtifactWarnings = InventoryWarningEntry.groupBySource(artifactWarnings);
        final Map>> groupedVulnerabilityWarnings = InventoryWarningEntry.groupBySource(vulnerabilityWarnings);

        if (!artifactWarnings.isEmpty()) {
            final TableTag artifactWarningsTable = table().withClass("basic-table");
            artifactWarningsTable.with(thead(tr(th("Id"), th("Component"), th("Version"), th("Messages"))));
            final TbodyTag artifactWarningsTbody = tbody();
            artifactWarningsTable.with(artifactWarningsTbody);

            for (Map.Entry>> entry : groupedArtifactWarnings.entrySet()) {
                final Artifact artifact = entry.getKey();
                final List> warnings = entry.getValue();

                artifactWarningsTbody.with(tr()
                        .with(td(artifact.getId()))
                        .with(td(artifact.getComponent()))
                        .with(td(artifact.getVersion()))
                        .with(td(createInventoryWarningsEntry(warnings, vulnerabilities))));
            }

            modal.getEditableContent().with(h3("Artifact"), artifactWarningsTable, br());
        }

        if (!vulnerabilityWarnings.isEmpty()) {
            final TableTag vulnerabilityWarningsTable = table().withClass("basic-table");
            vulnerabilityWarningsTable.with(thead(tr(th("Name"), th("Messages"))));
            final TbodyTag vulnerabilityWarningsTbody = tbody();
            vulnerabilityWarningsTable.with(vulnerabilityWarningsTbody);

            for (Map.Entry>> entry : groupedVulnerabilityWarnings.entrySet()) {
                final VulnerabilityMetaData vulnerabilityMetaData = entry.getKey();
                final List> warnings = entry.getValue();

                final String name = vulnerabilityMetaData.get(VulnerabilityMetaData.Attribute.NAME);
                vulnerabilityWarningsTbody.with(tr()
                        .with(td(name).attr("onclick", "showModal('" + name + "')").withClass("clickable"))
                        .with(td(createInventoryWarningsEntry(warnings, vulnerabilities))));
            }

            modal.getEditableContent().with(h3("Vulnerability"), vulnerabilityWarningsTable, br());
        }

        if (!sourcelessWarnings.isEmpty()) {
            final TableTag sourcelessWarningsTable = table().withClass("basic-table");
            sourcelessWarningsTable.with(thead(tr(th("Messages"))));
            final TbodyTag sourcelessWarningsTbody = tbody();
            sourcelessWarningsTable.with(sourcelessWarningsTbody);

            for (String warning : sourcelessWarnings) {
                sourcelessWarningsTbody.with(tr().with(td(warning)));
            }

            modal.getEditableContent().with(h3("Sourceless"), sourcelessWarningsTable);
        }

        return modal;
    }


    private  SpanTag createInventoryWarningsEntry(List> warnings, Collection vulnerabilities) {
        final SpanTag spanTag = span();
        final Set knownWarnings = new HashSet<>();

        for (int i = 0; i < warnings.size(); i++) {
            final InventoryWarningEntry warning = warnings.get(i);
            final String rawWarningText = warning.getWarning(); // "Warning (references CVE-2020-11979, CVE-2020-1945)"

            final Pattern REFERENCES_PATTERN = Pattern.compile("(.*) \\(references (.*)\\)");
            final Matcher matcher = REFERENCES_PATTERN.matcher(rawWarningText);

            final String croppedStart; // "Warning"
            final List references = new ArrayList<>(); // "CVE-2020-11979", "CVE-2020-1945"
            if (matcher.matches()) {
                croppedStart = matcher.group(1);
                references.addAll(Arrays.asList(matcher.group(2).split(", ")));
            } else {
                croppedStart = rawWarningText;
            }

            final String warningText = croppedStart + (StringUtils.hasText(warning.getCreator()) ? " (" + warning.getCreator() + ") " : " ");
            if (!knownWarnings.add(warningText)) {
                continue;
            }

            spanTag.with(text(warningText));
            if (!references.isEmpty()) {
                for (String reference : references) {
                    final String badge = makeVulnerabilityReferenceBadge(reference, vulnerabilities);
                    spanTag.with(rawHtml(badge));
                }
            }
            if (i < warnings.size() - 1) {
                spanTag.with(br());
            }
        }

        return spanTag;
    }

    private void applyInventoryInfoDashboardCustomization(Dashboard dashboard, VulnerabilityContextInventory vInventory) {
        final InventoryInfo vadCustomization = vInventory.getInventory().findInventoryInfo("vad-customization");

        if (vadCustomization != null) {
            if (StringUtils.hasText(vadCustomization.get("Title"))) {
                dashboard.setTitle(vadCustomization.get("Title"));
            }
            if (StringUtils.hasText(vadCustomization.get("Subtitle"))) {
                dashboard.setSubtitle(vadCustomization.get("Subtitle"));
            }

            if (StringUtils.hasText(vadCustomization.get("Footer"))) {
                dashboard.addBottomLeftBadge(vadCustomization.get("Footer"));
            }
        }
    }

    private Dashboard initializeDashboard(VulnerabilityContextInventory vInventory) {
        final Dashboard dashboard = new Dashboard();

        dashboard.setTitle("Vulnerability Assessment Dashboard");
        dashboard.setDescription("An interactive HTML dashboard that allows for the assessment of vulnerability data.");
        dashboard.setAuthor("Powered by {metæffekt}");
        dashboard.setAuthorUrl("https://www.metaeffekt.com");
        dashboard.setVersion(ARTIFACT_ANALYSIS_VERSION + " " + VERSION);
        dashboard.setContentSheetEntryNames("Vulnerability", "Vulnerabilities");

        this.applyInventoryInfoDashboardCustomization(dashboard, vInventory);

        dashboard.setChartJsEnabled(true);

        if (vInventory.getInventory().getArtifacts().isEmpty()) {
            dashboard.setEmptyDashboardText("The source inventory does not contain any components.");
            dashboard.setEmptyDashboardTextSize(250);
            dashboard.setEmptyDashboardIcon("\n" +
                    "  " +
                    ""); // ban
        }

        try {
            final List nvdLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/nvd.txt");
            dashboard.addLicenseText("NVD", String.join("\n", nvdLicense),
                    d -> true);

            final List msrcLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/msrc.txt");
            dashboard.addLicenseText("MSRC", String.join("\n", msrcLicense),
                    d -> d.getSheetParagraphTitles().contains("Microsoft Security Upgrade Guide"));

            final List ghsaLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/ghsa.txt");
            dashboard.addLicenseText("GitHub Advisory Database", String.join("\n", ghsaLicense),
                    d -> d.getSheetParagraphTitles().contains("GitHub Security Advisory"));

            final List eolLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/eol-date.txt");
            dashboard.addLicenseText("EOL", String.join("\n", eolLicense),
                    d -> d.getSheetParagraphTitles().contains("EOL"));

            final List certFrLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/cert-fr.txt");
            dashboard.addLicenseText("CERT-FR", String.join("\n", certFrLicense),
                    d -> d.getSheetParagraphTitles().contains("CERT-FR"));

            final List certEuLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/cert-eu.txt");
            dashboard.addLicenseText("CERT-EU", String.join("\n", certEuLicense),
                    d -> d.getSheetParagraphTitles().contains("CERT-EU"));

            final List certSeiLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/cert-sei.txt");
            dashboard.addLicenseText("CERT-SEI", String.join("\n", certSeiLicense),
                    d -> d.getSheetParagraphTitles().contains("CERT-SEI"));

            final List metaeffektLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/metaeffekt.txt");
            dashboard.addLicenseText("{metæffekt}", String.join("\n", metaeffektLicense),
                    d -> true);

            final List epssLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/epss.txt");
            dashboard.addLicenseText("EPSS", String.join("\n", epssLicense),
                    // FIXME: how can we dynamically determine whether EPSS is used in the dashboard
                    d -> true);

            final List kevLicense = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/licenses/kev.txt");
            dashboard.addLicenseText("KEV", String.join("\n", kevLicense),
                    // FIXME: how can we dynamically determine whether KEV is used in the dashboard
                    d -> true);

        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        // add filter templates
        for (CvssSeverityRanges.SeverityRange range : super.getSecurityPolicyConfiguration().getCvssSeverityRanges().getRanges()) {
            if (range.getName().equals("None")) {
                continue;
            }
            Dashboard.NavigationFilter lowerBound = Dashboard.navigationFilterBuilder()
                    .column(NavigationHeaders.NAVIGATION_CVSS_UNMODIFIED_OVERALL.getTitle().replace("
", "")) .operation(Dashboard.FilterOperation.LARGER_EQUALS) .value(range.getFloor()) .build(); Dashboard.NavigationFilter upperBound = Dashboard.navigationFilterBuilder() .column(NavigationHeaders.NAVIGATION_CVSS_UNMODIFIED_OVERALL.getTitle().replace("
", "")) .operation(Dashboard.FilterOperation.SMALLER_EQUALS) .value(range.getCeil()) .build(); dashboard.addNavigationFilterTemplates(range.getName(), lowerBound, upperBound); } dashboard.addNavigationFilterTemplates("Void", Dashboard.navigationFilterBuilder() .column(NavigationHeaders.NAVIGATION_STATUS.getTitle()) .operation(Dashboard.FilterOperation.EQUALS) .value("void") .build() ); dashboard.addNavigationFilterTemplates("Not Void", Dashboard.navigationFilterBuilder() .column(NavigationHeaders.NAVIGATION_STATUS.getTitle()) .operation(Dashboard.FilterOperation.NOT_EQUALS) .value("void") .build() ); dashboard.addNavigationFilterTemplates("Not Insignificant", Dashboard.navigationFilterBuilder() .column(NavigationHeaders.NAVIGATION_STATUS.getTitle()) .operation(Dashboard.FilterOperation.NOT_EQUALS) .value("insignificant") .build() ); dashboard.addNavigationFilterTemplates("In Review", Dashboard.navigationFilterBuilder() .column(NavigationHeaders.NAVIGATION_STATUS.getTitle()) .operation(Dashboard.FilterOperation.EQUALS) .value("in review") .build() ); dashboard.addNavigationFilterTemplates("Not In Review", Dashboard.navigationFilterBuilder() .column(NavigationHeaders.NAVIGATION_STATUS.getTitle()) .operation(Dashboard.FilterOperation.NOT_EQUALS) .value("in review") .build() ); dashboard.addNavigationFilterTemplates("Known exploited", Dashboard.navigationFilterBuilder() .column(NavigationHeaders.NAVIGATION_KEV_ENTRY.getTitle()) .operation(Dashboard.FilterOperation.NOT_EQUALS) .value("N/A") .build()); // add scripts that update the overview charts when a filter has been applied final List onFilterScript; try { onFilterScript = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/dashboard-vad-onfilter.js"); } catch (IOException e) { throw new RuntimeException("Unable to load resource [dashboard/dashboard-vad-onfilter.js]", e); } final StringBuilder prependOnFilterApplied = new StringBuilder(); prependOnFilterApplied.append("let cvssRanges= ["); StringJoiner rangesArray = new StringJoiner(","); for (CvssSeverityRanges.SeverityRange range : super.getSecurityPolicyConfiguration().getCvssSeverityRanges().getRanges()) { rangesArray.add("{name:'" + range.getName() + "',color:'" + range.getColor().toHex() + "',floor:" + range.getFloor() + ",ceil:" + range.getCeil() + "}"); } prependOnFilterApplied.append(rangesArray).append("];"); prependOnFilterApplied.append("let statuses = ").append(CentralSecurityPolicyConfiguration.VULNERABILITY_STATUS_DISPLAY_MAPPER_UNMODIFIED.getStatusNames().stream().collect(Collectors.joining("','", "['", "'];"))); prependOnFilterApplied.append(CentralSecurityPolicyConfiguration.VULNERABILITY_STATUS_DISPLAY_MAPPER_UNMODIFIED.getJsMappingFunction("reviewStateMappingFunction")).append(";"); onFilterScript.add(0, prependOnFilterApplied.toString()); dashboard.addOnFilterApplied(String.join("", onFilterScript)); // add more chart related methods try { dashboard.addAdditionalContent(script(String.join("", Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/dashboard-vad-scripts.js")))); } catch (IOException e) { throw new RuntimeException("Unable to load resource [dashboard/dashboard-vad-scripts.js]", e); } // add theme change listener final List onThemeChangeScript; try { onThemeChangeScript = Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/dashboard-vad-onthemechange.js"); dashboard.addOnThemeChange(String.join("", onThemeChangeScript)); } catch (IOException e) { throw new RuntimeException("Unable to load resource [dashboard/dashboard-vad-onthemechange.js]", e); } // add some custom css try { dashboard.addAdditionalContent(style(String.join("", Dashboard.readResourceAsStringList(Dashboard.class, "dashboard/dashboard-vad-style.css")))); } catch (IOException e) { throw new RuntimeException("Unable to load resource [dashboard/dashboard-vad-style.css]", e); } // prepare the navigation columns final NavigationCellStyle statusStyling = (cellData, additionalInformation) -> { final String generalStyling = "text-align:center;"; switch (cellData) { case VulnerabilityMetaData.STATUS_VALUE_INSIGNIFICANT: return generalStyling + "color: var(--pastel-gray);"; case VulnerabilityMetaData.STATUS_VALUE_APPLICABLE: return generalStyling + "color: var(--strong-dark-blue);"; case "incomplete": return generalStyling + "color: var(--strong-red);"; case VulnerabilityMetaData.STATUS_VALUE_NOTAPPLICABLE: return generalStyling + "color: var(--strong-light-green);"; case VulnerabilityMetaData.STATUS_VALUE_VOID: return generalStyling + "color: var(--strong-gray);"; default: return generalStyling + "color: var(--pastel-blue);"; } }; final NavigationCellStyle advisoriesCounterStyling = (cellData, additionalInformation) -> { final String generalStyling = "text-align:center;"; final String[] split = cellData.replaceAll("[()]", "").split(" ", 2); if ((split.length == 2 && split[0].equals(split[1]) || (split.length == 1 && split[0].equals("0")))) { return generalStyling + "color: var(--strong-light-green);"; } else if (split.length == 2) { return generalStyling + "color: var(--strong-yellow);"; } else { return generalStyling + "color: var(--strong-dark-orange);"; } }; final NavigationCellStyle eolStyling = (cellData, additionalInformation) -> { final String generalStyling = "text-align:center;"; switch (cellData) { case "supported": return generalStyling + "color: var(--strong-light-green);"; case "ending support": return generalStyling + "color: var(--strong-yellow);"; case "no support": return generalStyling + "color: var(--strong-red);"; case "extended support": return generalStyling + "color: var(--strong-yellow);"; default: return generalStyling + "color: var(--strong-dark-orange);"; } }; final NavigationCellStyle matchedViaStringStyling = (cellData, additionalInformation) -> { if (cellData.equals("N/A")) { return "color: var(--strong-dark-orange);"; } else { return null; } }; final NavigationCellStyle simpleCenteredTextStyling = (cellData, additionalInformation) -> "text-align:center;"; dashboard.addNavigationColumnHeader("Name", null, NavigationColumnHeaderConfig.Alignment.START); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_CVSS_UNMODIFIED_OVERALL.getTitle(), cvssScoringStyle, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_CVSS_MODIFIED_OVERALL.getTitle(), cvssScoringStyle, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_CVSS_BASE.getTitle(), cvssScoringStyle, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_CVSS_EXPLOITABILITY.getTitle(), cvssScoringStyle, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_CVSS_IMPACT.getTitle(), cvssScoringStyle, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_EPSS_SCORE.getTitle(), epssScoreStyle, NavigationColumnHeaderConfig.Alignment.CENTER, true); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_PRIORITY_SCORE.getTitle(), cvssScoringStyle, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_STATUS.getTitle(), statusStyling, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_PRIORITY_SCORE_LABEL.getTitle(), priorityLabelStyle, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_AMOUNT_ADVISORIES_REVIEWED.getTitle(), advisoriesCounterStyling, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_KEV_ENTRY.getTitle(), simpleCenteredTextStyling, NavigationColumnHeaderConfig.Alignment.CENTER, true); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_AMOUNT_ARTIFACTS.getTitle(), simpleCenteredTextStyling, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.EOL_STATE.getTitle(), eolStyling, NavigationColumnHeaderConfig.Alignment.CENTER, true); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_VERSION.getTitle(), null, NavigationColumnHeaderConfig.Alignment.START); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_COMPONENTS.getTitle(), null, NavigationColumnHeaderConfig.Alignment.START); dashboard.addNavigationColumnHeader(NavigationHeaders.CORRELATION_DISTANCE.getTitle(), simpleCenteredTextStyling, NavigationColumnHeaderConfig.Alignment.CENTER); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_USED_MATCHING_INFORMATION.getTitle(), matchedViaStringStyling, NavigationColumnHeaderConfig.Alignment.START); dashboard.addNavigationColumnHeader(NavigationHeaders.NAVIGATION_UNUSED_MATCHING_INFORMATION.getTitle(), matchedViaStringStyling, NavigationColumnHeaderConfig.Alignment.START); return dashboard; } public static List getEffectiveVulnerabilitiesAll(VulnerabilityContextInventory vInventory, VulnerabilityAssessmentDashboardEnrichmentConfiguration vadConfig, CentralSecurityPolicyConfiguration securityPolicy ) { final List vulnerabilities = vInventory.getSortedVulnerabilities(); final FilterAttribute filterAttribute = vadConfig.getVulnerabilityIncludeFilterAttribute(); final int sizeBefore = vulnerabilities.size(); final List filtered = vulnerabilities.stream() .filter(vulnerability -> { if (vulnerability.hasTag("marker")) { return true; } else if (filterAttribute != null && !filterAttribute.matches(vulnerability)) { return false; } else if (!vInventory.isVulnerabilityIncludedRegardingAdvisoryProviders(securityPolicy, vulnerability)) { return false; } else if (!vInventory.isVulnerabilityAboveIncludeScoreThreshold(securityPolicy, vulnerability)) { return false; } else if (!vInventory.isVulnerabilityIncludedRegardingAdvisoryReviewStatus(securityPolicy, vulnerability)) { return false; } // also apply new filtering steps to method below return true; }) .limit(vadConfig.getMaximumVulnerabilitiesPerDashboardCount()) .collect(Collectors.toList()); if (sizeBefore != filtered.size()) { LOG.info("Ignoring [{}] vulnerabilities for dashboard generation based on configured filters", sizeBefore - filtered.size()); } return filtered; } public static List getEffectiveVulnerabilitiesAll(VulnerabilityContextInventory vInventory, CentralSecurityPolicyConfiguration securityPolicy ) { final List vulnerabilities = vInventory.getSortedVulnerabilities(); final int sizeBefore = vulnerabilities.size(); final List filtered = vulnerabilities.stream() .filter(vulnerability -> { if (vulnerability.hasTag("marker")) { return true; } else if (!vInventory.isVulnerabilityIncludedRegardingAdvisoryProviders(securityPolicy, vulnerability)) { return false; } else if (!vInventory.isVulnerabilityAboveIncludeScoreThreshold(securityPolicy, vulnerability)) { return false; } else if (!vInventory.isVulnerabilityIncludedRegardingAdvisoryReviewStatus(securityPolicy, vulnerability)) { return false; } // also apply new filtering steps to method above return true; }) .collect(Collectors.toList()); if (sizeBefore != filtered.size()) { LOG.info("Ignoring [{}] vulnerabilities for dashboard generation based on configured filters", sizeBefore - filtered.size()); } return filtered; } private Map determineVulnerabilityDetailLevels(List vulnerabilities) { final Map vulnerabilityDetailLevels = new HashMap<>(); for (Vulnerability vulnerability : vulnerabilities) { final Set vulnerabilityArtifacts = vulnerability.getAffectedArtifactsByDefaultKey(); final List levels = configuration.getDetailLevels(vulnerability, vulnerability.getVulnerabilityStatus(), vulnerabilityArtifacts); final List artifactLevels = VadDetailLevelConfiguration.fromArtifacts(vulnerabilityArtifacts); artifactLevels.stream() .filter(detailLevel -> detailLevel.getMatcher().matches(vulnerability, vulnerability.getVulnerabilityStatus(), vulnerabilityArtifacts)) .forEach(levels::add); vulnerabilityDetailLevels.put(vulnerability, VadDetailLevelConfiguration.computeEffective(levels)); } return vulnerabilityDetailLevels; } private List determineEffectiveVulnerabilitiesWithoutIgnored(VulnerabilityContextInventory inventory, Set ignoredVulnerabilities) { return inventory.getVulnerabilities().stream() .filter(v -> !ignoredVulnerabilities.contains(v)) .sorted() .collect(Collectors.toList()); } private void setFaviconDependingOnVulnerabilityPresence(Dashboard dashboard, boolean containsVulnerabilities) { if (containsVulnerabilities) { dashboard.setFavicon(link().withRel("icon") .withHref("data:image/svg+xml," + "" + "")); } else { dashboard.setFavicon(link().withRel("icon") .withHref("data:image/svg+xml," + "" + "")); } } private Sheet createVulnerabilitySheet(Vulnerability vulnerability, Collection vulnerabilities, VulnerabilityTimelineGeneratorResult vulnerabilityTimelines, VadDetailLevelConfiguration detailLevel) { final Sheet vulnerabilitySheet = new Sheet(); final Set affectedArtifacts = vulnerability.getAffectedArtifactsByDefaultKey(); vulnerabilitySheet.setId(vulnerability.getId()); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_VULNERABILITY_NAME.getTitle(), vulnerability.getId()); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_AMOUNT_ARTIFACTS.getTitle(), affectedArtifacts.size()); final List artifactVersions = affectedArtifacts.stream() .map(Artifact::getVersion) .filter(Objects::nonNull) .distinct() .sorted(VersionComparator.INSTANCE) .collect(Collectors.toList()); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_VERSION.getTitle(), croppedNavigationTableContents(artifactVersions, 35)); final List componentNames = affectedArtifacts.stream() .map(Artifact::getComponent) .filter(Objects::nonNull) .distinct() .sorted() .collect(Collectors.toList()); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_COMPONENTS.getTitle(), croppedNavigationTableContents(componentNames, 35)); final List filteredSecurityAdvisories = vulnerability.getSecurityAdvisories().stream() .filter(advisor -> super.getSecurityPolicyConfiguration().isSecurityAdvisoryIncludedRegardingEntrySourceType(advisor.getType())) .filter(advisor -> super.getSecurityPolicyConfiguration().isSecurityAdvisoryIncludedRegardingEntryProvider(advisor.getSourceIdentifier().name())) .filter(advisor -> detailLevel.isAdvisoryTypeEnabled(advisor.getType())) .filter(advisor -> detailLevel.isAdvisoryProviderEnabled(advisor.getSourceIdentifier())) .sorted(Comparator.comparingInt(advisor -> SECURITY_ADVISORY_DISPLAY_ORDERING.indexOf(advisor.getSourceIdentifier()))) .collect(Collectors.toList()); final Map, List> sortedBySourceSecurityAdvisories = new LinkedHashMap<>(); final Map, AtomicInteger> amountReviewedAdvisories = new HashMap<>(); for (AdvisoryEntry securityAdvisory : filteredSecurityAdvisories) { sortedBySourceSecurityAdvisories.computeIfAbsent(securityAdvisory.getSourceIdentifier(), k -> new ArrayList<>()).add(securityAdvisory); amountReviewedAdvisories.computeIfAbsent(securityAdvisory.getSourceIdentifier(), k -> new AtomicInteger(0)); } // only used for quick access to details type for adding extra data aside the advisory sheets final boolean hasCustomTitle = vulnerability.getAdditionalAttribute(InventoryAttribute.STATUS_TITLE) != null; final boolean isCve = vulnerability.getId().startsWith("CVE-"); final boolean isMsrcAdvisor = vulnerability.getId().startsWith("ADV"); final boolean isMarkerVulnerability = vulnerability.hasTag("marker"); final boolean hasMicrosoftAdvisory = sortedBySourceSecurityAdvisories.containsKey(AdvisoryTypeStore.MSRC); // generate the sheet { final H1Tag mainSheetHeader = h1().withStyle("margin: -8px 0px 0px 0px; display: inline;"); mainSheetHeader .with(span(vulnerability.getId()).withClass("copy-to-clipboard-hover").attr("tooltip", "Click to copy to clipboard")) .with(script("document.querySelectorAll('.copy-to-clipboard-hover').forEach(function(element) { element.addEventListener('click', function() { copyToClipboard(element.innerText); element.classList.add('copied'); setTimeout(function() { element.classList.remove('copied'); }, 1000); }); });")) .with(text(" ")); if (isCve) { mainSheetHeader.with( this.makeHTMLLinkWithStyle("[NVD]", vulnerability.getUrl(), HrefTargets.TARGET_NVD.getTarget(), "font-size: 23px;"), text(" "), this.makeHTMLLinkWithStyle("[CVE Details]", "https://www.cvedetails.com/cve/" + vulnerability.getId(), HrefTargets.TARGET_CVEDETAILS.getTarget(), "font-size: 23px;"), text(" "), this.makeHTMLLinkWithStyle("[Mitre]", "https://www.cve.org/CVERecord?id=" + vulnerability.getId(), HrefTargets.TARGET_MITRE.getTarget(), "font-size: 23px;"), text(" ") ); } else if (vulnerability.getUrl() != null) { final String joinedSources = vulnerability.getDataSources().stream().map(ContentIdentifierStore.ContentIdentifier::getWellFormedName).collect(Collectors.joining(", ", "(", ")")); mainSheetHeader.with( this.makeHTMLLink(joinedSources.length() >= 3 ? "[URL]" : joinedSources, vulnerability.getUrl(), HrefTargets.TARGET_NVD.getTarget()), text(" ") ); } if (!isMsrcAdvisor && hasMicrosoftAdvisory) { mainSheetHeader.with( this.makeHTMLLinkWithStyle("[MSRC]", "https://msrc.microsoft.com/update-guide/en-US/vulnerability/" + vulnerability.getId(), HrefTargets.TARGET_MICROSOFT.getTarget(), "font-size: 23px;") ); } final SpanTag sheetHeader = span() .with(mainSheetHeader); if (hasCustomTitle) { sheetHeader.with(h2(vulnerability.getAdditionalAttribute(InventoryAttribute.STATUS_TITLE))); } vulnerabilitySheet.setTitle(sheetHeader); } // generate sheet paragraphs { vulnerabilitySheet.addParagraph(createParagraphDescription(vulnerability)); // create and add the paragraph that contains the artifact table and the product timelines if (!isMarkerVulnerability) { vulnerabilitySheet.addParagraph(createParagraphArtifactsCpe(vulnerability, vulnerabilitySheet)); if (!vulnerabilityTimelines.getTimelines().isEmpty()) { vulnerabilitySheet.addParagraph(createParagraphTimelines(vulnerability, vulnerabilityTimelines, detailLevel)); } } // build the status object from the VMD and use it to create the assessment paragraph if (!isMarkerVulnerability) { vulnerabilitySheet.addParagraph(createParagraphAssessment(vulnerability, vulnerabilitySheet)); } if (!isMarkerVulnerability) { vulnerabilitySheet.addParagraph(new VadPriorityScoreSection().createParagraphPriority(this, vulnerability, vulnerabilitySheet)); } // add a section for the eol-date information if (!isMarkerVulnerability && detailLevel.isEolDate()) { createParagraphEolDate(affectedArtifacts, vulnerabilitySheet).ifPresent(vulnerabilitySheet::addParagraph); } // list cwes (weaknesses) if (vulnerability.getCwes() != null && !vulnerability.getCwes().isEmpty()) { vulnerabilitySheet.addParagraph(createParagraphCwe(vulnerability)); } for (Map.Entry, List> advisoriesByType : sortedBySourceSecurityAdvisories.entrySet()) { final AdvisoryTypeIdentifier advisoryType = advisoriesByType.getKey(); final List advisories = advisoriesByType.getValue(); final AtomicInteger amountReviewed = amountReviewedAdvisories.get(advisoryType); if (advisoryType == AdvisoryTypeStore.MSRC) { vulnerabilitySheet.addParagraph(createParagraphMicrosoftVulnerabilityInformation(vulnerability, filteredSecurityAdvisories, amountReviewed, detailLevel)); } else if (advisoryType == AdvisoryTypeStore.GHSA) { vulnerabilitySheet.addParagraph(createParagraphGhsa(vulnerability, advisories, vulnerabilities, amountReviewed, detailLevel)); } else if (advisoryType == AdvisoryTypeStore.CERT_SEI) { vulnerabilitySheet.addParagraph(createParagraphCertSei(vulnerability, advisories, vulnerabilities, amountReviewed, detailLevel)); } else if (advisoryType == AdvisoryTypeStore.CERT_FR) { vulnerabilitySheet.addParagraph(createParagraphCertFr(vulnerability, advisories, vulnerabilities, amountReviewed, detailLevel)); } else if (advisoryType == AdvisoryTypeStore.CERT_EU) { vulnerabilitySheet.addParagraph(createParagraphCertEu(vulnerability, advisories, vulnerabilities, amountReviewed, detailLevel)); } else { LOG.warn("No implementation for advisory source [{} / {}] has been found, treating as generic advisory source (has [{}] advisories)", advisoryType, advisoryType.getWellFormedName(), advisories.size()); vulnerabilitySheet.addParagraph(createParagraphGenericAdvisory(vulnerability, advisories, vulnerabilities, amountReviewed, detailLevel)); } } // add the amount of reviewed cert entries to the navigation bar final int totalAdvisorEntries = filteredSecurityAdvisories.size(); final int reviewed = amountReviewedAdvisories.values().stream().mapToInt(AtomicInteger::get).sum(); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_AMOUNT_ADVISORIES_REVIEWED.getTitle(), totalAdvisorEntries + (reviewed > 0 ? " (" + reviewed + ")" : "")); // list references if enabled on detail level final Set references = vulnerability.getReferences(); if (!references.isEmpty() && detailLevel.isReferences()) { vulnerabilitySheet.addParagraph(createParagraphReferences(references)); } } return vulnerabilitySheet; } /** * Creates a {@link SpanTag} containing a comma-separated list of items, cropped to a maximum length. * If the concatenated string exceeds the maximum character limit, it appends a count of omitted items. * Additionally, if not all items are included in the displayed text, a tooltip with all items is added. * * @param items A list of string items to be included in the navigation table. This list should be * pre-sorted and filtered as necessary before being passed to this method. * @param maxCharactersOnField The maximum number of characters the display content can have. If the * total length of the concatenated items exceeds this limit, the method * will append ", +N" to the display content, where N is the number of * omitted items. * @return A {@link SpanTag} object containing the cropped list of items as its display content. If * not all items are displayed, the {@link SpanTag} will also contain a tooltip attribute * with all items. */ public static SpanTag croppedNavigationTableContents(List items, final int maxCharactersOnField) { final StringBuilder reducedDisplayContent = new StringBuilder(); int addedComponents = 0; for (String item : items) { final int currentLength = reducedDisplayContent.length(); if (currentLength != 0 && currentLength + item.length() + 2 > maxCharactersOnField) { reducedDisplayContent.append(", +").append(items.size() - items.indexOf(item)); break; } if (currentLength > 0) { reducedDisplayContent.append(", "); } addedComponents++; reducedDisplayContent.append(item); } final SpanTag displayContent = span(reducedDisplayContent.toString()); if (addedComponents != items.size()) { displayContent.attr("tooltip", String.join(", ", items)); } return displayContent; } /* -- START: SHEET PARAGRAPH GENERATION -- */ private SheetParagraph createParagraphDescription(Vulnerability vulnerability) { final SheetParagraph descriptionParagraph = new SheetParagraph(); descriptionParagraph.setIdentifier("Description"); if (vulnerability.getDescription() != null) { descriptionParagraph.with(vulnerability.getDescription()); } return descriptionParagraph; } private SheetParagraph createParagraphArtifactsCpe(Vulnerability vulnerability, Sheet vulnerabilitySheet) { final SheetParagraph artifactsCpeParagraph = new SheetParagraph(); artifactsCpeParagraph.setIdentifier("Artifacts & CPE"); artifactsCpeParagraph.setTitle("Artifacts & CPE"); final Set vulnerabilityAffectedArtifacts = vulnerability.getAffectedArtifactsByDefaultKey(); final Set> coveredCpes = new HashSet<>(); if (vulnerability.getMatchingSources().isEmpty()) { vulnerabilitySheet.addNavigationEntry(NavigationHeaders.CORRELATION_DISTANCE.getTitle(), 0); } else { final TableBuilder matchingSourcesTable = new TableBuilder("basic-table"); final Map> matchingCriteria = new LinkedHashMap<>(); final Artifact emptyArtifact = new Artifact(); final Set allMsProductIds = new HashSet<>(); for (DataSourceIndicator matchingSource : vulnerability.getMatchingSources()) { final DataSourceIndicator.Reason matchReason = matchingSource.getMatchReason(); final Artifact effectiveFoundArtifact; if (matchReason instanceof DataSourceIndicator.ArtifactReason) { final DataSourceIndicator.ArtifactReason artifactReason = ((DataSourceIndicator.ArtifactReason) matchReason); final Artifact foundArtifact = artifactReason.findArtifact(vulnerabilityAffectedArtifacts); if (foundArtifact == null) { effectiveFoundArtifact = new Artifact(); effectiveFoundArtifact.setId(artifactReason.getArtifactId()); effectiveFoundArtifact.setComponent(artifactReason.getArtifactComponent()); effectiveFoundArtifact.setVersion(artifactReason.getArtifactVersion()); } else { effectiveFoundArtifact = foundArtifact; } } else { effectiveFoundArtifact = emptyArtifact; } matchingCriteria.computeIfAbsent(effectiveFoundArtifact, k -> new ArrayList<>()).add(matchingSource); } double correlationDistance = 0; final Set navigationTableMatchingSources = new HashSet<>(); final Set allNavigationTableMatchingSources = new HashSet<>(); for (Map.Entry> artifactCriteria : matchingCriteria.entrySet()) { final Artifact artifact = artifactCriteria.getKey(); final List matchingSources = artifactCriteria.getValue(); final Map row = matchingSourcesTable.createRow(); row.put(Artifact.Attribute.ID.getKey(), createArtifactTableEntryForOptionallyEmpty(artifact.getId())); row.put(Artifact.Attribute.COMPONENT.getKey(), createArtifactTableEntryForOptionallyEmpty(artifact.getComponent())); row.put(Artifact.Attribute.VERSION.getKey(), createArtifactTableEntryForOptionallyEmpty(artifact.getVersion())); if (artifact.get(InventoryAttribute.EOL_ID.getKey()) != null) { row.put("EOL Id", a(artifact.get(InventoryAttribute.EOL_ID.getKey())).withHref("https://endoflife.date/" + artifact.get(InventoryAttribute.EOL_ID.getKey())).withTarget(HrefTargets.TARGET_EOL_DATE.getTarget())); } if (artifact.getUrl() != null) { row.put("URL", a(artifact.getUrl()).withTarget("_blank").withHref(artifact.getUrl())); } if (artifact.get(Constants.KEY_ORGANIZATION) != null) { row.put(Constants.KEY_ORGANIZATION, text(artifact.get(Constants.KEY_ORGANIZATION))); } final String dtCveFindings = artifact.get(InventoryAttribute.DT_CVE_FINDINGS.getKey()); if (dtCveFindings != null && !dtCveFindings.isEmpty()) { if (Arrays.asList(dtCveFindings.split(", ?")).contains(vulnerability.getId())) { row.put("Is DT Finding", text("Yes")); } else { row.put("Is DT Finding", text("No")); } } if (artifact.get(InventoryAttribute.MS_PRODUCT_ID.getKey()) != null) { row.put("MS Product ID", text(artifact.get(InventoryAttribute.MS_PRODUCT_ID.getKey()))); allNavigationTableMatchingSources.add("MS " + artifact.get(InventoryAttribute.MS_PRODUCT_ID.getKey())); } if (artifact.get(InventoryAttribute.PURL.getKey()) != null) { row.put(Artifact.Attribute.PURL.getKey(), text(artifact.get(InventoryAttribute.PURL.getKey()))); } if (!artifact.getProjects().isEmpty()) { row.put("Projects", text(String.join(", ", artifact.getProjects()))); } // matching criteria section final SpanTag matchingCriteriaContainer = span(); for (DataSourceIndicator matchingSource : matchingSources) { final DataSourceIndicator.Reason matchReason = matchingSource.getMatchReason(); final ContentIdentifierStore.ContentIdentifier provider = matchingSource.getDataSource(); final DivTag matchingCriteriaEntry = div().withClass("matching-criteria-badge"); final String overwriteSource = matchReason.overwriteSource(); if (overwriteSource == null) { matchingCriteriaEntry.with(span(provider.getWellFormedName())); } else { matchingCriteriaEntry.with(span(overwriteSource)); } if (matchReason instanceof DataSourceIndicator.ArtifactCpeReason) { final DataSourceIndicator.ArtifactCpeReason artifactCpeReason = ((DataSourceIndicator.ArtifactCpeReason) matchReason); final String affectedConfiguration = artifactCpeReason.getConfiguration(); final Cpe cpe = CommonEnumerationUtil.parseCpe(artifactCpeReason.getCpe()).orElse(null); if (cpe != null) { matchingCriteriaEntry.with(span(StringUtils.hasText(affectedConfiguration) ? affectedConfiguration : CommonEnumerationUtil.toCpe22UriOrFallbackToCpe23FS(cpe))); navigationTableMatchingSources.add(cpe.getVendor() + " / " + cpe.getProduct()); coveredCpes.add(Triple.of(cpe.getPart(), cpe.getVendor(), cpe.getProduct())); } else { matchingCriteriaEntry.with(span("un-parseable CPE: " + artifactCpeReason.getCpe())); navigationTableMatchingSources.add(artifactCpeReason.getCpe()); } } else if (matchReason instanceof DataSourceIndicator.MsrcProductReason) { final DataSourceIndicator.MsrcProductReason msrcProductReason = ((DataSourceIndicator.MsrcProductReason) matchReason); matchingCriteriaEntry.with(span("product " + msrcProductReason.getMsrcProductId())); navigationTableMatchingSources.add("MS " + msrcProductReason.getMsrcProductId()); allMsProductIds.add(msrcProductReason.getMsrcProductId()); } else if (matchReason instanceof DataSourceIndicator.AssessmentStatusReason) { final DataSourceIndicator.AssessmentStatusReason assessmentStatusReason = ((DataSourceIndicator.AssessmentStatusReason) matchReason); matchingCriteriaEntry.with(span(assessmentStatusReason.getOriginFileName())); } else if (matchReason instanceof DataSourceIndicator.ArtifactGhsaReason) { final DataSourceIndicator.ArtifactGhsaReason artifactGhsaReason = ((DataSourceIndicator.ArtifactGhsaReason) matchReason); final String coordinates = artifactGhsaReason.getCoordinates(); if (StringUtils.hasText(coordinates)) { navigationTableMatchingSources.add(coordinates.replaceAll(" [(\\[][^(\\[)\\]]+,[^(\\[)\\]]+[)\\]]$", "")); matchingCriteriaEntry.with(span(coordinates)); } } else if (matchReason instanceof DataSourceIndicator.AnyArtifactOverwriteSourceReason) { matchingCriteriaEntry.with(span(artifact.getId())); if (overwriteSource != null) { navigationTableMatchingSources.add(artifact.getId() + " (" + matchReason.overwriteSource() + ")"); } else { navigationTableMatchingSources.add(artifact.getId()); } } else if (matchReason instanceof DataSourceIndicator.AnyArtifactReason) { matchingCriteriaEntry.with(span(artifact.getId())); } else if (matchReason instanceof DataSourceIndicator.AnyReason) { final DataSourceIndicator.AnyReason anyReason = ((DataSourceIndicator.AnyReason) matchReason); matchingCriteriaEntry.with(span(anyReason.getDescription())); } else { matchingCriteriaEntry.with(span(matchReason.toString())); navigationTableMatchingSources.add(matchReason.toString()); } matchingCriteriaContainer.with(matchingCriteriaEntry); } final Set unContainedCpes = new HashSet<>(); final List allCpesOnArtifact = CommonEnumerationUtil.parseEffectiveCpe(artifact); // if at least one timeline or vulnerable product contains the exact vendor/product, the cpe is // contained in the current vulnerability and should be highlighted for (Cpe cpe : allCpesOnArtifact) { if (allCpesOnArtifact.size() != 1 && coveredCpes.stream().noneMatch(vp -> cpe.getPart().equals(vp.getLeft()) && cpe.getVendor().equals(vp.getMiddle()) && cpe.getProduct().equals(vp.getRight()))) { unContainedCpes.add(cpe); if (coveredCpes.stream().anyMatch(vp -> cpe.getPart().equals(vp.getLeft()) || cpe.getVendor().equals(vp.getMiddle()) || cpe.getProduct().equals(vp.getRight()) || cpe.getVendor().equals(vp.getRight()) || cpe.getProduct().equals(vp.getMiddle()))) { // if at least the vendor OR the product match, only add 0.5 to the cpe distance correlationDistance += 0.5; } else { // if the cpe does not match at all, add 1 to the cpe distance correlationDistance += 1; } } } for (Cpe uncontainedCpe : unContainedCpes.stream().sorted().collect(Collectors.toList())) { final DivTag cpeMatchingCriteriaEntry = div().withClasses("matching-criteria-badge", "inapplicable"); cpeMatchingCriteriaEntry.with(span("NVD")); cpeMatchingCriteriaEntry.with(span(CommonEnumerationUtil.toCpe22UriOrFallbackToCpe23FS(uncontainedCpe))); matchingCriteriaContainer.with(cpeMatchingCriteriaEntry); allNavigationTableMatchingSources.add(uncontainedCpe.getVendor() + " / " + uncontainedCpe.getProduct()); } if (artifact.get(InventoryAttribute.MS_PRODUCT_ID.getKey()) != null) { final String[] msProductIds = artifact.get(InventoryAttribute.MS_PRODUCT_ID.getKey()).split(", "); for (String msProductId : msProductIds) { if (!allMsProductIds.contains(msProductId)) { final DivTag msProductMatchingCriteriaEntry = div().withClasses("matching-criteria-badge", "inapplicable"); msProductMatchingCriteriaEntry.with(span("MSRC")); msProductMatchingCriteriaEntry.with(span("product " + msProductId)); matchingCriteriaContainer.with(msProductMatchingCriteriaEntry); } } } if (matchingCriteriaContainer.getNumChildren() > 0) { row.put("Criteria", matchingCriteriaContainer); } } artifactsCpeParagraph.with(matchingSourcesTable.generate(Arrays.asList("Id", "Component", "Version", "URL", Constants.KEY_ORGANIZATION, "Is DT Finding", "EOL Id", "MS Product ID", "PURL", "Criteria", "Projects"))); artifactsCpeParagraph.with(br()); // navigation entries vulnerabilitySheet.addNavigationEntry(NavigationHeaders.CORRELATION_DISTANCE.getTitle(), (String.valueOf(correlationDistance)).replace(".0", "")); final SpanTag content = span(); if (!navigationTableMatchingSources.isEmpty()) { navigationTableMatchingSources.stream() .sorted() .forEach(e -> content.with(span(e).withClass("badge-navigation"))); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_USED_MATCHING_INFORMATION.getTitle(), content); } // NAVIGATION_UNUSED_MATCHING_INFORMATION, find any matching sources that were not used allNavigationTableMatchingSources.addAll(navigationTableMatchingSources); allNavigationTableMatchingSources.removeAll(navigationTableMatchingSources); if (!allNavigationTableMatchingSources.isEmpty()) { final SpanTag unusedContent = span(); allNavigationTableMatchingSources.stream() .sorted() .forEach(e -> unusedContent.with(span(e).withClass("badge-navigation"))); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_UNUSED_MATCHING_INFORMATION.getTitle(), unusedContent); } } // check if there are artifacts that have the vulnerability. // this can happen in a variety of cases, add a hint why there are no matching artifacts if (vulnerabilityAffectedArtifacts.isEmpty()) { final String color, message; if (vulnerability.hasTag("added by status")) { color = "alert-success"; message = "This vulnerability has been added via a status file and was not matched by the inventory."; if (vulnerabilitySheet.getNavigationRow().getEntries().get(NavigationHeaders.NAVIGATION_USED_MATCHING_INFORMATION.getTitle()) == null) { vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_USED_MATCHING_INFORMATION.getTitle(), span("assessment").withClass("badge-navigation")); } } else { color = "alert-danger"; message = "There are no matching artifacts for this vulnerability."; if (vulnerabilitySheet.getNavigationRow().getEntries().get(NavigationHeaders.NAVIGATION_USED_MATCHING_INFORMATION.getTitle()) == null) { vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_USED_MATCHING_INFORMATION.getTitle(), "N/A"); } } artifactsCpeParagraph.with( div().withClasses("alert", color).attr("role", "alert").with( span(SvgIcon.ALERT.getTag(20)).withStyle("margin-right:4px;"), text(message) ) ); } return artifactsCpeParagraph; } private SheetParagraph createParagraphTimelines(Vulnerability vulnerability, VulnerabilityTimelineGeneratorResult vulnerabilityTimelines, VadDetailLevelConfiguration detailLevel) { final SheetParagraph timelinesParagraph = new SheetParagraph(); timelinesParagraph.setIdentifier("Version Timelines"); timelinesParagraph.setTitle("Version Timelines"); final Set vulnerabilityAffectedArtifacts = vulnerability.getAffectedArtifactsByDefaultKey(); if (detailLevel.isTimeline()) { // find cpe timelines to the artifacts final List timelines; if (vulnerability.optVulnerabilityStatus().map(s -> s.isLatestStatusHistoryEntryOfType(VulnerabilityMetaData.STATUS_VALUE_VOID)).orElse(false)) { timelines = new ArrayList<>(); } else { timelines = vulnerabilityTimelines.getTimelinesForArtifacts(vulnerabilityAffectedArtifacts, vulnerability.getId()); } // only keep timelines that: [have more than one cpe version] // this is done only now, since the step above (listing cpes) requires also the ones that have only one cpe version timelines.removeIf(timeline -> timeline.getVersions().size() <= 1); // iterate over remaining recommended product timelines and list their details if (!timelines.isEmpty() && timelines.get(0) != null && timelines.get(0).getVersions().size() > 1) { timelinesParagraph.with(b("Related Product Timeline" + plural(timelines.size()) + ":")); final UlTag timelineList = ul(); final List affectedCpes = VulnerableSoftwareTreeNode.getAllCpes(vulnerability.getVulnerableSoftwareConfigurations()); final Set> allVendorProducts = new HashSet<>(); final Map>> vendorProductsPerArtifact = new HashMap<>(); this.findVendorProductsOnArtifactsForVulnerabilityTimeline(vulnerabilityAffectedArtifacts, affectedCpes, allVendorProducts, vendorProductsPerArtifact); for (VulnerabilityTimeline timeline : timelines) { final List affectedArtifacts = new ArrayList<>(); for (Map.Entry>> artifactVendorProducts : vendorProductsPerArtifact.entrySet()) { final Artifact artifact = artifactVendorProducts.getKey(); final List> vendorProducts = artifactVendorProducts.getValue(); for (Pair vendorProduct : vendorProducts) { if (vendorProduct.getLeft().equals(timeline.getVendor()) && vendorProduct.getRight().equals(timeline.getProduct())) { affectedArtifacts.add(artifact); } } } final List customArtifactTimelineVersions = timeline.generateCustomVersionsFromArtifacts(vulnerabilityAffectedArtifacts); final LiTag timelineElement = li().with( b(rawHtml(timeline.getVendor() + " " + timeline.getProduct())), br(), iff(!affectedArtifacts.isEmpty(), join("Shown because of the following artifact" + plural(affectedArtifacts.size()) + ":", br())) ); // list artifacts that are contained in timeline for (Artifact artifact : affectedArtifacts) { if (artifact.getUrl() != null) { timelineElement.with(makeHTMLLink("" + artifact.getId() + "", artifact.getUrl(), HrefTargets.TARGET_ARTIFACT.getTarget())); } else { timelineElement.with(rawHtml("" + artifact.getId() + "")); } } final List cpeVersionList; if (configuration.isVulnerabilityTimelineHideIrrelevantVersions()) { cpeVersionList = timeline.getReducedOfficialAndArtifactMergedCpeVersions(vulnerabilityAffectedArtifacts, customArtifactTimelineVersions); } else { cpeVersionList = timeline.getOfficialAndArtifactMergedVersions(customArtifactTimelineVersions); } // get the versions that match the artifact version final List currentVersions = timeline.createVersionsForArtifactVersion(vulnerabilityAffectedArtifacts, customArtifactTimelineVersions); currentVersions.removeIf(currentVersion -> !cpeVersionList.contains(currentVersion)); // create chart that contains each version details // count the total amount of vulnerabilities in each version final LineChartDataset timelineChartDatasetTotalVulnerabilities = new LineChartDataset() .setLabel("Total") .setyAxisID("A") .setBorderColor(ColorScheme.STRONG_YELLOW.getColor()) .setTension(0.2) .addData(cpeVersionList.stream().map(TimelineVersion::getVulnerabilityCount).collect(Collectors.toList())); final LineChartData timelineChartData = new LineChartData() .addDataset(timelineChartDatasetTotalVulnerabilities); // highlight special versions for (TimelineVersion version : cpeVersionList) { if (currentVersions.contains(version)) { // the currently used version contains the vulnerability timelineChartDatasetTotalVulnerabilities.addPointBackgroundColor(ColorScheme.STRONG_BLUE.getColor()); timelineChartDatasetTotalVulnerabilities.addPointRadius(4); } else if (version.containsVulnerability(vulnerability)) { // the currently viewed vulnerability is contained in the version timelineChartDatasetTotalVulnerabilities.addPointBackgroundColor(ColorScheme.STRONG_RED.getColor()); timelineChartDatasetTotalVulnerabilities.addPointRadius(3); } else { // both sides do not contain matching vulnerability/version timelineChartDatasetTotalVulnerabilities.addPointBackgroundColor(ColorScheme.STRONG_YELLOW.getColor()); timelineChartDatasetTotalVulnerabilities.addPointRadius(3); } } // list the individual amount of vulnerabilities in the different categories final Map> versionSeverityCounts = new TreeMap<>(); for (TimelineVersion cpeVersion : cpeVersionList) { Map currentVersionSeverityCounts = cpeVersion.getVulnerabilitiesPerSeverity(); // then add them to the list as a single count per severity category for (CvssSeverityRanges.SeverityRange category : super.getSecurityPolicyConfiguration().getCvssSeverityRanges().getRanges()) { versionSeverityCounts.computeIfAbsent(category, l -> new ArrayList<>()).add(currentVersionSeverityCounts.getOrDefault(category, 0)); } } // add these severity counts to the chart for (Map.Entry> severityCountsPerVersion : versionSeverityCounts.entrySet()) { if (severityCountsPerVersion.getValue().stream().anyMatch(d -> d > 0)) { timelineChartData.addDataset(new LineChartDataset() .setLabel(severityCountsPerVersion.getKey().getName()) .setyAxisID("A") .setBorderColor(ColorScheme.deriveColor(severityCountsPerVersion.getKey().getColor().getColor(), (int) (0.5 * 255))) .setTension(0.2) .setBorderWidth(2) .addPointRadius(1.5) .addData(severityCountsPerVersion.getValue()) ); } } // add version/update as labels timelineChartData.addLabels(cpeVersionList.stream().map(TimelineVersion::getVersionUpdate).collect(Collectors.toList())); ChartOptions timelineChartOptions = new ChartOptions() .setInteraction(new InteractionOption().setMode("index").setIntersect(false)) .setResponsive(true) .setMaintainAspectRatio(false) .addScale("A", new LinearScaleOption() .setPosition("left") .setBeginAtZero(true) .setTitle(new ScaleTitleOption().setDisplay(true).setText("Vulnerabilities")) ); LineChart timelineChart = new LineChart() .setChartOptions(timelineChartOptions) .setChartData(timelineChartData); // build the chart as html and js initializing script final String timelineChartId = makeJsVar(vulnerability.getId(), random); timelineElement.with( div().attr("height", "340").withClass("cpe-timeline-chart-wrapper").with( canvas().attr("height", "340").withId("timelineChart" + timelineChartId) ), script(rawHtml("" + "var timelineChart = new Chart(\n" + " document.getElementById('timelineChart" + timelineChartId + "'),\n" + " " + timelineChart.build() + "\n" + ");" )) ); timelineList.with(timelineElement); } timelinesParagraph.with(timelineList); } } return timelinesParagraph; } private Optional createParagraphEolDate(Set artifacts, Sheet vulnerabilitySheet) { final Map> statesByProducts = ExportedCycleState.parseAndSortByProduct(artifacts); if (statesByProducts.isEmpty()) { return Optional.empty(); } final SheetParagraph paragraph = new SheetParagraph(); paragraph.setIdentifier("EOL"); paragraph.setTitle("End of Life"); int mostSevereRating = 0; for (Map.Entry> cycleStateEntry : statesByProducts.entrySet()) { final String productName = cycleStateEntry.getKey(); final List cycleStates = cycleStateEntry.getValue(); if (cycleStates == null) { LOG.warn("No cycle states found for product [{}]", productName); continue; } final List sortedCycleStates; { List tmp; try { tmp = cycleStates.stream() .filter(Objects::nonNull) .sorted(Comparator.comparing(ExportedCycleState::getCycle)) .collect(Collectors.toList()); } catch (Exception e) { // this was once the case, but should not happen anymore, see EolCycle#DEFAULT_COMPARATOR LOG.warn("Failed to sort cycle states for product [{}], using default order", productName, e); tmp = cycleStates; } sortedCycleStates = tmp; } paragraph.with(h3(text("Product: "), a(productName).withHref("https://endoflife.date/" + productName).withTarget(HrefTargets.TARGET_EOL_DATE.getTarget()))); final Map> artifactVersionCycleStateMap = ExportedCycleState.groupByArtifactVersion(sortedCycleStates); final TableBuilder table = new TableBuilder(); for (Map.Entry> versionEntry : artifactVersionCycleStateMap.entrySet()) { final List versionCycleStates = versionEntry.getValue(); final ExportedCycleState relevantState = versionCycleStates.get(0); final List artifactIds = new ArrayList<>(); for (ExportedCycleState versionCycleState : versionCycleStates) { if (versionCycleState.getArtifact() != null && StringUtils.hasText(versionCycleState.getArtifact().getId())) { artifactIds.add(versionCycleState.getArtifact().getId()); } } final Map row = table.createRow(); final CycleScenarioRating cycleScenarioRating; if (relevantState.getCycleStateScenario() == CycleStateScenario.EXTENDED_SUPPORT_NOT_PRESENT) { cycleScenarioRating = relevantState.getCycleStateExtendedSupportUnavailable().getRating(); switch (cycleScenarioRating) { case RATING_4: mostSevereRating = Math.max(mostSevereRating, 6); break; default: mostSevereRating = Math.max(mostSevereRating, cycleScenarioRating.getRating()); } } else { cycleScenarioRating = relevantState.getCycleStateExtendedSupportAvailable().getRating(); mostSevereRating = Math.max(mostSevereRating, cycleScenarioRating.getRating()); } { final String artifactId = relevantState.getArtifact().getId(); final String artifactVersion = relevantState.getArtifact().getVersion(); final SpanTag artifactCell = span() .withStyle("color: var(--" + cycleScenarioRating.getColor().getCssRootName() + ")"); for (int i = 0; i < artifactIds.size(); i++) { artifactCell.with(text(artifactIds.get(i))); if (i < artifactIds.size() - 1) { artifactCell.with(br()); } } if (artifactId != null && !artifactId.contains(artifactVersion)) { artifactCell.with(span(text(" ("), createArtifactTableEntryForOptionallyEmpty(relevantState.getArtifact().getVersion()), text(")"))); } row.put("Artifact", artifactCell); } row.put("Cycle", span( text(relevantState.getCycle().getCycle()), iff(StringUtils.hasText(relevantState.getCycle().getReleaseLabel()), text(" (" + relevantState.getCycle().getReleaseLabel() + ")")), iff(StringUtils.hasText(relevantState.getCycle().getLink()), a(" (link)").withHref(relevantState.getCycle().getLink()).withTarget(HrefTargets.TARGET_EOL_DATE.getTarget())), iff(relevantState.isLts(), text(" (LTS)")) ).withStyle("color: var(--" + cycleScenarioRating.getColor().getCssRootName() + ")") ); row.put("Support", span( span(relevantState.getSupportState().getIcon().getTag(15)), span( text(relevantState.getSupportState().getShortDescription()), br(), text((relevantState.getSupportMillis() < 0 ? "Ended: " : "Ends: ") + EolCycle.formatTimeUntilOrAgo(relevantState.getSupportMillis())) ) ) .withClass("eol-icon-container") .withStyle("color: var(--" + relevantState.getSupportState().getColor().getCssRootName() + ")") ); if (relevantState.getCycleStateScenario() == CycleStateScenario.EXTENDED_SUPPORT_INFORMATION_PRESENT) { row.put("Extended Support", span( span(relevantState.getExtendedSupportState().getIcon().getTag(15)), span( text(relevantState.getExtendedSupportState().getShortDescription()), br(), text((relevantState.getExtendedSupportMillis() < 0 ? "Ended: " : "Ends: ") + EolCycle.formatTimeUntilOrAgo(relevantState.getExtendedSupportMillis())) ) ) .withClass("eol-icon-container") .withStyle("color: var(--" + relevantState.getExtendedSupportState().getColor().getCssRootName() + ")") ); } final Map recommendations = new TreeMap<>(VersionComparator.INSTANCE.reversed()); if (!relevantState.isAlreadyLatestVersion() && StringUtils.hasText(relevantState.getLatestLifecycleVersion())) { recommendations.computeIfAbsent(relevantState.getLatestLifecycleVersion(), k -> new StringJoiner(", ")).add("latest"); } if (!relevantState.isAlreadyLatestCycleVersion() && StringUtils.hasText(relevantState.getLatestCycleVersion()) && !relevantState.getLatestCycleVersion().equals(relevantState.getLatestLifecycleVersion())) { recommendations.computeIfAbsent(relevantState.getLatestCycleVersion(), k -> new StringJoiner(", ")).add("latest in cycle"); } if (StringUtils.hasText(relevantState.getNextSupportedVersion())) { recommendations.computeIfAbsent(relevantState.getNextSupportedVersion(), k -> new StringJoiner(", ")).add("next supported"); } if (StringUtils.hasText(relevantState.getNextSupportedExtendedVersion()) && !relevantState.getNextSupportedExtendedVersion().equals(relevantState.getNextSupportedVersion())) { recommendations.computeIfAbsent(relevantState.getNextSupportedExtendedVersion(), k -> new StringJoiner(", ")).add("next supported extended"); } if (StringUtils.hasText(relevantState.getClosestActiveLtsVersion())) { recommendations.computeIfAbsent(relevantState.getClosestActiveLtsVersion(), k -> new StringJoiner(", ")).add("closest supported lts"); } if (StringUtils.hasText(relevantState.getLatestActiveLtsVersion())) { recommendations.computeIfAbsent(relevantState.getLatestActiveLtsVersion(), k -> new StringJoiner(", ")).add("latest lts"); } if (!recommendations.isEmpty()) { final Optional recommendationContents = recommendations.entrySet().stream() .map(e -> span( rawHtml(svgArrowUpDown(VersionComparator.INSTANCE.compare(e.getKey(), relevantState.getArtifact().getVersion()) > 0)), b(" " + e.getKey()), text(" ("), text(e.getValue().toString()), text(")") )) .reduce((a, b) -> span(a, br(), b)); recommendationContents.ifPresent(span -> row.put("Recommended Versions", span)); } } paragraph.with(table.generate().withClass("basic-table")); } final Map ratingToText = new HashMap() {{ put(1, "supported"); put(2, "ending support"); put(3, "extended support"); put(4, "ending support (ext)"); put(5, "no support"); put(6, "ending support (reg)"); }}; if (mostSevereRating >= 1 && mostSevereRating <= 5) { vulnerabilitySheet.addNavigationEntry(NavigationHeaders.EOL_STATE.getTitle(), ratingToText.get(mostSevereRating)); } return Optional.of(paragraph); } private String svgArrowUpDown(boolean up) { if (up) { return "" + "" + ""; } else { return "" + "" + ""; } } private SheetParagraph createParagraphAssessment(Vulnerability vulnerability, Sheet vulnerabilitySheet) { final SheetParagraph assessmentParagraph = new SheetParagraph(); assessmentParagraph.setIdentifier("Assessment"); assessmentParagraph.setTitle("Assessment"); // vulnerability status section final VulnerabilityStatus vulnerabilityStatus = vulnerability.getVulnerabilityStatus(); final List statusHistory = vulnerabilityStatus.getStatusHistory(); final VulnerabilityStatusHistoryEntry latestStatus = vulnerabilityStatus.getLatestActiveStatusHistoryEntry(); if (vulnerabilityStatus.hasReportedBy()) { assessmentParagraph.with(i("Reported by: "), rawHtml(vulnerabilityStatus.generateReportedByDateString()), br()); } if (vulnerabilityStatus.hasAcceptedBy()) { assessmentParagraph.with(i("Accepted by: "), rawHtml(vulnerabilityStatus.generateAcceptedByDateString()), br()); } if (!statusHistory.isEmpty() && StringUtils.hasText(statusHistory.get(0).getStatus())) { vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_STATUS.getTitle(), statusHistory.get(0).getStatus()); } else { vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_STATUS.getTitle(), "in review"); } // badge to open the assessment editor final JSONArray assessmentEditorAssessmentsData = new JSONArray(); for (VulnerabilityStatusHistoryEntry assessment : vulnerability.getVulnerabilityStatus().getStatusHistory()) { if (!assessment.isActive()) { continue; } final JSONObject assessmentData = new JSONObject() .put("assessmentStatus", assessment.getStatus() == null ? "none" : assessment.getStatus()) .put("assessmentRationale", assessment.getRationale()) .put("assessmentRisk", assessment.getRisk()) .put("assessmentMeasures", assessment.getMeasures()) .put("assessmentScore", assessment.getScore()) .put("assessmentAuthor", assessment.getAuthor()) .put("assessmentDate", assessment.getFormattedDate()) .put("assessmentLabelsInclude", assessment.getIncludeLabels()) .put("assessmentLabelsExclude", assessment.getExcludeLabels()); assessmentEditorAssessmentsData.put(assessmentData); } final JSONObject assessmentEditorData = new JSONObject() .put("assessments", assessmentEditorAssessmentsData) .put("vulnerability", new JSONArray().put(vulnerability.getId())) .put("title", vulnerabilityStatus.getTitle()) .put("reportedBy", vulnerabilityStatus.getReportedBy()) .put("reportedDate", vulnerabilityStatus.getReportedDate()) .put("acceptedBy", vulnerabilityStatus.getAcceptedBy()) // .put("assessmentCvssVector2P0All", vulnerabilityStatus.hasCvss2() ? vulnerabilityStatus.getCvss2() : null) // .put("assessmentCvssVector2P0Lower", vulnerabilityStatus.hasCvss2Lower() ? vulnerabilityStatus.getCvss2Lower() : null) // .put("assessmentCvssVector2P0Higher", vulnerabilityStatus.hasCvss2Higher() ? vulnerabilityStatus.getCvss2Higher() : null) // .put("assessmentCvssVector3P1All", vulnerabilityStatus.hasCvss3P1() ? vulnerabilityStatus.getCvss3() : null) // .put("assessmentCvssVector3P1Lower", vulnerabilityStatus.hasCvss3P1Lower() ? vulnerabilityStatus.getCvss3Lower() : null) // .put("assessmentCvssVector3P1Higher", vulnerabilityStatus.hasCvss3P1Higher() ? vulnerabilityStatus.getCvss3Higher() : null) // .put("assessmentCvssVector4P0All", vulnerabilityStatus.hasCvss4() ? vulnerabilityStatus.getCvss4() : null) // .put("assessmentCvssVector4P0Lower", vulnerabilityStatus.hasCvss4Lower() ? vulnerabilityStatus.getCvss4Lower() : null) // .put("assessmentCvssVector4P0Higher", vulnerabilityStatus.hasCvss4Higher() ? vulnerabilityStatus.getCvss4Higher() : null) ; final ATag assessmentEditorBadge = a("Edit in Assessment Editor") .withStyle("color: #fff; margin-bottom: 4px;") .withClasses("badge badge-primary") .attr("aria-expanded", "false") .attr("onclick", "openModal('assessment-editor-modal');assessmentEditorData={...assessmentEditorData, ..." + assessmentEditorData + "};updateAssessmentEditorModalVisuals()"); if (!statusHistory.isEmpty()) { // create an entry from the information provided in the most recent status file assessmentParagraph.with(createVulnerabilityStatusHistoryEntry(statusHistory.get(0), true) .withStyle("display:flex;flex-direction:column;width:fit-content;width:-moz-fit-content;margin-top:6px;") ); // create the rest of the history entries of the status if (statusHistory.size() > 1) { final String randomID = "" + Math.abs(statusHistory.hashCode()); assessmentParagraph.with(a("Show History") .withStyle("color: #fff; margin-bottom: 4px;") .withClasses("badge badge-primary") .attr("aria-expanded", "false") .attr("onclick", "if (document.getElementById('statusHistory" + randomID + "').classList.contains('hidden')) document.getElementById('statusHistory" + randomID + "').classList.remove('hidden'); else document.getElementById('statusHistory" + randomID + "').classList.add('hidden');this.text = this.text.indexOf('Show') !== -1 ? 'Hide History' : 'Show History'") ); assessmentParagraph.with(assessmentEditorBadge); final DivTag expandableCardBody = div().withClasses("card", "card-body"); for (VulnerabilityStatusHistoryEntry entry : statusHistory.stream().skip(1).collect(Collectors.toList())) { expandableCardBody.with(createVulnerabilityStatusHistoryEntry(entry, false)); } assessmentParagraph.with(div().withClass("hidden").withId("statusHistory" + randomID).with(expandableCardBody)).with(br()); } else { assessmentParagraph.with(assessmentEditorBadge); } assessmentParagraph.with(br()); } else { assessmentParagraph.with(assessmentEditorBadge); } // cvss charts final CvssSelectionResult selectedCvssVectors = vulnerability.getCvssSelectionResult(); final CvssVector overallInitialCvssVector = selectedCvssVectors.getSelectedInitialCvss(); final CvssVector overallContextCvssVector = selectedCvssVectors.getSelectedContextCvss(); final CvssVector overallContextOrInitialCvss = selectedCvssVectors.getSelectedContextIfAvailableOtherwiseInitial(); final boolean hasAnyCvssVectors = selectedCvssVectors.hasAnyCvss(); final boolean hasBaseCvss = selectedCvssVectors.hasInitialCvss(); final boolean hasEffectiveCvss = selectedCvssVectors.hasContextCvss(); final boolean overallContextOrInitialCvssHasBaseDefined = overallContextOrInitialCvss != null && overallContextOrInitialCvss.isBaseFullyDefined(); if (hasAnyCvssVectors) { assessmentParagraph.withStyle("min-height: 400px;"); } // apply navigation column text vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_CVSS_UNMODIFIED_OVERALL.getTitle(), hasBaseCvss ? overallInitialCvssVector.getBakedScores().getNormalizedOverallScore() : "N/A" ); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_CVSS_MODIFIED_OVERALL.getTitle(), hasEffectiveCvss ? overallContextCvssVector.getOverallScore() : (hasBaseCvss ? overallInitialCvssVector.getBakedScores().getNormalizedOverallScore() : "N/A"), hasEffectiveCvss ? null : "override_gray" ); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_CVSS_BASE.getTitle(), overallContextOrInitialCvssHasBaseDefined && !Double.isNaN(overallContextOrInitialCvss.getBakedScores().getBaseScore()) ? overallContextOrInitialCvss.getBaseScore() : "N/A"); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_CVSS_EXPLOITABILITY.getTitle(), overallContextOrInitialCvssHasBaseDefined && !Double.isNaN(overallContextOrInitialCvss.getBakedScores().getNormalizedExploitabilityScore()) ? overallContextOrInitialCvss.getBakedScores().getNormalizedExploitabilityScore() : "N/A"); vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_CVSS_IMPACT.getTitle(), overallContextOrInitialCvssHasBaseDefined && !Double.isNaN(overallContextOrInitialCvss.getBakedScores().getNormalizedImpactScore()) ? overallContextOrInitialCvss.getBakedScores().getNormalizedImpactScore() : "N/A"); { final UnescapedText exploitedTag = SvgIcon.EXCLAMATION_DIAMOND_FILL.getTag(22); //final UnescapedText ransomwareTag = SvgIcon.GEAR_FILL.getTag(22); if (vulnerability.getKevData() != null) { final KevData.RansomwareState ransomwareState = vulnerability.getKevData().getRansomwareState(); final SpanTag stateTag = span(); stateTag.with(span((ransomwareState.ordinal() + 1) + "").withStyle("display:inline-block;color:transparent;width:0px")); stateTag.with(span(exploitedTag).withStyle("color: var(--" + ColorScheme.STRONG_RED.getCssRootName() + ");")); /* if (ransomwareState == KevData.RansomwareState.KNOWN) { stateTag.with(text(" ")) .with(span(ransomwareTag).withStyle("color: var(--" + ColorScheme.STRONG_DARK_RED.getCssRootName() + ");")); }*/ vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_KEV_ENTRY.getTitle(), stateTag); } else { vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_KEV_ENTRY.getTitle(), span("0 N/A").withStyle("color:transparent")); } } // table showing detailed information on the CVSS vectors if (hasAnyCvssVectors) { final TableTag cvssOverviewTableBase = generateCvssDetailsTable(selectedCvssVectors, overallInitialCvssVector, overallContextCvssVector, true, false, true, vulnerability); final TableTag cvssOverviewTableEffective = generateCvssDetailsTable(selectedCvssVectors, overallInitialCvssVector, overallContextCvssVector, false, true, true, vulnerability); if (cvssOverviewTableBase != null) { assessmentParagraph.with(b("Initial CVSS Vectors")); final UniversalCvssCalculatorLinkGenerator generator = new UniversalCvssCalculatorLinkGenerator(); final List vectors = selectedCvssVectors.getAllVectors().getCvssVectors().stream() .filter(v -> v.getCvssSource().getHostingEntity() != KnownCvssEntities.ASSESSMENT) .collect(Collectors.toList()); for (CvssVector vector : vectors) { generator.addVectorForVulnerability(vector, vulnerability.getId()); } generator.setSelectedVector(overallInitialCvssVector); if (!generator.isEmpty()) { assessmentParagraph.with( a("Show [initial] in CVSS Calculator") .withHref(generateLinkForUniversalCvssCalculator(generator)) .withTarget(HrefTargets.TARGET_CVSS_DETAILS.getTarget()) .withClasses("badge", "badge-primary", "badge-underline", "clickable") .withStyle("color:var(--text-color-white);margin-left:0.7rem;")); } assessmentParagraph.with(br()).with(cvssOverviewTableBase).with(br()); } if (cvssOverviewTableEffective != null) { assessmentParagraph.with(b("Context CVSS Vectors")); final UniversalCvssCalculatorLinkGenerator generator = new UniversalCvssCalculatorLinkGenerator(); Stream.of( Pair.of(selectedCvssVectors.getInitialCvss2(), (CvssVector) null), Pair.of(selectedCvssVectors.getContextCvss2(), selectedCvssVectors.getInitialCvss2()), Pair.of(selectedCvssVectors.getInitialCvss3(), (CvssVector) null), Pair.of(selectedCvssVectors.getContextCvss3(), selectedCvssVectors.getInitialCvss3()), Pair.of(selectedCvssVectors.getInitialCvss4(), (CvssVector) null), Pair.of(selectedCvssVectors.getContextCvss4(), selectedCvssVectors.getInitialCvss4()) ).forEach(cvssVector -> generator.addVectorForVulnerability(cvssVector.getLeft(), vulnerability.getId()).setInitialCvssVectorUnchecked(cvssVector.getRight())); generator.setSelectedVector(overallContextCvssVector); if (!generator.isEmpty()) { assessmentParagraph.with( a("Show [all selected] in CVSS Calculator") .withHref(generateLinkForUniversalCvssCalculator(generator)) .withTarget(HrefTargets.TARGET_CVSS_DETAILS.getTarget()) .withClasses("badge", "badge-primary", "badge-underline", "clickable") .withStyle("color:var(--text-color-white);margin-left:0.7rem;")); } assessmentParagraph.with(br()).with(cvssOverviewTableEffective).with(br()); } } vulnerabilitySheet.addNavigationEntry(NavigationHeaders.NAVIGATION_EPSS_SCORE.getTitle(), vulnerability.getEpssData() != null ? String.format(Locale.ENGLISH, "%f", vulnerability.getEpssData().getEpssScore()) : "N/A"); // create radar chart for base/effective vectors if at least one of them is defined final DivTag cvssChartContainer = div().withClass("cvssChart-container-container"); if (hasBaseCvss) { final GeneratedChart generatedChart = createCombinedCvssChart( selectedCvssVectors.getInitialCvss2(), selectedCvssVectors.getInitialCvss3(), selectedCvssVectors.getInitialCvss4(), false, vulnerability.getId(), configuration.getVulnerabilitySvgChartInterpolationMethod()); generatedChart.writeSvgTo(configuration.getSvgDirectory()); cvssChartContainer.with(generatedChart.generateChartJsChart()); } if (hasEffectiveCvss) { final GeneratedChart generatedChart = createCombinedCvssChart( selectedCvssVectors.getContextCvss2() != null ? selectedCvssVectors.getContextCvss2() : selectedCvssVectors.getInitialCvss2(), selectedCvssVectors.getContextCvss3() != null ? selectedCvssVectors.getContextCvss3() : selectedCvssVectors.getInitialCvss3(), selectedCvssVectors.getContextCvss4() != null ? selectedCvssVectors.getContextCvss4() : selectedCvssVectors.getInitialCvss4(), true, vulnerability.getId(), configuration.getVulnerabilitySvgChartInterpolationMethod()); generatedChart.writeSvgTo(configuration.getSvgDirectory()); cvssChartContainer.with(generatedChart.generateChartJsChart()); } if (cvssChartContainer.getNumChildren() > 0) { assessmentParagraph.with(cvssChartContainer); } return assessmentParagraph; } private SheetParagraph createParagraphCwe(Vulnerability vulnerability) { final SheetParagraph cweParagraph = new SheetParagraph(); cweParagraph.setIdentifier("CWE"); cweParagraph.setTitle("CWE"); final UlTag list = ul(); for (String cwe : vulnerability.getCwes()) { final String cweId = cwe.replaceAll("[^-]+-(\\d+)(?: .+)?", "$1"); //2011-4089 final String description = cwe.replace("CWE-" + cweId, "").trim(); list.with(li( makeHTMLLink("CWE-" + cweId + "", "https://cwe.mitre.org/data/definitions/" + cweId + ".html", HrefTargets.TARGET_CWE.getTarget()), rawHtml(" " + description.replaceAll("^\\((.+)\\)$", "$1")) )); } cweParagraph.with(list); return cweParagraph; } private SheetParagraph createParagraphMicrosoftVulnerabilityInformation(Vulnerability vulnerability, List filteredSecurityAdvisories, AtomicInteger amountReviewedEntries, VadDetailLevelConfiguration detailLevel) { final SheetParagraph msParagraph = new SheetParagraph(); msParagraph.setIdentifier("Microsoft Security Upgrade Guide"); msParagraph.setTitle("Microsoft Security Upgrade Guide"); try { final List msrcAdvisories = vulnerability.getRelatedAdvisors(AdvisoryTypeStore.MSRC, MsrcAdvisorEntry.class).stream() .filter(filteredSecurityAdvisories::contains) .collect(Collectors.toList()); if (msrcAdvisories.isEmpty()) { return null; } final String msFixingIdValue = vulnerability.getAdditionalAttribute(InventoryAttribute.MS_FIXING_KB_IDENTIFIER.getKey()); final JSONObject fixingKbIds = new JSONObject(StringUtils.hasText(msFixingIdValue) ? msFixingIdValue : "{}"); if (!fixingKbIds.isEmpty()) { final ContentCard fixingKbIdsCard = new ContentCard() .withTitle("Summary of Security Updates") .withType(ContentCard.ContentCardType.GREEN); final UlTag fixingKbIdsList = ul(); fixingKbIdsCard.with(fixingKbIdsList); for (String key : fixingKbIds.keySet()) { final List values = fixingKbIds.getJSONArray(key).toList().stream().map(Object::toString).collect(Collectors.toList()); final LiTag fixingList = li(b(key), text(": ")); fixingKbIdsList.with(fixingList); for (Iterator iterator = values.iterator(); iterator.hasNext(); ) { final String value = iterator.next(); final boolean hasNext = iterator.hasNext(); fixingList.with(text(value + " (")) .with(makeHTMLLink("MS Support", "https://support.microsoft.com/en-us/help/" + value, HrefTargets.TARGET_MICROSOFT.getTarget())) .with(text(", ")) .with(makeHTMLLink("Update Catalog", "https://catalog.update.microsoft.com/Search.aspx?q=KB" + value, HrefTargets.TARGET_MICROSOFT.getTarget())) .with(text(")")) .with(iff(hasNext, rawHtml(", "))); } } msParagraph.with(fixingKbIdsCard.generate()); } final Set artifactProductIds = MsrcAdvisorEntry.getAllMsrcProductIds(vulnerability.getAffectedArtifactsByDefaultKey()); if (!artifactProductIds.isEmpty()) { final ContentCard productIdsToNamesCard = new ContentCard() .withTitle("Related Product") .withType(ContentCard.ContentCardType.DEFAULT); final UlTag productIdsToNamesList = ul(); productIdsToNamesCard.with(productIdsToNamesList); for (String productId : artifactProductIds) { final MsrcProduct product = msrcProductIndexQuery.get().findProductById(productId); if (product != null) { productIdsToNamesList.with(li(b(product.getId()), text(": " + product.getName() + " (" + product.getVendor() + ", " + product.getFamily() + ")"))); } } if (productIdsToNamesList.getNumChildren() > 0) { msParagraph.with(productIdsToNamesCard.generate()); } } // now generate sections for each advisory for (MsrcAdvisorEntry msrcAdvisory : msrcAdvisories) { final List descriptions = msrcAdvisory.getDescription(); final String title = msrcAdvisory.getSummary(); final List msRemediations = msrcAdvisory.getMsRemediations().stream().sorted().collect(Collectors.toList()); final List msThreats = msrcAdvisory.getMsThreats().stream().sorted().collect(Collectors.toList()); final List affectedProducts = msrcAdvisory.getAffectedProducts().stream().sorted().collect(Collectors.toList()); final Map noteMap = new HashMap<>(); if (descriptions != null) { for (DescriptionParagraph description : descriptions) { noteMap.put( description.getHeader(), findAndMakeCveLinks(description.getContent().replace(" k.startsWith("Tag")).findFirst().orElse(null); msParagraph.with( iffElse(tagKey != null, h4(linkTitle, text(" (" + noteMap.get(tagKey) + ")"), text(" (" + msrcAdvisory.getType() + ")")), h4(linkTitle, text(" (" + msrcAdvisory.getType() + ")")) ) ); noteMap.remove(tagKey); } else { linkTitle = null; msParagraph.with(h4("Untitled vulnerability")); } final CvssSelectionResult effectiveCvssVectors = vulnerability.selectEffectiveCvssVectors(msrcAdvisory.getCvssVectors(), super.getSecurityPolicyConfiguration()); final TableTag baseCvssTable = generateCvssDetailsTable(effectiveCvssVectors, null, null, true, true, false, vulnerability); if (baseCvssTable != null) { msParagraph.with(b("Product CVSS Vectors")).with(br()) .with(baseCvssTable).with(br()); } checkIfAdvisorHasBeenReviewedAndAppendCard(msParagraph, msrcAdvisory, vulnerability.getVulnerabilityStatus(), amountReviewedEntries, linkTitle); // notes, description, remediation, threats, faq, ... final List msVulnerabilityNotesOrder = Arrays.asList("Description", "FAQ", "CNA"); final List msVulnerabilityNotesReplacements = Arrays.asList("Description", "Frequently Asked Questions", "Vulnerability Naming Authority"); for (int i = 0; i < msVulnerabilityNotesOrder.size(); i++) { final String order = msVulnerabilityNotesOrder.get(i); final String replacementNoteTitle = msVulnerabilityNotesReplacements.get(i); if (noteMap.containsKey(order)) { final String noteText = noteMap.get(order); msParagraph.with(new ContentCard().withText(noteText).withTitle(replacementNoteTitle).withCharacterCollapseThreshold(1000).generate()); noteMap.remove(order); } } for (Map.Entry note : noteMap.entrySet()) { final String noteTitle = note.getKey(); final String noteText = note.getValue(); msParagraph.with(new ContentCard().withText(noteText).withTitle(noteTitle).withCharacterCollapseThreshold(1000).generate()); } if (!msRemediations.isEmpty()) { final UlTag remediationList = ul(); for (final Iterator iterator = msRemediations.iterator(); iterator.hasNext(); ) { final MsrcRemediation remediation = iterator.next(); final boolean hasNext = iterator.hasNext(); final String type = remediation.getType(); final String description = remediation.getDescription(); final Reference reference = remediation.getUrl(); final String supercedence = remediation.getSupercedence(); final String subType = remediation.getSubType(); final Set affectedProductIds = remediation.getAffectedProductIds(); final String productIdList = String.join(", ", affectedProductIds); if (!affectedProductIds.isEmpty() && artifactProductIds.stream().noneMatch(productIdList::contains)) { continue; } final LiTag remediationEntry = li(); remediationList.with(remediationEntry); final BTag link = reference != null ? b(makeHTMLLink(type + ": " + reference.getTitle(), reference.getUrl(), HrefTargets.TARGET_MICROSOFT.getTarget())) : null; if (link != null) { remediationEntry.with(link); } if (subType != null) { remediationEntry.with(text(" (" + subType + ")"), br()); } else if (link != null) { remediationEntry.with(br()); } if (description.matches("\\d+")) { remediationEntry.with( text("Patch available: "), makeHTMLLink(description, "https://www.catalog.update.microsoft.com/Search.aspx?q=KB" + description, HrefTargets.TARGET_MICROSOFT.getTarget()), br() ); } else if (StringUtils.hasText(description)) { remediationEntry.with(rawHtml("Description: " + description), br()); } if (supercedence != null) { remediationEntry.with(text("Supercedence: " + supercedence), br()); } if (StringUtils.hasText(productIdList)) { remediationEntry.with(text("Available for products: " + productIdList), br()); } if (hasNext) { remediationEntry.with(br()); } } if (remediationList.getNumChildren() > 0) { msParagraph.with(new ContentCard().with(remediationList).withTitle("Remediation").withCharacterCollapseThreshold(1000).generate()); } // threats if (!msThreats.isEmpty()) { final List highlightedProducts = new ArrayList<>(); final List regularProducts = new ArrayList<>(); for (MsThreat threat : msThreats) { final String type = threat.getType(); final String description = threat.getDescription() == null ? "" : threat.getDescription(); final String productId = threat.getProductId(); if (artifactProductIds.contains(productId)) { highlightedProducts.add(productId + ": " + description + " (" + type + ")"); } else if ("Exploit Status".equals(type)) { highlightedProducts.add(type + ": " + description.replace(";", ", ").replace(":", ": ")); } else { regularProducts.add((StringUtils.hasText(productId) ? productId + " " : "") + description + " (" + type + ")"); } } final UlTag threatsList = ul(); for (String highlightedProduct : highlightedProducts) { threatsList.with(li(b(highlightedProduct))); } for (String regularProduct : regularProducts) { threatsList.with(li(text(regularProduct))); } msParagraph.with(new ContentCard().withTitle("Threats").with(threatsList).withCharacterCollapseThreshold(250).generate()); } } // affected products if (!affectedProducts.isEmpty()) { final List highlightedProducts = new ArrayList<>(); final List regularProducts = new ArrayList<>(); for (String affectedProduct : affectedProducts) { final MsrcProduct product = msrcProductIndexQuery.get().findProductById(affectedProduct); final String content; if (product != null) { content = product.getId() + ": " + product.getName() + " (" + product.getFamily() + ")"; } else { content = affectedProduct; } if (artifactProductIds.contains(affectedProduct)) { highlightedProducts.add(content); } else { regularProducts.add(content); } } final UlTag affectedProductsList = ul(); for (String highlightedProduct : highlightedProducts) { affectedProductsList.with(li(b(highlightedProduct))); } for (String regularProduct : regularProducts) { affectedProductsList.with(li(text(regularProduct))); } msParagraph.with(new ContentCard().withTitle("Affected Products").with(affectedProductsList).withCharacterCollapseThreshold(250).generate()); } } } catch (JSONException e) { LOG.error("Unable to read JSON formatted MS Vulnerability data from inventory", e); } return msParagraph; } private SheetParagraph createParagraphCertSei(Vulnerability vulnerability, List filteredSecurityAdvisories, Collection vulnerabilities, AtomicInteger amountReviewedCertSei, VadDetailLevelConfiguration detailLevel) { final List certSeiAdvisories = vulnerability.getRelatedAdvisors(AdvisoryTypeStore.CERT_SEI, CertSeiAdvisorEntry.class).stream() .filter(filteredSecurityAdvisories::contains) .collect(Collectors.toList()); if (certSeiAdvisories.isEmpty()) { return null; } final SheetParagraph certSeiParagraph = new SheetParagraph(); certSeiParagraph.setIdentifier("CERT-SEI"); certSeiParagraph.setTitle("CERT-SEI Advisor" + plural(certSeiAdvisories.size())); for (CertSeiAdvisorEntry certSeiAdvisor : certSeiAdvisories) { appendDefaultAdvisorContentCards(certSeiParagraph, certSeiAdvisor, vulnerability, vulnerability.getVulnerabilityStatus(), vulnerabilities, amountReviewedCertSei, detailLevel); } return certSeiParagraph; } private SheetParagraph createParagraphCertFr(Vulnerability vulnerability, List filteredSecurityAdvisories, Collection vulnerabilities, AtomicInteger amountReviewedCertFr, VadDetailLevelConfiguration detailLevel) { final List certFrAdvisories = vulnerability.getRelatedAdvisors(AdvisoryTypeStore.CERT_FR, CertFrAdvisorEntry.class).stream() .filter(filteredSecurityAdvisories::contains) .collect(Collectors.toList()); if (certFrAdvisories.isEmpty()) { return null; } final SheetParagraph certFrParagraph = new SheetParagraph(); certFrParagraph.setIdentifier("CERT-FR"); certFrParagraph.setTitle("CERT-FR Advisor" + plural(certFrAdvisories.size())); for (CertFrAdvisorEntry certFrAdvisor : certFrAdvisories) { appendDefaultAdvisorContentCards(certFrParagraph, certFrAdvisor, vulnerability, vulnerability.getVulnerabilityStatus(), vulnerabilities, amountReviewedCertFr, detailLevel); } return certFrParagraph; } private SheetParagraph createParagraphCertEu(Vulnerability vulnerability, List filteredSecurityAdvisories, Collection vulnerabilities, AtomicInteger amountReviewedCertEu, VadDetailLevelConfiguration detailLevel) { final List certEuAdvisories = vulnerability.getRelatedAdvisors(AdvisoryTypeStore.CERT_EU, CertEuAdvisorEntry.class).stream() .filter(filteredSecurityAdvisories::contains) .collect(Collectors.toList()); if (certEuAdvisories.isEmpty()) { return null; } final SheetParagraph certEuParagraph = new SheetParagraph(); certEuParagraph.setIdentifier("CERT-EU"); certEuParagraph.setTitle("CERT-EU Advisor" + plural(certEuAdvisories.size())); for (CertEuAdvisorEntry certEuAdvisor : certEuAdvisories) { final List description = certEuAdvisor.getDescription(); for (int i = description.size() - 1; i >= 0; i--) { final DescriptionParagraph descriptionParagraph = description.get(i); if (Objects.equals(descriptionParagraph.getHeader(), "content_markdown")) { description.add(descriptionParagraph.deriveWithHeader("Details")); } } certEuAdvisor.getDescription().removeIf(e -> Objects.equals(e.getHeader(), "content_html") || Objects.equals(e.getHeader(), "content_markdown")); appendDefaultAdvisorContentCards(certEuParagraph, certEuAdvisor, vulnerability, vulnerability.getVulnerabilityStatus(), vulnerabilities, amountReviewedCertEu, detailLevel); } return certEuParagraph; } private SheetParagraph createParagraphGhsa(Vulnerability vulnerability, List filteredSecurityAdvisories, Collection vulnerabilities, AtomicInteger amountReviewedEntries, VadDetailLevelConfiguration detailLevel) { final List ghsaAdvisories = vulnerability.getRelatedAdvisors(AdvisoryTypeStore.GHSA, GhsaAdvisorEntry.class).stream() .filter(filteredSecurityAdvisories::contains) .collect(Collectors.toList()); if (ghsaAdvisories.isEmpty()) { return null; } final SheetParagraph ghsaParagraph = new SheetParagraph(); ghsaParagraph.setIdentifier("GitHub Security Advisory"); ghsaParagraph.setTitle("GitHub Security Advisory"); final Set knownIdentifiers = new HashSet<>(); for (GhsaAdvisorEntry entry : ghsaAdvisories) { if (!knownIdentifiers.add(entry.getId())) { continue; } appendDefaultAdvisorContentCards(ghsaParagraph, entry, vulnerability, vulnerability.getVulnerabilityStatus(), vulnerabilities, amountReviewedEntries, detailLevel); } return ghsaParagraph; } private SheetParagraph createParagraphGenericAdvisory(Vulnerability vulnerability, List securityAdvisories, Collection vulnerabilities, AtomicInteger amountReviewed, VadDetailLevelConfiguration detailLevel) { if (securityAdvisories.isEmpty()) { return null; } final String advisorySource = securityAdvisories.stream() .findFirst() .map(a -> a.getSourceIdentifier().getWellFormedName()) .orElse("Advisory"); final SheetParagraph paragraph = new SheetParagraph(); paragraph.setIdentifier(advisorySource); paragraph.setTitle(advisorySource + " Advisor" + plural(securityAdvisories.size())); for (AdvisoryEntry advisoryEntry : securityAdvisories) { appendDefaultAdvisorContentCards(paragraph, advisoryEntry, vulnerability, vulnerability.getVulnerabilityStatus(), vulnerabilities, amountReviewed, detailLevel); } return paragraph; } private void appendDefaultAdvisorContentCards(SheetParagraph paragraph, AdvisoryEntry advisoryEntry, Vulnerability vulnerability, VulnerabilityStatus vulnerabilityStatus, Collection vulnerabilities, AtomicInteger amountReviewedEntries, VadDetailLevelConfiguration detailLevel) { final AdvisoryTypeIdentifier advisoryProvider = advisoryEntry.getSourceIdentifier(); final String hrefTarget; // TODO: maybe move URL targets into ContentIdentifiers if (advisoryProvider == AdvisoryTypeStore.CERT_FR) { hrefTarget = HrefTargets.TARGET_CERT_FR.getTarget(); } else if (advisoryProvider == AdvisoryTypeStore.CERT_EU) { hrefTarget = HrefTargets.TARGET_CERT_EU.getTarget(); } else if (advisoryProvider == AdvisoryTypeStore.CERT_SEI) { hrefTarget = HrefTargets.TARGET_CERT_SEI.getTarget(); } else if (advisoryProvider == AdvisoryTypeStore.MSRC) { hrefTarget = HrefTargets.TARGET_MICROSOFT.getTarget(); } else if (advisoryProvider == AdvisoryTypeStore.GHSA) { hrefTarget = HrefTargets.TARGET_GITHUB.getTarget(); } else { hrefTarget = HrefTargets.TARGET_BLANK.getTarget(); } final ContainerTag titleTag; if (StringUtils.hasText(advisoryEntry.getUrl())) { titleTag = makeHTMLLink(advisoryEntry.getId(), advisoryEntry.getUrl(), hrefTarget); } else { titleTag = span(advisoryEntry.getId()); } checkIfAdvisorHasBeenReviewedAndAppendCard(paragraph, advisoryEntry, vulnerabilityStatus, amountReviewedEntries, titleTag); paragraph.with(h4().with(titleTag).with(text(" (" + advisoryEntry.getType() + ")"))); final TableTag cvssOverviewTable = generateCvssDetailsTable(vulnerability.selectEffectiveCvssVectors(advisoryEntry.getCvssVectors(), super.getSecurityPolicyConfiguration()), null, null, true, true, false, vulnerability); if (cvssOverviewTable != null) { paragraph.with(cvssOverviewTable).with(br()); } if (StringUtils.hasText(advisoryEntry.getSummary())) { paragraph.with(new ContentCard() .withTitle("Summary") .withText(advisoryEntry.getSummary()) .generate() ); } if (!advisoryEntry.getDescription().isEmpty()) { for (DescriptionParagraph descriptionParagraph : advisoryEntry.getDescription()) { if (descriptionParagraph.getContent() == null) { LOG.debug("Content of description paragraph is null: {}", descriptionParagraph); continue; } // regex \n( *\*(?!\*)) is for replacing newlines followed by a bullet point (not followed by another bullet point) final SpanTag contentFromMarkdown = Dashboard.markdownToHtml(findAndMakeCveMDLinks(Dashboard.attemptEscapeScripts(descriptionParagraph.getContent())).replaceAll("\\n( *\\*(?!\\*))", "$1"), hrefTarget); paragraph.with(new ContentCard() .withTitle(descriptionParagraph.getHeader() == null ? "Description" : org.apache.commons.lang3.StringUtils.capitalize(descriptionParagraph.getHeader())) .with(contentFromMarkdown) .withCharacterCollapseThreshold(2000) .generate()); } } if (StringUtils.hasText(advisoryEntry.getThreat())) { paragraph.with(new ContentCard() .withTitle("Threat") .with(advisoryEntry.getThreat()) .generate()); } if (StringUtils.hasText(advisoryEntry.getRecommendations())) { paragraph.with(new ContentCard() .withTitle("Recommendations") .with(advisoryEntry.getRecommendations()) .generate()); } if (StringUtils.hasText(advisoryEntry.getWorkarounds())) { paragraph.with(new ContentCard() .withTitle("Workarounds") .with(advisoryEntry.getWorkarounds()) .generate()); } if (!advisoryEntry.getAcknowledgements().isEmpty()) { paragraph.with(new ContentCard() .withTitle("Acknowledgements") .with(String.join(" ", advisoryEntry.getAcknowledgements())) .generate()); } if (!advisoryEntry.getReferences().isEmpty() && detailLevel.isAdvisoriesReferences()) { paragraph.with(new ContentCard() .withTitle("References") .withCharacterCollapseThreshold(2000) .with(ul(advisoryEntry.getReferences().stream().map(Reference::toHtmlATag).map(TagCreator::li).toArray(LiTag[]::new))) .generate() ); } // create badges that link to related vulnerabilities final List badges = new ArrayList<>(); for (String cveId : advisoryEntry.getReferencedVulnerabilities(VulnerabilityTypeStore.CVE)) { if (!cveId.equals(vulnerability.getId())) { badges.add(makeVulnerabilityReferenceBadge(cveId, vulnerabilities)); } } if (!badges.isEmpty()) { paragraph.with(new ContentCard() .withTitle("References to other CVE") .with(badges.stream().sorted().map(TagCreator::rawHtml).toArray(DomContent[]::new)) .generate() ); } advisoryEntry.getReferencedSecurityAdvisories().forEach((key, ids) -> { final List badgesOther = new ArrayList<>(); for (String id : ids) { badgesOther.add(makeBadge(id, "secondary")); } paragraph.with(new ContentCard() .withTitle("References to other " + key.getWellFormedName()) .with(badgesOther.toArray(new DomContent[0])) .generate() ); }); } private TableTag generateCvssDetailsTable(CvssSelectionResult selectedCvssVectors, CvssVector overallBaseCvss, CvssVector overallEffectiveCvss, boolean initial, boolean context, boolean displayUsage, Vulnerability vulnerability) { final Map> displayVectors = new LinkedHashMap<>(); final SpanTag chartFillTag = displayUsage ? span(SvgIcon.BAR_CHART_FILL.getTag(15)).withStyle("margin-right: 8px;") : null; final SpanTag borderWidthTag = displayUsage ? span(SvgIcon.BORDER_WIDTH.getTag(15)).withStyle("margin-right: 8px;") : null; if (context) { addVectorIfNotPresent(displayVectors, overallEffectiveCvss, borderWidthTag); addVectorIfNotPresent(displayVectors, selectedCvssVectors.getContextCvss2(), chartFillTag); addVectorIfNotPresent(displayVectors, selectedCvssVectors.getContextCvss3(), chartFillTag); addVectorIfNotPresent(displayVectors, selectedCvssVectors.getContextCvss4(), chartFillTag); } if (initial) { addVectorIfNotPresent(displayVectors, overallBaseCvss, borderWidthTag); addVectorIfNotPresent(displayVectors, selectedCvssVectors.getInitialCvss2(), chartFillTag); addVectorIfNotPresent(displayVectors, selectedCvssVectors.getInitialCvss3(), chartFillTag); addVectorIfNotPresent(displayVectors, selectedCvssVectors.getInitialCvss4(), chartFillTag); final List allVectors = selectedCvssVectors.getAllVectors().getCvssVectors().stream() .filter(v -> v.getCvssSource().getHostingEntity() != KnownCvssEntities.ASSESSMENT) .collect(Collectors.toList()); allVectors.forEach(vector -> addVectorIfNotPresent(displayVectors, vector)); } final List cvssTableHeaders = extractCvssDetailsTableHeaders(displayVectors); if (!displayUsage) { cvssTableHeaders.remove("Usage"); } final TableTag table = table().withClass("basic-table").with( tr().with( each(cvssTableHeaders, header -> th(header)) ) ); for (Map.Entry> cvssEntry : displayVectors.entrySet()) { final CvssVector vector = cvssEntry.getKey(); final List tag = cvssEntry.getValue(); table.with(createCvssDetailsTableRowForCvss(vector, tag, cvssTableHeaders, selectedCvssVectors, vulnerability)); } if (displayVectors.isEmpty()) { return null; } return table; } private SpanTag createCvssVectorSourceLinks(List sources) { final SpanTag span = span(); /*final CvssSource firstSource = sources.iterator().next(); try { final String vectorVersion = CvssVector.getVersionName(firstSource.getVectorClass()); span.withText(vectorVersion + " "); } catch (Exception e) { span.withText("UNKNOWN "); }*/ boolean hadAssessment = false; boolean isFirst = true; for (CvssSource source : sources) { if (!isFirst) { span.withText(" + "); } else { isFirst = false; } if (source.getHostingEntity() == KnownCvssEntities.ASSESSMENT) { if (!hadAssessment) { hadAssessment = true; span.with(text("Assessment")); } continue; } final String hostingEntityUrl = getCvssSourceLink(source.getHostingEntity()); if (hostingEntityUrl != null) { span.with(makeHTMLLink(escapeCvssSourceName(source.getHostingEntity().getName()), hostingEntityUrl, HrefTargets.TARGET_BLANK.getTarget()).withClass("preserve-color")); } else { span.withText(escapeCvssSourceName(source.getHostingEntity().getName())); } if (source.getIssuingEntityRole() != null) { span.with(text("-" + escapeCvssSourceName(source.getIssuingEntityRole().getName()))); } if (source.getIssuingEntity() != null) { final String issuingEntityUrl = getCvssSourceLink(source.getIssuingEntity()); if (issuingEntityUrl != null) { span.with(text("-")); span.with(makeHTMLLink(escapeCvssSourceName(source.getIssuingEntity().getName()), issuingEntityUrl, HrefTargets.TARGET_BLANK.getTarget()).withClass("preserve-color")); } else { span.with(text("-" + escapeCvssSourceName(source.getIssuingEntity().getName()))); } } } return span; } private String getCvssSourceLink(CvssSource.CvssEntity cvssEntity) { if (cvssEntity == null) { return null; } else if (cvssEntity.getUrl() != null) { return cvssEntity.getUrl().toString(); } else if (cvssEntity.getEmail() != null) { return "mailto:" + cvssEntity.getEmail(); } return null; } private static String escapeCvssSourceName(String name) { return name .replace("_", "\\_") .replace("-", "_"); } private void addVectorIfNotPresent(Map> vectors, CvssVector vector) { addVectorIfNotPresent(vectors, vector, null); } private void addVectorIfNotPresent(Map> vectors, CvssVector vector, DomContent tag) { if (vector == null) { return; } final Optional found = vectors.keySet().stream().filter(v -> v.equals(vector) && Objects.equals(CvssSource.toCombinedColumnHeaderString(v.getCvssSources()), CvssSource.toCombinedColumnHeaderString(vector.getCvssSources()))).findFirst(); if (found.isPresent()) { if (tag != null) { vectors.computeIfPresent(found.get(), (v, t) -> { t.add(tag); return t; }); } return; } final List created = vectors.computeIfAbsent(vector, v -> new ArrayList<>()); if (tag != null) { created.add(tag); } } private void checkIfAdvisorHasBeenReviewedAndAppendCard(SheetParagraph paragraph, AdvisoryEntry advisoryEntry, VulnerabilityStatus vulnerabilityStatus, AtomicInteger amountReviewedEntries, ContainerTag linkTag) { final VulnerabilityStatusReviewedEntry reviewedEntry = VulnerabilityStatusReviewedEntry.findReviewedEntry(advisoryEntry.getId(), vulnerabilityStatus.getReviewedAdvisories()); final boolean isAdvisoryReviewed = reviewedEntry != null; if (isAdvisoryReviewed) { if (linkTag != null) { markAdvisorTitleAsReviewed(linkTag); } amountReviewedEntries.getAndIncrement(); } if (isAdvisoryReviewed && StringUtils.hasText(reviewedEntry.getComment())) { paragraph.with(new ContentCard() .withTitle("Review Comment") .withText(reviewedEntry.getComment()) .withType(ContentCard.ContentCardType.GREEN) .generate() ); } } private void markAdvisorTitleAsReviewed(ContainerTag title) { title.withStyle("color:var(--strong-dark-green);").with( rawHtml("\n" + " " + "") // the double blank is required since rawHtml removes a single blank in front of '.' full stops. ); } private SheetParagraph createParagraphReferences(Collection references) { final SheetParagraph referencesParagraph = new SheetParagraph(); referencesParagraph.setIdentifier("References"); referencesParagraph.setTitle("References"); final UlTag referencesList = ul(); for (Reference reference : references) { referencesList.with(li( reference.toHtmlATag().withTarget(HrefTargets.TARGET_REFERENCE.getTarget()) )); } referencesParagraph.with(referencesList); return referencesParagraph; } /* -- END: SHEET PARAGRAPH GENERATION -- */ /* -- START: MODAL GENERATION -- */ private Modal generateOverviewModal(List charts) { final DivTag overviewModalChartsRow1 = div().withClass("unselectable").withStyle("width: 100%; text-align: center;"); final DivTag overviewModalChartsRow2 = div().withClass("unselectable").withStyle("width: 100%; text-align: center;"); for (int i = 0; i < charts.size(); i++) { final GeneratedChart chart = charts.get(i); final UnescapedText chartContent = chart.generateChartJsChart(); if (i < 3) { overviewModalChartsRow1.with(chartContent); } else { overviewModalChartsRow2.with(chartContent); } } return new Modal() .setId("overview-modal") .setTitle("Vulnerability Assessment Status") .setToggleKey("KeyO") .setToggleKeyActive(true) .setShowInSidebar(true) .setSvgIcon(SvgIcon.PIE_CHART_FILL) .with( // overviewModalStatisticsTable, overviewModalChartsRow1, overviewModalChartsRow2, span("Highlighted charts have been updated according to the applied filters") .withId("overview-charts-are-filtered-hint") .withStyle("margin-top:10px;") .withClasses("hidden", "badge", "badge-secondary") ); } private Modal generateAssessmentEditorModal() { final DivTag assessmentEditorModalContent = div().withId("assessment-editor-modal-content"); assessmentEditorModalContent // affects section with matching criteria .with(h3("Affects")) .with(div("Inventory scope assessments do not contain an 'affects' section.").withId("assessment-editor-inventory-scope-selected").withClass("hidden")) .with(div().withId("assessment-editor-affects-add-elements-container") .with( div().withClasses("matching-criteria-badge", "inapplicable").with(span("CPE"), span(input().withId("assessment-editor-affects-cpe-input").withPlaceholder("cpe:/a:").attr("size", 20))), div().withClasses("matching-criteria-badge", "inapplicable").with(span("Vulnerability"), span(input().withId("assessment-editor-affects-vulnerability-input").withPlaceholder("CVE-2021-1234").attr("size", 12))), div().withClasses("matching-criteria-badge", "inapplicable").with(span("CWE"), span(input().withId("assessment-editor-affects-cwe-input").withPlaceholder("CWE-123").attr("size", 8))), div().withClasses("matching-criteria-badge", "inapplicable").with(span("Condition"), span(input().withId("assessment-editor-affects-condition-input").withPlaceholder("").attr("size", 20))) ) ) .with(div().withId("assessment-editor-affects-container")) // general data section with scope, title, cvss vectors, reported and accepted .with(h3("General Data")) .with(div().withClasses("assessment-editor-data-attribute-container") .with( div().with(span("Scope"), span(select().withId("assessment-editor-scope-select").with( option("Artifact").withValue("artifact"), option("Inventory").withValue("inventory") ))), div().with(span("Title"), span(input().withId("assessment-editor-title-input").withPlaceholder("Title").attr("size", "50"))), div().with(span("Reported"), span( input().withId("assessment-editor-reported-by-input").withStyle("margin-right:3px;").withPlaceholder("Author").attr("size", "25"), input().withId("assessment-editor-reported-date-input").withPlaceholder(TimeUtils.formatNormalizedDate(new Date()).replaceAll(" \\d+:\\d+:\\d+", "")).attr("size", "22")), div().with(span("Accepted"), span( input().withId("assessment-editor-accepted-by-input").withStyle("margin-right:3px;").withPlaceholder("Author").attr("size", "25"), input().withId("assessment-editor-accepted-date-input").withPlaceholder(TimeUtils.formatNormalizedDate(new Date())).attr("size", "22"))) ), div().with(span("CVSS 2.0"), span( input().withId("assessment-editor-cvss-vector-2.0-all-input").withPlaceholder("all").withStyle("margin-right:3px;").attr("size", "15"), input().withId("assessment-editor-cvss-vector-2.0-lower-input").withPlaceholder("lower").withStyle("margin-right:3px;").attr("size", "14"), input().withId("assessment-editor-cvss-vector-2.0-higher-input").withPlaceholder("higher").attr("size", "15") )), div().with(span("CVSS 3.1"), span( input().withId("assessment-editor-cvss-vector-3.1-all-input").withPlaceholder("all").withStyle("margin-right:3px;").attr("size", "15"), input().withId("assessment-editor-cvss-vector-3.1-lower-input").withPlaceholder("lower").withStyle("margin-right:3px;").attr("size", "14"), input().withId("assessment-editor-cvss-vector-3.1-higher-input").withPlaceholder("higher").attr("size", "15") )), div().with(span("CVSS 4.0"), span( input().withId("assessment-editor-cvss-vector-4.0-all-input").withPlaceholder("all").withStyle("margin-right:3px;").attr("size", "15"), input().withId("assessment-editor-cvss-vector-4.0-lower-input").withPlaceholder("lower").withStyle("margin-right:3px;").attr("size", "14"), input().withId("assessment-editor-cvss-vector-4.0-higher-input").withPlaceholder("higher").attr("size", "15") )) ) ) // the following "Assessment" block is almost just like the affects section above .with(h3("Assessment").withStyle("margin-top:15px;")) .with(div().withId("assessment-editor-assessments-container").with( span().with( button("New").withId("assessment-editor-assessment-add-new-button").withClasses("btn", "btn-primary", "btn-sm").attr("type", "button").withStyle("margin-right:15px;"), span().withId("assessment-editor-assessment-add-button-container").with() ) )) .with(div().withId("assessment-editor-assessment-add-elements-container").withClasses("assessment-editor-data-attribute-container", "hidden") .with( div().with(span("Status"), span(select().withId("assessment-editor-assessment-status-select").with( option("Applicable").withValue("applicable"), option("Not Applicable").withValue("not applicable"), option("Insignificant").withValue("insignificant"), option("Void").withValue("void"), option("None (No status)").withValue("none") ))), div().with(span("Rationale"), span(textarea().withId("assessment-editor-assessment-rationale-input").withPlaceholder("Rationale").attr("rows", "1").attr("cols", "50"))), div().with(span("Risk"), span(textarea().withId("assessment-editor-assessment-risk-input").withPlaceholder("Risk").attr("rows", "1").attr("cols", "50"))), div().with(span("Measures"), span(textarea().withId("assessment-editor-assessment-measures-input").withPlaceholder("Measures").attr("rows", "1").attr("cols", "50"))), div().with(span("Score"), span(input().withId("assessment-editor-assessment-score-input").withPlaceholder("Score").attr("size", "50"))), div().with(span("Created"), span( input().withId("assessment-editor-assessment-author-input").withStyle("margin-right:3px;").withPlaceholder("Author").attr("size", "25"), input().withId("assessment-editor-assessment-date-input").withPlaceholder(TimeUtils.formatNormalizedDate(new Date())).attr("size", "22"))), div().with(span("Labels"), span( input().withId("assessment-editor-assessment-labels-include-input").withStyle("margin-right:3px;").withPlaceholder("include, label").attr("size", "25"), input().withId("assessment-editor-assessment-labels-exclude-input").withPlaceholder("exclude, label").attr("size", "22"))), div().with( button("Remove").withId("assessment-editor-assessment-remove-button").withClasses("btn", "btn-danger", "btn-sm").attr("type", "button").withStyle("margin-top:15px;") ) ) ) // code block output for YAML where the JS will generate the YAML into .with(h3().withStyle("margin-top:35px;").with(text("Generated Assessment File "), span().withId("assessment-editor-yaml-output-copy").withClasses("badge", "badge-secondary", "hidden").withStyle("margin-left:5px;").with(text("Copied!")))) .with(textarea().withId("assessment-editor-yaml-output").withClasses("yaml-output")) .with(script("document.getElementById('assessment-editor-yaml-output').addEventListener('focus', function() { copyToClipboard(document.getElementById('assessment-editor-yaml-output').value);" + "document.getElementById('assessment-editor-yaml-output-copy').classList.remove('hidden');" + "setTimeout(function() { document.getElementById('assessment-editor-yaml-output-copy').classList.add('hidden'); }, 1500); });")) ; return new Modal() .setId("assessment-editor-modal") .setTitle("Assessment Editor (beta)") .setToggleKeyActive(false) .setShowInSidebar(false) .setSvgIcon(SvgIcon.PENCIL_FILL) .withOnOpenScript("clearAssessmentEditorModal();") .with( assessmentEditorModalContent ); } private void addAdditionalInventoryInformation(Inventory inventory, Dashboard dashboard) { contributeInventoryAssessmentAdditionalInformation(inventory, dashboard); contributeSecurityPolicyAdditionalInformation(dashboard); } private void contributeSecurityPolicyAdditionalInformation(Dashboard dashboard) { final CentralSecurityPolicyConfiguration securityPolicy = super.getSecurityPolicyConfiguration(); final SpanTag securityPolicyExplanation = span(); { final String providedVectorsSelectorExplanation = securityPolicy.getInitialCvssSelector().explain("Initial Vectors"); final String effectiveVectorsSelectorExplanation = securityPolicy.getContextCvssSelector().explain("Context Vectors"); final SpanTag providedVectorsSelectorExplanationHtml = convertCvssSelectorExplanationToHtml(providedVectorsSelectorExplanation); final SpanTag effectiveVectorsSelectorExplanationHtml = convertCvssSelectorExplanationToHtml(effectiveVectorsSelectorExplanation); dashboard.addAdditionalInformationModalContent(h2("Security Policy"), securityPolicyExplanation); securityPolicyExplanation.with( text("The security policy used to generate the Vulnerability Assessment Dashboard has been configured as shown below."), br(), br() ); securityPolicyExplanation.with( div( h3("CVSS Selector for Provided Vectors"), text("The following CVSS selector is evaluated to find one representative CVSS vector from the unmodified vectors provided from the data sources per CVSS version."), br(), providedVectorsSelectorExplanationHtml ).withClass("left-bracket-container"), div( h3("CVSS Selector for Effective Vectors"), text("The following CVSS selector is evaluated to find one representative CVSS vector from the effective vectors provided from the data sources per CVSS version."), br(), effectiveVectorsSelectorExplanationHtml ).withClass("left-bracket-container") ); } { final UlTag filterList = ul(); securityPolicyExplanation.with( div( h3("CVSS Version Selection Policy"), text("The versioned vectors selected by the CVSS selectors above are then further reduced to a single vector per selector using the first filter that applies from these."), filterList ).withClass("left-bracket-container") ); for (CvssSelectionResult.CvssScoreVersionSelectionPolicy versionSelector : securityPolicy.getCvssVersionSelectionPolicy()) { final String explanation; switch (versionSelector) { case V2: explanation = "A vector with the version " + Cvss2.getVersionName(); break; case V3: explanation = "A vector with the version " + Cvss3P1.getVersionName(); break; case V4: explanation = "A vector with the version " + Cvss4P0.getVersionName(); break; case LATEST: explanation = "The vector with the highest version number"; break; case OLDEST: explanation = "The vector with the lowest version number"; break; case HIGHEST: explanation = "The vector with the highest overall score"; break; case LOWEST: explanation = "The vector with the lowest overall score"; break; default: explanation = "Unspecified version selection policy"; } filterList.with(li(explanation)); } } { final CvssSeverityRanges ranges = securityPolicy.getCvssSeverityRanges(); final UlTag severityRangesList = ul(); securityPolicyExplanation.with( div( h3("Severity Ranges"), text("The following severity ranges are used to categorize the CVSS vectors into severity levels."), br(), severityRangesList ).withClass("left-bracket-container") ); for (CvssSeverityRanges.SeverityRange range : ranges.getRanges()) { severityRangesList.with(li( cvssSeverityColorBadge(range) )); } } { final double vulnerabilityInsignificantStatusThreshold = securityPolicy.getInsignificantThreshold(); // final CvssSeverityRanges.SeverityRange insignificantRangeEqual = securityPolicy.getCvssSeverityRanges().getRange(vulnerabilityInsignificantStatusThreshold); // see comment below final CvssSeverityRanges.SeverityRange insignificantRangeBelow = securityPolicy.getCvssSeverityRanges().getRange(vulnerabilityInsignificantStatusThreshold - 0.1); // final String vulnerabilityStatusMapperName = securityPolicy.getVulnerabilityStatusDisplayMapper().getName(); // not displayed in the moment final double vulnerabilityIncludeScoreThreshold = securityPolicy.getIncludeScoreThreshold(); final List> includeVulnerabilitiesWithAdvisoryProviders = AdvisoryTypeStore.get().fromJsonNamesAndImplementations(securityPolicy.getIncludeVulnerabilitiesWithAdvisoryProviders()); final List includeVulnerabilitiesWithAdvisoryReviewStatus = securityPolicy.getIncludeVulnerabilitiesWithAdvisoryReviewStatus(); final DivTag vulnerabilityConfigExplanation = div(); if (vulnerabilityInsignificantStatusThreshold > 0) { vulnerabilityConfigExplanation.with( div( text("A vulnerability is mapped to the insignificant status if it is \"in review\" and has a CVSS overall score "), // the 'insignificant' behaviour has been changed for gen 3: (context, initial) <= threshold --> (context, initial) < threshold // cvssSeverityColorBadge("equals to " + vulnerabilityInsignificantStatusThreshold, insignificantRangeEqual), // text(" or "), cvssSeverityColorBadge("below " + vulnerabilityInsignificantStatusThreshold, insignificantRangeBelow), text(".") ) ); } if (vulnerabilityIncludeScoreThreshold > 0) { final CvssSeverityRanges.SeverityRange includeRangeEqual = securityPolicy.getCvssSeverityRanges().getRange(vulnerabilityIncludeScoreThreshold); vulnerabilityConfigExplanation.with( div( text("A vulnerability is excluded if it has a CVSS overall score "), cvssSeverityColorBadge("below " + vulnerabilityIncludeScoreThreshold, includeRangeEqual), text(".") ) ); } if (!includeVulnerabilitiesWithAdvisoryProviders.isEmpty() && includeVulnerabilitiesWithAdvisoryProviders.stream().noneMatch(source -> source == AdvisoryTypeStore.ANY_ADVISORY_FILTER_WILDCARD)) { vulnerabilityConfigExplanation.with( div( text("A vulnerability is excluded if it does not contain a security advisory from the following providers: "), text(includeVulnerabilitiesWithAdvisoryProviders.stream().map(ContentIdentifierStore.ContentIdentifier::getWellFormedName).collect(Collectors.joining(", "))) ) ); } if (!includeVulnerabilitiesWithAdvisoryReviewStatus.isEmpty() && !includeVulnerabilitiesWithAdvisoryReviewStatus.contains("ALL") && !includeVulnerabilitiesWithAdvisoryReviewStatus.contains("all") && !includeVulnerabilitiesWithAdvisoryReviewStatus.contains("ANY") && !includeVulnerabilitiesWithAdvisoryReviewStatus.contains("any")) { vulnerabilityConfigExplanation.with( div( text("A vulnerability is excluded if it does not reference a security advisory that has a review status of: "), text(String.join(", ", includeVulnerabilitiesWithAdvisoryReviewStatus)) ) ); } if (vulnerabilityConfigExplanation.getNumChildren() > 0) { securityPolicyExplanation.with( div( h3("Vulnerabilities"), vulnerabilityConfigExplanation ).withClass("left-bracket-container") ); } } { final List includeAdvisoryTypes = securityPolicy.getIncludeAdvisoryTypes(); final DivTag advisoryConfigExplanation = div(); if (!includeAdvisoryTypes.isEmpty() && !includeAdvisoryTypes.contains("ALL") && !includeAdvisoryTypes.contains("all") && !includeAdvisoryTypes.contains("ANY") && !includeAdvisoryTypes.contains("any")) { advisoryConfigExplanation.with( div( text("Security advisories are excluded if they are not of one of the following types: "), text(String.join(", ", includeAdvisoryTypes)) ) ); } if (advisoryConfigExplanation.getNumChildren() > 0) { securityPolicyExplanation.with( div( h3("Security Advisories"), advisoryConfigExplanation ).withClass("left-bracket-container") ); } } { // priority section final VulnerabilityPriorityScoreConfiguration pConf = super.getSecurityPolicyConfiguration().getPriorityScoreConfiguration(); VulnerabilityPriorityScoreConfiguration.EolConfiguration.NoExtendedSupportConfiguration eolNoExtended = pConf.getEol().getNoExtendedSupport(); VulnerabilityPriorityScoreConfiguration.EolConfiguration.ExtendedSupportConfiguration eolExtended = pConf.getEol().getExtendedSupport(); securityPolicyExplanation.with(div( h3("Priority Score"), text("The priority score of a vulnerability is determined by calculating the sum of the following metrics:"), ul( li("CVSS Overall: Either the Context CVSS, or as a fallback the Initial CVSS vector is used to calculate the base priority score the other metrics are added to."), li("Keywords: The sum of all matched keyword set scores."), li(text("EPSS: The exploit probability "), code("p"), text(" of the vulnerability and the parameters "), code("min=" + pConf.getEpss().getMin()), text(", "), code("f=" + pConf.getEpss().getf()), text(" and "), code("F=" + pConf.getEpss().getF()), text(" are used in the following formula only if "), code("p>=min"), text(", otherwise 0 is used: "), code(VulnerabilityPriorityCalculator.PriorityScoreResult.getEpssFormula()), br(), // f + ((p - min) / (1.0 - min)) * (F - f) text("This formula represents a truncated linear function between "), code("(min, f)"), text(" and "), code("(1, F)"), text(" with a minimum value of "), code("f"), text(" and a maximum value of "), code("F"), text(".") ), li(text("KEV: If an exploit is known on a vulnerability, "), code("exploit=" + pConf.getKev().getExploit()), text(" is added to the priority score. If additionally a use in a ransomware campaign is known, "), code("ransomware=" + pConf.getKev().getRansomware()), text(" is added.")), li(text("EOL: The end-of-life state of the affected product is evaluated. The priority score is increased by the value of the worst-case state, for all affected components."), br(), text("If the product does not provide extended support, the following values are used: "), code("supportValid=" + eolNoExtended.getSupportValid()), text(", "), code("supportEndingSoon=" + eolNoExtended.getSupportEndingSoon()), text(", "), code("supportExpired=" + eolNoExtended.getSupportExpired()), br(), text("If the product provides extended support, the following values are used: "), code("supportValid=" + eolExtended.getSupportValid()), text(", "), code("supportEndingSoon=" + eolExtended.getSupportEndingSoon()), text(", "), code("extendedSupportValid=" + eolExtended.getExtendedSupportValid()), text(", "), code("extendedSupportEndingSoon=" + eolExtended.getExtendedSupportEndingSoon()), text(", "), code("extendedSupportExpired=" + eolExtended.getExtendedSupportExpired())) ) ).withClass("left-bracket-container") ); } { // priority severity categories final CvssSeverityRanges priorityScoreSeverityRanges = super.getSecurityPolicyConfiguration().getPriorityScoreSeverityRanges(); final UlTag priorityScoreSeverityRangesList = ul(); securityPolicyExplanation.with( div( h3("Priority Score Severity Ranges"), text("The following severity ranges are used to categorize the priority score into severity levels, only if there is an elevation from the base cvss overall score through any of the other metrics."), br(), priorityScoreSeverityRangesList ).withClass("left-bracket-container") ); for (CvssSeverityRanges.SeverityRange range : priorityScoreSeverityRanges.getRanges()) { priorityScoreSeverityRangesList.with(li( cvssSeverityColorBadge(range) )); } } } private SpanTag cvssSeverityColorBadge(String text, CvssSeverityRanges.SeverityRange range) { return span(text).withClass("badge").withStyle("background-color:" + range.getColor().toHex() + ";color: " + range.getColor().getHexTextColor()); } private SpanTag cvssSeverityColorBadge(CvssSeverityRanges.SeverityRange range) { final String name = range.getName(); final String floor = range.getFloor() < -5000 ? "-∞" : String.valueOf(range.getFloor()); final String ceil = range.getCeil() > 5000 ? "∞" : String.valueOf(range.getCeil()); return cvssSeverityColorBadge(name + " (" + floor + " - " + ceil + ")", range); } private SpanTag convertCvssSelectorExplanationToHtml(String explanation) { final String[] lines = explanation.split("\n"); final SpanTag span = span(); // only one layer of unordered lists UlTag currentList = null; LiTag currentListItem = null; for (int i = 0; i < lines.length; i++) { final String line = lines[i]; if (line.startsWith("- ") && currentList == null) { currentList = ul(); span.with(currentList); currentListItem = li(); currentList.with(currentListItem); currentListItem.with(formatCvssSelectorExplanationLineContentToHtml(line.substring(2))); } else if (line.startsWith("- ") && currentList != null) { currentListItem = li(); currentList.with(currentListItem); currentListItem.with(formatCvssSelectorExplanationLineContentToHtml(line.substring(2))); } else if (line.startsWith(" ") && currentList != null) { if (currentListItem.getNumChildren() > 0) { currentListItem.with(br()); } currentListItem.with(formatCvssSelectorExplanationLineContentToHtml(line.substring(2))); } else if (!line.startsWith(" ")) { final boolean previousWasList = currentList != null; currentList = null; currentListItem = null; if (!line.isEmpty()) { span.with(formatCvssSelectorExplanationLineContentToHtml(line)); } if (!previousWasList && i < lines.length - 1) { span.with(br()); } } } return span; } private SpanTag formatCvssSelectorExplanationLineContentToHtml(String lineContent) { // The [Provided Vectors] CVSS Selector contains [1] rule that will be applied in the following order: // The [Provided Vectors] CVSS Selector contains [1] rule that will be applied in the following order: final SpanTag span = span(); final StringBuilder currentSegment = new StringBuilder(); boolean isInsideTag = false; for (int i = 0; i < lineContent.length(); i++) { final char c = lineContent.charAt(i); if (c == '[' && !isInsideTag) { isInsideTag = true; if (currentSegment.length() > 0) { span.with(text(currentSegment.toString())); currentSegment.setLength(0); } } else if (c == ']' && isInsideTag) { isInsideTag = false; final String tagContent = currentSegment.toString(); span.with(b("[" + tagContent + "]").withClass("monospace")); currentSegment.setLength(0); } else { currentSegment.append(c); } } if (currentSegment.length() > 0) { span.with(text(currentSegment.toString())); } return span; } private void contributeInventoryAssessmentAdditionalInformation(Inventory inventory, Dashboard dashboard) { final InventoryInfo statusInformation = inventory.findInventoryInfo(InventoryEnricher.INVENTORY_INFO_VULNERABILITY_STATUS_KEY); if (statusInformation != null) { final JSONArray vulnerabilityStatusJson = new JSONArray(statusInformation.get(InventoryEnricher.INVENTORY_INFO_VULNERABILITY_STATUS_INVENTORY_STATUSES_KEY)); final List statuses = VulnerabilityStatusConverter.fromJson(vulnerabilityStatusJson); if (!statuses.isEmpty()) { dashboard.addAdditionalInformationModalContent(h3("Inventory Vulnerability Status"), text("This status information is applied to all vulnerabilities in the inventory.")); for (VulnerabilityStatus status : statuses) { final DivTag statusCard = new DivTag(); if (!status.getStatusHistory().isEmpty()) { statusCard.with(createVulnerabilityStatusHistoryEntry(status.getStatusHistory().get(0), true)); for (int i = 1; i < status.getStatusHistory().size(); i++) { statusCard.with(createVulnerabilityStatusHistoryEntry(status.getStatusHistory().get(i), true)); } } if (status.getTitle() != null) { statusCard.with(createSlightHighlightText("Title: "), text(status.getTitle()), br()); } if (status.hasCvss2()) { statusCard.with(createSlightHighlightText("CVSS 2.0: "), text(status.getCvss2().toString()), br()); } if (status.hasCvss2Lower()) { statusCard.with(createSlightHighlightText("CVSS 2.0 (lower): "), text(status.getCvss2Lower().toString()), br()); } if (status.hasCvss2Higher()) { statusCard.with(createSlightHighlightText("CVSS 2.0 (higher): "), text(status.getCvss2Higher().toString()), br()); } if (status.hasCvss3P1()) { statusCard.with(createSlightHighlightText("CVSS 3.1: "), text(status.getCvss3P1().toString()), br()); } if (status.hasCvss3P1Lower()) { statusCard.with(createSlightHighlightText("CVSS 3.1 (lower): "), text(status.getCvss3P1Lower().toString()), br()); } if (status.hasCvss3P1Higher()) { statusCard.with(createSlightHighlightText("CVSS 3.1 (higher): "), text(status.getCvss3P1Higher().toString()), br()); } if (status.hasCvss4()) { statusCard.with(createSlightHighlightText("CVSS 4.0: "), text(status.getCvss4().toString()), br()); } if (status.hasCvss4Lower()) { statusCard.with(createSlightHighlightText("CVSS 4.0 (lower): "), text(status.getCvss4Lower().toString()), br()); } if (status.hasCvss4Higher()) { statusCard.with(createSlightHighlightText("CVSS 4.0 (higher): "), text(status.getCvss4Higher().toString()), br()); } if (status.hasReportedByInformation()) { statusCard.with(createSlightHighlightText("Reported by: "), text(status.generateReportedByDateString()), br()); } if (status.hasAcceptedByInformation()) { statusCard.with(createSlightHighlightText("Accepted by: "), text(status.generateAcceptedByDateString()), br()); } if (status.getReviewedAdvisories() != null && !status.getReviewedAdvisories().isEmpty()) { statusCard.with(createSlightHighlightText("Reviewed advisories: ")); for (VulnerabilityStatusReviewedEntry advisory : status.getReviewedAdvisories()) { statusCard.with(makeBadge(advisory.getAdvisor() + ": " + advisory.getId(), "primary")); } statusCard.with(br()); } statusCard.withClass("left-bracket-container"); dashboard.addAdditionalInformationModalContent(statusCard); } } } } /* -- END: MODAL GENERATION -- */ private DomContent createArtifactTableEntryForOptionallyEmpty(String value) { if (value != null && !value.equals("*") && !value.equals("-") && !value.equals("unspecific") && !value.equals("")) { return text(value); } else { return span("n.a.").withClass("cpe-missing-data"); } } private String plural(int size) { return size == 1 ? "" : "s"; } private static String makeJsVar(String text, Random random) { return (text + random.nextDouble()) .replace("-", "_") .replace(".", "_") .replaceAll("[^A-Za-z0-9]", ""); } /* -- START: VULNERABILITY TIMELINES CREATION -- */ private void findVendorProductsOnArtifactsForVulnerabilityTimeline(Set artifacts, List affectedCpes, Set> allVendorProducts, Map>> vendorProductsPerArtifact) { for (Artifact artifact : artifacts) { final List cpeUrisOnArtifact = CommonEnumerationUtil.parseEffectiveCpe(artifact); final List> vendorProductsOnArtifact = CommonEnumerationUtil.getVendorProducts(cpeUrisOnArtifact); final Set> relevantVendorProductsOnArtifact = vendorProductsOnArtifact.stream() .filter(o -> affectedCpes.stream().anyMatch(cpe -> o.getLeft().equals(cpe.getCpe().getVendor()) && o.getRight().equals(cpe.getCpe().getProduct()))) .sorted(Comparator.comparing(Pair::getLeft)) .collect(Collectors.toCollection(LinkedHashSet::new)); // if none of the CPE URIs are affected by the current vulnerability, add all to at least try to find vulnerability timelines if (relevantVendorProductsOnArtifact.size() == 0) { relevantVendorProductsOnArtifact.addAll(vendorProductsOnArtifact); } relevantVendorProductsOnArtifact.removeIf(o -> allVendorProducts.stream().anyMatch(e -> e.getLeft().equals(o.getLeft()) && e.getRight().equals(o.getRight()))); allVendorProducts.addAll(relevantVendorProductsOnArtifact); vendorProductsPerArtifact.put(artifact, vendorProductsOnArtifact); final boolean hasTooManyCPEs = configuration.getMaximumCpeForTimelinesPerVulnerability() != -1 && allVendorProducts.size() > configuration.getMaximumCpeForTimelinesPerVulnerability(); if (hasTooManyCPEs) { AtomicInteger limitCount = new AtomicInteger(); allVendorProducts.removeIf(o -> limitCount.incrementAndGet() > configuration.getMaximumCpeForTimelinesPerVulnerability()); LOG.warn("Stopped generation of vulnerability timelines, configured limit of [{}] CPEs per vulnerability reached. Consider providing initial CPE URIs.", configuration.getMaximumCpeForTimelinesPerVulnerability()); break; } } } /* -- END: VULNERABILITY TIMELINES CREATION -- */ /* -- START: CVSS CHART/TABLE GENERATION -- */ public static GeneratedChart createCombinedCvssChart(Cvss2 cvss2, Cvss3P1 cvss3, Cvss4P0 cvss4, boolean isModified, String vulnerabilityName, VulnerabilityAssessmentDashboardEnrichmentConfiguration.VulnerabilityCvssSvgChartInterpolationMethod interpolationMethod) { final RadarChartData cvssChartData = new RadarChartData(); cvssChartData.addLabels("Base", "Adj. Impact", "Impact", "Temporal", "Exploitability", "Environmental"); // check if vectors are defined and create dataset for each. display modified scores in any case. if (cvss4 != null) { final RadarChartDataset cvssChartCvss4Dataset = createCvssRadarChartDataset(cvss4, ColorScheme.CVSS_4.getColor(), "v4.0", true, interpolationMethod); cvssChartData.addDataset(cvssChartCvss4Dataset); } if (cvss3 != null) { final RadarChartDataset cvssChartCvss3P1Dataset = createCvssRadarChartDataset(cvss3, ColorScheme.CVSS_3.getColor(), "v3.1", true, interpolationMethod); cvssChartData.addDataset(cvssChartCvss3P1Dataset); } if (cvss2 != null) { final RadarChartDataset cvssChartCvss2Dataset = createCvssRadarChartDataset(cvss2, ColorScheme.CVSS_2.getColor(), "v2.0", true, interpolationMethod); cvssChartData.addDataset(cvssChartCvss2Dataset); } final ChartOptions cvssChartOptions = new ChartOptions() .setInteraction(new InteractionOption().setMode("index")) .setResponsive(true) .setMaintainAspectRatio(false) .addScale("r", new RadialScaleOption() .setBeginAtZero(true) .setMax(10) .setTicks(new RadialScaleTicksOption().setStepSize(2)) ) .setTitle(new TitleOption().setText((isModified ? "Context" : "Initial") + " CVSS Scores").setDisplay(true)); final RadarChart cvssChart = new RadarChart() .setChartData(cvssChartData) .setChartOptions(cvssChartOptions); // generate the same chart as svg final DefaultCategoryDataset cvssDataset = new DefaultCategoryDataset(); final Map versionToColorMap = new HashMap<>(); for (int i = cvssChartData.getDatasets().size() - 1; i >= 0; i--) { RadarChartDataset currentDataset = (RadarChartDataset) cvssChartData.getDatasets().get(i); // determine the CVSS version for the row label final String rowLabel; if (StringUtils.hasText(currentDataset.getLabel())) { rowLabel = currentDataset.getLabel(); if (rowLabel.contains("v2")) { versionToColorMap.put(rowLabel, ColorScheme.CVSS_2.getColor()); } else if (rowLabel.contains("v3")) { versionToColorMap.put(rowLabel, ColorScheme.CVSS_3.getColor()); } else if (rowLabel.contains("v4")) { versionToColorMap.put(rowLabel, ColorScheme.CVSS_4.getColor()); } else { versionToColorMap.put(rowLabel, ColorScheme.CVSS_2.getColor()); } } else if (cvss4 != null && i == 2) { // legacy conversion methods, I'm not sure if they are still required, but it can't hurt for now. rowLabel = Cvss4P0.getVersionName(); versionToColorMap.put(rowLabel, ColorScheme.CVSS_4.getColor()); } else if (cvss3 != null && i == 1) { rowLabel = Cvss3P1.getVersionName(); versionToColorMap.put(rowLabel, ColorScheme.CVSS_3.getColor()); } else if (cvss2 != null && i == 0) { rowLabel = Cvss2.getVersionName(); versionToColorMap.put(rowLabel, ColorScheme.CVSS_2.getColor()); } else { rowLabel = "Unknown"; } // populate the dataset for (int j = 0; j < currentDataset.getData().size(); j++) { String currentLabel = cvssChartData.getLabels().get(j); cvssDataset.addValue( currentDataset.getData().get(j), rowLabel, currentLabel ); } } final SpiderWebPlot plotModified = new SpiderWebPlot(cvssDataset); for (String version : versionToColorMap.keySet()) { plotModified.setSeriesPaint(cvssDataset.getRowIndex(version), versionToColorMap.get(version)); } plotModified.setToolTipGenerator(new StandardCategoryToolTipGenerator()); plotModified.setInsets(new RectangleInsets(0, 0, 0, 0)); plotModified.setMaxValue(10); plotModified.setNoDataMessage("No Cvss Data available"); final JFreeChart svgChart = new JFreeChart("", plotModified); svgChart.setBackgroundPaint(new ChartColor(255, 255, 255)); svgChart.setPadding(new RectangleInsets(0, 0, 0, 0)); return new GeneratedChartBuilder() .setjFreeChart(svgChart) .setjFreeChartName(isModified ? "cvss_modified_" + vulnerabilityName : "cvss_unmodified_" + vulnerabilityName) .setChartJsChart(cvssChart) .setChartJsChartId("cvssChart" + makeJsVar(vulnerabilityName, new Random())) .setCanvasOrParentTag(div().withClasses("cvssChart-container", isModified ? "modified" : "unmodified")) .setCharJsChartJsVarName("cvssChart") .createGeneratedChart(); } private static RadarChartDataset createCvssRadarChartDataset(CvssVector cvss, Color color, String label, boolean includeModified, VulnerabilityAssessmentDashboardEnrichmentConfiguration.VulnerabilityCvssSvgChartInterpolationMethod interpolationMethod) { final RadarChartDataset cvssChartDataset = new RadarChartDataset() .setLabel(label) .setBackgroundColor(ColorScheme.setOpacity(color, 0.2f)) .setBorderColor(color) .addPointBackgroundColor(color) .addPointBorderColor(Color.WHITE); final BakedCvssVectorScores bakedScores = cvss.getBakedScores(); if (includeModified) { final double baseScore = bakedScores.getNormalizedBaseScore(); final double impactScore = getEffectiveCvss4ScoreForUnknownScore(cvss, bakedScores.getNormalizedImpactScore(), cvss.getBaseScore()); final double exploitabilityScore = getEffectiveCvss4ScoreForUnknownScore(cvss, bakedScores.getNormalizedExploitabilityScore(), cvss.getBaseScore()); double adjustedImpactScore = getEffectiveCvss4ScoreForUnknownScore(cvss, bakedScores.getNormalizedAdjustedImpactScore(), Double.NaN); double temporalScore = getEffectiveCvss4ScoreForUnknownScore(cvss, bakedScores.getNormalizedTemporalScore(), Double.NaN); double environmentalScore = getEffectiveCvss4ScoreForUnknownScore(cvss, bakedScores.getNormalizedEnvironmentalScore(), Double.NaN); if (interpolationMethod == VulnerabilityAssessmentDashboardEnrichmentConfiguration.VulnerabilityCvssSvgChartInterpolationMethod.BASE_METRICS) { cvssChartDataset.addPointRadius(3); if (Double.isNaN(adjustedImpactScore) && !Double.isNaN(impactScore)) { adjustedImpactScore = impactScore; cvssChartDataset.addPointRadius(0); } else { cvssChartDataset.addPointRadius(3); } cvssChartDataset.addPointRadius(3); if (Double.isNaN(temporalScore) && !Double.isNaN(baseScore)) { temporalScore = baseScore; cvssChartDataset.addPointRadius(0); } else { cvssChartDataset.addPointRadius(3); } cvssChartDataset.addPointRadius(3); if (Double.isNaN(environmentalScore) && !Double.isNaN(baseScore)) { environmentalScore = baseScore; cvssChartDataset.addPointRadius(0); } else { cvssChartDataset.addPointRadius(3); } } else { // LINEAR final double angleStep = 60.0; cvssChartDataset.addPointRadius(3); if (Double.isNaN(adjustedImpactScore)) { adjustedImpactScore = cvssRadarChartInterpolate(baseScore, impactScore, angleStep, angleStep + angleStep / 2); cvssChartDataset.addPointRadius(0); } else { cvssChartDataset.addPointRadius(3); } cvssChartDataset.addPointRadius(3); if (Double.isNaN(temporalScore)) { temporalScore = cvssRadarChartInterpolate(impactScore, exploitabilityScore, angleStep, -angleStep / 2); cvssChartDataset.addPointRadius(0); } else { cvssChartDataset.addPointRadius(3); } cvssChartDataset.addPointRadius(3); if (Double.isNaN(environmentalScore)) { environmentalScore = cvssRadarChartInterpolate(exploitabilityScore, baseScore, angleStep, -angleStep * 2 - angleStep / 2); cvssChartDataset.addPointRadius(0); } else { cvssChartDataset.addPointRadius(3); } } cvssChartDataset.addData( baseScore, adjustedImpactScore, impactScore, temporalScore, exploitabilityScore, environmentalScore); } else { cvssChartDataset.addData( getEffectiveCvss4ScoreForUnknownScore(cvss, bakedScores.getNormalizedBaseScore(), cvss.getBaseScore()), getEffectiveCvss4ScoreForUnknownScore(cvss, bakedScores.getNormalizedImpactScore(), cvss.getBaseScore()), getEffectiveCvss4ScoreForUnknownScore(cvss, bakedScores.getNormalizedExploitabilityScore(), cvss.getBaseScore())); } return cvssChartDataset; } private static double getEffectiveCvss4ScoreForUnknownScore(CvssVector result, double score, double v4Score) { if (result instanceof Cvss4P0 && Double.isNaN(score)) { return v4Score; } else { return score; } } /** * Calculates the average position on a radar chart to create a linear line in between two data points. * * @param value1 The value (length) of the first point (left) * @param value2 The value (length) of the second point (right) * @param angleStep The angle that every next point is rotated by * @param baseAngle The angle value1 is positioned at (should not matter what angle is set) * @return The intersection point on the center line in between the two data points that creates a linear line when * connected to the other two points. */ public static double cvssRadarChartInterpolate(double value1, double value2, double angleStep, double baseAngle) { Vector vector1 = new Vector(value1, 0).rotate(baseAngle); Vector vector2 = new Vector(value2, 0).rotate(baseAngle - 2 * angleStep); Vector averageLine = new Vector(10, 0).rotate(baseAngle - angleStep); Vector averagePosition = Vector.calculateInterceptionPoint(vector1, vector2, new Vector(0, 0), averageLine); return averagePosition.length(); } private List extractCvssDetailsTableHeaders(Map> cvssVectorsMap) { final List cvssTableHeaders = new ArrayList<>(); final Set cvssVectors = cvssVectorsMap.keySet(); final boolean anyHasTagString = cvssVectorsMap.values().stream().anyMatch(element -> element != null && !element.isEmpty()); final List nonNullVectors = cvssVectors.stream() .filter(Objects::nonNull) .collect(Collectors.toList()); // common headers if (anyHasTagString) { cvssTableHeaders.add("Usage"); } cvssTableHeaders.add("Version"); cvssTableHeaders.add("Source"); cvssTableHeaders.add("Overall score"); cvssTableHeaders.add("Base score"); for (CvssVector cvss : nonNullVectors) { // add Impact and Exploitability headers if not already added and if it's CVSS v2 or v3, since v4 doesn't have these if (cvss instanceof MultiScoreCvssVector && !cvssTableHeaders.contains("Impact")) { cvssTableHeaders.add("Impact"); } } for (CvssVector cvss : nonNullVectors) { if (cvss instanceof MultiScoreCvssVector && !cvssTableHeaders.contains("Exploitability")) { cvssTableHeaders.add("Exploitability"); } } for (CvssVector cvss : nonNullVectors) { if (cvss instanceof MultiScoreCvssVector && !cvssTableHeaders.contains("Temporal") && ((MultiScoreCvssVector) cvss).isAnyTemporalDefined()) { cvssTableHeaders.add("Temporal"); } } for (CvssVector cvss : nonNullVectors) { if (cvss instanceof MultiScoreCvssVector && !cvssTableHeaders.contains("Environmental") && ((MultiScoreCvssVector) cvss).isAnyEnvironmentalDefined()) { cvssTableHeaders.add("Environmental"); } } for (CvssVector cvss : nonNullVectors) { if (cvss instanceof MultiScoreCvssVector && !cvssTableHeaders.contains("Adjusted impact") && !Double.isNaN(cvss.getBakedScores().getAdjustedImpactScore())) { cvssTableHeaders.add("Adjusted impact"); } } cvssTableHeaders.add("Vector"); return cvssTableHeaders; } private TrTag createCvssDetailsTableRowForCvss(CvssVector cvss, List tag, List headers, CvssSelectionResult selectedCvssVectors, Vulnerability vulnerability) { final TrTag row = tr(); final String cssClass = (cvss instanceof Cvss2) ? "cvss2-text" : (cvss instanceof Cvss3P1) ? "cvss3-text" : "cvss4-text"; final String vectorVersion = cvss.getName(); final SpanTag vectorSources = cvss.getLatestSource() == null ? span("Unknown") : createCvssVectorSourceLinks(cvss.getCvssSources()); if (headers.contains("Usage")) { if (tag == null || tag.isEmpty()) { row.with(td()); } else { tag.sort(Comparator.comparing((DomContent c) -> c.render()).reversed()); row.with(td(span().with(tag))); } } final BakedCvssVectorScores bakedScores = cvss.getBakedScores(); row.with( td(span().with(b(vectorVersion).withClass(cssClass))), td(span().with(vectorSources).withClass(cssClass)), createCvssDetailsTableStyledCell(cvss.getOverallScore(), bakedScores.getNormalizedOverallScore(), bakedScores.getUnNormalizedOverallScoreMax(), bakedScores::hasNormalizedOverallScore), createCvssDetailsTableStyledCell(bakedScores.getBaseScore(), bakedScores.getNormalizedBaseScore(), bakedScores.getUnNormalizedBaseScoreMax(), bakedScores::hasNormalizedBaseScore) ); if ((cvss instanceof Cvss2) || (cvss instanceof Cvss3P1)) { row.with( createCvssDetailsTableStyledCell(bakedScores.getImpactScore(), bakedScores.getNormalizedImpactScore(), bakedScores.getUnNormalizedImpactScoreMax(), bakedScores::hasNormalizedImpactScore), createCvssDetailsTableStyledCell(bakedScores.getExploitabilityScore(), bakedScores.getNormalizedExploitabilityScore(), bakedScores.getUnNormalizedExploitabilityScoreMax(), bakedScores::hasNormalizedExploitabilityScore) ); } else { if (headers.contains("Impact")) { row.with(td()); } if (headers.contains("Exploitability")) { row.with(td()); } } if (bakedScores.isTemporalScoreAvailable()) { row.with(createCvssDetailsTableStyledCell(bakedScores.getTemporalScore(), bakedScores.getNormalizedTemporalScore(), bakedScores.getUnNormalizedTemporalScoreMax(), bakedScores::hasNormalizedTemporalScore)); } else if (headers.contains("Temporal")) { row.with(td()); } if (bakedScores.isEnvironmentalScoreAvailable()) { row.with(createCvssDetailsTableStyledCell(bakedScores.getEnvironmentalScore(), bakedScores.getNormalizedEnvironmentalScore(), bakedScores.getUnNormalizedEnvironmentalScoreMax(), bakedScores::hasNormalizedEnvironmentalScore)); } else if (headers.contains("Environmental")) { row.with(td()); } if (bakedScores.getAdjustedImpactScore() > 0.0) { row.with(createCvssDetailsTableStyledCell(bakedScores.getAdjustedImpactScore(), bakedScores.getNormalizedAdjustedImpactScore(), bakedScores.getUnNormalizedAdjustedImpactScoreMax(), bakedScores::hasNormalizedAdjustedImpactScore)); } else if (headers.contains("Adjusted impact")) { row.with(td()); } final UniversalCvssCalculatorLinkGenerator aeUniversalWebEditorLink = new UniversalCvssCalculatorLinkGenerator(); final UniversalCvssCalculatorLinkGenerator.UniversalCvssCalculatorEntry linkCvssVectorEntry = aeUniversalWebEditorLink.addVectorForVulnerability(cvss, vulnerability.getId()); if (selectedCvssVectors.isContext(cvss)) { final CvssVector initialCvss = selectedCvssVectors.findWithSourceInInitial(cvss); if (initialCvss != null) { linkCvssVectorEntry.setInitialCvssVectorUnchecked(initialCvss); aeUniversalWebEditorLink.addVectorForVulnerability(initialCvss, vulnerability.getId()); } } aeUniversalWebEditorLink.setSelectedVector(cvss); row.with(td(a(cvss.toString()).withHref(generateLinkForUniversalCvssCalculator(aeUniversalWebEditorLink)).withTarget(HrefTargets.TARGET_CVSS_DETAILS.getTarget()))); return row; } private TdTag createCvssDetailsTableStyledCell(double score, double normalizedScore, double maxUnNormalizedScore, Supplier hasNormalizedScore) { final TdTag tag = td(String.valueOf(score)) .withStyle(cvssScoringStyle.style(String.valueOf(normalizedScore), null)); if (hasNormalizedScore.get()) { tag.withText(String.format("/%s → %s", maxUnNormalizedScore, normalizedScore)); } return tag; } /* -- END: CVSS CHART GENERATION -- */ /* -- START: VULNERABILITY STATUS HANDLING -- */ private DivTag createVulnerabilityStatusHistoryEntry(VulnerabilityStatusHistoryEntry entry, boolean isFirst) { final ContentCard statusCard = new ContentCard(); final String titleSuffix = (entry.isActive() ? "" : " (assessment inactive)") + (entry.getScope() != VulnerabilityStatus.Scope.INVENTORY ? "" : " (inventory scope)"); if (entry.getStatus() == null) { statusCard.withTitle("No status set" + titleSuffix); } else { switch (entry.getStatus()) { case VulnerabilityMetaData.STATUS_VALUE_APPLICABLE: statusCard.withTitle(VulnerabilityMetaData.STATUS_VALUE_APPLICABLE + titleSuffix) .withType(ContentCard.ContentCardType.RED); break; case VulnerabilityMetaData.STATUS_VALUE_NOTAPPLICABLE: statusCard.withTitle(VulnerabilityMetaData.STATUS_VALUE_NOTAPPLICABLE + titleSuffix) .withType(ContentCard.ContentCardType.GREEN); break; case VulnerabilityMetaData.STATUS_VALUE_INSIGNIFICANT: statusCard.withTitle(VulnerabilityMetaData.STATUS_VALUE_INSIGNIFICANT + titleSuffix) .withType(ContentCard.ContentCardType.GRAY); break; case VulnerabilityMetaData.STATUS_VALUE_VOID: statusCard.withTitle(VulnerabilityMetaData.STATUS_VALUE_VOID + titleSuffix) .withType(ContentCard.ContentCardType.LIGHT_GRAY); break; case VulnerabilityMetaData.STATUS_VALUE_IN_REVIEW: statusCard.withTitle(VulnerabilityMetaData.STATUS_VALUE_IN_REVIEW + titleSuffix) .withType(ContentCard.ContentCardType.PASTEL_BLUE); break; default: statusCard.withTitle("Unknown Status"); } } if (!entry.isActive() || (entry.getScope() == VulnerabilityStatus.Scope.INVENTORY && !isFirst)) { statusCard.withType(ContentCard.ContentCardType.LIGHT_GRAY); } if (entry.getIncludeLabels() != null && entry.getIncludeLabels().length > 0) { statusCard.with(span("Include feature labels: ").withClass("slight-text-highlight")); for (String label : entry.getIncludeLabels()) { statusCard.with(makeBadge(label, "primary")); } statusCard.with(br()); } if (entry.getExcludeLabels() != null && entry.getExcludeLabels().length > 0) { statusCard.with(span("Exclude feature labels: ").withClass("slight-text-highlight")); for (String label : entry.getExcludeLabels()) { statusCard.with(makeBadge(label, "primary")); } statusCard.with(br()); } final boolean hasAuthor = entry.getAuthor() != null && !entry.getAuthor().isEmpty(); final boolean hasDate = entry.getDate() != null; if (hasAuthor && hasDate) { statusCard.with(span("Author: ").withClass("slight-text-highlight"), text(entry.getAuthor() + " (" + entry.getFormattedDate() + ")")); } else if (hasAuthor) { statusCard.with(span("Author: ").withClass("slight-text-highlight"), text(entry.getAuthor())); } else if (hasDate) { statusCard.with(span("Date: ").withClass("slight-text-highlight"), text(entry.getFormattedDate())); } boolean lastEntryHadParagraph = false; if (entry.getRationale() != null && !entry.getRationale().isEmpty()) { if (statusCard.getNumChildren() > 0) { statusCard.with(br()); } String rationale = entry.getRationale(); if (entry == VulnerabilityStatusHistoryEntry.INSIGNIFICANT) { rationale = String.format(Locale.ENGLISH, rationale, super.securityPolicyConfiguration.getInsignificantThreshold()); } final SpanTag contentFromMarkdown = Dashboard.markdownToHtml(findAndMakeCveMDLinks(Dashboard.attemptEscapeScripts(rationale)), HrefTargets.TARGET_ARTIFACT.getTarget()); statusCard.with(span("Rationale: ").withClass("slight-text-highlight"), contentFromMarkdown); lastEntryHadParagraph = rationale.contains("

"); } if (entry.getRisk() != null && !entry.getRisk().isEmpty()) { if (statusCard.getNumChildren() > 0 && !lastEntryHadParagraph) { statusCard.with(br()); } final SpanTag contentFromMarkdown = Dashboard.markdownToHtml(findAndMakeCveMDLinks(Dashboard.attemptEscapeScripts(entry.getRisk())), HrefTargets.TARGET_ARTIFACT.getTarget()); statusCard.with(span("Risk: ").withClass("slight-text-highlight"), contentFromMarkdown); lastEntryHadParagraph = entry.getRisk().contains("

"); } if (entry.getMeasures() != null && !entry.getMeasures().isEmpty()) { if (statusCard.getNumChildren() > 0 && !lastEntryHadParagraph) { statusCard.with(br()); } final SpanTag contentFromMarkdown = Dashboard.markdownToHtml(findAndMakeCveMDLinks(Dashboard.attemptEscapeScripts(entry.getMeasures())), HrefTargets.TARGET_ARTIFACT.getTarget()); statusCard.with(span("Measures: ").withClass("slight-text-highlight"), contentFromMarkdown); } final boolean hasModifiedPriority = entry.getPriority() != 0; if (hasModifiedPriority) { if (statusCard.getNumChildren() > 0) { statusCard.with(br()); } statusCard.with(span("Display Priority: ").withClass("slight-text-highlight"), text(String.valueOf(entry.getPriority()))); if (entry.getPriority() < 0) { statusCard.with(span(" (appears lower than other status entries)")); } else if (entry.getPriority() > 0) { statusCard.with(span(" (appears higher than other status entries)")); } } return statusCard.generate(); } /* -- END: VULNERABILITY STATUS HANDLING -- */ /* -- START: ELEMENTS GENERATION -- */ private UnescapedText richTextField(String text) { return rawHtml(text.replaceAll("\\*\\*([^*]+)\\*\\*", "$1") .replaceAll("\\*([^*]+)\\*", "$1") .replaceAll("`([^`]+)`", "$1")); } protected SpanTag makeBadge(Object text, String type) { return span(String.valueOf(text)).withClasses("badge", "badge-" + type); } private String makeVulnerabilityReferenceBadge(String vulnerability, Collection allVulnerabilities) { if (allVulnerabilities.stream().anyMatch(v -> v.getId().equals(vulnerability))) { return "" + "" + vulnerability + ""; } else { return makeHTMLLink("" + vulnerability + "", "https://nvd.nist.gov/vuln/detail/" + vulnerability, HrefTargets.TARGET_NVD.getTarget()).render(); } } protected ATag makeHTMLLink(String text, String url, String target) { return a(rawHtml(text)).withTarget(target).withHref(String.valueOf(url).replace(" ", "%20")); } private ATag makeHTMLLinkWithStyle(String text, String url, String target, String style) { return a(rawHtml(text)).withTarget(target).withHref(String.valueOf(url).replace(" ", "%20")).withStyle(style); } /** * This pattern ensures that only CVEs that are not part of a path (/), Markdown link * ([]) or html tag content (<>) will match. */ private final static Pattern CVE_LINK_PATTERN = Pattern.compile("(?\"])(CVE-\\d+-\\d+)(?=[^0-9]|$)(?![/\\]<>\"])"); private String findAndMakeCveLinks(String text) { if (text == null) { return null; } return text.replaceAll(CVE_LINK_PATTERN.pattern(), "$1"); } private String findAndMakeCveMDLinks(String text) { if (text == null) { return null; } return text.replaceAll(CVE_LINK_PATTERN.pattern(), "[$1](https://nvd.nist.gov/vuln/detail/$1)"); } private SpanTag createSlightHighlightText(String text) { return span(text).withClass("slight-text-highlight"); } private final static Pattern HTTP_URL_REGEX = Pattern.compile("https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b([-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)"); protected SpanTag generateATagsForHyperlinks(String text, String target) { final Matcher matcher = HTTP_URL_REGEX.matcher(text); final SpanTag container = span(); int previousUrlEndIndex = 0; while (matcher.find()) { final int startIndex = matcher.start(); final int endIndex = matcher.end(); if (previousUrlEndIndex != startIndex) { final String previousSegment = text.substring(previousUrlEndIndex, startIndex); container.with(text(previousSegment)); } final String url = text.substring(startIndex, endIndex); container.with(a(url).withHref(url).withTarget(target)); previousUrlEndIndex = endIndex; } if (previousUrlEndIndex != text.length()) { final String previousSegment = text.substring(previousUrlEndIndex); container.with(text(previousSegment)); } return container; } /* -- END: ELEMENTS GENERATION -- */ /* -- START: OTHER UTILITIES -- */ public static double roundOneDecimal(double value) { return round(value, 1); } private static double round(double value, int precision) { int scale = (int) Math.pow(10, precision); return (double) Math.round(value * scale) / scale; } /* -- END: OTHER UTILITIES -- */ private final NavigationCellStyle cvssScoringStyle = (cellData, additionalInformation) -> { final String generalStyling = "text-align:end;"; try { if (additionalInformation instanceof String) { if (additionalInformation.equals("override_gray")) { return generalStyling + "color: var(--" + ColorScheme.PASTEL_GRAY.getCssRootName() + ");"; } else if (cellData.equals("N/A") || cellData.equals("NaN")) { return generalStyling; } } return generalStyling + "color: var(--" + super.getSecurityPolicyConfiguration().getCvssSeverityRanges().getRange(Double.parseDouble(cellData)).getColor().getCssRootName() + ");"; } catch (Exception e) { return generalStyling; } }; private final NavigationCellStyle priorityLabelStyle = (cellData, additionalInformation) -> { final String generalStyling = "text-align:center;"; try { if (StringUtils.hasText(cellData)) { return generalStyling + "color: var(--" + super.getSecurityPolicyConfiguration().getPriorityScoreSeverityRanges().getRangeByName(cellData).getColor().getCssRootName() + ");"; } } catch (Exception ignored) { } return generalStyling; }; private final NavigationCellStyle epssScoreStyle = (cellData, additionalInformation) -> { final String generalStyling = "text-align:end;"; try { if (additionalInformation instanceof String) { if (additionalInformation.equals("override_gray")) { return generalStyling + "color: var(--" + ColorScheme.PASTEL_GRAY.getCssRootName() + ");"; } else if (cellData.equals("N/A") || cellData.equals("NaN")) { return generalStyling; } } return generalStyling + "color: var(--" + getEpssScoreColor(Double.parseDouble(cellData)) + ");"; } catch (Exception e) { return generalStyling; } }; protected String getEpssScoreColor(double score) { if (score <= 0.01) return ColorScheme.PASTEL_GRAY.getCssRootName(); else if (score <= 0.1) return ColorScheme.STRONG_YELLOW.getCssRootName(); else if (score <= 0.8) return ColorScheme.STRONG_LIGHT_ORANGE.getCssRootName(); return ColorScheme.STRONG_RED.getCssRootName(); } @Getter @AllArgsConstructor protected enum NavigationHeaders { NAVIGATION_VULNERABILITY_NAME("Name"), NAVIGATION_PRIORITY_SCORE("Priority
Score"), NAVIGATION_PRIORITY_SCORE_LABEL("Priority
Label"), NAVIGATION_CVSS_MODIFIED_OVERALL("Context
Overall"), NAVIGATION_CVSS_UNMODIFIED_OVERALL("Initial
Overall"), NAVIGATION_CVSS_BASE("Base"), NAVIGATION_CVSS_EXPLOITABILITY("Exploit-
ability"), NAVIGATION_CVSS_IMPACT("Impact"), NAVIGATION_EPSS_SCORE("EPSS
Score"), NAVIGATION_AMOUNT_ARTIFACTS("Arti-
facts"), NAVIGATION_AMOUNT_ADVISORIES_REVIEWED("Security
Advisories"), NAVIGATION_KEV_ENTRY("KEV"), NAVIGATION_STATUS("Status"), NAVIGATION_USED_MATCHING_INFORMATION("Matched configurations"), NAVIGATION_UNUSED_MATCHING_INFORMATION("Unused configurations"), NAVIGATION_VERSION("Versions"), NAVIGATION_COMPONENTS("Components"), CORRELATION_DISTANCE("ΔCorr"), EOL_STATE("EOL"); private final String title; } @Getter @AllArgsConstructor protected enum HrefTargets { TARGET_NVD("nvd"), TARGET_CVSS_DETAILS("cvss_details"), TARGET_CVEDETAILS("cvedetails"), TARGET_MITRE("mitre"), TARGET_NVD_CPE_SEARCH("cpesearch"), TARGET_CERT_FR("certfr"), TARGET_CERT_EU("certeu"), TARGET_CERT_SEI("certsei"), TARGET_CERT_SEI_REFERENCE("certseiref"), TARGET_MICROSOFT("microsoft"), TARGET_GITHUB("ghsa"), TARGET_MICROSOFT_REFERENCE("microsoft_reference"), TARGET_CWE("cwe"), TARGET_REFERENCE("reference"), TARGET_ARTIFACT("artifact"), TARGET_EOL_DATE("eol_data"), TARGET_KEV("kev"), TARGET_BLANK("_blank"); private final String target; } @Getter public static class GeneratedChart { private final String chartJsChartId; private final Chart chartJsChart; private final String jFreeChartName; private final JFreeChart jFreeChart; private final DomContent canvasTag; private final String charJsChartJsVarName; private GeneratedChart(JFreeChart jFreeChart, String jFreeChartName, Chart chartJsChart, String chartJsChartId, ContainerTag canvasOrParentTag, String charJsChartJsVarName) { this.jFreeChartName = jFreeChartName; this.jFreeChart = jFreeChart; this.chartJsChartId = chartJsChartId; this.chartJsChart = chartJsChart; if (canvasOrParentTag instanceof CanvasTag) { this.canvasTag = canvasOrParentTag.withId(chartJsChartId); } else if (canvasOrParentTag.render().contains(" chartJsChart; private String chartJsChartId; private ContainerTag canvasOrParentTag; private String charJsChartJsVarName; public GeneratedChartBuilder setjFreeChart(JFreeChart jFreeChart) { this.jFreeChart = jFreeChart; return this; } public GeneratedChartBuilder setjFreeChartName(String jFreeChartName) { this.jFreeChartName = jFreeChartName; return this; } public GeneratedChartBuilder setChartJsChart(Chart chartJsChart) { this.chartJsChart = chartJsChart; return this; } public GeneratedChartBuilder setChartJsChartId(String chartJsChartId) { this.chartJsChartId = chartJsChartId; return this; } /** * If the tag provided is a/contains a {@link CanvasTag}, the id MUST NOT be set, as it will be overwritten. * * @param canvasOrParentTag the canvas tag or a parent tag with/without a canvas tag. * @return this */ public GeneratedChartBuilder setCanvasOrParentTag(ContainerTag canvasOrParentTag) { this.canvasOrParentTag = canvasOrParentTag; return this; } public GeneratedChartBuilder setCharJsChartJsVarName(String charJsChartJsVarName) { this.charJsChartJsVarName = charJsChartJsVarName; return this; } public GeneratedChart createGeneratedChart() { int jFreeChartAttributes = 0; if (jFreeChart != null) { jFreeChartAttributes++; } if (jFreeChartName != null) { jFreeChartAttributes++; } if (jFreeChartAttributes != 0 && jFreeChartAttributes != 2) { throw new IllegalStateException("jFreeChart and jFreeChartName must either both be null or both be set"); } int chartJsChartAttributes = 0; if (chartJsChart != null) { chartJsChartAttributes++; } if (chartJsChartId != null) { chartJsChartAttributes++; } if (canvasOrParentTag != null) { chartJsChartAttributes++; } if (charJsChartJsVarName != null) { chartJsChartAttributes++; } if (chartJsChartAttributes != 0 && chartJsChartAttributes != 4) { throw new IllegalStateException("chartJsChart, chartJsChartId, canvasOrParentTag and charJsChartJsVarName must either all be null or all be set"); } return new GeneratedChart(jFreeChart, jFreeChartName, chartJsChart, chartJsChartId, canvasOrParentTag, charJsChartJsVarName); } } private String generateLinkForUniversalCvssCalculator(UniversalCvssCalculatorLinkGenerator generator) { // generator.setBaseUrl("http://0.0.0.0:8000/site/index.html"); return generator .addOpenSection("base") .addOpenSection("environmental") .addOpenSection("environmental-base") .generateOptimizedLink(); } private final static String SEARCH_EMOJI = "\uD83D\uDD0D"; @Override public VulnerabilityAssessmentDashboardEnrichmentConfiguration getConfiguration() { return configuration; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy