From 99f75316ef14dfe893ae22fd77a59c399db4ef11 Mon Sep 17 00:00:00 2001 From: Carrie Plank Date: Thu, 5 Mar 2026 16:21:01 -0800 Subject: [PATCH] MSAL updates and exploit status table fix --- .../MsrcSecurityUpdates.psd1 | Bin 7338 -> 14910 bytes .../MsrcSecurityUpdates.tests.ps1 | 17 + .../Private/Get-CVRFID.ps1 | 6 +- .../Public/Get-KBDownloadUrl.ps1 | 58 +- .../Public/Get-MsrcCvrfAffectedSoftware.ps1 | 2 +- .../Public/Get-MsrcCvrfDocument.ps1 | 6 +- .../Public/Get-MsrcSecurityUpdate.ps1 | 5 +- .../Get-MsrcVulnerabilityReportHtml.ps1 | 802 +++++++++--------- .../Public/Set-MSRCApiKey.ps1 | 5 +- .../Public/Set-MsrcAdalAccessToken.ps1 | 45 - .../Public/Set-MsrcMsalAccessToken.ps1 | 55 ++ src/README.md | 20 +- 12 files changed, 541 insertions(+), 480 deletions(-) delete mode 100644 src/MsrcSecurityUpdates/Public/Set-MsrcAdalAccessToken.ps1 create mode 100644 src/MsrcSecurityUpdates/Public/Set-MsrcMsalAccessToken.ps1 diff --git a/src/MsrcSecurityUpdates/MsrcSecurityUpdates.psd1 b/src/MsrcSecurityUpdates/MsrcSecurityUpdates.psd1 index 364c03ddd44eef974b3ec97317b9e4621e82e336..07b9efcfe72ea8961c2e42645cbd1dca432bb525 100644 GIT binary patch literal 14910 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 literal 7338 zcmc&(+fo}z5`8Cvf5o=oCk6Gj$u&MGojp0Jv)6REceHn~ z-#vC8H#gq=zPZ8TMv+l@Q9TS5#wrtLQT;GcP*GT`Bjw(lt71mQOxarR&gRCo)xw76AhDl_$KDU$`MSZ0NqESm3cZoK=9vv~F8`1n=# zpTC%wjerAuX3sSi3G|v_U z0rxVC*T@Mch9Z@$*q4Rn3!|p9f^30sr7+OJ$_LDOcypt#t3!~s6YUbCVm**L)Jqa_ z5-k~NrMZ`JCuly9)?tT(>1>xYDYJ-UA`;-1a-;9zWCrT0kDE6UB*?p&$%Y0M^66UI z91)h(ha~_@NRO#3?7HJVQz({&Pzi7$vY7g}*Jrc?AEr?AwoEvA#>(k;Eg4HHo6?y! zsVH_S)=|k$v!pVTfI8m%F#0s)==^+n0d%se41S-dkw_8?+o1gDx=+2UbJ`KP>ZC~D zPFh916X|r<-Q9lU;drSsm6j8}WV5Gz@wErDYa$hNm}lW{@3oZSJ14 zrb8}nxslFXs|rbo<2{fXhZKFq5wbAWl<9)W3(Zv$N5aIGxUYo|fSp7s<3Iy? zX3=s-1-e(7qXi3N1egwUEEGQcQ$k$brW*>mkk^Xd=u@vI^7t=+?z#_kHoTKX`{Ubz zo@X5U&nDS!1+eoMX0rWzGwDqx$OajYz*XnaW9^~-?dhmYQ}EBVp9i5mul)#(6RnES z+|RREK0G;B5sSAaasr+IeUU!ZwmRh5#1k1S9KutG6)__=8f>5!i=-EEfxXdpGOOj( z-*fd*F5ujd&;2xpPK}WKe=!n0R>vVEDESy44F6kUlmG$$k_9X0)(Pn)O4u)o_@5>U z*ukng&Iv)ub@~rRee`mxOM}3FA>G*#0~>+g`lAh5^~`diPz=D`P`~Tql#A#N>>)-m zsOPXi>#Kp%U29aIrWjwczbNDkRsFGuE2Hi)P7vl$!A9j62>Jh9b-u4o1{#*4D2c>s zu%ZlHJtKj6ZotxK!p@Mx7&@JWBKG_X)F3>Rg4{bj+&qWLV#$CJoS>-_1s}K>m-}vz z-u#Jw-l7{ZMLo@PbZ$2tHW&u$ijjxF!*j1qP$5<}()SWWGBgJZf#eUWxGY&)l}bFvI+i~#}%Bcr`W$?tVgYE{1BMwwksR{AvUB(pRX^f zpd>1iJT{?^G@DFAw+nybvr#-#&Wa+pulM$*Kvg;Bd1bFA*n6yeyZ&dpGNK(UXk+Tx z8-u>>r2>uA>|LDqPluz^P8vH3_>77vhkY)`j)~WLVr2XUZK9zl*a!deA5!!=jnFU^ zu!!8ngkaH0ACZ=ZLR)l>lR=+H@~+X1Q80!Y`oOQKpvAv*M@tE|a(^;D=g354i)*{I#n+aN&fy%ZICbKb->K z6OBcNKk>>*K7?xF1YqKpE`FMJ`Ff$U@}a(Q+`K`n4VqksCS16y&mY$3!z91<%8h&| zKuP47y8eL!s1z<-ssa=T9K@3(=ef134a_d3!}WYaE&H&kv2A9VQq&N9f-52HW{1FERQ@Zcak&a@?g_P1+X8BC1l| zRm+Ais?{K($0JwoH(rSJ28bpoJgznv5AaCVucIyTB9Q&Y-%(O*U@w_l333X;xxGqG z^>}ybCSUAbT1N;1_Ct;2t2%CTYTW829UawF|H!5?F@KxeksRL8RkwX*VgjiJVIQ% zbz)ZyAV)FacgC`qODtrbs6`P}a$2LNlo41rb2z+)83AHHl%Te*r#0{*DpCT>Lnr2L zX_*vY0XV$01_j4m_f5^+PgSPPYX?xon2S>7;|Uh_+#shIp6|wuT|x(cXZnRNy)jlN zB2+i80>3PU!Jl$+UYJ|AFb6N^KQ93b2%Q@w;Lw==bb;!ue#3%F7^K%P7O#1TK~cY# zXM*+zsPrk^jnF-wNO*PM{Ok+M>eS-b`|mL^oP#=nlAk}Jfm>94=C8{|=>T~OH)1s@ z0Zr#t-WenISlzj84C3;5ZPcMpFxcR)EDjs-stgb1bGL -[CmdletBinding()] -[OutputType([System.String])] -Param ( - [Parameter(Mandatory,ValueFromPipeline)] - [PSCustomObject]$KBArticleObject -) -Begin { - $HTML_TO_RETURN = @() -} -Process { +Function Get-KBDownloadUrl { +<# + .SYNOPSIS + Takes the kb output from Get-MsrcCvrfAffectedSoftware and builds the html to insert into a document. + + .DESCRIPTION + Takes the kb output from Get-MsrcCvrfAffectedSoftware and builds the html to insert into a document. + + .PARAMETER KBArticleObject + The KB Article object that contains the id, url, and subtype. + + .EXAMPLE + [PSCustomObject]@{ID="kb123456"; URL="microsoft.com"; SubType="Required"} | Get-KBDownloadUrl +#> +[CmdletBinding()] +[OutputType([System.String])] +Param ( + [Parameter(Mandatory,ValueFromPipeline)] + [PSCustomObject]$KBArticleObject +) +Begin { + $HTML_TO_RETURN = @() +} +Process { if (-not($KBArticleObject)){ 'None' } else { @@ -36,9 +36,9 @@ Process { $HTML_TO_RETURN += $('{1}' -f $kb.URL, $kb.ID) } } - } -} -End { - $HTML_TO_RETURN -join '
' -} + } +} +End { + $HTML_TO_RETURN -join '
' +} } \ No newline at end of file diff --git a/src/MsrcSecurityUpdates/Public/Get-MsrcCvrfAffectedSoftware.ps1 b/src/MsrcSecurityUpdates/Public/Get-MsrcCvrfAffectedSoftware.ps1 index 03d7b87..0db8ec3 100644 --- a/src/MsrcSecurityUpdates/Public/Get-MsrcCvrfAffectedSoftware.ps1 +++ b/src/MsrcSecurityUpdates/Public/Get-MsrcCvrfAffectedSoftware.ps1 @@ -81,7 +81,7 @@ Process { Where-Object {$_.Type -eq $ThreatsImpactType } | Where-Object { $_.ProductID -contains $id } ).Description.Value - ); + ); Weakness = $v.CWE.Value ; 'Customer Action Required' = if ($customerActionNotes = $v.Notes | Where-Object { $_.Title -eq "Customer Action Required" }) { $customerActionNotes.Value diff --git a/src/MsrcSecurityUpdates/Public/Get-MsrcCvrfDocument.ps1 b/src/MsrcSecurityUpdates/Public/Get-MsrcCvrfDocument.ps1 index 2edfff7..c03fe77 100644 --- a/src/MsrcSecurityUpdates/Public/Get-MsrcCvrfDocument.ps1 +++ b/src/MsrcSecurityUpdates/Public/Get-MsrcCvrfDocument.ps1 @@ -80,9 +80,9 @@ Process { } - if ($global:MSRCAdalAccessToken) { - - $RestMethod.Headers.Add('Authorization', $global:MSRCAdalAccessToken.CreateAuthorizationHeader()) + if ($script:MSRCMsalAccessToken) { + + $RestMethod.Headers.Add('Authorization', "Bearer $($script:MSRCMsalAccessToken.AccessToken)") } diff --git a/src/MsrcSecurityUpdates/Public/Get-MsrcSecurityUpdate.ps1 b/src/MsrcSecurityUpdates/Public/Get-MsrcSecurityUpdate.ps1 index 3f52a49..18ada7b 100644 --- a/src/MsrcSecurityUpdates/Public/Get-MsrcSecurityUpdate.ps1 +++ b/src/MsrcSecurityUpdates/Public/Get-MsrcSecurityUpdate.ps1 @@ -171,9 +171,8 @@ Process { if ($global:msrcProxyCredential){ $RestMethod.Add('ProxyCredential' , $global:msrcProxyCredential) } - if ($global:MSRCAdalAccessToken) - { - $RestMethod.Headers.Add('Authorization' , $global:MSRCAdalAccessToken.CreateAuthorizationHeader()) + if ($script:MSRCMsalAccessToken){ + $RestMethod.Headers.Add('Authorization' , "Bearer $($script:MSRCMsalAccessToken.AccessToken)") } try { diff --git a/src/MsrcSecurityUpdates/Public/Get-MsrcVulnerabilityReportHtml.ps1 b/src/MsrcSecurityUpdates/Public/Get-MsrcVulnerabilityReportHtml.ps1 index 3bf9f1e..0708bca 100644 --- a/src/MsrcSecurityUpdates/Public/Get-MsrcVulnerabilityReportHtml.ps1 +++ b/src/MsrcSecurityUpdates/Public/Get-MsrcVulnerabilityReportHtml.ps1 @@ -1,5 +1,5 @@ Function Get-MsrcVulnerabilityReportHtml { -<# + <# .SYNOPSIS Use a CVRF document to create a Vulnerability summary @@ -41,41 +41,42 @@ Function Get-MsrcVulnerabilityReportHtml { 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)" - } + [CmdletBinding()] + [OutputType([string])] + Param( + + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + $Vulnerability, + + [Parameter(Mandatory, ValueFromPipelineByPropertyName)] + $ProductTree, + + [Switch]$ShowNoProgress + ) + Begin { - $css = @' + $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; @@ -108,9 +109,9 @@ Begin{ background-color: #C0C0C0; } '@ -} -Process { - $htmlDocumentTemplate = @' + } + Process { + $htmlDocumentTemplate = @' @@ -167,27 +168,27 @@ Process { '@ - $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)" + $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 + #region CVE Summary Table - $cveSummaryTableHtml = @' + $cveSummaryTableHtml = @' @@ -206,32 +207,33 @@ Process {
'@ - $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' + $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 (-not($MaximumSeverity)) { - $MaximumSeverity = 'Unknown' - } - if ($ImpactValues = ($v.Threats | Where-Object { $_.Type -eq $ThreatsImpactType }).Description.Value | Select-Object -Unique) { - $impactColumn = $ImpactValues -join ',
' - } else { - Write-Warning -Message "Could not determine the Impact from the Threats for $($v.CVE)" - $impactColumn = 'Unknown' - } + if ($ImpactValues = ($v.Threats | Where-Object { $_.Type -eq $ThreatsImpactType }).Description.Value | Select-Object -Unique) { + $impactColumn = $ImpactValues -join ',
' + } + else { + Write-Warning -Message "Could not determine the Impact from the Threats for $($v.CVE)" + $impactColumn = 'Unknown' + } - $vulnDescriptionColumnTemplate = @' + $vulnDescriptionColumnTemplate = @' CVE Title: {0}
Weakness: {7} @@ -252,32 +254,33 @@ Process {
'@ - $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 + $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('/') + if (($null -ne $highestCvssScore) -and ($null -ne $highestCvssScore.Vector) -and ($highestCvssScore.Vector.Split('/').Length -gt 1)) { + $cvssArray = $highestCvssScore.Vector.Split('/') - $cvssScoreTemplate = @' + $cvssScoreTemplate = @'
{0} @@ -296,131 +299,139 @@ Process { {2}
'@ - $cvssScoreSet = $cvssScoreTemplate -f @( - $rowTemplate = '{1}{3}' + $cvssScoreSet = $cvssScoreTemplate -f @( + $rowTemplate = '{1}{3}' - $baseTags = 'AC', 'AV', 'A', 'C', 'I', 'PR', 'S', 'UI' - $temporalTags = 'E', 'RC', 'RL' - $baseRows = '' - $temporalRows = '' - for($i = 1; $i -lt $cvssArray.Length; $i++) { + $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] + $element = $cvssArray[$i] + $split0 = $element.Split(':')[0] - $metric = $JsonMetrics.$split0 - $value = $JsonMetrics.$element + $metric = $JsonMetrics.$split0 + $value = $JsonMetrics.$element - $metricDescription = $JsonDescriptions.$split0 - $valueDescription = $JsonDescriptions.$element + $metricDescription = $JsonDescriptions.$split0 + $valueDescription = $JsonDescriptions.$element - $row = '' + $metric + '' - $row += '' + $value + '' + $row = '' + $metric + '' + $row += '' + $value + '' - 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 + 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 = '{0} Highest BaseScore:{1}/TemporalScore:{2}' -f $cvssArray[0], $highestCvssScore.BaseScore, $highestCvssScore.TemporalScore - $formattedScore, $baseRows, $temporalRows - ) - $cvssScoreSet - } else { - 'None' -join '
' - } - ), - # $cveFaq - $( - if ($cveFaq = ($v.Notes | Where-Object {$_.Title -eq 'FAQ'}).Value) { - $cveFaq -join '
' - } else { - 'None' -join '
' - } - ), - # $cveMitigation - $( - if ($cveMitigation = $v.Remediations | Where-Object { $_.Type -eq $RemediationsMitigationType }) { - $cveMitigation.Description.Value -join '
' - - } else { - 'None' -join '
' - } - ), - # $cveWorkaround - $( - if ( $cveWorkaround = ($v.Remediations | Where-Object {$_.Type -eq $RemediationsWorkaroundType }).Description.Value) { - $cveWorkaround -join '
' - } else { - 'None' -join '
' - } - ), - # $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 '    ') + $formattedScore, $baseRows, $temporalRows + ) + $cvssScoreSet } - } - if ( $RevisionStrings ) { - $RevisionStrings -join '
' - } else { - 'Unknown' -join '
' - } - ), - # Executive Summary - $( - if ($cveExecSummary = ($v.Notes | Where-Object {$_.Title -eq 'Description'}).Value) { - $cveExecSummary -join '
' - } else { - 'None' -join '
' - } - ) - # CWE Weakness - $( - if ($cveWeakness = $(if ($v.CWE) { '{0} : {1}' -f "$($v.CWE.ID)","$($v.CWE.Value)"})) { - $cveWeakness -join '
' - } else { - 'N/A' -join '
' - } - ) - # Customer Action Required - $( - if ($cveCustomerActionRequired = $v.Notes | Where-Object { $_.Title -eq "Customer Action Required" }) { - $cveCustomerActionRequired.Value -join '
' - } else { - 'Yes' -join '
' - } - ) + else { + 'None' -join '
' + } + ), + # $cveFaq + $( + if ($cveFaq = ($v.Notes | Where-Object { $_.Title -eq 'FAQ' }).Value) { + $cveFaq -join '
' + } + else { + 'None' -join '
' + } + ), + # $cveMitigation + $( + if ($cveMitigation = $v.Remediations | Where-Object { $_.Type -eq $RemediationsMitigationType }) { + $cveMitigation.Description.Value -join '
' - ) + } + else { + 'None' -join '
' + } + ), + # $cveWorkaround + $( + if ( $cveWorkaround = ($v.Remediations | Where-Object { $_.Type -eq $RemediationsWorkaroundType }).Description.Value) { + $cveWorkaround -join '
' + } + else { + 'None' -join '
' + } + ), + # $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 '
' + } + else { + 'Unknown' -join '
' + } + ), + # Executive Summary + $( + if ($cveExecSummary = ($v.Notes | Where-Object { $_.Title -eq 'Description' }).Value) { + $cveExecSummary -join '
' + } + else { + 'None' -join '
' + } + ) + # CWE Weakness + $( + if ($cveWeakness = $(if ($v.CWE) { '{0} : {1}' -f "$($v.CWE.ID)", "$($v.CWE.Value)" })) { + $cveWeakness -join '
' + } + else { + 'N/A' -join '
' + } + ) + # Customer Action Required + $( + if ($cveCustomerActionRequired = $v.Notes | Where-Object { $_.Title -eq "Customer Action Required" }) { + $cveCustomerActionRequired.Value -join '
' + } + else { + 'Yes' -join '
' + } + ) + ) - $cveSectionHtml += '

{0} - {1}

(
top)' -f $v.CVE, $cveTitle + $cveSectionHtml += '

{0} - {1}

(top)' -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 + #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 @( - @" + $cveSectionHtml += $cveSummaryTableHtml -f @( + @" $($_.CVE)
MITRE @@ -428,14 +439,24 @@ Process { NVD

Issuing CNA: $($($v.Notes | Where-Object type -eq $CNAType).Value)

"@, - $vulnDescriptionColumn, - $MaximumSeverity, - $impactColumn - ) - #endregion - - #region Exploitability Index Table - $exploitabilityIndexTableHtml = @' + $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 = @'

Exploitability Index

The following table provides an exploitability assessment for this vulnerability at the time of original publication.

@@ -456,43 +477,47 @@ Process { '@ - 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' - } + 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 + #endregion - #region Affected Software Table + #region Affected Software Table - $affectedSoftwareTableTemplate = @' + $affectedSoftwareTableTemplate = @' @@ -515,7 +540,7 @@ Process {
'@ - $affectedSoftwareRowTemplate = @' + $affectedSoftwareRowTemplate = @' @@ -529,117 +554,127 @@ Process { '@ - $cveSectionHtml += @' + $cveSectionHtml += @'

Affected Software

The following tables list the affected software details for the vulnerability.

'@ - $affectedSoftware = Get-MsrcCvrfAffectedSoftware -Vulnerability $v -ProductTree $ProductTree - $affectedSoftwareTableHtml = '' + $affectedSoftware = Get-MsrcCvrfAffectedSoftware -Vulnerability $v -ProductTree $ProductTree + $affectedSoftwareTableHtml = '' - $affectedSoftware.FullProductName | Sort-Object -Unique | ForEach-Object { + $affectedSoftware.FullProductName | Sort-Object -Unique | ForEach-Object { - $PN = $_ + $PN = $_ - $affectedSoftware | Where-Object {$_.FullProductName -eq $PN} | ForEach-Object { + $affectedSoftware | Where-Object { $_.FullProductName -eq $PN } | ForEach-Object { - $affectedSoftwareTableHtml += $affectedSoftwareRowTemplate -f @( - $PN, - $( -if ($PN -eq 'Microsoft Edge (Chromium-based)') { - @( - '{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 '
' - } - ), - $( - if (-not($_.Impact)) { - 'Unknown' - } else { - $($_.Impact | Select-Object -Unique) -join '
' - } - ), - $( - if (-not($_.Supercedence)) { - 'None' - } else { - $($_.Supercedence | Select-Object -Unique) -join '
' - } - ), - $( - - 'Base: {0}
Temporal: {1}
Vector: {2}
' -f ( - $( - if(-not($_.CvssScoreSet.base)) { - 'N/A' - } else{ - $_.CvssScoreSet.base - } - ) + $affectedSoftwareTableHtml += $affectedSoftwareRowTemplate -f @( + $PN, + $( + if ($PN -eq 'Microsoft Edge (Chromium-based)') { + @( + '
{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($_.CvssScoreSet.temporal)) { - 'N/A' - } else { - $_.CvssScoreSet.temporal - } - ) + $( + if (-not($_.Severity)) { + 'Unknown' + } + else { + $($_.Severity | Select-Object -Unique) -join '
' + } ), - ( - $( - if(-not($_.CvssScoreSet.vector)) { - 'N/A' - } else { - $_.CvssScoreSet.vector - } + $( + if (-not($_.Impact)) { + 'Unknown' + } + else { + $($_.Impact | Select-Object -Unique) -join '
' + } + ), + $( + if (-not($_.Supercedence)) { + 'None' + } + else { + $($_.Supercedence | Select-Object -Unique) -join '
' + } + ), + $( + + 'Base: {0}
Temporal: {1}
Vector: {2}
' -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 '
' + } + ), + $( + if (-not($_.RestartRequired)) { + 'Unknown' + } + else { + $($_.RestartRequired | Select-Object -Unique) -join '
' + } + ), + $( + if (-not($_.'Known Issue')) { + 'None' + } + else { + $_.'Known Issue' | Get-KBDownloadUrl + } ) - ), - $( - if (-not($_.FixedBuild)) { - 'Unknown' - } else { - $($_.FixedBuild | Select-Object -Unique) -join '
' - } - ), - $( - if (-not($_.RestartRequired)) { - 'Unknown' - } else { - $($_.RestartRequired | Select-Object -Unique) -join '
' - } - ), - $( - if (-not($_.'Known Issue')) { - 'None' - } else { - $_.'Known Issue' | Get-KBDownloadUrl - } ) - ) + } } - } - $cveSectionHtml += $affectedSoftwareTableTemplate -f @( - $v.CVE, - $affectedSoftwareTableHtml - ) - #endregion + $cveSectionHtml += $affectedSoftwareTableTemplate -f @( + $v.CVE, + $affectedSoftwareTableHtml + ) + #endregion - #region Acknowledgments Table - $acknowledgmentsTableTemplate = @' + #region Acknowledgments Table + $acknowledgmentsTableTemplate = @'

Acknowledgements

{0} {1}
@@ -655,47 +690,48 @@ if ($PN -eq 'Microsoft Edge (Chromium-based)') {
'@ - if ($v.Acknowledgments) { - $ackVal = '' - $v.Acknowledgments | ForEach-Object { + if ($v.Acknowledgments) { + $ackVal = '' + $v.Acknowledgments | ForEach-Object { - if ($_.Name.Value) { - $ackVal += $_.Name.Value - $ackVal += '
' - } - if ($_.URL) { - $ackVal += $_.URL - $ackVal += '
' + if ($_.Name.Value) { + $ackVal += $_.Name.Value + $ackVal += '
' + } + if ($_.URL) { + $ackVal += $_.URL + $ackVal += '
' + } + $ackVal += '

' } - $ackVal += '

' } - } else { - Write-Warning -Message "No Acknowledgments for $($v.CVE)" - $ackVal = 'None' + 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 - $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 { - '{3}{0}
{1} {2}' -f $_.Tag,$_.CVEID,$_.CVETitle,$_.CNA - }) -join "`n"), - $cveSectionHtml, - "$($MyInvocation.MyCommand.Version.ToString())", - $css,$global:msrcApiUrl + ( + $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 { + '{3}{0} {1} {2}' -f $_.Tag, $_.CVEID, $_.CVETitle, $_.CNA + }) -join "`n"), + $cveSectionHtml, + "$($MyInvocation.MyCommand.Version.ToString())", + $css, $global:msrcApiUrl + ) ) - ) -} -End {} + } + End {} } diff --git a/src/MsrcSecurityUpdates/Public/Set-MSRCApiKey.ps1 b/src/MsrcSecurityUpdates/Public/Set-MSRCApiKey.ps1 index e2c132a..3b629be 100644 --- a/src/MsrcSecurityUpdates/Public/Set-MSRCApiKey.ps1 +++ b/src/MsrcSecurityUpdates/Public/Set-MSRCApiKey.ps1 @@ -45,9 +45,8 @@ Process { Write-Verbose -Message "Successfully defined a msrcProxyCredential global variable that points to $($global:msrcProxy)" } - if ($global:MSRCAdalAccessToken) - { - Remove-Variable -Name MSRCAdalAccessToken -Scope Global + if ($script:MSRCMsalAccessToken){ + Remove-Variable -Name MSRCMsalAccessToken -Scope Script } } } diff --git a/src/MsrcSecurityUpdates/Public/Set-MsrcAdalAccessToken.ps1 b/src/MsrcSecurityUpdates/Public/Set-MsrcAdalAccessToken.ps1 deleted file mode 100644 index 9ec6c90..0000000 --- a/src/MsrcSecurityUpdates/Public/Set-MsrcAdalAccessToken.ps1 +++ /dev/null @@ -1,45 +0,0 @@ -Function Set-MSRCAdalAccessToken { -[CmdletBinding(SupportsShouldProcess)] -Param() -Begin {} -Process { - if ([AppDomain]::CurrentDomain.SetupInformation.TargetFrameworkName -like "*v5.*") { - throw ".Net Core v5.x is not currently supported" - } - - if ($PSCmdlet.ShouldProcess('Set the MSRCApiKey using MSRCAdalAccessToken')) { - Add-Type -Path "$PSScriptRoot/../Microsoft.IdentityModel.Clients.ActiveDirectory.dll" -ErrorAction SilentlyContinue -WarningAction SilentlyContinue - - $authority = 'https://login.windows.net/microsoft.onmicrosoft.com/' - - $authContext = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext($authority) - - $rUri = New-Object System.Uri -ArgumentList 'https://msrc-api-powershell' - - $promptBehavior = [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto - - - $ResourceId = 'https://msrc-api-prod.azurewebsites.net' - - $ClientId = 'c7fe3b9e-4d97-462d-ae1b-c16e679be355' - - $global:MSRCAdalAccessToken = $null - - if ($null -ne $authContext.AcquireToken) { - $global:MSRCAdalAccessToken = $authContext.AcquireToken($ResourceId, $ClientId, $rUri,$promptBehavior) - } elseif ($null -ne $authContext.AcquireTokenAsync) { - $platformParams = New-Object Microsoft.IdentityModel.Clients.ActiveDirectory.PlatformParameters($promptBehavior) - $task = $authContext.AcquireTokenAsync($ResourceId, $ClientId, $rUri,$platformParams) - $task.Wait() - $global:MSRCAdalAccessToken = $task.Result - } - - if ($null -ne $global:MSRCAdalAccessToken) { - Write-Verbose -Message "Successfully set your Access Token required by cmdlets of this module. Calls to the MSRC APIs will now use your access token." - } else { - throw "Failed Acquiring Access Token!" - } - } -} -End {} -} diff --git a/src/MsrcSecurityUpdates/Public/Set-MsrcMsalAccessToken.ps1 b/src/MsrcSecurityUpdates/Public/Set-MsrcMsalAccessToken.ps1 new file mode 100644 index 0000000..dbf142c --- /dev/null +++ b/src/MsrcSecurityUpdates/Public/Set-MsrcMsalAccessToken.ps1 @@ -0,0 +1,55 @@ +Function Set-MSRCMsalAccessToken { +[CmdletBinding(SupportsShouldProcess)] +Param( + [Parameter()] + [Alias('ClientId')] + [string]$ID, + + [Parameter()] + [string]$TenantId = 'microsoft.onmicrosoft.com', + + [Parameter()] + [string]$RedirectUri = 'http://localhost:50000' +) +Begin {} +Process { + if ($PSCmdlet.ShouldProcess('Set the MSRCApiKey using MSRCMsalAccessToken')) { + # Check if MSAL.PS module is available + if (-not (Get-Module -ListAvailable -Name MSAL.PS)) { + throw "MSAL.PS module is required. Please install it using: Install-Module -Name MSAL.PS" + } + + Import-Module MSAL.PS -ErrorAction Stop + + # Clear any existing cached token + $script:MSRCMsalAccessToken = $null + + try { + # Use MSAL.PS to acquire token interactively + $msalParams = @{ + ClientId = $ID + TenantId = $TenantId + Scopes = @("$ID/.default") + RedirectUri = $RedirectUri + Interactive = $true + } + + $tokenResult = Get-MsalToken @msalParams + + if ($tokenResult -and $tokenResult.AccessToken) { + # Store only the access token string, not the full object + $script:MSRCMsalAccessToken = @{ + AccessToken = $tokenResult.AccessToken + } + Write-Verbose -Message "Successfully set your Access Token required by cmdlets of this module. Calls to the MSRC APIs will now use your access token." + } else { + throw "Failed Acquiring Access Token!" + } + } + catch { + throw + } + } +} +End {} +} diff --git a/src/README.md b/src/README.md index fcd8ba3..6ee694b 100644 --- a/src/README.md +++ b/src/README.md @@ -44,16 +44,16 @@ Get-Command -Module MsrcSecurityUpdates CommandType Name Version Source ----------- ---- ------- ------ -Function Get-KBDownloadUrl 1.8.7 MsrcSecurityUpdates -Function Get-MsrcCvrfAffectedSoftware 1.8.7 MsrcSecurityUpdates -Function Get-MsrcCvrfCVESummary 1.8.7 MsrcSecurityUpdates -Function Get-MsrcCvrfDocument 1.8.7 MsrcSecurityUpdates -Function Get-MsrcCvrfExploitabilityIndex 1.8.7 MsrcSecurityUpdates -Function Get-MsrcSecurityBulletinHtml 1.8.7 MsrcSecurityUpdates -Function Get-MsrcSecurityUpdate 1.8.7 MsrcSecurityUpdates -Function Get-MsrcVulnerabilityReportHtml 1.8.7 MsrcSecurityUpdates -Function Set-MSRCAdalAccessToken 1.8.7 MsrcSecurityUpdates -Function Set-MSRCApiKey 1.8.7 MsrcSecurityUpdates +Function Get-KBDownloadUrl 2.0.0 MsrcSecurityUpdates +Function Get-MsrcCvrfAffectedSoftware 2.0.0 MsrcSecurityUpdates +Function Get-MsrcCvrfCVESummary 2.0.0 MsrcSecurityUpdates +Function Get-MsrcCvrfDocument 2.0.0 MsrcSecurityUpdates +Function Get-MsrcCvrfExploitabilityIndex 2.0.0 MsrcSecurityUpdates +Function Get-MsrcSecurityBulletinHtml 2.0.0 MsrcSecurityUpdates +Function Get-MsrcSecurityUpdate 2.0.0 MsrcSecurityUpdates +Function Get-MsrcVulnerabilityReportHtml 2.0.1 MsrcSecurityUpdates +Function Set-MSRCMsalAccessToken 2.0.0 MsrcSecurityUpdates +Function Set-MSRCApiKey 2.0.0 MsrcSecurityUpdates ``` ## Generating a HTML document of Monthly Updates