Skip to main content

Security Event Store

ISecurityEventStore records security-relevant events — failed sign-ins, authorization denials, injection attempts, rate-limit breaches — and queries them back. Excalibur.Security.AuditLogging supplies a SQL-backed implementation that writes through the audit store you already configured, so security events land in the same tamper-evident audit trail as everything else.

Registration

// The security event store writes through IAuditStore, so register an audit store first.
services.AddAuditLogging();
services.AddSqlServerAuditStore(connectionString);

// Then the security event store itself.
services.AddSqlSecurityEventStore();
AddSqlSecurityEventStore() does not bring its own storage

It registers ISecurityEventStore only. The implementation takes an IAuditStore constructor dependency and nothing in this call supplies one — a host that calls it without registering an audit store resolves ISecurityEventStore and fails at that point, not at startup. Register the audit store in the same composition.

The registration uses TryAdd, so a consumer-supplied ISecurityEventStore registered earlier wins and this call becomes a no-op.

Recording events

public sealed class SignInAuditor(ISecurityEventStore store)
{
public Task RecordFailureAsync(string userId, string sourceIp, CancellationToken ct) =>
store.StoreEventsAsync(
[
new SecurityEvent
{
Id = Guid.NewGuid(),
Timestamp = DateTimeOffset.UtcNow,
EventType = SecurityEventType.AuthenticationFailure,
Severity = SecuritySeverity.Medium,
Description = "Password sign-in rejected",
UserId = userId,
SourceIp = sourceIp,
},
],
ct);
}

StoreEventsAsync takes a sequence, so a request that produces several events is one round trip.

Querying events

var suspicious = await store.QueryEventsAsync(
new SecurityEventQuery
{
StartTime = DateTimeOffset.UtcNow.AddHours(-24),
MinimumSeverity = SecuritySeverity.High,
MaxResults = 500,
},
cancellationToken);

Every SecurityEventQuery filter is optional and they combine with AND. MaxResults defaults to 1000 — an unbounded query is not available, so a caller paging through a large window narrows by time rather than by asking for everything.

SecurityEvent

PropertyTypeNotes
IdGuidCaller-assigned; the store does not generate one
TimestampDateTimeOffsetWhen the event occurred
EventTypeSecurityEventTypeSee the values below
DescriptionstringFree text; defaults to empty rather than null
SeveritySecuritySeverityLow, Medium, High, Critical
CorrelationIdGuid?Ties the event to a request or message flow
UserIdstring?Subject, where one is known
SourceIpstring?Client address
UserAgentstring?Client application identifier
MessageTypestring?The message being handled, for dispatch-originated events
AdditionalDataIDictionary<string, object?>Defaults to an empty dictionary

Properties are init-only: an event is a record of something that happened and is not edited after construction.

SecurityEventType

AuthenticationSuccess, AuthenticationFailure, AuthorizationSuccess, AuthorizationFailure, ValidationFailure, ValidationError, InjectionAttempt, RateLimitExceeded, SuspiciousActivity, DataExfiltrationAttempt, ConfigurationChange, CredentialRotation, AuditLogAccess, SecurityPolicyViolation, EncryptionFailure, DecryptionFailure.

SecurityEventQuery

PropertyTypeDefault
StartTime / EndTimeDateTimeOffset?unbounded
EventTypeSecurityEventType?any
MinimumSeveritySecuritySeverity?any
UserId / SourceIpstring?any
CorrelationIdGuid?any
MaxResultsint1000

See Also