133 lines
5.0 KiB
PHP
133 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Server;
|
|
use App\Models\SshCredential;
|
|
use App\Services\FirewallService;
|
|
use App\Services\FleetService;
|
|
use App\Services\HardeningService;
|
|
use App\Support\Os\OsDetector;
|
|
use App\Support\Os\OsProfile;
|
|
use Mockery;
|
|
use Tests\TestCase;
|
|
|
|
class HardeningServiceTest extends TestCase
|
|
{
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function debianProfile(): OsProfile
|
|
{
|
|
return new OsProfile(
|
|
family: 'debian',
|
|
id: 'debian',
|
|
prettyName: 'Debian GNU/Linux 13',
|
|
packageManager: 'apt',
|
|
firewallTool: 'ufw',
|
|
serviceManager: 'systemd',
|
|
available: ['apt-get', 'dpkg', 'systemctl', 'fail2ban-client', 'ufw'],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* The checklist must agree with the fail2ban/firewall panels: a component counts as
|
|
* installed when the package manager lists it (^ii/rpm) OR its binary is on PATH OR
|
|
* the service is running — not via dpkg `^ii` alone (the false "nicht installiert").
|
|
*/
|
|
public function test_installed_probe_falls_back_to_binary_and_service(): void
|
|
{
|
|
$os = $this->debianProfile();
|
|
|
|
$detector = Mockery::mock(OsDetector::class);
|
|
$detector->shouldReceive('detect')->andReturn($os);
|
|
|
|
$captured = '';
|
|
$fleet = Mockery::mock(FleetService::class);
|
|
$fleet->shouldReceive('runPrivileged')
|
|
->andReturnUsing(function ($server, $cmd) use (&$captured) {
|
|
$captured = $cmd;
|
|
|
|
// fail2ban running from a source/`rc` install: present + active.
|
|
return ['ok' => true, 'output' => "fail2ban 1 active\nfirewall 1 active\nautoupdate 0 inactive"];
|
|
});
|
|
|
|
$service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);
|
|
|
|
$rows = $service->state(new Server);
|
|
|
|
// fail2ban installed-probe gains the binary fallback (was dpkg ^ii only — the bug).
|
|
$this->assertStringContainsString('command -v fail2ban-client', $captured);
|
|
// firewall installed-probe gains the package-manager fallback (was command -v only).
|
|
$this->assertStringContainsString('dpkg -l ufw', $captured);
|
|
|
|
$f2b = collect($rows)->firstWhere('key', 'fail2ban');
|
|
$this->assertTrue($f2b['featureOn'], 'fail2ban active must read as on');
|
|
}
|
|
|
|
public function test_autoupdate_row_is_neutral_and_never_insecure(): void
|
|
{
|
|
$os = $this->debianProfile();
|
|
|
|
$detector = Mockery::mock(OsDetector::class);
|
|
$detector->shouldReceive('detect')->andReturn($os);
|
|
|
|
$fleet = Mockery::mock(FleetService::class);
|
|
$fleet->shouldReceive('runPrivileged')->andReturn([
|
|
'ok' => true,
|
|
// auto-update reported INACTIVE by the host:
|
|
'output' => "fail2ban 1 active\nfirewall 1 active\nautoupdate 1 inactive",
|
|
]);
|
|
|
|
$service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);
|
|
$rows = $service->state(new Server);
|
|
|
|
$au = collect($rows)->firstWhere('key', 'unattended');
|
|
$this->assertTrue($au['secure'], 'auto-updates must never read as insecure');
|
|
$this->assertTrue($au['neutral'] ?? false, 'auto-updates row must be flagged neutral');
|
|
$this->assertFalse($au['featureOn'], 'inactive auto-updates → toggle shows enable');
|
|
}
|
|
|
|
/**
|
|
* Lock-out guard: when Clusev connects AS root, disabling SSH root login (PermitRootLogin no)
|
|
* would sever its own access — it must be refused before any command runs.
|
|
*/
|
|
public function test_disabling_root_login_is_refused_when_clusev_connects_as_root(): void
|
|
{
|
|
$detector = Mockery::mock(OsDetector::class);
|
|
$detector->shouldReceive('detect')->andReturn($this->debianProfile());
|
|
|
|
$fleet = Mockery::mock(FleetService::class);
|
|
$fleet->shouldNotReceive('runPrivileged'); // guard must short-circuit before any command
|
|
|
|
$server = new Server;
|
|
$server->setRelation('credential', new SshCredential(['username' => 'root']));
|
|
|
|
$service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);
|
|
$res = $service->apply($server, 'ssh_root', false);
|
|
|
|
$this->assertFalse($res['ok']);
|
|
$this->assertSame(__('backend.ssh_root_self_lockout'), $res['output']);
|
|
}
|
|
|
|
public function test_disabling_root_login_proceeds_for_a_non_root_credential(): void
|
|
{
|
|
$detector = Mockery::mock(OsDetector::class);
|
|
$detector->shouldReceive('detect')->andReturn($this->debianProfile());
|
|
|
|
$fleet = Mockery::mock(FleetService::class);
|
|
$fleet->shouldReceive('runPrivileged')->once()->andReturn(['ok' => true, 'output' => '']);
|
|
|
|
$server = new Server;
|
|
$server->setRelation('credential', new SshCredential(['username' => 'deploy']));
|
|
|
|
$service = new HardeningService($fleet, Mockery::mock(FirewallService::class), $detector);
|
|
$res = $service->apply($server, 'ssh_root', false);
|
|
|
|
$this->assertTrue($res['ok']);
|
|
}
|
|
}
|