first commit
This commit is contained in:
41
app/Http/Middleware/AuditLog.php
Normal file
41
app/Http/Middleware/AuditLog.php
Normal 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;
|
||||
}
|
||||
}
|
||||
47
app/Http/Middleware/EnsureMfaIsVerified.php
Normal file
47
app/Http/Middleware/EnsureMfaIsVerified.php
Normal 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);
|
||||
}
|
||||
}
|
||||
31
app/Http/Middleware/EnsurePasswordIsNotExpired.php
Normal file
31
app/Http/Middleware/EnsurePasswordIsNotExpired.php
Normal 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);
|
||||
}
|
||||
}
|
||||
24
app/Http/Middleware/ForceHttps.php
Normal file
24
app/Http/Middleware/ForceHttps.php
Normal 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);
|
||||
}
|
||||
}
|
||||
46
app/Http/Middleware/HandleInertiaRequests.php
Normal file
46
app/Http/Middleware/HandleInertiaRequests.php
Normal 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(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
38
app/Http/Middleware/IpWhitelist.php
Normal file
38
app/Http/Middleware/IpWhitelist.php
Normal 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);
|
||||
}
|
||||
}
|
||||
43
app/Http/Middleware/SecurityHeaders.php
Normal file
43
app/Http/Middleware/SecurityHeaders.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user