Dynamics AX: notice a GRANT within minutes, one AX lookup per custom-rule run
The detector looks again every 5 minutes at an AX database where the agent lacks part of the read access, so a GRANT takes effect quickly. The AX SQL test checks that only standard AX tables are read. The hub end-to-end test expects no AX rule outcomes without the opt-in. Docs: what 1.5's synthetic AX checks can reuse. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
cd72f14883
commit
08cd741b52
9 changed files with 103 additions and 89 deletions
|
|
@ -17,7 +17,7 @@ En database er AX, når systemtabellerne `SQLDICTIONARY` og `DATAAREA` ligger i
|
|||
| `PARTITION` og modeldatabasen `<db>_model` på instansen | AX 2012 R2/R3 |
|
||||
| `PARTITION`, intet modellager | Dynamics 365 F&O on-premises (formodet) |
|
||||
|
||||
Genkendelsen læser kun metadata (`sys.tables`, `sys.columns`, `HAS_PERMS_BY_NAME`) og kræver ikke mere end [`grant-least-privilege.sql`](../db/permissions/grant-least-privilege.sql). Den kører hver time i de databaser, der er valgt til overvågning. **AX-tjek kører kun i databaser, der er genkendt som AX og valgt til overvågning**; de generelle tjek kører i alle valgte databaser som hidtil.
|
||||
Genkendelsen læser kun metadata (`sys.tables`, `sys.columns`, `HAS_PERMS_BY_NAME`) og kræver ikke mere end [`grant-least-privilege.sql`](../db/permissions/grant-least-privilege.sql). Den kører hver time i de databaser, der er valgt til overvågning (hvert 5. minut for en AX-database, hvor agenten endnu ikke har den fulde læseadgang, så en GRANT virker inden for få minutter). **AX-tjek kører kun i databaser, der er genkendt som AX og valgt til overvågning**; de generelle tjek kører i alle valgte databaser som hidtil.
|
||||
|
||||
## Valgfri læseadgang til AX-tabellerne
|
||||
|
||||
|
|
@ -119,3 +119,7 @@ ORDER BY s.name, t.name, c.column_id;
|
|||
```
|
||||
|
||||
og gemmes tabulatorsepareret som `docs/dynamics-ax/schema-ax2009.tsv` eller `schema-ax2012.tsv`. Testene (`AxSchemaAdapterTests`, `DynamicsAxSqlTests`, `SqlScriptTests`) bruger så det rigtige skema i stedet for det dokumenterede, og viser præcis hvilke kolonner versionens adapter ([`AxSchemaAdapter.cs`](../src/SQLInsight.Core/DynamicsAx/AxSchemaAdapter.cs)) forventer, men som mangler. Al viden om en versions tabeller ligger i dens adapter; ret den dér, og opdatér teksterne "ikke verificeret mod kundedata" for versionen (`AxVersions.IsVerified`).
|
||||
|
||||
## Genbrug i andre tjek
|
||||
|
||||
Et syntetisk AX-logon-tjek (fase 1.5) hører ikke til her, men kan genbruge AX-viden herfra i stedet for at kende AX' tabeller selv: `GET /api/v1/dynamics-ax/databases` (eller snapshot'et `dynamics-ax` i repository'et, typen `AxDatabaseSnapshot`) giver for hver overvåget AX-database version, skema, firmaer (`DATAAREA`) og modeldatabase; `AxDetection` og `AxSchemaAdapter` i `SQLInsight.Core.DynamicsAx` er den eneste kilde til, hvilke tabeller og kolonner en version har; og `AxSessionMapper` oversætter en SQL-session til AX-bruger og AX-session, hvor AOS'en sætter `CONTEXT_INFO`.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ public sealed class CustomQueryCollector(CustomRuleCatalog catalog, DynamicsAx.A
|
|||
try
|
||||
{
|
||||
List<string>? databases = null;
|
||||
HashSet<string>? axDatabases = null;
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (rule.Scope == CustomRuleScope.Server)
|
||||
|
|
@ -53,7 +54,7 @@ public sealed class CustomQueryCollector(CustomRuleCatalog catalog, DynamicsAx.A
|
|||
if (rule.DynamicsAx)
|
||||
{
|
||||
// The AX detection needs SQL Server 2005 (sys.columns); AX rules have nowhere to run before that.
|
||||
var axDatabases = context.Instance.Version.Major >= 9
|
||||
axDatabases ??= context.Instance.Version.Major >= 9
|
||||
? (await _ax.GetAsync(context).ConfigureAwait(false)).Databases.Select(d => d.Database).ToHashSet(StringComparer.OrdinalIgnoreCase)
|
||||
: [];
|
||||
var targets = databases.Where(axDatabases.Contains).ToList();
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ public sealed class AxDatabaseDetector
|
|||
{
|
||||
public static readonly TimeSpan Refresh = TimeSpan.FromHours(1);
|
||||
|
||||
/// <summary>An AX database without the full read access is looked at again sooner, so a GRANT takes effect within minutes.</summary>
|
||||
public static readonly TimeSpan RefreshWithoutReadAccess = TimeSpan.FromMinutes(5);
|
||||
|
||||
private static TimeSpan RefreshFor(AxDatabase? database) => database is not null && AxReadAccess.Missing(database).Count > 0 ? RefreshWithoutReadAccess : Refresh;
|
||||
|
||||
private readonly Dictionary<string, (DateTimeOffset At, AxDatabase? Database, string? Problem)> _cache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private string? _login;
|
||||
|
||||
|
|
@ -44,7 +49,7 @@ public sealed class AxDatabaseDetector
|
|||
continue;
|
||||
}
|
||||
|
||||
if (force || !_cache.TryGetValue(name, out var cached) || context.Now - cached.At >= Refresh)
|
||||
if (force || !_cache.TryGetValue(name, out var cached) || context.Now - cached.At >= RefreshFor(cached.Database))
|
||||
{
|
||||
cached = await DetectAsync(context, name, names).ConfigureAwait(false);
|
||||
_cache[name] = cached;
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ public sealed class DynamicsAxSqlTests
|
|||
{
|
||||
Assert.DoesNotContain(DiscoveryTests.Sql2008Only, p => p.IsMatch(sql));
|
||||
Assert.DoesNotContain("NOLOCK", sql, StringComparison.OrdinalIgnoreCase);
|
||||
Assert.DoesNotContain("PXI_", sql, StringComparison.OrdinalIgnoreCase);
|
||||
// Only AX's own standard tables, never anything else.
|
||||
var tables = System.Text.RegularExpressions.Regex.Matches(sql, @"\[(?:bmssa|dbo)\]\.\[(?<t>[A-Z_]+)\]").Select(m => m.Groups["t"].Value).ToList();
|
||||
Assert.All(tables, t => Assert.Contains(t, AxDetection.KnownTables));
|
||||
Assert.DoesNotContain("USE ", sql, StringComparison.Ordinal);
|
||||
Assert.DoesNotMatch(@"\b(INSERT|UPDATE|DELETE|MERGE|TRUNCATE|ALTER|CREATE|DROP|EXEC\s*\()\b", sql.Replace("sp_executesql", string.Empty, StringComparison.Ordinal));
|
||||
Assert.StartsWith("EXEC [AXDB].sys.sp_executesql", sql, StringComparison.Ordinal);
|
||||
|
|
|
|||
|
|
@ -61,7 +61,6 @@ public sealed class SqlScriptTests
|
|||
Assert.Contains("Versioner:", header, StringComparison.Ordinal);
|
||||
Assert.Contains("Collector:", header, StringComparison.Ordinal);
|
||||
Assert.Contains("ikke verificeret mod kundedata", header, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("PXI_", header, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>File name → the generated SQL.</summary>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ public class HubEndToEndTests(SqlServerFixture sql, HubFixture hub) : IClassFixt
|
|||
Assert.Null(instance.ServerName); // not sent by default
|
||||
var findings = await db.Findings.Where(f => f.InstanceId == instance.Id && f.ResolvedAt == null).ToListAsync();
|
||||
Assert.Contains(findings, f => f.RuleId == "SQLI-CFG-001");
|
||||
Assert.True(await db.RuleStates.CountAsync(r => r.InstanceId == instance.Id) >= BuiltInRules.Create().Count);
|
||||
// Every built-in rule's outcome, except the Dynamics AX rules: no database has AX findings to the hub turned on.
|
||||
Assert.True(await db.RuleStates.CountAsync(r => r.InstanceId == instance.Id)
|
||||
>= BuiltInRules.Create().Count(r => !Core.Configuration.DynamicsAxFindings.IsDynamicsAxRule(r.Id)));
|
||||
Assert.False(await db.RuleStates.AnyAsync(r => r.InstanceId == instance.Id && r.RuleId.StartsWith("SQLI-AX-")));
|
||||
|
||||
var latest = Assert.Single(await _repository.GetLatestSubmissionsAsync());
|
||||
Assert.Equal("e2e-sql01", latest.InstanceName);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue