HTML report
The pass/fail matrix for the change board, with the authorized-use notice and the run header.
Search hans.study
Indexed across articles, news, KB updates, knowledge base, books, learning, and tools. Press Esc to close.
// Open-source script · Study Tools
PortProof is a single PowerShell script that proves a declared list of firewall paths is open. You give it a profile of source, target, and port requirements. It probes each one once, returns a pass/fail matrix, writes HTML, CSV, and JSON reports, and exits non-zero when a required path fails, so a change window can gate on the result.
A vendor says "open these ports" the week before a cutover, and the only way to know before go-live is to run something during the change window. That check kept being the same few lines of Test-NetConnection in a loop, with nothing to show the change board afterwards. PortProof is that loop, kept, with a report and an exit code.
It answers "is the specific set of paths this system requires open, yes or no". It does not answer "what is open across this range". That is discovery, and PortProof refuses range sweeps.
The pass/fail matrix for the change board, with the authorized-use notice and the run header.
One row per probe: outcome, state, latency, error, profile row, and the resolved addresses.
The same results with the run header, for a pipeline that reads them. Piped to the success stream when no output folder is given.
| Exit | Meaning |
|---|---|
0 | Every required row is Pass. Also -Version and an admissible -DryRun. |
1 | A required row is Fail or Inconclusive. An inconclusive required row does not pass a gate. |
2 | A refused or malformed profile or argument, a cap exceeded, or an internal error. The console message names what to change. |
Open|Filtered, which means PortProof could not tell, not that the port is open.-Icmp. Three profiles ship with it, each row traced to public Microsoft documentation with a provenance file: Active Directory and domain controller reachability, SQL Server, and an RDP plus WinRM management baseline. Profiles are CSV or JSON, with a Required flag per row, and group placeholders such as %CLIENT% that you bind at run time with -Set.
This is a real run from the repository's samples, against loopback listeners in a lab, with the operator fields redacted. Six of the eight rows were required and three of those did not pass, so the exit code is 1.
total 8 pass 3 fail 3 inconclusive 2
required 6 required not passed 3
not passed: row 4 127.0.0.1 -> 127.0.0.2 TCP/53 Inconclusive LocalPolicy
not passed: row 6 127.0.0.1 -> 127.0.0.3 TCP/5432 Fail Timeout
not passed: row 7 127.0.0.1 -> 127.0.0.2 TCP/25 Fail Timeout
exit code 1 | Row | Target | Probe | Service | Required | Outcome | State | Error |
|---|---|---|---|---|---|---|---|
| 2 | 127.0.0.2 | TCP/33945 | HTTP | yes | Pass | Open | None |
| 4 | 127.0.0.2 | TCP/53 | DNS | yes | Inconclusive | LocalPolicy | |
| 6 | 127.0.0.3 | TCP/5432 | PostgreSQL | yes | Fail | Unreachable | Timeout |
| 9 | 127.0.0.2 | UDP/123 | NTP | no | Inconclusive | Open|Filtered | NoResponse |
Row 4 is a TCP/53 probe that the operator host's own VPN policy refused locally. PortProof reports that as LocalPolicy rather than as a verdict on the target.
It needs Windows PowerShell 5.1, or PowerShell 7.4 and later, and no administrator rights for any probe, including -Icmp. Download PortProof.ps1 from the GitHub repository, verify it, and unblock it. The script is not Authenticode-signed, so check the hash and the build attestation first.
Get-FileHash -Algorithm SHA256 .\PortProof.ps1
gh attestation verify PortProof.ps1 --owner hansstudy
Unblock-File .\PortProof.ps1 Run it with -DryRun first to see the probe list and a worst-case duration. Nothing is resolved or sent on a dry run.
# 1. See what would run and how long it could take, without sending anything.
.\PortProof.ps1 -Profile profiles\ad-dc.csv -Set "CLIENT=10.10.1.50;DC=dc01.corp.example" -DryRun
# 2. Run it for real and write a report.
.\PortProof.ps1 -Profile profiles\ad-dc.csv -Set "CLIENT=10.10.1.50;DC=dc01.corp.example" -Out .\out
# 3. Open the result.
Start-Process .\out\portproof-report.html -Format Html,Csv,Json picks the report types, and -Icmp adds an echo per target. -Set takes every binding in one string joined with semicolons. -AllowCidr lets a group be a single IPv4 block from /8 to /32. -NoOperator replaces the operator user and host name in the report with "redacted".
To gate a change window, call it with -File and check the process exit code:
powershell -NoProfile -File PortProof.ps1 -Profile .\profile.csv -Out .\out
if ($LASTEXITCODE -ne 0) { <fail the change window> } Do not gate on -Command "...; exit $LASTEXITCODE". A PowerShell parameter-binding error stops PortProof from running at all, and that form can report a stale 0.
-DryRun target list before a live run..ipv6-literal.net. A resolved name that lands in one of those classes is refused on the live run, not on -DryRun.-AllowLarge. Defaults are a 2,000 ms timeout, 16 concurrent probes, and 50 probes per second. Many probes against one unreachable target run slowly by design, so read the dry-run estimate before assuming a stall.Unreachable instead. Both states fail a required row. Raise -Timeout to about 2,500 ms if you need to tell them apart..local names can trigger LLMNR, NetBIOS, or mDNS queries from Windows itself. Use fully qualified names or IP literals. A profile author who controls a DNS zone also learns the profile was run.LocalPolicy or Open|Filtered and says nothing about the target.-NoOperator is the only redaction there is.Published as working software, not a supported product, with no SLA. Security reports go through the repository's private vulnerability reporting.
Apache-2.0. See the LICENSE in the repository. Microsoft, Windows Server, Active Directory, and SQL Server are trademarks of their owners. PortProof is independent and not affiliated with them.
No. It needs Windows PowerShell 5.1, or PowerShell 7.4 and later, and no administrator rights for any probe, including -Icmp.
No. It answers whether the specific set of paths a system requires is open, yes or no. It doesn't answer what is open across a range, because that is discovery, and PortProof refuses range sweeps.
A full TCP connect with no payload and no banner read, a single zero-length UDP datagram, and an ICMP echo only with -Icmp, plus DNS lookups for host names. It makes one attempt per probe with no retries, and it should only be run against systems you own or have written authorisation to assess.
Run it with -DryRun first to see the probes and a worst-case duration, then run it for real with a profile. The exit code is 0 when every required row passes, 1 when a required row fails or is inconclusive, and 2 for a refused or malformed profile. Call it with -File and check the process exit code.
PortProof sends a single zero-length datagram, and a timeout is reported as Open|Filtered. That means it could not tell, not that the port is open.
It's free and open source under Apache-2.0, published as working software, not a supported product. It sends no telemetry and opens no listening port.
Source, the profile schema, the threat model, and the sample outputs are in the repository.
Two tools run by default to help me understand how the site is used. You can turn either off at any time. Cloudflare's server-side analytics is always on and never sees your identity.