Итерация 2: сканирование сети
- Phinx миграции: network_ranges, scan_jobs, discovered_hosts, audit_log - Models: NetworkRange, ScanJob, DiscoveredHost - Repositories с CRUD для всех сущностей - NetworkScanner: ping sweep + TCP connect + ARP table + reverse DNS - PingScanner, TcpPortScanner (curl_multi для параллелизма), ArpTableReader - HostFingerprintService: vendor detection + confidence scoring - ScanJobRunner + CLI worker (bin/run-scan-worker.php) - Discovery UI: управление диапазонами, запуск скана, таблица находок - NetworkRangeController с валидацией CIDR - Обновлён public/index.php с DI конфигурацией для новых сервисов
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Services\Discovery;
|
||||
|
||||
class ArpTableReader
|
||||
{
|
||||
/**
|
||||
* Read ARP table from system.
|
||||
* Returns array keyed by IP => ['mac' => '...', 'vendor' => '...']
|
||||
*/
|
||||
public function read(): array
|
||||
{
|
||||
$entries = [];
|
||||
|
||||
// Try ip neigh first (modern Linux)
|
||||
$output = [];
|
||||
$exitCode = 0;
|
||||
exec('ip neigh show 2>/dev/null', $output, $exitCode);
|
||||
if ($exitCode === 0) {
|
||||
foreach ($output as $line) {
|
||||
if (preg_match('/^(\d+\.\d+\.\d+\.\d+)\s+.*lladdr\s+([0-9a-f:]+)/i', $line, $m)) {
|
||||
$entries[$m[1]] = ['mac' => strtolower($m[2]), 'vendor' => null];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: arp -a
|
||||
if (empty($entries)) {
|
||||
exec('arp -a 2>/dev/null', $output, $exitCode);
|
||||
if ($exitCode === 0) {
|
||||
foreach ($output as $line) {
|
||||
if (preg_match('/\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-f:]+)/i', $line, $m)) {
|
||||
$entries[$m[1]] = ['mac' => strtolower($m[2]), 'vendor' => null];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $entries;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Services\Discovery;
|
||||
|
||||
class HostFingerprintService
|
||||
{
|
||||
/**
|
||||
* Guess vendor based on open ports and hostname.
|
||||
*/
|
||||
public function guessVendor(array $openPorts, ?string $hostname): ?string
|
||||
{
|
||||
$portVendorMap = [
|
||||
9100 => 'HP/Printer',
|
||||
631 => 'CUPS/Apple',
|
||||
548 => 'Apple/AFP',
|
||||
445 => 'Microsoft/SMB',
|
||||
22 => null, // Too common
|
||||
];
|
||||
|
||||
foreach ($portVendorMap as $port => $vendor) {
|
||||
if ($vendor !== null && in_array($port, $openPorts, true)) {
|
||||
return $vendor;
|
||||
}
|
||||
}
|
||||
|
||||
if ($hostname !== null) {
|
||||
$hostname = strtolower($hostname);
|
||||
if (str_contains($hostname, 'apple') || str_contains($hostname, 'macbook') || str_contains($hostname, 'iphone')) {
|
||||
return 'Apple';
|
||||
}
|
||||
if (str_contains($hostname, 'synology') || str_contains($hostname, 'nas')) {
|
||||
return 'Synology';
|
||||
}
|
||||
if (str_contains($hostname, 'router') || str_contains($hostname, 'gateway')) {
|
||||
return 'Router/Network';
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate confidence score 0-100 based on found data.
|
||||
*/
|
||||
public function calculateConfidence(int $openPortCount, bool $hasHostname): int
|
||||
{
|
||||
$score = 10; // Base: host is alive
|
||||
|
||||
if ($openPortCount > 0) {
|
||||
$score += min(40, $openPortCount * 5);
|
||||
}
|
||||
|
||||
if ($hasHostname) {
|
||||
$score += 30;
|
||||
}
|
||||
|
||||
return min(100, $score);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Services\Discovery;
|
||||
|
||||
class PingScanner
|
||||
{
|
||||
/**
|
||||
* Check if host is alive via ICMP ping.
|
||||
* Requires ping command in container.
|
||||
*/
|
||||
public function ping(string $ip, int $timeoutMs = 500): bool
|
||||
{
|
||||
// Use ping with timeout (Linux: -W in seconds)
|
||||
$timeoutSec = max(1, (int)ceil($timeoutMs / 1000));
|
||||
$output = [];
|
||||
$exitCode = 0;
|
||||
exec(sprintf('ping -c 1 -W %d %s 2>/dev/null', $timeoutSec, escapeshellarg($ip)), $output, $exitCode);
|
||||
return $exitCode === 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Services\Discovery;
|
||||
|
||||
class TcpPortScanner
|
||||
{
|
||||
/**
|
||||
* Scan a list of TCP ports on a host.
|
||||
*
|
||||
* @param int[] $ports
|
||||
* @param int $timeoutMs connection timeout per port
|
||||
* @param int $maxConcurrency max parallel connections
|
||||
* @return int[] list of open ports
|
||||
*/
|
||||
public function scan(string $ip, array $ports, int $timeoutMs = 200, int $maxConcurrency = 50): array
|
||||
{
|
||||
$openPorts = [];
|
||||
$timeoutSec = $timeoutMs / 1000;
|
||||
|
||||
if (function_exists('curl_multi_init')) {
|
||||
// Parallel scan with curl_multi
|
||||
$chunks = array_chunk($ports, $maxConcurrency);
|
||||
foreach ($chunks as $chunk) {
|
||||
$multiHandle = curl_multi_init();
|
||||
$handles = [];
|
||||
foreach ($chunk as $port) {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, "http://{$ip}:{$port}");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $timeoutMs);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT_MS, $timeoutMs);
|
||||
curl_setopt($ch, CURLOPT_NOBODY, true);
|
||||
curl_multi_add_handle($multiHandle, $ch);
|
||||
$handles[$port] = $ch;
|
||||
}
|
||||
|
||||
// Execute
|
||||
$running = null;
|
||||
do {
|
||||
curl_multi_exec($multiHandle, $running);
|
||||
curl_multi_select($multiHandle, 0.1);
|
||||
} while ($running > 0);
|
||||
|
||||
// Check results
|
||||
foreach ($handles as $port => $ch) {
|
||||
$err = curl_errno($ch);
|
||||
// CURLE_COULDNT_CONNECT (7) = port closed, others might mean open or filtered
|
||||
if ($err !== CURLE_COULDNT_CONNECT) {
|
||||
$openPorts[] = $port;
|
||||
}
|
||||
curl_multi_remove_handle($multiHandle, $ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
|
||||
curl_multi_close($multiHandle);
|
||||
}
|
||||
} else {
|
||||
// Sequential fallback
|
||||
foreach ($ports as $port) {
|
||||
$fp = @fsockopen($ip, $port, $errno, $errstr, $timeoutSec);
|
||||
if ($fp !== false) {
|
||||
$openPorts[] = $port;
|
||||
fclose($fp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort($openPorts);
|
||||
return $openPorts;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user