vulkro scan-port
Inventory every process on the host that has bound a TCP LISTEN
socket or a UDP port. Pairs with vulkro scan to reconcile static
analysis ("the code says auth is enforced") with the runtime state
("process X is binding port 8080 right now"). Optional --kill and
--monitor modes.
The subcommand is license-free: a runtime-inventory tool does not gate on a paid license. The SAST detectors are what monetises.
macOS / Linux only today. Windows support that wraps netstat -ano
tasklist /vis a follow-up; the underlying data model is identical so the CLI does not change when the Windows backend lands.
Usage
vulkro scan-port [FLAGS]
Flags
| Flag | Description |
|---|---|
--port <N> | Only show listeners on port N. |
--include-system | Include system daemons (launchd, systemd, mDNSResponder, containerd, WindowServer, etc.). By default they're hidden so the table stays focused on the user's custom processes. |
--format <FMT> | table (default) or json. |
--kill <PID> | Send SIGTERM to the named pid. Prompts on the tty unless --force is also passed. Refuses non-tty stdin without --force (CI safety). |
--force | When passed with --kill, sends SIGKILL and skips the tty confirmation prompt. |
--monitor | Poll the inventory every 2 seconds, printing only the deltas (newly bound listener, listener gone). Initial dump is shown so the operator sees the current state first. Press Ctrl-C to exit. |
Examples
List every user-owned listener:
$ vulkro scan-port
PID PPID USER PROTO BIND COMMAND COMMAND LINE
94530 arpit TCP *:3031 node node /.../docusaurus serve --port 3031 --no-open
4693 arpit TCP ::1:5173 node node /.../vite/bin/vite.js
55247 arpit TCP 127.0.0.1:8801 Python /.../Python.app/...
3 listener(s); 0 system-daemon listener(s) hidden.
Filter to a single port:
$ vulkro scan-port --port 3031
PID PPID USER PROTO BIND COMMAND COMMAND LINE
94530 arpit TCP *:3031 node node /.../docusaurus serve --port 3031 --no-open
1 listener(s); 0 system-daemon listener(s) hidden.
JSON for automation:
$ vulkro scan-port --port 3031 --format json
{
"listeners": [
{
"pid": 94530,
"ppid": null,
"user": "arpit",
"command": "node",
"command_line": "node /.../docusaurus serve --port 3031 --no-open",
"protocol": "TCP",
"port": 3031,
"bind_addr": "*"
}
],
"system_skipped": 0
}
Kill a stray dev server with a confirmation prompt:
$ vulkro scan-port --kill 94530
Send SIGTERM to pid 94530? [y/N] y
✓ sent SIGTERM to pid 94530
Monitor for new listeners during a deploy:
$ vulkro scan-port --monitor
Monitoring port listeners; press Ctrl-C to exit.
PID PPID USER PROTO BIND COMMAND COMMAND LINE
4693 arpit TCP ::1:5173 node node /.../vite/bin/vite.js
1 listener(s); 0 system-daemon listener(s) hidden.
+ 99012 node bound *:3000 (TCP)
+ 99013 node bound *:8080 (TCP)
- 4693 node released ::1:5173 (TCP)
How it works
The CLI shells out to lsof -i -P -n -F pcLnTt (which is on the
default install of every supported platform) and parses the
field-format output. Listeners are detected via the per-socket
TST=LISTEN field for TCP and the presence of a bound port for UDP.
Outbound established TCP sockets and the *:* wildcard ephemeral
UDP shapes are dropped so the table reflects only the host's
exposed surface.
Full command lines are resolved in a single batched
ps -o pid=,command= call, so the command_line column does not
incur per-process IO.
The system-daemon classifier hides a curated list: launchd,
systemd, kernel_task, cron, mDNSResponder, configd,
loginwindow, WindowServer, coreaudiod, containerd,
containerd-shim, dockerd, bluetoothd, trustd, secd,
ntpd, timed, plus a handful of platform variants. Pass
--include-system to surface them.
Exit codes
| Code | Meaning |
|---|---|
0 | Success: list / monitor exited cleanly, or --kill succeeded, or no listeners matched the filter. |
1 | Reserved for future --fail-on <pattern> style usage. Today no path returns 1. |
2 | Setup or IO error: lsof not on PATH, malformed lsof output, kill failed, filesystem error. |
Pairing with vulkro scan
A common workflow: run vulkro scan on the source tree, then
vulkro scan-port on the running host. The SAST scanner reports
the code's intent; scan-port reports the actual exposed surface.
When they disagree:
- Listener with no source-side endpoint: a runtime add-on (admin agent, debug port left on, container probe) is binding a port the source code never declared.
- Source-side endpoint with no listener: the application declares it but the process isn't actually listening (maybe configuration is wrong, or the route is never registered at runtime).
Either case is a real finding worth tracking.
Safety: the --kill flag
The CLI follows the "executing actions with care" guidance:
- Without
--force,--killprintsSend SIGTERM to pid N? [y/N]and waits on the tty. - If stdin is not a tty (CI, redirected input), the CLI refuses the signal and exits 2. This prevents an automated script from accidentally killing a production process when the prompt cannot be answered.
- With
--force, the prompt is skipped and the signal isSIGKILL(uncatchable). Reserved for the operator who is certain about the target.