fix(site-event-logging): resolve SiteEventLogging-012..014 — fault dropped-event tasks, escape LIKE wildcards, re-triage startup-purge finding (Won't Fix)

This commit is contained in:
Joseph Doherty
2026-05-17 03:18:41 -04:00
parent a58cec5776
commit 6d63fef934
6 changed files with 226 additions and 23 deletions

View File

@@ -30,6 +30,19 @@ public class EventLogQueryService : IEventLogQueryService
_logger = logger;
}
/// <summary>
/// Escapes the SQL <c>LIKE</c> metacharacters (<c>\</c>, <c>%</c>, <c>_</c>) in a
/// user-supplied keyword so it is matched as a literal substring. Used together
/// with a <c>LIKE ... ESCAPE '\'</c> clause.
/// </summary>
private static string EscapeLikePattern(string input)
{
return input
.Replace("\\", "\\\\")
.Replace("%", "\\%")
.Replace("_", "\\_");
}
public EventLogQueryResponse ExecuteQuery(EventLogQueryRequest request)
{
try
@@ -78,8 +91,14 @@ public class EventLogQueryService : IEventLogQueryService
if (!string.IsNullOrWhiteSpace(request.KeywordFilter))
{
whereClauses.Add("(message LIKE $keyword OR source LIKE $keyword)");
parameters.Add(new SqliteParameter("$keyword", $"%{request.KeywordFilter}%"));
// Keyword search is a literal substring match. The LIKE
// metacharacters % and _ (and the escape char itself) must be
// escaped so identifiers such as "store_and_forward" or a literal
// "%" are not misinterpreted as wildcards (SiteEventLogging-013).
var escaped = EscapeLikePattern(request.KeywordFilter);
whereClauses.Add(
"(message LIKE $keyword ESCAPE '\\' OR source LIKE $keyword ESCAPE '\\')");
parameters.Add(new SqliteParameter("$keyword", $"%{escaped}%"));
}
var whereClause = whereClauses.Count > 0

View File

@@ -160,9 +160,13 @@ public class SiteEventLogger : ISiteEventLogger, IDisposable
if (!_writeQueue.Writer.TryWrite(pending))
{
// The channel is unbounded, so the only way TryWrite fails is that the
// writer has been completed (logger disposed). Drop silently — there is
// nowhere to persist the event.
pending.Completion.TrySetResult();
// writer has been completed (logger disposed). The event cannot be
// persisted — fault the Task (SiteEventLogging-012) rather than
// reporting false success, so a caller that awaits a critical audit
// event can tell it was dropped.
pending.Completion.TrySetException(
new ObjectDisposedException(nameof(SiteEventLogger),
"Event could not be recorded: the event logger has been disposed."));
}
return pending.Completion.Task;
@@ -191,9 +195,20 @@ public class SiteEventLogger : ISiteEventLogger, IDisposable
cmd.ExecuteNonQuery();
});
// WithConnection returns false only when the logger has been
// disposed; the event simply cannot be persisted in that case.
pending.Completion.TrySetResult();
if (written)
{
pending.Completion.TrySetResult();
}
else
{
// WithConnection returns false only when the logger has been
// disposed mid-drain; the event was not persisted. Fault the
// Task (SiteEventLogging-012) instead of reporting false
// success for a dropped audit event.
pending.Completion.TrySetException(
new ObjectDisposedException(nameof(SiteEventLogger),
"Event could not be recorded: the event logger was disposed before the write completed."));
}
}
catch (Exception ex)
{