- "Ikke målbar" texts, notes and detection problems carry the GRANT text with the agent's login and database user. API v1 (batch-jobs, number-sequences, recids, databases, rule-states) returned them as they were, although logins are redacted everywhere else. AxReadAccess.WithoutPrincipals replaces them with the placeholder unless sensitive values are allowed; the grant script also hides a database user that differs from the login. - SQLI-AX-010 put the host and program name (in 2-tier AX 3.0 the user's own PC) in the finding's explanation, which reaches the hub unredacted for an opted-in database. They are only in the sensitive evidence now, with the raw CONTEXT_INFO; the CONTEXT_INFO parsing says it is not verified against customer data. - A database named e.g. "AX · test" was read from a subject as "AX", whose hub opt-in then applied. Middle dots in the database are doubled in AX subjects, and the AX community rules' subjects are built the same way. - API v1 now also has the detection problems (/detection-problems), what each raw batch STATUS means, and the thresholds behind the number sequence and RecId pages; the blocking explanation is no longer redacted (it holds nothing sensitive now). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
101 lines
5 KiB
C#
101 lines
5 KiB
C#
using System.Globalization;
|
||
using System.Text;
|
||
using SQLInsight.Core.Data;
|
||
|
||
namespace SQLInsight.Core.DynamicsAx;
|
||
|
||
/// <summary>Who in AX is behind a SQL Server session, as far as SQL Server can tell.</summary>
|
||
/// <param name="AxUser">The AX user id, when the AOS put it in CONTEXT_INFO.</param>
|
||
/// <param name="AxSessionId">The AX session id, when the AOS put it in CONTEXT_INFO.</param>
|
||
/// <param name="Source">"context_info", or "ikke tilgængelig".</param>
|
||
/// <param name="Explanation">How it was found, or why it cannot be (Danish).</param>
|
||
/// <param name="RawContext">CONTEXT_INFO as text, when it is printable.</param>
|
||
public sealed record AxSessionIdentity(string? AxUser, int? AxSessionId, string Source, string Explanation, string? RawContext = null)
|
||
{
|
||
public bool Known => AxUser is not null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 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.
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>CONTEXT_INFO hex as printable text (ASCII, or UTF-16 when every second byte is zero), or null.</summary>
|
||
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;
|
||
}
|
||
}
|