Public/Get-MsrcVulnerabilityReportHtml.ps1

Function Get-MsrcVulnerabilityReportHtml {
    <#
 
    .SYNOPSIS
        Use a CVRF document to create a Vulnerability summary
 
    .DESCRIPTION
        Use a CVRF document to create a Vulnerability summary
 
    .PARAMETER Vulnerability
        The Vulnerability node of a CVRF document
 
    .PARAMETER ProductTree
        The ProductTree node of a CVRF document
 
    .EXAMPLE
        Get-MsrcCvrfDocument -ID 2016-Aug |
        Get-MsrcVulnerabilityReportHtml |
        Out-File -FilePath Cvrf-CVE-Summary.html
 
        It creates a report with all the Vulnerabilities in a CVRF document
 
    .EXAMPLE
        $cvrfDoc = Get-MsrcCvrfDocument -ID 2016-Nov
        $cvrfDoc.Vulnerability | Foreach-Object {
 
            Write-Verbose "Dealing with CVE: $($_.CVE)" -Verbose
            Get-MsrcVulnerabilityReportHtml -Vulnerability $_ -ProductTree $cvrfDoc.ProductTree |
            Out-File -FilePath "Cvrf-$($vulnerability.CVE)-Summary.html"
        }
 
        It creates a report for each of the Vulnerabilities in a CVRF document
 
    .EXAMPLE
        $cvrfDoc = Get-MsrcCvrfDocument -ID 2016-Nov
        $HT = @{
            Vulnerability = ($cvrfDoc.Vulnerability | Where-Object {$_.CVE -In @('CVE-2016-0026','CVE-2016-7202','CVE-2016-3343')})
            ProductTree = $cvrfDoc.ProductTree
        }
        Get-MsrcVulnerabilityReportHtml @HT | Out-File -FilePath Cvrf-CVE-Summary.html
 
        It creates a report for specific Vulnerabilities in a CVRF document
#>

    [CmdletBinding()]
    [OutputType([string])]
    Param(

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        $Vulnerability,

        [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
        $ProductTree,

        [Switch]$ShowNoProgress
    )
    Begin {

        $HT = @{ ErrorAction = 'Stop' }

        $MaximumSeverityType = 3
        $ThreatsImpactType = 0
        $ThreatsExploitStatusType = 1
        $TagType = 7
        $CNAType = 8
        $RemediationsMitigationType = 1
        $RemediationsWorkaroundType = 0

        try {
            $JsonMetrics = Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath 'CVSS-Metrics.json' @HT) @HT |
            Out-String @HT | ConvertFrom-Json @HT

            $JsonDescriptions = Get-Content -Path (Join-Path -Path $PSScriptRoot -ChildPath 'CVSS-Descriptions.json'@HT) @HT |
            Out-String @HT | ConvertFrom-Json @HT
        }
        catch {
            Throw "Failed to get required json files content because $($_.Exception.Message)"
        }

        $css = @'
        body {
          background-color: white;
          font-family: sans-serif;
        }
 
        h1 {
          color: black;
        }
        table {
          font-family: Arial, Helvetica, sans-serif;
          border-collapse: collapse;
          width: 100%;
        }
 
        table td, th {
          border: 1px solid #ddd;
          padding: 8px;
        }
 
        table tr:nth-child(even){
          background-color: #ddd;
        }
 
        table tr:hover {background-color: #FAF0E6;}
 
        table th {
          padding-top: 12px;
          padding-bottom: 12px;
          text-align: left;
          background-color: #C0C0C0;
        }
'@

    }
    Process {
        $htmlDocumentTemplate = @'
<html>
<head>
    <!-- Created by module version {2} and API version {4} -->
    <!-- this is the css from the old bulletin site. Change this to better style your report to your liking -->
    <!-- <link rel="stylesheet" href="https://i-technet.sec.s-msft.com/Combined.css?resources=0:ImageSprite,0:TopicResponsive,0:TopicResponsive.MediaQueries,1:CodeSnippet,1:ProgrammingSelector,1:ExpandableCollapsibleArea,0:CommunityContent,1:TopicNotInScope,1:FeedViewerBasic,1:ImageSprite,2:Header.2,2:HeaderFooterSprite,2:Header.MediaQueries,2:Banner.MediaQueries,3:megabladeMenu.1,3:MegabladeMenu.MediaQueries,3:MegabladeMenuSpriteCluster,0:Breadcrumbs,0:Breadcrumbs.MediaQueries,0:ResponsiveToc,0:ResponsiveToc.MediaQueries,1:NavSidebar,0:LibraryMemberFilter,4:StandardRating,2:Footer.2,5:LinkList,2:Footer.MediaQueries,0:BaseResponsive,6:MsdnResponsive,0:Tables.MediaQueries,7:SkinnyRatingResponsive,7:SkinnyRatingV2;/Areas/Library/Content:0,/Areas/Epx/Content/Css:1,/Areas/Epx/Themes/TechNet/Content:2,/Areas/Epx/Themes/Shared/Content:3,/Areas/Global/Content:4,/Areas/Epx/Themes/Base/Content:5,/Areas/Library/Themes/Msdn/Content:6,/Areas/Library/Themes/TechNet/Content:7&amp;v=9192817066EC5D087D15C766A0430C95"> -->
 
    <!-- this style section changes cell widths in the exec header table so that the affected products at the end are wide enough to read -->
    <style>
    {3}
        #execHeader td:first-child {{ width: 10% ;}}
        #execHeader td:nth-child(5) {{ width: 37% ;}}
    </style>
 
    <!-- this section defines explicit width for all cells in the affected software tables. This is so the column width is the same across each product -->
    <style>
        .affected_software td:first-child {{ width: 34% ; }}
        .affected_software td:nth-child(2) {{ width: 14% ; }}
        .affected_software td:nth-child(3) {{ width: 6% ; }}
        .affected_software td:nth-child(4) {{ width: 6% ; }}
        .affected_software td:nth-child(5) {{ width: 7.5% ; }}
        .affected_software td:nth-child(6) {{ width: 28.5% ; }}
        .affected_software td:nth-child(7) {{ width: 4% ; }}
    </style>
 
    <!-- remove spacing between table of contents cells -->
    <style>
        #tableOfContents tr td {{ padding: 2px; }}
    </style>
 
    <style>
        .cvss_table tr:nth-child(odd) {{background: #ededed}}
    </style>
 
</head>
 
<body lang=EN-US link=blue>
<div id="documentWrapper" style="width: 90%; margin-left: auto; margin-right: auto;">
 
<h1 id="top">Microsoft CVE Summary</h1>
 
<p style="margin:0; padding:0">This report contains detail for the following vulnerabilities:</p>
<table id="tableOfContents" style="width:78%; margin-top:5">
 <tr>
  <th>CVE Issued by</th>
  <th>Tag</th>
  <th>CVE ID</th>
  <th>CVE Title</th>
 </tr>
 {0}
</table>
{1}
</div>
<br>
 </body>
</html>
'@

        $cveListHtmlObjects = @()

        $cveSectionHtml = ''

        $TotalCVE = $Vulnerability.Count
        $count = 0
        $Vulnerability | ForEach-Object -Process {
            $count++
            $v = $_
            $Progress = @{
                Activity        = 'Getting Msrc Vulnerability Html Report'
                Status          = "$($count)/$($TotalCVE) => $($v.CVE) "
                PercentComplete = ($count / $TotalCVE * 100)
                ErrorAction     = 'SilentlyContinue'
            }
            if (-not($ShowNoProgress)) { Write-Progress @Progress }
            Write-Verbose -Message "Dealing with $($_.CVE)"

            #region CVE Summary Table

            $cveSummaryTableHtml = @'
<table id="execHeader" border=1 cellpadding=0 width="99%">
 <thead style="background-color: #ededed">
  <tr>
   <td><b>CVE ID</b></td>
   <td><b>Vulnerability Description</b></td>
   <td><b>Maximum Severity Rating</b></td>
   <td><b>Vulnerability Impact</b></td>
  </tr>
 </thead>
 <tr>
     <td>{0}</td>
     <td>{1}</td>
     <td>{2}</td>
     <td>{3}</td>
 </tr>
</table>
'@


            $MaximumSeverity = Switch (
                ($_.Threats | Where-Object { $_.Type -eq $MaximumSeverityType }).Description.Value | Select-Object -Unique
            ) {
                'Critical' { 'Critical'  ; break }
                'Important' { 'Important' ; break }
                'Moderate' { 'Moderate'  ; break }
                'Low' { 'Low'       ; break }
                'None' { 'None'       ; break }
                default {
                    Write-Warning "Could not determine the Maximum Severity from the Threats for $($v.CVE)"
                    'Unknown'
                }
            }
            if (-not($MaximumSeverity)) {
                $MaximumSeverity = 'Unknown'
            }


            if ($ImpactValues = ($v.Threats | Where-Object { $_.Type -eq $ThreatsImpactType }).Description.Value | Select-Object -Unique) {
                $impactColumn = $ImpactValues -join ',<br>'
            }
            else {
                Write-Warning -Message "Could not determine the Impact from the Threats for $($v.CVE)"
                $impactColumn = 'Unknown'
            }

            $vulnDescriptionColumnTemplate = @'
        <b>CVE Title:</b> {0}
        <br>
        <b>Weakness:</b> {7}
        <br>
        <b>Customer Action Required:</b> {8}
        <br>
        <b>CVSS:</b> <br>{1}
        <br>
        <b>Executive Summary:</b> <br>{6}
        <br>
        <b>FAQ:</b><br>{2}
        <br>
        <b>Mitigations:</b><br>{3}
        <br>
        <b>Workarounds:</b><br>{4}
        <br>
        <b>Revision:</b><br>{5}
        <br>
'@


            $vulnDescriptionColumn = $vulnDescriptionColumnTemplate -f @(
                # $cveTitle
                $(
                    if ($cveTitle = $v.Title.Value) {
                        $cveTitle
                    }
                    else {
                        Write-Warning -Message "Missing Title for $($v.CVE)"
                        ($cveTitle = 'Unknown')
                    }
                ),
                # $cvssScoreSet
                $(
                    #Scores among the affected products can be different. So, just find the most severe.
                    $highestBase = 0.0
                    $highestCvssScore = $null
                    ForEach ($score in $v.CvssScoreSets) {
                        if ($score.BaseScore -gt $highestBase) {
                            $highestBase = $score.BaseScore
                            $highestCvssScore = $score
                        }
                    }

                    if (($null -ne $highestCvssScore) -and ($null -ne $highestCvssScore.Vector) -and ($highestCvssScore.Vector.Split('/').Length -gt 1)) {
                        $cvssArray = $highestCvssScore.Vector.Split('/')

                        $cvssScoreTemplate = @'
                        <br> <b>{0}</b>
                        <table class="cvss_table" border=1 cellpadding=0 width="99%">
                            <thead>
                                <tr>
                                    <td colspan="7"><b>Base score metrics</b></td>
                                </tr>
                            </thead>
                            {1}
                        </table>
                        <table class="cvss_table" border=1 cellpadding=0 width="99%">
                            <thead>
                                <tr>
                                    <td colspan="7"><b>Temporal score metrics</b></td>
                                </tr>
                            </thead>
                            {2}
                        </table>
'@

                        $cvssScoreSet = $cvssScoreTemplate -f @(
                            $rowTemplate = '<tr><td title="{0}"><b>{1}</b></td><td title="{2}"><b>{3}</b></td></tr>'

                            $baseTags = 'AC', 'AV', 'A', 'C', 'I', 'PR', 'S', 'UI'
                            $temporalTags = 'E', 'RC', 'RL'
                            $baseRows = ''
                            $temporalRows = ''
                            for ($i = 1; $i -lt $cvssArray.Length; $i++) {

                                $element = $cvssArray[$i]
                                $split0 = $element.Split(':')[0]

                                $metric = $JsonMetrics.$split0
                                $value = $JsonMetrics.$element

                                $metricDescription = $JsonDescriptions.$split0
                                $valueDescription = $JsonDescriptions.$element


                                $row = '<tr><td><b>' + $metric + '</b></td>'
                                $row += '<td><b>' + $value + '</b></td></tr>'

                                if (($null -ne $metricDescription) -and ($null -ne $valueDescription)) {
                                    if ($baseTags.Contains($split0)) {
                                        $baseRows += $rowTemplate -f $metricDescription, $metric, $valueDescription, $value
                                    }
                                    else {
                                        if ($temporalTags.Contains($split0)) {
                                            $temporalRows += $rowTemplate -f $metricDescription, $metric, $valueDescription, $value
                                        }
                                    }
                                }
                            }

                            $formattedScore = '{0} Highest BaseScore:{1}/TemporalScore:{2}' -f $cvssArray[0], $highestCvssScore.BaseScore, $highestCvssScore.TemporalScore

                            $formattedScore, $baseRows, $temporalRows
                        )
                        $cvssScoreSet
                    }
                    else {
                        'None' -join '<br>'
                    }
                ),
                # $cveFaq
                $(
                    if ($cveFaq = ($v.Notes | Where-Object { $_.Title -eq 'FAQ' }).Value) {
                        $cveFaq -join '<br>'
                    }
                    else {
                        'None' -join '<br>'
                    }
                ),
                # $cveMitigation
                $(
                    if ($cveMitigation = $v.Remediations | Where-Object { $_.Type -eq $RemediationsMitigationType }) {
                        $cveMitigation.Description.Value -join '<br>'

                    }
                    else {
                        'None' -join '<br>'
                    }
                ),
                # $cveWorkaround
                $(
                    if ( $cveWorkaround = ($v.Remediations | Where-Object { $_.Type -eq $RemediationsWorkaroundType }).Description.Value) {
                        $cveWorkaround -join '<br>'
                    }
                    else {
                        'None' -join '<br>'
                    }
                ),
                # $Revision
                $(
                    $RevisionStrings = @()
                    $v.RevisionHistory |
                    ForEach-Object {
                        $_ | Add-Member -MemberType NoteProperty -Name RevisionDate -Value ([datetime]$_.Date) -Force -PassThru
                    } | Sort-Object RevisionDate |
                    ForEach-Object {
                        if ( $revision = $($_.Number, $_.RevisionDate.ToString('d'), $_.Description.Value) ) {
                            $RevisionStrings += $($revision -join '&nbsp&nbsp&nbsp&nbsp')
                        }
                    }
                    if ( $RevisionStrings ) {
                        $RevisionStrings -join '<br>'
                    }
                    else {
                        'Unknown' -join '<br>'
                    }
                ),
                # Executive Summary
                $(
                    if ($cveExecSummary = ($v.Notes | Where-Object { $_.Title -eq 'Description' }).Value) {
                        $cveExecSummary -join '<br>'
                    }
                    else {
                        'None' -join '<br>'
                    }
                )
                # CWE Weakness
                $(
                    if ($cveWeakness = $(if ($v.CWE) { '{0} : {1}' -f "$($v.CWE.ID)", "$($v.CWE.Value)" })) {
                        $cveWeakness -join '<br>'
                    }
                    else {
                        'N/A' -join '<br>'
                    }
                )
                # Customer Action Required
                $(
                    if ($cveCustomerActionRequired = $v.Notes | Where-Object { $_.Title -eq "Customer Action Required" }) {
                        $cveCustomerActionRequired.Value -join '<br>'
                    }
                    else {
                        'Yes' -join '<br>'
                    }
                )
            )

            $cveSectionHtml += '<h1 id="{0}">{0} - {1}</h1> (<a href="#top">top</a>)' -f $v.CVE, $cveTitle

            #region CVE Summary List
            $cveListHtmlObjects += [PSCustomObject]@{
                Tag      = $($v.Notes | Where-Object type -eq $TagType).Value
                CNA      = $($v.Notes | Where-Object type -eq $CNAType).Value
                CVEID    = $v.CVE
                CVETitle = $cveTitle
            }
            #endregion

            $cveSectionHtml += $cveSummaryTableHtml -f @(
                @"
<a href=`"https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/$($_.CVE)`">$($_.CVE)</a>
<br>
<a href=`"https://cve.mitre.org/cgi-bin/cvename.cgi?name=$($_.CVE)`">MITRE</a>
<br>
<a href=`"https://web.nvd.nist.gov/view/vuln/detail?vulnId=$($_.CVE)`">NVD</a>
<p>Issuing CNA: $($($v.Notes | Where-Object type -eq $CNAType).Value)</p>
"@
,
                $vulnDescriptionColumn,
                $MaximumSeverity,
                $impactColumn
            )
            #endregion

            #region Exploitability Index Table

            #Reset exploitability state for this CVE
            $ExploitStatus = [PSCustomObject]@{
            
                PubliclyDisclosed     = $null
                Exploited             = $null
                LatestSoftwareRelease = $null
                OlderSoftwareRelease  = $null
                DenialOfService       = $null
            }
            $exploitabilityIndexTableHtml = @'
<h2>Exploitability Index</h2>
 
<p>The following table provides an <a href="https://www.microsoft.com/en-us/msrc/exploitability-index?rtc=1">exploitability assessment</a> for this vulnerability at the time of original publication.</p>
 
<table border=1 cellpadding=0 width="99%">
 <thead style="background-color: #ededed">
  <tr>
   <td><b>Exploitability Assessment</b></td>
   <td><b>Publicly Disclosed</b></td>
   <td><b>Exploited</b></td>
  </tr>
 </thead>
 <tr>
     <td>{0}</td>
     <td>{1}</td>
     <td>{2}</td>
 </tr>
</table>
'@


            if ($ExploitStatusThreat = ($v.Threats | Where-Object { $_.Type -eq $ThreatsExploitStatusType } | Select-Object -Last 1).Description.Value) {
                $ExploitStatus = Get-MsrcThreatExploitStatus -ExploitStatusString $ExploitStatusThreat
            }
            else {
                Write-Warning -Message "Missing ExploitStatus for $($v.CVE)"
            }
        
            $cveSectionHtml += $exploitabilityIndexTableHtml -f @(
                # $LatestSoftwareRelease
                $(
                    if ($ExploitStatus.LatestSoftwareRelease) {
                        $ExploitStatus.LatestSoftwareRelease
                    }
                    else {
                        'Not Found'
                    }
                ),
                # $publicly disclosed
                $(
                    if ($ExploitStatus.PubliclyDisclosed) {
                        $ExploitStatus.PubliclyDisclosed
                    }
                    else {
                        'Not Found'
                    }
                ),
                # $Exploited
                $(
                    if ($ExploitStatus.Exploited) {
                        $ExploitStatus.Exploited
                    }
                    else {
                        'Not Found'
                    }
                )
            )
            #endregion

            #region Affected Software Table

            $affectedSoftwareTableTemplate = @'
<table class="affected_software" border=1 cellpadding=0 width="99%">
    <thead style="background-color: #ededed">
        <tr>
            <td colspan="9"><b>{0}</b></td>
        </tr>
    </thead>
        <tr>
            <td><b>Product</b></td>
            <td><b>KB Article</b></td>
            <td><b>Severity</b></td>
            <td><b>Impact</b></td>
            <td><b>Supercedence</b></td>
            <td><b>CVSS Score Set</b></td>
            <td><b>Fixed Build</b></td>
            <td><b>Restart Required</b></td>
            <td><b>Known Issue</b></td>
        </tr>
        {1}
</table>
<br>
'@


            $affectedSoftwareRowTemplate = @'
        <tr>
                <td>{0}</td>
                <td>{1}</td>
                <td>{2}</td>
                <td>{3}</td>
                <td>{4}</td>
                <td>{5}</td>
                <td>{6}</td>
                <td>{7}</td>
                <td>{8}</td>
        </tr>
'@


            $cveSectionHtml += @'
<h2>Affected Software</h2>
 
<p>The following tables list the affected software details for the vulnerability.</p>
'@

            $affectedSoftware = Get-MsrcCvrfAffectedSoftware -Vulnerability $v -ProductTree $ProductTree
            $affectedSoftwareTableHtml = ''


            $affectedSoftware.FullProductName | Sort-Object -Unique | ForEach-Object {

                $PN = $_

                $affectedSoftware | Where-Object { $_.FullProductName -eq $PN } | ForEach-Object {

                    $affectedSoftwareTableHtml += $affectedSoftwareRowTemplate -f @(
                        $PN,
                        $(
                            if ($PN -eq 'Microsoft Edge (Chromium-based)') {
                                @(
                                    '<a href="{0}" >{1} ({2})' -f 'https://learn.microsoft.com/en-us/deployedge/microsoft-edge-relnote-stable-channel',
                                    "$($_.KBArticle.ID)", "$($_.KBArticle.SubType)"
                                )
                            }
                            else {
                                $_.KBArticle | Get-KBDownloadUrl
                            }
                        ),
                        $(
                            if (-not($_.Severity)) {
                                'Unknown'
                            }
                            else {
                                $($_.Severity | Select-Object -Unique) -join '<br />'
                            }
                        ),
                        $(
                            if (-not($_.Impact)) {
                                'Unknown'
                            }
                            else {
                                $($_.Impact | Select-Object -Unique) -join '<br />'
                            }
                        ),
                        $(
                            if (-not($_.Supercedence)) {
                                'None'
                            }
                            else {
                                $($_.Supercedence | Select-Object -Unique) -join '<br />'
                            }
                        ),
                        $(

                            'Base: {0}<br />Temporal: {1}<br />Vector: {2}<br />' -f (
                                $(
                                    if (-not($_.CvssScoreSet.base)) {
                                        'N/A'
                                    }
                                    else {
                                        $_.CvssScoreSet.base
                                    }
                                )
                            ),
                            (
                                $(
                                    if (-not($_.CvssScoreSet.temporal)) {
                                        'N/A'
                                    }
                                    else {
                                        $_.CvssScoreSet.temporal
                                    }
                                )
                            ),
                            (
                                $(
                                    if (-not($_.CvssScoreSet.vector)) {
                                        'N/A'
                                    }
                                    else {
                                        $_.CvssScoreSet.vector
                                    }
                                )
                            )
                        ),
                        $(
                            if (-not($_.FixedBuild)) {
                                'Unknown'
                            }
                            else {
                                $($_.FixedBuild | Select-Object -Unique) -join '<br />'
                            }
                        ),
                        $(
                            if (-not($_.RestartRequired)) {
                                'Unknown'
                            }
                            else {
                                $($_.RestartRequired | Select-Object -Unique) -join '<br />'
                            }
                        ),
                        $(
                            if (-not($_.'Known Issue')) {
                                'None'
                            }
                            else {
                                $_.'Known Issue' | Get-KBDownloadUrl
                            }
                        )
                    )
                }
            }

            $cveSectionHtml += $affectedSoftwareTableTemplate -f @(
                $v.CVE,
                $affectedSoftwareTableHtml
            )
            #endregion

            #region Acknowledgments Table
            $acknowledgmentsTableTemplate = @'
<h2>Acknowledgements</h2>
<table border=1 cellpadding=0 width="99%">
 <thead style="background-color: #ededed">
    <tr>
        <td><b>CVE ID</b></td>
        <td><b>Acknowledgements</b></td>
    </tr>
    </thead>
 <tr>
     <td>{0}</td>
     <td>{1}</td>
 </tr>
</table>
'@


            if ($v.Acknowledgments) {
                $ackVal = ''
                $v.Acknowledgments | ForEach-Object {

                    if ($_.Name.Value) {
                        $ackVal += $_.Name.Value
                        $ackVal += '<br>'
                    }
                    if ($_.URL) {
                        $ackVal += $_.URL
                        $ackVal += '<br>'
                    }
                    $ackVal += '<br><br>'
                }
            }
            else {
                Write-Warning -Message "No Acknowledgments for $($v.CVE)"
                $ackVal = 'None'
            }

            $cveSectionHtml += $acknowledgmentsTableTemplate -f @(
                $v.CVE,
                $ackVal
            )
        } -End {
            Write-Progress -Activity 'Getting Msrc Vulnerability Html Report' -Completed
        }
        #endregion

        (
            $htmlDocumentTemplate -f @(
                #sort the objects and put them into the table of contents format before injecting into the document template:
                ($( $cveListHtmlObjects | Sort-Object -Property Tag |
                    ForEach-Object {
                        '<tr><td>{3}</td><td>{0}</td> <td><a href="#{1}">{1}</a></td> <td>{2}</td></tr>' -f $_.Tag, $_.CVEID, $_.CVETitle, $_.CNA
                    }) -join "`n"),
                $cveSectionHtml,
                "$($MyInvocation.MyCommand.Version.ToString())",
                $css, $global:msrcApiUrl

            )
        )
    }
    End {}
}