Files

249 lines
9.5 KiB
JavaScript
Raw Permalink Normal View History

"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const {
parseSsOutput,
parseNetstatOutput,
parseLsofOutput,
parseListeningPorts,
} = require("./portOps.cjs");
test("parseSsOutput reads tcp/udp listeners and process fields", () => {
const sample = `
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=1234,fd=3))
tcp LISTEN 0 511 *:80 *:* users:(("nginx",pid=99,fd=6))
udp UNCONN 0 0 127.0.0.1:53 0.0.0.0:* users:(("systemd-resolve",pid=500,fd=12))
tcp6 LISTEN 0 128 [::]:443 [::]:* users:(("nginx",pid=99,fd=7))
`;
const ports = parseSsOutput(sample);
assert.equal(ports.length, 4);
assert.deepEqual(
ports.find((p) => p.port === 22),
{
id: "tcp|*|22|1234",
protocol: "tcp",
address: "*",
port: 22,
pid: 1234,
processName: "sshd",
},
);
assert.equal(ports.find((p) => p.port === 80)?.processName, "nginx");
assert.equal(ports.find((p) => p.port === 53)?.protocol, "udp");
assert.equal(ports.find((p) => p.port === 443)?.protocol, "tcp6");
});
test("parseNetstatOutput maps pid/program and keeps BusyBox UDP without PID", () => {
const sample = `
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1234/sshd
tcp6 0 0 :::80 :::* LISTEN 99/nginx
udp 0 0 127.0.0.1:53 0.0.0.0:*
udp 0 0 0.0.0.0:5353 0.0.0.0:* 456/dnsmasq
`;
const ports = parseNetstatOutput(sample);
assert.equal(ports.length, 4);
assert.equal(ports.find((p) => p.port === 22)?.processName, "sshd");
assert.equal(ports.find((p) => p.port === 80)?.address, "*");
assert.equal(ports.find((p) => p.port === 53)?.protocol, "udp");
assert.equal(ports.find((p) => p.port === 53)?.pid, null);
assert.equal(ports.find((p) => p.port === 5353)?.pid, 456);
assert.equal(ports.find((p) => p.port === 5353)?.processName, "dnsmasq");
});
test("parseNetstatOutput keeps spaced UDP program names without State", () => {
const sample = `
udp 0 0 0.0.0.0:5353 0.0.0.0:* 882/avahi-daemon: r
udp 0 0 0.0.0.0:53 0.0.0.0:* 100/named -u bind
`;
const ports = parseNetstatOutput(sample);
assert.equal(ports.find((p) => p.port === 5353)?.pid, 882);
assert.equal(ports.find((p) => p.port === 5353)?.processName, "avahi-daemon: r");
assert.equal(ports.find((p) => p.port === 53)?.pid, 100);
assert.equal(ports.find((p) => p.port === 53)?.processName, "named -u bind");
});
test("parseNetstatOutput understands macOS dotted addresses", () => {
const sample = `
Active Internet connections
Proto Recv-Q Send-Q Local Address Foreign Address (state)
tcp4 0 0 *.22 *.* LISTEN
tcp4 0 0 127.0.0.1.631 *.* LISTEN
`;
const ports = parseNetstatOutput(sample);
assert.equal(ports.length, 2);
assert.equal(ports.find((p) => p.port === 22)?.address, "*");
assert.equal(ports.find((p) => p.port === 631)?.address, "127.0.0.1");
});
test("parseListeningPorts merges ss/netstat/lsof and prefers process-aware rows", () => {
const stdout = `
__NC_PORTS_BEGIN__
__NC_NETSTAT__
tcp4 0 0 *.22 *.* LISTEN
__NC_LSOF__
sshd 1234 root 3u IPv4 0x1 0t0 TCP *:22 (LISTEN)
__NC_PORTS_END__
`;
const ports = parseListeningPorts(stdout);
assert.equal(ports.length, 1);
assert.equal(ports[0].port, 22);
assert.equal(ports[0].pid, 1234);
assert.equal(ports[0].processName, "sshd");
});
test("parseLsofOutput reads TCP LISTEN rows", () => {
const sample = `
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 99 root 6u IPv4 0xabc 0t0 TCP *:80 (LISTEN)
`;
const ports = parseLsofOutput(sample);
assert.equal(ports.length, 1);
assert.equal(ports[0].port, 80);
assert.equal(ports[0].processName, "nginx");
});
test("parseNetstatOutput rejects established TCP and connected UDP", () => {
const sample = `
tcp 0 0 10.0.0.1:22 10.0.0.2:40000 ESTABLISHED 1234/sshd
tcp 0 0 10.0.0.1:22 10.0.0.2:40000 1234/sshd
udp 0 0 10.0.0.5:68 10.0.0.1:67
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 9/sshd
`;
const ports = parseNetstatOutput(sample);
assert.equal(ports.length, 1);
assert.equal(ports[0].port, 22);
assert.equal(ports[0].processName, "sshd");
});
test("parseLsofOutput ignores connected UDP and non-listen TCP", () => {
const sample = `
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
chrome 1000 user 10u IPv4 0x1 0t0 UDP 10.0.0.1:53122->8.8.8.8:53
nginx 99 root 6u IPv4 0xabc 0t0 TCP 10.0.0.1:80->10.0.0.2:12345 (ESTABLISHED)
sshd 1234 root 3u IPv4 0x2 0t0 TCP *:22 (LISTEN)
`;
const ports = parseLsofOutput(sample);
assert.equal(ports.length, 1);
assert.equal(ports[0].port, 22);
});
test("parseLsofOutput keeps bracketed IPv6 listeners as tcp6", () => {
const sample = `
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
nginx 99 root 7u IPv6 0xabc 0t0 TCP [::1]:80 (LISTEN)
`;
const ports = parseLsofOutput(sample);
assert.equal(ports.length, 1);
assert.equal(ports[0].protocol, "tcp6");
assert.equal(ports[0].address, "::1");
assert.equal(ports[0].port, 80);
});
test("parseNetstatOutput keeps macOS IPv6 dotted ports", () => {
const sample = `
tcp6 0 0 ::1.631 *.* LISTEN
tcp6 0 0 fe80::1%lo0.22 *.* LISTEN
`;
const ports = parseNetstatOutput(sample);
assert.equal(ports.length, 2);
assert.equal(ports.find((p) => p.port === 631)?.address, "::1");
assert.equal(ports.find((p) => p.port === 22)?.address, "fe80::1");
});
test("parseSsOutput rejects UDP with non-wildcard peers", () => {
const sample = `
udp UNCONN 0 0 10.0.0.5:68 10.0.0.1:67 users:(("dhclient",pid=7,fd=1))
udp UNCONN 0 0 0.0.0.0:5353 0.0.0.0:* users:(("avahi",pid=8,fd=1))
`;
const ports = parseSsOutput(sample);
assert.equal(ports.length, 1);
assert.equal(ports[0].port, 5353);
});
test("parseListeningPorts merges ss tcp6 with lsof IPv6 without duplicating", () => {
const stdout = `
__NC_PORTS_BEGIN__
__NC_SS__
tcp6 LISTEN 0 128 [::]:80 [::]:* users:(("nginx",pid=99,fd=7))
__NC_LSOF__
nginx 99 root 7u IPv6 0xabc 0t0 TCP *:80 (LISTEN)
__NC_PORTS_END__
`;
const ports = parseListeningPorts(stdout);
assert.equal(ports.length, 1);
assert.equal(ports[0].protocol, "tcp6");
assert.equal(ports[0].pid, 99);
});
test("parseListeningPorts keeps SO_REUSEPORT listeners as separate pid rows", () => {
const sample = `
tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("app",pid=11,fd=3))
tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("app",pid=12,fd=3))
`;
const ports = parseSsOutput(sample);
assert.equal(ports.length, 2);
assert.deepEqual(ports.map((p) => p.pid).sort((a, b) => a - b), [11, 12]);
});
test("parseListeningPorts reads Windows UDP endpoints from Protocol field", () => {
const stdout = `
__NC_PORTS_BEGIN__
__NC_WIN__
[{"LocalAddress":"0.0.0.0","LocalPort":53,"OwningProcess":500,"Protocol":"udp"},{"LocalAddress":"::","LocalPort":22,"OwningProcess":1234,"Protocol":"tcp"}]
__NC_PORTS_END__
`;
const ports = parseListeningPorts(stdout);
assert.equal(ports.length, 2);
assert.equal(ports.find((p) => p.port === 53)?.protocol, "udp");
assert.equal(ports.find((p) => p.port === 22)?.protocol, "tcp6");
});
test("parseLsofOutput keeps Idle UDP listeners", () => {
const sample = `
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
mdnsd 200 root 12u IPv4 0x1 0t0 UDP *:5353
`;
const ports = parseLsofOutput(sample);
assert.equal(ports.length, 1);
assert.equal(ports[0].protocol, "udp");
assert.equal(ports[0].port, 5353);
});
test("parseNetstatOutput accepts macOS tcp46/udp46 dual-stack rows", () => {
const sample = `
tcp46 0 0 *.80 *.* LISTEN
udp46 0 0 *.5353 *.*
`;
const ports = parseNetstatOutput(sample);
assert.equal(ports.length, 2);
assert.equal(ports.find((p) => p.port === 80)?.protocol, "tcp6");
assert.equal(ports.find((p) => p.port === 5353)?.protocol, "udp6");
});
test("parseListeningPorts merges macOS tcp46 netstat with lsof IPv6", () => {
const stdout = `
__NC_PORTS_BEGIN__
__NC_NETSTAT__
tcp46 0 0 *.80 *.* LISTEN
__NC_LSOF__
nginx 99 root 7u IPv6 0xabc 0t0 TCP *:80 (LISTEN)
__NC_PORTS_END__
`;
const ports = parseListeningPorts(stdout);
assert.equal(ports.length, 1);
assert.equal(ports[0].protocol, "tcp6");
assert.equal(ports[0].pid, 99);
});
test("parseSsOutput expands multi-pid users tuples", () => {
const sample = `
tcp LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("app",pid=11,fd=3),("app",pid=12,fd=4))
`;
const ports = parseSsOutput(sample);
assert.equal(ports.length, 2);
assert.deepEqual(ports.map((p) => p.pid).sort((a, b) => a - b), [11, 12]);
});