stik-cli 0.1.0 → 0.2.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/README.md +173 -16
- package/bin/stik.js +0 -0
- package/dist/darwin-arm64/stik +0 -0
- package/lib/launch.js +12 -2
- package/package.json +29 -20
- package/bin/note.js +0 -2
- package/dist/darwin-arm64/note +0 -0
package/README.md
CHANGED
|
@@ -1,34 +1,191 @@
|
|
|
1
1
|
# stik
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**A passive watcher that tells you, in plain English, what's on your network — and taps you on the shoulder when something new shows up.**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://github.com/adamsjack711-ux/stik-cli/actions/workflows/ci.yml)
|
|
6
|
+
[](https://go.dev)
|
|
7
|
+
[](LICENSE)
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
+
<p align="center">
|
|
10
|
+
<img src="https://raw.githubusercontent.com/adamsjack711-ux/stik-cli/main/demo/stik.gif" alt="stik noticing a new device join the network and naming it" width="760">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
> **nmap** tells you what's on your network *right now*.
|
|
14
|
+
> **stik** remembers what's *normal*, and tells you when that changes.
|
|
15
|
+
|
|
16
|
+
Every other tool in this space — Wireshark, nmap, tcpdump — hands you **evidence** and leaves you to draw the conclusion. Nobody knows what `a4:83:e7:2f:11:0c` is. Everybody knows what *"Dylan's iPhone"* is. stik exists to produce that second sentence.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
⚠ A new device joined 2 minutes ago.
|
|
20
|
+
Amazon device, first seen 17:34.
|
|
21
|
+
Its network-card maker is Amazon; it hasn't announced a name.
|
|
22
|
+
Run `stik devices` to see it, or `stik name` to label it.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
9
26
|
|
|
10
27
|
## Install
|
|
11
28
|
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
stik is a single Go binary. It uses `libpcap` for capture (bundled on macOS; `libpcap0.8` ships on most Linux desktops).
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# from source (needs Go 1.26+ and libpcap headers) — installs a binary named `stik`
|
|
33
|
+
go install github.com/adamsjack711-ux/stik-cli/cmd/stik@latest
|
|
34
|
+
|
|
35
|
+
# or clone + make
|
|
36
|
+
git clone https://github.com/adamsjack711-ux/stik-cli
|
|
37
|
+
cd stik-cli && make install # builds and installs to /usr/local/bin
|
|
14
38
|
```
|
|
15
39
|
|
|
16
|
-
|
|
40
|
+
On Debian/Ubuntu, building from source needs the pcap headers: `sudo apt install libpcap-dev`.
|
|
17
41
|
|
|
18
|
-
|
|
42
|
+
Packet capture requires elevated privileges. On macOS you can grant your user access to the BPF devices once (Wireshark's *ChmodBPF* helper does this) instead of running as root; otherwise run stik with `sudo`.
|
|
19
43
|
|
|
20
|
-
|
|
21
|
-
note add "ship v0.1" --tag release --due "in 2d" --color blue
|
|
22
|
-
note ls
|
|
23
|
-
note show <id>
|
|
44
|
+
---
|
|
24
45
|
|
|
25
|
-
|
|
46
|
+
## 30-second quickstart
|
|
47
|
+
|
|
48
|
+
**First run walks you through your network, one device at a time.** This is the whole idea: it builds the baseline of "normal", and it gets you to actually *look* at what's connected — usually for the first time.
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
$ stik
|
|
52
|
+
|
|
53
|
+
👋 Welcome to stik. Let's learn what's on your network.
|
|
54
|
+
Listening passively for 10s — stik only ever listens, it never sends traffic.
|
|
55
|
+
|
|
56
|
+
Found 8 devices on your network. Let's figure out what they are.
|
|
57
|
+
|
|
58
|
+
1/8 Apple iPhone — "Dylans-iPhone"
|
|
59
|
+
Its network-card maker is Apple.
|
|
60
|
+
Is this yours? [Y/n/skip] y
|
|
61
|
+
name it: my phone
|
|
62
|
+
✓ saved as "my phone"
|
|
63
|
+
|
|
64
|
+
2/8 Amazon device — no hostname
|
|
65
|
+
Its network-card maker is Amazon; it hasn't announced a name.
|
|
66
|
+
Is this yours? [Y/n/skip] y
|
|
67
|
+
name it: kitchen echo
|
|
68
|
+
✓ saved as "kitchen echo"
|
|
69
|
+
...
|
|
26
70
|
```
|
|
27
71
|
|
|
28
|
-
|
|
72
|
+
After that, `stik` is a one-line glance — and usually boring. **Boring is the feature.**
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
$ stik
|
|
76
|
+
✓ Everything looks normal. 8 known devices.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Leave the watcher running in the background, and you get a **desktop notification** the moment something unrecognized joins:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
stik daemon
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Because nobody watches a TUI all day. The alert comes to you.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Commands
|
|
90
|
+
|
|
91
|
+
| Command | What it does |
|
|
92
|
+
|---|---|
|
|
93
|
+
| `stik` | Status: is anything new? (runs the setup wizard on first use) |
|
|
94
|
+
| `stik devices` | List every device in plain terms (`--verbose` for MACs & details) |
|
|
95
|
+
| `stik watch` | Live view; new devices highlight as they appear |
|
|
96
|
+
| `stik daemon` | Background watcher; fires a desktop notification on a new device |
|
|
97
|
+
| `stik name <who>` | Name a device — match by name, hostname, IP, or MAC |
|
|
98
|
+
| `stik forget <who>` | Remove a device from the registry |
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
$ stik devices
|
|
102
|
+
Known (4)
|
|
103
|
+
• my phone (Apple iPhone)
|
|
104
|
+
last seen 2 minutes ago · dylans-iphone
|
|
105
|
+
• living room TV (Apple TV)
|
|
106
|
+
last seen just now · apple-tv
|
|
107
|
+
• work laptop (Apple MacBook)
|
|
108
|
+
last seen 5 minutes ago · dylans-macbook
|
|
109
|
+
• the router (TP-Link device)
|
|
110
|
+
last seen just now
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## What stik can — and can't — see
|
|
116
|
+
|
|
117
|
+
stik is deliberately narrow, and says so up front:
|
|
29
118
|
|
|
30
|
-
|
|
119
|
+
- **Passive only.** It *listens*; it never transmits. No ARP scanning, no ARP spoofing, no port scanning. This is a hard line.
|
|
120
|
+
- **Broadcast/multicast only.** On a switched network you physically cannot see other devices' unicast traffic — the switch doesn't forward it to your port. stik reads only the protocols every device on the LAN legitimately broadcasts: **ARP**, **mDNS** (5353), and **DHCP** (67/68).
|
|
121
|
+
- **For networks you own.** Point it at your own home or lab network.
|
|
122
|
+
|
|
123
|
+
That narrowness is the honest shape of the problem, not a limitation stik is hiding. See [Design notes](#design-notes) for why.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## How stik identifies a device
|
|
128
|
+
|
|
129
|
+
Three broadcast protocols, combined into one sentence:
|
|
130
|
+
|
|
131
|
+
- **ARP** → the MAC↔IP pairing. The ground truth of who's on the wire.
|
|
132
|
+
- **mDNS** → hostnames. Apple and many IoT devices announce themselves constantly (`Dylans-iPhone.local`) — free, high-quality identity.
|
|
133
|
+
- **DHCP** → the *set and order* of requested options is a fingerprint that often reveals the OS even when nothing else does, plus a vendor-class string like `android-dhcp-14`.
|
|
134
|
+
- **OUI** → the first three bytes of the MAC map to a manufacturer, via the **embedded IEEE registry** (no network fetch — stik works on a network it doesn't trust).
|
|
135
|
+
|
|
136
|
+
Then it writes the verdict: *"Apple iPhone"*, *"Amazon device"*, *"unknown device (Espressif — likely IoT)"*, or — when a phone is using a randomized address — *"device with a private address"*.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Design notes
|
|
141
|
+
|
|
142
|
+
The interesting decisions, and why they went the way they did.
|
|
143
|
+
|
|
144
|
+
### Verdicts, not packets
|
|
145
|
+
|
|
146
|
+
The prime directive: **output the conclusion, not the evidence.** A tool that prints `unrecognized OUI a4:83:e7` has made the human do the work. stik prints *"a device we don't recognize."* Raw MACs, IPs, and DHCP fingerprints exist, but they live behind `--verbose`. If a design choice makes the output more technically complete but less humanly legible, it's the wrong choice.
|
|
147
|
+
|
|
148
|
+
### Passive, and broadcast-only — the limit stik refuses to cross
|
|
149
|
+
|
|
150
|
+
On a switched network, one host cannot see another host's unicast traffic; the switch simply doesn't deliver it. The only ways around that are **port mirroring** (needs switch access you usually don't have) or **ARP spoofing** — telling every device you're the router so their traffic flows through you. Spoofing is an *attack* technique. stik will not do it.
|
|
151
|
+
|
|
152
|
+
So stik confines itself to what any device on the LAN can legitimately hear: broadcast and multicast. That's a real limit, and stik states it plainly rather than overclaiming. Explaining the boundary you *chose not to cross* is the honest way to build a tool like this.
|
|
153
|
+
|
|
154
|
+
### Why DHCP option ordering fingerprints an OS
|
|
155
|
+
|
|
156
|
+
When a device joins a network it sends a DHCP request containing a **Parameter Request List** (option 55): the specific options it wants, *in a specific order*. That order is baked into each OS's DHCP client and barely changes between versions — so `1,3,6,15,26,28,51,58,59,43` says "Android" and `1,121,3,6,15,119,252,95,44,46` says "Apple". stik preserves the order exactly, because the order is the signal. (Option 60, the vendor class, is an even more direct hint when a device sends one.)
|
|
157
|
+
|
|
158
|
+
### Randomized MACs, and why naive vendor lookup lies
|
|
159
|
+
|
|
160
|
+
Modern phones rotate their MAC address per network to resist tracking. A randomized MAC has an invented prefix, so an OUI lookup will either fail or — worse — *coincidentally match some real vendor and confidently report the wrong thing.* stik checks the **locally-administered bit** (`0x02` of the first octet) first. If it's set, stik doesn't trust the OUI at all and says *"device with a private address"* — unless mDNS gave a real name, in which case the name wins. Getting this right is the difference between a demo and a tool.
|
|
161
|
+
|
|
162
|
+
### One storage module
|
|
163
|
+
|
|
164
|
+
Everything stik persists lives in a single JSON file (`~/.stik/devices.json`), and exactly one package (`internal/store`) ever touches it. Writes are **atomic** — a sibling temp file is written and then `rename`d over the target — so a crash or a second process can never leave a half-written, unparseable registry behind. A corrupt file is backed up and the registry starts fresh rather than wedging the tool. The store's interface is tiny on purpose: a SQLite backend could replace it without a single command changing.
|
|
165
|
+
|
|
166
|
+
### Embedded OUI table, no runtime fetch
|
|
167
|
+
|
|
168
|
+
The IEEE manufacturer database (~40,000 prefixes) is compressed and embedded in the binary at build time. stik never phones home to identify a device — which matters, because you might be pointing it at a network you don't trust.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Development
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
make build # build ./stik
|
|
176
|
+
make test # go test ./...
|
|
177
|
+
make vet # go vet
|
|
178
|
+
make oui # regenerate the embedded IEEE OUI table
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The dissectors are tested against **real serialized packet bytes** (ARP, mDNS, DHCP) rather than mocks — the same frames libpcap would hand them. Lane-free logic like OUI lookup, randomized-MAC detection, known-vs-new, atomic writes, and corrupt-store recovery all have focused unit tests.
|
|
182
|
+
|
|
183
|
+
Record the demo GIF with [vhs](https://github.com/charmbracelet/vhs):
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
bash demo/record.sh
|
|
187
|
+
```
|
|
31
188
|
|
|
32
189
|
## License
|
|
33
190
|
|
|
34
|
-
MIT
|
|
191
|
+
MIT — see [LICENSE](LICENSE). Built for networks you own.
|
package/bin/stik.js
CHANGED
|
File without changes
|
package/dist/darwin-arm64/stik
CHANGED
|
Binary file
|
package/lib/launch.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Thin Node shim: locate the bundled Go binary for this platform (or a
|
|
4
|
+
// STIK_BIN override) and exec it, passing through argv, stdio, and exit code.
|
|
5
|
+
// The npm package is just a delivery vehicle for the compiled `stik` binary.
|
|
6
|
+
|
|
3
7
|
const { spawnSync } = require('child_process');
|
|
4
8
|
const path = require('path');
|
|
5
9
|
const fs = require('fs');
|
|
@@ -30,10 +34,16 @@ function launch(name) {
|
|
|
30
34
|
`${name}: no prebuilt binary for ${process.platform}-${process.arch}.`
|
|
31
35
|
);
|
|
32
36
|
console.error(
|
|
33
|
-
'stik currently ships macOS arm64
|
|
37
|
+
'stik currently ships a macOS arm64 binary. On other platforms, build from'
|
|
38
|
+
);
|
|
39
|
+
console.error(
|
|
40
|
+
'source (needs Go 1.26+ and libpcap):'
|
|
41
|
+
);
|
|
42
|
+
console.error(
|
|
43
|
+
' go install github.com/adamsjack711-ux/stik-cli/cmd/stik@latest'
|
|
34
44
|
);
|
|
35
45
|
console.error(
|
|
36
|
-
`
|
|
46
|
+
`then point ${name.toUpperCase()}_BIN at the resulting binary.`
|
|
37
47
|
);
|
|
38
48
|
process.exit(1);
|
|
39
49
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stik-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Passive network watcher — tells you in plain English what's on your network, and taps you on the shoulder when something new shows up.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"network",
|
|
7
|
+
"security",
|
|
8
|
+
"arp",
|
|
9
|
+
"mdns",
|
|
10
|
+
"dhcp",
|
|
11
|
+
"passive",
|
|
12
|
+
"monitoring",
|
|
13
|
+
"devices",
|
|
14
|
+
"lan",
|
|
15
|
+
"cli"
|
|
16
|
+
],
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"author": "Jack Adams-Lovell",
|
|
19
|
+
"homepage": "https://github.com/adamsjack711-ux/stik-cli#readme",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/adamsjack711-ux/stik-cli.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/adamsjack711-ux/stik-cli/issues"
|
|
26
|
+
},
|
|
5
27
|
"bin": {
|
|
6
|
-
"stik": "bin/stik.js"
|
|
7
|
-
"note": "bin/note.js"
|
|
28
|
+
"stik": "bin/stik.js"
|
|
8
29
|
},
|
|
9
30
|
"files": [
|
|
10
31
|
"bin",
|
|
@@ -20,21 +41,9 @@
|
|
|
20
41
|
"arm64"
|
|
21
42
|
],
|
|
22
43
|
"engines": {
|
|
23
|
-
"node": ">=
|
|
44
|
+
"node": ">=16"
|
|
24
45
|
},
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
},
|
|
29
|
-
"keywords": [
|
|
30
|
-
"sticky-notes",
|
|
31
|
-
"notes",
|
|
32
|
-
"cli",
|
|
33
|
-
"ideas",
|
|
34
|
-
"capture",
|
|
35
|
-
"sqlite",
|
|
36
|
-
"terminal"
|
|
37
|
-
],
|
|
38
|
-
"author": "Jack Adams-Lovell",
|
|
39
|
-
"license": "MIT"
|
|
46
|
+
"scripts": {
|
|
47
|
+
"build": "./build.sh"
|
|
48
|
+
}
|
|
40
49
|
}
|
package/bin/note.js
DELETED
package/dist/darwin-arm64/note
DELETED
|
Binary file
|