netsweep 1.0.0 → 1.0.1

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/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "netsweep",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Network Swiss Army Knife - CLI tool for comprehensive network diagnostics",
5
5
  "type": "module",
6
6
  "bin": {
7
- "netprobe": "./src/index.ts"
7
+ "netsweep": "./dist/index.js"
8
8
  },
9
9
  "scripts": {
10
- "start": "bun run src/index.ts",
10
+ "build": "bun build src/index.ts --outdir dist --target node --format esm",
11
+ "prepublishOnly": "npm run build",
12
+ "start": "node dist/index.js",
11
13
  "dev": "bun run --watch src/index.ts"
12
14
  },
13
15
  "keywords": [
@@ -31,11 +33,10 @@
31
33
  },
32
34
  "homepage": "https://github.com/Johannes-Berggren/netprobe#readme",
33
35
  "engines": {
34
- "node": ">=18",
35
- "bun": ">=1.0"
36
+ "node": ">=18"
36
37
  },
37
38
  "files": [
38
- "src",
39
+ "dist",
39
40
  "README.md"
40
41
  ],
41
42
  "dependencies": {
@@ -45,6 +46,7 @@
45
46
  "ora": "^8.0.1"
46
47
  },
47
48
  "devDependencies": {
48
- "@types/bun": "latest"
49
+ "@types/bun": "latest",
50
+ "@types/node": "^20.0.0"
49
51
  }
50
52
  }
