From e7732f5cee8486893bfe78888f1c573329db26e9 Mon Sep 17 00:00:00 2001 From: mirivlad Date: Tue, 26 May 2026 07:26:55 +0800 Subject: [PATCH] =?UTF-8?q?=D0=98=D1=82=D0=B5=D1=80=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=201:=20=D0=BA=D0=B0=D1=80=D0=BA=D0=B0=D1=81=20Slim=204?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D0=BB=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Slim 4 + php-di контейнер с маршрутизацией - Docker Compose: app (PHP 8.3-FPM) + web (nginx) + db (MariaDB) - Phinx миграция: таблица users (uuid, username, password_hash) - Auth: login/logout с сессиями - Setup: создание первого пользователя - Dashboard с layout (Bootstrap 5.3) - CLI: bin/console, bin/run-scan-worker.php - Smoke test: scripts/check.sh - README.md, PLAN.md --- .env.example | 17 + .gitignore | 3 + PLAN.md | 393 ++ README.md | 74 + app/Controllers/AuthController.php | 67 + app/Controllers/DashboardController.php | 21 + app/Controllers/SetupController.php | 75 + app/Middleware/AuthMiddleware.php | 39 + app/Models/User.php | 25 + app/Repositories/UserRepository.php | 75 + app/Services/AuthService.php | 53 + bin/console | 57 + composer.json | 42 + composer.lock | 5123 +++++++++++++++++ db/migrations/20250526000001_create_users.php | 21 + docker-compose.yml | 49 + docker/app/Dockerfile | 29 + docker/web/default.conf | 21 + phinx.php | 31 + public/assets/css/app.css | 22 + public/index.php | 99 + scripts/check.sh | 28 + storage/logs/.gitkeep | 0 storage/reports/.gitkeep | 0 storage/scans/.gitkeep | 0 templates/auth/login.php | 31 + templates/auth/setup.php | 30 + templates/dashboard/index.php | 71 + templates/layout.php | 67 + Проект - Домовой.md | 1221 ++++ ...кое задание - Домовой.md | 1006 ++++ 31 files changed, 8790 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 PLAN.md create mode 100644 README.md create mode 100644 app/Controllers/AuthController.php create mode 100644 app/Controllers/DashboardController.php create mode 100644 app/Controllers/SetupController.php create mode 100644 app/Middleware/AuthMiddleware.php create mode 100644 app/Models/User.php create mode 100644 app/Repositories/UserRepository.php create mode 100644 app/Services/AuthService.php create mode 100755 bin/console create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 db/migrations/20250526000001_create_users.php create mode 100644 docker-compose.yml create mode 100644 docker/app/Dockerfile create mode 100644 docker/web/default.conf create mode 100644 phinx.php create mode 100644 public/assets/css/app.css create mode 100644 public/index.php create mode 100755 scripts/check.sh create mode 100644 storage/logs/.gitkeep create mode 100644 storage/reports/.gitkeep create mode 100644 storage/scans/.gitkeep create mode 100644 templates/auth/login.php create mode 100644 templates/auth/setup.php create mode 100644 templates/dashboard/index.php create mode 100644 templates/layout.php create mode 100644 Проект - Домовой.md create mode 100644 Техническое задание - Домовой.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..61f0c77 --- /dev/null +++ b/.env.example @@ -0,0 +1,17 @@ +APP_ENV=development +APP_SECRET=change-this-to-random-string +ENCRYPTION_KEY=change-this-to-32-byte-hex + +DB_HOST=db +DB_PORT=3306 +DB_DATABASE=domovoy +DB_USERNAME=domovoy +DB_PASSWORD=domovoy + +MYSQL_ROOT_PASSWORD=rootpass + +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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41e3c84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +.env +.phpunit.result.cache diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..26a5998 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,393 @@ +# План реализации проекта "Домовой" + +Документ представляет собой пошаговый план разработки. +Каждая итерация — самостоятельный кусок функционала, который +можно реализовать, протестировать и запустить независимо. + +После каждой итерации запускается scripts/check.sh. +Без успешного smoke test итерация не считается завершённой. + +--- + +## Итерация 1. Каркас приложения + +Цель: приложение открывается, есть логин, база, миграции, layout. + +Что делаем: +- Slim 4 приложение с маршрутами +- Docker Compose: app (PHP 8.3) + MariaDB +- .env.example со всеми переменными +- Phinx: конфигурация, миграция users +- Регистрация первого пользователя (CLI setup) +- Логин/Logout с сессиями +- Bootstrap layout с левым меню и верхней панелью +- Dashboard с заглушками счётчиков +- scripts/check.sh с базовыми проверками + +Файлы: + composer.json + phinx.php + docker-compose.yml + docker/Dockerfile + .env.example + public/index.php + public/assets/css/app.css + app/Controllers/AuthController.php + app/Controllers/DashboardController.php + app/Controllers/SetupController.php + app/Middleware/AuthMiddleware.php + app/Repositories/UserRepository.php + app/Services/AuthService.php + templates/layout.php + templates/auth/login.php + templates/dashboard/index.php + migrations/20250526000001CreateUsers.php + bin/console + scripts/check.sh + README.md + +Проверка: + docker compose up -d --build + docker compose exec app php vendor/bin/phinx migrate + docker compose exec app php bin/console setup:user + curl -I http://localhost:8080/login + # Открыть в браузере, залогиниться, увидеть dashboard + +--- + +## Итерация 2. Сканирование сети + +Цель: добавить диапазон, запустить scan, увидеть найденные хосты. + +Что делаем: +- Миграции: network_ranges, scan_jobs, discovered_hosts +- NetworkRanges CRUD (вкладка Discovery) +- NetworkScanner: ping sweep + TCP connect + ARP + reverse DNS +- CLI worker bin/run-scan-worker.php +- Страница Discovery: таблица discovered_hosts +- htmx для запуска scan и обновления статуса scan_jobs +- Audit log для действий сканрования + +Файлы (новые/изменённые): + migrations/20250526000002CreateNetworkRanges.php + migrations/20250526000003CreateScanJobs.php + migrations/20250526000004CreateDiscoveredHosts.php + migrations/20250526000005CreateAuditLog.php + app/Controllers/DiscoveryController.php + app/Controllers/NetworkRangeController.php + app/Services/Discovery/NetworkScanner.php + app/Services/Discovery/PingScanner.php + app/Services/Discovery/TcpPortScanner.php + app/Services/Discovery/ArpTableReader.php + app/Services/Discovery/HostFingerprintService.php + app/Services/Jobs/ScanJobRunner.php + app/Services/Jobs/JobQueue.php + app/Repositories/ScanJobRepository.php + app/Repositories/DiscoveredHostRepository.php + app/Repositories/NetworkRangeRepository.php + app/Repositories/AuditLogRepository.php + templates/discovery/index.php + templates/discovery/ranges.php + templates/discovery/hosts.php + bin/run-scan-worker.php + +Проверка: + # Добавить диапазон 192.168.1.0/24 + # Запустить scan через UI + # В другом терминале: + docker compose exec app php bin/run-scan-worker.php + # Увидеть найденные хосты в таблице Discovery + +--- + +## Итерация 3. Инвентарь устройств + +Цель: создавать карточки устройств руками и из найденных хостов. + +Что делаем: +- Миграция devices +- Devices CRUD (список, создание, редактирование, карточка) +- Создание device из discovered_host (кнопка "Создать устройство") +- Игнорирование discovered_host (кнопка "Игнорировать") +- Карточка устройства со всеми полями +- Merge suggestions (по MAC → высокая, hostname → средняя, IP → низкая) +- Обновление Dashboard: реальные счётчики устройств + +Файлы (новые/изменённые): + migrations/...CreateDevices.php + app/Controllers/DeviceController.php + app/Services/Inventory/DeviceService.php + app/Services/Inventory/MergeSuggestionService.php + app/Repositories/DeviceRepository.php + templates/devices/index.php + templates/devices/create.php + templates/devices/edit.php + templates/devices/show.php + templates/dashboard/index.php (обновить счётчики) + +Проверка: + # Создать устройство вручную + # Создать устройство из discovered_host + # Открыть карточку, проверить все поля + # Проверить merge suggestions + +--- + +## Итерация 4. SSH-доступы + +Цель: добавить SSH-доступ к устройству, проверить подключение. + +Что делаем: +- Миграция credentials +- Credentials CRUD (только SSH для MVP) +- Шифрование секретов (defuse/php-encryption) +- Тест подключения через phpseclib +- Блок доступов на карточке устройства +- CredentialVault для шифрования/дешифрования +- Сохранение результата теста (last_test_status, last_test_at) + +Файлы (новые/изменённые): + migrations/...CreateCredentials.php + app/Controllers/CredentialController.php + app/Services/Security/CredentialVault.php + app/Services/HostScan/SshClientFactory.php + app/Repositories/CredentialRepository.php + templates/credentials/_form.php + templates/credentials/_test_result.php + templates/devices/show.php (добавить блок доступов) + .env.example (добавить ENCRYPTION_KEY) + +Проверка: + # Добавить SSH-доступ к устройству + # Нажать "Тест" — увидеть результат подключения + # Секрет не отображается в UI открытым текстом + # В базе хранится зашифрованное значение + +--- + +## Итерация 5. Deep scan Linux-хоста + +Цель: по SSH собрать read-only информацию с Linux-хоста. + +Что делаем: +- Миграции: host_scans +- LinuxHostScanner: hostname, os, ip, ports, disk, systemd, cron +- CommandWhitelist с массивами аргументов +- SshClientFactory с таймаутами (connect=5s, auth=10s, cmd=8s) +- Сохранение raw JSON в storage/scans/ +- Summary в host_scans.summary_json +- Кнопка "Глубокий скан" на карточке устройства +- Страница результата скана +- Worker поддерживает тип host_deep_scan + +Файлы (новые/изменённые): + migrations/...CreateHostScans.php + app/Controllers/HostScanController.php + app/Services/HostScan/LinuxHostScanner.php + app/Services/HostScan/CommandWhitelist.php + app/Repositories/HostScanRepository.php + templates/host_scans/show.php + templates/devices/show.php (кнопка deep scan) + bin/run-scan-worker.php (поддержка host_deep_scan) + config/scan_commands.php (CommandWhitelist mapping) + .env.example (добавить SSH_* таймауты) + +Проверка: + # Добавить SSH-доступ к устройству + # Нажать "Глубокий скан" + # В другом терминале: + docker compose exec app php bin/run-scan-worker.php + # Увидеть результат: hostname, os, порты, диски, systemd, cron + # raw JSON сохранён в storage/scans/ + +--- + +## Итерация 6. Docker scan + detected_services + +Цель: найти контейнеры и создать предложения сервисов. + +Что делаем: +- Миграция detected_services +- DockerScanner: docker ps, inspect, volumes, networks +- SecretMasker для env-переменных (PASSWORD, TOKEN, KEY...) +- Создание detected_services kind=docker_container +- Страница "Найденные сервисы" (detected services) +- Действия: Создать сервис, Объединить, Игнорировать, Детали +- Host scan обогащается docker-сканом + +Файлы (новые/изменённые): + migrations/...CreateDetectedServices.php + app/Controllers/DetectedServiceController.php + app/Services/HostScan/DockerScanner.php + app/Services/Security/SecretMasker.php + app/Repositories/DetectedServiceRepository.php + templates/services/detected.php + templates/services/_detected_row.php + app/Services/HostScan/LinuxHostScanner.php (добавить docker) + +Проверка: + # Запустить deep scan на хосте с Docker + # Увидеть detected_services с kind=docker_container + # Секретные env-переменные замаскированы + # Страница найденных сервисов показывает таблицу + +--- + +## Итерация 7. Создание сервисов + связи + +Цель: превратить найденный сервис в карточку с отношениями. + +Что делаем: +- Миграции: services, service_endpoints, relations +- Service CRUD +- Создание service из detected_service (с выбором типа) +- Автоматическое создание service_endpoints (port, url) +- Создание связей (Device runs_on Service, Service exposes Endpoint) +- Карточка сервиса +- Обновление detected_service.status = accepted + +Файлы (новые/изменённые): + migrations/...CreateServices.php + migrations/...CreateServiceEndpoints.php + migrations/...CreateRelations.php + app/Controllers/ServiceController.php + app/Services/Inventory/ServiceInventoryService.php + app/Services/Inventory/RelationService.php + app/Repositories/ServiceRepository.php + app/Repositories/ServiceEndpointRepository.php + app/Repositories/RelationRepository.php + templates/services/index.php + templates/services/create.php + templates/services/show.php + templates/services/detected.php (кнопка "Создать сервис") + +Проверка: + # Нажать "Создать сервис" на detected_service + # Выбрать тип (web_app, database...) + # Сервис создан, endpoint-ы созданы, связи созданы + # detected_service.status = accepted + # Карточка сервиса показывает связи + +--- + +## Итерация 8. Nginx/Cron/Backup сканеры + +Цель: находить nginx vhost-ы, cron-задачи, backup-подсказки. + +Что делаем: +- NginxScanner: парсинг server_name, listen, proxy_pass, root +- CronScanner: crontab -l, /etc/crontab, /etc/cron.d/* +- BackupHintScanner: поиск rsync, borg, restic, tar, mysqldump... +- detected_services kind=nginx_vhost, cron_job, backup_hint +- Миграция domains +- Создание domain suggestions из nginx server_name +- Host scan обогащается nginx/cron/backup сканами + +Файлы (новые/изменённые): + migrations/...CreateDomains.php + app/Services/HostScan/NginxScanner.php + app/Services/HostScan/CronScanner.php + app/Services/HostScan/BackupHintScanner.php + app/Repositories/DomainRepository.php + templates/host_scans/show.php (добавить nginx/cron/backup) + templates/devices/show.php (найденные сервисы) + +Проверка: + # Deep scan на хосте с nginx + cron + backup scripts + # Увидеть detected_services: nginx_vhost, cron_job, backup_hint + # Формулировка backup_hint: "Похоже на backup job" + # Domains созданы из nginx server_name + # Приватные ключи сертификатов НЕ прочитаны + +--- + +## Итерация 9. Риски + Dashboard + История + +Цель: предупреждения на dashboard, история сканов, diff. + +Что делаем: +- RiskAnalyzer: сервис без backup, публичный endpoint, SSH открыт, + нет свежего scan, TLS истекает +- Dashboard: реальные предупреждения +- История scan jobs +- DiffAnalyzer: новые/пропавшие хосты, порты, контейнеры между сканами +- DiffAnalyzer: новые/пропавшие хосты, порты, контейнеры между сканами +- Documents CRUD (заметки/runbooks) + +Файлы (новые/изменённые): + migrations/...CreateDocuments.php + app/Controllers/DocumentController.php + app/Controllers/ScanHistoryController.php + app/Controllers/DiffController.php + app/Services/Analysis/RiskAnalyzer.php + app/Services/Analysis/DiffAnalyzer.php + app/Repositories/DocumentRepository.php + templates/dashboard/index.php (real warnings) + templates/scans/history.php + templates/scans/diff.php + templates/documents/index.php + templates/documents/create.php + +Проверка: + # Dashboard показывает предупреждения ( service без backup и т.д. ) + # История scan jobs с статусами + # Diff между двумя сканами: новые/пропавшие хосты + # Документы: создать runbook, привязать к устройству + +--- + +## Итерация 10. Полировка + документация + +Цель: финальная доводка проекта. + +Что делаем: +- Полный audit log для всех действий +- Документация в README полностью +- Скриншоты в README (опционально) +- Проверка всех security-правил +- scripts/check.sh обновлён под все итерации +- Финальный smoke test: весь цикл scan → device → service + +--- + +## Сводная таблица + +Итерация | Что делаем | Зависимости +1 | Каркас: Slim, Docker, auth, layout | — +2 | Network scan | 1 +3 | Devices CRUD | 1 +4 | SSH credentials | 1, 3 +5 | Deep scan Linux | 1, 3, 4 +6 | Docker scan + detected_services | 1, 5 +7 | Services + relations | 1, 6 +8 | Nginx/Cron/Backup scanners | 1, 5, 7 +9 | Risks + Dashboard + History + Docs | 1-8 +10 | Полировка + README | 1-9 + +--- + +## Правила для каждой итерации + +1. Не переходить к следующей, пока check.sh не прошёл. +2. Показывать реальный вывод smoke test, а не "готово". +3. Маленькие атомарные коммиты. +4. Контроллеры — только роутинг, бизнес-логика в Services. +5. SQL только в Repository-классах. +6. Shell-команды только через CommandWhitelist (массивы аргументов). +7. Никакого произвольного shell executor из UI. +8. Никакого "чистый JS потом допишем" — весь JS заявлен заранее. + +--- + +## Порядок выдачи задания owl-alpha + +Рекомендуется выдавать по одной итерации: + +Шаг 1: "Прочитай ТЗ и PLAN.md. Сделай Итерацию 1." +Шаг 2: "Прочитай текущий код. Сделай Итерацию 2 по PLAN.md." +... + +После каждой итерации — ревью, smoke test, фикс замечаний, +и только потом следующая итерация. + +Никогда не давай больше одной итерации за раз. diff --git a/README.md b/README.md new file mode 100644 index 0000000..47563f3 --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# Домовой + +Self-hosted система инвентаризации домашней и малой серверной инфраструктуры. + +## Что делает + +- Сканирует заданные локальные сетевые диапазоны +- Находит устройства и предлагает создать карточки +- После добавления SSH-доступа — read-only deep scan хоста +- Находит сервисы, Docker-контейнеры, nginx vhost-ы, cron-задачи, backup-подсказки +- Создаёт карточки сервисов и связи между устройствами и сервисами + +## Стек + +- PHP 8.3, Slim Framework 4, PDO +- MariaDB +- Bootstrap 5.3, htmx +- Docker Compose +- Phinx (миграции) + +## Предупреждение + +Сканируйте только свои сети. Приложение read-only и ничего не меняет на удалённых хостах. + +## Установка + +```bash +git clone +cd domovoy +cp .env.example .env +# Отредактируйте .env: задайте APP_SECRET, ENCRYPTION_KEY, DB_PASSWORD +docker compose up -d --build +docker compose exec app php vendor/bin/phinx migrate +docker compose exec app php bin/setup first-admin +# Откройте http://localhost:8080 +``` + +## Запуск + +```bash +docker compose up -d +# http://localhost:8080 +``` + +## Миграции + +```bash +docker compose exec app php vendor/bin/phinx migrate +docker compose exec app php vendor/bin/phinx rollback +docker compose exec app php vendor/bin/phinx create MigrationName +``` + +## Smoke test + +```bash +./scripts/check.sh +``` + +## Структура + +``` +domovoy/ + app/ # Controllers, Services, Repositories, Middleware + db/migrations/ # Phinx миграции + public/ # Точка входа, assets + templates/ # PHP шаблоны + bin/ # CLI команды + docker/ # Dockerfile, nginx config + storage/ # логи, scan results +``` + +## Лицензия + +MIT diff --git a/app/Controllers/AuthController.php b/app/Controllers/AuthController.php new file mode 100644 index 0000000..a69432f --- /dev/null +++ b/app/Controllers/AuthController.php @@ -0,0 +1,67 @@ +authService = $authService; + } + + public function loginForm(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if (isset($_SESSION['user_id'])) { + return $response + ->withHeader('Location', '/dashboard') + ->withStatus(302); + } + + ob_start(); + require dirname(__DIR__, 2) . '/templates/auth/login.php'; + $body = ob_get_clean(); + $response->getBody()->write($body); + return $response; + } + + public function login(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $data = $request->getParsedBody(); + $username = $data['username'] ?? ''; + $password = $data['password'] ?? ''; + + $user = $this->authService->authenticate($username, $password); + + if ($user === null) { + ob_start(); + $error = 'Invalid username or password'; + require dirname(__DIR__, 2) . '/templates/auth/login.php'; + $body = ob_get_clean(); + $response->getBody()->write($body); + return $response->withStatus(401); + } + + $_SESSION['user_id'] = $user->id; + $_SESSION['username'] = $user->username; + + return $response + ->withHeader('Location', '/dashboard') + ->withStatus(302); + } + + public function logout(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + session_destroy(); + return $response + ->withHeader('Location', '/login') + ->withStatus(302); + } +} diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php new file mode 100644 index 0000000..4204f09 --- /dev/null +++ b/app/Controllers/DashboardController.php @@ -0,0 +1,21 @@ +getBody()->write($body); + return $response; + } +} diff --git a/app/Controllers/SetupController.php b/app/Controllers/SetupController.php new file mode 100644 index 0000000..eae8b42 --- /dev/null +++ b/app/Controllers/SetupController.php @@ -0,0 +1,75 @@ +authService = $authService; + } + + public function form(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if ($this->authService->getUserCount() > 0) { + return $response + ->withHeader('Location', '/login') + ->withStatus(302); + } + + ob_start(); + $error = null; + require dirname(__DIR__, 2) . '/templates/auth/setup.php'; + $body = ob_get_clean(); + $response->getBody()->write($body); + return $response; + } + + public function create(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + if ($this->authService->getUserCount() > 0) { + return $response + ->withHeader('Location', '/login') + ->withStatus(302); + } + + $data = $request->getParsedBody(); + $username = trim($data['username'] ?? ''); + $password = $data['password'] ?? ''; + + if (strlen($username) < 2 || strlen($password) < 8) { + ob_start(); + $error = 'Username must be at least 2 chars, password at least 8'; + require dirname(__DIR__, 2) . '/templates/auth/setup.php'; + $body = ob_get_clean(); + $response->getBody()->write($body); + return $response->withStatus(400); + } + + try { + $user = $this->authService->createUser($username, $password); + } catch (\RuntimeException $e) { + ob_start(); + $error = $e->getMessage(); + require dirname(__DIR__, 2) . '/templates/auth/setup.php'; + $body = ob_get_clean(); + $response->getBody()->write($body); + return $response->withStatus(400); + } + + $_SESSION['user_id'] = $user->id; + $_SESSION['username'] = $user->username; + + return $response + ->withHeader('Location', '/dashboard') + ->withStatus(302); + } +} diff --git a/app/Middleware/AuthMiddleware.php b/app/Middleware/AuthMiddleware.php new file mode 100644 index 0000000..c173b16 --- /dev/null +++ b/app/Middleware/AuthMiddleware.php @@ -0,0 +1,39 @@ +getUri()->getPath(); + + $publicPaths = ['/login', '/setup']; + $isPublic = in_array($path, $publicPaths, true); + + if (!isset($_SESSION['user_id']) && !$isPublic) { + $response = new \Slim\Psr7\Response(); + return $response + ->withHeader('Location', '/login') + ->withStatus(302); + } + + // If logged in and trying to access login/setup, redirect to dashboard + if (isset($_SESSION['user_id']) && $isPublic) { + $response = new \Slim\Psr7\Response(); + return $response + ->withHeader('Location', '/dashboard') + ->withStatus(302); + } + + return $handler->handle($request); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..4e11c6a --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,25 @@ +id = $data['id']; + $user->username = $data['username']; + $user->passwordHash = $data['password_hash']; + $user->createdAt = new \DateTimeImmutable($data['created_at']); + $user->updatedAt = new \DateTimeImmutable($data['updated_at']); + return $user; + } +} diff --git a/app/Repositories/UserRepository.php b/app/Repositories/UserRepository.php new file mode 100644 index 0000000..8043379 --- /dev/null +++ b/app/Repositories/UserRepository.php @@ -0,0 +1,75 @@ +pdo = $pdo; + } + + public function findById(string $id): ?User + { + $stmt = $this->pdo->prepare('SELECT * FROM users WHERE id = :id'); + $stmt->execute(['id' => $id]); + $row = $stmt->fetch(); + return $row ? User::fromArray($row) : null; + } + + public function findByUsername(string $username): ?User + { + $stmt = $this->pdo->prepare('SELECT * FROM users WHERE username = :username'); + $stmt->execute(['username' => $username]); + $row = $stmt->fetch(); + return $row ? User::fromArray($row) : null; + } + + public function findAll(): array + { + $stmt = $this->pdo->query('SELECT * FROM users ORDER BY created_at DESC'); + $users = []; + while ($row = $stmt->fetch()) { + $users[] = User::fromArray($row); + } + return $users; + } + + public function count(): int + { + $stmt = $this->pdo->query('SELECT COUNT(*) FROM users'); + return (int) $stmt->fetchColumn(); + } + + public function save(User $user): void + { + if ($user->id === null) { + $user->id = \Ramsey\Uuid\Uuid::uuid4()->toString(); + } + + $now = (new \DateTimeImmutable())->format('Y-m-d H:i:s'); + $user->updatedAt = new \DateTimeImmutable($now); + if ($user->createdAt === null) { + $user->createdAt = new \DateTimeImmutable($now); + } + + $stmt = $this->pdo->prepare( + 'INSERT INTO users (id, username, password_hash, created_at, updated_at) + VALUES (:id, :username, :password_hash, :created_at, :updated_at)' + ); + $stmt->execute([ + 'id' => $user->id, + 'username' => $user->username, + 'password_hash' => $user->passwordHash, + 'created_at' => $user->createdAt->format('Y-m-d H:i:s'), + 'updated_at' => $user->updatedAt->format('Y-m-d H:i:s'), + ]); + } +} diff --git a/app/Services/AuthService.php b/app/Services/AuthService.php new file mode 100644 index 0000000..c705cbe --- /dev/null +++ b/app/Services/AuthService.php @@ -0,0 +1,53 @@ +userRepository = $userRepository; + } + + public function authenticate(string $username, string $password): ?User + { + $user = $this->userRepository->findByUsername($username); + if ($user === null) { + return null; + } + + if (!password_verify($password, $user->passwordHash)) { + return null; + } + + return $user; + } + + public function createUser(string $username, string $password): User + { + if ($this->userRepository->findByUsername($username) !== null) { + throw new \RuntimeException("User '{$username}' already exists"); + } + + $user = new User(); + $user->username = $username; + $user->passwordHash = password_hash($password, PASSWORD_ARGON2ID); + $user->createdAt = new \DateTimeImmutable(); + $user->updatedAt = new \DateTimeImmutable(); + + $this->userRepository->save($user); + return $user; + } + + public function getUserCount(): int + { + return $this->userRepository->count(); + } +} diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..0f89a38 --- /dev/null +++ b/bin/console @@ -0,0 +1,57 @@ +safeLoad(); + +$pdo = new PDO( + sprintf( + 'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', + getenv('DB_HOST') ?: 'db', + getenv('DB_PORT') ?: 3306, + getenv('DB_DATABASE') ?: 'domovoy' + ), + getenv('DB_USERNAME') ?: 'domovoy', + getenv('DB_PASSWORD') ?: 'domovoy', + [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + ] +); + +$authService = new AuthService(new UserRepository($pdo)); + +$command = $argv[1] ?? 'help'; + +switch ($command) { + case 'setup:user': + if ($argv[2] ?? false) { + // CLI mode: bin/console setup:user username password + $username = $argv[2]; + $password = $argv[3] ?? 'password123'; + } else { + // Interactive mode + echo "Username: "; + $username = trim(fgets(STDIN)); + echo "Password: "; + $password = trim(fgets(STDIN)); + } + + $user = $authService->createUser($username, $password); + echo "User '{$user->username}' created successfully.\n"; + break; + + case 'help': + default: + echo "Domovoy CLI\n\n"; + echo "Commands:\n"; + echo " setup:user [username] [password] Create first admin user\n"; + echo " help Show this help\n"; + break; +} diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..7f0e0a7 --- /dev/null +++ b/composer.json @@ -0,0 +1,42 @@ +{ + "name": "domovoy/domovoy", + "description": "Self-hosted inventory system for home/small infrastructure", + "type": "project", + "require": { + "php": ">=8.3", + "slim/slim": "^4.14", + "slim/psr7": "^1.7", + "php-di/php-di": "^7.0", + "monolog/monolog": "^3.0", + "vlucas/phpdotenv": "^5.6", + "ramsey/uuid": "^4.7", + "symfony/process": "^7.0", + "phpseclib/phpseclib": "~3.0", + "defuse/php-encryption": "^2.4", + "robmorgan/phinx": "^0.15" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "autoload": { + "psr-4": { + "Domovoy\\": "app/" + } + }, + "autoload-dev": { + "psr-4": { + "Domovoy\\Tests\\": "tests/" + } + }, + "scripts": { + "migrate": "php vendor/bin/phinx migrate", + "migrate:rollback": "php vendor/bin/phinx rollback", + "migrate:create": "php vendor/bin/phinx create" + }, + "config": { + "sort-packages": true, + "platform": { + "php": "8.3.0" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..fe98c1f --- /dev/null +++ b/composer.lock @@ -0,0 +1,5123 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "050312589fce63ca73aef06f0e23375a", + "packages": [ + { + "name": "brick/math", + "version": "0.14.8", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629", + "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629", + "shasum": "" + }, + "require": { + "php": "^8.2" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpstan/phpstan": "2.1.22", + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.14.8" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2026-02-10T14:33:43+00:00" + }, + { + "name": "cakephp/chronos", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/cakephp/chronos.git", + "reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/chronos/zipball/e6e777b534244911566face8a5dbdbd7f7bda5a6", + "reference": "e6e777b534244911566face8a5dbdbd7f7bda5a6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "cakephp/cakephp-codesniffer": "^5.0", + "phpunit/phpunit": "^10.5.58 || ^11.5.3 || ^12.1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cake\\Chronos\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "http://nesbot.com" + }, + { + "name": "The CakePHP Team", + "homepage": "https://cakephp.org" + } + ], + "description": "A simple API extension for DateTime.", + "homepage": "https://cakephp.org", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "issues": "https://github.com/cakephp/chronos/issues", + "source": "https://github.com/cakephp/chronos" + }, + "time": "2026-04-10T02:50:39+00:00" + }, + { + "name": "cakephp/core", + "version": "5.3.6", + "source": { + "type": "git", + "url": "https://github.com/cakephp/core.git", + "reference": "9c458b0e9322ec88bc4c758b33cde6a0abf49d12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/core/zipball/9c458b0e9322ec88bc4c758b33cde6a0abf49d12", + "reference": "9c458b0e9322ec88bc4c758b33cde6a0abf49d12", + "shasum": "" + }, + "require": { + "cakephp/utility": "^5.3.0", + "league/container": "^5.1", + "php": ">=8.2", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^2.0" + }, + "suggest": { + "cakephp/cache": "To use Configure::store() and restore().", + "cakephp/event": "To use PluginApplicationInterface or plugin applications.", + "league/container": "To use Container and ServiceProvider classes" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.next": "5.4.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Cake\\Core\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/core/graphs/contributors" + } + ], + "description": "CakePHP Framework Core classes", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "core", + "framework" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/core" + }, + "time": "2026-05-15T03:31:14+00:00" + }, + { + "name": "cakephp/database", + "version": "5.3.6", + "source": { + "type": "git", + "url": "https://github.com/cakephp/database.git", + "reference": "442d12bf0a1edeffc555a59de9f955cb0619c7ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/database/zipball/442d12bf0a1edeffc555a59de9f955cb0619c7ca", + "reference": "442d12bf0a1edeffc555a59de9f955cb0619c7ca", + "shasum": "" + }, + "require": { + "cakephp/chronos": "^3.3", + "cakephp/core": "^5.3.0", + "cakephp/datasource": "^5.3.0", + "php": ">=8.2", + "psr/log": "^3.0" + }, + "require-dev": { + "cakephp/i18n": "^5.3.0", + "cakephp/log": "^5.3.0" + }, + "suggest": { + "cakephp/i18n": "If you are using locale-aware datetime formats.", + "cakephp/log": "If you want to use query logging without providing a logger yourself." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.next": "5.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cake\\Database\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/database/graphs/contributors" + } + ], + "description": "Flexible and powerful Database abstraction library with a familiar PDO-like API", + "homepage": "https://cakephp.org", + "keywords": [ + "abstraction", + "cakephp", + "database", + "database abstraction", + "pdo" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/database" + }, + "time": "2026-05-21T19:38:13+00:00" + }, + { + "name": "cakephp/datasource", + "version": "5.3.6", + "source": { + "type": "git", + "url": "https://github.com/cakephp/datasource.git", + "reference": "b768f0c0bf0fca815f83c4caef14e7fbb2409bf5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/datasource/zipball/b768f0c0bf0fca815f83c4caef14e7fbb2409bf5", + "reference": "b768f0c0bf0fca815f83c4caef14e7fbb2409bf5", + "shasum": "" + }, + "require": { + "cakephp/core": "^5.3.0", + "php": ">=8.2", + "psr/simple-cache": "^2.0 || ^3.0" + }, + "require-dev": { + "cakephp/cache": "^5.3.0", + "cakephp/collection": "^5.3.0", + "cakephp/utility": "^5.3.0" + }, + "suggest": { + "cakephp/cache": "If you decide to use Query caching.", + "cakephp/collection": "If you decide to use ResultSetInterface.", + "cakephp/utility": "If you decide to use EntityTrait." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.next": "5.4.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cake\\Datasource\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/datasource/graphs/contributors" + } + ], + "description": "Provides connection managing and traits for Entities and Queries that can be reused for different datastores", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "connection management", + "datasource", + "entity", + "query" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/datasource" + }, + "time": "2026-05-20T10:27:33+00:00" + }, + { + "name": "cakephp/utility", + "version": "5.3.6", + "source": { + "type": "git", + "url": "https://github.com/cakephp/utility.git", + "reference": "4c703a010b9d955fed44731669e35d3043425cc7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/utility/zipball/4c703a010b9d955fed44731669e35d3043425cc7", + "reference": "4c703a010b9d955fed44731669e35d3043425cc7", + "shasum": "" + }, + "require": { + "cakephp/core": "^5.3.0", + "php": ">=8.2" + }, + "suggest": { + "ext-intl": "To use Text::transliterate() or Text::slug()", + "lib-ICU": "To use Text::transliterate() or Text::slug()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-5.next": "5.4.x-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Cake\\Utility\\": "." + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/utility/graphs/contributors" + } + ], + "description": "CakePHP Utility classes such as Inflector, String, Hash, and Security", + "homepage": "https://cakephp.org", + "keywords": [ + "cakephp", + "hash", + "inflector", + "security", + "string", + "utility" + ], + "support": { + "forum": "https://stackoverflow.com/tags/cakephp", + "irc": "irc://irc.freenode.org/cakephp", + "issues": "https://github.com/cakephp/cakephp/issues", + "source": "https://github.com/cakephp/utility" + }, + "time": "2026-05-21T19:38:13+00:00" + }, + { + "name": "defuse/php-encryption", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/defuse/php-encryption.git", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/defuse/php-encryption/zipball/f53396c2d34225064647a05ca76c1da9d99e5828", + "reference": "f53396c2d34225064647a05ca76c1da9d99e5828", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "paragonie/random_compat": ">= 2", + "php": ">=5.6.0" + }, + "require-dev": { + "phpunit/phpunit": "^5|^6|^7|^8|^9|^10", + "yoast/phpunit-polyfills": "^2.0.0" + }, + "bin": [ + "bin/generate-defuse-key" + ], + "type": "library", + "autoload": { + "psr-4": { + "Defuse\\Crypto\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Hornby", + "email": "taylor@defuse.ca", + "homepage": "https://defuse.ca/" + }, + { + "name": "Scott Arciszewski", + "email": "info@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "Secure PHP Encryption Library", + "keywords": [ + "aes", + "authenticated encryption", + "cipher", + "crypto", + "cryptography", + "encrypt", + "encryption", + "openssl", + "security", + "symmetric key cryptography" + ], + "support": { + "issues": "https://github.com/defuse/php-encryption/issues", + "source": "https://github.com/defuse/php-encryption/tree/v2.4.0" + }, + "time": "2023-06-19T06:10:36+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-04-16T14:03:50+00:00" + }, + { + "name": "league/container", + "version": "5.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/container.git", + "reference": "58accbc032f0090a9bd08326f93062c5a658b2c5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/container/zipball/58accbc032f0090a9bd08326f93062c5a658b2c5", + "reference": "58accbc032f0090a9bd08326f93062c5a658b2c5", + "shasum": "" + }, + "require": { + "php": "^8.1", + "psr/container": "^2.0.2", + "psr/event-dispatcher": "^1.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "replace": { + "orno/di": "~2.0" + }, + "require-dev": { + "nette/php-generator": "^4.1", + "nikic/php-parser": "^5.0", + "phpstan/phpstan": "^2.1.11", + "phpunit/phpunit": "^10.5.45|^11.5.15|^12.0", + "roave/security-advisories": "dev-latest", + "scrutinizer/ocular": "^1.9", + "squizlabs/php_codesniffer": "^3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev", + "dev-3.x": "3.x-dev", + "dev-4.x": "4.x-dev", + "dev-5.x": "5.x-dev", + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Container\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Phil Bennett", + "email": "mail@philbennett.co.uk", + "role": "Developer" + } + ], + "description": "A fast and intuitive dependency injection container.", + "homepage": "https://github.com/thephpleague/container", + "keywords": [ + "container", + "dependency", + "di", + "injection", + "league", + "provider", + "service" + ], + "support": { + "issues": "https://github.com/thephpleague/container/issues", + "source": "https://github.com/thephpleague/container/tree/5.2.0" + }, + "funding": [ + { + "url": "https://github.com/philipobenito", + "type": "github" + } + ], + "time": "2026-03-19T18:52:39+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "FastRoute\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "support": { + "issues": "https://github.com/nikic/FastRoute/issues", + "source": "https://github.com/nikic/FastRoute/tree/master" + }, + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.1.3", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "infection/infection": "^0", + "nikic/php-fuzzer": "^0", + "phpunit/phpunit": "^9|^10|^11", + "vimeo/psalm": "^4|^5|^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2025-09-24T15:06:41+00:00" + }, + { + "name": "paragonie/random_compat", + "version": "v9.99.100", + "source": { + "type": "git", + "url": "https://github.com/paragonie/random_compat.git", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", + "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", + "shasum": "" + }, + "require": { + "php": ">= 7" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*", + "vimeo/psalm": "^1" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "polyfill", + "pseudorandom", + "random" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/random_compat/issues", + "source": "https://github.com/paragonie/random_compat" + }, + "time": "2020-10-15T08:29:30+00:00" + }, + { + "name": "php-di/invoker", + "version": "2.3.7", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/Invoker.git", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/Invoker/zipball/3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "reference": "3c1ddfdef181431fbc4be83378f6d036d59e81e1", + "shasum": "" + }, + "require": { + "php": ">=7.3", + "psr/container": "^1.0|^2.0" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "mnapoli/hard-mode": "~0.3.0", + "phpunit/phpunit": "^9.0 || ^10 || ^11 || ^12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Invoker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Generic and extensible callable invoker", + "homepage": "https://github.com/PHP-DI/Invoker", + "keywords": [ + "callable", + "dependency", + "dependency-injection", + "injection", + "invoke", + "invoker" + ], + "support": { + "issues": "https://github.com/PHP-DI/Invoker/issues", + "source": "https://github.com/PHP-DI/Invoker/tree/2.3.7" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2025-08-30T10:22:22+00:00" + }, + { + "name": "php-di/php-di", + "version": "7.1.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-DI/PHP-DI.git", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-DI/PHP-DI/zipball/f88054cc052e40dbe7b383c8817c19442d480352", + "reference": "f88054cc052e40dbe7b383c8817c19442d480352", + "shasum": "" + }, + "require": { + "laravel/serializable-closure": "^1.0 || ^2.0", + "php": ">=8.0", + "php-di/invoker": "^2.0", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "^1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3", + "friendsofphp/proxy-manager-lts": "^1", + "mnapoli/phpunit-easymock": "^1.3", + "phpunit/phpunit": "^9.6 || ^10 || ^11", + "vimeo/psalm": "^5|^6" + }, + "suggest": { + "friendsofphp/proxy-manager-lts": "Install it if you want to use lazy injection (version ^1)" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "DI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The dependency injection container for humans", + "homepage": "https://php-di.org/", + "keywords": [ + "PSR-11", + "container", + "container-interop", + "dependency injection", + "di", + "ioc", + "psr11" + ], + "support": { + "issues": "https://github.com/PHP-DI/PHP-DI/issues", + "source": "https://github.com/PHP-DI/PHP-DI/tree/7.1.1" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/php-di/php-di", + "type": "tidelift" + } + ], + "time": "2025-08-16T11:10:48+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "phpseclib/phpseclib", + "version": "3.0.52", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/2adaefc83df2ec548558307690f376dd7d4f4fce", + "reference": "2adaefc83df2ec548558307690f376dd7d4f4fce", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1|^2|^3", + "paragonie/random_compat": "^1.4|^2.0|^9.99.99", + "php": ">=5.6.1" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "ext-dom": "Install the DOM extension to load XML formatted public keys.", + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", + "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." + }, + "type": "library", + "autoload": { + "files": [ + "phpseclib/bootstrap.php" + ], + "psr-4": { + "phpseclib3\\": "phpseclib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", + "role": "Developer" + }, + { + "name": "Graham Campbell", + "email": "graham@alt-three.com", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "blowfish", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "twofish", + "x.509", + "x509" + ], + "support": { + "issues": "https://github.com/phpseclib/phpseclib/issues", + "source": "https://github.com/phpseclib/phpseclib/tree/3.0.52" + }, + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2026-04-27T07:02:15+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.2", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "8429c78ca35a09f27565311b98101e2826affde0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0", + "reference": "8429c78ca35a09f27565311b98101e2826affde0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.2" + }, + "time": "2025-12-14T04:43:48+00:00" + }, + { + "name": "robmorgan/phinx", + "version": "0.15.5", + "source": { + "type": "git", + "url": "https://github.com/cakephp/phinx.git", + "reference": "a81c0846256fb9131c4c06d119fbff9d01cbc198" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cakephp/phinx/zipball/a81c0846256fb9131c4c06d119fbff9d01cbc198", + "reference": "a81c0846256fb9131c4c06d119fbff9d01cbc198", + "shasum": "" + }, + "require": { + "cakephp/database": "^5.0.2", + "php-64bit": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/config": "^3.4|^4.0|^5.0|^6.0|^7.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "cakephp/cakephp": "^5.0.2", + "cakephp/cakephp-codesniffer": "^5.0", + "ext-json": "*", + "ext-pdo": "*", + "phpunit/phpunit": "^9.5.19", + "symfony/yaml": "^3.4|^4.0|^5.0|^6.0|^7.0" + }, + "suggest": { + "ext-json": "Install if using JSON configuration format", + "ext-pdo": "PDO extension is needed", + "symfony/yaml": "Install if using YAML configuration format" + }, + "bin": [ + "bin/phinx" + ], + "type": "library", + "autoload": { + "psr-4": { + "Phinx\\": "src/Phinx/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Morgan", + "email": "robbym@gmail.com", + "homepage": "https://robmorgan.id.au", + "role": "Lead Developer" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com", + "homepage": "https://shadowhand.me", + "role": "Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Developer" + }, + { + "name": "CakePHP Community", + "homepage": "https://github.com/cakephp/phinx/graphs/contributors", + "role": "Developer" + } + ], + "description": "Phinx makes it ridiculously easy to manage the database migrations for your PHP app.", + "homepage": "https://phinx.org", + "keywords": [ + "database", + "database migrations", + "db", + "migrations", + "phinx" + ], + "support": { + "issues": "https://github.com/cakephp/phinx/issues", + "source": "https://github.com/cakephp/phinx/tree/0.15.5" + }, + "time": "2023-12-05T13:24:00+00:00" + }, + { + "name": "slim/psr7", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim-Psr7.git", + "reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim-Psr7/zipball/76e7e3b1cdfd583e9035c4c966c08e01e45ce959", + "reference": "76e7e3b1cdfd583e9035c4c966c08e01e45ce959", + "shasum": "" + }, + "require": { + "fig/http-message-util": "^1.1.5", + "php": "^8.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.0 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0", + "psr/http-message-implementation": "^1.0 || ^2.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.5|| ^2.0", + "ext-json": "*", + "http-interop/http-factory-tests": "^1.0 || ^2.0", + "php-http/psr7-integration-tests": "^1.5", + "phpstan/phpstan": "^2.1", + "phpunit/phpunit": "^9.6 || ^10", + "squizlabs/php_codesniffer": "^3.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\Psr7\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "https://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "https://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "https://www.lgse.com" + } + ], + "description": "Strict PSR-7 implementation", + "homepage": "https://www.slimframework.com", + "keywords": [ + "http", + "psr-7", + "psr7" + ], + "support": { + "issues": "https://github.com/slimphp/Slim-Psr7/issues", + "source": "https://github.com/slimphp/Slim-Psr7/tree/1.8.0" + }, + "time": "2025-11-02T17:51:19+00:00" + }, + { + "name": "slim/slim", + "version": "4.15.2", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "e12cb05ca2a14e8f459d019e87a31dc915b80470" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/e12cb05ca2a14e8f459d019e87a31dc915b80470", + "reference": "e12cb05ca2a14e8f459d019e87a31dc915b80470", + "shasum": "" + }, + "require": { + "ext-json": "*", + "nikic/fast-route": "^1.3", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "psr/container": "^1.0 || ^2.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "adriansuter/php-autoload-override": "^1.4 || ^2", + "ext-simplexml": "*", + "guzzlehttp/psr7": "^2.6", + "httpsoft/http-message": "^1.1", + "httpsoft/http-server-request": "^1.1", + "laminas/laminas-diactoros": "^2.17 || ^3", + "nyholm/psr7": "^1.8", + "nyholm/psr7-server": "^1.1", + "phpspec/prophecy": "^1.19", + "phpspec/prophecy-phpunit": "^2.1", + "phpstan/phpstan": "^1 || ^2", + "phpunit/phpunit": "^9.6 || ^10 || ^11 || ^12", + "slim/http": "^1.3", + "slim/psr7": "^1.6", + "squizlabs/php_codesniffer": "^3.10", + "vimeo/psalm": "^5 || ^6" + }, + "suggest": { + "ext-simplexml": "Needed to support XML format in BodyParsingMiddleware", + "ext-xml": "Needed to support XML format in BodyParsingMiddleware", + "php-di/php-di": "PHP-DI is the recommended container library to be used with Slim", + "slim/psr7": "Slim PSR-7 implementation. See https://www.slimframework.com/docs/v4/start/installation.html for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "https://silentworks.co.uk" + }, + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "https://akrabat.com" + }, + { + "name": "Pierre Berube", + "email": "pierre@lgse.com", + "homepage": "https://www.lgse.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://www.slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "support": { + "docs": "https://www.slimframework.com/docs/v4/", + "forum": "https://discourse.slimframework.com/", + "irc": "irc://irc.freenode.net:6667/slimphp", + "issues": "https://github.com/slimphp/Slim/issues", + "rss": "https://www.slimframework.com/blog/feed.rss", + "slack": "https://slimphp.slack.com/", + "source": "https://github.com/slimphp/Slim", + "wiki": "https://github.com/slimphp/Slim/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/slimphp", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/slim/slim", + "type": "tidelift" + } + ], + "time": "2026-05-22T08:00:12+00:00" + }, + { + "name": "symfony/config", + "version": "v7.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/config.git", + "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/config/zipball/d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", + "reference": "d91b6c7cd2a8c9a9c2b8d26c8f5ed48edf99ef57", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/filesystem": "^7.1|^8.0", + "symfony/polyfill-ctype": "~1.8" + }, + "conflict": { + "symfony/finder": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "require-dev": { + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/finder": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Config\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/config/tree/v7.4.10" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-03T14:20:49+00:00" + }, + { + "name": "symfony/console", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/ed0107e43ab452aa77ae99e005b95e56b556e075", + "reference": "ed0107e43ab452aa77ae99e005b95e56b556e075", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2|^8.0" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/lock": "^6.4|^7.0|^8.0", + "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/stopwatch": "^6.4|^7.0|^8.0", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-13T12:04:42+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-11T16:38:44+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "9c862df890f7c833b1101ac5578ec4dcf199efb5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/9c862df890f7c833b1101ac5578ec4dcf199efb5", + "reference": "9c862df890f7c833b1101ac5578ec4dcf199efb5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T12:39:52+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6b177d03d2eb04a6c9d01bab9818fb93a30ce7fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6b177d03d2eb04a6c9d01bab9818fb93a30ce7fd", + "reference": "6b177d03d2eb04a6c9d01bab9818fb93a30ce7fd", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T14:08:27+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/d9593c9efa40499eb078b81144de42cbc28a31f0", + "reference": "d9593c9efa40499eb078b81144de42cbc28a31f0", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-11T16:55:21+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/string", + "version": "v7.4.11", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", + "reference": "965f7306a43383d02c6aca1e3f3bd2f0ea5dee15", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.33", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/intl": "^6.4|^7.0|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.4.11" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-13T12:04:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + } + ], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.12", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/2c1ed04922802c15e1de5d7447b4856de949cf56", + "reference": "2c1ed04922802c15e1de5d7447b4856de949cf56", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.1", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.3.1" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.46" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.12" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-12-24T07:01:01+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2f3a64888c814fc235386b7387dd5b5ed92ad903", + "reference": "2f3a64888c814fc235386b7387dd5b5ed92ad903", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T13:52:54+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.55", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/adc7262fccc12de2b30f12a8aa0b33775d814f00", + "reference": "adc7262fccc12de2b30f12a8aa0b33775d814f00", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.12", + "phpunit/php-file-iterator": "^5.1.1", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.3", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.2", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.55" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:37:06+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "abandoned": true, + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "abandoned": true, + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "reference": "2c95e1e86cb8dd41beb8d502057d1081ccc8eca9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:26:40+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/70a298763b40b213ec087c51c739efcaa90bcd74", + "reference": "70a298763b40b213ec087c51c739efcaa90bcd74", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:12:51+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.3" + }, + "platform-dev": [], + "platform-overrides": { + "php": "8.3.0" + }, + "plugin-api-version": "2.6.0" +} diff --git a/db/migrations/20250526000001_create_users.php b/db/migrations/20250526000001_create_users.php new file mode 100644 index 0000000..77d7ef4 --- /dev/null +++ b/db/migrations/20250526000001_create_users.php @@ -0,0 +1,21 @@ +table('users', ['id' => false, 'primary_key' => ['id']]); + $table + ->addColumn('id', 'uuid', ['null' => false]) + ->addColumn('username', 'string', ['limit' => 255, 'null' => false]) + ->addColumn('password_hash', 'string', ['limit' => 255, 'null' => false]) + ->addColumn('created_at', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) + ->addColumn('updated_at', 'datetime', ['null' => false, 'default' => 'CURRENT_TIMESTAMP']) + ->addIndex(['username'], ['unique' => true]) + ->create(); + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ed8c1a3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +services: + app: + build: + context: . + dockerfile: docker/app/Dockerfile + container_name: domovoy-app + volumes: + - .:/var/www/html + - ./storage:/var/www/html/storage + expose: + - "9000" + depends_on: + - db + environment: + - APP_ENV=${APP_ENV:-development} + - APP_SECRET=${APP_SECRET} + - ENCRYPTION_KEY=${ENCRYPTION_KEY} + - DB_HOST=${DB_HOST:-db} + - DB_PORT=${DB_PORT:-3306} + - DB_DATABASE=${DB_DATABASE:-domovoy} + - DB_USERNAME=${DB_USERNAME:-domovoy} + - DB_PASSWORD=${DB_PASSWORD:-domovoy} + + web: + image: nginx:alpine + container_name: domovoy-web + ports: + - "8080:80" + volumes: + - .:/var/www/html + - ./docker/web/default.conf:/etc/nginx/conf.d/default.conf + depends_on: + - app + + db: + image: mariadb:11 + container_name: domovoy-db + ports: + - "3307:3306" + volumes: + - db_data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-rootpass} + - MYSQL_DATABASE=${DB_DATABASE:-domovoy} + - MYSQL_USER=${DB_USERNAME:-domovoy} + - MYSQL_PASSWORD=${DB_PASSWORD:-domovoy} + +volumes: + db_data: diff --git a/docker/app/Dockerfile b/docker/app/Dockerfile new file mode 100644 index 0000000..902398f --- /dev/null +++ b/docker/app/Dockerfile @@ -0,0 +1,29 @@ +FROM php:8.3-fpm + +RUN apt-get update && apt-get install -y \ + mariadb-client \ + iproute2 \ + iputils-ping \ + net-tools \ + dnsutils \ + openssh-client \ + openssl \ + unzip \ + && rm -rf /var/lib/apt/lists/* + +RUN docker-php-ext-install pdo pdo_mysql + +COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/ +RUN install-php-extensions zip mbstring xml curl + +WORKDIR /var/www/html + +# Copy vendor from host (pre-installed with correct PHP version) +COPY vendor/ vendor/ +COPY composer.json composer.lock ./ + +RUN mkdir -p storage/logs storage/scans storage/reports \ + && chown -R www-data:www-data storage + +EXPOSE 9000 +CMD ["php-fpm"] diff --git a/docker/web/default.conf b/docker/web/default.conf new file mode 100644 index 0000000..46a2fb0 --- /dev/null +++ b/docker/web/default.conf @@ -0,0 +1,21 @@ +server { + listen 80; + server_name localhost; + root /var/www/html/public; + index index.php; + + location / { + try_files $uri $uri /index.php?$query_string; + } + + location ~ \.php$ { + fastcgi_pass app:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + } + + location ~ /\. { + deny all; + } +} diff --git a/phinx.php b/phinx.php new file mode 100644 index 0000000..15fe4fe --- /dev/null +++ b/phinx.php @@ -0,0 +1,31 @@ + [ + 'migrations' => __DIR__ . '/db/migrations', + 'seeds' => __DIR__ . '/db/seeds', + ], + 'environments' => [ + 'default_migration_table' => 'phinxlog', + 'default_environment' => 'development', + 'production' => [ + 'adapter' => 'mysql', + 'host' => getenv('DB_HOST') ?: 'db', + 'port' => getenv('DB_PORT') ?: 3306, + 'name' => getenv('DB_DATABASE') ?: 'domovoy', + 'user' => getenv('DB_USERNAME') ?: 'domovoy', + 'pass' => getenv('DB_PASSWORD') ?: 'domovoy', + 'charset' => 'utf8mb4', + ], + 'development' => [ + 'adapter' => 'mysql', + 'host' => getenv('DB_HOST') ?: 'db', + 'port' => getenv('DB_PORT') ?: 3306, + 'name' => getenv('DB_DATABASE') ?: 'domovoy', + 'user' => getenv('DB_USERNAME') ?: 'domovoy', + 'pass' => getenv('DB_PASSWORD') ?: 'domovoy', + 'charset' => 'utf8mb4', + ], + ], + 'version_order' => 'creation', +]; diff --git a/public/assets/css/app.css b/public/assets/css/app.css new file mode 100644 index 0000000..4e73771 --- /dev/null +++ b/public/assets/css/app.css @@ -0,0 +1,22 @@ +/* Domovoy custom styles */ +body { + background-color: #f8f9fa; +} + +#sidebar-wrapper .list-group-item:hover { + background-color: #343a40 !important; + color: #fff !important; +} + +#sidebar-wrapper .list-group-item.active { + background-color: #0d6efd !important; + border-color: #0d6efd !important; +} + +.card { + box-shadow: 0 1px 3px rgba(0,0,0,0.12); +} + +.navbar-brand { + font-weight: bold; +} diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..2c8cfa6 --- /dev/null +++ b/public/index.php @@ -0,0 +1,99 @@ +safeLoad(); + +// Build container +$containerBuilder = new ContainerBuilder(); +$containerBuilder->addDefinitions([ + 'settings' => [ + 'db' => [ + 'host' => getenv('DB_HOST') ?: 'db', + 'port' => (int)(getenv('DB_PORT') ?: 3306), + 'database' => getenv('DB_DATABASE') ?: 'domovoy', + 'username' => getenv('DB_USERNAME') ?: 'domovoy', + 'password' => getenv('DB_PASSWORD') ?: 'domovoy', + ], + 'app' => [ + 'env' => getenv('APP_ENV') ?: 'development', + 'secret' => getenv('APP_SECRET') ?: 'change-me', + 'encryption_key' => getenv('ENCRYPTION_KEY') ?: '', + ], + 'ssh' => [ + 'connect_timeout' => (int)(getenv('SSH_CONNECT_TIMEOUT_SECONDS') ?: 5), + 'auth_timeout' => (int)(getenv('SSH_AUTH_TIMEOUT_SECONDS') ?: 10), + 'command_timeout' => (int)(getenv('SSH_COMMAND_TIMEOUT_SECONDS') ?: 8), + 'total_scan_timeout' => (int)(getenv('SSH_TOTAL_SCAN_TIMEOUT_SECONDS') ?: 60), + 'retry_count' => (int)(getenv('SSH_RETRY_COUNT') ?: 0), + ], + ], + 'logger' => function () { + $logger = new \Monolog\Logger('domovoy'); + $logger->pushHandler(new \Monolog\Handler\StreamHandler( + dirname(__DIR__) . '/storage/logs/app.log', + \Monolog\Level::Debug + )); + return $logger; + }, + PDO::class => function ($c) { + $db = $c->get('settings')['db']; + $dsn = sprintf( + 'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', + $db['host'], + $db['port'], + $db['database'] + ); + return new PDO($dsn, $db['username'], $db['password'], [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + ]); + }, + \Domovoy\Repositories\UserRepository::class => function ($c) { + return new \Domovoy\Repositories\UserRepository($c->get(PDO::class)); + }, + \Domovoy\Services\AuthService::class => function ($c) { + return new \Domovoy\Services\AuthService($c->get(\Domovoy\Repositories\UserRepository::class)); + }, + \Domovoy\Controllers\AuthController::class => function ($c) { + return new \Domovoy\Controllers\AuthController($c->get(\Domovoy\Services\AuthService::class)); + }, + \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(); + }, +]); +$container = $containerBuilder->build(); + +// Build Slim app +AppFactory::setContainer($container); +$app = AppFactory::create(); + +$app->addRoutingMiddleware(); +$app->addBodyParsingMiddleware(); +$app->add(new \Domovoy\Middleware\AuthMiddleware()); +$app->addErrorMiddleware(true, true, true); + +// Routes +$app->get('/login', [\Domovoy\Controllers\AuthController::class, 'loginForm'])->setName('login'); +$app->post('/login', [\Domovoy\Controllers\AuthController::class, 'login'])->setName('login.post'); +$app->get('/setup', [\Domovoy\Controllers\SetupController::class, 'form'])->setName('setup'); +$app->post('/setup', [\Domovoy\Controllers\SetupController::class, 'create'])->setName('setup.post'); + +$app->group('', function (\Slim\Routing\RouteCollectorProxy $group) { + $group->get('/dashboard', [\Domovoy\Controllers\DashboardController::class, 'index'])->setName('dashboard'); + $group->post('/logout', [\Domovoy\Controllers\AuthController::class, 'logout'])->setName('logout'); +}); + +$app->run(); diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100755 index 0000000..0e7f627 --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,28 @@ +#!/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 "=== wait for db ===" +sleep 5 + +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 ===" diff --git a/storage/logs/.gitkeep b/storage/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/storage/reports/.gitkeep b/storage/reports/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/storage/scans/.gitkeep b/storage/scans/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/templates/auth/login.php b/templates/auth/login.php new file mode 100644 index 0000000..871ed13 --- /dev/null +++ b/templates/auth/login.php @@ -0,0 +1,31 @@ + +
+
+
+
+

