harden(opsec): disguise honeypot action labels in the audit log

The audit log (readable by every role) rendered "Honeypot-Treffer" / "Honeypot hit" next to
the decoy paths, which advertises the deception layer to anyone with panel/audit access — an
attacker seeing it would know wp-admin/phpmyadmin/etc. are traps. Rename the VISIBLE labels to
neutral WAF/IDS-style threat logging so the mechanism is no longer self-revealing. The DB action
keys stay security.honeypot_* (Threats intel, history, error styling, and tests key off them);
only the human-facing text changes:

  security.honeypot_hit    -> "Angriffsversuch"              / "Attack attempt"
  security.honeypot_login  -> "Login-Angriffsversuch"        / "Login attack attempt"
  security.honeytoken_used -> "Kompromittierte Zugangsdaten" / "Compromised credentials"

This covers the /audit page, the dashboard audit panel, and the Threats feed rows (all resolve
the same audit.actions label). A regression test asserts the audit log renders the disguised
labels and never leaks "Honeypot"/"Honeytoken" (nor the raw key). 618 tests, de/en parity, Pint.

NOTE: the admin-only Threats page header (subtitle/feed title/Honeytoken KPI) and the settings
honeypot-status section still name it "Honeypot" — those are manage-panel-gated; disguising them
too is a separate, optional follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-07-05 18:59:56 +02:00
parent 930f78efbd
commit bf4366dfc9
4 changed files with 33 additions and 9 deletions

View File

@ -60,9 +60,12 @@ return [
'password.change' => 'Passwort geändert', 'password.change' => 'Passwort geändert',
'password.reset' => 'Passwort zurückgesetzt', 'password.reset' => 'Passwort zurückgesetzt',
'profile.update' => 'Profil aktualisiert', 'profile.update' => 'Profil aktualisiert',
'security.honeypot_hit' => 'Honeypot-Treffer', // Neutral, WAF/IDS-style labels — deliberately do NOT reveal the deception layer to anyone
'security.honeypot_login' => 'Honeypot-Login-Versuch', // reading the audit log. The DB action keys stay `security.honeypot_*`; only the visible text
'security.honeytoken_used' => 'Honeytoken benutzt', // is disguised so an attacker with panel/audit access cannot tell the decoy paths are a trap.
'security.honeypot_hit' => 'Angriffsversuch',
'security.honeypot_login' => 'Login-Angriffsversuch',
'security.honeytoken_used' => 'Kompromittierte Zugangsdaten',
'server.create' => 'Server angelegt', 'server.create' => 'Server angelegt',
'server.credential_updated' => 'SSH-Zugangsdaten aktualisiert', 'server.credential_updated' => 'SSH-Zugangsdaten aktualisiert',
'ssh_key.add' => 'SSH-Schlüssel hinzugefügt', 'ssh_key.add' => 'SSH-Schlüssel hinzugefügt',

View File

@ -60,9 +60,12 @@ return [
'password.change' => 'Password changed', 'password.change' => 'Password changed',
'password.reset' => 'Password reset', 'password.reset' => 'Password reset',
'profile.update' => 'Profile updated', 'profile.update' => 'Profile updated',
'security.honeypot_hit' => 'Honeypot hit', // Neutral, WAF/IDS-style labels — deliberately do NOT reveal the deception layer to anyone
'security.honeypot_login' => 'Honeypot login attempt', // reading the audit log. The DB action keys stay `security.honeypot_*`; only the visible text
'security.honeytoken_used' => 'Honeytoken used', // is disguised so an attacker with panel/audit access cannot tell the decoy paths are a trap.
'security.honeypot_hit' => 'Attack attempt',
'security.honeypot_login' => 'Login attack attempt',
'security.honeytoken_used' => 'Compromised credentials',
'server.create' => 'Server added', 'server.create' => 'Server added',
'server.credential_updated' => 'SSH credentials updated', 'server.credential_updated' => 'SSH credentials updated',
'ssh_key.add' => 'SSH key added', 'ssh_key.add' => 'SSH key added',

View File

@ -48,6 +48,24 @@ class AuditLogDisplayTest extends TestCase
->assertDontSee('wg.set-endpoint'); ->assertDontSee('wg.set-endpoint');
} }
public function test_honeypot_events_render_disguised_and_never_leak_the_deception(): void
{
// OPSEC: the audit log is readable by every role, so the honeypot/deception action labels must
// NOT reveal that the decoy paths are a trap. The DB action keys stay security.honeypot_* (for
// the Threats intel + history), but the VISIBLE label reads like generic WAF/IDS threat logging.
AuditEvent::create(['actor' => 'system', 'action' => 'security.honeypot_hit', 'target' => '/wp-admin', 'ip' => '203.0.113.9']);
AuditEvent::create(['actor' => 'system', 'action' => 'security.honeypot_login', 'target' => '/wp-login.php', 'ip' => '203.0.113.9']);
AuditEvent::create(['actor' => 'system', 'action' => 'security.honeytoken_used', 'target' => 'db_password', 'ip' => '203.0.113.9']);
Livewire::test(Index::class)
->assertSee('Angriffsversuch') // honeypot_hit → disguised
->assertSee('Login-Angriffsversuch') // honeypot_login → disguised
->assertSee('Kompromittierte Zugangsdaten') // honeytoken_used → disguised
->assertDontSee('Honeypot') // the word never reaches the audit log
->assertDontSee('Honeytoken')
->assertDontSee('honeypot'); // nor the raw action key
}
public function test_search_matches_the_readable_label(): void public function test_search_matches_the_readable_label(): void
{ {
AuditEvent::create(['actor' => 'Administrator', 'action' => 'wg.set-endpoint', 'target' => 'x', 'ip' => '127.0.0.1']); AuditEvent::create(['actor' => 'Administrator', 'action' => 'wg.set-endpoint', 'target' => 'x', 'ip' => '127.0.0.1']);

View File

@ -102,8 +102,8 @@ class ThreatsPageTest extends TestCase
->test(Index::class) ->test(Index::class)
->assertSee(self::ATTACKER_IP) ->assertSee(self::ATTACKER_IP)
->assertSee('/wp-login.php') ->assertSee('/wp-login.php')
->assertSee('Honeypot-Treffer') // localized action label (DE default) ->assertSee('Angriffsversuch') // disguised action label (DE default) — no "Honeypot" leak
->assertSee('Honeytoken benutzt') ->assertSee('Kompromittierte Zugangsdaten')
->assertViewHas('probes_24h', 2) // 2 recent hits, the 3-day-old one excluded ->assertViewHas('probes_24h', 2) // 2 recent hits, the 3-day-old one excluded
->assertViewHas('banned_total', 1) ->assertViewHas('banned_total', 1)
->assertViewHas('honeytoken_trips', 1) ->assertViewHas('honeytoken_trips', 1)
@ -123,7 +123,7 @@ class ThreatsPageTest extends TestCase
Livewire::actingAs($this->admin()) Livewire::actingAs($this->admin())
->test(Index::class) ->test(Index::class)
->assertSee(self::ATTACKER_IP) ->assertSee(self::ATTACKER_IP)
->assertSee('Honeypot-Login-Versuch') // localized action label (DE default) ->assertSee('Login-Angriffsversuch') // disguised action label (DE default) — no "Honeypot" leak
->assertSee('admin') // captured tried_user ->assertSee('admin') // captured tried_user
->assertSee('hunter2') // captured tried_pass ->assertSee('hunter2') // captured tried_pass
->assertViewHas('login_attempts_24h', 1); ->assertViewHas('login_attempts_24h', 1);