@@ -1,107 +0,0 @@
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 DELETED
@@ -1,52 +0,0 @@
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
- });
@@ -1,33 +0,0 @@
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
- }
@@ -1,76 +0,0 @@
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
- }
@@ -1,66 +0,0 @@
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
- }
@@ -1,85 +0,0 @@
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
- }
package/src/ui/output.ts DELETED
@@ -1,131 +0,0 @@
1
- import chalk from 'chalk';
2
- import boxen from 'boxen';
3
- import Table from 'cli-table3';
4
- import type { Device } from '../scanners/devices';
5
- import type { ConnectionInfo } from '../scanners/connection';
6
- import type { SpeedResult } from '../scanners/speed';
7
- import type { PortResult } from '../scanners/ports';
8
- import { formatSpeed, formatLatency, padRight } from '../utils/format';
9
-
10
- const BOX_WIDTH = 62;
11
-
12
- export function header() {
13
- console.log(
14
- boxen(chalk.bold.cyan(' NETPROBE - Network Diagnostics '), {
15
- padding: 0,
16
- margin: { top: 1, bottom: 0, left: 0, right: 0 },
17
- borderStyle: 'round',
18
- borderColor: 'cyan',
19
- })
20
- );
21
- console.log();
22
- }
23
-
24
- export function section(title: string, content: string[]) {
25
- const titleLine = `─ ${chalk.white.bold(title)} `;
26
- const dashesNeeded = BOX_WIDTH - title.length - 4;
27
-
28
- console.log(chalk.gray('┌' + titleLine + '─'.repeat(dashesNeeded) + '┐'));
29
-
30
- content.forEach(line => {
31
- const visibleLen = stripAnsi(line).length;
32
- const padding = BOX_WIDTH - visibleLen - 4;
33
- console.log(chalk.gray('│ ') + line + ' '.repeat(Math.max(0, padding)) + chalk.gray(' │'));
34
- });
35
-
36
- console.log(chalk.gray('└' + '─'.repeat(BOX_WIDTH) + '┘'));
37
- console.log();
38
- }
39
-
40
- export function connectionSection(info: ConnectionInfo) {
41
- section('CONNECTION', [
42
- `${chalk.gray('Interface:')} ${chalk.white(info.interface)}`,
43
- `${chalk.gray('Local IP:')} ${chalk.white(info.localIP)}`,
44
- `${chalk.gray('Gateway:')} ${chalk.white(info.gateway)}`,
45
- `${chalk.gray('External IP:')} ${chalk.white(info.externalIP)}`,
46
- `${chalk.gray('DNS:')} ${chalk.white(info.dns.join(', '))}`,
47
- ]);
48
- }
49
-
50
- export function speedSection(speed: SpeedResult) {
51
- section('SPEED', [
52
- `${chalk.green('↓')} ${chalk.gray('Download:')} ${chalk.white.bold(formatSpeed(speed.download))}`,
53
- `${chalk.blue('↑')} ${chalk.gray('Upload:')} ${chalk.white.bold(formatSpeed(speed.upload))}`,
54
- `${chalk.gray('Latency:')} ${chalk.white(formatLatency(speed.latency))} ${chalk.gray(`(jitter: ${formatLatency(speed.jitter)})`)}`,
55
- ]);
56
- }
57
-
58
- export function devicesSection(devices: Device[]) {
59
- const table = new Table({
60
- head: ['IP', 'MAC', 'Vendor', 'Name'].map(h => chalk.cyan(h)),
61
- style: {
62
- head: [],
63
- border: ['gray'],
64
- },
65
- chars: {
66
- 'top': '─', 'top-mid': '┬', 'top-left': '┌', 'top-right': '┐',
67
- 'bottom': '─', 'bottom-mid': '┴', 'bottom-left': '└', 'bottom-right': '┘',
68
- 'left': '│', 'left-mid': '├', 'mid': '─', 'mid-mid': '┼',
69
- 'right': '│', 'right-mid': '┤', 'middle': '│'
70
- },
71
- colWidths: [17, 20, 14, 14],
72
- });
73
-
74
- devices.forEach(d => {
75
- const name = d.isCurrentDevice
76
- ? chalk.green('This Mac')
77
- : d.hostname
78
- ? truncate(d.hostname, 12)
79
- : chalk.gray('-');
80
-
81
- table.push([
82
- d.ip,
83
- d.mac,
84
- truncate(d.vendor, 12),
85
- name,
86
- ]);
87
- });
88
-
89
- console.log(chalk.gray(`┌─ ${chalk.white.bold('DEVICES')} (${devices.length} found) ${'─'.repeat(BOX_WIDTH - 18 - String(devices.length).length)}┐`));
90
- console.log(table.toString());
91
- console.log();
92
- }
93
-
94
- export function portsSection(ports: PortResult[], host: string) {
95
- if (ports.length === 0) {
96
- section(`GATEWAY PORTS (${host})`, [
97
- chalk.gray('No open ports found'),
98
- ]);
99
- return;
100
- }
101
-
102
- const lines = ports.map(p => {
103
- const portStr = padRight(`${p.port}/tcp`, 10);
104
- const serviceStr = padRight(p.service, 10);
105
- return `${chalk.white(portStr)}${chalk.gray(serviceStr)}${chalk.green('OPEN')}`;
106
- });
107
-
108
- section(`GATEWAY PORTS (${host})`, lines);
109
- }
110
-
111
- function truncate(str: string, len: number): string {
112
- if (str.length <= len) return str;
113
- return str.slice(0, len - 1) + '…';
114
- }
115
-
116
- function stripAnsi(str: string): string {
117
- // eslint-disable-next-line no-control-regex
118
- return str.replace(/\x1B\[[0-9;]*m/g, '');
119
- }
120
-
121
- export interface ScanResults {
122
- connection?: ConnectionInfo;
123
- devices?: Device[];
124
- speed?: SpeedResult;
125
- ports?: PortResult[];
126
- gateway?: string;
127
- }
128
-
129
- export function outputJson(results: ScanResults) {
130
- console.log(JSON.stringify(results, null, 2));
131
- }
package/src/ui/spinner.ts DELETED
@@ -1,29 +0,0 @@
1
- import ora, { type Ora } from 'ora';
2
-
3
- let currentSpinner: Ora | null = null;
4
-
5
- export function startSpinner(text: string): Ora {
6
- if (currentSpinner) {
7
- currentSpinner.stop();
8
- }
9
- currentSpinner = ora({
10
- text,
11
- spinner: 'dots',
12
- }).start();
13
- return currentSpinner;
14
- }
15
-
16
- export function updateSpinner(text: string): void {
17
- if (currentSpinner) {
18
- currentSpinner.text = text;
19
- } else {
20
- startSpinner(text);
21
- }
22
- }
23
-
24
- export function stopSpinner(): void {
25
- if (currentSpinner) {
26
- currentSpinner.stop();
27
- currentSpinner = null;
28
- }
29
- }
package/src/utils/exec.ts DELETED
@@ -1,10 +0,0 @@
1
- import { $ } from 'bun';
2
-
3
- export async function exec(command: string): Promise<string> {
4
- try {
5
- const result = await $`sh -c ${command}`.text();
6
- return result;
7
- } catch (error) {
8
- return '';
9
- }
10
- }
@@ -1,18 +0,0 @@
1
- export function formatSpeed(mbps: number): string {
2
- if (mbps >= 1000) {
3
- return `${(mbps / 1000).toFixed(1)} Gbps`;
4
- }
5
- return `${mbps.toFixed(1)} Mbps`;
6
- }
7
-
8
- export function formatLatency(ms: number): string {
9
- return `${Math.round(ms)}ms`;
10
- }
11
-
12
- export function padRight(str: string, len: number): string {
13
- return str.length >= len ? str.slice(0, len) : str + ' '.repeat(len - str.length);
14
- }
15
-
16
- export function padLeft(str: string, len: number): string {
17
- return str.length >= len ? str.slice(0, len) : ' '.repeat(len - str.length) + str;
18
- }