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) { $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,$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 = $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 # MIIXxQYJKoZIhvcNAQcCoIIXtjCCF7ICAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUu3KPWUJbOjEKOstaOLBVzsqu # pKmgghL4MIID7jCCA1egAwIBAgIQfpPr+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 # ggUnMIIED6ADAgECAhAJT00SLqoJkIvAj67NF8OqMA0GCSqGSIb3DQEBCwUAMHIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJ # RCBDb2RlIFNpZ25pbmcgQ0EwHhcNMTYwNjA2MDAwMDAwWhcNMTkwNjExMTIwMDAw # WjBkMQswCQYDVQQGEwJDSDESMBAGA1UECBMJU29sb3RodXJuMREwDwYDVQQHDAhE # w6RuaWtlbjEWMBQGA1UEChMNYmFzZVZJU0lPTiBBRzEWMBQGA1UEAxMNYmFzZVZJ # U0lPTiBBRzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJ+YpjWmBGJ6 # 6p3mACb/iu1w1oUOFAPZVNSZ8nPOY2MNtzi8d2RRSf16+VVSBhy4wv5sg0QAu76I # 1B5mwWA73gjDERH4LRvisNLrd5cR/CyS1DLZvHY01g7Ck7MtNSekjPEHIc6LFK/4 # 5gQ28nAPcanR2wo+RPGxu34QXKg3ceBH92POm1GDGGUMsTjP7ME7ZOeLKLScJD/V # rmMH/B6K7ApfAF2O/szxFXrEo+5VcloWoCRHmbFe7nLnAC8k5I63ZBmiSi6EBc89 # ID+XaVWLYvVCNwI/PVEanmDxBG9SAxRnJtcUAYg62S84ClXNj2y53xPUbdZvz3mC # RTivIlhjH9ECAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrEuXsqCqOl6nEDwGD5 # LfZldQ5YMB0GA1UdDgQWBBR6hPT/LYCRb+slld/aUoR4eQYCQDAOBgNVHQ8BAf8E # BAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAwbjA1oDOgMYYvaHR0 # cDovL2NybDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwNaAz # oDGGL2h0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3MtZzEu # Y3JsMEwGA1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0dHBz # Oi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcBAQR4 # MHYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggrBgEF # BQcwAoZCaHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFz # c3VyZWRJRENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN # AQELBQADggEBAI5wXkMjGctA2E/fchGVptw2Qzdp1a3C1ApX4STqxhkKaQMMJao7 # cHarrQctdjRo2YHEsEsPpOKpQcB2gEUnhWInaghmq618MC/UYZtL/hUcGraEhRO6 # PEDoM/2Xz1+EJJbgmS812YOih1xXrbzfgKE3Zl01VsoNjPvsD4XtEuD0Utjrwsh/ # Qy3gD9Wb925oYOuIz9hp1+jmnQu7hlRaVr7TtxR4aTtTqQdAv35FKPqJdXXUZ9Y9 # otWAWBgWb8YFqMTw6gig3EUORB+MyPXN/zCdwrbAcXlrMIPHhKsvJ6UkxfQkfb4Z # oztVtMUBChHanEVcX4bVFQwNnDVcrlt8w6IwggUwMIIEGKADAgECAhAECRgbX9W7 # ZnVTQ7VvlVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV # BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBa # Fw0yODEwMjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy # dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lD # ZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3 # DQEBAQUAA4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/l # qJ3bMtdx6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fT # eyOU5JEjlpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqH # CN8M9eJNYBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+ # bMt+dDk2DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLo # LFH3c7y9hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIB # yTASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAK # BggrBgEFBQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9v # Y3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGln # aWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHow # eDA6oDigNoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl # ZElEUm9vdENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0Rp # Z2lDZXJ0QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwA # AgQwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAK # BghghkgBhv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0j # BBgwFoAUReuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7s # DVoks/Mi0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGS # dQ9RtG6ljlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6 # r7VRwo0kriTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo # +MUSaJ/PQMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qz # sIzV6Q3d9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHq # aGxEMrJmoecYpJpkUe8xggQ3MIIEMwIBATCBhjByMQswCQYDVQQGEwJVUzEVMBMG # A1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEw # LwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENB # AhAJT00SLqoJkIvAj67NF8OqMAkGBSsOAwIaBQCgeDAYBgorBgEEAYI3AgEMMQow # CKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcC # AQsxDjAMBgorBgEEAYI3AgEVMCMGCSqGSIb3DQEJBDEWBBSGLBCCp+liyoCFj95O # FeVIz4bIGDANBgkqhkiG9w0BAQEFAASCAQAsSb4l1/TgDhFs9SutE8BmDPQ+XXml # z6EtE1MIPfpzeETmta4zwTJndTm+QyxEvkKHOPNdQOwiqjieX1nJoNBZiA2C7RX2 # Pub1eeh+O8gEb8ISl40usUe1EWzaX6bcMKSxbP3dtgLQaMeHXEkK0aRz8Su/b/CX # 0OTopqQlDEeEUrSD4MMqXKtBVfk0mud0nVE8MgYSUT6k27Xxtr67UyilInIuTBWH # 42PtSt5+aeL+xGNP6NchDmRg3ffh4s0sdquHsr53hNphEdtZ9F1//UU6zeK2AtxH # V6z+Jw15hL2McfOqPQJhILsA1vec9mCebM1XYwECkX1Js8MiYHtPGtMYoYICCzCC # AgcGCSqGSIb3DQEJBjGCAfgwggH0AgEBMHIwXjELMAkGA1UEBhMCVVMxHTAbBgNV # BAoTFFN5bWFudGVjIENvcnBvcmF0aW9uMTAwLgYDVQQDEydTeW1hbnRlYyBUaW1l # IFN0YW1waW5nIFNlcnZpY2VzIENBIC0gRzICEA7P9DjI/r81bgTYapgbGlAwCQYF # Kw4DAhoFAKBdMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkF # MQ8XDTE5MDIxMzIyMTExN1owIwYJKoZIhvcNAQkEMRYEFGsvyd6ZI34GAiySCm+n # jy58SGh7MA0GCSqGSIb3DQEBAQUABIIBAFMiUYbmxJJQLUH6MjAqzN8Ijv7Jxp4o # Vw76/lq9swLDvgR5gs0ke6FLyN3/JcOKsIqDOR7l35uKen5uCwKIwL7JDQkI1BjY # XfifmaDsv8N594Ecm7GDLM0vbISX+dT/pzV8bJD5tmY+gTUmVWw5znoWfKjxUyzU # mAGRDKkGPhgZ/hNq/oCYeoAv72O4R56XzaSkqkIYLk8GhwCFTEdbtc05+cVLYtoq # I8dJs0B5j+9Qgry7odGDbJqPU6PDXhcTRFR08n7G/bRwEynFMq7l1uJ17YiwVDF6 # mPGHbyN1sm5F1+wJhU+k3k9P9PHdajJBWCxy/i+VcuGGG41bp+Fw55Y= # SIG # End signature block |