[ 'app_key' => self::CANARY_APP, 'db_password' => self::CANARY_DB, 'api_key' => self::CANARY_API, ], ]); } /** Simulate a request from a specific client IP via REMOTE_ADDR (trustProxies is prod-gated). */ private function fromIp(string $method, string $uri, string $ip, array $data = []) { return $this->call($method, $uri, $data, [], [], ['REMOTE_ADDR' => $ip]); } public function test_wp_login_decoy_serves_fake_wordpress_and_bans(): void { $response = $this->fromIp('GET', '/wp-login.php', self::ATTACKER_IP); $response->assertStatus(200); $response->assertSee('wp-submit', false); $response->assertSee('WordPress', false); $this->assertTrue(BannedIp::where('ip', self::ATTACKER_IP)->exists()); $this->assertTrue(AuditEvent::where('action', 'security.honeypot_hit')->exists()); } public function test_env_decoy_leaks_canary_and_bans(): void { $canary = config('clusev.honeypot.canaries.app_key'); $response = $this->fromIp('GET', '/.env', self::ATTACKER_IP); $response->assertStatus(200); $response->assertSee($canary, false); $this->assertTrue(BannedIp::where('ip', self::ATTACKER_IP)->exists()); $this->assertTrue(AuditEvent::where('action', 'security.honeypot_hit')->exists()); } public function test_git_config_decoy_serves_fake_git_config_and_bans(): void { $response = $this->fromIp('GET', '/.git/config', self::ATTACKER_IP); $response->assertStatus(200); $response->assertHeader('Content-Type', 'text/plain; charset=UTF-8'); $response->assertSee('[remote "origin"]', false); $response->assertSee('[core]', false); $this->assertTrue(BannedIp::where('ip', self::ATTACKER_IP)->exists()); $this->assertTrue(AuditEvent::where('action', 'security.honeypot_hit')->exists()); } public function test_honeytoken_replay_in_post_body_api_key_is_blocked_banned_and_audited(): void { $canary = config('clusev.honeypot.canaries.api_key'); // An unauthenticated POST web route carrying the leaked canary in the api_key BODY field trips // the middleware (broadcasting/auth is in the web group, so the appended DetectHoneytoken runs). // The 403 fires before the route's own handler, proving the tripwire — not the endpoint — responded. $response = $this->fromIp('POST', '/broadcasting/auth', self::ATTACKER_IP, ['api_key' => $canary]); $response->assertStatus(403); $this->assertTrue(BannedIp::where('ip', self::ATTACKER_IP)->exists()); $this->assertTrue( AuditEvent::where('action', 'security.honeytoken_used') ->where('target', 'api_key') ->where('ip', self::ATTACKER_IP) ->exists() ); } public function test_login_post_with_password_equal_to_canary_does_not_ban(): void { // `password` is no longer scanned — a login carrying a value that happens to equal a canary // (e.g. a reflected value) must NOT trip the honeytoken tripwire. $this->fromIp('POST', '/broadcasting/auth', self::ATTACKER_IP, ['password' => self::CANARY_DB]); $this->assertDatabaseCount('banned_ips', 0); $this->assertFalse(AuditEvent::where('action', 'security.honeytoken_used')->exists()); } public function test_get_query_string_with_canary_token_does_not_ban(): void { // The query string is never scanned — a reflected GET decoy link ?token= must NOT ban // the victim who clicks it. $this->fromIp('GET', '/login?token='.self::CANARY_API, self::ATTACKER_IP); $this->assertDatabaseCount('banned_ips', 0); $this->assertFalse(AuditEvent::where('action', 'security.honeytoken_used')->exists()); } public function test_authenticated_user_hitting_decoy_gets_404_and_is_not_banned(): void { $user = User::factory()->create(['must_change_password' => false]); $response = $this->actingAs($user)->fromIp('GET', '/wp-login.php', self::ATTACKER_IP); $response->assertStatus(404); $this->assertDatabaseCount('banned_ips', 0); $this->assertFalse(AuditEvent::where('action', 'security.honeypot_hit')->exists()); } public function test_ban_now_fires_even_when_bruteforce_protection_is_disabled(): void { // Disabling the brute-force login toggle must NOT silently disable honeypot bans: banNow no // longer consults enabled(). The honeypot flag alone gates whether the trap runs. Setting::put('bruteforce_enabled', '0'); $banned = app(BruteforceGuard::class)->banNow(self::ATTACKER_IP, 'honeytoken'); $this->assertTrue($banned); $this->assertTrue(BannedIp::where('ip', self::ATTACKER_IP)->exists()); } public function test_disabled_honeypot_returns_404_without_ban_or_audit(): void { config(['clusev.honeypot.enabled' => false]); $response = $this->fromIp('GET', '/wp-login.php', self::ATTACKER_IP); $response->assertStatus(404); $this->assertDatabaseCount('banned_ips', 0); $this->assertFalse(AuditEvent::where('action', 'security.honeypot_hit')->exists()); } public function test_exempt_ip_gets_deception_but_is_not_banned(): void { // Default test client IP is 127.0.0.1 (loopback → exempt): deception still fires, ban does not. $response = $this->get('/wp-login.php'); $response->assertStatus(200); $response->assertSee('wp-submit', false); // The hit is still audited, but banNow no-ops for the exempt IP → no ban row. $this->assertTrue(AuditEvent::where('action', 'security.honeypot_hit')->exists()); $this->assertDatabaseCount('banned_ips', 0); } }