devbits 1.3.3__tar.gz → 1.4.0__tar.gz

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.
Files changed (29) hide show
  1. {devbits-1.3.3 → devbits-1.4.0}/PKG-INFO +66 -1
  2. {devbits-1.3.3 → devbits-1.4.0}/README.md +65 -0
  3. {devbits-1.3.3 → devbits-1.4.0}/devbits/__init__.py +1 -1
  4. {devbits-1.3.3 → devbits-1.4.0}/devbits/cli.py +135 -2
  5. devbits-1.4.0/devbits/network.py +967 -0
  6. {devbits-1.3.3 → devbits-1.4.0}/devbits/scripts.py +4 -0
  7. {devbits-1.3.3 → devbits-1.4.0}/devbits.egg-info/PKG-INFO +66 -1
  8. {devbits-1.3.3 → devbits-1.4.0}/devbits.egg-info/SOURCES.txt +1 -0
  9. {devbits-1.3.3 → devbits-1.4.0}/devbits.egg-info/entry_points.txt +1 -0
  10. {devbits-1.3.3 → devbits-1.4.0}/pyproject.toml +2 -1
  11. devbits-1.4.0/tests/test_netsurvey.py +346 -0
  12. devbits-1.3.3/devbits/network.py +0 -311
  13. {devbits-1.3.3 → devbits-1.4.0}/LICENSE +0 -0
  14. {devbits-1.3.3 → devbits-1.4.0}/devbits/cache.py +0 -0
  15. {devbits-1.3.3 → devbits-1.4.0}/devbits/gui.py +0 -0
  16. {devbits-1.3.3 → devbits-1.4.0}/devbits/image.py +0 -0
  17. {devbits-1.3.3 → devbits-1.4.0}/devbits/media.py +0 -0
  18. {devbits-1.3.3 → devbits-1.4.0}/devbits/project.py +0 -0
  19. {devbits-1.3.3 → devbits-1.4.0}/devbits/tui.py +0 -0
  20. {devbits-1.3.3 → devbits-1.4.0}/devbits/utils.py +0 -0
  21. {devbits-1.3.3 → devbits-1.4.0}/devbits/wifi.py +0 -0
  22. {devbits-1.3.3 → devbits-1.4.0}/devbits.egg-info/dependency_links.txt +0 -0
  23. {devbits-1.3.3 → devbits-1.4.0}/devbits.egg-info/requires.txt +0 -0
  24. {devbits-1.3.3 → devbits-1.4.0}/devbits.egg-info/top_level.txt +0 -0
  25. {devbits-1.3.3 → devbits-1.4.0}/setup.cfg +0 -0
  26. {devbits-1.3.3 → devbits-1.4.0}/tests/test_cli.py +0 -0
  27. {devbits-1.3.3 → devbits-1.4.0}/tests/test_gui_cli.py +0 -0
  28. {devbits-1.3.3 → devbits-1.4.0}/tests/test_tui.py +0 -0
  29. {devbits-1.3.3 → devbits-1.4.0}/tests/test_wifi.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devbits
3
- Version: 1.3.3
3
+ Version: 1.4.0
4
4
  Summary: A lightweight CLI toolkit for daily development utilities.
5
5
  Author: Bruce Chuang
6
6
  License-Expression: MIT
@@ -85,6 +85,7 @@ clipvideo --help
85
85
  | Command | Description |
86
86
  |---------|-------------|
87
87
  | `netscan` | List devices connected to your local network (Wi-Fi / router) with their IP, MAC, and hostname. `--lookup` adds the manufacturer. |
88
+ | `netsurvey` | Map **every network segment** visible on the current link — your own interfaces, foreign subnets on the same switch, the addresses in use, and the conflicts between them. Built for shared networks (trade shows, offices). |
88
89
  | `wifi` | Manage Wi-Fi: `list`, `connect` (arrow-key picker + hidden password prompt), `on`, `off`, `forget`. Linux and Windows fully; macOS without `list`. |
89
90
 
90
91
  ## Examples
@@ -124,6 +125,18 @@ netscan --lookup
124
125
  # Scan a specific subnet, faster, without hostname lookups
125
126
  netscan --network 192.168.1.0/24 --timeout 0.5 --no-resolve
126
127
 
