using System.Globalization; using System.Text; using SQLInsight.Core.Data; namespace SQLInsight.Core.DynamicsAx; /// Who in AX is behind a SQL Server session, as far as SQL Server can tell. /// The AX user id, when the AOS put it in CONTEXT_INFO. /// The AX session id, when the AOS put it in CONTEXT_INFO. /// "context_info", or "ikke tilgængelig". /// How it was found, or why it cannot be (Danish). /// CONTEXT_INFO as text, when it is printable. public sealed record AxSessionIdentity(string? AxUser, int? AxSessionId, string Source, string Explanation, string? RawContext = null) { public bool Known => AxUser is not null; } /// /// Maps SQL Server sessions to Dynamics AX users. All AX users reach SQL Server through the AOS's own login, so the /// login says nothing about the AX user. AX 2009 and 2012 (and F&O) AOS can write the AX user id and AX session id to /// CONTEXT_INFO on every connection when "connectioncontext" is turned on in the AOS configuration (registry); Microsoft /// documents this for AX 2012 – the exact text layout is not verified against customer data, so the user is taken as the /// first token with a letter and the session as the first number. AX 3.0 and 4.0 never set CONTEXT_INFO: the AX user /// cannot be found from SQL Server, only the host (the AOS in a 3-tier installation, the user's own PC for a 2-tier /// client) and the program. /// public static class AxSessionMapper { public static AxSessionIdentity Map(SessionInfo session, AxVersion version) { ArgumentNullException.ThrowIfNull(session); var text = Decode(session.ContextInfo); if (version is AxVersion.Axapta30 or AxVersion.Ax40) { return new AxSessionIdentity(null, null, "ikke tilgængelig", // No host or program name here: the explanation is not sensitive (it may reach SQLInsight Hub); they are // in the finding's sensitive evidence "Vært / program". $"{AxVersions.Text(version)} sætter ikke CONTEXT_INFO, så AX-brugeren kan ikke ses fra SQL Server. Værten under målingerne er " + "AOS'en ved en 3-lags-installation og brugerens egen pc ved en 2-lags-klient. Brugeren kan findes i AX under Online brugere ud fra " + "tidspunktet og værten.", text); } if (text is not null) { var tokens = text.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); var user = tokens.FirstOrDefault(t => t.Any(char.IsLetter)); int? axSession = tokens.FirstOrDefault(t => t.All(char.IsAsciiDigit)) is { } digits && int.TryParse(digits, NumberStyles.None, CultureInfo.InvariantCulture, out var id) ? id : null; if (user is not null) { return new AxSessionIdentity(user, axSession, "context_info", "Fra CONTEXT_INFO, som AOS'en sætter, når connectioncontext er slået til (første ord med bogstaver er taget som AX-brugeren; " + "tekstformatet er dokumenteret af Microsoft for AX 2012, men ikke verificeret mod kundedata – se den rå CONTEXT_INFO under målingerne).", text); } } return new AxSessionIdentity(null, null, "ikke tilgængelig", "AOS'en har ikke sat CONTEXT_INFO på sessionen. Slå connectioncontext til i AOS-konfigurationen (registreringsdatabasen på AOS-serveren, " + "kræver genstart af AOS) for at se AX-brugeren bag SQL-sessioner.", text); } /// CONTEXT_INFO hex as printable text (ASCII, or UTF-16 when every second byte is zero), or null. public static string? Decode(string? hex) { if (string.IsNullOrWhiteSpace(hex)) { return null; } byte[] bytes; try { bytes = Convert.FromHexString(hex.StartsWith("0x", StringComparison.OrdinalIgnoreCase) ? hex[2..] : hex); } catch (FormatException) { return null; } var length = bytes.Length; while (length > 0 && bytes[length - 1] == 0) { length--; } if (length == 0) { return null; } var unicode = length >= 4 && Enumerable.Range(0, (length + 1) / 2).All(i => 2 * i + 1 >= length || bytes[2 * i + 1] == 0); var text = unicode ? Encoding.Unicode.GetString(bytes, 0, Math.Min(bytes.Length, length + (length % 2))) : Encoding.Latin1.GetString(bytes, 0, length); var printable = new string(text.Select(c => char.IsControl(c) ? ' ' : c).ToArray()).Trim(); return printable.Length == 0 || printable.Count(c => char.IsAsciiLetterOrDigit(c) || c is ' ' or '\\' or '-' or '_' or '.' or '@' or 'æ' or 'ø' or 'å' or 'Æ' or 'Ø' or 'Å') < printable.Length * 0.8 ? null : printable; } }