- new collector instance-config: collation, edition, version, services and
accounts (sys.dm_server_services, 2008 R2 SP1+), Instant File
Initialization and Lock Pages in Memory (where the build has the
columns), tempdb files, default paths; plus a fingerprint of the whole
setup (sp_configure, trace flags, services, version, tempdb files)
- changes are computed against the previous collection, which the agent
restores from the repository after a restart; migration 0008 keeps them
in insight.ConfigurationChanges (as long as resolved findings)
- SQLI-CFG-013 "Konfiguration ændret": information for a day when
sp_configure, a trace flag or a service account changes (account names
are sensitive evidence)
- page /instanser/{navn}/konfiguration: instance, CPU and memory, services,
tempdb, trace flags, non-default sp_configure (default vs. in use vs.
configured) and the change log; linked from the instance page
- the management report lists configuration changes in the period
- trace flag reading shared by trace-flags and instance-config
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
444 lines
23 KiB
C#
444 lines
23 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")));
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// A setting someone changed: sp_configure, a trace flag or a service account. Information, kept open for a day, so the
|
||
/// change is visible to whoever looks (and in the change log on the configuration page for good).
|
||
/// </summary>
|
||
public sealed class ConfigurationChangedRule : Rule
|
||
{
|
||
public override string Id => "SQLI-CFG-013";
|
||
public override string Title => "Konfiguration ændret";
|
||
|
||
public override RuleEvaluation Evaluate(RuleContext context)
|
||
{
|
||
ArgumentNullException.ThrowIfNull(context);
|
||
var snapshot = context.Data.Get<InstanceConfigurationSnapshot>();
|
||
return Result(snapshot.RecentChanges
|
||
.Where(c => ConfigurationFingerprint.IsNotable(c.Area))
|
||
.Select(c =>
|
||
{
|
||
var account = c.Area == ConfigurationFingerprint.ServiceAccount;
|
||
var what = c.Area == ConfigurationFingerprint.TraceFlag ? $"Trace flag {c.Name}" : c.Name;
|
||
var title = (c.OldValue, c.NewValue) switch
|
||
{
|
||
(null, _) when c.Area == ConfigurationFingerprint.TraceFlag => $"{what} er slået til",
|
||
(_, null) when c.Area == ConfigurationFingerprint.TraceFlag => $"{what} er slået fra",
|
||
_ when account => $"Tjenestekontoen for {what} er ændret",
|
||
_ => $"{what} er ændret fra {c.OldValue ?? "–"} til {c.NewValue ?? "–"}",
|
||
};
|
||
return CreateFinding(
|
||
Severity.Info,
|
||
title,
|
||
"En indstilling på instansen er ændret. Det er ikke nødvendigvis et problem, men ændringer af serverindstillinger, "
|
||
+ "trace flags og tjenestekonti bør være planlagte og kendte, fordi de påvirker hele instansen.",
|
||
"Bekræft, at ændringen var planlagt, og at den er dokumenteret. Hele historikken står under Konfiguration på instansens side.",
|
||
[
|
||
new Evidence("Område", c.Area), new Evidence("Indstilling", c.Name),
|
||
new Evidence("Fra", c.OldValue ?? "–", Sensitive: account), new Evidence("Til", c.NewValue ?? "–", Sensitive: account),
|
||
new Evidence("Opdaget", c.DetectedAt.UtcDateTime.ToString("yyyy-MM-dd HH:mm 'UTC'", CultureInfo.InvariantCulture)),
|
||
],
|
||
subject: $"config: {c.Area}/{c.Name}/{c.DetectedAt.UtcDateTime:yyyyMMddHHmm}");
|
||
}));
|
||
}
|
||
}
|