128
+ # Map every network segment and address around you (passive, ~2s)
129
+ netsurvey
130
+
131
+ # Segments and conflicts only, no address lists
132
+ netsurvey --summary
133
+
134
+ # Also ping-sweep your ranges and the discovered ones
135
+ netsurvey --sweep
136
+
137
+ # Check specific ranges before assigning them at a venue
138
+ netsurvey --include 192.168.1.0/24 --include 10.0.0.0/24
139
+
127
140
  # Show the Wi-Fi networks in range (Linux / Windows)
128
141
  wifi list
129
142
 
@@ -146,6 +159,58 @@ wifi forget OldCafe
146
159
  > laptops that use a randomized/private MAC show up as `(private)` and can't be
147
160
  > attributed to a vendor.
148
161
 
162
+ ### Surveying a shared network
163
+
164
+ `netscan` answers "who is on **my** subnet?". `netsurvey` answers "what is on
165
+ this **wire**?" — the question that matters in an exhibition hall, an office, or
166
+ a co-working space where other people's routers hand out their own ranges:
167
+
168
+ ```
169
+ INTERFACES
170
+ en0 192.168.0.122/24 gateway 192.168.0.1
171
+ utun6 10.2.244.44/32
172
+
173
+ SEGMENTS (3 total, 2 yours, 9 address(es) in use)
174
+ 192.168.0.0/24 local (en0) swept 6 host(s)
175
+ 192.168.0.1 06:f2:67:75:4d:e2 gateway,arp,ping gateway / router
176
+ 192.168.0.122 4e:e5:41:93:7c:1f self,arp,mdns this device
177
+ ...
178
+ 10.77.0.0/24 foreign passive 2 host(s)
179
+ 10.77.0.9 de:ad:be:ef:00:01 ssdp,arp
180
+
181
+ CONFLICTS (2)
182
+ ! [duplicate-ip] 192.168.0.50 answers from 2 MAC addresses (…) — two devices claim the same address
183
+ - [shared-l2] 1 foreign segment(s) share this link: 10.77.0.0/24
184
+ ```
185
+
186
+ Three independent sources are combined, so segments you have no address in still
187
+ show up:
188
+
189
+ - **every local interface** with its real netmask (`ip` / `ifconfig` / `ipconfig`);
190
+ - the **ARP / neighbour cache**, which lists link neighbours *regardless of
191
+ subnet* — this is what exposes somebody else's range on the same switch;
192
+ - **SSDP and mDNS** answers, sent out of each interface (skip with `--no-passive`).
193
+
194
+ The default pass is passive and takes about two seconds. `--sweep` additionally
195
+ ping-sweeps your ranges and the discovered ones so each address list is complete;
196
+ `--include CIDR` sweeps a specific range you are about to assign. Ranges larger
197
+ than `--max-sweep` (4096 hosts) are narrowed to the /24 around your own address
198
+ or skipped, with a note on stderr.
199
+
200
+ Reported conflicts:
201
+
202
+ | Kind | Meaning |
203
+ |------|---------|
204
+ | `duplicate-ip` | One address answers from two MACs — two devices claim it. |
205
+ | `overlap` | A foreign range, or a second local interface (VPN!), overlaps one of your subnets. |
206
+ | `no-dhcp` | An interface self-assigned a `169.254.x.x` address; no DHCP server answered. |
207
+ | `router-bridge` | One MAC answers for addresses in several segments — a router joining them. |
208
+ | `shared-l2` | Foreign segments are present on your link (informational). |
209
+
210
+ > Survey only networks you are entitled to. `netsurvey` sends ordinary pings and
211
+ > standard discovery multicast — nothing privileged — but probing networks you
212
+ > don't administer may still violate policy or law.
213
+
149
214
  ### Wi-Fi
150
215
 
151
216
  `wifi connect` lists everything in range — move with ↑/↓, press Enter to join,
@@ -68,6 +68,7 @@ clipvideo --help
68
68
  | Command | Description |
69
69
  |---------|-------------|
70
70
  | `netscan` | List devices connected to your local network (Wi-Fi / router) with their IP, MAC, and hostname. `--lookup` adds the manufacturer. |
71
+ | `netsurvey` | Map **every network segment** visible on the current link — your own interfaces, foreign subnets on the same switch, the addresses in use, and the conflicts between them. Built for shared networks (trade shows, offices). |
71
72
  | `wifi` | Manage Wi-Fi: `list`, `connect` (arrow-key picker + hidden password prompt), `on`, `off`, `forget`. Linux and Windows fully; macOS without `list`. |
72
73
 
73
74
  ## Examples
@@ -107,6 +108,18 @@ netscan --lookup
107
108
  # Scan a specific subnet, faster, without hostname lookups
