import { exec } from 'node:child_process'; import { promisify } from 'node:util'; import { env } from '@/config/env'; import type { DiscoveryResult, DiscoveryTarget, Scanner } from '../types'; const execAsync = promisify(exec); /** * Lit la table ARP locale via `ip neigh` (Linux) ou `arp -a` (fallback). * Ne scanne pas activement : retourne ce que le kernel a déjà vu. * Pour forcer la résolution, il faut ping-sweeper d'abord (voir discovery.service). */ export const arpScanner: Scanner = { kind: 'arp', label: 'Table ARP locale', enabled: env.DISCOVERY_ENABLE_ARP, async run(_target: DiscoveryTarget): Promise { const { stdout } = await execAsync('ip neigh show').catch(() => ({ stdout: '' })); if (stdout) return parseIpNeigh(stdout); const { stdout: arpOut } = await execAsync('arp -a').catch(() => ({ stdout: '' })); return parseArpA(arpOut); }, }; function parseIpNeigh(output: string): DiscoveryResult[] { // Exemple : 192.168.1.10 dev eth0 lladdr aa:bb:cc:dd:ee:ff REACHABLE return output .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => { const ipMatch = line.match(/^(\d{1,3}(?:\.\d{1,3}){3})/); const macMatch = line.match(/lladdr\s+([0-9a-fA-F:]{17})/); if (!ipMatch) return null; return { ipAddress: ipMatch[1], macAddress: macMatch?.[1], } satisfies DiscoveryResult; }) .filter((r): r is DiscoveryResult => r !== null); } function parseArpA(output: string): DiscoveryResult[] { // Exemple : ? (192.168.1.10) at aa:bb:cc:dd:ee:ff [ether] on eth0 return output .split('\n') .map((line) => line.trim()) .filter(Boolean) .map((line) => { const ipMatch = line.match(/\((\d{1,3}(?:\.\d{1,3}){3})\)/); const macMatch = line.match(/([0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5})/); if (!ipMatch) return null; return { ipAddress: ipMatch[1], macAddress: macMatch?.[1], } satisfies DiscoveryResult; }) .filter((r): r is DiscoveryResult => r !== null); }