argument('segment'); $force = $this->option('force'); if ($segmentId) { $segments = NetworkSegment::where('id', $segmentId)->where('active', true)->get(); if ($segments->isEmpty()) { $this->error("Segment #{$segmentId} nicht gefunden oder inaktiv."); return Command::FAILURE; } } else { $segments = NetworkSegment::where('active', true)->get() ->filter(fn($s) => $force || $s->isScanDue()); } if ($segments->isEmpty()) { $this->info('Kein Segment fällig. Mit --force erzwingen.'); return Command::SUCCESS; } foreach ($segments as $segment) { $this->info("Scanne Segment: {$segment->name} ({$segment->subnet}) ..."); $this->scanSegment($segment); } return Command::SUCCESS; } private function scanSegment(NetworkSegment $segment): void { // Subnetz aus "192.168.86.0/24" extrahieren, oder direkt als Range nutzen $target = $segment->subnet; $options = $segment->nmap_options ?: '-sn'; // nmap im XML-Modus ausführen $cmd = "nmap {$options} -oX - " . escapeshellarg($target) . " 2>/dev/null"; $xmlOutput = shell_exec($cmd); if (empty($xmlOutput)) { $this->warn(" nmap lieferte keine Ausgabe. Ist nmap installiert? (sudo pacman -S nmap)"); Log::warning("NetworkScan: nmap gab keine Ausgabe für Segment {$segment->id}"); return; } $hosts = $this->parseNmapXml($xmlOutput); $this->info(" {$hosts['online']} online von " . count($hosts['rows']) . " gefunden."); DB::transaction(function () use ($segment, $hosts, $target) { $scan = NetworkScan::create([ 'segment_id' => $segment->id, 'subnet' => $target, 'source' => 'auto', 'scanner' => 'nmap', 'total_hosts' => count($hosts['rows']), 'online_hosts' => $hosts['online'], 'new_devices' => 0, 'changed_devices' => 0, 'created_by' => null, ]); $newDevices = 0; $changedDevices = 0; foreach ($hosts['rows'] as $row) { $hostRecord = NetworkHost::create([ 'scan_id' => $scan->id, 'ip_address' => $row['ip'], 'mac_address' => $row['mac'] ?: null, 'hostname' => $row['hostname'] ?: null, 'mac_vendor' => $row['vendor'] ?: null, 'status' => $row['status'], 'ping_ms' => null, 'ttl' => null, ]); if (!empty($row['mac'])) { [$newDevices, $changedDevices] = $this->processDevice( $hostRecord, $row, $scan->id, $newDevices, $changedDevices ); } elseif ($row['status'] === 'online') { // Kein MAC (Remote-Subnet / kein Root-ARP) → Tracking per Hostname/IP [$newDevices, $changedDevices] = $this->processDeviceWithoutMac( $hostRecord, $row, $scan->id, $newDevices, $changedDevices ); } } $scan->update([ 'new_devices' => $newDevices, 'changed_devices' => $changedDevices, ]); }); $segment->update(['last_scanned_at' => now()]); $this->info(" ✓ Scan gespeichert."); } private function parseNmapXml(string $xml): array { $rows = []; $online = 0; try { $doc = new \SimpleXMLElement($xml); } catch (\Exception $e) { Log::error('NetworkScan: nmap XML parse error: ' . $e->getMessage()); return ['rows' => [], 'online' => 0]; } foreach ($doc->host as $host) { $status = (string) $host->status['state']; // up | down $ip = ''; $mac = ''; $vendor = ''; foreach ($host->address as $addr) { $type = (string) $addr['addrtype']; if ($type === 'ipv4') { $ip = (string) $addr['addr']; } elseif ($type === 'mac') { $mac = $this->normalizeMac((string) $addr['addr']); $vendor = (string) $addr['vendor']; } } $hostname = ''; foreach ($host->hostnames->hostname ?? [] as $hn) { if ((string) $hn['type'] === 'PTR') { $hostname = (string) $hn['name']; break; } } if ($status === 'up') { $online++; } if ($ip) { $rows[] = [ 'ip' => $ip, 'mac' => $mac, 'vendor' => $vendor, 'hostname' => $hostname, 'status' => $status === 'up' ? 'online' : 'offline', ]; } } return ['rows' => $rows, 'online' => $online]; } private function processDevice( NetworkHost $hostRecord, array $row, int $scanId, int $newDevices, int $changedDevices ): array { $device = NetworkDevice::firstOrNew(['mac_address' => $row['mac']]); $isNew = !$device->exists; if ($isNew) { $device->fill([ 'current_ip' => $row['ip'], 'hostname' => $row['hostname'] ?: null, 'mac_vendor' => $row['vendor'] ?: null, 'status' => $row['status'], 'first_seen_at' => now(), 'last_seen_at' => now(), ])->save(); NetworkDeviceEvent::create([ 'device_id' => $device->id, 'scan_id' => $scanId, 'event_type' => 'new_device', 'new_value' => $row['ip'], 'description' => "Erstes Erscheinen: {$row['ip']}" . ($row['vendor'] ? " ({$row['vendor']})" : ''), ]); $newDevices++; } else { $events = []; if ($device->current_ip !== $row['ip']) { $events[] = [ 'event_type' => 'ip_changed', 'old_value' => $device->current_ip, 'new_value' => $row['ip'], 'description' => "IP geändert: {$device->current_ip} → {$row['ip']}", ]; $changedDevices++; } if ($device->status !== $row['status']) { $events[] = [ 'event_type' => $row['status'] === 'online' ? 'came_online' : 'went_offline', 'old_value' => $device->status, 'new_value' => $row['status'], 'description' => $row['status'] === 'online' ? "Gerät wieder online ({$row['ip']})" : "Gerät offline ({$device->current_ip})", ]; } $device->update([ 'current_ip' => $row['ip'], 'hostname' => $row['hostname'] ?: $device->hostname, 'status' => $row['status'], 'last_seen_at' => now(), ]); foreach ($events as $event) { NetworkDeviceEvent::create(array_merge($event, [ 'device_id' => $device->id, 'scan_id' => $scanId, ])); } } $hostRecord->update(['device_id' => $device->id]); return [$newDevices, $changedDevices]; } /** * Geräte ohne MAC-Adresse tracken (z.B. Remote-Subnet wo kein ARP möglich ist). * Primärschlüssel: Hostname (stabil per DNS). Fallback: IP-Adresse. */ private function processDeviceWithoutMac( NetworkHost $hostRecord, array $row, int $scanId, int $newDevices, int $changedDevices ): array { // Vorhandenes Gerät suchen: erst per Hostname, dann per IP $device = null; if (!empty($row['hostname'])) { $device = NetworkDevice::whereNull('mac_address') ->where('hostname', $row['hostname']) ->first(); } if (!$device) { $device = NetworkDevice::whereNull('mac_address') ->where('current_ip', $row['ip']) ->first(); } $isNew = !$device; if ($isNew) { $device = new NetworkDevice(); $device->fill([ 'mac_address' => null, 'current_ip' => $row['ip'], 'hostname' => $row['hostname'] ?: null, 'mac_vendor' => null, 'status' => $row['status'], 'first_seen_at' => now(), 'last_seen_at' => now(), ])->save(); NetworkDeviceEvent::create([ 'device_id' => $device->id, 'scan_id' => $scanId, 'event_type' => 'new_device', 'new_value' => $row['ip'], 'description' => 'Erstes Erscheinen (kein MAC): ' . $row['ip'] . ($row['hostname'] ? " ({$row['hostname']})" : ''), ]); $newDevices++; } else { $events = []; // IP-Wechsel: Hostname gleich, aber andere IP if ($device->current_ip !== $row['ip']) { $events[] = [ 'event_type' => 'ip_changed', 'old_value' => $device->current_ip, 'new_value' => $row['ip'], 'description' => "IP geändert: {$device->current_ip} → {$row['ip']}", ]; $changedDevices++; } if ($device->status !== $row['status']) { $events[] = [ 'event_type' => $row['status'] === 'online' ? 'came_online' : 'went_offline', 'old_value' => $device->status, 'new_value' => $row['status'], 'description' => $row['status'] === 'online' ? "Gerät wieder online ({$row['ip']})" : "Gerät offline ({$device->current_ip})", ]; } $device->update([ 'current_ip' => $row['ip'], 'hostname' => $row['hostname'] ?: $device->hostname, 'status' => $row['status'], 'last_seen_at' => now(), ]); foreach ($events as $event) { NetworkDeviceEvent::create(array_merge($event, [ 'device_id' => $device->id, 'scan_id' => $scanId, ])); } } $hostRecord->update(['device_id' => $device->id]); return [$newDevices, $changedDevices]; } private function normalizeMac(string $mac): string { $clean = preg_replace('/[^a-fA-F0-9]/', '', $mac); if (strlen($clean) !== 12) { return ''; } return strtoupper(implode(':', str_split($clean, 2))); } }