108
109
  netscan --network 192.168.1.0/24 --timeout 0.5 --no-resolve
109
110
 
111
+ # Map every network segment and address around you (passive, ~2s)
112
+ netsurvey
113
+
114
+ # Segments and conflicts only, no address lists
115
+ netsurvey --summary
116
+
117
+ # Also ping-sweep your ranges and the discovered ones
118
+ netsurvey --sweep
119
+
120
+ # Check specific ranges before assigning them at a venue
121
+ netsurvey --include 192.168.1.0/24 --include 10.0.0.0/24
122
+
110
123
  # Show the Wi-Fi networks in range (Linux / Windows)
111
124
  wifi list
112
125
 
@@ -129,6 +142,58 @@ wifi forget OldCafe
129
142
  > laptops that use a randomized/private MAC show up as `(private)` and can't be
130
143
  > attributed to a vendor.
131
144
 
145
+ ### Surveying a shared network
146
+
147
+ `netscan` answers "who is on **my** subnet?". `netsurvey` answers "what is on
148
+ this **wire**?" — the question that matters in an exhibition hall, an office, or
149
+ a co-working space where other people's routers hand out their own ranges:
150
+
151
+ ```
152
+ INTERFACES
153
+ en0 192.168.0.122/24 gateway 192.168.0.1
154
+ utun6 10.2.244.44/32
155
+
156
+ SEGMENTS (3 total, 2 yours, 9 address(es) in use)
157
+ 192.168.0.0/24 local (en0) swept 6 host(s)
158
+ 192.168.0.1 06:f2:67:75:4d:e2 gateway,arp,ping gateway / router
159
+ 192.168.0.122 4e:e5:41:93:7c:1f self,arp,mdns this device
160
+ ...
161
+ 10.77.0.0/24 foreign passive 2 host(s)
162
+ 10.77.0.9 de:ad:be:ef:00:01 ssdp,arp
163
+
164
+ CONFLICTS (2)
165
+ ! [duplicate-ip] 192.168.0.50 answers from 2 MAC addresses (…) — two devices claim the same address
166
+ - [shared-l2] 1 foreign segment(s) share this link: 10.77.0.0/24
167
+ ```
168
+
169
+ Three independent sources are combined, so segments you have no address in still
170
+ show up:
171
+
172
+ - **every local interface** with its real netmask (`ip` / `ifconfig` / `ipconfig`);
173
+ - the **ARP / neighbour cache**, which lists link neighbours *regardless of
174
+ subnet* — this is what exposes somebody else's range on the same switch;
175
+ - **SSDP and mDNS** answers, sent out of each interface (skip with `--no-passive`).
176
+
177
+ The default pass is passive and takes about two seconds. `--sweep` additionally
178
+ ping-sweeps your ranges and the discovered ones so each address list is complete;
179
+ `--include CIDR` sweeps a specific range you are about to assign. Ranges larger
180
+ than `--max-sweep` (4096 hosts) are narrowed to the /24 around your own address
181
+ or skipped, with a note on stderr.
182
+
183
+ Reported conflicts:
184
+
185
+ | Kind | Meaning |
186
+ |------|---------|
187
+ | `duplicate-ip` | One address answers from two MACs — two devices claim it. |
188
+ | `overlap` | A foreign range, or a second local interface (VPN!), overlaps one of your subnets. |
189
+ | `no-dhcp` | An interface self-assigned a `169.254.x.x` address; no DHCP server answered. |
190
+ | `router-bridge` | One MAC answers for addresses in several segments — a router joining them. |
191
+ | `shared-l2` | Foreign segments are present on your link (informational). |
192
+
193
+ > Survey only networks you are entitled to. `netsurvey` sends ordinary pings and
194
+ > standard discovery multicast — nothing privileged — but probing networks you
195
+ > don't administer may still violate policy or law.
196
+
132
197
  ### Wi-Fi
133
198
 
134
199
  `wifi connect` lists everything in range — move with ↑/↓, press Enter to join,
@@ -1,3 +1,3 @@
1
1
  """devbits: A lightweight CLI toolkit for daily development utilities."""
2
2
 
3
- __version__ = "1.3.3"
3
+ __version__ = "1.4.0"
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import argparse
4
+ import ipaddress
4
5
  import os
5
6
  import shutil
6
7
  import sys
@@ -33,6 +34,9 @@ _ANSI = {
33
34
  "signal": "\033[32m",
34
35
  "lock": "\033[33m",
35
36
  "dim": "\033[2m",
37
+ "segment": "\033[1;36m",
38
+ "warn": "\033[1;31m",
39
+ "note": "\033[2m",
36
40
  "reset": "\033[0m",
37
41
  }
