Итерация 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,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Controllers;
|
||||
|
||||
use Domovoy\Models\NetworkRange;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class DiscoveryController
|
||||
{
|
||||
private \Domovoy\Repositories\NetworkRangeRepository $networkRangeRepository;
|
||||
private \Domovoy\Repositories\ScanJobRepository $scanJobRepository;
|
||||
private \Domovoy\Repositories\DiscoveredHostRepository $discoveredHostRepository;
|
||||
|
||||
public function __construct(
|
||||
\Domovoy\Repositories\NetworkRangeRepository $networkRangeRepository,
|
||||
\Domovoy\Repositories\ScanJobRepository $scanJobRepository,
|
||||
\Domovoy\Repositories\DiscoveredHostRepository $discoveredHostRepository
|
||||
) {
|
||||
$this->networkRangeRepository = $networkRangeRepository;
|
||||
$this->scanJobRepository = $scanJobRepository;
|
||||
$this->discoveredHostRepository = $discoveredHostRepository;
|
||||
}
|
||||
|
||||
public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
ob_start();
|
||||
$username = $_SESSION['username'] ?? 'User';
|
||||
$ranges = $this->networkRangeRepository->findAll();
|
||||
$scanJobs = $this->scanJobRepository->findRecent(10);
|
||||
$newHosts = $this->discoveredHostRepository->findByStatus('new', 20);
|
||||
require dirname(__DIR__, 2) . '/templates/discovery/index.php';
|
||||
$body = ob_get_clean();
|
||||
$response->getBody()->write($body);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function startScan(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$rangeId = $data['range_id'] ?? null;
|
||||
|
||||
// Create scan job(s)
|
||||
if ($rangeId !== null) {
|
||||
$range = $this->networkRangeRepository->findById((int)$rangeId);
|
||||
if ($range === null || !$range->enabled) {
|
||||
return $response->withStatus(404)->write('Range not found');
|
||||
}
|
||||
$this->createScanJob('network_discovery', $range->id);
|
||||
} else {
|
||||
// Scan all enabled ranges
|
||||
$ranges = $this->networkRangeRepository->findEnabled();
|
||||
foreach ($ranges as $range) {
|
||||
$this->createScanJob('network_discovery', $range->id);
|
||||
}
|
||||
}
|
||||
|
||||
return $response
|
||||
->withHeader('Location', '/discovery')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
public function ignoreHost(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$hostId = $data['host_id'] ?? null;
|
||||
|
||||
if ($hostId !== null) {
|
||||
$host = $this->discoveredHostRepository->findById((int)$hostId);
|
||||
if ($host !== null) {
|
||||
$host->status = 'ignored';
|
||||
$this->discoveredHostRepository->save($host);
|
||||
}
|
||||
}
|
||||
|
||||
return $response
|
||||
->withHeader('Location', '/discovery')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
private function createScanJob(string $type, int $rangeId): void
|
||||
{
|
||||
$job = new \Domovoy\Models\ScanJob();
|
||||
$job->type = $type;
|
||||
$job->status = 'pending';
|
||||
$job->networkRangeId = $rangeId;
|
||||
$job->createdBy = $_SESSION['user_id'] ?? null;
|
||||
$this->scanJobRepository->save($job);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Domovoy\Controllers;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class NetworkRangeController
|
||||
{
|
||||
private \Domovoy\Repositories\NetworkRangeRepository $repository;
|
||||
|
||||
public function __construct(\Domovoy\Repositories\NetworkRangeRepository $repository)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$name = trim($data['name'] ?? '');
|
||||
$cidr = trim($data['cidr'] ?? '');
|
||||
|
||||
if ($name === '' || $cidr === '') {
|
||||
$response->getBody()->write('Name and CIDR are required');
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
// Basic CIDR validation
|
||||
if (!$this->isValidCidr($cidr)) {
|
||||
$response->getBody()->write('Invalid CIDR format');
|
||||
return $response->withStatus(400);
|
||||
}
|
||||
|
||||
$range = new \Domovoy\Models\NetworkRange();
|
||||
$range->name = $name;
|
||||
$range->cidr = $cidr;
|
||||
$range->enabled = true;
|
||||
$this->repository->save($range);
|
||||
|
||||
return $response
|
||||
->withHeader('Location', '/discovery')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
public function toggle(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$id = $data['id'] ?? null;
|
||||
|
||||
if ($id !== null) {
|
||||
$range = $this->repository->findById((int)$id);
|
||||
if ($range !== null) {
|
||||
$range->enabled = !$range->enabled;
|
||||
$this->repository->save($range);
|
||||
}
|
||||
}
|
||||
|
||||
return $response
|
||||
->withHeader('Location', '/discovery')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
public function delete(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$data = $request->getParsedBody();
|
||||
$id = $data['id'] ?? null;
|
||||
|
||||
if ($id !== null) {
|
||||
$this->repository->delete((int)$id);
|
||||
}
|
||||
|
||||
return $response
|
||||
->withHeader('Location', '/discovery')
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
private function isValidCidr(string $cidr): bool
|
||||
{
|
||||
if (!str_contains($cidr, '/')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
[$ip, $mask] = explode('/', $cidr, 2);
|
||||
|
||||
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$maskInt = (int)$mask;
|
||||
return $maskInt >= 8 && $maskInt <= 32;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user