// Open-source script · Study Tools

PortProof

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.

Who it is for

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.

  • Change windows and firewall cutovers. Run it before and after the change and keep the report with the ticket.
  • Vendor-required paths. Before a Genetec or other VMS commissioning, write the vendor's port list into a profile and prove the paths are open before anyone is on site. No vendor security-platform profile ships with it, so the profile is yours to write from the vendor's documentation.
  • Gating a script. The exit code lets a scheduled task or a change-window script stop when a required row does not pass.

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.

What it produces

OUT-01

HTML report

The pass/fail matrix for the change board, with the authorized-use notice and the run header.

OUT-02

CSV results

One row per probe: outcome, state, latency, error, profile row, and the resolved addresses.

OUT-03

JSON results

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 codes

ExitMeaning
0Every required row is Pass. Also -Version and an admissible -DryRun.
1A required row is Fail or Inconclusive. An inconclusive required row does not pass a gate.
2A refused or malformed profile or argument, a cap exceeded, or an internal error. The console message names what to change.

Probes

  • TCP: a full connect, never a half-open scan, never a payload, never a banner read.
  • UDP: a single zero-length datagram. A timeout is reported Open|Filtered, which means PortProof could not tell, not that the port is open.
  • ICMP: one echo per distinct resolved target, only with -Icmp.
  • One connection attempt per probe and no retries. Two rows that resolve to the same address, port, and protocol are probed once.

Bundled profiles

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.

A sample of the output

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
RowTargetProbeServiceRequiredOutcomeStateError
2127.0.0.2TCP/33945HTTPyesPassOpenNone
4127.0.0.2TCP/53DNSyesInconclusiveLocalPolicy
6127.0.0.3TCP/5432PostgreSQLyesFailUnreachableTimeout
9127.0.0.2UDP/123NTPnoInconclusiveOpen|FilteredNoResponse

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.

How to run it

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.

Limits and safety notes

  • Authorized use only. Run it against systems you own or have written authorisation to assess. It sends TCP connects, UDP datagrams, and optionally ICMP echoes to what the profile declares, plus DNS lookups for host names. It does not exploit or log in to anything.
  • A third-party profile is a request to scan. Whoever writes the profile chooses every target and port. Read a profile you did not write, and review the -DryRun target list before a live run.
  • Refused addresses. The safety gate refuses this-network, broadcast, multicast, and link-local targets, and any name ending .ipv6-literal.net. A resolved name that lands in one of those classes is refused on the live run, not on -DryRun.
  • Probe cap. The default ceiling is 1,024 probes, and widening it takes -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.
  • Windows closed-port timing. A closed TCP port can take about two seconds to report as closed, so at the default timeout it may read Unreachable instead. Both states fail a required row. Raise -Timeout to about 2,500 ms if you need to tell them apart.
  • Source is a label. Every probe leaves from the host running the script. The profile's Source values name the path being proven, not a socket source address. There is no remote or multi-source mode.
  • Name resolution is a signal. Single-label and .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.
  • Local software can block a probe. A VPN client or endpoint-security product can refuse an outbound attempt, port 53 in particular. That reads LocalPolicy or Open|Filtered and says nothing about the target.
  • Treat the reports like a network diagram. They name hosts, addresses, and open ports, and by default the operator user and host. -NoOperator is the only redaction there is.
  • No telemetry. It sends nothing about its use to anyone, and it opens no listening port.

Published as working software, not a supported product, with no SLA. Security reports go through the repository's private vulnerability reporting.

Licence

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.

Related

Questions

Does PortProof need admin rights?

No. It needs Windows PowerShell 5.1, or PowerShell 7.4 and later, and no administrator rights for any probe, including -Icmp.

Is PortProof a port scanner?

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.

What does PortProof send to the target?

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.

How do I use PortProof as a change window gate?

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.

What does Open|Filtered mean in the UDP results?

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.

Is PortProof free, and does it send anything about my use?

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.

PortProof on GitHub

Source, the profile schema, the threat model, and the sample outputs are in the repository.