netsweep 1.0.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) 2025 Johannes Berggren
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,122 @@
1
+ # netprobe
2
+
3
+ Network Swiss Army Knife - A single command for comprehensive network diagnostics.
4
+
5
+ ```
6
+ ╭──────────────────────────────────╮
7
+ │ NETPROBE - Network Diagnostics │
8
+ ╰──────────────────────────────────╯
9
+ ┌─ CONNECTION ────────────────────────────────────────────────┐
10
+ │ Interface: Wi-Fi (en0) │
11
+ │ Local IP: 192.168.0.31 │
12
+ │ Gateway: 192.168.0.1 │
13
+ │ External IP: 85.123.45.67 │
14
+ │ DNS: 1.1.1.1, 8.8.8.8 │
15
+ └──────────────────────────────────────────────────────────────┘
16
+ ┌─ SPEED ─────────────────────────────────────────────────────┐
17
+ │ ↓ Download: 245.3 Mbps │
18
+ │ ↑ Upload: 48.7 Mbps │
19
+ │ Latency: 12ms (jitter: 2.3ms) │
20
+ └──────────────────────────────────────────────────────────────┘
21
+ ┌─ DEVICES (4 found) ─────────────────────────────────────────┐
22
+ │ IP │ MAC │ Vendor │ Name │
23
+ ├─────────────────┼────────────────────┼──────────────┼────────┤
24
+ │ 192.168.0.1 │ F0:81:75:22:32:22 │ Sagemcom │ - │
25
+ │ 192.168.0.7 │ 00:17:88:2E:09:31 │ Philips │ - │
26
+ │ 192.168.0.31 │ 80:A9:97:35:64:71 │ Apple │ This Mac│
27
+ └─────────────────┴────────────────────┴──────────────┴────────┘
28
+ ┌─ GATEWAY PORTS (192.168.0.1) ───────────────────────────────┐
29
+ │ 53/tcp DNS OPEN │
30
+ │ 80/tcp HTTP OPEN │
31
+ └──────────────────────────────────────────────────────────────┘
32
+ ```
33
+
34
+ ## Features
35
+
36
+ - **Connection Info** - Local IP, gateway, external IP, DNS servers
37
+ - **Device Discovery** - Find all devices on your network via ARP with vendor identification
38
+ - **Speed Test** - Download/upload speeds and latency via Cloudflare
39
+ - **Port Scanner** - Scan common ports on any host
40
+ - **Beautiful Output** - Clean terminal UI with colors and tables
41
+ - **JSON Output** - Pipe results to other tools
42
+
43
+ ## Installation
44
+
45
+ ### Using Bun (recommended)
46
+
47
+ ```bash
48
+ # Run directly without installing
49
+ bunx netsweep
50
+
51
+ # Or install globally
52
+ bun install -g netsweep
53
+ netprobe
54
+ ```
55
+
56
+ ### Using npm
57
+
58
+ ```bash
59
+ # Run directly
60
+ npx netsweep
61
+
62
+ # Or install globally
63
+ npm install -g netsweep
64
+ netprobe
65
+ ```
66
+
67
+ ### From source
68
+
69
+ ```bash
70
+ git clone https://github.com/jberggren/netprobe.git
71
+ cd netprobe
72
+ bun install
73
+ bun link
74
+ netprobe
75
+ ```
76
+
77
+ ## Usage
78
+
79
+ ```bash
80
+ # Full network scan (connection, speed, devices, ports)
81
+ netprobe
82
+
83
+ # Individual scans
84
+ netprobe -d # Devices only
85
+ netprobe -s # Speed test only
86
+ netprobe -p # Gateway ports only
87
+
88
+ # Scan specific host
89
+ netprobe -p -t 192.168.0.7
90
+
91
+ # JSON output for scripting
92
+ netprobe --json
93
+ netprobe -d --json | jq '.devices[] | select(.vendor == "Apple")'
94
+ ```
95
+
96
+ ## Options
97
+
98
+ | Flag | Short | Description |
99
+ |------|-------|-------------|
100
+ | `--all` | `-a` | Run all scans (default) |
101
+ | `--devices` | `-d` | Scan for network devices |
102
+ | `--speed` | `-s` | Run speed test |
103
+ | `--ports` | `-p` | Scan gateway ports |
104
+ | `--target <ip>` | `-t` | Scan specific IP for ports |
105
+ | `--json` | | Output as JSON |
106
+ | `--help` | `-h` | Show help |
107
+
108
+ ## Requirements
109
+
110
+ - **Bun** >= 1.0 or **Node.js** >= 18
111
+ - **macOS** (Linux support coming soon)
112
+
113
+ ## How it works
114
+
115
+ - **Connection**: Uses `ipconfig`, `netstat`, and ipify.org API
116
+ - **Devices**: Parses the ARP table with MAC vendor lookup (1000+ vendors)
117
+ - **Speed**: Tests against Cloudflare's speed test endpoints
118
+ - **Ports**: TCP connect scan on common service ports
119
+
120
+ ## License
121
+
122
+ MIT
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "netsweep",
3
+ "version": "1.0.0",
4
+ "description": "Network Swiss Army Knife - CLI tool for comprehensive network diagnostics",
5
+ "type": "module",
6
+ "bin": {
7
+ "netprobe": "./src/index.ts"
8
+ },
9
+ "scripts": {
10
+ "start": "bun run src/index.ts",
11
+ "dev": "bun run --watch src/index.ts"
12
+ },
13
+ "keywords": [
14
+ "network",
15
+ "diagnostics",
16
+ "cli",
17
+ "speed-test",
18
+ "port-scanner",
19
+ "arp",
20
+ "devices",
21
+ "terminal"
22
+ ],
23
+ "author": "Johannes Berggren",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/Johannes-Berggren/netprobe.git"
28
+ },
29
+ "bugs": {
30
+ "url": "https://github.com/Johannes-Berggren/netprobe/issues"
31
+ },
32
+ "homepage": "https://github.com/Johannes-Berggren/netprobe#readme",
33
+ "engines": {
34
+ "node": ">=18",
35
+ "bun": ">=1.0"
36
+ },
37
+ "files": [
38
+ "src",
39
+ "README.md"
40
+ ],
41
+ "dependencies": {
42
+ "chalk": "^5.3.0",
43
+ "boxen": "^7.1.1",
44
+ "cli-table3": "^0.6.3",
45
+ "ora": "^8.0.1"
46
+ },
47
+ "devDependencies": {
48
+ "@types/bun": "latest"
49
+ }
50
+ }
@@ -0,0 +1,107 @@
1
+ import { getConnectionInfo } from '../scanners/connection';
2
+ import { scanDevices } from '../scanners/devices';
3
+ import { runSpeedTest } from '../scanners/speed';
4
+ import { scanPorts } from '../scanners/ports';
5
+ import {
6
+ header,
7
+ connectionSection,
8
+ speedSection,
9
+ devicesSection,
10
+ portsSection,
11
+ outputJson,
12
+ type ScanResults,
13
+ } from '../ui/output';
14
+ import { startSpinner, updateSpinner, stopSpinner } from '../ui/spinner';
15
+
16
+ export interface ScanOptions {
17
+ devices: boolean;
18
+ speed: boolean;
19
+ ports: boolean;
20
+ json: boolean;
21
+ target?: string;
22
+ }
23
+
24
+ export async function scan(options: ScanOptions): Promise<void> {
25
+ const results: ScanResults = {};
26
+
27
+ // Always get connection info first (needed for other scans)
28
+ if (!options.json) {
29
+ startSpinner('Getting connection info...');
30
+ }
31
+
32
+ try {
33
+ results.connection = await getConnectionInfo();
34
+ results.gateway = options.target || results.connection.gateway;
35
+ } catch (error) {
36
+ stopSpinner();
37
+ if (!options.json) {
38
+ console.error('Failed to get connection info');
39
+ }
40
+ return;
41
+ }
42
+
43
+ // Run device scan
44
+ if (options.devices) {
45
+ if (!options.json) {
46
+ updateSpinner('Scanning for devices...');
47
+ }
48
+ try {
49
+ results.devices = await scanDevices(results.connection.localIP);
50
+ } catch {
51
+ // Continue with other scans
52
+ }
53
+ }
54
+
55
+ // Run speed test
56
+ if (options.speed) {
57
+ if (!options.json) {
58
+ updateSpinner('Running speed test...');
59
+ }
60
+ try {
61
+ results.speed = await runSpeedTest((stage) => {
62
+ if (!options.json) {
63
+ updateSpinner(stage);
64
+ }
65
+ });
66
+ } catch {
67
+ // Continue with other scans
68
+ }
69
+ }
70
+
71
+ // Run port scan
72
+ if (options.ports && results.gateway) {
73
+ if (!options.json) {
74
+ updateSpinner(`Scanning ports on ${results.gateway}...`);
75
+ }
76
+ try {
77
+ results.ports = await scanPorts(results.gateway);
78
+ } catch {
79
+ // Continue
80
+ }
81
+ }
82
+
83
+ stopSpinner();
84
+
85
+ // Output results
86
+ if (options.json) {
87
+ outputJson(results);
88
+ } else {
89
+ header();
90
+
91
+ if (results.connection) {
92
+ connectionSection(results.connection);
93
+ }
94
+
95
+ if (results.speed) {
96
+ speedSection(results.speed);
97
+ }
98
+
99
+ if (results.devices) {
100
+ devicesSection(results.devices);
101
+ }
102
+
103
+ if (results.ports !== undefined && results.gateway) {
104
+ portsSection(results.ports, results.gateway);
105
+ }
106
+ }
107
+ }
package/src/index.ts ADDED
@@ -0,0 +1,52 @@
1
+ #!/usr/bin/env bun
2
+ import { scan } from './commands/scan';
3
+
4
+ const args = process.argv.slice(2);
5
+
6
+ if (args.includes('--help') || args.includes('-h')) {
7
+ console.log(`
8
+ ${'\x1b[36m'}netprobe${'\x1b[0m'} - Network Swiss Army Knife
9
+
10
+ ${'\x1b[1m'}Usage:${'\x1b[0m'} netprobe [options]
11
+
12
+ ${'\x1b[1m'}Options:${'\x1b[0m'}
13
+ --all, -a Run all scans (default)
14
+ --devices, -d Only scan for devices
15
+ --speed, -s Only run speed test
16
+ --ports, -p Only scan gateway ports
17
+ --target, -t <ip> Scan specific IP for ports
18
+ --json Output as JSON
19
+ --help, -h Show help
20
+
21
+ ${'\x1b[1m'}Examples:${'\x1b[0m'}
22
+ netprobe Full network scan
23
+ netprobe -d List network devices only
24
+ netprobe -s Speed test only
25
+ netprobe -p Scan gateway ports
26
+ netprobe -p -t 192.168.0.7 Scan ports on specific host
27
+ netprobe --json Output results as JSON
28
+ `);
29
+ process.exit(0);
30
+ }
31
+
32
+ // Parse target option
33
+ let target: string | undefined;
34
+ const targetIndex = args.findIndex(a => a === '-t' || a === '--target');
35
+ if (targetIndex !== -1 && args[targetIndex + 1]) {
36
+ target = args[targetIndex + 1];
37
+ }
38
+
39
+ // Determine which scans to run
40
+ const hasSpecificFlags = args.includes('-d') || args.includes('--devices') ||
41
+ args.includes('-s') || args.includes('--speed') ||
42
+ args.includes('-p') || args.includes('--ports');
43
+
44
+ const runAll = args.includes('-a') || args.includes('--all') || !hasSpecificFlags;
45
+
46
+ await scan({
47
+ devices: runAll || args.includes('-d') || args.includes('--devices'),
48
+ speed: runAll || args.includes('-s') || args.includes('--speed'),
49
+ ports: runAll || args.includes('-p') || args.includes('--ports'),
50
+ json: args.includes('--json'),
51
+ target,
52
+ });
@@ -0,0 +1,33 @@
1
+ import { exec } from '../utils/exec';
2
+
3
+ export interface ConnectionInfo {
4
+ interface: string;
5
+ localIP: string;
6
+ gateway: string;
7
+ externalIP: string;
8
+ dns: string[];
9
+ }
10
+
11
+ export async function getConnectionInfo(): Promise<ConnectionInfo> {
12
+ // Try to find the active interface
13
+ const [localIPEn0, localIPEn1, gateway, externalIP, dns] = await Promise.all([
14
+ exec('ipconfig getifaddr en0'),
15
+ exec('ipconfig getifaddr en1'),
16
+ exec("netstat -nr | grep default | head -1 | awk '{print $2}'"),
17
+ fetch('https://api.ipify.org', { signal: AbortSignal.timeout(5000) })
18
+ .then(r => r.text())
19
+ .catch(() => 'Unknown'),
20
+ exec("scutil --dns | grep 'nameserver\\[' | head -4 | awk '{print $3}'"),
21
+ ]);
22
+
23
+ const localIP = localIPEn0.trim() || localIPEn1.trim() || 'Unknown';
24
+ const interfaceName = localIPEn0.trim() ? 'Wi-Fi (en0)' : localIPEn1.trim() ? 'Ethernet (en1)' : 'Unknown';
25
+
26
+ return {
27
+ interface: interfaceName,
28
+ localIP,
29
+ gateway: gateway.trim() || 'Unknown',
30
+ externalIP: externalIP.trim(),
31
+ dns: dns.trim().split('\n').filter(Boolean),
32
+ };
33
+ }
@@ -0,0 +1,76 @@
1
+ import { exec } from '../utils/exec';
2
+ import { lookupVendor } from '../utils/vendor';
3
+
4
+ export interface Device {
5
+ ip: string;
6
+ mac: string;
7
+ vendor: string;
8
+ hostname?: string;
9
+ isCurrentDevice?: boolean;
10
+ }
11
+
12
+ // Normalize MAC address to have leading zeros (0:17:88 -> 00:17:88)
13
+ function normalizeMac(mac: string): string {
14
+ return mac
15
+ .split(':')
16
+ .map(part => part.padStart(2, '0'))
17
+ .join(':')
18
+ .toUpperCase();
19
+ }
20
+
21
+ // Check if IP is a multicast or broadcast address
22
+ function isSpecialAddress(ip: string): boolean {
23
+ const firstOctet = parseInt(ip.split('.')[0]);
24
+ // Filter multicast (224-239), broadcast (.255), and link-local (169.254)
25
+ if (firstOctet >= 224 && firstOctet <= 239) return true;
26
+ if (ip.endsWith('.255')) return true;
27
+ if (ip.startsWith('169.254.')) return true;
28
+ return false;
29
+ }
30
+
31
+ export async function scanDevices(localIP?: string): Promise<Device[]> {
32
+ // Get current device's MAC
33
+ const ifconfigOutput = await exec('ifconfig en0 | grep ether');
34
+ const currentMacMatch = ifconfigOutput.match(/ether\s+([0-9a-f:]+)/i);
35
+ const currentMac = currentMacMatch ? normalizeMac(currentMacMatch[1]) : '';
36
+
37
+ // Read ARP table
38
+ const arpOutput = await exec('arp -a');
39
+
40
+ // Parse: "? (192.168.0.1) at f0:81:75:22:32:22 on en0 [ethernet]"
41
+ // or: "hostname (192.168.0.1) at f0:81:75:22:32:22 on en0 [ethernet]"
42
+ const devices = arpOutput
43
+ .split('\n')
44
+ .filter(line => line.includes(' at ') && !line.includes('incomplete'))
45
+ .map(line => {
46
+ const ipMatch = line.match(/\(([0-9.]+)\)/);
47
+ const macMatch = line.match(/at ([0-9a-f:]+)/i);
48
+ const hostMatch = line.match(/^(\S+)\s+\(/);
49
+
50
+ if (!ipMatch || !macMatch) return null;
51
+
52
+ const ip = ipMatch[1];
53
+
54
+ // Skip multicast and broadcast addresses
55
+ if (isSpecialAddress(ip)) return null;
56
+
57
+ const mac = normalizeMac(macMatch[1]);
58
+ const isCurrentDevice = localIP === ip || mac === currentMac;
59
+
60
+ return {
61
+ ip,
62
+ mac,
63
+ vendor: lookupVendor(mac),
64
+ hostname: hostMatch?.[1] !== '?' ? hostMatch[1] : undefined,
65
+ isCurrentDevice,
66
+ };
67
+ })
68
+ .filter(Boolean) as Device[];
69
+
70
+ // Sort by IP numerically
71
+ return devices.sort((a, b) => {
72
+ const aNum = a.ip.split('.').reduce((acc, n, i) => acc + parseInt(n) * Math.pow(256, 3 - i), 0);
73
+ const bNum = b.ip.split('.').reduce((acc, n, i) => acc + parseInt(n) * Math.pow(256, 3 - i), 0);
74
+ return aNum - bNum;
75
+ });
76
+ }
@@ -0,0 +1,66 @@
1
+ import { connect, type Socket } from 'net';
2
+
3
+ const COMMON_PORTS: Record<number, string> = {
4
+ 21: 'FTP',
5
+ 22: 'SSH',
6
+ 23: 'Telnet',
7
+ 25: 'SMTP',
8
+ 53: 'DNS',
9
+ 80: 'HTTP',
10
+ 110: 'POP3',
11
+ 143: 'IMAP',
12
+ 443: 'HTTPS',
13
+ 445: 'SMB',
14
+ 548: 'AFP',
15
+ 3306: 'MySQL',
16
+ 3389: 'RDP',
17
+ 5432: 'PostgreSQL',
18
+ 5900: 'VNC',
19
+ 8080: 'HTTP-Alt',
20
+ 8443: 'HTTPS-Alt',
21
+ };
22
+
23
+ export interface PortResult {
24
+ port: number;
25
+ service: string;
26
+ open: boolean;
27
+ }
28
+
29
+ export async function scanPorts(
30
+ host: string,
31
+ ports = Object.keys(COMMON_PORTS).map(Number)
32
+ ): Promise<PortResult[]> {
33
+ const results = await Promise.all(
34
+ ports.map(port => checkPort(host, port))
35
+ );
36
+
37
+ return results
38
+ .filter(r => r.open)
39
+ .map(r => ({ ...r, service: COMMON_PORTS[r.port] || 'Unknown' }));
40
+ }
41
+
42
+ function checkPort(host: string, port: number, timeout = 1000): Promise<PortResult> {
43
+ return new Promise(resolve => {
44
+ const socket: Socket = connect({ host, port, timeout });
45
+
46
+ const cleanup = () => {
47
+ socket.removeAllListeners();
48
+ socket.destroy();
49
+ };
50
+
51
+ socket.on('connect', () => {
52
+ cleanup();
53
+ resolve({ port, service: '', open: true });
54
+ });
55
+
56
+ socket.on('error', () => {
57
+ cleanup();
58
+ resolve({ port, service: '', open: false });
59
+ });
60
+
61
+ socket.on('timeout', () => {
62
+ cleanup();
63
+ resolve({ port, service: '', open: false });
64
+ });
65
+ });
66
+ }
@@ -0,0 +1,85 @@
1
+ export interface SpeedResult {
2
+ download: number; // Mbps
3
+ upload: number; // Mbps
4
+ latency: number; // ms
5
+ jitter: number; // ms
6
+ }
7
+
8
+ export async function runSpeedTest(
9
+ onProgress?: (stage: string) => void
10
+ ): Promise<SpeedResult> {
11
+ // Latency test
12
+ onProgress?.('Testing latency...');
13
+ const latencies = await Promise.all([
14
+ pingHost('1.1.1.1'),
15
+ pingHost('8.8.8.8'),
16
+ pingHost('208.67.222.222'),
17
+ ]);
18
+ const validLatencies = latencies.filter(l => l > 0);
19
+ const latency = validLatencies.length > 0 ? average(validLatencies) : 0;
20
+ const jitter = validLatencies.length > 1 ? standardDeviation(validLatencies) : 0;
21
+
22
+ // Download test using Cloudflare's speed test endpoint
23
+ onProgress?.('Testing download...');
24
+ const downloadBytes = 10_000_000; // 10MB for faster results
25
+ const downloadStart = performance.now();
26
+
27
+ try {
28
+ const response = await fetch(`https://speed.cloudflare.com/__down?bytes=${downloadBytes}`, {
29
+ signal: AbortSignal.timeout(30000),
30
+ });
31
+ await response.arrayBuffer(); // Consume the response
32
+ } catch {
33
+ // If Cloudflare fails, return partial results
34
+ return { download: 0, upload: 0, latency, jitter };
35
+ }
36
+
37
+ const downloadTime = (performance.now() - downloadStart) / 1000;
38
+ const download = (downloadBytes * 8) / downloadTime / 1_000_000;
39
+
40
+ // Upload test
41
+ onProgress?.('Testing upload...');
42
+ const uploadData = new Uint8Array(5_000_000); // 5MB
43
+ const uploadStart = performance.now();
44
+
45
+ try {
46
+ await fetch('https://speed.cloudflare.com/__up', {
47
+ method: 'POST',
48
+ body: uploadData,
49
+ signal: AbortSignal.timeout(30000),
50
+ });
51
+ } catch {
52
+ return { download, upload: 0, latency, jitter };
53
+ }
54
+
55
+ const uploadTime = (performance.now() - uploadStart) / 1000;
56
+ const upload = (uploadData.length * 8) / uploadTime / 1_000_000;
57
+
58
+ return { download, upload, latency, jitter };
59
+ }
60
+
61
+ async function pingHost(host: string): Promise<number> {
62
+ const start = performance.now();
63
+ try {
64
+ await fetch(`https://${host}`, {
65
+ method: 'HEAD',
66
+ mode: 'no-cors',
67
+ signal: AbortSignal.timeout(5000),
68
+ });
69
+ return performance.now() - start;
70
+ } catch {
71
+ return 0;
72
+ }
73
+ }
74
+
75
+ function average(arr: number[]): number {
76
+ if (arr.length === 0) return 0;
77
+ return arr.reduce((a, b) => a + b, 0) / arr.length;
78
+ }
79
+
80
+ function standardDeviation(arr: number[]): number {
81
+ if (arr.length < 2) return 0;
82
+ const avg = average(arr);
83
+ const squareDiffs = arr.map(value => Math.pow(value - avg, 2));
84
+ return Math.sqrt(average(squareDiffs));
85
+ }