sqlinsight-pro/src/SQLInsight.Core/Configuration/DatabaseSelection.cs
Søren Eggert Lundsteen Olsen 4ebae4efe6 Dynamics AX core: central detector, per-version schema adapters, read-access GRANT
One place recognises AX (version 3.0/4.0/2009/2012/2012 R2/F&O, schema owner,
model store) from metadata only. Per-version adapters hold all table/column
knowledge, check every column in sys.columns before use and degrade to
'ikke målbar' with the exact column or the exact GRANT text. Only AX 3.0 is
verified against a real customer schema; 2009/2012 are written from
Microsoft's documented schema (docs/dynamics-ax/*.documented.tsv, replaced
by a real export when one is dropped in). grant-dynamics-ax-read.sql is
version- and schema-aware (bmssa) and covers exactly the adapters' tables.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:52:58 +02:00

101 lines
4.9 KiB
C#

namespace SQLInsight.Core.Configuration;
/// <summary>
/// Which databases on an instance are monitored, as chosen in SQLInsight Web (Opsætning → Overvågede servere). The
/// system databases (master, model, msdb, tempdb) are always monitored: backups of master and msdb and the state of
/// tempdb concern the whole instance. A database nobody has decided on yet (new since the last discovery) follows
/// <see cref="MonitorNew"/>.
/// </summary>
public sealed class DatabaseSelection
{
private static readonly HashSet<string> SystemDatabases = new(["master", "model", "msdb", "tempdb"], StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _monitored;
private readonly HashSet<string> _notMonitored;
private readonly HashSet<string> _dynamicsAxToHub;
/// <summary>Every database, and no Dynamics AX findings to the hub: what 1.2 and earlier did.</summary>
public static readonly DatabaseSelection All = new(true, [], [], []);
public DatabaseSelection(bool monitorNew, IEnumerable<string> monitored, IEnumerable<string> notMonitored, IEnumerable<string> dynamicsAxToHub)
{
ArgumentNullException.ThrowIfNull(monitored);
ArgumentNullException.ThrowIfNull(notMonitored);
ArgumentNullException.ThrowIfNull(dynamicsAxToHub);
MonitorNew = monitorNew;
_monitored = new HashSet<string>(monitored, StringComparer.OrdinalIgnoreCase);
_notMonitored = new HashSet<string>(notMonitored, StringComparer.OrdinalIgnoreCase);
_notMonitored.ExceptWith(_monitored);
_dynamicsAxToHub = new HashSet<string>(dynamicsAxToHub, StringComparer.OrdinalIgnoreCase);
}
/// <summary>Databases not yet decided on (new on the instance) are monitored.</summary>
public bool MonitorNew { get; }
/// <summary>Nothing is left out: every database is monitored.</summary>
public bool IsEverything => MonitorNew && _notMonitored.Count == 0;
public static bool IsSystemDatabase(string database) => SystemDatabases.Contains(database);
/// <summary>Whether the database-specific checks run in <paramref name="database"/>.</summary>
public bool Includes(string database)
{
ArgumentNullException.ThrowIfNull(database);
if (IsSystemDatabase(database) || _monitored.Contains(database))
{
return true;
}
return !_notMonitored.Contains(database) && MonitorNew;
}
/// <summary>
/// Whether findings about Dynamics AX in <paramref name="database"/> may be sent to SQLInsight Hub. Off unless the
/// customer turned it on for that database.
/// </summary>
public bool SendsDynamicsAxToHub(string? database) => database is not null && _dynamicsAxToHub.Contains(database) && Includes(database);
/// <summary>At least one database sends Dynamics AX findings to the hub.</summary>
public bool AnyDynamicsAxToHub => _dynamicsAxToHub.Any(Includes);
/// <summary>Keeps the databases in <paramref name="names"/> that are monitored.</summary>
public List<string> Filter(IEnumerable<string> names) => names.Where(Includes).ToList();
}
/// <summary>
/// Dynamics AX findings and SQLInsight Hub: they stay in the customer's network unless sending is turned on for the
/// database (default off).
/// </summary>
public static class DynamicsAxFindings
{
/// <summary>The community AX rules (rules/community/dynamics-ax.json).</summary>
public const string RulePrefix = "COM-AX-";
/// <summary>The built-in AX rules (1.4: batch jobs, number sequences, RecId, AX users behind blocking).</summary>
public const string BuiltInRulePrefix = "SQLI-AX-";
private const string DatabaseSubject = "database: ";
/// <summary>Separates the database from the rest of an AX finding's subject: <c>database: AX30 · batch: 4711</c>.</summary>
public const string SubjectSeparator = " · ";
public static bool IsDynamicsAxRule(string ruleId) =>
ruleId?.StartsWith(RulePrefix, StringComparison.OrdinalIgnoreCase) == true || ruleId?.StartsWith(BuiltInRulePrefix, StringComparison.OrdinalIgnoreCase) == true;
/// <summary>The subject of an AX finding about <paramref name="detail"/> in <paramref name="database"/>.</summary>
public static string Subject(string database, string? detail = null) =>
detail is null ? DatabaseSubject + database : DatabaseSubject + database + SubjectSeparator + detail;
/// <summary>The database a finding is about, from its subject (<c>database: AX30</c> or <c>database: AX30 · …</c>), or null.</summary>
public static string? DatabaseOf(string? subject)
{
if (subject is null || !subject.StartsWith(DatabaseSubject, StringComparison.OrdinalIgnoreCase) || subject.Length <= DatabaseSubject.Length)
{
return null;
}
var rest = subject[DatabaseSubject.Length..];
var separator = rest.IndexOf(SubjectSeparator, StringComparison.Ordinal);
return separator > 0 ? rest[..separator] : rest;
}
}