Public/Get-NSPFortinetFirmware.ps1
|
function Get-NSPFortinetFirmware { <# .SYNOPSIS Lists firmware files on the Fortinet support portal for a product, release and model: the latest patch of a branch (7.6) or an exact one (7.6.7). .DESCRIPTION Reads the portal's firmware download tree (the Downloads > Firmware Download page), which is laid out as <Product>\v<major>.00\<branch>\<patch>, for example FortiGate\v7.00\7.6\7.6.7. Run Connect-NSPFortinetSupport first. Nothing is downloaded; pipe the result to Save-NSPFortinetFirmware. Returns one object per file: Product, Version the product and the patch folder (7.6.7) Model the model from the file name: FGT_1001F-v7.6.7... gives 1001F; other families keep their prefix (FWF_60F, FGR_60F); empty for other files Name, SizeKB, Date as the portal lists them HasChecksum whether the portal publishes a checksum for the file FilePath the portal path, used to request the download .PARAMETER Product The portal's product name, as the firmware page lists it. Default FortiGate. .PARAMETER Version A branch (7.6: its newest patch) or a patch (7.6.7). Several are allowed. Fortinet publishes a patch's images in stages, so with -Model a model missing from the newest patch comes from the newest of the last 3 patches that has it (7.6.7 until 7.6.8 has it). .PARAMETER Model Models to keep, matched against Model or the file name's prefix_model (1001F, FWF_60F, VM64*, FGT_100F). Wildcards work. A file with no model in its name (release notes, MIBs) is left out when this is used. .PARAMETER Filter Wildcards on the whole file name (*.out). Applied after -Model. .PARAMETER Path A portal folder to list directly, for products laid out differently (FortiGate\v7.00\7.6\7.6.7). Replaces -Version. .EXAMPLE Get-NSPFortinetFirmware -Version 7.6 -Model 1001F, 100F .EXAMPLE Get-NSPFortinetFirmware -Version 7.4, 7.6 -Model 60F -Filter *.out | Save-NSPFortinetFirmware -Destination D:\Firmware .EXAMPLE Get-NSPFortinetFirmware -Version 7.6.7 | Out-GridView -PassThru | Save-NSPFortinetFirmware -Destination D:\Firmware #> [CmdletBinding(DefaultParameterSetName = 'Version')] [OutputType([pscustomobject])] param( [Parameter(ValueFromPipelineByPropertyName)][string]$Product = 'FortiGate', [Parameter(ParameterSetName = 'Version', Mandatory, ValueFromPipelineByPropertyName)][ValidatePattern('^\d+\.\d+(\.\d+)?$')][string[]]$Version, [Parameter(ValueFromPipelineByPropertyName)][string[]]$Model, [Parameter(ValueFromPipelineByPropertyName)][string[]]$Filter, [Parameter(ParameterSetName = 'Path', Mandatory)][string]$Path ) process { $list = { param([string]$Folder) Invoke-NSPFortinetSupportApi -Path 'ticket/api/tk/download/firmwareDownloadInfo' -Body @{ productId = $Product; relativePath = $Folder } } # Each request is a list of folders to try, newest first. Only a branch has more than one. $requests = @(if ($PSCmdlet.ParameterSetName -eq 'Path') { [pscustomobject]@{ Folders = @($Path.Trim('\')) } } else { foreach ($wanted in $Version) { $parts = $wanted.Split('.') $branch = "$($parts[0]).$($parts[1])" # Walk down: the branch folder may sit under v<major>.00 or straight under the product. $current = $Product $inMajorFolder = $false while ($true) { $names = @((& $list $current).data | Where-Object { -not $_.isFile } | ForEach-Object name) if ($names -contains $branch) { $current = "$current\$branch"; break } $majorFolder = @($names | Where-Object { $_ -match "^v$($parts[0])\.\d+$" } | Select-Object -First 1) if (-not $majorFolder.Count -or $inMajorFolder) { throw "$Product has no $branch folder under '$current' (found: $($names -join ', '))." } $current = "$current\$($majorFolder[0])" $inMajorFolder = $true } $patches = @((& $list $current).data | Where-Object { -not $_.isFile -and $_.name -match "^$([regex]::Escape($branch))\.\d+$" } | ForEach-Object name) if ($parts.Count -eq 3) { if ($patches -notcontains $wanted) { throw "$Product $wanted is not on the portal (under '$current': $($patches -join ', '))." } [pscustomobject]@{ Folders = @("$current\$wanted") } } else { if (-not $patches.Count) { throw "$Product $branch has no patch folders under '$current'." } # Fortinet publishes a patch's images in stages, so a model may only be in an # earlier patch for a while: with -Model, look back up to $lookBack patches. $lookBack = if ($Model) { 3 } else { 1 } $newest = @($patches | Sort-Object { [version]$_ } -Descending | Select-Object -First $lookBack) [pscustomobject]@{ Folders = @($newest | ForEach-Object { "$current\$_" }) } } } }) foreach ($request in $requests) { $found = [Collections.Generic.List[string]]::new() foreach ($folder in $request.Folders) { # Stop once every -Model pattern has a file from a newer patch. if ($found.Count -and -not @($Model | Where-Object { $pattern = $_; -not @($found | Where-Object { $_ -like $pattern }).Count }).Count) { break } $patch = Split-Path -Leaf $folder $fromThisPatch = [Collections.Generic.List[string]]::new() foreach ($item in @((& $list $folder).data | Where-Object isFile)) { # FGT_1001F-v7.6.7... is 1001F. Other families in the same folder keep their prefix # (FWF_60F, FGR_60F), so a FortiWiFi 60F is never mistaken for a FortiGate 60F. $fileModel = '' $fullModel = '' if ($item.name -match '^(?<family>[A-Za-z0-9]+)_(?<model>[^-]+?)-v\d') { $fullModel = "$($Matches['family'])_$($Matches['model'])" $fileModel = if ($Matches['family'] -eq 'FGT') { $Matches['model'] } else { $fullModel } } if ($Model -and -not ($fileModel -and @($Model | Where-Object { $fileModel -like $_ -or $fullModel -like $_ }).Count)) { continue } if ($Filter -and -not @($Filter | Where-Object { $item.name -like $_ }).Count) { continue } # A model a newer patch already supplied is not taken again from an older one. if ($fileModel -and $found.Contains($fileModel)) { continue } if ($fileModel) { $fromThisPatch.Add($fileModel) } [pscustomobject]@{ PSTypeName = 'NSP.FortiGate.FortinetFirmware' Product = $Product Version = $patch Model = $fileModel Name = $item.name SizeKB = [long]$item.size Date = $item.dateModified HasChecksum = [bool]$item.displayChecksum FilePath = "$folder\$($item.name)" } } foreach ($name in $fromThisPatch) { if (-not $found.Contains($name)) { $found.Add($name) } } } } } } |