26 KiB
Краткое описание
Нужно разработать self-hosted web-приложение "Домовой" для инвентаризации домашней и малой серверной инфраструктуры.
Главная идея: приложение не требует вручную заносить всю инфраструктуру с нуля. Оно умеет сканировать заданные локальные сетевые диапазоны, находить устройства, предлагать создать карточки устройств, затем после добавления доступа к устройству запускать read-only deep scan по SSH и предлагать создать карточки сервисов, контейнеров, портов, доменов, cron-задач и backup-подсказок.
Приложение должно быть безопасным, локальным и read-only в части сканирования. Оно не должно ничего менять на удалённых хостах.
Основной стек
Backend:
- PHP 8.3+
- Slim Framework 4
- PDO
- MariaDB/MySQL
Frontend:
- Bootstrap 5.3
- Vanilla JavaScript (только для тривиальных действий)
- htmx для частичных обновлений без перезагрузки страницы
- server-side rendered templates
htmx использовать для:
- обновления таблицы scan jobs;
- принятия/игнорирования найденного host;
- запуска сканирования;
- подгрузки деталей без полной перезагрузки;
- создания устройства из discovered_host.
Alpine.js — только если htmx не покрывает кейс. Никакого "чистый JS потом допишем" — весь JS должен быть заявлен заранее и обоснован.
Deploy:
- Docker Compose
Composer-зависимости:
- slim/slim:"4.*"
- slim/psr7
- php-di/php-di
- monolog/monolog
- vlucas/phpdotenv
- ramsey/uuid
- symfony/process
- phpseclib/phpseclib:"~3.0"
- defuse/php-encryption
composer require robmorgan/phinx
Dev-зависимости: composer require --dev phpunit/phpunit
Не использовать тяжёлый frontend framework. Не использовать ORM в первой версии. Не использовать произвольное выполнение shell-команд от пользователя.
Миграции выполняются через Phinx: php vendor/bin/phinx migrate php vendor/bin/phinx rollback php vendor/bin/phinx create MigrationName
Каждая миграция — отдельный класс с методами up() и down(). Файл phinx.php — точка входа для конфигурации Phinx.
Архитектура выполнения задач
HTTP-запросы НЕ должны выполнять сканирование напрямую. Правильная архитектура:
- Web UI создаёт запись в scan_jobs (status=pending).
- Отдельный CLI worker (bin/run-scan-worker.php) забирает pending-задачи и выполняет их.
- Web UI показывает статус задачи через polling (htmx).
Это означает:
- PHP не висит внутри HTTP-запроса во время сканирования.
- Время выполнения HTTP-запроса — миллисекунды.
- Worker можно запускать вручную или через cron/supervisor.
- Worker должен обрабатывать сигналы (SIGTERM) для graceful shutdown.
Важные ограничения безопасности
- Сканировать только явно добавленные пользователем диапазоны.
- По умолчанию считать допустимыми только private/local сети:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
- fd00::/8
- link-local
- Не делать brute force.
- Не делать exploit/probe.
- Не пытаться подбирать пароли.
- Deep scan запускать только после явного добавления доступа пользователем.
- Все команды deep scan должны быть read-only.
- Секреты хранить только в зашифрованном виде.
- Секреты не выводить в UI и логи.
- Любой найденный объект сначала попадает в "Найденное", а не автоматически в основной инвентарь.
- Все действия сканера логировать.
- Запрещено делать универсальный shell executor, принимающий произвольную команду из UI.
Главный пользовательский сценарий
- Пользователь устанавливает приложение через Docker Compose.
- Заходит в web UI.
- Создаёт первый локальный аккаунт.
- Добавляет сетевой диапазон, например 192.168.1.0/24.
- Запускает скан сети.
- Видит список найденных хостов.
- Для нужных хостов нажимает "Создать устройство".
- Открывает карточку устройства.
- Добавляет SSH-доступ.
- Нажимает "Тест подключения".
- Если тест успешен, нажимает "Глубокий скан".
- Приложение собирает read-only информацию с хоста.
- Приложение показывает найденные порты, Docker-контейнеры, systemd-сервисы, cron-задачи, nginx-vhost-ы.
- Пользователь подтверждает найденные сервисы.
- Приложение создаёт карточки сервисов и связи между устройством, сервисами, портами, доменами и backup-подсказками.
Архитектура каталогов
Создать структуру:
domovoy/ app/ Controllers/ Services/ Discovery/ HostScan/ Inventory/ Security/ Jobs/ Analysis/ Repositories/ Middleware/ 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
База данных
Нужны миграции для таблиц:
users
- id
- username
- password_hash
- created_at
- updated_at
network_ranges
- id
- name
- cidr
- enabled
- created_at
- updated_at
scan_jobs
- id
- type
- status
- started_at
- finished_at
- error_message
- created_by
- created_at
type:
- network_discovery
- host_deep_scan
- docker_scan
- nginx_scan
- cron_scan
status:
- 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
status:
- 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
type:
- 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
type:
- 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
status:
- new
- accepted
- merged
- ignored
services
- id
- name
- type
- description
- device_id
- status
- importance
- url
- main_port
- created_at
- updated_at
type:
- 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
relation_type:
- 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
audit_log
- id
- user_id
- action
- entity_type
- entity_id
- details_json
- created_at
Web UI
Общий layout
Сделать Bootstrap layout:
Левое меню:
- Dashboard
- Сканирование сети
- Устройства
- Сервисы
- Документы
- Настройки
Верхняя панель:
- название "Домовой"
- текущий пользователь
- logout
Dashboard
Показывать:
- количество устройств
- количество сервисов
- количество новых находок
- количество предупреждений
- последний сетевой скан
- последние найденные хосты
- последние deep scan результаты
Сканирование сети
Страница должна позволять:
- Добавить network range.
- Включить/выключить range.
- Запустить scan.
- Посмотреть список scan jobs.
- Посмотреть результаты discovered_hosts.
Таблица discovered_hosts:
- IP
- Hostname
- MAC
- Vendor
- Open ports
- Confidence
- Status
- Actions
Actions:
- Создать устройство
- Объединить с устройством
- Игнорировать
- Детали
Устройства
Страницы:
- список устройств
- создание устройства вручную
- редактирование устройства
- карточка устройства
Карточка устройства должна показывать:
- основные поля
- IP/MAC/hostname/vendor
- тип
- важность
- заметки
- доступы
- найденные сервисы
- подтверждённые сервисы
- история сканов
- документы
Доступы
На карточке устройства добавить блок SSH-доступов.
Для SSH-доступа:
- name
- username
- port
- auth_method password/private_key
- password/private_key
Кнопки:
- Тест
- Сохранить
Тест должен проверять подключение через phpseclib.
Секреты хранить в зашифрованном виде.
Deep scan
На карточке устройства добавить кнопку:
"Глубокий скан"
После запуска:
- создать scan_job
- выполнить read-only SSH команды
- сохранить raw JSON в storage/scans/
- summary сохранить в host_scans.summary_json
- создать detected_services
Network discovery implementation
Сделать сервисы:
- NetworkScanner
- PingScanner
- TcpPortScanner
- ArpTableReader
- HostFingerprintService
Первый MVP network scan:
- Получить список IP из CIDR.
- Для каждого IP:
- ping check
- TCP connect scan по базовым портам
- reverse DNS lookup
- После sweep прочитать ARP table:
- ip neigh
- arp -a как fallback
- Обогатить найденные хосты MAC-адресами.
- Если MAC известен — попытаться определить vendor через локальную OUI-базу или оставить пустым.
- Сохранить discovered_hosts.
Базовые TCP-порты:
- 22
- 23
- 53
- 80
- 443
- 445
- 548
- 631
- 3306
- 5432
- 6379
- 8000
- 8080
- 8443
- 9000
- 9090
- 9100
Не делать агрессивное сканирование. Добавить timeout. Добавить limit параллельности.
SSH deep scan implementation
Сделать сервисы:
- SshClientFactory
- LinuxHostScanner
- DockerScanner
- NginxScanner
- CronScanner
- SystemdScanner
- BackupHintScanner
- CommandWhitelist
- SecretMasker
Все команды должны быть в CommandWhitelist.
Запрещено принимать произвольную команду из UI.
Формат команд в CommandWhitelist — массивы аргументов, НЕ строки:
'linux.docker_ps' => ['docker', 'ps', '--format', '{{json .}}'] 'linux.hostnamectl' => ['hostnamectl'] 'linux.ip_addr' => ['ip', '-j', 'addr']
ЗАПРЕЩЕНО использовать строковые шаблоны: // НЕПРАВИЛЬНО: 'linux.docker_ps' => "docker ps --format '{{json .}}'"
Массивы аргументов безопаснее — не ломаются при передаче через Symfony Process, не конфликтуют с Twig-шаблонами, не требуют экранирования кавычек и фигурных скобок.
Разрешённые read-only команды (формат: ключ => массив аргументов):
'linux.hostname' => ['hostname'] 'linux.hostnamectl' => ['hostnamectl'] 'linux.uname' => ['uname', '-a'] 'linux.os_release' => ['cat', '/etc/os-release'] 'linux.ip_addr' => ['ip', '-j', 'addr'] 'linux.ip_route' => ['ip', 'route'] 'linux.ss_tulpen' => ['ss', '-tulpen'] 'linux.df' => ['df', '-h'] 'linux.lsblk' => ['lsblk', '-J'] 'linux.mount' => ['mount'] 'linux.systemctl_units' => ['systemctl', 'list-units', '--type=service', '--all', '--no-pager'] 'linux.systemctl_timers' => ['systemctl', 'list-timers', '--all', '--no-pager'] 'linux.crontab_user' => ['crontab', '-l'] 'linux.crontab_system' => ['cat', '/etc/crontab'] 'linux.crontab_d' => ['ls', '-la', '/etc/cron.d', '/etc/cron.daily', '/etc/cron.hourly', '/etc/cron.weekly', '/etc/cron.monthly'] 'linux.docker_ps' => ['docker', 'ps', '--format', '{{json .}}'] 'linux.docker_networks' => ['docker', 'network', 'ls', '--format', '{{json .}}'] 'linux.docker_volumes' => ['docker', 'volume', 'ls', '--format', '{{json .}}'] 'linux.docker_compose_ls' => ['docker', 'compose', 'ls', '--format', 'json']
Каждая команда выполняется отдельно. Если отдельная команда возвращает ошибку или timeout — записать ошибку в raw JSON этого collector-а, продолжить остальные.
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.
Если команда недоступна или возвращает ошибку, не падать всем сканом. Записать ошибку конкретного collector-а в raw JSON.
DockerScanner
Если docker доступен:
- Получить список контейнеров.
- Для каждого контейнера получить:
- name
- image
- status
- ports
- mounts
- networks
- labels
- restart policy
- health status
- Не показывать значения секретных env-переменных.
- По контейнерам создать detected_services kind=docker_container.
Секретные имена:
- PASSWORD
- PASS
- SECRET
- TOKEN
- KEY
- PRIVATE
- CREDENTIAL
NginxScanner
Если nginx найден:
- Проверить наличие:
- /etc/nginx/nginx.conf
- /etc/nginx/sites-enabled/
- /etc/nginx/conf.d/
- Прочитать только конфиги.
- Не читать private key files.
- Вытащить:
- server_name
- listen
- proxy_pass
- root
- ssl_certificate
- Создать detected_services kind=nginx_vhost.
- Создать domain suggestions.
CronScanner и BackupHintScanner
CronScanner должен собрать:
- crontab -l
- /etc/crontab
- файлы /etc/cron.d
BackupHintScanner должен искать признаки:
- rsync
- borg
- restic
- rclone
- tar
- zip
- mysqldump
- mariadb-dump
- pg_dump
- sqlite
- docker exec
- scp
- sftp
Если найдено, создать detected_services kind=backup_hint.
Формулировка в UI: "Похоже на backup job", а не "Это точно backup".
Создание сервиса из detected_service
На странице найденных сервисов добавить действие:
"Создать сервис"
При создании:
- Создать services.
- Создать service_endpoints, если есть порт/url.
- Создать relation:
- Device runs_on Service или
- Service exposes Endpoint
- detected_service.status = accepted
Merge / deduplication
Для discovered_hosts сделать простую логику:
Если MAC совпадает с device.mac_address:
- предложить merge с высокой уверенностью.
Если hostname совпадает:
- предложить merge со средней уверенностью.
Если IP совпадает:
- предложить merge с низкой уверенностью, потому что IP может меняться.
В MVP достаточно показывать suggestions в UI. Автоматически не объединять.
Audit log
Логировать:
- login
- logout
- network scan started
- host scan started
- device created from discovered host
- credential created
- credential tested
- service created from detected service
- object ignored
- object merged
Docker Compose
Сделать docker-compose.yml:
services:
- app
- db
app:
- PHP 8.3
- composer install
- volume для проекта
- volume storage
- depends_on db
db:
- mariadb
- volume db_data
- env из .env
Добавить .env.example:
- APP_ENV
- APP_SECRET
- DB_HOST
- DB_PORT
- DB_DATABASE
- DB_USERNAME
- DB_PASSWORD
- ENCRYPTION_KEY
README
README должен содержать:
- Что такое Домовой.
- Предупреждение: сканировать только свои сети.
- Установка.
- Запуск docker-compose.
- Создание первого пользователя.
- Добавление network range.
- Первый scan.
- Создание устройства из найденного хоста.
- Добавление SSH-доступа.
- Запуск deep scan.
- Где смотреть логи.
- Как запустить миграции.
CI/CD и smoke tests
Создать файл scripts/check.sh — локальный чек-скрипт, который запускается после каждой итерации.
Минимальный набор проверок:
#!/usr/bin/env bash set -euo pipefail
echo "=== composer validate ===" composer validate --no-check-publish
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 ===" 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 ==="
Для следующих итераций добавлять свои проверки:
- network scan: "docker compose exec app php bin/console scan:test"
- device CRUD: curl к страницам devices
- credentials: тест SSH mock-connection
Головное правило: итерация не считается завершённой, пока check.sh не прошёл успешно без ошибок.
Технические требования
- Код должен быть простым и читаемым.
- Не делать преждевременно сложную архитектуру.
- Контроллеры не должны содержать бизнес-логику.
- SQL изолировать в Repository-классах.
- Сканеры изолировать в Services.
- Все shell-команды запускать только через CommandWhitelist.
- Все ошибки scanner collector-ов сохранять, но не ронять весь scan.
- UI должен быть рабочим, пусть и простым.
- Bootstrap использовать без сборки frontend.
- Vanilla JS только для небольших интерактивных действий.
- Никаких React/Vue.
- Никаких внешних SaaS-зависимостей.
Порядок реализации
Итерация 1
Сделать каркас:
- Slim app
- Docker Compose
- DB connection
- migrations runner
- auth
- dashboard
- layout
Результат должен запускаться.
Итерация 2
Сделать:
- network_ranges CRUD
- scan_jobs table
- discovered_hosts table
- NetworkScanner с ping/tcp scan
- страницу результатов
Итерация 3
Сделать:
- devices CRUD
- создание device из discovered_host
- ignore discovered_host
- карточку устройства
Итерация 4
Сделать:
- credentials CRUD для SSH
- шифрование секрета
- тест SSH-подключения через phpseclib
Итерация 5
Сделать:
- HostScan по SSH
- LinuxHostScanner
- raw JSON сохранение
- summary на карточке устройства
Итерация 6
Сделать:
- DockerScanner
- detected_services из контейнеров
- страницу найденных сервисов
Итерация 7
Сделать:
- создание service из detected_service
- service_endpoints
- relations
Итерация 8
Сделать:
- NginxScanner
- CronScanner
- BackupHintScanner
Итерация 9
Сделать:
- RiskAnalyzer
- dashboard warnings
- scan history
- basic diff
Формат ответа после каждой итерации
После работы нужно выдать:
- Что сделано.
- Какие файлы созданы/изменены.
- Как запустить.
- Какие команды выполнить.
- Что проверить вручную.
- Известные ограничения.
- Что делать следующей итерацией.
Не переходить к следующей итерации без успешного запуска текущей. Обязательно запустить scripts/check.sh и показать вывод. Без успешного smoke test итерация не считается завершённой.
Правила для owl-alpha / LLM-кодера
Запрещено:
- переписывать уже готовое без причины;
- делать большие коммиты — только маленькие атомарные;
- выдумывать несуществующие зависимости;
- делать произвольный shell executor;
- использовать строковые шаблоны для docker-команд;
- писать "чистый JS" без обоснования — весь JS заранее заявлен.
Требуется:
- маленькие коммиты;
- список изменённых файлов после каждой итерации;
- инструкции запуска;
- self-check через scripts/check.sh;
- показать реальный вывод smoke test, а не утверждать "готово".
Итерация 1
Сделай только Итерацию 1 проекта "Домовой".
Нужно:
- создать каркас Slim 4 приложения;
- настроить Docker Compose с app + MariaDB;
- сделать .env.example;
- сделать простой migrations runner;
- сделать таблицу users;
- сделать регистрацию первого пользователя через CLI или временную страницу setup;
- сделать login/logout;
- сделать Dashboard;
- сделать Bootstrap layout с меню;
- написать README с запуском.
Не реализовывать network scan, devices, credentials и deep scan в этой итерации. После выполнения выдать список файлов, команды запуска и что проверить вручную.