first commit
This commit is contained in:
7
tests/Feature/ExampleTest.php
Normal file
7
tests/Feature/ExampleTest.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
test('returns a successful response', function () {
|
||||
$response = $this->get(route('home'));
|
||||
|
||||
$response->assertOk();
|
||||
});
|
||||
36
tests/Feature/Security/AccessControlTest.php
Normal file
36
tests/Feature/Security/AccessControlTest.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use App\Models\PersonalData;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
|
||||
use function Pest\Laravel\seed;
|
||||
|
||||
beforeEach(fn () => seed(RolesAndPermissionsSeeder::class));
|
||||
|
||||
it('разграничивает доступ к ПДн по ролям (УПД.2, УПД.5)', function () {
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('admin');
|
||||
|
||||
$owner = User::factory()->create();
|
||||
$owner->assignRole('user');
|
||||
|
||||
$stranger = User::factory()->create();
|
||||
$stranger->assignRole('user');
|
||||
|
||||
$data = PersonalData::create(['owner_id' => $owner->id, 'last_name' => 'Сидоров']);
|
||||
|
||||
expect($admin->can('view', $data))->toBeTrue()
|
||||
->and($owner->can('view', $data))->toBeTrue()
|
||||
->and($stranger->can('view', $data))->toBeFalse();
|
||||
});
|
||||
|
||||
it('аудитор не имеет прав на работу с ПДн (разделение обязанностей)', function () {
|
||||
$auditor = User::factory()->create();
|
||||
$auditor->assignRole('auditor');
|
||||
|
||||
$data = PersonalData::create(['last_name' => 'Кузнецов']);
|
||||
|
||||
expect($auditor->can('view', $data))->toBeFalse()
|
||||
->and($auditor->hasPermissionTo('audit.view'))->toBeTrue();
|
||||
});
|
||||
33
tests/Feature/Security/AuditLogTest.php
Normal file
33
tests/Feature/Security/AuditLogTest.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use App\Facades\Audit;
|
||||
use App\Models\AuditLog;
|
||||
|
||||
it('подписывает записи журнала и подтверждает целостность (РСБ.3)', function () {
|
||||
Audit::log('pdn.viewed', 'read', 'PersonalData:1');
|
||||
Audit::log('pdn.updated', 'update', 'PersonalData:1');
|
||||
|
||||
expect(AuditLog::count())->toBe(2)
|
||||
->and(Audit::verifyIntegrity())->toBe([]);
|
||||
});
|
||||
|
||||
it('обнаруживает подделку записи журнала (РСБ.3)', function () {
|
||||
Audit::log('pdn.viewed', 'read', 'PersonalData:1');
|
||||
$entry = Audit::log('pdn.updated', 'update', 'PersonalData:1');
|
||||
|
||||
// Изменение записи в обход подписи.
|
||||
$entry->result = 'failure';
|
||||
$entry->saveQuietly();
|
||||
|
||||
expect(Audit::verifyIntegrity())->toContain($entry->id);
|
||||
});
|
||||
|
||||
it('обнаруживает разрыв хеш-цепочки при удалении записи (РСБ.3)', function () {
|
||||
Audit::log('e1', 'a', 'R:1');
|
||||
$second = Audit::log('e2', 'a', 'R:2');
|
||||
Audit::log('e3', 'a', 'R:3');
|
||||
|
||||
$second->delete();
|
||||
|
||||
expect(Audit::verifyIntegrity())->not->toBe([]);
|
||||
});
|
||||
41
tests/Feature/Security/AuthenticationTest.php
Normal file
41
tests/Feature/Security/AuthenticationTest.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
|
||||
use function Pest\Laravel\post;
|
||||
use function Pest\Laravel\seed;
|
||||
|
||||
beforeEach(fn () => seed(RolesAndPermissionsSeeder::class));
|
||||
|
||||
it('блокирует вход после превышения числа попыток (ИАФ.6)', function () {
|
||||
$user = User::factory()->create(['email' => 'lock@example.local']);
|
||||
$max = (int) config('security.lockout.max_attempts');
|
||||
|
||||
for ($i = 0; $i < $max; $i++) {
|
||||
post('/login', ['email' => $user->email, 'password' => 'wrong-password']);
|
||||
}
|
||||
|
||||
// Превышение лимита: попытка отклоняется и фиксируется блокировка (РСБ.2).
|
||||
post('/login', ['email' => $user->email, 'password' => 'wrong-password'])
|
||||
->assertSessionHasErrors('email');
|
||||
|
||||
$this->assertDatabaseHas('audit_log', ['event_type' => 'auth.locked'], config('audit.connection'));
|
||||
});
|
||||
|
||||
it('регистрирует неудачный вход в журнале аудита (РСБ.2)', function () {
|
||||
User::factory()->create(['email' => 'audit-login@example.local']);
|
||||
|
||||
post('/login', ['email' => 'audit-login@example.local', 'password' => 'wrong']);
|
||||
|
||||
$this->assertDatabaseHas('audit_log', ['event_type' => 'auth.login.failed'], config('audit.connection'));
|
||||
});
|
||||
|
||||
it('требует второй фактор после входа, если MFA не настроена (ИАФ.4)', function () {
|
||||
$user = User::factory()->create(['password' => bcrypt('Password123!@#')]);
|
||||
|
||||
post('/login', ['email' => $user->email, 'password' => 'Password123!@#']);
|
||||
|
||||
// Доступ в защищённую зону перенаправляет на настройку MFA.
|
||||
$this->get('/dashboard')->assertRedirect(route('mfa.setup'));
|
||||
});
|
||||
42
tests/Feature/Security/PasswordPolicyTest.php
Normal file
42
tests/Feature/Security/PasswordPolicyTest.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Rules\PasswordNotReused;
|
||||
use App\Services\Auth\PasswordManager;
|
||||
use App\Support\PasswordPolicy;
|
||||
|
||||
it('запрещает повторное использование последних паролей (ИАФ.3)', function () {
|
||||
$user = User::factory()->create();
|
||||
$manager = app(PasswordManager::class);
|
||||
|
||||
$manager->change($user, 'FirstPass123!@#');
|
||||
$manager->change($user, 'SecondPass123!@#');
|
||||
|
||||
$rule = new PasswordNotReused($user->fresh());
|
||||
|
||||
$failed = false;
|
||||
$rule->validate('password', 'FirstPass123!@#', function () use (&$failed) {
|
||||
$failed = true;
|
||||
});
|
||||
|
||||
expect($failed)->toBeTrue();
|
||||
});
|
||||
|
||||
it('определяет истёкший срок действия пароля (ИАФ.3)', function () {
|
||||
$expired = User::factory()->passwordExpired()->create();
|
||||
$fresh = User::factory()->create();
|
||||
|
||||
expect($expired->passwordExpired())->toBeTrue()
|
||||
->and($fresh->passwordExpired())->toBeFalse();
|
||||
});
|
||||
|
||||
it('применяет минимальную длину пароля из политики (ИАФ.3)', function () {
|
||||
$rule = PasswordPolicy::rule();
|
||||
|
||||
$validator = validator(
|
||||
['password' => 'Short1!'],
|
||||
['password' => $rule],
|
||||
);
|
||||
|
||||
expect($validator->fails())->toBeTrue();
|
||||
});
|
||||
30
tests/Feature/Security/PdnEncryptionTest.php
Normal file
30
tests/Feature/Security/PdnEncryptionTest.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Models\PersonalData;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
it('хранит поля ПДн в зашифрованном виде (ЗНИ)', function () {
|
||||
$pd = PersonalData::create([
|
||||
'last_name' => 'Иванов',
|
||||
'passport' => '1234 567890',
|
||||
]);
|
||||
|
||||
// В приложении значение доступно в открытом виде.
|
||||
expect($pd->fresh()->last_name)->toBe('Иванов');
|
||||
|
||||
// В БД хранится шифртекст, а не исходное значение.
|
||||
$raw = DB::table('personal_data')->where('id', $pd->id)->value('last_name');
|
||||
expect($raw)->not->toBe('Иванов')
|
||||
->and(strlen((string) $raw))->toBeGreaterThan(20);
|
||||
});
|
||||
|
||||
it('контролирует целостность записи ПДн (ОЦЛ.2)', function () {
|
||||
$pd = PersonalData::create(['last_name' => 'Петров']);
|
||||
|
||||
expect($pd->integrityValid())->toBeTrue();
|
||||
|
||||
// Подмена шифртекста в обход приложения нарушает контрольную сумму.
|
||||
DB::table('personal_data')->where('id', $pd->id)->update(['last_name' => 'tampered']);
|
||||
|
||||
expect(PersonalData::find($pd->id)->integrityValid())->toBeFalse();
|
||||
});
|
||||
13
tests/Feature/Security/SecurityHeadersTest.php
Normal file
13
tests/Feature/Security/SecurityHeadersTest.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
use function Pest\Laravel\get;
|
||||
|
||||
it('устанавливает заголовки безопасности (ЗИС)', function () {
|
||||
$response = get('/');
|
||||
|
||||
$response->assertHeader('X-Frame-Options', 'DENY')
|
||||
->assertHeader('X-Content-Type-Options', 'nosniff')
|
||||
->assertHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
|
||||
expect($response->headers->get('Content-Security-Policy'))->toContain("default-src 'self'");
|
||||
});
|
||||
Reference in New Issue
Block a user