Skip to main content

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 /v is 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

FlagDescription
--port <N>Only show listeners on port N.
--include-systemInclude 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).
--forceWhen passed with --kill, sends SIGKILL and skips the tty confirmation prompt.
--monitorPoll 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

CodeMeaning
0Success: list / monitor exited cleanly, or --kill succeeded, or no listeners matched the filter.
1Reserved for future --fail-on <pattern> style usage. Today no path returns 1.
2Setup 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, --kill prints Send 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 is SIGKILL (uncatchable). Reserved for the operator who is certain about the target.