Вход в Домовой

+ +
+ +
+
+ + +
+
+ + +
+ +
+ +
+
+
+
+ + diff --git a/templates/auth/setup.php b/templates/auth/setup.php new file mode 100644 index 0000000..3606fc7 --- /dev/null +++ b/templates/auth/setup.php @@ -0,0 +1,30 @@ + +
+
+
+
+

Создание первого пользователя

+

Это будет администратор системы "Домовой"

+ +
+ +
+
+ + +
+
+ + +
Минимум 8 символов
+
+ +
+
+
+
+
+ + diff --git a/templates/dashboard/index.php b/templates/dashboard/index.php new file mode 100644 index 0000000..47f8035 --- /dev/null +++ b/templates/dashboard/index.php @@ -0,0 +1,71 @@ + +

Dashboard

+ +
+
+
+
+
Устройства
+

0

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

0

+
+
+
+
+
+
+
Новые находки
+

0

+
+
+
+
+
+
+
Требуют внимания
+

0

+
+
+
+
+ +
+
+
+
+ Последние найденные хосты +
+
+

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

+
+
+
+
+
+
+ Последние события +
+
+

Нет событий.

+
+
+
+
+ +
+
+ Последний скан сети +
+
+

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

+
+
+ + diff --git a/templates/layout.php b/templates/layout.php new file mode 100644 index 0000000..336a34d --- /dev/null +++ b/templates/layout.php @@ -0,0 +1,67 @@ + + + + + + Домовой + + + + + + + + + diff --git a/Проект - Домовой.md b/Проект - Домовой.md new file mode 100644 index 0000000..f584ddb --- /dev/null +++ b/Проект - Домовой.md @@ -0,0 +1,1221 @@ + +**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-ом: + +1. Web UI создаёт scan_job со статусом "pending". +2. CLI worker (bin/run-scan-worker.php) забирает pending-задачи + из базы и выполняет их. +3. 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: + +```bash +#!/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 — нет завершения. + +--- \ No newline at end of file diff --git a/Техническое задание - Домовой.md b/Техническое задание - Домовой.md new file mode 100644 index 0000000..0ebc0e6 --- /dev/null +++ b/Техническое задание - Домовой.md @@ -0,0 +1,1006 @@ + +## Краткое описание + +Нужно разработать 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-запросы НЕ должны выполнять сканирование напрямую. +Правильная архитектура: + +1. Web UI создаёт запись в scan_jobs (status=pending). +2. Отдельный CLI worker (bin/run-scan-worker.php) забирает + pending-задачи и выполняет их. +3. Web UI показывает статус задачи через polling (htmx). + +Это означает: +- PHP не висит внутри HTTP-запроса во время сканирования. +- Время выполнения HTTP-запроса — миллисекунды. +- Worker можно запускать вручную или через cron/supervisor. +- Worker должен обрабатывать сигналы (SIGTERM) для graceful + shutdown. + +## Важные ограничения безопасности + +1. Сканировать только явно добавленные пользователем диапазоны. +2. По умолчанию считать допустимыми только private/local сети: + - 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. Секреты хранить только в зашифрованном виде. +9. Секреты не выводить в UI и логи. +10. Любой найденный объект сначала попадает в "Найденное", а не автоматически в основной инвентарь. +11. Все действия сканера логировать. +12. Запрещено делать универсальный shell executor, принимающий произвольную команду из UI. + +## Главный пользовательский сценарий + +1. Пользователь устанавливает приложение через Docker Compose. +2. Заходит в web UI. +3. Создаёт первый локальный аккаунт. +4. Добавляет сетевой диапазон, например 192.168.1.0/24. +5. Запускает скан сети. +6. Видит список найденных хостов. +7. Для нужных хостов нажимает "Создать устройство". +8. Открывает карточку устройства. +9. Добавляет SSH-доступ. +10. Нажимает "Тест подключения". +11. Если тест успешен, нажимает "Глубокий скан". +12. Приложение собирает read-only информацию с хоста. +13. Приложение показывает найденные порты, Docker-контейнеры, systemd-сервисы, cron-задачи, nginx-vhost-ы. +14. Пользователь подтверждает найденные сервисы. +15. Приложение создаёт карточки сервисов и связи между устройством, сервисами, портами, доменами и 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 результаты + +### Сканирование сети + +Страница должна позволять: + +1. Добавить network range. +2. Включить/выключить range. +3. Запустить scan. +4. Посмотреть список scan jobs. +5. Посмотреть результаты 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: + +1. Получить список IP из CIDR. +2. Для каждого IP: + - ping check + - TCP connect scan по базовым портам + - reverse DNS lookup +3. После sweep прочитать ARP table: + - ip neigh + - arp -a как fallback +4. Обогатить найденные хосты MAC-адресами. +5. Если MAC известен — попытаться определить vendor через локальную OUI-базу или оставить пустым. +6. Сохранить 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 доступен: + +1. Получить список контейнеров. +2. Для каждого контейнера получить: + - name + - image + - status + - ports + - mounts + - networks + - labels + - restart policy + - health status +3. Не показывать значения секретных env-переменных. +4. По контейнерам создать detected_services kind=docker_container. + +Секретные имена: +- PASSWORD +- PASS +- SECRET +- TOKEN +- KEY +- PRIVATE +- CREDENTIAL + +## NginxScanner + +Если nginx найден: + +1. Проверить наличие: + - /etc/nginx/nginx.conf + - /etc/nginx/sites-enabled/ + - /etc/nginx/conf.d/ +2. Прочитать только конфиги. +3. Не читать private key files. +4. Вытащить: + - server_name + - listen + - proxy_pass + - root + - ssl_certificate +5. Создать detected_services kind=nginx_vhost. +6. Создать 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 + +На странице найденных сервисов добавить действие: + +"Создать сервис" + +При создании: +1. Создать services. +2. Создать service_endpoints, если есть порт/url. +3. Создать relation: + - Device runs_on Service + или + - Service exposes Endpoint +4. 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 должен содержать: + +1. Что такое Домовой. +2. Предупреждение: сканировать только свои сети. +3. Установка. +4. Запуск docker-compose. +5. Создание первого пользователя. +6. Добавление network range. +7. Первый scan. +8. Создание устройства из найденного хоста. +9. Добавление SSH-доступа. +10. Запуск deep scan. +11. Где смотреть логи. +12. Как запустить миграции. + +## 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 не прошёл успешно без ошибок. + +## Технические требования + +1. Код должен быть простым и читаемым. +2. Не делать преждевременно сложную архитектуру. +3. Контроллеры не должны содержать бизнес-логику. +4. SQL изолировать в Repository-классах. +5. Сканеры изолировать в Services. +6. Все shell-команды запускать только через CommandWhitelist. +7. Все ошибки scanner collector-ов сохранять, но не ронять весь scan. +8. UI должен быть рабочим, пусть и простым. +9. Bootstrap использовать без сборки frontend. +10. Vanilla JS только для небольших интерактивных действий. +11. Никаких React/Vue. +12. Никаких внешних 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 + +## Формат ответа после каждой итерации + +После работы нужно выдать: + +1. Что сделано. +2. Какие файлы созданы/изменены. +3. Как запустить. +4. Какие команды выполнить. +5. Что проверить вручную. +6. Известные ограничения. +7. Что делать следующей итерацией. + +Не переходить к следующей итерации без успешного запуска текущей. +Обязательно запустить 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 в этой итерации. +После выполнения выдать список файлов, команды запуска и что проверить вручную. \ No newline at end of file