diff --git a/MSCatalog/Classes/MSCatalogUpdate.Class.ps1 b/MSCatalog/Classes/MSCatalogUpdate.Class.ps1 index 75db9c5..a0940af 100644 --- a/MSCatalog/Classes/MSCatalogUpdate.Class.ps1 +++ b/MSCatalog/Classes/MSCatalogUpdate.Class.ps1 @@ -28,4 +28,89 @@ class MSCatalogUpdate { } } } -} \ No newline at end of file +} + + +class MSCatalogUpdateWithDetails : MSCatalogUpdate { + + [string] $UpdateId + [string] $Description + [string] $Architecture + [string] $Classification + [string] $SupportedProducts + [string] $SupportedLanguages + [string] $MsrcNumber + [string] $MsrcSeverity + [string] $KbArticle + [string] $MoreInformationUrl + [string] $SupportUrl + [PSCustomObject[]] $SupersededBy + [PSCustomObject[]] $Supersedes + [string] $RestartBehavior + [string] $UserInput + [string] $Exclusive + [string] $RequiresNetwork + [string] $UninstallNotes + [string] $UninstallSteps + + MSCatalogUpdateWithDetails($Row, $IncludeFileNames) : base($Row, $IncludeFileNames) { + $HtmlDocs = Invoke-CatalogItemDetails($this.Guid) + + ## Overview Section + $HtmlDocOverview = $HtmlDocs["Overview"] + $this.UpdateId = $HtmlDocOverview.GetElementbyId("ScopedViewHandler_UpdateID").InnerText.Trim() + $this.Description = $HtmlDocOverview.GetElementbyId("ScopedViewHandler_desc").InnerText.Trim() + $this.Architecture = $HtmlDocOverview.GetElementbyId("archDiv").ChildNodes[2].InnerText.Trim() + $this.Classification = $HtmlDocOverview.GetElementbyId("classificationDiv").ChildNodes[2].InnerText.Trim() + $this.SupportedProducts = $HtmlDocOverview.GetElementbyId("productsDiv").ChildNodes[2].InnerText.Trim() -replace '\s(?=\s|,)','' #removes whitespaces followed by whitspaces or commas. + $this.SupportedLanguages = $HtmlDocOverview.GetElementbyId("languagesDiv").ChildNodes[2].InnerText.Trim() -replace '\s(?=\s|,)','' + $this.MsrcNumber = $HtmlDocOverview.GetElementbyId("securityBullitenDiv").ChildNodes[2].InnerText.Trim() + $this.MsrcSeverity = $HtmlDocOverview.GetElementbyId("ScopedViewHandler_msrcSeverity").InnerText.Trim() + $this.KbArticle = $HtmlDocOverview.GetElementbyId("kbDiv").ChildNodes[2].InnerText.Trim() + $this.MoreInformationUrl = $HtmlDocOverview.GetElementbyId("moreInfoDiv").SelectNodes("div").InnerText.Trim() + $this.SupportUrl = $HtmlDocOverview.GetElementbyId("suportUrlDiv").SelectNodes("div").InnerText.Trim() + + ## Package Details Section + $HtmlDocDetails = $HtmlDocs["PackageDetails"] + $SupersededByBox = $HtmlDocDetails.GetElementbyId("supersededbyInfo").SelectNodes("div") + $this.SupersededBy = [MSCatalogItemReferenceList]::new($SupersededByBox).ItemReferenceList + $SupersedesBox = $HtmlDocDetails.GetElementbyId("supersedesInfo").SelectNodes("div") + $this.Supersedes = [MSCatalogItemReferenceList]::new($SupersedesBox).ItemReferenceList + + ## Install Details Section + $HtmlDocInstall = $HtmlDocs["InstallDetails"] + $this.RestartBehavior = $HtmlDocInstall.GetElementbyId("ScopedViewHandler_rebootBehavior").InnerText.Trim() + $this.UserInput = $HtmlDocInstall.GetElementbyId("ScopedViewHandler_userInput").InnerText.Trim() + $this.Exclusive = $HtmlDocInstall.GetElementbyId("ScopedViewHandler_installationImpact").InnerText.Trim() + $this.RequiresNetwork = $HtmlDocInstall.GetElementbyId("ScopedViewHandler_connectivity").InnerText.Trim() + $this.UninstallNotes = $HtmlDocInstall.GetElementbyId("uninstallNotesDiv").SelectNodes("div").InnerText.Trim() + $this.UninstallSteps = $HtmlDocInstall.GetElementbyId("uninstallStepsDiv").SelectNodes("div").InnerText.Trim() + } +} + +class MSCatalogItemReferenceList { + [PSCustomObject[]] $ItemReferenceList = @() + + MSCatalogItemReferenceList($InfoBox) { + + foreach ($node in $InfoBox) { + $ItemReference = [PSCustomObject]@{ + Name = $node.InnerText.Trim() + KbArticle = [string]::Empty + UpdateID = [string]::Empty + } + + $ItemReference.KbArticle = if ($ItemReference.Name -match 'KB\d+'){ + $Matches.0 + } + $ItemReference.UpdateID = if ($node.SelectNodes("a").Count -eq 1) { + + $href = $node.SelectNodes("a").GetAttributeValue("href", [string]::Empty) + if ($href.contains("updateid")) { + ($href -split ("updateid="))[1].Trim() + } + } + $this.ItemReferenceList += $ItemReference + } + } +} diff --git a/MSCatalog/Private/Invoke-CatalogItemDetails.ps1 b/MSCatalog/Private/Invoke-CatalogItemDetails.ps1 new file mode 100644 index 0000000..19b6ad3 --- /dev/null +++ b/MSCatalog/Private/Invoke-CatalogItemDetails.ps1 @@ -0,0 +1,48 @@ + +function Invoke-CatalogItemDetails { + + [CmdletBinding()] + param ( + [parameter(Mandatory = $true)] + [string] $UpdateId + ) + + + + $BaseUri = "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$([uri]::EscapeDataString($UpdateId))" + $Tabs = @("Overview", "LanguageSelection", "PackageDetails", "InstallDetails") + $Params = @{ + ContentType = "application/x-www-form-urlencoded" + UseBasicParsing = $true + ErrorAction = "Stop" + } + $HtmlDocs = @{} + + + + foreach ($tab in $Tabs) { + try { + Set-TempSecurityProtocol + + $response = Invoke-WebRequest -Uri $BaseUri+"#"+$tab @Params + $HtmlTab = [HtmlAgilityPack.HtmlDocument]::new() + $HtmlTab.LoadHtml($response.RawContent.ToString()) + + $CouldNotBeFound = $HtmlTab.GetElementbyId("ctl00_catalogBody_thanksNoUpdate") + if ($null -eq $CouldNotBeFound) { + + $HtmlDocs[$tab] = $HtmlTab + } + else { + throw "Could not retrieve details for the Update with ID $UpdateId" + } + + Set-TempSecurityProtocol -ResetToDefault + }catch { + throw $_ + } + } + + $HtmlDocs + +} \ No newline at end of file diff --git a/MSCatalog/Public/Get-MSCatalogUpdate.ps1 b/MSCatalog/Public/Get-MSCatalogUpdate.ps1 index 09b4264..e11a7b3 100644 --- a/MSCatalog/Public/Get-MSCatalogUpdate.ps1 +++ b/MSCatalog/Public/Get-MSCatalogUpdate.ps1 @@ -27,6 +27,9 @@ function Get-MSCatalogUpdate { .PARAMETER ExcludePreview Exclude preview updates from the search results. + .PARAMETER IncludeDetails + Includes the details of the Updates, such as Description, Architecture, Classification, Supported products and languages, MSRC number, etc. + .PARAMETER AllPages By default the Get-MSCatalogUpdate command returns the first page of results from catalog.update.micrsosoft.com, which is limited to 25 updates. If you specify this switch the command will instead return all pages of search results. @@ -69,6 +72,9 @@ function Get-MSCatalogUpdate { [Parameter(Mandatory = $false)] [switch] $ExcludePreview, + [Parameter(Mandatory = $false)] + [switch] $IncludeDetails, + [Parameter(Mandatory = $false)] [switch] $AllPages ) @@ -155,7 +161,15 @@ function Get-MSCatalogUpdate { if ($Rows.Count -gt 0) { foreach ($Row in $Rows) { if ($Row.Id -ne "headerRow") { - [MSCatalogUpdate]::new($Row, $IncludeFileNames) + if ($IncludeDetails) { + + Write-Progress -Activity "Parsing Updates from Catalog" -CurrentOperation $Row.Id + [MSCatalogUpdateWithDetails]::new($Row, $IncludeFileNames) + } + else { + [MSCatalogUpdate]::new($Row, $IncludeFileNames) + } + } } } else { diff --git a/README.md b/README.md index 4e0c2b2..9bbb00c 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,13 @@ Get-MSCatalogUpdate -Search "Cumulative Update for Windows Server 2016 (1803)" - **NOTE: This could cause a significant number of web requests. The catalog website will only provide 25 results at a time and this would just keep looping over all available results until it reaches the maximum of 1000.** +In order to obtain more details for each patch, use the parameter `IncludeDetails`. For each search result, addtional details such as architecture, KB Article, MSRC references and supersedence information are parsed as well. Be aware that this increases the amount of requests by a factor of three. All details are avaible in the returned objects, but are not displayed by default. + +```powershell +Get-MSCatalogUpdate -Search "Cumulative Update for Windows Server 2016 (1803)" -IncludeDetails +``` + + ## Save-MSCatalogUpdate This command is used to download update files from the [https://www.catalog.update.microsoft.com](https://www.catalog.update.microsoft.com)