first commit
This commit is contained in:
50
app/Models/AuditLog.php
Normal file
50
app/Models/AuditLog.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Запись журнала регистрации событий безопасности.
|
||||
*
|
||||
* Мера ФСТЭК: РСБ.2, РСБ.3. Хранится в отдельном соединении (audit) с правами
|
||||
* только на INSERT/SELECT. Изменение/удаление записей пользователями запрещено.
|
||||
*
|
||||
* @property string $event_id
|
||||
* @property string $event_type
|
||||
* @property string|null $user_id
|
||||
* @property string|null $ip
|
||||
* @property string|null $user_agent
|
||||
* @property string|null $session_id
|
||||
* @property string|null $resource
|
||||
* @property string $action
|
||||
* @property string $result
|
||||
* @property array<string, mixed>|null $details
|
||||
* @property string|null $prev_hash
|
||||
* @property string $signature
|
||||
* @property int $id
|
||||
* @property CarbonImmutable|null $created_at
|
||||
*/
|
||||
class AuditLog extends Model
|
||||
{
|
||||
// Журнал неизменяемый: updated_at не нужен.
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $casts = [
|
||||
'details' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function getConnectionName(): ?string
|
||||
{
|
||||
return config('audit.connection', 'audit');
|
||||
}
|
||||
|
||||
public function getTable(): string
|
||||
{
|
||||
return config('audit.table', 'audit_log');
|
||||
}
|
||||
}
|
||||
74
app/Models/Concerns/HasPdnEncryption.php
Normal file
74
app/Models/Concerns/HasPdnEncryption.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models\Concerns;
|
||||
|
||||
use App\Services\Crypto\PdnCipher;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Прозрачное шифрование полей персональных данных на уровне приложения.
|
||||
*
|
||||
* Мера ФСТЭК: ЗНИ (защита носителей), ОЦЛ.2 (контроль целостности данных).
|
||||
*
|
||||
* Использование в модели:
|
||||
*
|
||||
* use HasPdnEncryption;
|
||||
* protected array $encrypted = ['last_name', 'passport', 'snils'];
|
||||
*
|
||||
* Шифрование выполняется выбранным драйвером (config/security.php → encryption),
|
||||
* что позволяет заменить AES на сертифицированное СКЗИ (ГОСТ) без правки моделей.
|
||||
*/
|
||||
trait HasPdnEncryption
|
||||
{
|
||||
public function setAttribute($key, $value)
|
||||
{
|
||||
if ($this->isPdnEncrypted($key) && $value !== null) {
|
||||
$value = $this->pdnCipher()->encrypt((string) $value);
|
||||
}
|
||||
|
||||
return parent::setAttribute($key, $value);
|
||||
}
|
||||
|
||||
public function getAttribute($key)
|
||||
{
|
||||
$value = parent::getAttribute($key);
|
||||
|
||||
if ($this->isPdnEncrypted($key) && $value !== null && is_string($value)) {
|
||||
try {
|
||||
return $this->pdnCipher()->decrypt($value);
|
||||
} catch (Throwable $e) {
|
||||
// Нарушение целостности/невозможность расшифровки — инцидент (ИНЦ.1).
|
||||
Log::channel(config('audit.siem.channel', 'stack'))->error('pdn.decrypt_failed', [
|
||||
'model' => static::class,
|
||||
'attribute' => $key,
|
||||
'id' => $this->getKey(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Список зашифрованных атрибутов модели.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function encryptedAttributes(): array
|
||||
{
|
||||
return $this->encrypted;
|
||||
}
|
||||
|
||||
protected function isPdnEncrypted(string $key): bool
|
||||
{
|
||||
return in_array($key, $this->encryptedAttributes(), true);
|
||||
}
|
||||
|
||||
protected function pdnCipher(): PdnCipher
|
||||
{
|
||||
return app(PdnCipher::class);
|
||||
}
|
||||
}
|
||||
31
app/Models/PasswordHistory.php
Normal file
31
app/Models/PasswordHistory.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
/**
|
||||
* История паролей пользователя для запрета повторного использования.
|
||||
*
|
||||
* Мера ФСТЭК: ИАФ.3 — запрет повтора последних N паролей.
|
||||
*
|
||||
* @property int $user_id
|
||||
* @property string $password_hash
|
||||
*/
|
||||
class PasswordHistory extends Model
|
||||
{
|
||||
public const UPDATED_AT = null;
|
||||
|
||||
protected $table = 'password_histories';
|
||||
|
||||
protected $fillable = ['user_id', 'password_hash'];
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
103
app/Models/PersonalData.php
Normal file
103
app/Models/PersonalData.php
Normal file
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasPdnEncryption;
|
||||
use Database\Factories\PersonalDataFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
/**
|
||||
* ПРИМЕР модели с персональными данными (УЗ-1).
|
||||
*
|
||||
* Меры ФСТЭК: ЗНИ (шифрование полей), ОЦЛ.2 (контроль целостности через
|
||||
* контрольную сумму), УПД.2 (разграничение доступа через PersonalDataPolicy).
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $owner_id
|
||||
* @property string|null $last_name
|
||||
* @property string|null $first_name
|
||||
* @property string|null $passport
|
||||
* @property string|null $snils
|
||||
*/
|
||||
class PersonalData extends Model
|
||||
{
|
||||
/** @use HasFactory<PersonalDataFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use HasPdnEncryption;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $table = 'personal_data';
|
||||
|
||||
protected $fillable = [
|
||||
'owner_id',
|
||||
'subject_pseudonym',
|
||||
'last_name',
|
||||
'first_name',
|
||||
'middle_name',
|
||||
'birth_date',
|
||||
'passport',
|
||||
'snils',
|
||||
'phone',
|
||||
];
|
||||
|
||||
/**
|
||||
* Шифруемые поля ПДн (мера ЗНИ).
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $encrypted = [
|
||||
'last_name',
|
||||
'first_name',
|
||||
'middle_name',
|
||||
'birth_date',
|
||||
'passport',
|
||||
'snils',
|
||||
'phone',
|
||||
];
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
// Контроль целостности (ОЦЛ.2): пересчёт контрольной суммы при сохранении.
|
||||
static::saving(function (PersonalData $model): void {
|
||||
$model->checksum = $model->calculateChecksum();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BelongsTo<User, $this>
|
||||
*/
|
||||
public function owner(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'owner_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* HMAC по расшифрованному содержимому ПДн-полей — для проверки целостности.
|
||||
*/
|
||||
public function calculateChecksum(): string
|
||||
{
|
||||
$payload = [];
|
||||
|
||||
foreach ($this->encrypted as $attribute) {
|
||||
$payload[$attribute] = $this->getAttribute($attribute);
|
||||
}
|
||||
|
||||
$key = (string) config('audit.hmac_key');
|
||||
$key = str_starts_with($key, 'base64:') ? (string) base64_decode(substr($key, 7), true) : $key;
|
||||
|
||||
return hash_hmac('sha256', (string) json_encode($payload, JSON_UNESCAPED_UNICODE), $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка целостности записи (ОЦЛ.2).
|
||||
*/
|
||||
public function integrityValid(): bool
|
||||
{
|
||||
return $this->checksum !== null
|
||||
&& hash_equals($this->checksum, $this->calculateChecksum());
|
||||
}
|
||||
}
|
||||
145
app/Models/User.php
Normal file
145
app/Models/User.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Models\Concerns\HasPdnEncryption;
|
||||
use Database\Factories\UserFactory;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
/**
|
||||
* Учётная запись пользователя.
|
||||
*
|
||||
* Меры ФСТЭК: ИАФ.2 (жизненный цикл идентификатора, soft delete), ИАФ.4 (MFA),
|
||||
* ИАФ.3 (срок действия пароля), УПД.4 (роли через spatie/laravel-permission).
|
||||
*
|
||||
* @property int $id
|
||||
* @property string $name
|
||||
* @property string $email
|
||||
* @property Carbon|null $email_verified_at
|
||||
* @property string $password
|
||||
* @property Carbon|null $password_changed_at
|
||||
* @property string|null $mfa_secret Зашифровано (ЗНИ)
|
||||
* @property string|null $mfa_recovery_codes Зашифровано (ЗНИ), JSON
|
||||
* @property Carbon|null $mfa_confirmed_at
|
||||
* @property bool $is_blocked
|
||||
* @property Carbon|null $last_login_at
|
||||
* @property string|null $remember_token
|
||||
*/
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use HasPdnEncryption;
|
||||
use HasRoles;
|
||||
use Notifiable;
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
'mfa_secret',
|
||||
'mfa_recovery_codes',
|
||||
];
|
||||
|
||||
/**
|
||||
* Поля ПДн/секреты, шифруемые на уровне приложения (мера ЗНИ).
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected array $encrypted = [
|
||||
'mfa_secret',
|
||||
'mfa_recovery_codes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'password_changed_at' => 'datetime',
|
||||
'mfa_confirmed_at' => 'datetime',
|
||||
'last_login_at' => 'datetime',
|
||||
'is_blocked' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Многофакторная аутентификация (ИАФ.4)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function hasMfaEnabled(): bool
|
||||
{
|
||||
return $this->mfa_secret !== null && $this->mfa_confirmed_at !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function recoveryCodes(): array
|
||||
{
|
||||
$raw = $this->mfa_recovery_codes;
|
||||
|
||||
return $raw ? (array) json_decode($raw, true) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $codes
|
||||
*/
|
||||
public function setRecoveryCodes(array $codes): void
|
||||
{
|
||||
$this->mfa_recovery_codes = (string) json_encode(array_values($codes), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Парольная политика (ИАФ.3)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function passwordExpired(): bool
|
||||
{
|
||||
$maxAge = (int) config('security.password.max_age_days');
|
||||
|
||||
if ($maxAge <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$changedAt = $this->password_changed_at ?? $this->created_at;
|
||||
|
||||
return $changedAt === null || $changedAt->lt(now()->subDays($maxAge));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HasMany<PasswordHistory, $this>
|
||||
*/
|
||||
public function passwordHistories(): HasMany
|
||||
{
|
||||
return $this->hasMany(PasswordHistory::class);
|
||||
}
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Состояние учётной записи (ИАФ.2, УПД.1)
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
public function isBlocked(): bool
|
||||
{
|
||||
return (bool) $this->is_blocked;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user