NestedModules/HttpConnectivityTester/HttpConnectivityTester.psm1
Set-StrictMode -Version 4 $rateLimitCount = 0 $sleepSeconds = 5 * 60 Function Get-ErrorMessage() { <# .SYNOPSIS Gets a formatted error message from an error record. .DESCRIPTION Gets a formatted error message from an error record. .EXAMPLE Get-ErrorMessage -ErrorRecords $_ #> [CmdletBinding()] [OutputType([string])] Param( [Parameter(Mandatory=$true, HelpMessage='The PowerShell error record object to get information from')] [ValidateNotNullOrEmpty()] [System.Management.Automation.ErrorRecord]$ErrorRecord ) Process { $msg = [System.Environment]::NewLine,'Exception Message: ',$ErrorRecord.Exception.Message -join '' if($null -ne $ErrorRecord.Exception.HResult) { $msg = $msg,[System.Environment]::NewLine,'Exception HRESULT: ',('{0:X}' -f $ErrorRecord.Exception.HResult),$ErrorRecord.Exception.HResult -join '' } if($null -ne $ErrorRecord.Exception.StackTrace) { $msg = $msg,[System.Environment]::NewLine,'Exception Stacktrace: ',$ErrorRecord.Exception.StackTrace -join '' } if ($null -ne ($ErrorRecord.Exception | Get-Member | Where-Object { $_.Name -eq 'WasThrownFromThrowStatement'})) { $msg = $msg,[System.Environment]::NewLine,'Explicitly Thrown: ',$ErrorRecord.Exception.WasThrownFromThrowStatement -join '' } if ($null -ne $ErrorRecord.Exception.InnerException) { if ($ErrorRecord.Exception.InnerException.Message -ne $ErrorRecord.Exception.Message) { $msg = $msg,[System.Environment]::NewLine,'Inner Exception: ',$ErrorRecord.Exception.InnerException.Message -join '' } if($null -ne $ErrorRecord.Exception.InnerException.HResult) { $msg = $msg,[System.Environment]::NewLine,'Inner Exception HRESULT: ',('{0:X}' -f $ErrorRecord.Exception.InnerException.HResult),$ErrorRecord.Exception.InnerException.HResult -join '' } } $msg = $msg,[System.Environment]::NewLine,'Call Site: ',$ErrorRecord.InvocationInfo.PositionMessage -join '' if ($null -ne ($ErrorRecord | Get-Member | Where-Object { $_.Name -eq 'ScriptStackTrace'})) { $msg = $msg,[System.Environment]::NewLine,"Script Stacktrace: ",$ErrorRecord.ScriptStackTrace -join '' } return $msg } } Function Get-BlueCoatSiteReview() { [CmdletBinding()] [OutputType([psobject])] Param ( [Parameter(Mandatory=$true, HelpMessage='The URL to get BlueCoat Site Review information for.')] [ValidateNotNullOrEmpty()] [Uri]$Url, [Parameter(Mandatory=$false, HelpMessage='The user agent.')] [ValidateNotNullOrEmpty()] [string]$UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36', [Parameter(Mandatory=$false, HelpMessage='Disable throttling.')] [switch]$NoThrottle ) if ($Url.OriginalString.ToLower().StartsWith('http://') -or $Url.OriginalString.ToLower().StartsWith('https://')) { $testUri = $Url } else { $testUri = [Uri]('http://{0}' -f $Url.OriginalString) } $newLine = [System.Environment]::NewLine $throttle = !$NoThrottle if ($throttle) { $rateLimitCount++ if($rateLimitCount -gt 10) { $nowTime = [DateTime]::Now $resumeTime = $nowTime.AddSeconds($sleepSeconds) Write-Verbose -Message ('Paused for {0} seconds. Current time: {1} Resume time: {2}' -f $sleepSeconds,$nowTime,$resumeTime) Start-Sleep -Seconds $sleepSeconds $nowTime = [DateTime]::Now Write-Verbose -Message ('Resumed at {0}' -f $nowTime) $rateLimitCount = 1 # needs to be 1 since BlueCoat Site Review API is called when exiting this if statement. If left at 0, then will hit the rate limit on successive calls to this cmdlet } } $uri = $testUri $proxyUri = [System.Net.WebRequest]::GetSystemWebProxy().GetProxy($uri) $params = @{ Uri = 'https://sitereview.bluecoat.com/resource/lookup'; Method = 'POST'; ProxyUseDefaultCredentials = (([string]$proxyUri) -ne $uri); UseBasicParsing = $true; UserAgent = $UserAgent ContentType = 'application/json'; Body = (@{url = $uri; captcha = ''} | ConvertTo-Json); Headers = @{Referer = 'https://sitereview.bluecoat.com'} ; Verbose = $false } if (([string]$proxyUri) -ne $uri) { $params.Add('Proxy',$proxyUri) } $ProgressPreference = [System.Management.Automation.ActionPreference]::SilentlyContinue $statusCode = 0 $statusDescription = '' try { $response = Invoke-WebRequest @params $statusCode = $response.StatusCode } catch [System.Net.WebException] { $statusCode = [int]$_.Exception.Response.StatusCode $statusDescription = $_.Exception.Response.StatusDescription } if ($statusCode -ne 200) { throw "BlueCoat Site Review REST API request failed. Status code: $statusCode Status description: $statusDescription" } $returnedJson = $response.Content #Write-Verbose -Message ('JSON: {0}' -f $returnedJson) $siteReview = $returnedJson | ConvertFrom-Json if ($siteReview.PSObject.Properties.Name -contains 'errorType') { throw ('Error retrieving Blue Coat data. Error Type: {0} Error Message: {1}' -f $siteReview.errorType, $siteReview.error) } $cats = @{} $siteReview.categorization | ForEach-Object { $link = ('https://sitereview.bluecoat.com/catdesc.jsp?catnum={0}' -f $_.num) $cats.Add($_.name,$link) } $dateMatched = $siteReview.rateDate -match 'Last Time Rated/Reviewed:\s*(.+)\s*{{.*' $lastRated = '' if($dateMatched -and $matches.Count -ge 2) { $lastRated = $matches[1].Trim() } $siteReviewObject = [pscustomobject]@{ SubmittedUri = $Uri; ReturnedUri = [System.Uri]$siteReview.url; Rated = $siteReview.unrated -eq 'false' LastedRated = $lastRated; Locked = $siteReview.locked -eq 'true'; LockMessage = if ($siteReview.locked -eq 'true') {[string]$siteReview.lockedMessage} else {''}; Pending = $siteReview.multiple -eq 'true'; PendingMessage = if ($siteReview.multiple -eq 'true') {[string]$siteReview.multipleMessage} else {''}; Categories = $cats; } Write-Verbose -Message ('{0}Rated: {1}{2}Last Rated: {3}{4}Locked: {5}{6}Lock Message: {7}{8}Pending: {9}{10}Pending Message: {11}{12}Categories: {13}{14}{15}' -f $newLine,$siteReviewObject.Rated,$newLine,$siteReviewObject.LastedRated,$newLine,$siteReviewObject.Locked,$newLine,$siteReviewObject.LockMessage,$newLine,$siteReviewObject.Pending,$newLine,$siteReviewObject.PendingMessage,$newLine,($siteReviewObject.Categories.Keys -join ','),$newLine,$newLine) return $siteReviewObject } Function Get-IPAddress() { <# .SYNOPSIS Gets the IP address(es) for a URL. .DESCRIPTION Gets the IP address(es) for a URL. .EXAMPLE Get-IPAddress -Url http://www.site.com #> [CmdletBinding()] [OutputType([string[]])] Param ( [Parameter(Mandatory=$true, HelpMessage='The URL to get the IP address for.')] [ValidateNotNullOrEmpty()] [System.Uri]$Url ) $addresses = [string[]]@() $dnsResults = $null $dnsResults = @(Resolve-DnsName -Name $Url.Host -NoHostsFile -Type A_AAAA -QuickTimeout -ErrorAction SilentlyContinue | Where-Object {$_.Type -eq 'A'}) $addresses = [string[]]@($dnsResults | ForEach-Object { try { $_.IpAddress } catch [System.Management.Automation.PropertyNotFoundException] {Write-Verbose "No IP in Object."} }) # IpAddress results in a PropertyNotFoundException when a URL is blocked upstream return [string[]](,$addresses) } Function Get-IPAlias() { <# .SYNOPSIS Gets DNS alias for a URL. .DESCRIPTION Gets DNS alias for a URL. .EXAMPLE Get-IPAlias -Url http://www.site.com #> [CmdletBinding()] [OutputType([string[]])] Param ( [Parameter(Mandatory=$true, HelpMessage='The URL to get the alias address for.')] [ValidateNotNullOrEmpty()] [System.Uri]$Url ) $aliases = [string[]]@() $dnsResults = $null $dnsResults = @(Resolve-DnsName -Name $Url.Host -NoHostsFile -QuickTimeout -ErrorAction SilentlyContinue | Where-Object { $_.Type -eq 'CNAME' }) #$aliases = [string[]]@($dnsResults | ForEach-Object { try { $_.NameHost } catch [System.Management.Automation.PropertyNotFoundException] {} }) # NameHost results in a PropertyNotFoundException when a URL is blocked upstream $aliases = [string[]]@($dnsResults | ForEach-Object { $_.NameHost }) return [string[]](,$aliases) } Function Get-CertificateErrorMessage() { <# .SYNOPSIS Gets certificate error messages for an HTTPS URL. .DESCRIPTION Gets certificate error messages for an HTTPS URL. .EXAMPLE Get-CertificateErrorMessage -Url http://www.site.com -Certificate $certificate -Chain $chain -PolicyError $policyError #> [CmdletBinding()] [OutputType([string])] Param( [Parameter(Mandatory=$true, HelpMessage='The URL to test')] [ValidateNotNullOrEmpty()] [Uri]$Url, [Parameter(Mandatory=$true, HelpMessage='The certificate')] [ValidateNotNull()] [Security.Cryptography.X509Certificates.X509Certificate]$Certificate, [Parameter(Mandatory=$true, HelpMessage='The certificate chain')] [ValidateNotNull()] $Chain, # had to drop [Security.Cryptography.X509Certificates.X509Chain] otherwise call to Get-CertificateErrorMessage fails with "Cannot process argument transformation on parameter 'Chain'. Cannot create object of type "System.Security.Cryptography.X509Certificates.X509Chain". "ChainContext" is a ReadOnly property." [Parameter(Mandatory=$true, HelpMessage='The SSL error')] [ValidateNotNull()] [Net.Security.SslPolicyErrors]$PolicyError ) $details = '' if($PolicyError -ne [Net.Security.SslPolicyErrors]::None) { switch ($PolicyError) { 'RemoteCertificateChainErrors' { if ($Chain.ChainElements.Count -gt 0 -and $Chain.ChainStatus.Count -gt 0) { if ($Chain.ChainElements.Count -gt 0 -or $Chain.ChainStatus.Count -gt 0) { Write-Verbose -Message ('Multiple remote certificate chain elements exist. ChainElement Count: {0} ChainStatus Count: {1}' -f $Chain.ChainElements.Count,$Chain.ChainStatus.Count) } #todo support more than one chain $element = $Chain.ChainElements[0] $status = $Chain.ChainStatus[0] $details = ('Certificate chain error. Error: {0} Reason: {1} Certificate: {2}' -f $status.Status, $status.StatusInformation,$element.Certificate.ToString($false)) } else { $details = ('Certificate chain error. Certificate: {0}' -f $Certificate.ToString($false)) } break } 'RemoteCertificateNameMismatch' { $cert = New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList $Certificate $sanExtension = $cert.Extensions | Where-Object { $_.Oid.FriendlyName -eq 'Subject Alternative Name' } if ($null -eq $sanExtension) { $subject = $cert.Subject.Split(',')[0].Replace('CN=', '') $details = ('Remote certificate name mismatch. Host: {0} Subject: {1}' -f $Url.Host,$subject) } else { $subject = $certificate.Subject.Split(',')[0].Replace('CN=', '') $asnData = New-Object Security.Cryptography.AsnEncodedData -ArgumentList $sanExtension.Oid,$sanExtension.RawData $sans = $asnData.Format($false).Replace('DNS Name=', '').Replace(',', '').Split(@(' '), [StringSplitOptions]::RemoveEmptyEntries) $details = ('Remote certificate name mismatch. Host: {0} Subject: {1} SANs: {2}' -f $Url.Host,$subject,($sans -join ', ')) } break } 'RemoteCertificateNotAvailable' { $details = 'Remote certificate not available.' } 'None' { break } default { $details = ('Unrecognized remote certificate error. {0}' -f $PolicyError) break } } } return $details } Function Get-HttpConnectivity() { <# .SYNOPSIS Gets HTTP connectivity information for a URL. .DESCRIPTION Gets HTTP connectivity information for a URL. .EXAMPLE Get-HttpConnectivity -TestUrl http://www.site.com .EXAMPLE Get-HttpConnectivity -TestUrl http://www.site.com -UrlPattern http://*.site.com .EXAMPLE Get-HttpConnectivity -TestUrl http://www.site.com -Method POST .EXAMPLE Get-HttpConnectivity -TestUrl http://www.site.com -ExpectedStatusCode 400 .EXAMPLE Get-HttpConnectivity -TestUrl http://www.site.com -Description 'A site that does something' .EXAMPLE Get-HttpConnectivity -TestUrl http://www.site.com -UserAgent 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.84 Safari/537.36'' .EXAMPLE Get-HttpConnectivity -TestUrl http://www.site.com -IgnoreCertificateValidationErrors .EXAMPLE Get-HttpConnectivity -TestUrl http://www.site.com -PerformBluecoatLookup #> [CmdletBinding()] [OutputType([void])] Param( [Parameter(Mandatory=$true, HelpMessage='The URL to test.')] [ValidateNotNullOrEmpty()] [Uri]$TestUrl, [Parameter(Mandatory=$false, HelpMessage='The URL pattern to unblock when the URL to unblock is not a literal URL.')] [ValidateNotNullOrEmpty()] [string]$UrlPattern, [Parameter(Mandatory=$false, HelpMessage="The HTTP method used to test the URL. Defaults to 'GET'.")] [ValidateNotNullOrEmpty()] [ValidateSet('HEAD','GET', 'POST', IgnoreCase=$true)] [string]$Method = 'GET', [Parameter(Mandatory=$false, HelpMessage='The HTTP status code expected to be returned. Defaults to 200.')] [ValidateNotNullOrEmpty()] [Int32[]]$ExpectedStatusCode = 200, [Parameter(Mandatory=$false, HelpMessage='A description of the connectivity test or purpose of the URL.')] [ValidateNotNullOrEmpty()] [string]$Description, [Parameter(Mandatory=$false, HelpMessage='The HTTP user agent. Defaults to the Chrome browser user agent.')] [ValidateNotNullOrEmpty()] [string]$UserAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36', [Parameter(Mandatory=$false, HelpMessage="Whether to ignore certificate validation errors so they don't affect the connectivity test. Some HTTPS endpoints are not meant to be accessed by a browser so the endpoint will not validate against browser security requirements.")] [switch]$IgnoreCertificateValidationErrors, [Parameter(Mandatory=$false, HelpMessage='Whether to perform a Symantec BlueCoat Site Review lookup on the URL. Warning: The BlueCoat Site Review REST API is rate limited. Automatic throttling is performed when this parameter is used.')] [switch]$PerformBluecoatLookup ) $parameters = $PSBoundParameters $isVerbose = $verbosePreference -eq 'Continue' if ($TestUrl.OriginalString.ToLower().StartsWith('http://') -or $TestUrl.OriginalString.ToLower().StartsWith('https://')) { $testUri = $TestUrl } else { $testUri = [Uri]('http://{0}' -f $testUri.OriginalString) } if($parameters.ContainsKey('UrlPattern')) { $UnblockUrl = $UrlPattern } else { $UnblockUrl = $testUri.OriginalString # ('{0}//{1}' -f $testUri.Scheme,$testUri.Host) } $newLine = [System.Environment]::NewLine Write-Verbose -Message ('{0}*************************************************{1}Testing {2}{3}*************************************************{4}' -f $newLine,$newLine,$testUri,$newLine,$newLine) $script:ServerCertificate = $null $script:ServerCertificateChain = $null $script:ServerCertificateError = $null # can't use Invoke-WebRequest and override the callback due to PowerShell Runspace errors described in this post: http://huddledmasses.org/blog/validating-self-signed-certificates-properly-from-powershell/ if($IgnoreCertificateValidationErrors) { $RemoteCertificateValidationCallback = { param([object]$sender, [Security.Cryptography.X509Certificates.X509Certificate]$certificate, [Security.Cryptography.X509Certificates.X509Chain]$chain, [Net.Security.SslPolicyErrors]$sslPolicyErrors) $script:ServerCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certificate $script:ServerCertificateChain = $chain | Select-Object * # clone chain object otherwise we lose ChainElements and ChainStatus property contents on variable assignment... weird $script:ServerCertificateError = $sslPolicyErrors return $true } } else { $RemoteCertificateValidationCallback = { param([object]$sender, [Security.Cryptography.X509Certificates.X509Certificate]$certificate, [Security.Cryptography.X509Certificates.X509Chain]$chain, [Net.Security.SslPolicyErrors]$sslPolicyErrors) $script:ServerCertificate = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2 $certificate $script:ServerCertificateChain = $chain | Select-Object * # clone chain object otherwise we lose ChainElements and ChainStatus property contents on variable assignment... weird $script:ServerCertificateError = $sslPolicyErrors return [Net.Security.SslPolicyErrors]::None -eq $sslPolicyErrors } } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls11 $proxyUri = [Net.WebRequest]::GetSystemWebProxy().GetProxy($testUri) $request = [Net.WebRequest]::CreateHttp($testUri) $request.Proxy = if ($testUri -ne $proxyUri) { [Net.WebRequest]::DefaultWebProxy } else { $null } $request.UseDefaultCredentials = ($testUri -ne $proxyUri) $request.UserAgent = $UserAgent; $request.Method = $Method $request.ServerCertificateValidationCallback = $RemoteCertificateValidationCallback $statusCode = 0 $statusMessage = '' $response = $null try { $response = $request.GetResponse() $httpResponse = $response -as [Net.HttpWebResponse] $statusCode = $httpResponse.StatusCode $statusMessage = $httpResponse.StatusDescription } catch [System.Net.WebException] { # useful WINHTTP error message code values and descriptions. will be in the exception # https://msdn.microsoft.com/en-us/library/windows/desktop/aa383770(v=vs.85).aspx # https://msdn.microsoft.com/en-us/library/windows/desktop/aa384110(v=vs.85).aspx $statusMessage = Get-ErrorMessage -ErrorRecord $_ try { $statusCode = [int]$_.Exception.Response.StatusCode # StatusCode property results in a PropertyNotFoundException exception when the URL is blocked upstream } catch [System.Management.Automation.PropertyNotFoundException] { Write-Verbose -Message ('Unable to access {0} due to {1}' -f $testUri,$statusMessage) } } finally { if ($null -ne $response) { $response.Close() } } $hasServerCertificateError = if ($null -eq $script:ServerCertificateError -or $IgnoreCertificateValidationErrors) { $false } else { $script:ServerCertificateError -ne [Net.Security.SslPolicyErrors]::None } $serverCertificateErrorMessage = '' if ($testUri.Scheme.ToLower() -eq 'https' -and $hasServerCertificateError) { $serverCertificateErrorMessage = Get-CertificateErrorMessage -Url $testUri -Certificate $script:ServerCertificate -Chain $script:ServerCertificateChain -PolicyError $script:ServerCertificateError } $serverCertificateObject = [pscustomobject]@{ Certificate = $script:ServerCertificate | Select-Object -Property * -ExcludeProperty RawData; # RawData property makes JSON files to large when calling Save-HttpConnectivity Chain = $script:ServerCertificateChain; Error = $script:ServerCertificateError; ErrorMessage = $serverCertificateErrorMessage; HasError = $hasServerCertificateError; IgnoreError = $IgnoreCertificateValidationErrors; } $address = Get-IPAddress -Url $testUri -Verbose:$false $alias = Get-IPAlias -Url $testUri -Verbose:$false $resolved = (@($address)).Length -ge 1 -or (@($alias)).Length -ge 1 $actualStatusCode = [int]$statusCode $isBlocked = $statusCode -eq 0 -and $resolved $urlType = if ($UnblockUrl.Contains('*')) { 'Pattern' } else { 'Literal' } $isUnexpectedStatus = !($statusCode -in @(200,400,403,404,500,501,503,504)) $simpleStatusMessage = if ($isUnexpectedStatus) { $statusMessage } else { '' } $connectivitySummary = ('{0}Test Url: {1}{2}Url to Unblock: {3}{4}Url Type: {5}{6}Description: {7}{8}Resolved: {9}{10}IP Addresses: {11}{12}DNS Aliases: {13}{14}Actual Status Code: {15}{16}Expected Status Code: {17}{18}Is Unexpected Status Code: {19}{20}Status Message: {21}{22}Blocked: {23}{24}Certificate Error: {25}{26}Certificate Error Message: {27}{28}Ignore Certificate Validation Errors: {29}{30}{31}' -f $newLine,$testUri,$newLine,$UnblockUrl,$newLine,$urlType,$newLine,$Description,$newLine,$resolved,$newLine,($address -join ', '),$newLine,($alias -join ', '),$newLine,$actualStatusCode,$newLine,($ExpectedStatusCode -join ","),$newLine,$isUnexpectedStatus,$newLine,$simpleStatusMessage,$newLine,$isBlocked,$newLine,$serverCertificateObject.HasError,$newLine,$serverCertificateObject.ErrorMessage,$newLine,$serverCertificateObject.IgnoreError,$newLine,$newLine) Write-Verbose -Message $connectivitySummary $bluecoat = $null if ($PerformBluecoatLookup) { try { $bluecoat = Get-BlueCoatSiteReview -Url $testUri -Verbose:$isVerbose } catch { Write-Verbose -Message $_ } } $connectivity = [pscustomobject]@{ TestUrl = $testUri; UnblockUrl = $UnblockUrl; UrlType = $urlType; Resolved = $resolved; IpAddresses = [string[]]$address; DnsAliases = [string[]]$alias; Description = $Description; ActualStatusCode = [int]$actualStatusCode; ExpectedStatusCode = [Int32[]]$ExpectedStatusCode; UnexpectedStatus = $isUnexpectedStatus; StatusMessage = $simpleStatusMessage; DetailedStatusMessage = $statusMessage; Blocked = $isBlocked; ServerCertificate = $serverCertificateObject; BlueCoat = $bluecoat; } return $connectivity } Function Save-HttpConnectivity() { <# .SYNOPSIS Saves HTTP connectivity objects to a JSON file. .DESCRIPTION Saves HTTP connectivity objects to a JSON file. .EXAMPLE Save-HttpConnectivity -FileName 'Connectivity' -Objects $connectivity .EXAMPLE Save-HttpConnectivity -FileName 'Connectivity' -Objects $connectivity -OutputPath "$env:userprofile\Documents\ConnectivityTestResults" .EXAMPLE Save-HttpConnectivity -FileName 'Connectivity' -Objects $connectivity -Compress #> [CmdletBinding()] [OutputType([void])] Param( [Parameter(Mandatory=$true, HelpMessage='The filename without the extension.')] [ValidateNotNullOrEmpty()] [string]$FileName, [Parameter(Mandatory=$true, HelpMessage='The connectivity object(s) to save.')] [System.Collections.Generic.List[pscustomobject]]$Objects, [Parameter(Mandatory=$false, HelpMessage="The path to save the file to. Defaults to the user's Desktop folder.")] [string]$OutputPath, [Parameter(Mandatory=$false, HelpMessage='Compress the JSON text output.')] [switch]$Compress ) $parameters = $PSBoundParameters if (-not($parameters.ContainsKey('OutputPath'))) { $OutputPath = $env:USERPROFILE,'Desktop' -join [System.IO.Path]::DirectorySeparatorChar } $OutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) if (-not(Test-Path -Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory } #$fileName = ($targetUrl.OriginalString.Split([string[]][IO.Path]::GetInvalidFileNameChars(),[StringSplitOptions]::RemoveEmptyEntries)) -join '-' $json = $Objects | ConvertTo-Json -Depth 3 -Compress:$Compress $json | Out-File -FilePath "$OutputPath\$FileName.json" -NoNewline -Force } # SIG # Begin signature block # MIIZwgYJKoZIhvcNAQcCoIIZszCCGa8CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUtWJMNytcSJ/zwpy7YkAQyBLx # ZLigghUDMIID7jCCA1egAwIBAgIQfpPr+3zGTlnqS5p31Ab8OzANBgkqhkiG9w0B # AQUFADCBizELMAkGA1UEBhMCWkExFTATBgNVBAgTDFdlc3Rlcm4gQ2FwZTEUMBIG # A1UEBxMLRHVyYmFudmlsbGUxDzANBgNVBAoTBlRoYXd0ZTEdMBsGA1UECxMUVGhh # d3RlIENlcnRpZmljYXRpb24xHzAdBgNVBAMTFlRoYXd0ZSBUaW1lc3RhbXBpbmcg # Q0EwHhcNMTIxMjIxMDAwMDAwWhcNMjAxMjMwMjM1OTU5WjBeMQswCQYDVQQGEwJV # UzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9yYXRpb24xMDAuBgNVBAMTJ1N5bWFu # dGVjIFRpbWUgU3RhbXBpbmcgU2VydmljZXMgQ0EgLSBHMjCCASIwDQYJKoZIhvcN # AQEBBQADggEPADCCAQoCggEBALGss0lUS5ccEgrYJXmRIlcqb9y4JsRDc2vCvy5Q # WvsUwnaOQwElQ7Sh4kX06Ld7w3TMIte0lAAC903tv7S3RCRrzV9FO9FEzkMScxeC # i2m0K8uZHqxyGyZNcR+xMd37UWECU6aq9UksBXhFpS+JzueZ5/6M4lc/PcaS3Er4 # ezPkeQr78HWIQZz/xQNRmarXbJ+TaYdlKYOFwmAUxMjJOxTawIHwHw103pIiq8r3 # +3R8J+b3Sht/p8OeLa6K6qbmqicWfWH3mHERvOJQoUvlXfrlDqcsn6plINPYlujI # fKVOSET/GeJEB5IL12iEgF1qeGRFzWBGflTBE3zFefHJwXECAwEAAaOB+jCB9zAd # BgNVHQ4EFgQUX5r1blzMzHSa1N197z/b7EyALt0wMgYIKwYBBQUHAQEEJjAkMCIG # CCsGAQUFBzABhhZodHRwOi8vb2NzcC50aGF3dGUuY29tMBIGA1UdEwEB/wQIMAYB # Af8CAQAwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NybC50aGF3dGUuY29tL1Ro # YXd0ZVRpbWVzdGFtcGluZ0NBLmNybDATBgNVHSUEDDAKBggrBgEFBQcDCDAOBgNV # HQ8BAf8EBAMCAQYwKAYDVR0RBCEwH6QdMBsxGTAXBgNVBAMTEFRpbWVTdGFtcC0y # MDQ4LTEwDQYJKoZIhvcNAQEFBQADgYEAAwmbj3nvf1kwqu9otfrjCR27T4IGXTdf # plKfFo3qHJIJRG71betYfDDo+WmNI3MLEm9Hqa45EfgqsZuwGsOO61mWAK3ODE2y # 0DGmCFwqevzieh1XTKhlGOl5QGIllm7HxzdqgyEIjkHq3dlXPx13SYcqFgZepjhq # IhKjURmDfrYwggSjMIIDi6ADAgECAhAOz/Q4yP6/NW4E2GqYGxpQMA0GCSqGSIb3 # DQEBBQUAMF4xCzAJBgNVBAYTAlVTMR0wGwYDVQQKExRTeW1hbnRlYyBDb3Jwb3Jh # dGlvbjEwMC4GA1UEAxMnU3ltYW50ZWMgVGltZSBTdGFtcGluZyBTZXJ2aWNlcyBD # QSAtIEcyMB4XDTEyMTAxODAwMDAwMFoXDTIwMTIyOTIzNTk1OVowYjELMAkGA1UE # BhMCVVMxHTAbBgNVBAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTQwMgYDVQQDEytT # eW1hbnRlYyBUaW1lIFN0YW1waW5nIFNlcnZpY2VzIFNpZ25lciAtIEc0MIIBIjAN # BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAomMLOUS4uyOnREm7Dv+h8GEKU5Ow # mNutLA9KxW7/hjxTVQ8VzgQ/K/2plpbZvmF5C1vJTIZ25eBDSyKV7sIrQ8Gf2Gi0 # jkBP7oU4uRHFI/JkWPAVMm9OV6GuiKQC1yoezUvh3WPVF4kyW7BemVqonShQDhfu # ltthO0VRHc8SVguSR/yrrvZmPUescHLnkudfzRC5xINklBm9JYDh6NIipdC6Anqh # d5NbZcPuF3S8QYYq3AhMjJKMkS2ed0QfaNaodHfbDlsyi1aLM73ZY8hJnTrFxeoz # C9Lxoxv0i77Zs1eLO94Ep3oisiSuLsdwxb5OgyYI+wu9qU+ZCOEQKHKqzQIDAQAB # o4IBVzCCAVMwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAO # BgNVHQ8BAf8EBAMCB4AwcwYIKwYBBQUHAQEEZzBlMCoGCCsGAQUFBzABhh5odHRw # Oi8vdHMtb2NzcC53cy5zeW1hbnRlYy5jb20wNwYIKwYBBQUHMAKGK2h0dHA6Ly90 # cy1haWEud3Muc3ltYW50ZWMuY29tL3Rzcy1jYS1nMi5jZXIwPAYDVR0fBDUwMzAx # oC+gLYYraHR0cDovL3RzLWNybC53cy5zeW1hbnRlYy5jb20vdHNzLWNhLWcyLmNy # bDAoBgNVHREEITAfpB0wGzEZMBcGA1UEAxMQVGltZVN0YW1wLTIwNDgtMjAdBgNV # HQ4EFgQURsZpow5KFB7VTNpSYxc/Xja8DeYwHwYDVR0jBBgwFoAUX5r1blzMzHSa # 1N197z/b7EyALt0wDQYJKoZIhvcNAQEFBQADggEBAHg7tJEqAEzwj2IwN3ijhCcH # bxiy3iXcoNSUA6qGTiWfmkADHN3O43nLIWgG2rYytG2/9CwmYzPkSWRtDebDZw73 # BaQ1bHyJFsbpst+y6d0gxnEPzZV03LZc3r03H0N45ni1zSgEIKOq8UvEiCmRDoDR # EfzdXHZuT14ORUZBbg2w6jiasTraCXEQ/Bx5tIB7rGn0/Zy2DBYr8X9bCT2bW+IW # yhOBbQAuOA2oKY8s4bL0WqkBrxWcLC9JG9siu8P+eJRRw4axgohd8D20UaF5Mysu # e7ncIAkTcetqGVvP6KUwVyyJST+5z3/Jvz4iaGNTmr1pdKzFHTx/kuDDvBzYBHUw # ggWtMIIElaADAgECAhAEP0tn9l4Sf9gdog2gb/SWMA0GCSqGSIb3DQEBBQUAMGUx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEVWIENvZGUgU2lnbmlu # ZyBDQTAeFw0yMDAzMDYwMDAwMDBaFw0yMzAzMTUxMjAwMDBaMIHOMRMwEQYLKwYB # BAGCNzwCAQMTAkNIMRowGAYLKwYBBAGCNzwCAQITCVNvbG90aHVybjEdMBsGA1UE # DwwUUHJpdmF0ZSBPcmdhbml6YXRpb24xGDAWBgNVBAUTD0NIRS0zMTQuNjM5LjUy # MzELMAkGA1UEBhMCQ0gxEjAQBgNVBAgTCVNvbG90aHVybjERMA8GA1UEBwwIRMOk # bmlrZW4xFjAUBgNVBAoTDWJhc2VWSVNJT04gQUcxFjAUBgNVBAMTDWJhc2VWSVNJ # T04gQUcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCn0xZCT8yT681H # ZVY8gtUlURKywy8Nfq8uiv/jJJU+/Tf4HHXXJzHo96ZFo/WOWMD3WMWRYRnpj95P # ZbfLaF+ki/PURRhp9/oT/p5O3zTv4Jqnig7AOeIL5dt9W5Uij9rDOEZhmFpVT08K # CKhMNMMu7MhBs+uHBlyQ70j5H2IjBjePtEDYcakbv1RNDK5hU+k2UqKZEQSaqt2+ # riewxS2R4RUvZJ5nRraf4pNYqDdem2H0vJ17zHsG+ZB0YFLk/P3i6r4tJEAksYAU # kuJsFDt0Yz9xM2qmG2Rr4iw7AUTfE5Gx0NNWD/fMWFP/2sD3VkHA8Mz8PAokDfFz # 21OqYrXPAgMBAAGjggHtMIIB6TAfBgNVHSMEGDAWgBStaQZw/IAbFrOpGJRrlAKG # XvcnjDAdBgNVHQ4EFgQURdlk/2RkqKDvZs8sol0UhzmJTCowNwYDVR0RBDAwLqAs # BggrBgEFBQcIA6AgMB4MHENILVNPTE9USFVSTi1DSEUtMzE0LjYzOS41MjMwDgYD # VR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHMGA1UdHwRsMGowM6Ax # oC+GLWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9FVkNvZGVTaWduaW5nLWcxLmNy # bDAzoDGgL4YtaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0VWQ29kZVNpZ25pbmct # ZzEuY3JsMEsGA1UdIAREMEIwNwYJYIZIAYb9bAMCMCowKAYIKwYBBQUHAgEWHGh0 # dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwBwYFZ4EMAQMweQYIKwYBBQUHAQEE # bTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wQwYIKwYB # BQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEVWQ29k # ZVNpZ25pbmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQUFAAOCAQEA # GYerL9YA8gW4cx7nWEaDFpN2XnaY4+90Nl8gaj6aeQj6kwIfjWLWAzByDdVNvxSk # rwXdfo3dkG5DNNI3wPR2SE2iyImDF6zXTThccBqkwE1x1Tb5qfhaA48jf18f8Jbv # VgvtbZWXph1b+ALyD2911b34Qt6cYmolg19vkmWXZUADRjA11S3VHhhH4GLKeHoE # 23jSSs69tQPNC1jdS+Rx6yO/Ya14UrDwOrJo1qSn2xTilf9s77mSxRJCpL8Cd1PU # HPvugUFHLw9nqOQAMUb7cHdDUREs7Brvfcyo0qRx7lyKjIM1d0wGtiBz+8kQJcSC # dK9S8HGSD3y4R1N++Y8gYTCCBrUwggWdoAMCAQICEA3Q4zdKyVvb+mtDSypI7AYw # DQYJKoZIhvcNAQEFBQAwbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0 # IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNl # cnQgSGlnaCBBc3N1cmFuY2UgRVYgUm9vdCBDQTAeFw0xMjA0MTgxMjAwMDBaFw0y # NzA0MTgxMjAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0 # IEVWIENvZGUgU2lnbmluZyBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC # ggEBALkGdBxdtCCqqSGoKkJGqyUgFyXLIo+QoqAxa4MFda+yDnwSSXtqhmSED4Pc # ZLmxbhYFPhyVuefniG24YoGQedTd9eKW+cO1iCNXShrPcSnpCACPtZjjpzL9rC64 # 9JNT9Ao5Q5Gv1Wvo1J9GvY49q+L5K9TqAEBmJLfof7REdY14mq4xwTfPTh9b+EVK # 1z/CyZIGZL7eBoqv0OiKsfAsiABvC9yFp0zLBr/WLioybilxr44i8w/Q2JhILagI # y7aLI8Jj4LZz6299Jk+L9zQ9N4YMt3gn9MKG20NrWvg9PfTosGJWxufteKH7/Xpy # TzJlxHzDxHegBDIy7Y8/r4bdftECAwEAAaOCA1gwggNUMBIGA1UdEwEB/wQIMAYB # Af8CAQAwDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMH8GCCsG # AQUFBwEBBHMwcTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29t # MEkGCCsGAQUFBzAChj1odHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNl # cnRIaWdoQXNzdXJhbmNlRVZSb290Q0EuY3J0MIGPBgNVHR8EgYcwgYQwQKA+oDyG # Omh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VF # VlJvb3RDQS5jcmwwQKA+oDyGOmh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwggHEBgNVHSAEggG7MIIBtzCC # AbMGCWCGSAGG/WwDAjCCAaQwOgYIKwYBBQUHAgEWLmh0dHA6Ly93d3cuZGlnaWNl # cnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5odG0wggFkBggrBgEFBQcCAjCCAVYe # ggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0AGgAaQBzACAAQwBlAHIAdABpAGYA # aQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1AHQAZQBzACAAYQBjAGMAZQBwAHQA # YQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABpAGcAaQBDAGUAcgB0ACAAQwBQAC8A # QwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBlAGwAeQBpAG4AZwAgAFAAYQByAHQA # eQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBoAGkAYwBoACAAbABpAG0AaQB0ACAA # bABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAgAGEAcgBlACAAaQBuAGMAbwByAHAA # bwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAgAGIAeQAgAHIAZQBmAGUAcgBlAG4A # YwBlAC4wHQYDVR0OBBYEFK1pBnD8gBsWs6kYlGuUAoZe9yeMMB8GA1UdIwQYMBaA # FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQCeW5Y6LhKI # rKsBbaSfdeQBh6OlMte8uql+o9YUF/fCE2t8c48rauUPJllosI4lm2zv+myTkgjB # Tc9FnpxG1h50oZsUo/oBL0qxAeFyQEgRE2i5Np2RS9fCORIQwcTcu2IUFCphXU84 # fGYfxhv/rb5Pf5Rbc0MAD01zt1HPDvZ3wFvNNIzZYxOqDmER1vKOJ/y0e7i5ESCR # hnjqDtQo/yrVJDjoN7LslrufvEoWUOFev1F9I6Ayx8GUnnrJwCaizCWHoBJ+dJ8t # jbHI54S+udHp3rtqTohzceEiOMskh+lzflGy/5jrTn4v4MoO+rNe0boFQqhIn4P2 # P8TKqN9ooFBhMYIEKTCCBCUCAQEweTBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM # RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQD # ExtEaWdpQ2VydCBFViBDb2RlIFNpZ25pbmcgQ0ECEAQ/S2f2XhJ/2B2iDaBv9JYw # CQYFKw4DAhoFAKB4MBgGCisGAQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcN # AQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUw # IwYJKoZIhvcNAQkEMRYEFNMIPOUnpO0mI6t88q4bfxLdjOiuMA0GCSqGSIb3DQEB # AQUABIIBAG4lz7VNQYcj3ocOhIOV58TWxl+awnJDZbEGASuq3jMvWJ4FqxGYso/V # 2PzV8Y18ggqIt1RipxjRNjBHLMZISSoPgLBEhOHrmLi9l8NauHtABnNVmvAkgu8J # 0ymg9QWsSp4MOM3LF0DU7E4O0QLX0xS08T1z/lYvk6LT6NzQosDQPNfO4knI0rJh # bMr4rQHtscRnMU2OEDBFLgh5ImTMm5MMjO79Du94hts0nJUuIa2Qz4q7eEYWHFkC # Njm6JOFovtSPM3TIe6JY/ELGgHGZ3XrjpSIaAIWhrj+eTssJHGzffq1FBME34kyD # u2CrnIZriqyVOhckt5C2RNB6xMY1q36hggILMIICBwYJKoZIhvcNAQkGMYIB+DCC # AfQCAQEwcjBeMQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9y # YXRpb24xMDAuBgNVBAMTJ1N5bWFudGVjIFRpbWUgU3RhbXBpbmcgU2VydmljZXMg # Q0EgLSBHMgIQDs/0OMj+vzVuBNhqmBsaUDAJBgUrDgMCGgUAoF0wGAYJKoZIhvcN # AQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjAwNjAyMjE0NTUxWjAj # BgkqhkiG9w0BCQQxFgQUaR4bwwnLsfEdfhcmEPVfJRA1Ua8wDQYJKoZIhvcNAQEB # BQAEggEAnsTzqrSIezU05R3Li6Qpt8jbHLx5UDDn0aV+57ceLKAg4j2PJ73AZ1J2 # Ltf5mFiLAOj/Y4hBzLvoCmtzvIbsYWGD6JlaWq98H/TodqXghJ89ZcfENaoIf1E8 # BIEVbh2nf9WC4Av+newE88ItFP1SV9XypzqgBrrGrwRl+s3JHBT6S+tAE9nc1MXB # q0tTU5DYHt1aEtYEnJpwmgGRQBK29uYwuOAupEsEihFk8lvgF13N53f1odO9QSZk # JE7nGGZOPGC1QlrtUpoA5PtyTis+xiurVcT6OtLgep52Gh8+N15PxvcshyAJq3+l # hjd36K8HYpSBUhZWMTde2JL8ozNbcQ== # SIG # End signature block |