A PortWho run is a short pipeline: enumerate endpoints, apply the endpoint filters, inspect each owning process once, apply the text search, sort, render. Every step is read-only, and a failure in one process never discards the listing.
1. Endpoints come from the IP Helper tables
PortWho reads four tables through the Windows IP Helper API (iphlpapi.dll):
| Table | Call |
|---|---|
| Listening TCP sockets, IPv4 and IPv6 | GetExtendedTcpTable with TCP_TABLE_OWNER_PID_LISTENER |
| Bound UDP sockets, IPv4 and IPv6 | GetExtendedUdpTable with UDP_TABLE_OWNER_PID |
Each row carries the local address, the local port and the owning PID. The native buffers are copied into managed memory and decoded from the documented row layouts. If the IPv6 stack is not installed, Windows answers ERROR_NOT_SUPPORTED for the IPv6 tables and PortWho reports no IPv6 endpoints instead of failing.
Two consequences follow from the tables chosen:
- Listening TCP only. The listener table holds no established, closing or outbound connections. A port that is busy because of an outbound connection does not appear.
- Rows exactly as Windows reports them. PortWho does not de-duplicate. Two UDP sockets sharing an address and port through
SO_REUSEADDRare two rows, usually with the same PID.
2. The PID is mapped to a process with the least privilege that works
For each distinct PID, PortWho opens the process with PROCESS_QUERY_LIMITED_INFORMATION, the smallest access right that still lets QueryFullProcessImageName return the executable path. The process name is that file name without extension, matching Get-Process.
Two PIDs never get a handle: 0 is reported as System Idle Process and 4 as System, both with status system. They are kernel pseudo-processes without a user-mode executable, so there is no path, no service list and no signature to check; the details view says so in the Note line.
When OpenProcess fails, PortWho records why instead of giving up:
| Windows error | Status | What you see |
|---|---|---|
ERROR_ACCESS_DENIED |
accessDenied |
name (access denied), with the note “Windows denied access to the process; it is protected or runs with higher privileges.” |
ERROR_INVALID_PARAMETER, and the PID is no longer in the system process list |
notFound |
(exited): the process ended between the endpoint snapshot and the lookup. |
| anything else, or the path query itself fails | failed |
name (lookup failed), with the Windows error text in the note. |
The name next to (access denied) comes from the system process list, which Windows exposes without a process handle; it is loaded once per run, only when needed. With no path available, the signature status of such a process is notChecked and the table shows -.
PortWho never requests elevation. It runs as the user who started it and reports what that user is allowed to see.
3. Services are mapped through the service control manager
svchost.exe hosts many services, so its name alone answers nothing. PortWho opens the service control manager with enumerate rights only and asks once per run for every Win32 service in every state, with process information (EnumServicesStatusEx). Running services are grouped by host PID and sorted by key name. Each endpoint then shows the services hosted by its owning process: key names in the table, key name plus display name in the details view, both in JSON.
Kernel pseudo-processes get an empty list. If the service control manager cannot be queried, PortWho prints one warning to standard error and continues with empty service lists.
4. The executable is verified with Authenticode
When a path is known and --no-signature was not given, PortWho verifies the file with WinVerifyTrust. Each distinct path is verified once per run, so many svchost.exe processes cost one check. The file is opened read-only with full sharing so that a running executable can be read. Then:
- Embedded signature first. The file itself is verified. A valid signature gives
signed; a verification failure givesinvalid; “no signature” moves on to the catalog. - Catalog signature second. Most operating-system binaries carry no embedded signature; instead their SHA-256 hash is listed in a Windows security catalog. PortWho hashes the file through the catalog administrator API, looks for a catalog containing that hash and verifies the file as a member of it. Success gives
catalogSigned; a failed verification givesinvalid; no catalog givesunsigned. - Unavailable means no verdict was possible: the file could not be opened or hashed, the catalog database could not be opened, or Windows answered with a file-level error (not found, access denied, sharing violation).
The signer identity is read from the verified chain while the trust state is still open: the subject’s common name becomes signer, its O= component becomes organization, and the issuer’s common name becomes issuer. The table shows the organization when there is one, since a product-named certificate (for example .NET) is easier to recognize by its company. For a catalog-signed file these names describe the catalog’s signer: who vouches for the file, not necessarily who built it.
An invalid verdict keeps the signer the file claims and adds the Windows error text and code, so the details view reads like INVALID (claims Example Corp): <reason> (0x80096010).
Verification is offline
PortWho runs WinVerifyTrust with revocation checking disabled and the chain engine restricted to cached URL retrievals. Signature checks therefore never make a network request on your behalf. The trade-off is explicit: a certificate revoked after the fact is not detected, and a chain whose intermediate certificates are not already cached on the machine may fail to build and be reported as invalid with the corresponding Windows error.
5. Search, sort, render
<port>, --tcp/--udp, --ipv4/--ipv6 and --pid are applied before step 2, so a narrow query inspects only the processes that matter. --search needs process information and runs after inspection, matching the process name, executable path, service names and display names, and the signer. Results are sorted by port, protocol, address family, address and PID, then rendered. See the command-line reference and JSON output.