Skip to content
Flag of Europe
Made in the European Union · Independently built · Released under EUPL 1.2
Security Audit

Security Audit

Audit logging is not “log a string somewhere”. A security audit needs typed events, a query API, and a sink that survives without external infrastructure. security-core ships all three.

Status: implemented. Feature §2 in Konzept-V00.60.00.md. Delivered 2026-05-10; route + REST endpoint added 2026-05-11.

The contract

public interface SecurityAuditService {
  void publish(AuditEvent event);
  List<AuditEvent> query(AuditQuery query);
}

Two implementations come with the library:

  • NoopSecurityAuditService — accepts and discards. Useful for tests or for production deployments that route events elsewhere.
  • DefaultCompositeAuditService — fans publish(...) out to one or more AuditSinks and serves query(...) from the first sink that supports it.

The 16 event types

AuditEvent is a sealed interface. The full taxonomy:

CategoryEvents
AuthenticationLoginSucceeded, LoginFailed, LogoutPerformed
AuthorizationAccessGranted, AccessDenied, ActionDenied
Role administrationRoleAssigned, RoleRevoked
User administrationUserCreated, UserDeleted
SessionsSessionCreated, SessionExpired, SessionInvalidated
BootstrapBootstrapAdminCreated, BootstrapTokenRejected
Brute-forceBruteForceLimitReached

Every event is an immutable record. Pattern-match on the sealed hierarchy:

switch (event) {
  case LoginSucceeded e        -> metrics.loginOk(e.subjectId());
  case LoginFailed e           -> metrics.loginFail(e.username(), e.reason());
  case BruteForceLimitReached e -> alerts.notify(e);
  case AccessDenied e          -> denyCounter.increment();
  // exhaustive — compiler enforces all 16
}

Sinks

public interface AuditSink {
  void accept(AuditEvent event);
  List<AuditEvent> query(AuditQuery query);  // optional
}

Two come with the library:

  • LoggingAuditSink — writes every event through java.util.logging at INFO. Zero dependencies, fine for development and for piping into existing log aggregation.
  • RingBufferAuditSink — keeps the last N events in memory. Survives restart only as long as the JVM lives, but answers query() efficiently and powers the in-app audit viewers below.

Want Postgres / Kafka / S3? Implement AuditSink and register it with the CompositeAuditService. No core change.

Querying

public record AuditQuery(
    Instant since,
    Instant until,
    Optional<SubjectId> subjectId,
    int limit
) {}

Used by both viewers; designed to be the same query both in-process and through the REST API so the same code can run against either.

Vaadin /audit route

Both Vaadin demos expose a /audit route, gated by @RequiresPermission("audit:read"). It renders a Grid backed by RingBufferAuditSink with filters for subject and time.

REST GET /api/audit

The REST demo exposes the same data via JSON, gated by the same permission and routed through RestAuthorizationFilter. 403 for non-admins, 200 + JSON array for admins:

curl -H "Authorization: Bearer <token>" \
  http://localhost:8080/api/audit?subject=alice&limit=50

Wiring it in your application

public class AppBootstrap {
  public static SecurityAuditService createAudit() {
    var ring     = new RingBufferAuditSink(10_000);
    var logging  = new LoggingAuditSink();
    return new DefaultCompositeAuditService(List.of(ring, logging));
  }
}

Register via META-INF/services/com.svenruppert.vaadin.security.audit.SecurityAuditService. The Vaadin and REST adapters publish their built-in events (LoginSucceeded, AccessDenied, etc.) automatically once the service is resolved.