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;
}
}