32 lines
1.2 KiB
JavaScript
32 lines
1.2 KiB
JavaScript
// Decoupled LAN scanner: pure parser + a thin arp-scan runner (exec injected
|
|
// for tests). The repo/cron own persistence — this module only produces rows.
|
|
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
|
|
const pexec = promisify(execFile);
|
|
|
|
// A locally-administered (randomized) MAC has bit 0x02 set in its first octet.
|
|
export function isRandomizedMac(mac) {
|
|
const first = parseInt(String(mac).split(':')[0], 16);
|
|
return Number.isFinite(first) && (first & 0x02) === 0x02;
|
|
}
|
|
|
|
// Keep only "IP<ws>MAC<ws>[vendor]" lines; ignore banner/footer/garbage.
|
|
export function parseArpScan(text) {
|
|
const re = /^(\d{1,3}(?:\.\d{1,3}){3})\s+([0-9a-fA-F]{2}(?::[0-9a-fA-F]{2}){5})\s*(.*)$/;
|
|
const out = [];
|
|
for (const line of String(text).split('\n')) {
|
|
const m = line.match(re);
|
|
if (!m) continue;
|
|
const mac = m[2].toLowerCase();
|
|
out.push({ ip: m[1], mac, vendor: m[3].trim(), randomized: isRandomizedMac(mac) });
|
|
}
|
|
return out;
|
|
}
|
|
|
|
// Run arp-scan on the local /24. `exec(file, args) -> {stdout}` injected for tests.
|
|
export async function runScan({ exec = pexec } = {}) {
|
|
const { stdout } = await exec('arp-scan', ['--localnet', '--plain', '--retry=2']);
|
|
return parseArpScan(stdout);
|
|
}
|