first commit

This commit is contained in:
brusnitsyn
2026-06-24 17:20:43 +09:00
commit 43499acf1c
165 changed files with 25929 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Middleware;
use App\Facades\Audit;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Журналирование обращений к защищаемым маршрутам (мера РСБ.2).
*
* Назначается группе маршрутов, работающих с ПДн / API. Фиксирует факт
* изменяющего обращения (POST/PUT/PATCH/DELETE) и результат по HTTP-коду.
* Точечный аудит конкретных операций выполняется через App\Facades\Audit
* в контроллерах и Observer'ах моделей.
*/
class AuditLog
{
/** HTTP-методы, изменяющие состояние и подлежащие регистрации. */
private const MUTATING = ['POST', 'PUT', 'PATCH', 'DELETE'];
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
if (in_array($request->method(), self::MUTATING, true)) {
$status = $response->getStatusCode();
Audit::log(
eventType: 'http.request',
action: $request->method().' '.$request->path(),
resource: $request->route()?->getName() ?? $request->path(),
result: $status < 400 ? 'success' : 'failure',
details: ['status' => $status],
);
}
return $response;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Проверка прохождения второго фактора аутентификации в текущей сессии.
*
* Мера ФСТЭК: ИАФ.4 многофакторная аутентификация (обязательна для УЗ-1).
*
* Логика:
* - если MFA не требуется конфигурацией пропускаем;
* - если у пользователя не настроен второй фактор отправляем на настройку;
* - если фактор настроен, но не подтверждён в сессии на ввод кода.
*/
class EnsureMfaIsVerified
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if ($user === null || ! config('security.mfa.required')) {
return $next($request);
}
if (! $user->hasMfaEnabled()) {
if ($request->routeIs('mfa.setup', 'mfa.enable', 'logout')) {
return $next($request);
}
return redirect()->route('mfa.setup');
}
if (! $request->session()->get('mfa.verified', false)) {
if ($request->routeIs('mfa.challenge', 'mfa.verify', 'logout')) {
return $next($request);
}
return redirect()->route('mfa.challenge');
}
return $next($request);
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use App\Models\User;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Принудительная смена пароля по истечении срока действия.
*
* Мера ФСТЭК: ИАФ.3 управление сроком действия пароля (для УЗ-1 90 дней).
*/
class EnsurePasswordIsNotExpired
{
public function handle(Request $request, Closure $next): Response
{
$user = $request->user();
if ($user instanceof User && $user->passwordExpired()) {
if ($request->routeIs('password.expired', 'password.update', 'logout')) {
return $next($request);
}
return redirect()->route('password.expired');
}
return $next($request);
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Принудительное использование HTTPS вне локальной среды.
*
* Мера ФСТЭК: ИАФ.5, ЗИС.9 защита данных при передаче (TLS обязателен).
*/
class ForceHttps
{
public function handle(Request $request, Closure $next): Response
{
if (! $request->isSecure() && ! app()->environment('local', 'testing')) {
return redirect()->secure($request->getRequestUri(), 301);
}
return $next($request);
}
}

View File

@@ -0,0 +1,46 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
/**
* The root template that's loaded on the first page visit.
*
* @see https://inertiajs.com/server-side-setup#root-template
*
* @var string
*/
protected $rootView = 'app';
/**
* Determines the current asset version.
*
* @see https://inertiajs.com/asset-versioning
*/
public function version(Request $request): ?string
{
return parent::version($request);
}
/**
* Define the props that are shared by default.
*
* @see https://inertiajs.com/shared-data
*
* @return array<string, mixed>
*/
public function share(Request $request): array
{
return [
...parent::share($request),
'name' => config('app.name'),
'auth' => [
'user' => $request->user(),
],
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Component\HttpFoundation\Response;
/**
* Ограничение доступа к административным разделам по списку IP/подсетей.
*
* Мера ФСТЭК: УПД управление доступом, ЗИС.17. Включается через
* config('security.ip_whitelist'). Используется для admin-маршрутов.
*/
class IpWhitelist
{
public function handle(Request $request, Closure $next): Response
{
if (! config('security.ip_whitelist.enabled')) {
return $next($request);
}
$ranges = (array) config('security.ip_whitelist.ranges');
if ($ranges !== [] && ! IpUtils::checkIp($request->ip(), $ranges)) {
Log::channel(config('audit.siem.channel', 'stack'))->warning('ip_whitelist.denied', [
'ip' => $request->ip(),
'path' => $request->path(),
]);
abort(Response::HTTP_FORBIDDEN, 'Доступ с данного адреса запрещён.');
}
return $next($request);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Установка заголовков безопасности HTTP-ответа.
*
* Меры ФСТЭК: ЗИС (защита информационной системы), защита от XSS/clickjacking.
* HSTS добавляется только для HTTPS-запросов (ИАФ.5, ЗИС.9).
*/
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('X-Frame-Options', 'DENY');
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
$response->headers->set('Permissions-Policy', 'geolocation=(), camera=(), microphone=()');
$response->headers->set('Content-Security-Policy', (string) config('security.headers.csp'));
$response->headers->set('X-Permitted-Cross-Domain-Policies', 'none');
$response->headers->set('Cross-Origin-Opener-Policy', 'same-origin');
// Не раскрываем используемое ПО (ОДТ/ЗИС).
$response->headers->remove('X-Powered-By');
$response->headers->remove('Server');
if ($request->isSecure()) {
$maxAge = (int) config('security.headers.hsts_max_age');
$response->headers->set(
'Strict-Transport-Security',
"max-age={$maxAge}; includeSubDomains; preload"
);
}
return $response;
}
}