using System.Text.RegularExpressions; namespace Tango.Portal.Chat.Web.Services { public sealed class KqlGuard { private static readonly string[] Banned = new[] { "externaldata", "evaluate", "cluster(", "database(", "ingest", "datatable", "delete", "drop", "truncate", "update", "set", "materializedview", "mv-merge", "alter", "create", "append", "ingestiontime()", ".show", ".set", ".clear", ".drop", ".alter" }; public KqlValidationResult Validate(string kql) { var text = kql.ToLowerInvariant(); foreach (var token in Banned) { var pattern = $@"\b{Regex.Escape(token)}\b"; if (Regex.IsMatch(text, pattern, RegexOptions.IgnoreCase)) return KqlValidationResult.Fail($"Query uses banned token: {token}"); } // Ensure only allowed tables are referenced (quick heuristic) //var tableNames = new HashSet(allowTables.Select(t => t.ToLowerInvariant())); //var tableRefs = Regex.Matches(text, @"\b([A-Za-z_][A-Za-z0-9_]*)\bTable").Cast().Select(m => m.Value.ToLowerInvariant().Replace("table", "")); //foreach (var tr in tableRefs) // if (!tableNames.Contains(tr)) // return KqlValidationResult.Fail($"Query references non-allowlisted table: {tr}"); // Encourage summarize/top/take to avoid huge result sets //if (!(text.Contains("summarize") || text.Contains("| take ") || text.Contains("| top "))) // return KqlValidationResult.Fail("Query must include summarize/top/take to limit results."); return KqlValidationResult.Ok(); } } public readonly struct KqlValidationResult { public bool IsOk { get; } public string? Error { get; } private KqlValidationResult(bool ok, string? error) { IsOk = ok; Error = error; } public static KqlValidationResult Ok() => new(true, null); public static KqlValidationResult Fail(string error) => new(false, error); } }