sqlinsight-pro/src/SQLInsight.Core/DynamicsAx/AxSessionMapper.cs
Søren Eggert Lundsteen Olsen f4d2603181 Review 1.4: AX texts in API v1 and the hub without logins or host names; API has what the pages show
- "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>
2026-10-02 14:29:34 +02:00

101 lines
5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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