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>
71 lines
3.2 KiB
C#
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);
|
|
}
|
|
}
|