diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index 4204f09..97f239c 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -9,10 +9,24 @@ use Psr\Http\Message\ServerRequestInterface; class DashboardController { + private \Domovoy\Repositories\DeviceRepository $deviceRepository; + private \Domovoy\Repositories\ScanJobRepository $scanJobRepository; + + public function __construct( + \Domovoy\Repositories\DeviceRepository $deviceRepository, + \Domovoy\Repositories\ScanJobRepository $scanJobRepository + ) { + $this->deviceRepository = $deviceRepository; + $this->scanJobRepository = $scanJobRepository; + } + public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { ob_start(); $username = $_SESSION['username'] ?? 'User'; + $deviceCount = $this->deviceRepository->getCount(); + $newDiscoveries = $this->deviceRepository->getNewDiscoveriesCount(); + $recentScans = $this->scanJobRepository->findRecent(5); require dirname(__DIR__, 2) . '/templates/dashboard/index.php'; $body = ob_get_clean(); $response->getBody()->write($body); diff --git a/app/Controllers/DeviceController.php b/app/Controllers/DeviceController.php new file mode 100644 index 0000000..4284f97 --- /dev/null +++ b/app/Controllers/DeviceController.php @@ -0,0 +1,182 @@ +deviceService = $deviceService; + $this->discoveredHostRepository = $discoveredHostRepository; + } + + public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + ob_start(); + $username = $_SESSION['username'] ?? 'User'; + $devices = $this->deviceService->getAllDevices(); + require dirname(__DIR__, 2) . '/templates/devices/index.php'; + $body = ob_get_clean(); + $response->getBody()->write($body); + return $response; + } + + public function createForm(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + ob_start(); + $username = $_SESSION['username'] ?? 'User'; + $device = new Device(); + $types = Device::$types; + $importances = Device::$importances; + require dirname(__DIR__, 2) . '/templates/devices/form.php'; + $body = ob_get_clean(); + $response->getBody()->write($body); + return $response; + } + + public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $data = $request->getParsedBody(); + + $device = new Device(); + $device->name = trim($data['name'] ?? ''); + $device->type = $data['type'] ?? 'unknown'; + $device->description = trim($data['description'] ?? '') ?: null; + $device->primaryIp = trim($data['primary_ip'] ?? '') ?: null; + $device->macAddress = trim($data['mac_address'] ?? '') ?: null; + $device->hostname = trim($data['hostname'] ?? '') ?: null; + $device->vendor = trim($data['vendor'] ?? '') ?: null; + $device->osName = trim($data['os_name'] ?? '') ?: null; + $device->osVersion = trim($data['os_version'] ?? '') ?: null; + $device->location = trim($data['location'] ?? '') ?: null; + $device->importance = $data['importance'] ?? 'normal'; + + $this->deviceService->updateDevice($device); + + return $response + ->withHeader('Location', '/devices') + ->withStatus(302); + } + + public function show(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface + { + $id = (int)$args['id']; + $device = $this->deviceService->getDevice($id); + + if ($device === null) { + return $response->withStatus(404)->write('Device not found'); + } + + ob_start(); + $username = $_SESSION['username'] ?? 'User'; + $types = Device::$types; + $importances = Device::$importances; + require dirname(__DIR__, 2) . '/templates/devices/show.php'; + $body = ob_get_clean(); + $response->getBody()->write($body); + return $response; + } + + public function editForm(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface + { + $id = (int)$args['id']; + $device = $this->deviceService->getDevice($id); + + if ($device === null) { + return $response->withStatus(404)->write('Device not found'); + } + + ob_start(); + $username = $_SESSION['username'] ?? 'User'; + $types = Device::$types; + $importances = Device::$importances; + require dirname(__DIR__, 2) . '/templates/devices/form.php'; + $body = ob_get_clean(); + $response->getBody()->write($body); + return $response; + } + + public function update(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface + { + $id = (int)$args['id']; + $device = $this->deviceService->getDevice($id); + + if ($device === null) { + return $response->withStatus(404)->write('Device not found'); + } + + $data = $request->getParsedBody(); + $device->name = trim($data['name'] ?? ''); + $device->type = $data['type'] ?? 'unknown'; + $device->description = trim($data['description'] ?? '') ?: null; + $device->primaryIp = trim($data['primary_ip'] ?? '') ?: null; + $device->macAddress = trim($data['mac_address'] ?? '') ?: null; + $device->hostname = trim($data['hostname'] ?? '') ?: null; + $device->vendor = trim($data['vendor'] ?? '') ?: null; + $device->osName = trim($data['os_name'] ?? '') ?: null; + $device->osVersion = trim($data['os_version'] ?? '') ?: null; + $device->location = trim($data['location'] ?? '') ?: null; + $device->importance = $data['importance'] ?? 'normal'; + $device->status = $data['status'] ?? 'active'; + + $this->deviceService->updateDevice($device); + + return $response + ->withHeader('Location', '/devices/' . $device->id) + ->withStatus(302); + } + + public function delete(ServerRequestInterface $request, ResponseInterface $response, array $args): ResponseInterface + { + $id = (int)$args['id']; + $this->deviceService->deleteDevice($id); + + return $response + ->withHeader('Location', '/devices') + ->withStatus(302); + } + + public function createFromHost(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $data = $request->getParsedBody(); + $hostId = $data['host_id'] ?? null; + $name = trim($data['name'] ?? ''); + + if ($hostId === null || $name === '') { + return $response->withStatus(400)->write('Host ID and name required'); + } + + $host = $this->discoveredHostRepository->findById((int)$hostId); + if ($host === null) { + return $response->withStatus(404)->write('Host not found'); + } + + $device = $this->deviceService->createFromDiscoveredHost( + $name, + $host->ipAddress, + $host->macAddress, + $host->hostname, + $host->vendor + ); + + // Mark host as accepted + $host->status = 'accepted'; + $host->matchedDeviceId = (string)$device->id; + $this->discoveredHostRepository->save($host); + + return $response + ->withHeader('Location', '/devices/' . $device->id) + ->withStatus(302); + } +} diff --git a/app/Models/Device.php b/app/Models/Device.php new file mode 100644 index 0000000..2ef1385 --- /dev/null +++ b/app/Models/Device.php @@ -0,0 +1,52 @@ +id = (int)$data['id']; + $obj->name = $data['name']; + $obj->type = $data['type']; + $obj->description = $data['description'] ?? null; + $obj->primaryIp = $data['primary_ip'] ?? null; + $obj->macAddress = $data['mac_address'] ?? null; + $obj->hostname = $data['hostname'] ?? null; + $obj->vendor = $data['vendor'] ?? null; + $obj->osName = $data['os_name'] ?? null; + $obj->osVersion = $data['os_version'] ?? null; + $obj->location = $data['location'] ?? null; + $obj->importance = $data['importance']; + $obj->status = $data['status']; + $obj->createdAt = new \DateTimeImmutable($data['created_at']); + $obj->updatedAt = new \DateTimeImmutable($data['updated_at']); + return $obj; + } + + public static array $types = [ + 'server', 'router', 'nas', 'desktop', 'laptop', + 'phone', 'printer', 'iot', 'vm', 'container_host', 'unknown', + ]; + + public static array $importances = ['critical', 'high', 'normal', 'low']; +} diff --git a/app/Repositories/DeviceRepository.php b/app/Repositories/DeviceRepository.php new file mode 100644 index 0000000..34a3043 --- /dev/null +++ b/app/Repositories/DeviceRepository.php @@ -0,0 +1,125 @@ +pdo = $pdo; + } + + public function findById(int $id): ?Device + { + $stmt = $this->pdo->prepare('SELECT * FROM devices WHERE id = :id'); + $stmt->execute(['id' => $id]); + $row = $stmt->fetch(); + return $row ? Device::fromArray($row) : null; + } + + public function findAll(string $sortBy = 'name'): array + { + $allowed = ['name', 'type', 'status', 'primary_ip', 'created_at']; + $sort = in_array($sortBy, $allowed, true) ? $sortBy : 'name'; + $stmt = $this->pdo->query("SELECT * FROM devices ORDER BY {$sort} ASC"); + $results = []; + while ($row = $stmt->fetch()) { + $results[] = Device::fromArray($row); + } + return $results; + } + + public function findByMac(string $macAddress): ?Device + { + $stmt = $this->pdo->prepare('SELECT * FROM devices WHERE mac_address = :mac'); + $stmt->execute(['mac' => strtolower($macAddress)]); + $row = $stmt->fetch(); + return $row ? Device::fromArray($row) : null; + } + + public function findByName(string $name): ?Device + { + $stmt = $this->pdo->prepare('SELECT * FROM devices WHERE name = :name'); + $stmt->execute(['name' => $name]); + $row = $stmt->fetch(); + return $row ? Device::fromArray($row) : null; + } + + public function getCount(): int + { + return (int)$this->pdo->query('SELECT COUNT(*) FROM devices')->fetchColumn(); + } + + public function getNewDiscoveriesCount(): int + { + $stmt = $this->pdo->query("SELECT COUNT(*) FROM discovered_hosts WHERE status = 'new'"); + return (int)$stmt->fetchColumn(); + } + + public function save(Device $device): void + { + $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s'); + if ($device->id === null) { + $stmt = $this->pdo->prepare( + 'INSERT INTO devices (name, type, description, primary_ip, mac_address, hostname, + vendor, os_name, os_version, location, importance, status, created_at, updated_at) + VALUES (:name, :type, :description, :primary_ip, :mac_address, :hostname, + :vendor, :os_name, :os_version, :location, :importance, :status, :created_at, :updated_at)' + ); + $stmt->execute([ + 'name' => $device->name, + 'type' => $device->type, + 'description' => $device->description, + 'primary_ip' => $device->primaryIp, + 'mac_address' => $device->macAddress !== null ? strtolower($device->macAddress) : null, + 'hostname' => $device->hostname, + 'vendor' => $device->vendor, + 'os_name' => $device->osName, + 'os_version' => $device->osVersion, + 'location' => $device->location, + 'importance' => $device->importance, + 'status' => $device->status, + 'created_at' => $now, + 'updated_at' => $now, + ]); + $device->id = (int)$this->pdo->lastInsertId(); + } else { + $stmt = $this->pdo->prepare( + 'UPDATE devices SET name = :name, type = :type, description = :description, + primary_ip = :primary_ip, mac_address = :mac_address, hostname = :hostname, + vendor = :vendor, os_name = :os_name, os_version = :os_version, + location = :location, importance = :importance, status = :status, + updated_at = :updated_at WHERE id = :id' + ); + $stmt->execute([ + 'id' => $device->id, + 'name' => $device->name, + 'type' => $device->type, + 'description' => $device->description, + 'primary_ip' => $device->primaryIp, + 'mac_address' => $device->macAddress !== null ? strtolower($device->macAddress) : null, + 'hostname' => $device->hostname, + 'vendor' => $device->vendor, + 'os_name' => $device->osName, + 'os_version' => $device->osVersion, + 'location' => $device->location, + 'importance' => $device->importance, + 'status' => $device->status, + 'updated_at' => $now, + ]); + } + } + + public function delete(int $id): void + { + $stmt = $this->pdo->prepare('DELETE FROM devices WHERE id = :id'); + $stmt->execute(['id' => $id]); + } +} diff --git a/app/Services/Inventory/DeviceService.php b/app/Services/Inventory/DeviceService.php new file mode 100644 index 0000000..65ce06a --- /dev/null +++ b/app/Services/Inventory/DeviceService.php @@ -0,0 +1,59 @@ +deviceRepository = $deviceRepository; + } + + public function createFromDiscoveredHost( + string $name, + string $ipAddress, + ?string $macAddress = null, + ?string $hostname = null, + ?string $vendor = null + ): Device { + $device = new Device(); + $device->name = $name; + $device->primaryIp = $ipAddress; + $device->macAddress = $macAddress; + $device->hostname = $hostname; + $device->vendor = $vendor; + $device->type = 'unknown'; + $device->status = 'active'; + $device->importance = 'normal'; + + $this->deviceRepository->save($device); + return $device; + } + + public function getAllDevices(): array + { + return $this->deviceRepository->findAll(); + } + + public function getDevice(int $id): ?Device + { + return $this->deviceRepository->findById($id); + } + + public function updateDevice(Device $device): void + { + $this->deviceRepository->save($device); + } + + public function deleteDevice(int $id): void + { + $this->deviceRepository->delete($id); + } +} diff --git a/app/Services/Inventory/MergeSuggestionService.php b/app/Services/Inventory/MergeSuggestionService.php new file mode 100644 index 0000000..fba0ba5 --- /dev/null +++ b/app/Services/Inventory/MergeSuggestionService.php @@ -0,0 +1,58 @@ +deviceRepository = $deviceRepository; + } + + /** + * Find merge suggestions for a discovered host. + * Returns array of ['device' => Device, 'confidence' => int, 'reason' => string] + */ + public function findSuggestions(DiscoveredHost $host): array + { + $suggestions = []; + + // High confidence: MAC address match + if ($host->macAddress !== null) { + $device = $this->deviceRepository->findByMac($host->macAddress); + if ($device !== null) { + $suggestions[] = [ + 'device' => $device, + 'confidence' => 90, + 'reason' => 'MAC адрес совпадает', + ]; + } + } + + // Medium confidence: hostname match + if ($host->hostname !== null) { + $device = $this->deviceRepository->findByName($host->hostname); + if ($device !== null) { + $suggestions[] = [ + 'device' => $device, + 'confidence' => 60, + 'reason' => 'Hostname совпадает', + ]; + } + } + + // Low confidence: IP match + if ($host->primaryIp !== null) { + // Would need findByIp in repository — skip for now + } + + return $suggestions; + } +} diff --git a/db/migrations/20250526000006_create_devices.php b/db/migrations/20250526000006_create_devices.php new file mode 100644 index 0000000..3a055d2 --- /dev/null +++ b/db/migrations/20250526000006_create_devices.php @@ -0,0 +1,33 @@ +table('devices'); + $table + ->addColumn('name', 'string', ['limit' => 255, 'null' => false]) + ->addColumn('type', 'string', ['limit' => 30, 'null' => false, 'default' => 'unknown']) + ->addColumn('description', 'text', ['null' => true]) + ->addColumn('primary_ip', 'string', ['limit' => 45, 'null' => true]) + ->addColumn('mac_address', 'string', ['limit' => 17, 'null' => true]) + ->addColumn('hostname', 'string', ['limit' => 255, 'null' => true]) + ->addColumn('vendor', 'string', ['limit' => 255, 'null' => true]) + ->addColumn('os_name', 'string', ['limit' => 255, 'null' => true]) + ->addColumn('os_version', 'string', ['limit' => 255, 'null' => true]) + ->addColumn('location', 'string', ['limit' => 255, 'null' => true]) + ->addColumn('importance', 'string', ['limit' => 20, 'null' => false, 'default' => 'normal']) + ->addColumn('status', 'string', ['limit' => 20, 'null' => false, 'default' => 'active']) + ->addColumn('created_at', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) + ->addColumn('updated_at', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) + ->addIndex(['primary_ip']) + ->addIndex(['mac_address'], ['unique' => true]) + ->addIndex(['type']) + ->addIndex(['status']) + ->create(); + } +} diff --git a/public/index.php b/public/index.php index c54fd21..e8a8a0f 100644 --- a/public/index.php +++ b/public/index.php @@ -114,8 +114,11 @@ $containerBuilder->addDefinitions([ \Domovoy\Controllers\SetupController::class => function ($c) { return new \Domovoy\Controllers\SetupController($c->get(\Domovoy\Services\AuthService::class)); }, - \Domovoy\Controllers\DashboardController::class => function () { - return new \Domovoy\Controllers\DashboardController(); + \Domovoy\Controllers\DashboardController::class => function ($c) { + return new \Domovoy\Controllers\DashboardController( + $c->get(\Domovoy\Repositories\DeviceRepository::class), + $c->get(\Domovoy\Repositories\ScanJobRepository::class) + ); }, \Domovoy\Controllers\DiscoveryController::class => function ($c) { return new \Domovoy\Controllers\DiscoveryController( @@ -129,6 +132,22 @@ $containerBuilder->addDefinitions([ $c->get(\Domovoy\Repositories\NetworkRangeRepository::class) ); }, + // Inventory + \Domovoy\Repositories\DeviceRepository::class => function ($c) { + return new \Domovoy\Repositories\DeviceRepository($c->get(PDO::class)); + }, + \Domovoy\Services\Inventory\DeviceService::class => function ($c) { + return new \Domovoy\Services\Inventory\DeviceService($c->get(\Domovoy\Repositories\DeviceRepository::class)); + }, + \Domovoy\Services\Inventory\MergeSuggestionService::class => function ($c) { + return new \Domovoy\Services\Inventory\MergeSuggestionService($c->get(\Domovoy\Repositories\DeviceRepository::class)); + }, + \Domovoy\Controllers\DeviceController::class => function ($c) { + return new \Domovoy\Controllers\DeviceController( + $c->get(\Domovoy\Services\Inventory\DeviceService::class), + $c->get(\Domovoy\Repositories\DiscoveredHostRepository::class) + ); + }, ]); $container = $containerBuilder->build(); @@ -161,6 +180,16 @@ $app->group('', function (\Slim\Routing\RouteCollectorProxy $group) { $group->post('/discovery/ranges/create', [\Domovoy\Controllers\NetworkRangeController::class, 'create'])->setName('discovery.ranges.create'); $group->post('/discovery/ranges/toggle', [\Domovoy\Controllers\NetworkRangeController::class, 'toggle'])->setName('discovery.ranges.toggle'); $group->post('/discovery/ranges/delete', [\Domovoy\Controllers\NetworkRangeController::class, 'delete'])->setName('discovery.ranges.delete'); + + // Devices CRUD + $group->get('/devices', [\Domovoy\Controllers\DeviceController::class, 'index'])->setName('devices'); + $group->get('/devices/create', [\Domovoy\Controllers\DeviceController::class, 'createForm'])->setName('devices.create'); + $group->post('/devices/create', [\Domovoy\Controllers\DeviceController::class, 'create'])->setName('devices.create.post'); + $group->get('/devices/{id}', [\Domovoy\Controllers\DeviceController::class, 'show'])->setName('devices.show'); + $group->get('/devices/{id}/edit', [\Domovoy\Controllers\DeviceController::class, 'editForm'])->setName('devices.edit'); + $group->post('/devices/{id}/update', [\Domovoy\Controllers\DeviceController::class, 'update'])->setName('devices.update'); + $group->post('/devices/{id}/delete', [\Domovoy\Controllers\DeviceController::class, 'delete'])->setName('devices.delete'); + $group->post('/devices/from-host', [\Domovoy\Controllers\DeviceController::class, 'createFromHost'])->setName('devices.from_host'); }); $app->run(); diff --git a/templates/dashboard/index.php b/templates/dashboard/index.php index 47f8035..d1c35ea 100644 --- a/templates/dashboard/index.php +++ b/templates/dashboard/index.php @@ -6,15 +6,7 @@
Устройства
-

0

-
-
- -
-
-
-
Сервисы
-

0

+

@@ -22,6 +14,14 @@
Новые находки
+

+
+
+ +
+
+
+
Сервисы

0

@@ -39,33 +39,45 @@
-
- Последние найденные хосты -
+
Последние сканирования
-

Нет данных. Запустите сканирование сети.

+ +

Нет запусков сканирования.

+ + + + + + + + + + + + +
ТипСтатусКогда
type) ?> + status) { + 'pending' => 'bg-warning', 'running' => 'bg-info', + 'done' => 'bg-success', 'failed' => 'bg-danger', + default => 'bg-secondary', + }; + ?> + status) ?> + createdAt->format('m-d H:i') ?>
+
-
- Последние события -
+
Быстрые действия
- -
-
- Последний скан сети -
-
-

Сканирование ещё не запускалось.

-
-
diff --git a/templates/devices/form.php b/templates/devices/form.php new file mode 100644 index 0000000..b97b55d --- /dev/null +++ b/templates/devices/form.php @@ -0,0 +1,71 @@ + +

id === null ? 'Добавить устройство' : 'Редактировать: ' . htmlspecialchars($device->name) ?>

+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + Отмена +
+
+ + diff --git a/templates/devices/index.php b/templates/devices/index.php new file mode 100644 index 0000000..8bcc92a --- /dev/null +++ b/templates/devices/index.php @@ -0,0 +1,57 @@ + +

Устройства

+ +
+ Добавить устройство +
+ + +
Нет устройств. Добавьте первое устройство вручную или создайте из найденного хоста.
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
НазваниеТипIPMACVendorВажностьСтатусДействия
name) ?>type) ?>primaryIp ?? '-') ?>macAddress ?? '-') ?>vendor ?? '-') ?> + importance) { + 'critical' => 'bg-danger', + 'high' => 'bg-warning', + 'low' => 'bg-secondary', + default => 'bg-light text-dark', + }; + ?> + importance) ?> + + + status) ?> + + + Изменить +
+ + + diff --git a/templates/devices/show.php b/templates/devices/show.php new file mode 100644 index 0000000..b9d4b0f --- /dev/null +++ b/templates/devices/show.php @@ -0,0 +1,35 @@ + +
+

name) ?>

+
+ Изменить +
+ +
+
+
+ +
+
+ + + + + + + +
Типtype) ?>
IPprimaryIp ?? '-') ?>
MACmacAddress ?? '-') ?>
Hostnamehostname ?? '-') ?>
Vendorvendor ?? '-') ?>
OSosName ?? '') . ' ' . ($device->osVersion ?? '')) ?>
+
+
+ + + + + + + +
Важностьimportance) ?>
Статусstatus) ?>
Locationlocation ?? '-') ?>
Описаниеdescription ?? '-') ?>
СозданоcreatedAt->format('Y-m-d H:i') ?>
ОбновленоupdatedAt->format('Y-m-d H:i') ?>
+
+
+ + diff --git a/templates/discovery/index.php b/templates/discovery/index.php index 6ae8c78..0759a18 100644 --- a/templates/discovery/index.php +++ b/templates/discovery/index.php @@ -144,6 +144,11 @@ openPorts) ? '-' : implode(', ', $host->openPorts) ?> confidence ?>% +
+ + + +