netbeagle 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michel Salib
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,164 @@
1
+ <p align="center">
2
+ <img src="assets/logo.svg" width="128" height="128" alt="netbeagle logo: a friendly beagle peeking over a network switch, paws on its edge, two ports plugged in and one link light blinking">
3
+ </p>
4
+
5
+ <h1 align="center">netbeagle</h1>
6
+
7
+ <p align="center">See which process (or container) listens on each port, who can reach it, who is connected to it, and how everything on your machine connects.</p>
8
+
9
+ <p align="center">
10
+ <a href="https://www.npmjs.com/package/netbeagle"><img src="https://img.shields.io/npm/v/netbeagle?color=a8521a" alt="npm version"></a>
11
+ <a href="https://github.com/michelsalib/netbeagle/actions/workflows/ci.yml"><img src="https://github.com/michelsalib/netbeagle/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
12
+ <img src="https://img.shields.io/node/v/netbeagle" alt="Node version">
13
+ <img src="https://img.shields.io/badge/platform-Linux%20%7C%20WSL-blue" alt="Linux and WSL">
14
+ <a href="LICENSE"><img src="https://img.shields.io/npm/l/netbeagle" alt="MIT license"></a>
15
+ <a href="https://scorecard.dev/viewer/?uri=github.com/michelsalib/netbeagle"><img src="https://api.scorecard.dev/projects/github.com/michelsalib/netbeagle/badge" alt="OpenSSF Scorecard"></a>
16
+ </p>
17
+
18
+ What is on port 5432, a local postgres or the one in docker? Can my laptop's neighbours reach it, or only
19
+ this machine? Which process keeps a connection open to that server, and through which interface and
20
+ gateway does it leave? netbeagle answers those in one command, by reading what the kernel already
21
+ knows (`/proc`, `/sys`) and asking docker which container sits behind each published port.
22
+
23
+ **Try it, nothing to install:**
24
+
25
+ ```sh
26
+ npx netbeagle # every listening port
27
+ npx netbeagle map # how everything connects
28
+ npx netbeagle who 5432 # what is on 5432
29
+ ```
30
+
31
+ ```
32
+ $ netbeagle
33
+ PORT ADDRESS REACHABLE FROM CLIENTS OWNER
34
+ tcp 22 * every interface 1 root's process (sudo to see it)
35
+ udp 53 127.0.0.53 this machine only systemd-resolve's process (sudo to see it)
36
+ tcp 5000 127.0.0.1 this machine only 1 node 300
37
+ tcp 5432 0.0.0.0 every interface container pg :5432
38
+ tcp 8080 192.168.1.10 eth0 node 300
39
+ ```
40
+
41
+ ```
42
+ $ netbeagle map
43
+ box
44
+
45
+ listening on every interface
46
+ tcp 22 root's process (sudo to see it) ← 1 client
47
+ tcp 5432 container pg :5432
48
+
49
+ lo loopback up 127.0.0.1/8 ::1/128
50
+ routes ::1/128
51
+ listen udp 53 systemd-resolve's process (sudo to see it)
52
+ listen tcp 5000 node 300 ← 1 client
53
+ local curl 400 → :5000 node 300
54
+
55
+ eth0 ethernet up 192.168.1.10/24 fe80::5054:ff:fe12:3456/64
56
+ routes default via 192.168.1.1, 192.168.1.0/24
57
+ listen tcp 8080 node 300
58
+ out curl 400 → 140.82.121.4:443
59
+
60
+ br-abc bridge up 172.18.0.1/16 docker network app_default
61
+ routes 172.18.0.0/16
62
+ holds pg 172.18.0.2 ← host :5432
63
+ 1 port: veth1
64
+
65
+ down, no address: docker0
66
+ dns 127.0.0.53 (local resolver)
67
+ ```
68
+
69
+ ## Install
70
+
71
+ netbeagle is a single JavaScript file that runs on Node ≥ 20 or Bun, on Linux and WSL. Without either,
72
+ the installer fetches a standalone binary instead (x64 or arm64), which needs nothing.
73
+
74
+ ```sh
75
+ npx netbeagle map # or: bunx netbeagle map, nothing to install
76
+ npm install -g netbeagle # or: bun add -g netbeagle
77
+ curl -fsSL https://github.com/michelsalib/netbeagle/releases/latest/download/install.sh | sh
78
+ ```
79
+
80
+ To pin a version, or to read the script before running it:
81
+
82
+ ```sh
83
+ curl -fsSL https://github.com/michelsalib/netbeagle/releases/download/v0.1.0/install.sh -o install.sh
84
+ less install.sh && sh install.sh
85
+ gh attestation verify install.sh -R michelsalib/netbeagle # optional: proves GitHub Actions built it
86
+ ```
87
+
88
+ The binaries are release files too (`netbeagle-linux-x64`, `netbeagle-linux-arm64`), listed in
89
+ `SHA256SUMS` and attested like the rest; see [SECURITY.md](SECURITY.md#verifying-a-release).
90
+
91
+ Without root, netbeagle can't see other users' processes, so their sockets show the user only;
92
+ `sudo netbeagle` shows them all. netbeagle only reads: see [what it does with your machine](SECURITY.md#what-netbeagle-does-with-your-machine).
93
+
94
+ ## Commands
95
+
96
+ | Command | What it shows |
97
+ |---|---|
98
+ | `netbeagle` / `netbeagle list` | every listening port (TCP and UDP): the addresses it is bound to, who can reach it (this machine only, every interface, or the interfaces holding its address), how many clients are connected, and its owner: a process, or the docker container behind a published port |
99
+ | `netbeagle who PORT` | what listens on `PORT`: its command line, or its container with the address and network inside; where it is reachable from; every client connected now, with its process when it runs here; and connections from this machine to `PORT` elsewhere. Exits 1 when there are none |
100
+ | `netbeagle conns` | every connection, grouped by process: inbound (←) to its ports and outbound (→), with the process, container or route at the other end. `--all` adds sockets opening or closing |
101
+ | `netbeagle interfaces` | every interface: kind (ethernet, wifi, bridge, veth, tun, wireguard…), state, addresses, routes, bridge ports, docker containers on it, traffic |
102
+ | `netbeagle route HOST` | where packets to `HOST` go: this machine, a container, an attached network, or a gateway; with the interface, the route and the source address |
103
+ | `netbeagle map` | the whole picture, interface by interface: what is open everywhere, what is bound to each interface, the containers behind each bridge, the connections leaving through each one, which local process talks to which, and the DNS servers |
104
+
105
+ `netbeagle COMMAND --help` (or `netbeagle help COMMAND`) shows one command's usage, options and examples.
106
+ `if`, `ifaces`, `connections`, `graph` and `ls` are accepted too.
107
+
108
+ Options: `--json`, `--tcp`, `--udp`, `-a, --all`, `--no-docker` (don't ask docker), `--show-secrets` (command
109
+ lines are shown with the values of `--token=`, `--password` and the like masked by default).
110
+
111
+ ## How it works
112
+
113
+ - **Sockets** come from `/proc/net/tcp`, `tcp6`, `udp` and `udp6`; IPv4 clients of a socket bound to `::`
114
+ (`::ffff:1.2.3.4`) are shown as IPv4. Each socket's process is found through the `socket:[inode]` links
115
+ in `/proc/PID/fd`. Without root, other users' processes can't be read: their sockets show the user
116
+ only, and `sudo netbeagle` shows them all.
117
+ - **Interfaces** come from `/sys/class/net` (kind, state, bridge and its ports, traffic) and their
118
+ addresses from `getifaddrs`. **Routes** from the kernel's main table, `/proc/net/route` and
119
+ `/proc/net/ipv6_route`; `route` picks the longest matching prefix, then the lowest metric, as the
120
+ kernel does (policy routing, `ip rule`, is not replayed).
121
+ - **Containers**: `docker inspect` of the running containers gives their networks (and the host bridge
122
+ each uses, `docker0` or `br-<network id>`) and their published ports. A published port belongs to
123
+ its container, whether `docker-proxy` holds a socket for it or the firewall forwards it with none.
124
+ - **Who reaches what**: a socket on `127.0.0.1`, `::1` or another address of the loopback is reachable
125
+ from this machine only; on `0.0.0.0` or `::`, from every interface (`::` takes IPv4 too, as
126
+ `net.ipv6.bindv6only` is 0 by default); on one address, from the interface holding it. Firewall rules
127
+ are not taken into account.
128
+ - **WSL**: Windows forwards its own `localhost` to ports bound to `127.0.0.1` or every interface in the
129
+ WSL VM, so those say `+ Windows`.
130
+ - Connections between two processes of this machine are matched end to end, so `curl 400 → :5000 node
131
+ 300` names both sides.
132
+
133
+ ## Status
134
+
135
+ Linux, including WSL. Planned: macOS (`lsof`, `netstat`), Windows (`Get-NetTCPConnection`),
136
+ `netbeagle doctor` (services exposed on every interface, ports bound twice, DNS servers with no
137
+ route), firewall rules, and network namespaces (inside containers).
138
+
139
+ ## The pack
140
+
141
+ netbeagle has a sibling, built the same way: one small file for Node or Bun, or a standalone binary,
142
+ nothing to configure.
143
+
144
+ | Tool | What it finds | Try it |
145
+ |---|---|---|
146
+ | **netbeagle** | which process or container listens on each port, who can reach it, and how everything on your machine connects | `npx netbeagle` |
147
+ | **[envhound](https://github.com/michelsalib/envhound)** | which startup file (or registry key) sets each environment variable, and your `PATH` | `npx envhound` |
148
+
149
+ ## Development
150
+
151
+ ```sh
152
+ bun install
153
+ bun run dev -- map # run from source
154
+ bun test # unit tests on test/fixtures/root, plus the built CLI on node on this machine
155
+ bun run typecheck
156
+ bun run lint # Biome: formatting and lint
157
+ bun run build # dist/netbeagle.js, plain JS for node >= 20
158
+ ```
159
+
160
+ `test/fixtures/root` is a small machine as `/proc`, `/sys` and `/etc` show it, written by
161
+ `scripts/fixture.py` (its docstring describes it). Source uses only `node:` APIs, so the build runs on
162
+ both Node and Bun.
163
+
164
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for pull requests and releases.
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ import{lookup as Nt}from"node:dns/promises";import{isIP as Et}from"node:net";import{parseArgs as Ct}from"node:util";var E={name:"netbeagle",version:"0.1.0",description:"See which process listens on each port, which interfaces reach it, and how everything on your machine connects",keywords:["network","ports","netstat","ss","lsof","interfaces","docker","routes"],license:"MIT",author:"Michel Salib",repository:{type:"git",url:"git+https://github.com/michelsalib/netbeagle.git"},homepage:"https://github.com/michelsalib/netbeagle#readme",bugs:"https://github.com/michelsalib/netbeagle/issues",type:"module",bin:{netbeagle:"dist/netbeagle.js"},files:["dist/netbeagle.js"],engines:{node:">=20"},scripts:{dev:"bun src/cli.ts",build:"bun build src/cli.ts --target=node --minify --outfile dist/netbeagle.js","build:bin":"bun scripts/build-binaries.ts",test:"bun test --timeout 30000",typecheck:"tsc",lint:"biome check",prepublishOnly:"bun run typecheck && bun run lint && bun run build && bun test --timeout 30000"},devDependencies:{"@biomejs/biome":"2.5.15","@types/bun":"^1.4.2","@types/node":"^26.6.4",typescript:"^7.0.2"}};import{isIPv4 as fe,isIPv6 as me}from"node:net";function K(e){if(fe(e))return e.split(".").map(Number);if(!me(e))return;let t=e.replace(/%.*$/,""),n=t.match(/(\d+\.\d+\.\d+\.\d+)$/);if(n?.[1]){let[a,d,u,p]=n[1].split(".").map(Number);t=`${t.slice(0,-n[1].length)}${(a<<8|d).toString(16)}:${(u<<8|p).toString(16)}`}let[o="",s]=t.split("::"),r=o?o.split(":"):[],i=s?s.split(":"):[];return(s===void 0?r:[...r,...Array(8-r.length-i.length).fill("0"),...i]).flatMap((a)=>{let d=Number.parseInt(a,16);return[d>>8,d&255]})}var he=(e)=>fe(e)?4:me(e)?6:void 0;function Y(e){let t=[];for(let r=0;r<16;r+=2)t.push((e[r]??0)<<8|(e[r+1]??0));let n=-1,o=1;for(let r=0;r<8;){if(t[r]!==0){r++;continue}let i=r;while(i<8&&t[i]===0)i++;if(i-r>o)n=r,o=i-r;r=i}let s=t.map((r)=>r.toString(16));if(n<0)return s.join(":");return`${s.slice(0,n).join(":")}::${s.slice(n+o).join(":")}`}function z(e){let t=Number.parseInt(e,16);return[t&255,t>>>8&255,t>>>16&255,t>>>24&255]}var S=(e)=>z(e).join(".");function ge(e){let t=[];for(let n=0;n<32;n+=8)t.push(...z(e.slice(n,n+8)));if(t.slice(0,10).every((n)=>n===0)&&t[10]===255&&t[11]===255)return{family:4,address:t.slice(12).join(".")};return{family:6,address:Y(t)}}function q(e){let t=[];for(let n=0;n<32;n+=2)t.push(Number.parseInt(e.slice(n,n+2),16));return Y(t)}function X(e,t,n){let o=K(e),s=K(t);if(!o||!s||o.length!==s.length)return!1;for(let r=0;r<n;r++){let i=r>>3,c=128>>(r&7);if(((o[i]??0)&c)!==((s[i]??0)&c))return!1}return!0}function be(e){let n=(K(e.address)??[]).map((o,s)=>{let r=Math.max(0,Math.min(8,e.prefix-s*8));return o&(255<<8-r&255)});return e.family===4?n.join("."):Y(n)}var k=(e)=>e==="0.0.0.0"||e==="::",v=(e)=>e==="::1"||e.startsWith("127."),ke=(e)=>e.startsWith("169.254.")||/^fe[89ab][0-9a-f]:/i.test(e),I=(e)=>e.address.includes(":")?`[${e.address}]:${e.port}`:`${e.address}:${e.port}`,we=(e)=>z(e).reduce((t,n)=>t+n.toString(2).replaceAll("0","").length,0);var Q=(e,t)=>e.users[t]??String(t),Z=(e,t)=>t===void 0?void 0:e.processes.find((n)=>n.pid===t),C=(e,t)=>e.interfaces.find((n)=>n.addresses.some((o)=>o.address===t));function ye(e,t){for(let n of e.containers)for(let o of n.networks)if(o.ip===t)return{container:n,network:o};return}function j(e,t){let n=he(t)??4,o={dest:t,family:n};if(v(t))return{...o,kind:"loopback",iface:"lo",source:t};let s=C(e,t);if(s)return{...o,kind:"local",iface:s.name,source:t};let r=e.routes.filter((p)=>p.family===n&&X(t,p.dest,p.prefix)).sort((p,f)=>f.prefix-p.prefix||p.metric-f.metric)[0];if(!r)return{...o,kind:"unreachable"};let i=e.interfaces.find((p)=>p.name===r.iface),c=r.gateway??t,a=i?.addresses.filter((p)=>p.family===n)??[],d=a.find((p)=>X(c,be(p),p.prefix))?.address??a.find((p)=>!ke(p.address))?.address??a[0]?.address,u=ye(e,t);return{...o,kind:u?"container":r.gateway?"gateway":"network",iface:r.iface,gateway:r.gateway,route:r,source:d,container:u?.container}}function ot(e,t){let n=(r)=>v(r)||C(e,r)?.kind==="loopback";if(t.every(n))return{scope:"local",interfaces:["lo"]};let o=e.interfaces.filter((r)=>r.state!=="down"&&r.addresses.length);if(t.some(k)){let r=new Set(t.some((i)=>i==="::")?[4,6]:[4]);return{scope:"everywhere",interfaces:o.filter((i)=>!i.master).filter((i)=>i.addresses.some((c)=>r.has(c.family))).map((i)=>i.name)}}let s=new Set;for(let r of t){if(v(r))s.add("lo");let i=C(e,r);if(i)s.add(i.name)}return{scope:"interfaces",interfaces:[...s]}}var rt=(e,t,n,o)=>e.proto===t&&e.hostPort===n&&(o===void 0||k(e.hostIp)||k(o)||e.hostIp===o);function st(e,t,n,o){for(let s of e.containers)for(let r of s.ports)if(rt(r,t,n,o))return{container:s,published:r};return}var $e=(e)=>e.state==="LISTEN"||e.state==="UNCONN"&&e.local.port!==0;function R(e){let t=new Map;for(let o of e.sockets.filter($e)){let s=st(e,o.proto,o.local.port,o.local.address),r=s?`c${s.container.id}`:o.pid!==void 0?`p${o.pid}`:`u${o.uid}`,i=`${o.proto}/${o.local.port}/${r}`,c=t.get(i);if(!c)c={proto:o.proto,port:o.local.port,addresses:[],reach:{scope:"local",interfaces:[]},process:Z(e,o.pid),user:Q(e,o.uid),container:s?.container,published:s?.published,sockets:[],clients:[]},t.set(i,c);if(c.sockets.push(o),!c.addresses.includes(o.local.address))c.addresses.push(o.local.address)}for(let o of e.containers)for(let s of o.ports){if([...t.values()].some((a)=>a.proto===s.proto&&a.port===s.hostPort&&a.container?.id===o.id))continue;let i=`${s.proto}/${s.hostPort}/c${o.id}`,c=t.get(i);if(c){if(!c.addresses.includes(s.hostIp))c.addresses.push(s.hostIp);continue}t.set(i,{proto:s.proto,port:s.hostPort,addresses:[s.hostIp],reach:{scope:"local",interfaces:[]},user:"root",container:o,published:s,sockets:[],clients:[]})}let n=[...t.values()];for(let o of n)o.reach=ot(e,o.addresses);for(let o of P(e,n))if(o.direction==="in")O(n,o.proto,o.local)?.clients.push(o);return n.sort((o,s)=>o.port-s.port||o.proto.localeCompare(s.proto))}function O(e,t,n){return e.find((o)=>o.proto===t&&o.port===n.port&&o.addresses.some((s)=>s===n.address||k(s)))}function P(e,t){let n=t??R(e),o=[];for(let s of e.sockets){if($e(s)||s.remote.port===0)continue;let r=O(n,s.proto,s.local)?"in":"out";o.push({proto:s.proto,local:s.local,remote:s.remote,state:s.state,process:Z(e,s.pid),user:s.inode===0?void 0:Q(e,s.uid),direction:r,peer:it(e,n,s)})}return o}function it(e,t,n){let o=n.remote.address;if(v(o)||C(e,o)){let r=e.sockets.find((c)=>c!==n&&c.proto===n.proto&&c.local.port===n.remote.port&&c.remote.port===n.local.port&&c.local.address===o&&c.remote.address===n.local.address),i=O(t,n.proto,n.remote);return{kind:"local",process:Z(e,r?.pid),user:r?Q(e,r.uid):void 0,listener:i&&O(t,n.proto,n.local)!==i?i:void 0}}let s=ye(e,o);if(s)return{kind:"container",container:s.container,network:s.network.name};return{kind:"remote",route:j(e,o)}}function xe(e,t){return{port:t,listeners:R(e).filter((n)=>n.port===t),outbound:P(e).filter((n)=>n.direction==="out"&&n.remote.port===t&&!(n.peer.kind==="local"&&n.peer.listener?.port===t))}}function ee(e){let t=R(e),n=P(e,t).filter((r)=>r.state==="ESTABLISHED"),o=[],s=[];for(let r of e.interfaces){if(r.master)continue;let i=e.containers.flatMap((d)=>d.networks.filter((u)=>u.bridge===r.name).map((u)=>({container:d,ip:u.ip}))),c={iface:r,routes:e.routes.filter((d)=>d.iface===r.name),listeners:t.filter((d)=>d.reach.scope!=="everywhere"&&d.reach.interfaces.includes(r.name)),outbound:n.filter((d)=>d.direction==="out"&&d.peer.kind==="remote"&&d.peer.route.iface===r.name),local:r.kind==="loopback"?n.filter((d)=>d.peer.kind==="local"&&d.direction==="out"):[],containers:i},a=!r.addresses.length&&!c.listeners.length&&!c.outbound.length&&!i.length;if(r.state==="down"&&a)s.push(r.name);else o.push(c)}return{hostname:e.hostname,environment:e.environment,everywhere:t.filter((r)=>r.reach.scope==="everywhere"),interfaces:o,idle:s,dns:e.dns.map((r)=>({server:r,route:j(e,r)})),hidden:e.hidden}}var Se=(e)=>e.sockets.filter((t)=>t.pid===void 0&&t.uid!==e.uid).length;import{readFileSync as $t}from"node:fs";import{hostname as xt}from"node:os";import{join as B}from"node:path";import{spawnSync as ct}from"node:child_process";function at(e){let t;try{t=JSON.parse(e)}catch{return[]}return t.map((n)=>{let o=[];for(let[s,r]of Object.entries(n.NetworkSettings?.Ports??{})){let[i="0",c="tcp"]=s.split("/");for(let a of r??[])o.push({proto:c,hostIp:a.HostIp||"0.0.0.0",hostPort:Number(a.HostPort),containerPort:Number(i)})}return{id:(n.Id??"").slice(0,12),name:(n.Name??"").replace(/^\//,""),image:n.Config?.Image??"",project:n.Config?.Labels?.["com.docker.compose.project"],networks:Object.entries(n.NetworkSettings?.Networks??{}).map(([s,r])=>({name:s,bridge:s==="bridge"?"docker0":r.NetworkID&&!["host","none"].includes(s)?`br-${r.NetworkID.slice(0,12)}`:void 0,ip:r.IPAddress||r.GlobalIPv6Address||void 0})),ports:o}})}function ve(){let e=(o)=>ct("docker",o,{encoding:"utf8",timeout:5000}),t=e(["ps","-q"]);if(t.status!==0||!t.stdout.trim())return[];let n=e(["inspect",...t.stdout.trim().split(/\s+/)]);return n.status===0?at(n.stdout):[]}import{existsSync as L,readdirSync as te,readFileSync as dt,readlinkSync as ut}from"node:fs";import{networkInterfaces as pt}from"node:os";import{basename as Ie,join as b}from"node:path";function Re(){let e={};for(let[t,n]of Object.entries(pt()))e[t]=(n??[]).map((o)=>({family:o.family==="IPv6"?6:4,address:o.address.replace(/%.*$/,""),prefix:Number(o.cidr?.split("/")[1]??(o.family==="IPv6"?128:32))}));return e}function Pe(e,t){let n=b(e,"class/net"),o=[];try{o=te(n)}catch{}let s=o.map((r)=>{let i=b(n,r),c=(m)=>{try{return dt(b(i,m),"utf8").trim()}catch{return""}},a=(m)=>Number(c(m))||0,d;try{d=Ie(ut(b(i,"master")))}catch{}let u=[];try{u=te(b(i,"brif"))}catch{u=te(i).filter((m)=>m.startsWith("lower_")).map((m)=>m.slice(6))}let p=c("operstate"),f=c("address");return{name:r,index:a("ifindex"),kind:lt(i,c),state:p==="up"?"up":p==="down"?"down":"unknown",mtu:a("mtu"),mac:f&&f!=="00:00:00:00:00:00"?f:void 0,addresses:t[r]??[],master:d,members:u.sort(),rxBytes:a("statistics/rx_bytes"),txBytes:a("statistics/tx_bytes")}});for(let r of s)if(r.kind==="loopback"&&r.state==="unknown"&&r.addresses.length)r.state="up";return s.sort((r,i)=>r.index-i.index)}function lt(e,t){let n=t("type");if(n==="772")return"loopback";let o=t("uevent").match(/^DEVTYPE=(.*)$/m)?.[1];if(o==="wlan"||L(b(e,"wireless")))return"wifi";if(o==="bridge"||L(b(e,"bridge")))return"bridge";if(o==="vlan")return"vlan";if(o==="bond"||L(b(e,"bonding")))return"bond";if(o==="wireguard"||n==="65534")return o==="wireguard"?"wireguard":"tun";if(Ie(e).startsWith("veth"))return"veth";if(L(b(e,"device")))return"ethernet";return"virtual"}import{readdirSync as Te,readFileSync as Ne,readlinkSync as ft}from"node:fs";import{join as A}from"node:path";function Ee(e){let t=[],n=new Map,o=0,s=[];try{s=Te(e).filter((r)=>/^\d+$/.test(r))}catch{}s.sort((r,i)=>Number(r)-Number(i));for(let r of s){let i=Number(r),c=A(e,r),a;try{a=Te(A(c,"fd"))}catch{o++;continue}let d=0;for(let u of a){let p;try{p=ft(A(c,"fd",u))}catch{continue}let f=p.match(/^socket:\[(\d+)\]$/);if(!f)continue;d++;let m=Number(f[1]);if(!n.has(m))n.set(m,i)}if(d===0)continue;t.push(mt(c,i))}return{processes:t,owners:n,hidden:o}}function mt(e,t){let n=(d)=>{try{return Ne(A(e,d),"utf8")}catch{return""}},o=n("comm").trim(),s=n("cmdline").split("\x00").filter(Boolean),r=s[0]?.split("/").pop(),i=(o&&s.join(" ").includes(o)?o:r||o)||String(t),c=s.join(" ")||`[${i}]`,a=Number(n("status").match(/^Uid:\s+(\d+)/m)?.[1]??-1);return{pid:t,name:i,cmd:c,uid:a}}function Ce(e){let t={},n="";try{n=Ne(e,"utf8")}catch{}for(let o of n.split(`
3
+ `)){let[s,,r]=o.split(":");if(s&&r&&/^\d+$/.test(r))t[Number(r)]=s}return t}import{readFileSync as ht}from"node:fs";import{join as gt}from"node:path";var Oe=1,je=2,Le=512;function bt(e){let t=[];for(let n of e.split(`
4
+ `).slice(1)){let[o,s,r,i,,,c,a]=n.trim().split(/\s+/);if(!o||!s||!r||!i||!a)continue;let d=Number.parseInt(i,16);if(!(d&Oe)||d&Le)continue;t.push({family:4,dest:S(s),prefix:we(a),gateway:d&je?S(r):void 0,iface:o,metric:Number(c)||0})}return t}function kt(e){let t=[];for(let n of e.split(`
5
+ `)){let o=n.trim().split(/\s+/);if(o.length<10)continue;let[s="",r="0",,,i="",c="0",,,a="0",d=""]=o,u=Number.parseInt(a,16);if(!(u&Oe)||u&Le)continue;let p=q(s),f=Number.parseInt(r,16);if(p.startsWith("ff")||f===128&&d!=="lo"||d==="lo"&&p!=="::1")continue;let m=q(i);t.push({family:6,dest:p,prefix:f,gateway:u&je&&m!=="::"?m:void 0,iface:d,metric:Number.parseInt(c,16)})}return t}function Ae(e){let t=(n)=>{try{return ht(gt(e,n),"utf8")}catch{return""}};return[...bt(t("net/route")),...kt(t("net/ipv6_route"))]}import{readFileSync as wt}from"node:fs";import{join as M}from"node:path";var yt={"01":"ESTABLISHED","02":"SYN_SENT","03":"SYN_RECV","04":"FIN_WAIT1","05":"FIN_WAIT2","06":"TIME_WAIT","07":"CLOSE","08":"CLOSE_WAIT","09":"LAST_ACK","0A":"LISTEN","0B":"CLOSING"};function W(e,t,n){let o=[];for(let s of e.split(`
6
+ `).slice(1)){let r=s.trim().split(/\s+/);if(r.length<10)continue;let[,i="",c="",a="",,,,d="0",,u="0"]=r,p=(T)=>{let[l="",h="0"]=T.split(":");return{...n?ge(l):{family:4,address:S(l)},port:Number.parseInt(h,16)}},f=p(i),m=p(c),x=t==="udp"?m.port===0?"UNCONN":"ESTABLISHED":yt[a.toUpperCase()];if(!x)continue;o.push({proto:t,family:f.family,local:{address:f.address,port:f.port},remote:{address:m.address,port:m.port},state:x,uid:Number(d),inode:Number(u)})}return o}var _=(e)=>{try{return wt(e,"utf8")}catch{return""}};function Me(e){return[...W(_(M(e,"net/tcp")),"tcp",!1),...W(_(M(e,"net/tcp6")),"tcp",!0),...W(_(M(e,"net/udp")),"udp",!1),...W(_(M(e,"net/udp6")),"udp",!0)]}function We(e={}){let t=e.root??"/",n=B(t,"proc"),o=(d)=>{try{return $t(B(t,d),"utf8")}catch{return""}},s=Pe(B(t,"sys"),e.addresses??Re()),r=Ee(n),i=Me(n).map((d)=>({...d,pid:r.owners.get(d.inode)})),c=new Set(s.map((d)=>d.name)),a=(e.containers??ve()).map((d)=>({...d,networks:d.networks.map((u)=>({...u,bridge:u.bridge&&c.has(u.bridge)?u.bridge:void 0}))}));return{hostname:e.hostname??xt(),environment:/microsoft|wsl/i.test(o("proc/sys/kernel/osrelease"))?"wsl":"linux",interfaces:s,routes:Ae(n),sockets:i,processes:r.processes,containers:a,users:Ce(B(t,"etc/passwd")),dns:[...o("etc/resolv.conf").matchAll(/^\s*nameserver\s+(\S+)/gm)].map((d)=>d[1]??""),uid:e.uid??process.getuid?.()??0,hidden:r.hidden}}function g(e){let t=(n)=>(o)=>e.color&&o?`\x1B[${n}m${o}\x1B[0m`:o;return{bold:t("1"),dim:t("2"),red:t("31"),green:t("32"),yellow:t("33"),blue:t("34"),magenta:t("35"),cyan:t("36")}}function ne(e,t){return t!==void 0&&t>0&&e.length>t?`${e.slice(0,Math.max(0,t-1))}…`:e}function _e(e,t,n){let o=e[0]?.map((s,r)=>Math.max(...e.map((i)=>(i[r]??"").length)))??[];return e.map((s)=>{let r=0;return s.map((i,c)=>{let d=c===s.length-1?ne(i,n===void 0?void 0:Math.max(10,n-r)):i.padEnd(o[c]??0);return r+=(o[c]??0)+2,(t[c]??((u)=>u))(d)}).join(" ").trimEnd()}).join(`
7
+ `)}var oe=(e)=>`${e.name} ${e.pid}`,re=(e)=>e.process?oe(e.process):e.user?`${e.user}'s process`:"closing, no process";function H(e){if(e.container)return`container ${e.container.name}${e.published?` :${e.published.containerPort}`:""}`;if(e.process)return oe(e.process);return`${e.user}'s process (sudo to see it)`}function Fe(e,t){let n=t.wsl&&e.addresses.some((s)=>k(s)||s.startsWith("127.")),o=e.reach.scope==="local"?"this machine only":e.reach.scope==="everywhere"?"every interface":e.reach.interfaces.join(", ")||"no interface";return n?`${o} + Windows`:o}var St=(e)=>{let t=e.addresses;if(t.length===2&&t.includes("0.0.0.0")&&t.includes("::"))return"*";return t.join(", ")};function D(e,t){switch(e.kind){case"local":{let n=e.listener?H(e.listener):e.process?oe(e.process):e.user&&`${e.user}'s process`||"this machine";return`${t} ${n}`}case"container":return`${t} container ${e.container.name}`;case"remote":return t}}var se=(e)=>e.kind==="unreachable"?"no route":e.gateway?`via ${e.iface} → ${e.gateway}`:`via ${e.iface}`;function He(e,t){let n=g(t);if(!e.length)return n.dim("nothing is listening");let o=[["PORT","ADDRESS","REACHABLE FROM","CLIENTS","OWNER"],...e.map((a)=>[`${a.proto} ${a.port}`,St(a),Fe(a,t),a.clients.length?String(a.clients.length):"",H(a)])],s=(a)=>a.startsWith("this machine")?n.green(a):a.startsWith("every")?n.yellow(a):a,r=[n.bold,(a)=>a,s,n.cyan,(a)=>a],[i=""]=_e(o,r.map(()=>(a)=>a),t.width).split(`
8
+ `),c=_e(o,r,t.width).split(`
9
+ `).slice(1);return[n.dim(i),...c].join(`
10
+ `)}var Be=(e)=>{let t=["B","kB","MB","GB","TB"],n=0,o=e;while(o>=1000&&n<t.length-1)o/=1000,n++;return`${n?o.toFixed(1):o} ${t[n]}`},vt=(e)=>e.addresses.map((t)=>`${t.address}/${t.prefix}`);function Je(e,t,n=""){let o=g(t),s=e.state==="up"?o.green("up"):e.state==="down"?o.red("down"):o.dim(e.state);return[o.bold(e.name),e.kind,s,vt(e).join(" "),n&&o.dim(n)].filter(Boolean).join(" ")}var Ve=(e)=>{let t=[...new Set(e.containers.flatMap((n)=>n.container.networks.filter((o)=>o.bridge===e.iface.name).map((o)=>o.name)))];return t.length?`docker network ${t.join(", ")}`:""},F=(e)=>e.filter((t)=>!(t.family===6&&t.dest==="fe80::")),ie=(e)=>(e.prefix===0?"default":`${e.dest}/${e.prefix}`)+(e.gateway?` via ${e.gateway}`:"");function Ge(e,t,n){let o=g(n),s=[];for(let r of e){let i=r.iface;s.push(Je(i,n,Ve(r)));let c=[];if(F(r.routes).length)c.push(["routes",F(r.routes).map(ie).join(", ")]);if(i.members.length){let u=i.members.filter((p)=>p.startsWith("veth")).length;c.push(["ports",u===i.members.length?`${u} veth`:i.members.join(", ")])}if(r.containers.length)c.push(["containers",r.containers.map((u)=>`${u.container.name}${u.ip?` ${u.ip}`:""}`).join(", ")]);let a=r.listeners.length;if(a)c.push(["listening",`${a} port${a>1?"s":""} bound here`]);if(r.outbound.length)c.push(["out",`${r.outbound.length} connection${r.outbound.length>1?"s":""}`]);let d=[i.mac,i.mtu?`mtu ${i.mtu}`:"",`↓ ${Be(i.rxBytes)} ↑ ${Be(i.txBytes)}`].filter(Boolean);c.push(["",d.join(" ")]);for(let[u,p]of c)s.push(` ${o.dim(u.padEnd(10))} ${ne(p,n.width&&n.width-13)}`);s.push("")}if(t.length)s.push(o.dim(`down, no address: ${t.join(", ")}`));return s.join(`
11
+ `).trimEnd()}function Ue(e,t){let n=g(t);if(!e.length)return n.dim("no connections");let o=new Map;for(let r of e){let i=re(r);o.set(i,[...o.get(i)??[],r])}let s=[];for(let[r,i]of[...o].sort((c,a)=>a[1].length-c[1].length)){s.push(`${n.bold(r)} ${n.dim(`${i.length} connection${i.length>1?"s":""}`)}`);let c=new Map;for(let a of i){let d=a.direction==="in"?n.magenta("←"):n.cyan("→"),u=a.direction==="in"?`:${a.local.port} `:"",p=a.peer.kind==="remote"?n.dim(se(a.peer.route)):"",f=a.state==="ESTABLISHED"?"":n.yellow(a.state),m=[` ${u}${d} ${D(a.peer,I(a.remote))}`,p,f].filter(Boolean).join(" ");c.set(m,(c.get(m)??0)+1)}for(let[a,d]of c)s.push(d>1?`${a} ${n.dim(`×${d}`)}`:a)}return s.join(`
12
+ `)}function Ke(e,t){let n=g(t),o=[];if(!e.listeners.length&&!e.outbound.length)return`nothing listens on port ${e.port}, and nothing here talks to it`;for(let s of e.listeners){o.push(`${n.bold(`${s.proto} ${s.port}`)} ${H(s)}`);let r=[];if(s.process)r.push(["process",`${s.process.cmd} ${n.dim(`(pid ${s.process.pid}, ${s.user})`)}`]);else if(!s.container)r.push(["process",n.yellow(`owned by ${s.user}: run with sudo to see which process`)]);if(s.container){let i=s.container;r.push(["container",`${i.name} ${n.dim(i.image)}${i.project?n.dim(` compose project ${i.project}`):""}`]);for(let c of i.networks)r.push(["",`→ ${c.ip??"?"}:${s.published?.containerPort??s.port} on ${c.name}${c.bridge?n.dim(` (${c.bridge})`):""}`])}if(r.push(["bound to",s.addresses.join(", ")]),r.push(["reachable",Fe(s,t)+(s.reach.scope==="everywhere"?n.dim(`: ${s.reach.interfaces.join(", ")}`):"")]),s.reach.scope==="everywhere")r.push(["",n.yellow("any machine that can reach one of those interfaces can connect")]);if(s.clients.length){r.push(["clients",`${s.clients.length}`]);for(let i of s.clients)r.push(["",`← ${D(i.peer,I(i.remote))}`])}else r.push(["clients",n.dim("none right now")]);for(let[i,c]of r)o.push(` ${n.dim(i.padEnd(10))} ${c}`);o.push("")}if(e.outbound.length){o.push(n.bold(`connections to port ${e.port} elsewhere`));for(let s of e.outbound){let r=re(s),i=s.peer.kind==="remote"?` ${n.dim(se(s.peer.route))}`:"";o.push(` ${r} → ${D(s.peer,I(s.remote))}${i}`)}}return o.join(`
13
+ `).trimEnd()}function Ye(e,t,n){let o=g(n),s=t===e.dest?e.dest:`${t} (${e.dest})`,r=[];switch(e.kind){case"loopback":return`${o.bold(s)} is this machine, through the loopback ${o.dim("(lo)")}`;case"local":return`${o.bold(s)} is this machine: an address of ${e.iface}, delivered through the loopback`;case"unreachable":return`${o.bold(s)}: ${o.red("no route")}: packets to it are dropped`;case"container":r.push(["is",`container ${e.container?.name}, on this machine`]);break;case"network":r.push(["is",`on the network of ${e.iface}, reached directly`]);break;case"gateway":r.push(["is",`beyond ${e.gateway}, the gateway on ${e.iface}`])}if(r.push(["interface",e.iface??""]),e.route)r.push(["route",`${ie(e.route)}${e.route.metric?` metric ${e.route.metric}`:""}`]);if(e.source)r.push(["from",e.source]);return[o.bold(s),...r.map(([i,c])=>` ${o.dim(i.padEnd(10))} ${c}`)].join(`
14
+ `)}function ze(e,t){let n=g(t),o=[];o.push(`${n.bold(e.hostname)}${e.environment==="wsl"?n.dim(" (WSL: Windows reaches ports on 127.0.0.1 and every interface through its localhost)"):""}`),o.push("");let s=(r)=>`${n.bold(`${r.proto} ${r.port}`.padEnd(9))} ${H(r)}${r.clients.length?n.cyan(` ← ${r.clients.length} client${r.clients.length>1?"s":""}`):""}`;if(e.everywhere.length){o.push(`${n.yellow("listening on every interface")}`);for(let r of e.everywhere)o.push(` ${s(r)}`);o.push("")}for(let r of e.interfaces){let i=r.iface,c=Ve(r);if(o.push(Je(i,t,c)),F(r.routes).length)o.push(` ${n.dim("routes")} ${F(r.routes).map(ie).join(", ")}`);for(let a of r.listeners)o.push(` ${n.green("listen")} ${s(a)}`);for(let a of r.containers)o.push(` ${n.blue("holds")} ${a.container.name}${a.ip?` ${a.ip}`:""}${It(a.container)}`);if(i.members.length)o.push(n.dim(` ${i.members.length} port${i.members.length>1?"s":""}: ${i.members.join(" ")}`).slice(0,t.width??1/0));o.push(...De(r.outbound,"out",t)),o.push(...De(r.local,"local",t)),o.push("")}if(e.idle.length)o.push(n.dim(`down, no address: ${e.idle.join(", ")}`));if(e.dns.length)o.push(`${n.dim("dns")} ${e.dns.map((r)=>`${r.server} ${n.dim(r.route.kind==="loopback"||r.route.kind==="local"?"(local resolver)":se(r.route))}`).join(", ")}`);if(e.hidden)o.push(n.dim(`${e.hidden} processes of other users hidden: run with sudo to see their sockets`));return o.join(`
15
+ `).trimEnd()}function It(e){let t=e.ports.filter((n,o,s)=>s.findIndex((r)=>r.hostPort===n.hostPort&&r.proto===n.proto)===o);return t.length?` ← host ${t.map((n)=>`:${n.hostPort}${n.hostPort!==n.containerPort?`→${n.containerPort}`:""}`).join(" ")}`:""}function De(e,t,n){let o=g(n),s=new Map;for(let i of e){let c=re(i),a=t==="local"?D(i.peer,`:${i.remote.port}`):I(i.remote),d=s.get(c)??new Map;d.set(a,(d.get(a)??0)+1),s.set(c,d)}let r=t==="out"?o.cyan("out"):o.magenta("local");return[...s].map(([i,c])=>{let a=[...c].map(([d,u])=>u>1?`${d} ×${u}`:d).join(", ");return` ${r}${t==="out"?" ":" "}${ne(`${i} → ${a}`,n.width&&n.width-10)}`})}var ce=(e)=>e&&{pid:e.pid,name:e.name,cmd:e.cmd},J=(e)=>e&&{id:e.id,name:e.name,image:e.image,project:e.project};function Rt(e){switch(e.kind){case"local":return{kind:e.kind,process:ce(e.process),user:e.user,listener:e.listener&&{proto:e.listener.proto,port:e.listener.port,container:e.listener.container?.name}};case"container":return{kind:e.kind,container:J(e.container),network:e.network};case"remote":return{kind:e.kind,route:V(e.route)}}}var V=(e)=>({...e,container:J(e.container)}),w=(e)=>({...e,process:ce(e.process),peer:Rt(e.peer)}),G=(e)=>({proto:e.proto,port:e.port,addresses:e.addresses,reach:e.reach,process:ce(e.process),user:e.user,container:J(e.container),published:e.published,clients:e.clients.map(w)}),ae=(e)=>({...e.iface,routes:e.routes,listeners:e.listeners.map((t)=>({proto:t.proto,port:t.port})),outbound:e.outbound.map(w),local:e.local.map(w),containers:e.containers.map((t)=>({...J(t.container),ip:t.ip}))}),qe=(e)=>({...e,everywhere:e.everywhere.map(G),interfaces:e.interfaces.map(ae),dns:e.dns.map((t)=>({server:t.server,route:V(t.route)}))});var Pt={json:["--json","machine-readable output"],tcp:["--tcp","TCP only"],udp:["--udp","UDP only"],all:["-a, --all","also connections that are opening or closing (TIME_WAIT…)"],"no-docker":["--no-docker","don't ask docker which containers publish ports"],"show-secrets":["--show-secrets",`don't mask the values of --token=, --password and the like
16
+ in command lines`],help:["-h, --help","show help: netbeagle COMMAND --help for one command"],version:["-v, --version","show the version"]},U={list:{usage:["[list]"],summary:"every listening port: its process or container, and who can reach it",about:`Lists every port something listens on, with the process (or the docker container)
17
+ behind it, the addresses it is bound to, and who can reach it: this machine only,
18
+ every interface, or the interfaces holding its address. Ports docker publishes
19
+ without a socket of their own are listed too. This is what netbeagle does with no
20
+ command.`,options:["json","tcp","udp","no-docker"],examples:["netbeagle","netbeagle list --tcp --json"]},who:{usage:["who PORT"],summary:"what listens on PORT, who can reach it, who is connected to it",about:`Shows what listens on PORT: the process and its command line, or the container
21
+ and the address inside it the port forwards to; the addresses it is bound to and
22
+ the interfaces that reach it; and every client connected right now, with its
23
+ process when it runs here. Also lists connections from this machine to PORT
24
+ elsewhere. Exits 1 when there are none.`,options:["json","show-secrets","no-docker"],examples:["netbeagle who 5432","netbeagle who 443"]},conns:{usage:["conns"],summary:"every connection, by process, with what is at the other end",about:`Lists the open connections of each process: inbound ones (←) to its listening
25
+ ports and outbound ones (→). The other end is named when it is on this machine
26
+ (its process, or the container behind the port) or in a container; otherwise
27
+ netbeagle shows the interface and gateway the traffic leaves through.`,options:["json","all","tcp","udp","no-docker"],examples:["netbeagle conns","netbeagle conns --all"]},interfaces:{usage:["interfaces"],summary:"every network interface: addresses, routes, bridges, containers",about:`Lists the network interfaces: kind (ethernet, wifi, bridge, veth, tun…), state,
28
+ addresses, the routes going through each, the ports of a bridge and the docker
29
+ containers on it, and the traffic so far.`,options:["json","no-docker"],examples:["netbeagle interfaces","netbeagle if --json"]},route:{usage:["route HOST"],summary:"which interface and gateway traffic to HOST takes",about:`Resolves HOST (a name or an address) and shows where packets to it go, as the
30
+ kernel's main routing table decides: this machine, a container, a network one of
31
+ the interfaces is on, or a gateway; with the route and the source address.`,options:["json","no-docker"],examples:["netbeagle route github.com","netbeagle route 172.18.0.2"]},map:{usage:["map"],summary:"how everything connects, interface by interface",about:`The whole picture: the ports open on every interface; then each interface with
32
+ its routes, the ports bound to it, the containers behind it, and the connections
33
+ leaving through it; on the loopback, which local process talks to which. Ends
34
+ with the DNS servers and the way to them.`,options:["json","no-docker"],examples:["netbeagle map"]}},de={ls:"list",if:"interfaces",ifaces:"interfaces",connections:"conns",graph:"map"},Xe=(e)=>{let t=[...e,"help"].map((o)=>Pt[o]).filter((o)=>!!o),n=Math.max(...t.map(([o])=>o.length));return t.map(([o,s])=>` ${o.padEnd(n)} ${s.replaceAll(`
35
+ `,`
36
+ ${" ".repeat(n)} `)}`)};function ue(){let e=Object.values(U),t=Math.max(...e.map((n)=>(n.usage[0]??"").length));return["Usage: netbeagle [COMMAND] [OPTIONS]","","Commands:",...e.map((n)=>` ${(n.usage[0]??"").padEnd(t)} ${n.summary}`),"","Options:",...Xe(["json","no-docker","show-secrets","version"]),"","netbeagle COMMAND --help shows one command's usage, options and examples.","Without root, sockets of other users' processes show only their user: sudo shows them all.","","From the same pack: envhound, which startup file sets each environment variable."," npx envhound",""].join(`
37
+ `)}function Qe(e){let t=U[de[e]??e];if(!t)return;return[`Usage: ${t.usage.map((n)=>`netbeagle ${n}`).join(`
38
+ `)}`,"",t.about,"","Options:",...Xe(t.options),"","Examples:",...t.examples.map((n)=>` ${n}`),""].join(`
39
+ `)}var pe=["...........tttttttttt...........",".........tttttttttttttt.........","........tttttttccttttttt........","......tttttttttccttttttttt......",".....ttttttttttcctttttttttt.....","...eetttttttttcccctttttttttee...","..eeetttttttttccccttttttttteee..","..eeetttbbbbttccccttbbbbttteee..",".eeeetttttttttccccttttttttteeee.",".eeeettttwddttccccttwddtttteeee.",".eeeettttdddttccccttdddtttteeee.",".eeeettttdddtcccccctdddtttteeee.",".eeeettttttttcccccctttttttteeee.",".eeeettttcccccddddccccctttteeee.",".eeeetttcccccddddddcccccttteeee.",".eeeetttcccccddddddcccccttteeee.",".eeeetttccccccddddccccccttteeee.",".eeeettttccdcccddcccdcctttteeee.","..eeeeettcccdddppdddccctteeeee..","..eeeeeettccccppppcccctteeeeee..","...eeeeeetttccppppccttteeeeee...","....eeTTTTTTttttttttTTTTTTee....",".ffffTTTTTTTffffffffTTTTTTTffff.",".fkkkTbTbTbTkkkkkkkkTbTbTbTkkkf.",".fkkkookkkookkkGGkkkookkkggkkkf.",".fkkkkkkkkkkkkkkkkkkkkkkkkkkkkf.",".fkkooookooooklyylkooooklyylkkf.",".fkkooookooookllllkooookllllkkf.",".fkkkookkkookkkllkkkookkkllkkkf.",".fffffffffffffffllfffffffffllff."],le={t:["e08a3a",173],T:["e08a3a",173],e:["8c4214",94],b:["a8521a",130],c:["fbf1e3",255],d:["2b1d14",234],w:["ffffff",231],p:["f07c8a",210],f:["4a5266",240],k:["0f1218",233],o:["3a4152",238],l:["4fa3e0",74],y:["f3c969",222],g:["7fd18b",114],G:["7fd18b",114]};function Ze(e,t){let n=([c,a],d)=>t.truecolor?`${d};2;${parseInt(c.slice(0,2),16)};${parseInt(c.slice(2,4),16)};${parseInt(c.slice(4),16)}`:`${d};5;${a}`,o=(c)=>c!=="."&&(t.color||!"dwk".includes(c)),s=(c,a)=>{if(!o(c)&&!o(a))return" ";if(!t.color)return o(c)?o(a)?"█":"▀":"▄";if(!o(c))return`\x1B[${n(le[a],38)}m▄\x1B[0m`;let d="G".includes(c)?"5;":"",u=o(a)?`;${n(le[a],48)}`:"";return`\x1B[${d}${n(le[c],38)}${u}m▀\x1B[0m`},r=[];for(let c=0;c*2<pe.length;c++){let a=pe[c*2],d=pe[c*2+1]??".".repeat(a.length),u="";for(let f=0;f<a.length;f++)u+=s(a[f],d[f]);let p=u.replace(/ +$/,"");r.push({out:p,width:a.length-(u.length-p.length)})}let i=Math.floor((r.length-e.length)/2);if((t.columns||80)<38+Math.max(...e.map((c)=>c.length)))return[...r.map((c)=>` ${c.out}`),"",...e.map((c)=>c?` ${c}`:"")].join(`
40
+ `)+`
41
+ `;return r.map((c,a)=>{let d=e[a-i];return d?` ${c.out}${" ".repeat(32-c.width)} ${d}`:` ${c.out}`}).join(`
42
+ `)+`
43
+ `}var Tt=new RegExp("((?:^|\\s)--?[\\w-]*(?:token|secret|passw(?:or)?d|api[-_]?key|private[-_]?key|credential|auth(?![a-z]))[\\w-]*(?:=|\\s+)|[\\w-]*(?:token|secret|passw(?:or)?d|api[-_]?key|private[-_]?key|credential|auth(?![a-z]))[\\w-]*=)(?!-)[^\\s&]+","gi"),et=(e)=>e.replace(Tt,"$1********");var Ot="see what listens on each port, who reaches it, and how everything connects",tt="netbeagle --help";function y(e){return console.error(`netbeagle: ${e}
44
+ see: ${tt}`),2}async function jt(e){let{values:t,positionals:n}=Ct({args:e,allowPositionals:!0,options:{json:{type:"boolean"},tcp:{type:"boolean"},udp:{type:"boolean"},all:{type:"boolean",short:"a"},"no-docker":{type:"boolean"},"show-secrets":{type:"boolean"},help:{type:"boolean",short:"h"},version:{type:"boolean",short:"v"}}}),o=process.stdout.isTTY,s=()=>Ze([`netbeagle ${E.version}`,"","see what listens on each","port, who reaches it, and","how everything connects"],{color:!process.env.NO_COLOR,truecolor:/^(truecolor|24bit)$/i.test(process.env.COLORTERM??""),columns:process.stdout.columns}),r=n[0]==="help",i=(r?n[1]:t.help?n[0]:void 0)??void 0;if(t.help||r){if(i){let l=Qe(i);if(!l)return y(`no command '${i}'`);return process.stdout.write(l),0}return process.stdout.write(o?`${s()}
45
+ ${ue()}`:`netbeagle ${E.version}
46
+ ${Ot}
47
+
48
+ ${ue()}`),0}if(t.version)return process.stdout.write(o?s():`${E.version}
49
+ `),0;let[c="list",...a]=n,d=de[c]??c;if(!U[d])return y(`no command '${c}'`);if(tt=`netbeagle ${d} --help`,process.platform!=="linux")return console.error("netbeagle reads Linux's /proc and /sys, so it runs on Linux and WSL for now; macOS and Windows are planned"),2;let u=process.stdout.isTTY,p=We(t["no-docker"]?{containers:[]}:{});if(!t["show-secrets"])for(let l of p.processes)l.cmd=et(l.cmd);let f={color:u&&!process.env.NO_COLOR,width:u?process.stdout.columns:void 0,wsl:p.environment==="wsl"},m=(l,h)=>console.log(t.json?JSON.stringify(h(),null,2):l()),x=(l)=>t.tcp||t.udp?!!t[l]:!0,T=()=>{let l=Se(p);if(l&&!t.json)console.error(g({...f,color:!!process.stderr.isTTY&&!process.env.NO_COLOR}).dim(`
50
+ ${l} sockets belong to other users' processes: sudo shows which`))};switch(d){case"list":{let l=R(p).filter((h)=>x(h.proto));return m(()=>He(l,f),()=>l.map(G)),T(),0}case"who":{let l=Number(a[0]?.replace(/^:/,""));if(!a[0]||!Number.isInteger(l)||l<1||l>65535)return y("who takes a port number, e.g. netbeagle who 5432");let h=xe(p,l);return m(()=>Ke(h,f),()=>({port:l,listeners:h.listeners.map(G),outbound:h.outbound.map(w)})),h.listeners.length||h.outbound.length?0:1}case"conns":{let l=P(p).filter((h)=>x(h.proto)).filter((h)=>t.all||h.state==="ESTABLISHED");return m(()=>Ue(l,f),()=>l.map(w)),T(),0}case"interfaces":{let l=ee(p);return m(()=>Ge(l.interfaces,l.idle,f),()=>l.interfaces.map(ae)),0}case"route":{let l=a[0];if(!l)return y("route takes a host name or an address, e.g. netbeagle route github.com");let h=l;if(!Et(l))try{h=(await Nt(l)).address}catch{return console.error(`netbeagle: cannot resolve ${l}`),1}let N=j(p,h);return m(()=>Ye(N,l,f),()=>({host:l,...V(N)})),N.kind==="unreachable"?1:0}case"map":{let l=ee(p);return m(()=>ze(l,f),()=>qe(l)),0}}return y(`no command '${c}'`)}jt(process.argv.slice(2)).then((e)=>{process.exitCode=e},(e)=>{if(e instanceof Error&&"code"in e&&String(e.code).startsWith("ERR_PARSE_ARGS"))process.exitCode=y(e.message);else console.error(`netbeagle: ${e instanceof Error?e.message:String(e)}`),process.exitCode=1});
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "netbeagle",
3
+ "version": "0.1.0",
4
+ "description": "See which process listens on each port, which interfaces reach it, and how everything on your machine connects",
5
+ "keywords": [
6
+ "network",
7
+ "ports",
8
+ "netstat",
9
+ "ss",
10
+ "lsof",
11
+ "interfaces",
12
+ "docker",
13
+ "routes"
14
+ ],
15
+ "license": "MIT",
16
+ "author": "Michel Salib",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/michelsalib/netbeagle.git"
20
+ },
21
+ "homepage": "https://github.com/michelsalib/netbeagle#readme",
22
+ "bugs": "https://github.com/michelsalib/netbeagle/issues",
23
+ "type": "module",
24
+ "bin": {
25
+ "netbeagle": "dist/netbeagle.js"
26
+ },
27
+ "files": [
28
+ "dist/netbeagle.js"
29
+ ],
30
+ "engines": {
31
+ "node": ">=20"
32
+ },
33
+ "scripts": {
34
+ "dev": "bun src/cli.ts",
35
+ "build": "bun build src/cli.ts --target=node --minify --outfile dist/netbeagle.js",
36
+ "build:bin": "bun scripts/build-binaries.ts",
37
+ "test": "bun test --timeout 30000",
38
+ "typecheck": "tsc",
39
+ "lint": "biome check",
40
+ "prepublishOnly": "bun run typecheck && bun run lint && bun run build && bun test --timeout 30000"
41
+ },
42
+ "devDependencies": {
43
+ "@biomejs/biome": "2.5.15",
44
+ "@types/bun": "^1.4.2",
45
+ "@types/node": "^26.6.4",
46
+ "typescript": "^7.0.2"
47
+ }
48
+ }