domovoy/app/Services/Discovery/NetworkScanner.php

123 lines
3.8 KiB
PHP

<?php
declare(strict_types=1);
namespace Domovoy\Services\Discovery;
use Domovoy\Models\DiscoveredHost;
use Domovoy\Models\NetworkRange;
use Domovoy\Repositories\DiscoveredHostRepository;
class NetworkScanner
{
private PingScanner $pingScanner;
private TcpPortScanner $tcpPortScanner;
private ArpTableReader $arpTableReader;
private HostFingerprintService $fingerprintService;
private DiscoveredHostRepository $discoveredHostRepository;
/** @var int[] */
private array $defaultPorts = [22, 23, 53, 80, 443, 445, 548, 631, 3306, 5432, 6379, 8000, 8080, 8443, 9000, 9090, 9100];
public function __construct(
PingScanner $pingScanner,
TcpPortScanner $tcpPortScanner,
ArpTableReader $arpTableReader,
HostFingerprintService $fingerprintService,
DiscoveredHostRepository $discoveredHostRepository
) {
$this->pingScanner = $pingScanner;
$this->tcpPortScanner = $tcpPortScanner;
$this->arpTableReader = $arpTableReader;
$this->fingerprintService = $fingerprintService;
$this->discoveredHostRepository = $discoveredHostRepository;
}
/**
* @return DiscoveredHost[]
*/
public function scan(NetworkRange $range): array
{
$ips = $this->enumerateIps($range->cidr);
$aliveHosts = [];
foreach ($ips as $ip) {
// Step 1: Ping sweep
if (!$this->pingScanner->ping($ip)) {
continue;
}
$host = new DiscoveredHost();
$host->ipAddress = $ip;
$host->firstSeen = new \DateTimeImmutable();
$host->lastSeen = new \DateTimeImmutable();
// Step 2: TCP port scan
$openPorts = $this->tcpPortScanner->scan($ip, $this->defaultPorts, 200, 50);
$host->openPorts = $openPorts;
// Step 3: Reverse DNS
$hostname = $this->resolveHostname($ip);
$host->hostname = $hostname;
// Step 4: Determine vendor by port patterns
$host->vendor = $this->fingerprintService->guessVendor($openPorts, $hostname);
// Step 5: Confidence score
$host->confidence = $this->fingerprintService->calculateConfidence(count($openPorts), $hostname !== null);
$aliveHosts[] = $host;
}
// Step 6: ARP table enrichment
$arpEntries = $this->arpTableReader->read();
foreach ($aliveHosts as $host) {
if (isset($arpEntries[$host->ipAddress])) {
$host->macAddress = $arpEntries[$host->ipAddress]['mac'] ?? null;
$host->vendor = $host->vendor ?: ($arpEntries[$host->ipAddress]['vendor'] ?? null);
}
$this->discoveredHostRepository->save($host);
}
return $aliveHosts;
}
/**
* @return string[]
*/
private function enumerateIps(string $cidr): array
{
if (str_contains($cidr, '/32')) {
return [explode('/', $cidr)[0]];
}
// Only handle /24 or larger for MVP (to avoid scanning huge ranges)
if (preg_match('#^(\d+\.\d+\.\d+)\.(\d+)/(\d+)$#', $cidr, $m)) {
$prefix = $m[1];
$suffix = (int)$m[2];
$mask = (int)$m[3];
if ($mask > 24) {
// Treat smaller than /24 as single IP
return [$cidr];
}
$offset = $mask === 24 ? 0 : $suffix;
$count = 2 ** (24 - $mask);
$ips = [];
for ($i = 1; $i <= $count; $i++) {
$ips[] = $prefix . '.' . ($offset + $i);
}
return $ips;
}
return [];
}
private function resolveHostname(string $ip): ?string
{
$hostname = @gethostbyaddr($ip);
return ($hostname !== false && $hostname !== $ip) ? $hostname : null;
}
}