123 lines
3.8 KiB
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;
|
|
}
|
|
}
|