presseportale/tests/Feature/LogChannelsTest.php
Kevin Adametz 5dfd622141 security: Andere Geräte abmelden, IP-weites Login-Limit, dauerhafte Sperrmarke, Passwortregeln, CSP im Melde-Modus
Restliche Phase 4 aus dem Sicherheitsaudit 07.10.2026:

- Sicherheitsseite: „Andere Geräte abmelden“ rotiert remember_token und
  beendet alle anderen Sessions, die aktuelle bleibt (nicht während einer
  Impersonation).
- LoginIpThrottle: 20 Fehlversuche je IP in 15 Minuten über alle Konten.
  Gezählt über das Failed-Event, damit Livewire-Login und Fortifys
  POST /login denselben Zähler teilen; Erfolg setzt ihn nicht zurück.
- Sperrmarke für Sessions in users.sessions_revoked_at_ms statt nur im
  Cache — übersteht cache:clear. Die alte Cache-Marke zählt übergangsweise
  mit und kann im nächsten Release entfallen.
- Neue und geänderte Passwörter: mindestens 12 Zeichen, in Produktion
  zusätzlich uncompromised() (HIBP, k-Anonymität). Bestehende bleiben gültig.
- Content-Security-Policy-Report-Only in Produktion; Meldungen über
  /csp-report nach storage/logs/csp-*.log, je Meldung höchstens einmal pro
  Stunde, ohne Query und tokenartige Pfadsegmente.

Migration: users.sessions_revoked_at_ms (users hat FULLTEXT, 25 MB, Sekunden).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:02:49 +00:00

70 lines
2.7 KiB
PHP

<?php
use App\Services\Web\MarketDataService;
use App\Support\OperationalAlert;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Psr\Log\LoggerInterface;
/**
* Log-Aufteilung: je Thema eine Tagesdatei, das allgemeine Log bleibt für
* Exceptions und Unzugeordnetes. Die Suite selbst schreibt nur nach
* testing-*.log (phpunit.xml + TestCase::setUp).
*/
test('every topic channel writes its own daily file with the shared retention', function (string $channel, string $file) {
// TestCase biegt die Themen-Channels für die Suite um — hier zählt die
// ausgelieferte Konfiguration aus config/logging.php.
$config = require base_path('config/logging.php');
expect($config['channels'][$channel])
->toMatchArray([
'driver' => 'daily',
'path' => storage_path("logs/{$file}.log"),
'days' => 14,
]);
})->with([
'billing' => ['billing', 'billing'],
'ki' => ['ki', 'ki'],
'monitoring' => ['monitoring', 'monitoring'],
'market' => ['market', 'market'],
'csp' => ['csp', 'csp'],
'admin_slow' => ['admin_slow', 'admin-slow'],
]);
test('the default stack rotates daily instead of growing one single file', function () {
$config = require base_path('config/logging.php');
expect($config['channels']['stack']['channels'])->toBe(['daily'])
->and($config['channels']['daily']['path'])->toBe(storage_path('logs/laravel.log'));
});
test('the test suite logs into the dedicated testing channel only', function () {
expect(config('logging.default'))->toBe('testing')
->and(config('logging.channels.testing.path'))->toBe(storage_path('logs/testing.log'));
foreach (['billing', 'ki', 'monitoring', 'market'] as $channel) {
expect(config("logging.channels.{$channel}.path"))->toBe(storage_path('logs/testing.log'));
}
});
test('operational alerts go to the monitoring channel', function () {
config()->set('monitoring.alerts.enabled', false);
$monitoringLog = Mockery::spy(LoggerInterface::class);
Log::shouldReceive('channel')->once()->with('monitoring')->andReturn($monitoringLog);
OperationalAlert::send('Bounce-Postfach konnte nicht gelesen werden');
$monitoringLog->shouldHaveReceived('critical')
->withArgs(fn (string $message): bool => str_contains($message, 'Bounce-Postfach'))
->once();
});
test('market data failures go to the market channel', function () {
Http::fake(['*' => Http::response('', 500)]);
$marketLog = Mockery::spy(LoggerInterface::class);
Log::shouldReceive('channel')->with('market')->andReturn($marketLog);
app(MarketDataService::class)->refresh();
$marketLog->shouldHaveReceived('warning')->atLeast()->once();
});