Files
Network-MGMT/app/Services/NetworkScanImporter.php

272 lines
10 KiB
PHP

<?php
namespace App\Services;
use App\Models\NetworkDevice;
use App\Models\NetworkDeviceEvent;
use App\Models\NetworkHost;
use App\Models\NetworkScan;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class NetworkScanImporter
{
private NetworkScan $scan;
private int $newDevices = 0;
private int $changedDevices = 0;
private int $onlineHosts = 0;
/**
* Importiert eine Angry IP Scanner .txt Exportdatei.
*/
public function importAngryIpScannerFile(string $filePath, int $createdBy, ?int $segmentId = null): NetworkScan
{
// Datei einlesen und UTF-8 BOM entfernen
$content = file_get_contents($filePath);
$content = preg_replace('/^\xEF\xBB\xBF/', '', $content); // UTF-8 BOM
$content = str_replace("\r\n", "\n", $content); // Windows CRLF → LF
$content = str_replace("\r", "\n", $content); // altes Mac CR → LF
$lines = array_filter(explode("\n", $content), fn($l) => trim($l) !== '');
$lines = array_values($lines);
$scanner = '';
$subnet = '';
$headers = [];
$rows = [];
$splitPattern = null; // wird beim ersten Header-Treffer gesetzt
foreach ($lines as $line) {
$trimmed = trim($line);
if (str_starts_with($trimmed, 'Erstellt von') || str_starts_with($trimmed, 'Created by')) {
$scanner = trim(str_replace(['Erstellt von ', 'Created by '], '', $trimmed));
continue;
}
if (str_starts_with($trimmed, 'Gescannt') || str_starts_with($trimmed, 'Scanned')) {
preg_match('/(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/', $trimmed, $m);
$subnet = $m[1] ?? '0.0.0.0';
continue;
}
if (str_starts_with($trimmed, 'http')) {
continue;
}
// Header-Zeile erkennen (beginnt mit "IP")
if (str_starts_with($trimmed, 'IP') && empty($headers)) {
// Trennzeichen auto-detektieren
if (substr_count($trimmed, "\t") >= 2) {
$splitPattern = '/\t/';
} elseif (substr_count($trimmed, ';') >= 2) {
$splitPattern = '/;/';
} elseif (substr_count($trimmed, ',') >= 2) {
$splitPattern = '/,/';
} else {
// Angry IP Scanner: mehrere normale Leerzeichen als Trennzeichen
// Non-Breaking Spaces (0xC2 0xA0) bleiben innerhalb von Werten erhalten
$splitPattern = '/ {2,}/';
}
$headers = array_map('trim', preg_split($splitPattern, $trimmed));
continue;
}
// Datenzeile: muss mit IP-Adresse beginnen
if (!empty($headers) && $splitPattern !== null
&& preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/', $trimmed)) {
$cols = array_map(
fn($v) => trim(str_replace("\xc2\xa0", ' ', $v)), // Non-Breaking Spaces → normale Spaces
preg_split($splitPattern, $line)
);
$row = [];
foreach ($headers as $i => $h) {
$row[$h] = $cols[$i] ?? '';
}
$rows[] = $row;
}
}
// DEBUG: Log parse results
Log::info('NetworkScanImporter: scanner=' . $scanner . ', subnet=' . $subnet);
Log::info('NetworkScanImporter: separator hex=' . bin2hex($separator));
Log::info('NetworkScanImporter: headers=' . json_encode($headers));
Log::info('NetworkScanImporter: rows found=' . count($rows));
if (!empty($rows)) {
Log::info('NetworkScanImporter: first row=' . json_encode($rows[0]));
}
return DB::transaction(function () use ($rows, $scanner, $subnet, $createdBy, $segmentId) {
$this->scan = NetworkScan::create([
'segment_id' => $segmentId,
'subnet' => $subnet,
'source' => 'import',
'scanner' => $scanner,
'created_by' => $createdBy,
]);
foreach ($rows as $row) {
$this->processRow($row);
}
$this->scan->update([
'total_hosts' => count($rows),
'online_hosts' => $this->onlineHosts,
'new_devices' => $this->newDevices,
'changed_devices'=> $this->changedDevices,
]);
return $this->scan;
});
}
private function processRow(array $row): void
{
$ip = trim($this->extractColumn($row, ['IP', 'IP-Adresse', 'IP Address', 'IP Adresse']));
$ping = trim($this->extractColumn($row, ['Ping', 'Ping (ms)']));
$host = $this->cleanValue($this->extractColumn($row, ['Hostname', 'Host Name', 'DNS-Hostname']));
$mac = $this->normalizeMAC($this->extractColumn($row, ['MAC Adresse', 'MAC Addresse', 'MAC Address', 'MAC-Adresse', 'MAC']));
$vendor = $this->cleanValue($this->extractColumn($row, ['MAC Hersteller', 'MAC Vendor', 'Hersteller', 'Vendor']));
$ttl = (int) $this->cleanValue($this->extractColumn($row, ['TTL'])) ?: null;
$ports = $this->cleanValue($this->extractColumn($row, ['Ports', 'Gefilterte Ports', 'Offene Ports']));
$netbios= $this->cleanValue($this->extractColumn($row, ['NetBIOS Info', 'NetBIOS-Info', 'NetBIOS']));
$http = $this->cleanValue($this->extractColumn($row, ['HTTP Sender', 'HTTP']));
$web = $this->cleanValue($this->extractColumn($row, ['Web Erkennung', 'Web Detection', 'Webserver']));
$pingMs = null;
$status = 'offline';
// Online-Erkennung: Ping enthält eine Zahl gefolgt von "ms"
if (preg_match('/(\d+)\s*ms/i', $ping, $m)) {
$pingMs = (int) $m[1];
$status = 'online';
$this->onlineHosts++;
}
// Host-Eintrag speichern
$host_record = NetworkHost::create([
'scan_id' => $this->scan->id,
'ip_address' => $ip,
'mac_address' => $mac ?: null,
'hostname' => $host ?: null,
'mac_vendor' => $vendor ?: null,
'status' => $status,
'ping_ms' => $pingMs,
'netbios_info' => $netbios ?: null,
'ttl' => $ttl,
'ports' => $ports ?: null,
'http_sender' => $http ?: null,
'web_detection' => $web ?: null,
]);
// Geräte-Master nur wenn MAC bekannt
if (!empty($mac)) {
$this->processDevice($host_record, $ip, $mac, $host, $vendor, $netbios, $ttl, $ports, $status);
}
}
private function processDevice(
NetworkHost $hostRecord,
string $ip, string $mac, ?string $hostname,
?string $vendor, ?string $netbios, ?int $ttl,
?string $ports, string $status
): void {
$device = NetworkDevice::firstOrNew(['mac_address' => $mac]);
$isNew = !$device->exists;
if ($isNew) {
$device->fill([
'current_ip' => $ip,
'hostname' => $hostname,
'mac_vendor' => $vendor,
'status' => $status,
'netbios_name' => $netbios,
'ttl' => $ttl,
'ports' => $ports,
'first_seen_at'=> now(),
'last_seen_at' => now(),
])->save();
NetworkDeviceEvent::create([
'device_id' => $device->id,
'scan_id' => $this->scan->id,
'event_type' => 'new_device',
'new_value' => $ip,
'description'=> "Erstes Erscheinen: {$ip} ({$vendor})",
]);
$this->newDevices++;
} else {
$events = [];
// IP-Änderung erkennen
if ($device->current_ip !== $ip) {
$events[] = [
'event_type' => 'ip_changed',
'old_value' => $device->current_ip,
'new_value' => $ip,
'description'=> "IP geändert von {$device->current_ip} zu {$ip}",
];
$this->changedDevices++;
}
// Online/Offline-Status
if ($device->status !== $status) {
$events[] = [
'event_type' => $status === 'online' ? 'came_online' : 'went_offline',
'old_value' => $device->status,
'new_value' => $status,
'description'=> $status === 'online'
? "Gerät wieder online ({$ip})"
: "Gerät offline ({$device->current_ip})",
];
}
$device->update([
'current_ip' => $ip,
'hostname' => $hostname ?? $device->hostname,
'status' => $status,
'last_seen_at' => now(),
]);
foreach ($events as $event) {
NetworkDeviceEvent::create(array_merge($event, [
'device_id' => $device->id,
'scan_id' => $this->scan->id,
]));
}
}
$hostRecord->update(['device_id' => $device->id]);
}
private function extractColumn(array $row, array $keys): string
{
foreach ($keys as $key) {
if (isset($row[$key]) && $row[$key] !== '') {
return $row[$key];
}
}
return '';
}
/**
* Bereinigt Angry IP Scanner Platzhalterwerte wie [n/a], [n/s], [n/d] zu leerem String.
*/
private function cleanValue(string $value): string
{
if (preg_match('/^\[n\/[asd]\]$/i', trim($value))) {
return '';
}
return trim($value);
}
private function normalizeMAC(string $mac): string
{
// Verschiedene MAC-Formate normalisieren zu XX:XX:XX:XX:XX:XX
$clean = preg_replace('/[^a-fA-F0-9]/', '', $mac);
if (strlen($clean) !== 12) {
return '';
}
return strtoupper(implode(':', str_split($clean, 2)));
}
}