100 lines
3.7 KiB
PHP
100 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Server;
|
|
use App\Services\Fail2banService;
|
|
use App\Services\FleetService;
|
|
use App\Support\Os\OsDetector;
|
|
use App\Support\Os\OsProfile;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Mockery;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* `fail2ban-client set <jail> banip <ip>` is built by interpolating the jail name. The
|
|
* base64 transport in runPrivileged() neutralises SHELL metacharacters, but a jail name
|
|
* beginning with `-` (e.g. "-h", "--help") is still parsed by fail2ban-client as an
|
|
* OPTION rather than a positional jail argument. The jail reaches this sink from a
|
|
* client-controlled source — Fail2banBan::$jail (a wire:model property) and
|
|
* Fail2banBans::unbanIp($jail) (a method argument) — so validJail() must reject a
|
|
* leading dash (and dot) while still accepting real jail names that contain an internal
|
|
* dash, like "nginx-http-auth".
|
|
*/
|
|
class Fail2banJailValidationTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Mockery::close();
|
|
parent::tearDown();
|
|
}
|
|
|
|
private function server(): Server
|
|
{
|
|
return Server::create(['name' => 'box', 'ip' => '10.0.0.2', 'ssh_port' => 22, 'status' => 'online']);
|
|
}
|
|
|
|
/** FleetService whose privileged exec must NEVER run (guard fires first). */
|
|
private function fleetNeverRuns(): FleetService
|
|
{
|
|
$fleet = Mockery::mock(FleetService::class);
|
|
$fleet->shouldReceive('runPrivileged')->never();
|
|
|
|
return $fleet;
|
|
}
|
|
|
|
private function service(FleetService $fleet, ?OsDetector $detector = null): Fail2banService
|
|
{
|
|
return new Fail2banService($fleet, $detector ?? Mockery::mock(OsDetector::class));
|
|
}
|
|
|
|
public function test_ban_rejects_a_jail_beginning_with_a_dash(): void
|
|
{
|
|
$res = $this->service($this->fleetNeverRuns())->ban($this->server(), '-h', '1.2.3.4');
|
|
|
|
$this->assertFalse($res['ok']);
|
|
$this->assertSame(__('backend.invalid_input'), $res['output']);
|
|
}
|
|
|
|
public function test_unban_rejects_a_double_dash_option_jail(): void
|
|
{
|
|
$res = $this->service($this->fleetNeverRuns())->unban($this->server(), '--all', '1.2.3.4');
|
|
|
|
$this->assertFalse($res['ok']);
|
|
$this->assertSame(__('backend.invalid_input'), $res['output']);
|
|
}
|
|
|
|
public function test_ban_rejects_a_jail_beginning_with_a_dot(): void
|
|
{
|
|
$res = $this->service($this->fleetNeverRuns())->ban($this->server(), '.evil', '1.2.3.4');
|
|
|
|
$this->assertFalse($res['ok']);
|
|
$this->assertSame(__('backend.invalid_input'), $res['output']);
|
|
}
|
|
|
|
/**
|
|
* A real jail with an INTERNAL dash must still pass validation — proves the tightened
|
|
* regex is not over-broad. It gets PAST validJail and reaches support(), which we stub
|
|
* to report "unsupported"; the distinguishing signal is that the output is the support
|
|
* reason, NOT invalid_input (which would mean validJail wrongly rejected it).
|
|
*/
|
|
public function test_valid_jail_with_internal_dash_passes_validation(): void
|
|
{
|
|
// A real profile where fail2ban is unsupported (apk/openrc), so support() returns a reason.
|
|
$profile = new OsProfile('alpine', 'alpine', 'Alpine Linux', 'apk', 'none', 'openrc', []);
|
|
$reason = $profile->supports('fail2ban');
|
|
$this->assertNotNull($reason); // sanity: this profile really reports unsupported
|
|
|
|
$detector = Mockery::mock(OsDetector::class);
|
|
$detector->shouldReceive('detect')->andReturn($profile);
|
|
|
|
$res = $this->service($this->fleetNeverRuns(), $detector)
|
|
->unban($this->server(), 'nginx-http-auth', '1.2.3.4');
|
|
|
|
$this->assertFalse($res['ok']);
|
|
$this->assertSame($reason, $res['output']); // reached support(), not the validJail reject
|
|
}
|
|
}
|