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&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>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 '    ') } } 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>' } ) ) $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 $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 {} } |