38
42
 
@@ -491,6 +495,58 @@ def build_parser() -> argparse.ArgumentParser:
491
495
  help="Disable colored output (also honors NO_COLOR).")
492
496
  p.set_defaults(func=cmd_netscan)
493
497
 
498
+ # ── netsurvey ──────────────────────────────────────────────
499
+ p = sub.add_parser(
500
+ "netsurvey",
501
+ help="Map every network segment and address in use around you.",
502
+ formatter_class=argparse.RawDescriptionHelpFormatter,
503
+ description=(
504
+ "Survey the whole environment instead of a single subnet: list your own\n"
505
+ "interfaces and their ranges, every segment visible on the same wire, the\n"
506
+ "addresses in use in each, and the clashes between them.\n\n"
507
+ "Built for shared networks — a trade-show hall, an office, a co-working\n"
508
+ "space — where someone else's router hands out an overlapping range and\n"
509
+ "traffic quietly goes to the wrong place. Reports duplicate IPs, ranges\n"
510
+ "that overlap your own, links with no DHCP, and routers bridging two\n"
511
+ "segments.\n\n"
512
+ "The default pass is passive and takes a couple of seconds: it reads the\n"
513
+ "ARP / neighbour cache (which lists neighbours on *foreign* subnets too)\n"
514
+ "and asks for SSDP and mDNS answers. --sweep additionally ping-sweeps\n"
515
+ "your ranges and the discovered ones so each address list is complete.\n\n"
516
+ "Survey only networks you are entitled to; probing networks you don't\n"
517
+ "administer may violate policy or law.\n\n"
518
+ "Examples:\n"
519
+ " devbits netsurvey # quick passive map\n"
520
+ " devbits netsurvey --summary # segments only\n"
521
+ " devbits netsurvey --sweep # also enumerate every host\n"
522
+ " devbits netsurvey --include 192.168.1.0/24 --include 10.0.0.0/24\n"
523
+ " devbits netsurvey --sweep --group-prefix 16 --timeout 0.4"
524
+ ),
525
+ )
526
+ p.add_argument("--sweep", action="store_true",
527
+ help="Also ping-sweep your own and the discovered segments (slower, complete).")
528
+ p.add_argument("--include", metavar="CIDR", action="append", default=[],
529
+ help="Extra range to sweep, e.g. 10.0.0.0/24. Repeatable.")
530
+ p.add_argument("--group-prefix", type=int, default=24, metavar="N",
531
+ help="Prefix used to group addresses outside your own subnets. Default: 24")
532
+ p.add_argument("--timeout", type=float, default=0.6,
533
+ help="Per-host ping timeout in seconds. Default: 0.6")
534
+ p.add_argument("--workers", type=int, default=128,
535
+ help="Number of concurrent ping workers. Default: 128")
536
+ p.add_argument("--discover-timeout", type=float, default=2.0, metavar="SECONDS",
537
+ help="How long to listen for SSDP / mDNS answers. Default: 2.0")
538
+ p.add_argument("--no-passive", action="store_true",
539
+ help="Skip the multicast discovery step; read the ARP cache only.")
540
+ p.add_argument("--resolve", action="store_true",
541
+ help="Reverse-DNS each address for its hostname (slower).")
542
+ p.add_argument("--max-sweep", type=int, default=4096, metavar="HOSTS",
543
+ help="Refuse to sweep a range larger than this. Default: 4096")
544
+ p.add_argument("--summary", action="store_true",
545
+ help="List segments and conflicts only, without the addresses.")
546
+ p.add_argument("--no-color", action="store_true",
547
+ help="Disable colored output (also honors NO_COLOR).")
548
+ p.set_defaults(func=cmd_netsurvey)
549
+
494
550
  # ── wifi ───────────────────────────────────────────────────
