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>
101 lines
4.9 KiB
C#
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;
|
|
}
|
|
}
|