25 KiB
Self-hosted система инвентаризации домашней/малой инфраструктуры с автосканированием сети, глубоким сканированием хостов и созданием карточек устройств/сервисов из найденного.
Ключевая цепочка:
скан сети→ найденные хосты→ подтверждение человеком→ карточки устройств→ добавление доступов→ глубокий скан хоста→ найденные сервисы/контейнеры/домены/бэкапы→ подтверждение человеком→ живая карта инфраструктуры
И важная поправка под Hermes/owl-alpha: не давать кодеру “построй весь Домовой” одним заходом. Лучше давать стадии по 1–2 экрана/модуля за раз, иначе он может нагенерировать архитектурной каши.
1. Базовый стек
Я бы оставил стек максимально простой и твой:
Backend:
PHP 8.3+
Framework: Slim Framework 4
DB: MariaDB / MySQL
Frontend: Bootstrap 5.3 + htmx + Alpine.js при необходимости
Templates: PHP templates или Twig
Auth: локальная авторизация
Deploy: Docker Compose
Scanning: PHP services + системные read-only команды
SSH: phpseclib
Encryption: defuse/php-encryption или libsodium
Slim 4 ставится через Composer как slim/slim:"4.*", а Bootstrap сейчас живёт в ветке 5.3, у которой официальная документация ведёт current major release v5.x и последнюю ветку 5.3.x. Для SSH из PHP нормально подходит phpseclib 3.x, он ставится через Composer как phpseclib/phpseclib:~3.0. Для шифрования секретов можно взять defuse/php-encryption, это PHP-библиотека для шифрования данных ключом/паролем.
2. Composer-зависимости
Минимальный набор:
composer require slim/slim:"4.*"
composer require slim/psr7
composer require php-di/php-di
composer require monolog/monolog
composer require vlucas/phpdotenv
composer require ramsey/uuid
composer require symfony/process
composer require phpseclib/phpseclib:"~3.0"
composer require defuse/php-encryption
Миграции и тесты:
composer require robmorgan/phinx
composer require --dev phpunit/phpunit
Опционально:
composer require twig/twig
composer require nesbot/carbon
Я бы не тащил ORM на первом этапе. Обычный PDO + Repository-классы. Так проще, прозрачнее и меньше магии.
3. Системные зависимости на хосте
Для контейнера/хоста, где крутится Домовой:
php-cli
php-fpm
php-mysql
php-curl
php-mbstring
php-xml
php-zip
mariadb-client
iproute2
iputils-ping
arp-sca
nnmap
net-tools
dnsutils
avahi-utils
smbclient / nbtscan опционально
snmp опционально
openssh-client
openssl
docker-cli опционально
Но важно: nmap и arp-scan — необязательные backends, а не обязательная основа. Первый MVP может жить на:
ping sweep
ARP table
TCP connect scan
reverse DNS
А потом уже добавить nmap/arp-scan как улучшение.
4. Границы безопасности
Это надо заложить сразу, иначе проект может превратиться в мутный сетевой сканер.
Жёсткие правила MVP
1. Сканируются только явно добавленные пользователем диапазоны.
2. По умолчанию разрешены только private/local ranges:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- fd00::/8
- link-local
3. Никаких brute force.
4. Никаких exploit/probe.
5. Никаких попыток подобрать пароли.
6. Deep scan только после явного добавления доступа.
7. Все команды deep scan — read-only.
8. Секреты в UI никогда не показываются открытым текстом.
9. Любой найденный объект сначала попадает в "Найденное", а не сразу в основной инвентарь.
10. Всё, что делает сканер, логируется.
Идеология:
Домовой не чинит и не ломает.
Домовой смотрит, запоминает и предлагает.
Архитектура выполнения сканирования
HTTP-запрос НЕ должен выполнять сканирование напрямую. Сканирование сети и deep scan выполняются отдельным CLI worker-ом:
- Web UI создаёт scan_job со статусом "pending".
- CLI worker (bin/run-scan-worker.php) забирает pending-задачи из базы и выполняет их.
- Web UI показывает статус задачи через htmx polling.
Worker должен:
- обрабатывать SIGTERM для graceful shutdown;
- запускать только одну задачу за раз (для MVP);
- обновлять статус scan_jobs в процессе;
- логировать прогресс.
Аргументы запуска worker-а:
php bin/run-scan-worker.php # забрать одну pending-задачу и выполнить
php bin/run-scan-worker.php --loop # бесконечный цикл обработки
Стратегии сканирования сети (по приоритету):
Приоритет 1 — если доступны arp-scan или nmap:
- arp-scan для быстрого обнаружения MAC + IP
- nmap для диапазонного скана с портами
Приоритит 2 — системные команды, работающие без дополнительных PAK-тов:
- ip neigh / arp table для ARP-записей
- ping sweep с жёстким timeout (200ms)
- TCP connect scan с timeout=200ms, concurrency=50
Приоритет 3 — fallback:
- только ping sweep, если ничего другого не доступно
Порядок имеет значение — быстрые методы сначала, медленные как fallback. Для /24 сети с 18 портами TCP-скан может занять минуты. Поэтому CLI worker обязателен, а PHP-скан внутри HTTP-запроса запрещён.
5. Архитектура проекта
domovoy/
app/
Controllers/
AuthController.php
DashboardController.php
DiscoveryController.php
DeviceController.php
CredentialController.php
HostScanController.php
ServiceController.php
DocumentController.php
Services/
Discovery/
NetworkScanner.php
PingScanner.php
TcpPortScanner.php
ArpTableReader.php
MdnsScanner.php
HostFingerprintService.php
HostScan/
SshClientFactory.php
LinuxHostScanner.php
DockerScanner.php
NginxScanner.php
CronScanner.php
SystemdScanner.php
BackupHintScanner.php
Inventory/
DeviceService.php
ServiceInventoryService.php
MergeSuggestionService.php
RelationService.php
Security/
CredentialVault.php
CommandWhitelist.php
SecretMasker.php
Jobs/
ScanJobRunner.php
JobQueue.php
Analysis/
RiskAnalyzer.php
DiffAnalyzer.php
Repositories/
UserRepository.php
ScanJobRepository.php
DiscoveredHostRepository.php
DeviceRepository.php
CredentialRepository.php
HostScanRepository.php
ServiceRepository.php
RelationRepository.php
AuditLogRepository.php
Middleware/
AuthMiddleware.php
CsrfMiddleware.php
public/
index.php
assets/
css/
js/
templates/
layout.php
auth/
dashboard/
discovery/
devices/
credentials/
services/
documents/
migrations/
storage/
logs/
scans/
reports/
bin/
console
run-scan-worker.php
docker/
docker-compose.yml
composer.json
.env.example
README.md
6. Главные сущности БД
users
id
username
password_hash
created_at
updated_at
network_ranges
id
name
cidr
enabled
created_at
updated_at
Пример:
Home LAN — 192.168.1.0/24
WireGuard LAN — 10.12.1.0/24
scan_jobs
id
type
status
started_at
finished_at
error_message
created_by
created_at
Типы:
network_discovery
host_deep_scan
docker_scan
nginx_scan
cron_scan
Статусы:
pending
running
done
failed
cancelled
discovered_hosts
Это всё, что нашёл сканер, но пользователь ещё не подтвердил.
id
scan_job_id
ip_address
mac_address
hostname
vendor
detected_os
open_ports_json
protocols_json
fingerprint_json
confidence
status
matched_device_id
first_seen
last_seen
created_at
updated_at
Статусы:
new
accepted
merged
ignored
ignored_always
devices
Это уже подтверждённые железки/виртуалки.
id
name
type
description
primary_ip
mac_address
hostname
vendor
os_name
os_version
location
importance
status
created_at
updated_at
Типы:
server
router
nas
desktop
laptop
phone
printer
iot
vm
container_host
unknown
credentials
Доступы к устройствам.
id
device_id
type
name
username
port
auth_method
encrypted_secret
encrypted_private_key
public_key_fingerprint
last_test_status
last_test_at
created_at
updated_at
Типы:
ssh
snmp
http_api
routeros
manual
Для MVP реально нужен только ssh.
host_scans
Результаты глубокого скана устройства.
id
device_id
credential_id
scan_job_id
status
raw_json_path
summary_json
started_at
finished_at
error_message
created_at
detected_services
То, что найдено на хосте, но ещё не подтверждено как сервис.
id
host_scan_id
device_id
kind
name
source
port
protocol
raw_json
suggested_service_id
status
created_at
updated_at
kind:
open_port
systemd_unit
docker_container
nginx_vhost
cron_job
backup_hint
database_hint
services
Подтверждённые сервисы.
id
name
type
description
device_id
status
importance
url
main_port
created_at
updated_at
Типы:
web_app
database
reverse_proxy
backup
monitoring
storage
git
media
system
unknown
service_endpoints
id
service_id
protocol
host
port
url
is_public
created_at
updated_at
domains
id
domain
target_service_id
target_device_id
source
notes
created_at
updated_at
relations
Универсальный граф связей.
id
from_type
from_id
to_type
to_id
relation_type
created_at
Типы связей:
runs_on
depends_on
proxied_by
uses_database
uses_volume
backed_up_by
exposes
resolves_to
documents
id
entity_type
entity_id
title
body_markdown
created_at
updated_at
Для заметок/runbook-ов.
audit_log
id
user_id
action
entity_type
entity_id
details_json
created_at
7. Логика сканирования сети
Что делает network discovery
На входе:
CIDR: 192.168.1.0/24
На выходе:
список DiscoveredHost
Методы первого MVP:
1. Ping sweep
2. TCP connect scan по базовым портам
3. чтение локальной ARP-таблицы
4. reverse DNS lookup
5. определение vendor по MAC OUI, если MAC известен
Базовые порты:
22 SSH
23 Telnet
53 DNS
80 HTTP
443 HTTPS
445 SMB
548 AFP
631 CUPS
3306 MySQL
5432 PostgreSQL
6379 Redis
8000 HTTP-alt
8080 HTTP-alt
8443 HTTPS-alt
9000 Portainer/various
9090 Prometheus/various
9100 Printer/Node exporter
Позже добавить:
mDNS
NetBIOS
SNMP
nmap backend
router DHCP leases importer
8. Логика deep scan по SSH
После того как пользователь создал устройство и добавил SSH-доступ, можно нажать:
[Тест подключения]
[Глубокий скан]
Команды Linux scan
Только read-only:
hostname
hostnamectl
uname -a
cat /etc/os-release
ip -j addr
ip route
ss -tulpen
df -h
lsblk -J
mount
systemctl list-units --type=service --all --no-pager
systemctl list-timers --all --no-pager
crontab -l
cat /etc/crontab
ls -la /etc/cron.d /etc/cron.daily /etc/cron.hourly /etc/cron.weekly /etc/cron.monthly
docker ps --format '{{json .}}'
docker network ls --format '{{json .}}'
docker volume ls --format '{{json .}}'
docker compose ls --format json
Команды должны идти через whitelist, а не через произвольный shell.
То есть не так:
$ssh->exec($userCommand);
А так:
$scanner->runAllowedCommand('linux.hostnamectl');
И уже внутри:
'linux.hostnamectl' => ['hostnamectl']
Формат команд в CommandWhitelist — массивы аргументов, НЕ строки. Это важно: docker-команды содержат {{json .}}, что ломает и Symfony Process, и Twig-шаблонизатор. Массив аргументов не требует экранирования.
ЗАПРЕЩЕНО:
'linux.docker_ps' => "docker ps --format '{{json .}}'"
ПРАВИЛЬНО:
'linux.docker_ps' => ['docker', 'ps', '--format', '{{json .}}']
SSH таймауты
Все SSH-операции должны иметь жёсткие таймауты. Конфигурируются через .env:
SSH_CONNECT_TIMEOUT_SECONDS=5
SSH_AUTH_TIMEOUT_SECONDS=10
SSH_COMMAND_TIMEOUT_SECONDS=8
SSH_TOTAL_SCAN_TIMEOUT_SECONDS=60
SSH_RETRY_COUNT=0
Для MVP retry отключён — лучше быстро и честно упасть, чем зависнуть. Каждый collector внутри deep scan обрабатывает timeout отдельно — ошибка одного collector-а не роняет весь scan.
9. Сервисные сканеры
DockerScanner
Собирает:
containers
images
ports
mounts
volumes
networks
labels
restart policy
health status
compose project
Из контейнеров создаёт detected_services.
Пример:
container: nextcloud-app
image: nextcloud:apache
ports: 8080:80
mounts: /srv/nextcloud/data:/var/www/html/data
suggested service type: web_app
NginxScanner
Читает:
/etc/nginx/nginx.conf
/etc/nginx/sites-enabled/*
/etc/nginx/conf.d/*
Вынимает:
server_name
listen
proxy_pass
root
ssl_certificate
ssl_certificate_key path, но ключ не читать
access_log
error_log
Важно: приватные ключи сертификатов не читать.
CronScanner
Читает:
user crontab
/etc/crontab
/etc/cron.d/*
И создаёт кандидатов:
cron_job
backup_hint
maintenance_task
unknown_task
BackupHintScanner
Ищет в командах/скриптах признаки:
rsync
borg
restic
rclone
tar
zip
mysqldump
mariadb-dump
pg_dump
sqlite dump
docker exec ... dump
scp
sftp
Но вывод должен быть осторожным:
Похоже на backup job
а не:
Это точно полноценный бэкап
10. Экранная структура
Dashboard
Домовой
Устройства: 12
Сервисы: 18
Новые находки: 7
Требуют внимания: 4
Последний скан сети: сегодня 14:22
Блоки:
- Новые найденные устройства
- Сервисы без подтверждённого бэкапа
- Хосты без свежего скана
- Истекающие сертификаты
- Последние изменения
Discovery
Сканирование сети
[Добавить диапазон]
[Запустить скан]
Таблица:
IP | Hostname | MAC | Vendor | Ports | Статус | Действия
Действия:
[Создать устройство]
[Объединить]
[Игнорировать]
[Детали]
Devices
Карточка устройства:
Название
Тип
IP
MAC
Hostname
Vendor
OS
Роль
Важность
Заметки
Доступы
Найденные сервисы
Подтверждённые сервисы
Документы
История
Credentials
Для SSH:
Название доступа
Host
Port
Username
Auth method:
- password
- private key
Password/private key
[Тест]
[Сохранить]
Host Scan
На карточке устройства:
[Глубокий скан]
Результаты:
- Система
- Открытые порты
- Docker
- Systemd
- Cron
- Nginx
- Возможные сервисы
- Возможные бэкапы
Найденные сервисы
Название | Тип | Источник | Устройство | Порт | Уверенность | Действия
Действия:
[Создать сервис]
[Объединить]
[Игнорировать]
[Детали]
11. CI/CD и smoke tests
Создать файл scripts/check.sh:
#!/usr/bin/env bash
set -euo pipefail
echo "=== composer validate ==="
composer validate --no-check-publish
echo "=== composer install ==="
composer install --no-interaction --prefer-dist
echo "=== PHP syntax check ==="
find app public bin -name "*.php" -print0 | xargs -0 -n1 php -l
echo "=== docker compose config ==="
docker compose config > /dev/null
echo "=== docker compose up ==="
docker compose up -d --build
echo "=== phinx migrate (test DB) ==="
docker compose exec app php vendor/bin/phinx migrate
echo "=== HTTP health check ==="
curl -sI http://localhost:8080/login | head -1
echo "=== ALL CHECKS PASSED ==="
Головное правило: итерация не считается завершённой, пока check.sh не прошёл успешно без ошибок.
Для следующих итераций добавлять свои проверки:
- network scan: "docker compose exec app php bin/console scan:test"
- device CRUD: curl к /devices, /devices/create
- credentials: тест SSH mock-connection
Не переходить к следующей итерации без успешного запуска check.sh. Обязательно показывать реальный вывод, а не утверждать "готово".
12. План реализации
Этап 0. Каркас проекта
Цель: приложение открывается, есть логин, база, миграции, layout.
Результат:
- docker-compose поднимает app + db
- есть /login
- есть /dashboard
- есть миграции
- есть базовый UI
Этап 1. Инвентарь устройств вручную
Цель: можно создавать карточки устройств руками.
Результат:
- CRUD устройств
- типы устройств
- заметки
- список устройств
- карточка устройства
Этап 2. Сканирование сети
Цель: можно добавить диапазон и найти хосты.
Результат:
- CRUD network_ranges
- запуск network scan
- таблица discovered_hosts
- ping/tcp scan
- ARP table enrichment
- reverse DNS
Этап 3. Превращение найденного в устройство
Цель: найденный хост можно принять в инвентарь.
Результат:
- кнопка "Создать устройство"
- кнопка "Игнорировать"
- простое объединение с существующим устройством
- discovered_host получает статус
Этап 4. SSH-доступы
Цель: к устройству можно добавить SSH-доступ и проверить его.
Результат:
- CRUD credentials
- шифрование секрета
- тест подключения
- сохранение результата теста
Этап 5. Deep scan Linux-хоста
Цель: по SSH собрать базовую информацию.
Результат:
- hostname/os/ip/ports/disk/systemd/timers/cron
- сохранение raw scan JSON
- отображение summary
Этап 6. Docker scan
Цель: найти контейнеры и предложить сервисы.
Результат:
- docker ps
- docker inspect для контейнеров
- volumes/networks/ports
- detected_services kind=docker_container
Этап 7. Nginx scan
Цель: найти домены и proxy_pass.
Результат:
- чтение nginx-конфигов
- парсинг server_name/listen/proxy_pass
- detected_services kind=nginx_vhost
- domains/proxy suggestions
Этап 8. Создание сервисов из найденного
Цель: пользователь подтверждает найденный сервис.
Результат:
- service proposals
- create service from detected_service
- relation Device → Service
- endpoint creation
Этап 9. Cron/backup hints
Цель: находить задачи, похожие на бэкапы.
Результат:
- cron parser
- systemd timer parser
- backup keyword detection
- detected_services kind=backup_hint
Этап 10. Риски
Цель: простые предупреждения.
Результат:
- сервис без backup_hint
- публичный web endpoint без документации
- SSH открыт
- нет свежего deep scan
- TLS скоро истекает, если cert найден
Этап 11. История и diff
Цель: видеть, что изменилось между сканами.
Результат:
- история scan jobs
- сравнение open ports
- новые/пропавшие хосты
- новые/пропавшие контейнеры
13. Как давать задание Hermes/owl-alpha
Я бы не давал ему “весь проект” целиком. Лучше так:
1. Сначала дать общее ТЗ и попросить создать каркас.
2. Потом отдельной задачей миграции.
3. Потом отдельной задачей CRUD устройств.
4. Потом отдельной задачей network scan.
5. Потом отдельной задачей SSH credentials.
Для owl-alpha особенно важно:
- запрещать переписывать уже готовое без причины;
- требовать маленькие коммиты;
- требовать список изменённых файлов;
- требовать инструкции запуска;
- требовать self-check;
- не разрешать выдумывать несуществующие зависимости;
- не разрешать делать произвольный shell executor;
- не разрешать писать "чистый JS" без обоснования;
- запрещать строковые шаблоны для docker-команд;
- требовать запуск scripts/check.sh после каждой итерации;
- требовать показать реальный вывод check.sh, а не утверждать "готово".
Головное правило: итерация не считается завершённой, пока check.sh не прошёл успешно. Без smoke test — нет завершения.