sqlinsight-pro/src/SQLInsight.Rules/Custom/DeclarativeRule.cs
Søren Eggert Lundsteen Olsen 6b945295d0 Dynamics AX collectors: shared detector, batch jobs, number sequences and RecId
dynamics-ax (hourly) lists the monitored AX databases with version, schema,
companies and read access; discovery and the AX community rules (now marked
dynamicsAx) use the same detection. dynamics-ax-batch (5 min) reads BATCH
(3.0/4.0) or BATCHJOB/BATCHJOBHISTORY (2009+), keeps each job's own run
history; dynamics-ax-sequences (hourly) reads NUMBERSEQUENCETABLE and
SYSTEMSEQUENCES and forecasts from SQLInsight's own daily samples. All reads
run through [db].sys.sp_executesql under read committed with a 2 s lock
timeout (snapshot when allowed), bounded with TOP, never NOLOCK, never
written. Blocking now captures CONTEXT_INFO for the AX user mapping.

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

71 lines
3.2 KiB
C#

using SQLInsight.Core.Collection;
using SQLInsight.Core.CustomRules;
using SQLInsight.Core.Findings;
using SQLInsight.Core.Instances;
using SQLInsight.Rules.Engine;
namespace SQLInsight.Rules.Custom;
/// <summary>A rule defined as data (<see cref="CustomRuleDefinition"/>): every row its query returns is a finding.</summary>
public sealed class DeclarativeRule(CustomRuleDefinition definition) : IRule
{
public CustomRuleDefinition Definition { get; } = definition ?? throw new ArgumentNullException(nameof(definition));
public string Id => Definition.Id;
public string Title => Definition.Title;
public IReadOnlyCollection<Capability> RequiredCapabilities => [];
public RuleEvaluation Evaluate(RuleContext context)
{
ArgumentNullException.ThrowIfNull(context);
if (context.Instance.Version < Definition.MinimumSqlVersion)
{
return RuleEvaluation.NotApplicable($"Ikke relevant for {context.Instance.Release.DisplayName()}: reglen kræver SQL Server {Definition.MinimumVersion} eller nyere.");
}
var results = context.Data.Get<CustomQueryResults>();
if (!results.Results.TryGetValue(Definition.Id, out var result))
{
throw new DataUnavailableException("Reglens forespørgsel er ikke kørt endnu.");
}
if (result.NotRelevant is { } notRelevant)
{
return RuleEvaluation.NotApplicable(notRelevant);
}
if (result.Error is { } error)
{
if (result.PermissionDenied)
{
return RuleEvaluation.NotMeasurable("Agentens login mangler rettigheder til reglens forespørgsel: " + error);
}
throw new InvalidOperationException("Reglens forespørgsel fejlede: " + error);
}
if (result.Rows.Count == 0)
{
return RuleEvaluation.Passed();
}
var references = Definition.References.Select(r => new Reference(r.Title, r.Url)).ToArray();
var findings = result.Rows.Select(row => new Finding
{
RuleId = Definition.Id,
Severity = Definition.Severity,
Title = CustomRuleDefinition.Fill(Definition.FindingTitle, row),
Subject = Definition.Subject is { } subject ? CustomRuleDefinition.Fill(subject, row) : row.Values.FirstOrDefault(),
Explanation = CustomRuleDefinition.Fill(Definition.Explanation, row)
+ (Definition.Origin == "community" ? " (Community-regel" + (Definition.Author is { } a ? $" af {a}" : "") + ".)" : " (Lokal regel.)"),
Recommendation = CustomRuleDefinition.Fill(Definition.Recommendation, row),
Evidence = row
.Where(c => Definition.Evidence is null || Definition.Evidence.Contains(c.Key, StringComparer.OrdinalIgnoreCase))
.Select(c => new Evidence(c.Key, c.Value ?? "(null)", Definition.SensitiveColumns.Contains(c.Key, StringComparer.OrdinalIgnoreCase)))
.Concat(result.Truncated ? [new Evidence("Bemærk", $"Kun de første {CustomRuleDefinition.MaxRowsPerRule} rækker er medtaget.")] : [])
.ToList(),
References = references,
}).ToArray();
return RuleEvaluation.Found(findings);
}
}