495
551
  p = sub.add_parser(
496
552
  "wifi",
@@ -741,8 +797,6 @@ def cmd_samplefiles(args: argparse.Namespace) -> None:
741
797
 
742
798
 
743
799
  def cmd_netscan(args: argparse.Namespace) -> None:
744
- import ipaddress
745
-
746
800
  color = _use_color(False if args.no_color else None)
747
801
  network = ipaddress.ip_network(args.network, strict=False) if args.network else None
748
802
 
@@ -771,6 +825,85 @@ def cmd_netscan(args: argparse.Namespace) -> None:
771
825
  print(f"Found {len(devices)} device(s).")
772
826
 
773
827
 
828
+ def _survey_source_label(sources: set[str], width: int = 22) -> str:
829
+ """Condense a device's discovery sources into a stable, readable column."""
830
+ order = ["self", "gateway", "arp", "ping", "ssdp", "mdns"]
831
+ label = ",".join(source for source in order if source in sources) or "-"
832
+ if len(label) > width - 1:
833
+ label = label[: width - 2] + "…"
834
+ return f"{label:<{width}}"
835
+
836
+
837
+ def cmd_netsurvey(args: argparse.Namespace) -> None:
838
+ from .network import survey_network
839
+
840
+ color = _use_color(False if args.no_color else None)
841
+ if not 8 <= args.group_prefix <= 32:
842
+ raise ValueError("--group-prefix must be between 8 and 32")
843
+
844
+ survey, notes = survey_network(
845
+ include=args.include,
846
+ group_prefix=args.group_prefix,
847
+ sweep=args.sweep,
848
+ passive=not args.no_passive,
849
+ timeout=args.timeout,
850
+ workers=args.workers,
851
+ discover_timeout=args.discover_timeout,
852
+ resolve=args.resolve,
853
+ max_sweep_hosts=args.max_sweep,
854
+ progress=lambda message: print(message, file=sys.stderr),
855
+ )
856
+
857
+ print(_colorize("INTERFACES", "header", color))
858
+ for nic in survey.interfaces:
859
+ note = "gateway " + survey.gateway if survey.gateway and \
860
+ ipaddress.ip_address(survey.gateway) in nic.network else ""
861
+ if nic.is_link_local:
862
+ note = "self-assigned (no DHCP)"
863
+ name_col = _colorize(f"{nic.name:<12}", "self" if nic.ip == survey.self_ip else "dim", color)
864
+ print(f" {name_col}{f'{nic.ip}/{nic.prefix}':<22}{_colorize(note, 'note', color)}".rstrip())
865
+
866
+ print()
867
+ local_count = sum(1 for segment in survey.segments if segment.is_local)
868
+ print(_colorize(
869
+ f"SEGMENTS ({len(survey.segments)} total, {local_count} yours, "
870
+ f"{survey.device_count} address(es) in use)", "header", color,
871
+ ))
872
+ for segment in survey.segments:
873
+ scope = f"local ({', '.join(segment.interfaces)})" if segment.is_local else "foreign"
874
+ how = "swept" if segment.swept else "passive"
875
+ head = f" {str(segment.network):<20}{scope:<24}{how:<10}{len(segment.devices)} host(s)"
876
+ print(_colorize(head.rstrip(), "segment" if segment.is_local else "warn", color))
877
+ if args.summary:
878
+ continue
879
+ for device in segment.devices:
880
+ note = "this device" if device.is_self else ("gateway / router" if device.is_gateway else "")
881
+ kind = "self" if device.is_self else ("gateway" if device.is_gateway else None)
882
+ ip_col = _colorize(f"{device.ip:<16}", kind, color) if kind else f"{device.ip:<16}"
883
+ mac_col = _colorize(f"{device.mac or '-':<20}", "mac", color)
884
+ host_col = _colorize(f"{device.hostname or '-':<28}", "host", color) if args.resolve else ""
885
+ seen_col = _colorize(_survey_source_label(device.sources), "note", color)
886
+ print(f" {ip_col}{mac_col}{host_col}{seen_col}{note}".rstrip())
887
+
888
+ print()
889
+ if survey.conflicts:
890
+ print(_colorize(f"CONFLICTS ({len(survey.conflicts)})", "header", color))
891
+ for conflict in survey.conflicts:
892
+ marker = "!" if conflict.kind != "shared-l2" else "-"
893
+ kind = "warn" if conflict.kind != "shared-l2" else "note"
894
+ print(_colorize(f" {marker} [{conflict.kind}] {conflict.message}", kind, color))
895
+ else:
896
+ print("No address or range conflicts detected.")
897
+
898
+ for note in notes:
899
+ print(_colorize(f" note: {note}", "note", color), file=sys.stderr)
900
+ if not args.sweep and not args.include:
901
+ print(_colorize(
902
+ "Passive pass only — run with --sweep to enumerate every address in each segment.",
903
+ "note", color,
904
+ ), file=sys.stderr)
905
+
906
+
774
907
  # ---------------------------------------------------------------------------
775
908
  # wifi
776
909
  # ---------------------------------------------------------------------------