Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Waiting to run
- Managed Instance reports 12.0.2000.8 but is evergreen: treated as the newest engine with all capabilities; new SelfManagedInfrastructure capability so backup, CHECKDB, files, tempdb, memory, patch level and lifecycle rules report 'handled by Azure' instead of false findings - Microsoft Entra ID login (managed identity, service principal, default credential chain) via SqlConnection.AccessTokenCallback - Least-privilege script recognizes Managed Instance; deadlock collector falls back to the ring buffer when event files cannot be read - Web UI shows the right release for Managed Instance - README and installation docs: notifications and Managed Instance - Catalog test: 61 rules with unique, well-formed ids Integration suite green on SQL Server 2017 and 2022. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
402 lines
20 KiB
C#
402 lines
20 KiB
C#
using System.Globalization;
|
|
using SQLInsight.Core.Collection;
|
|
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Core.Instances;
|
|
using SQLInsight.Rules.Engine;
|
|
|
|
namespace SQLInsight.Rules.Configuration;
|
|
|
|
/// <summary>
|
|
/// A server option with a value that is a known problem. Declarative, so each such piece of expertise is a few lines.
|
|
/// </summary>
|
|
public sealed class ConfigurationOptionRule(
|
|
string id,
|
|
string option,
|
|
Func<long, bool> isProblem,
|
|
Severity severity,
|
|
string title,
|
|
string explanation,
|
|
string recommendation,
|
|
long? recommendedValue,
|
|
Reference reference,
|
|
Func<RuleContext, bool>? appliesTo = null,
|
|
IReadOnlyCollection<Capability>? requires = null) : Rule
|
|
{
|
|
public override string Id => id;
|
|
public override string Title => title;
|
|
public override IReadOnlyCollection<Capability> RequiredCapabilities => requires ?? [];
|
|
|
|
public override RuleEvaluation Evaluate(RuleContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
if (appliesTo is not null && !appliesTo(context))
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
var value = ConfigurationValues.Get(context, option);
|
|
if (!isProblem(value.ValueInUse))
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
return RuleEvaluation.Found(CreateFinding(
|
|
severity,
|
|
title,
|
|
explanation,
|
|
recommendation,
|
|
[new Evidence(option, value.ValueInUse.ToString(CultureInfo.InvariantCulture))],
|
|
fixScript: recommendedValue is { } v ? ConfigurationValues.FixScript(option, v) : null,
|
|
references: reference));
|
|
}
|
|
}
|
|
|
|
internal static class ConfigurationValues
|
|
{
|
|
public static ConfigurationOption Get(RuleContext context, string option) =>
|
|
context.Data.Get<ConfigurationSnapshot>().Find(option)
|
|
?? throw new DataUnavailableException($"Indstillingen '{option}' findes ikke på {context.Instance.Release.DisplayName()}.");
|
|
|
|
public static string FixScript(string option, long value) => $"""
|
|
EXEC sys.sp_configure N'show advanced options', 1;
|
|
RECONFIGURE;
|
|
EXEC sys.sp_configure N'{option}', {value.ToString(CultureInfo.InvariantCulture)};
|
|
RECONFIGURE;
|
|
""";
|
|
}
|
|
|
|
/// <summary>
|
|
/// The configuration rules that need no more than one option value.
|
|
/// </summary>
|
|
public static class SimpleConfigurationRules
|
|
{
|
|
public static IEnumerable<IRule> Create() =>
|
|
[
|
|
new ConfigurationOptionRule(
|
|
"SQLI-CFG-006", "priority boost", v => v != 0, Severity.High,
|
|
"Priority boost er slået til",
|
|
"Priority boost hæver SQL Servers trådprioritet over resten af Windows. Det kan udsulte operativsystemet, "
|
|
+ "netværk og cluster-tjenester, så serveren bliver ustabil eller en failover-cluster tror, noden er død. "
|
|
+ "Microsoft har markeret indstillingen som forældet.",
|
|
"Slå priority boost fra. Ændringen kræver genstart af SQL Server-tjenesten.",
|
|
0, Docs.Learn("priority boost", "database-engine/configure-windows/configure-the-priority-boost-server-configuration-option"),
|
|
requires: [Capability.SelfManagedInfrastructure]),
|
|
|
|
new ConfigurationOptionRule(
|
|
"SQLI-CFG-007", "lightweight pooling", v => v != 0, Severity.High,
|
|
"Lightweight pooling (fiber mode) er slået til",
|
|
"Fiber mode får SQL Server til at planlægge fibre i stedet for tråde. Det understøttes ikke af blandt andet CLR, "
|
|
+ "distribuerede transaktioner og mange komponenter, og det giver sjældent en målbar gevinst på moderne hardware.",
|
|
"Slå lightweight pooling fra, medmindre Microsoft Support har anbefalet det til netop denne workload.",
|
|
0, Docs.Learn("lightweight pooling", "database-engine/configure-windows/lightweight-pooling-server-configuration-option"),
|
|
requires: [Capability.SelfManagedInfrastructure]),
|
|
|
|
new ConfigurationOptionRule(
|
|
"SQLI-CFG-008", "xp_cmdshell", v => v != 0, Severity.Medium,
|
|
"xp_cmdshell er slået til",
|
|
"xp_cmdshell lader T-SQL køre vilkårlige kommandoer i Windows med SQL Server-tjenestekontoens rettigheder. "
|
|
+ "Et SQL injection-hul eller en kompromitteret sysadmin-konto bliver dermed til adgang til hele serveren.",
|
|
"Slå xp_cmdshell fra. Opgaver der kræver kommandolinjen, bør køre som SQL Agent-jobtrin med en proxy-konto.",
|
|
0, Docs.Learn("xp_cmdshell", "database-engine/configure-windows/xp-cmdshell-server-configuration-option"),
|
|
requires: [Capability.SelfManagedInfrastructure]),
|
|
|
|
new ConfigurationOptionRule(
|
|
"SQLI-CFG-009", "backup compression default", v => v == 0, Severity.Low,
|
|
"Backup-komprimering er ikke standard",
|
|
"Komprimerede backups er typisk 3-5 gange mindre og hurtigere, fordi flaskehalsen oftest er I/O og ikke CPU. "
|
|
+ "Uden standardindstillingen skal hvert backup-job selv huske WITH COMPRESSION.",
|
|
"Slå backup compression default til. Undlad kun, hvis databaserne allerede er krypteret med TDE før 2016 "
|
|
+ "eller data i forvejen er komprimeret.",
|
|
1, Docs.Learn("backup compression default", "database-engine/configure-windows/view-or-configure-the-backup-compression-default-server-configuration-option"),
|
|
c => c.Instance.EngineEdition != EngineEdition.Express,
|
|
[Capability.SelfManagedInfrastructure]),
|
|
|
|
new ConfigurationOptionRule(
|
|
"SQLI-CFG-010", "remote admin connections", v => v == 0, Severity.Low,
|
|
"Dedicated Admin Connection kan kun bruges lokalt",
|
|
"Når SQL Server hænger (fx ved worker thread exhaustion), er Dedicated Admin Connection (DAC) ofte eneste vej ind. "
|
|
+ "Som standard virker den kun fra selve serveren, som man måske ikke kan logge på i den situation.",
|
|
"Slå remote admin connections til, så DAC kan bruges med ADMIN: fra en anden maskine.",
|
|
1, Docs.Learn("remote admin connections", "database-engine/configure-windows/remote-admin-connections-server-configuration-option"),
|
|
requires: [Capability.SelfManagedInfrastructure]),
|
|
];
|
|
}
|
|
|
|
/// <summary>
|
|
/// max server memory left at the default: SQL Server may take all memory and starve Windows.
|
|
/// </summary>
|
|
public sealed class MaxServerMemoryDefaultRule : Rule
|
|
{
|
|
public const string Option = "max server memory (MB)";
|
|
|
|
public override string Id => "SQLI-CFG-001";
|
|
public override string Title => "max server memory er ikke sat";
|
|
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SelfManagedInfrastructure];
|
|
|
|
public override RuleEvaluation Evaluate(RuleContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
var value = ConfigurationValues.Get(context, Option).ValueInUse;
|
|
if (value < int.MaxValue)
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
var physical = context.Data.TryGet<ServerSnapshot>(out var server) ? server.PhysicalMemoryMb : null;
|
|
var recommended = physical is { } p ? MemoryGuidance.Recommended(p) : (long?)null;
|
|
var evidence = new List<Evidence> { new(Option, value.ToString(CultureInfo.InvariantCulture)) };
|
|
if (physical is { } mb)
|
|
{
|
|
evidence.Add(new Evidence("Fysisk hukommelse (MB)", Format.Number(mb)));
|
|
}
|
|
|
|
return RuleEvaluation.Found(CreateFinding(
|
|
Severity.High,
|
|
Title,
|
|
"Med standardværdien må SQL Server bruge al hukommelse på serveren. Buffer pool vokser, til Windows begynder at "
|
|
+ "presse den tilbage; resultatet er paging, lange pauser og i værste fald at SQL Servers hukommelse bliver trimmet.",
|
|
recommended is { } r
|
|
? $"Sæt max server memory til ca. {Format.Number(r)} MB (fysisk hukommelse minus det største af 4 GB og 10 %), "
|
|
+ "og træk mere fra, hvis andre tjenester (SSIS, SSAS, antivirus) kører på serveren."
|
|
: "Sæt max server memory til fysisk hukommelse minus det største af 4 GB og 10 %, og træk mere fra for andre tjenester.",
|
|
evidence,
|
|
fixScript: recommended is { } fix ? ConfigurationValues.FixScript(Option, fix) : null,
|
|
references: Docs.Learn("Server memory configuration", "database-engine/configure-windows/server-memory-server-configuration-options")));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// max server memory set so high that the operating system has too little left.
|
|
/// </summary>
|
|
public sealed class MaxServerMemoryTooHighRule : Rule
|
|
{
|
|
public override string Id => "SQLI-CFG-002";
|
|
public override string Title => "max server memory efterlader for lidt til Windows";
|
|
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.SelfManagedInfrastructure];
|
|
|
|
public override RuleEvaluation Evaluate(RuleContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
var value = ConfigurationValues.Get(context, MaxServerMemoryDefaultRule.Option).ValueInUse;
|
|
var physical = context.Data.Get<ServerSnapshot>().PhysicalMemoryMb
|
|
?? throw new DataUnavailableException("Fysisk hukommelse kan ikke aflæses på denne version.");
|
|
|
|
// The default is reported by SQLI-CFG-001.
|
|
if (value >= int.MaxValue || physical - value >= MemoryGuidance.Reserve(physical) / 2)
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
var recommended = MemoryGuidance.Recommended(physical);
|
|
return RuleEvaluation.Found(CreateFinding(
|
|
Severity.Medium,
|
|
Title,
|
|
$"max server memory er sat til {Format.Number(value)} MB på en server med {Format.Number(physical)} MB. "
|
|
+ "Der er ikke plads nok til Windows, tråd-stacks, CLR og andre allokeringer uden for buffer pool, så serveren risikerer paging.",
|
|
$"Sænk max server memory til ca. {Format.Number(recommended)} MB.",
|
|
[new Evidence(MaxServerMemoryDefaultRule.Option, Format.Number(value)), new Evidence("Fysisk hukommelse (MB)", Format.Number(physical))],
|
|
fixScript: ConfigurationValues.FixScript(MaxServerMemoryDefaultRule.Option, recommended),
|
|
references: Docs.Learn("Server memory configuration", "database-engine/configure-windows/server-memory-server-configuration-options")));
|
|
}
|
|
}
|
|
|
|
internal static class MemoryGuidance
|
|
{
|
|
/// <summary>Memory to leave for the OS: the larger of 4 GB and 10 %.</summary>
|
|
public static long Reserve(long physicalMb) => Math.Max(4096, physicalMb / 10);
|
|
|
|
public static long Recommended(long physicalMb) => Math.Max(physicalMb / 2, physicalMb - Reserve(physicalMb));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cost threshold for parallelism at the 1990s default of 5.
|
|
/// </summary>
|
|
public sealed class CostThresholdRule : Rule
|
|
{
|
|
public const string Option = "cost threshold for parallelism";
|
|
|
|
public override string Id => "SQLI-CFG-003";
|
|
public override string Title => "cost threshold for parallelism står på standardværdien 5";
|
|
|
|
public override RuleEvaluation Evaluate(RuleContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
var value = ConfigurationValues.Get(context, Option).ValueInUse;
|
|
var maxdop = ConfigurationValues.Get(context, "max degree of parallelism").ValueInUse;
|
|
var cpus = context.Data.Get<ServerSnapshot>().LogicalCpuCount;
|
|
if (value != 5 || maxdop == 1 || cpus <= 1)
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
return RuleEvaluation.Found(CreateFinding(
|
|
Severity.Medium,
|
|
Title,
|
|
"Standardværdien 5 blev fastlagt i 1990'erne. I dag betyder den, at selv små forespørgsler kører parallelt, "
|
|
+ "hvilket koster CPU og worker threads og ofte giver CXPACKET-ventetider uden at gøre noget hurtigere.",
|
|
"Sæt cost threshold for parallelism til 50 som udgangspunkt, og justér ud fra estimerede omkostninger i plan cache "
|
|
+ "eller Query Store. Planer, der hidtil kørte parallelt, kan ændre sig, så følg med i Query Store bagefter.",
|
|
[new Evidence(Option, "5"), new Evidence("max degree of parallelism", maxdop.ToString(CultureInfo.InvariantCulture)),
|
|
new Evidence("Logiske CPU'er", cpus.ToString(CultureInfo.InvariantCulture))],
|
|
fixScript: ConfigurationValues.FixScript(Option, 50),
|
|
references: Docs.Learn("cost threshold for parallelism", "database-engine/configure-windows/configure-the-cost-threshold-for-parallelism-server-configuration-option")));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// MAXDOP not following Microsoft's guidance for the NUMA layout.
|
|
/// </summary>
|
|
public sealed class MaxDopRule : Rule
|
|
{
|
|
public const string Option = "max degree of parallelism";
|
|
|
|
public override string Id => "SQLI-CFG-004";
|
|
public override string Title => "MAXDOP passer ikke til serverens NUMA-layout";
|
|
|
|
public override RuleEvaluation Evaluate(RuleContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
var value = ConfigurationValues.Get(context, Option).ValueInUse;
|
|
var server = context.Data.Get<ServerSnapshot>();
|
|
var schedulers = server.VisibleOnlineSchedulers ?? server.LogicalCpuCount;
|
|
var nodes = Math.Max(1, server.NumaNodeCount ?? 1);
|
|
var perNode = Math.Max(1, schedulers / nodes);
|
|
|
|
var recommended = Recommended(nodes, perNode);
|
|
var tooWide = value == 0 ? recommended < schedulers : value > recommended;
|
|
if (!tooWide || schedulers <= 1)
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
return RuleEvaluation.Found(CreateFinding(
|
|
Severity.Medium,
|
|
Title,
|
|
value == 0
|
|
? $"MAXDOP 0 lader én forespørgsel bruge alle {schedulers} schedulers. Store parallelle planer på tværs af NUMA-noder "
|
|
+ "giver fjern-hukommelsesadgang, koordineringsoverhead og kan tømme serveren for worker threads."
|
|
: $"MAXDOP {value} er højere end Microsofts vejledning for {nodes} NUMA-node(r) med {perNode} schedulers hver.",
|
|
$"Sæt MAXDOP til {recommended} efter Microsofts vejledning. På SQL Server 2016 og nyere kan det også sættes pr. database "
|
|
+ "med ALTER DATABASE SCOPED CONFIGURATION.",
|
|
[new Evidence(Option, value.ToString(CultureInfo.InvariantCulture)),
|
|
new Evidence("Schedulers", schedulers.ToString(CultureInfo.InvariantCulture)),
|
|
new Evidence("NUMA-noder", nodes.ToString(CultureInfo.InvariantCulture))],
|
|
fixScript: ConfigurationValues.FixScript(Option, recommended),
|
|
references: Docs.Learn("max degree of parallelism", "database-engine/configure-windows/configure-the-max-degree-of-parallelism-server-configuration-option")));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Microsoft's guidance (SQL Server 2016+): one NUMA node → up to 8; several nodes → the schedulers per node
|
|
/// up to 16, and with more than 16 per node half of them, at most 16.
|
|
/// </summary>
|
|
internal static int Recommended(int nodes, int perNode) => nodes <= 1
|
|
? Math.Min(8, perNode)
|
|
: perNode <= 16 ? perNode : Math.Min(16, perNode / 2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Plan cache full of single-use ad hoc plans without optimize for ad hoc workloads.
|
|
/// </summary>
|
|
public sealed class OptimizeForAdHocRule : Rule
|
|
{
|
|
public const string Option = "optimize for ad hoc workloads";
|
|
|
|
public override string Id => "SQLI-CFG-005";
|
|
public override string Title => "Plan cache er fyldt med engangsplaner";
|
|
public override IReadOnlyCollection<Capability> RequiredCapabilities => [Capability.DynamicManagementViews];
|
|
|
|
public override RuleEvaluation Evaluate(RuleContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
var value = ConfigurationValues.Get(context, Option).ValueInUse;
|
|
var cache = context.Data.Get<PlanCacheSnapshot>();
|
|
var share = cache.TotalMb > 0 ? cache.SingleUseAdhocMb / cache.TotalMb : 0;
|
|
if (value != 0 || share < 0.25 || cache.SingleUseAdhocMb < 100)
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
return RuleEvaluation.Found(CreateFinding(
|
|
Severity.Low,
|
|
Title,
|
|
$"{Format.Number(cache.SingleUseAdhocMb)} MB ({Format.Number(share * 100)} %) af plan cache er ad hoc-planer, der kun er brugt én gang. "
|
|
+ "Den hukommelse mangler i buffer pool, og applikationen sender sandsynligvis uparametriseret SQL.",
|
|
"Slå optimize for ad hoc workloads til, så en plan først gemmes helt ved anden brug. Den egentlige løsning er at "
|
|
+ "parametrisere forespørgslerne i applikationen.",
|
|
[new Evidence("Engangsplaner", Format.Number(cache.SingleUseAdhocPlans)),
|
|
new Evidence("Engangsplaner (MB)", Format.Number(cache.SingleUseAdhocMb)),
|
|
new Evidence("Plan cache i alt (MB)", Format.Number(cache.TotalMb))],
|
|
fixScript: ConfigurationValues.FixScript(Option, 1),
|
|
references: Docs.Learn("optimize for ad hoc workloads", "database-engine/configure-windows/optimize-for-ad-hoc-workloads-server-configuration-option")));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Options changed with sp_configure but not applied.
|
|
/// </summary>
|
|
public sealed class PendingConfigurationRule : Rule
|
|
{
|
|
// Configured 0 while SQL Server reports the effective minimum; not a pending change.
|
|
private static readonly HashSet<string> Ignored = new(StringComparer.OrdinalIgnoreCase) { "min server memory (MB)" };
|
|
|
|
public override string Id => "SQLI-CFG-011";
|
|
public override string Title => "Konfigurationsændringer venter på RECONFIGURE eller genstart";
|
|
|
|
public override RuleEvaluation Evaluate(RuleContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
var pending = context.Data.Get<ConfigurationSnapshot>().Options
|
|
.Where(o => o.Value != o.ValueInUse && !Ignored.Contains(o.Name))
|
|
.ToList();
|
|
if (pending.Count == 0)
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
return RuleEvaluation.Found(CreateFinding(
|
|
Severity.Low,
|
|
Title,
|
|
"Disse indstillinger er ændret med sp_configure, men den kørende værdi er en anden. Ændringen træder i kraft ved "
|
|
+ "næste RECONFIGURE eller genstart, måske uventet, fx når nogen ændrer en helt anden indstilling.",
|
|
"Afgør for hver indstilling, om ændringen er ønsket. Kør RECONFIGURE (eller genstart for statiske indstillinger), "
|
|
+ "eller sæt værdien tilbage.",
|
|
pending.Select(o => new Evidence(o.Name, $"konfigureret {o.Value}, i brug {o.ValueInUse}")),
|
|
references: Docs.Learn("Server configuration options", "database-engine/configure-windows/server-configuration-options-sql-server")));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// No way to see deadlocks on versions without system_health.
|
|
/// </summary>
|
|
public sealed class DeadlockTraceFlagRule : Rule
|
|
{
|
|
public override string Id => "SQLI-CFG-012";
|
|
public override string Title => "Deadlocks bliver ikke registreret";
|
|
|
|
public override RuleEvaluation Evaluate(RuleContext context)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(context);
|
|
if (context.Capabilities.Has(Capability.SystemHealthSession))
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
var flags = context.Data.Get<TraceFlagSnapshot>();
|
|
if (flags.IsEnabled(1222) || flags.IsEnabled(1204))
|
|
{
|
|
return RuleEvaluation.Passed();
|
|
}
|
|
|
|
return RuleEvaluation.Found(CreateFinding(
|
|
Severity.Low,
|
|
Title,
|
|
$"{context.Instance.Release.DisplayName()} har ikke system_health-sessionen, og hverken trace flag 1222 eller 1204 er slået til. "
|
|
+ "Deadlocks kan derfor ikke analyseres bagefter; applikationen ser kun fejl 1205.",
|
|
"Slå trace flag 1222 til globalt (DBCC TRACEON(1222, -1)), og tilføj -T1222 som startparameter, så det overlever genstart. "
|
|
+ "Deadlock-grafen skrives så til error log.",
|
|
[new Evidence("Globale trace flags", flags.GlobalTraceFlags.Count == 0 ? "ingen" : string.Join(", ", flags.GlobalTraceFlags))],
|
|
fixScript: "DBCC TRACEON(1222, -1);",
|
|
references: Docs.Learn("Deadlocks guide", "relational-databases/sql-server-deadlocks-guide")));
|
|
}
|
|
}
|