Fase 1: patch level rule with build catalog; README status
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 / Build og unit tests (push) Waiting to run
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
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 / Build og unit tests (push) Waiting to run
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
SQLI-VER-002 flags instances behind the newest known CU, security update or final service pack (catalog as of 2026-09-29, shipped as rule pack data and called out when older than 120 days). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
This commit is contained in:
parent
5bb1c37fef
commit
3c35f92785
7 changed files with 190 additions and 9 deletions
19
README.md
19
README.md
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
SQLInsight Pro er et overvågningsværktøj til Microsoft SQL Server, skrevet i C# på .NET. Det kører hos kunden og forlader ikke netværket, medmindre kunden selv vælger det. Værktøjet nøjes ikke med at vise tal. Det forklarer, hvad der er galt, hvorfor det er et problem, og hvordan det løses. Rådene har samme niveau som dem, man får fra en erfaren DBA.
|
SQLInsight Pro er et overvågningsværktøj til Microsoft SQL Server, skrevet i C# på .NET. Det kører hos kunden og forlader ikke netværket, medmindre kunden selv vælger det. Værktøjet nøjes ikke med at vise tal. Det forklarer, hvad der er galt, hvorfor det er et problem, og hvordan det løses. Rådene har samme niveau som dem, man får fra en erfaren DBA.
|
||||||
|
|
||||||
> **Status:** Fase 0 (fundament) er i gang. Se [Roadmap](#roadmap) og [CONTRIBUTING.md](CONTRIBUTING.md) for at bygge og teste.
|
> **Status:** Fase 1 (Community MVP) er implementeret: agent, 42 regler, lokalt repository og web-UI. Se [installation](docs/installation.md), [Roadmap](#roadmap) og [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -172,14 +172,15 @@ Hvert **finding** indeholder:
|
||||||
- [x] Bidragsguide, code of conduct og issue templates
|
- [x] Bidragsguide, code of conduct og issue templates
|
||||||
|
|
||||||
### Fase 1: Community MVP
|
### Fase 1: Community MVP
|
||||||
- [ ] Agent som Windows Service med forbindelse til en eller flere SQL Server-instanser
|
- [x] Agent som Windows Service (og systemd) med forbindelse til en eller flere SQL Server-instanser, parallelt og isoleret
|
||||||
- [ ] Collectors: wait stats, blocking, deadlocks (XE), CPU/memory, I/O-latens og konfiguration
|
- [x] Collectors: wait stats, blocking, deadlocks (system_health), CPU, hukommelse, I/O-latens, konfiguration, databaser, backups, CHECKDB, VLF'er, Query Store, tempdb, trace flags, suspect pages og plan cache
|
||||||
- [ ] Regelmotor med de første ~30 regler, som hver har forklaring og anbefaling
|
- [x] Regelmotor med 42 regler, som hver har forklaring, evidens, anbefaling og eventuelt et fix-script
|
||||||
- [ ] Lokalt repository med retention-politik
|
- [x] Lokalt repository med migrationer, sporing af fund (set første gang, senest, løst), timeaggregering og retention
|
||||||
- [ ] Web-UI: serveroversigt, liste over fund, detaljevisning og simple trends
|
- [x] Web-UI: oversigt, fund, detaljer, trends, ventetider, I/O, blocking og deadlocks (.xdl). Kan køre på IIS eller som Windows-tjeneste, med Windows-login
|
||||||
- [ ] Registrering af version og CU-niveau med advarsel om manglende opdateringer
|
- [x] Registrering af version og CU-niveau med advarsel om manglende opdateringer (katalog pr. september 2026)
|
||||||
- [ ] Legacy-collectors til 2000/2005/2008: blocking, waits, konfiguration, backup og version
|
- [x] Legacy-collectors til 2000/2005/2008: versionsspecifik SQL er skrevet og unit-testet, men ikke kørt mod rigtige instanser (kræver VM'er, se Fase 0)
|
||||||
- [ ] MSI-installer og dokumentation
|
- [x] Installationsvejledning ([docs/installation.md](docs/installation.md))
|
||||||
|
- [ ] MSI-installer (kræver Windows-byggemiljø, WiX)
|
||||||
|
|
||||||
### Fase 2: Dyb analyse
|
### Fase 2: Dyb analyse
|
||||||
- [ ] Integration med Query Store: regressioner, analyse af planændringer og parameter sniffing
|
- [ ] Integration med Query Store: regressioner, analyse af planændringer og parameter sniffing
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ public static class BuiltInRules
|
||||||
[
|
[
|
||||||
// Version
|
// Version
|
||||||
new SupportLifecycleRule(SupportLifecycleCatalog.LoadEmbedded()),
|
new SupportLifecycleRule(SupportLifecycleCatalog.LoadEmbedded()),
|
||||||
|
new PatchLevelRule(BuildCatalog.LoadEmbedded(), SupportLifecycleCatalog.LoadEmbedded()),
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
new MaxServerMemoryDefaultRule(),
|
new MaxServerMemoryDefaultRule(),
|
||||||
|
|
|
||||||
41
src/SQLInsight.Rules/Lifecycle/BuildCatalog.cs
Normal file
41
src/SQLInsight.Rules/Lifecycle/BuildCatalog.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using SQLInsight.Core.Instances;
|
||||||
|
|
||||||
|
namespace SQLInsight.Rules.Lifecycle;
|
||||||
|
|
||||||
|
public sealed record KnownBuild(SqlServerRelease Release, SqlServerVersion Build, string Name, string Kb, DateOnly Released, bool Security);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The newest known build per release. Shipped as data with the rule pack; <see cref="AsOf"/> says how current it is.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class BuildCatalog(DateOnly asOf, IEnumerable<KnownBuild> builds)
|
||||||
|
{
|
||||||
|
private const string ResourceName = "SQLInsight.Rules.build-catalog.json";
|
||||||
|
|
||||||
|
private readonly Dictionary<SqlServerRelease, KnownBuild> _builds = builds.ToDictionary(b => b.Release);
|
||||||
|
|
||||||
|
public DateOnly AsOf { get; } = asOf;
|
||||||
|
|
||||||
|
public KnownBuild? Latest(SqlServerRelease release) => _builds.GetValueOrDefault(release);
|
||||||
|
|
||||||
|
public static BuildCatalog LoadEmbedded()
|
||||||
|
{
|
||||||
|
using var stream = typeof(BuildCatalog).Assembly.GetManifestResourceStream(ResourceName)
|
||||||
|
?? throw new InvalidOperationException($"Embedded resource '{ResourceName}' is missing.");
|
||||||
|
var document = JsonSerializer.Deserialize<Document>(stream, JsonOptions)
|
||||||
|
?? throw new InvalidOperationException("The build catalog is empty.");
|
||||||
|
return new BuildCatalog(document.AsOf, document.Releases.Select(r =>
|
||||||
|
new KnownBuild(r.Release, SqlServerVersion.Parse(r.Build), r.Name, r.Kb, r.Released, r.Security)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true,
|
||||||
|
Converters = { new JsonStringEnumConverter() },
|
||||||
|
};
|
||||||
|
|
||||||
|
private sealed record Document(DateOnly AsOf, IReadOnlyList<Entry> Releases);
|
||||||
|
|
||||||
|
private sealed record Entry(SqlServerRelease Release, string Build, string Name, string Kb, DateOnly Released, bool Security);
|
||||||
|
}
|
||||||
61
src/SQLInsight.Rules/Lifecycle/PatchLevelRule.cs
Normal file
61
src/SQLInsight.Rules/Lifecycle/PatchLevelRule.cs
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using SQLInsight.Core.Findings;
|
||||||
|
using SQLInsight.Core.Instances;
|
||||||
|
using SQLInsight.Rules.Engine;
|
||||||
|
|
||||||
|
namespace SQLInsight.Rules.Lifecycle;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instance behind the newest known cumulative update, security update or service pack.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PatchLevelRule(BuildCatalog catalog, SupportLifecycleCatalog lifecycle) : Rule
|
||||||
|
{
|
||||||
|
/// <summary>After this long the catalog itself is flagged as possibly outdated.</summary>
|
||||||
|
public static readonly TimeSpan CatalogMaxAge = TimeSpan.FromDays(120);
|
||||||
|
|
||||||
|
public override string Id => "SQLI-VER-002";
|
||||||
|
public override string Title => "Instansen mangler opdateringer";
|
||||||
|
|
||||||
|
public override RuleEvaluation Evaluate(RuleContext context)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(context);
|
||||||
|
var instance = context.Instance;
|
||||||
|
var latest = catalog.Latest(instance.Release)
|
||||||
|
?? throw new Core.Collection.DataUnavailableException($"Regelpakken kender ikke seneste build for {instance.Release.DisplayName()}. Opdater SQLInsight Pro.");
|
||||||
|
|
||||||
|
if (instance.Version >= latest.Build)
|
||||||
|
{
|
||||||
|
return RuleEvaluation.Passed();
|
||||||
|
}
|
||||||
|
|
||||||
|
var today = DateOnly.FromDateTime(context.Now.UtcDateTime);
|
||||||
|
var supported = lifecycle.Find(instance.Release)?.ExtendedEnd is { } end && today <= end;
|
||||||
|
var catalogAge = today.ToDateTime(TimeOnly.MinValue) - catalog.AsOf.ToDateTime(TimeOnly.MinValue);
|
||||||
|
var current = $"{instance.Version}" + (instance.ProductLevel is { } level ? $" ({level}{(instance.ProductUpdateLevel is { } cu ? " " + cu : string.Empty)})" : string.Empty);
|
||||||
|
|
||||||
|
var evidence = new List<Evidence>
|
||||||
|
{
|
||||||
|
new("Installeret build", current),
|
||||||
|
new("Seneste kendte build", $"{latest.Build} ({latest.Name}, {latest.Kb}, {latest.Released.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)})"),
|
||||||
|
new("Katalog pr.", catalog.AsOf.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
|
||||||
|
+ (catalogAge > CatalogMaxAge ? " (forældet: opdater SQLInsight Pro for at se nyere opdateringer)" : string.Empty)),
|
||||||
|
};
|
||||||
|
|
||||||
|
return RuleEvaluation.Found(CreateFinding(
|
||||||
|
supported ? Severity.High : Severity.Medium,
|
||||||
|
$"{instance.Release.DisplayName()} kører {instance.Version}; seneste er {latest.Name} ({latest.Build})",
|
||||||
|
supported
|
||||||
|
? "Instansen mangler opdateringer, som Microsoft har udgivet. Cumulative updates indeholder rettelser af fejl, "
|
||||||
|
+ "performanceproblemer og sikkerhedshuller" + (latest.Security ? ", og den seneste er en sikkerhedsopdatering." : ".")
|
||||||
|
+ " Microsoft anbefaler at installere CU'er proaktivt."
|
||||||
|
: "Versionen er uden for Microsofts support, men instansen mangler også den seneste service pack, som i det mindste "
|
||||||
|
+ "retter de kendte fejl fra versionens levetid.",
|
||||||
|
(supported
|
||||||
|
? $"Installér {latest.Name} ({latest.Kb}). Test først i et testmiljø, og tag en fuld backup inden. "
|
||||||
|
+ "Står instansen på GDR-sporet (kun sikkerhedsopdateringer), kan den flyttes til CU-sporet ved at installere seneste CU."
|
||||||
|
: $"Installér {latest.Name} ({latest.Kb}) som et første skridt, og planlæg opgradering til en supporteret version (se SQLI-VER-001).")
|
||||||
|
+ " Ved Always On: opdatér sekundære replikaer først, lav failover, og opdatér derefter den tidligere primære.",
|
||||||
|
evidence,
|
||||||
|
references: new Reference("Seneste opdateringer til SQL Server", new Uri("https://learn.microsoft.com/troubleshoot/sql/releases/download-and-install-latest-updates"))));
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/SQLInsight.Rules/Lifecycle/build-catalog.json
Normal file
17
src/SQLInsight.Rules/Lifecycle/build-catalog.json
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"asOf": "2026-09-29",
|
||||||
|
"comment": "Newest known build per release. Supported releases: latest CU or security update. Older releases: final service pack (later security-only builds may exist). Update with the rule pack.",
|
||||||
|
"releases": [
|
||||||
|
{ "release": "SqlServer2025", "build": "17.0.5005.3", "name": "CU9", "kb": "KB5122048", "released": "2026-09-15", "security": false },
|
||||||
|
{ "release": "SqlServer2022", "build": "16.0.4295.3", "name": "CU27", "kb": "KB5104824", "released": "2026-09-15", "security": false },
|
||||||
|
{ "release": "SqlServer2019", "build": "15.0.4490.9", "name": "CU32 + sikkerhedsopdatering sept. 2026", "kb": "KB5122772", "released": "2026-09-08", "security": true },
|
||||||
|
{ "release": "SqlServer2017", "build": "14.0.3550.4", "name": "CU31 + sikkerhedsopdatering sept. 2026", "kb": "KB5122774", "released": "2026-09-08", "security": true },
|
||||||
|
{ "release": "SqlServer2016", "build": "13.0.6300.2", "name": "SP3", "kb": "KB5003279", "released": "2021-09-15", "security": false },
|
||||||
|
{ "release": "SqlServer2014", "build": "12.0.6024.0", "name": "SP3", "kb": "KB4022619", "released": "2018-10-30", "security": false },
|
||||||
|
{ "release": "SqlServer2012", "build": "11.0.7001.0", "name": "SP4", "kb": "KB4018073", "released": "2017-10-05", "security": false },
|
||||||
|
{ "release": "SqlServer2008R2", "build": "10.50.6000.34", "name": "SP3", "kb": "KB2979597", "released": "2014-09-26", "security": false },
|
||||||
|
{ "release": "SqlServer2008", "build": "10.0.6000.29", "name": "SP4", "kb": "KB2979596", "released": "2014-09-30", "security": false },
|
||||||
|
{ "release": "SqlServer2005", "build": "9.0.5000.0", "name": "SP4", "kb": "KB2463332", "released": "2010-12-17", "security": false },
|
||||||
|
{ "release": "SqlServer2000", "build": "8.0.2039.0", "name": "SP4", "kb": "KB884525", "released": "2005-05-06", "security": false }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -10,5 +10,6 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Lifecycle\support-lifecycle.json" LogicalName="SQLInsight.Rules.support-lifecycle.json" />
|
<EmbeddedResource Include="Lifecycle\support-lifecycle.json" LogicalName="SQLInsight.Rules.support-lifecycle.json" />
|
||||||
|
<EmbeddedResource Include="Lifecycle\build-catalog.json" LogicalName="SQLInsight.Rules.build-catalog.json" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
59
tests/SQLInsight.Rules.Tests/PatchLevelRuleTests.cs
Normal file
59
tests/SQLInsight.Rules.Tests/PatchLevelRuleTests.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
using SQLInsight.Core.Findings;
|
||||||
|
using SQLInsight.Core.Instances;
|
||||||
|
using SQLInsight.Rules.Engine;
|
||||||
|
using SQLInsight.Rules.Lifecycle;
|
||||||
|
using static SQLInsight.Rules.Tests.TestInstances;
|
||||||
|
|
||||||
|
namespace SQLInsight.Rules.Tests;
|
||||||
|
|
||||||
|
public class PatchLevelRuleTests
|
||||||
|
{
|
||||||
|
private static readonly PatchLevelRule Rule = new(BuildCatalog.LoadEmbedded(), SupportLifecycleCatalog.LoadEmbedded());
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Catalog_has_a_build_for_every_known_release()
|
||||||
|
{
|
||||||
|
var catalog = BuildCatalog.LoadEmbedded();
|
||||||
|
|
||||||
|
foreach (var release in Enum.GetValues<SqlServerRelease>().Where(r => r != SqlServerRelease.Unknown))
|
||||||
|
{
|
||||||
|
var latest = catalog.Latest(release);
|
||||||
|
Assert.NotNull(latest);
|
||||||
|
Assert.Equal(release, latest.Build.Release);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Current_build_passes()
|
||||||
|
{
|
||||||
|
Assert.Equal(RuleOutcome.Passed, Rule.Evaluate(Context("16.0.4295.3")).Outcome);
|
||||||
|
Assert.Equal(RuleOutcome.Passed, Rule.Evaluate(Context("16.0.4300.1")).Outcome); // newer than the catalog
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Old_cu_on_supported_release_is_high()
|
||||||
|
{
|
||||||
|
var finding = Assert.Single(Rule.Evaluate(Context("15.0.4430.1")).Findings); // 2019 CU32 without 2026 security updates
|
||||||
|
|
||||||
|
Assert.Equal(Severity.High, finding.Severity);
|
||||||
|
Assert.Contains("KB5122772", finding.Recommendation, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("sikkerhedsopdatering", finding.Explanation, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Missing_service_pack_on_unsupported_release_is_medium()
|
||||||
|
{
|
||||||
|
var finding = Assert.Single(Rule.Evaluate(Context("10.50.4000.0")).Findings); // 2008 R2 SP2
|
||||||
|
|
||||||
|
Assert.Equal(Severity.Medium, finding.Severity);
|
||||||
|
Assert.Contains("SP3", finding.Title, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Old_catalog_is_called_out()
|
||||||
|
{
|
||||||
|
var finding = Assert.Single(Rule.Evaluate(Context("15.0.4430.1", new DateTimeOffset(2027, 6, 1, 0, 0, 0, TimeSpan.Zero))).Findings);
|
||||||
|
|
||||||
|
Assert.Contains(finding.Evidence, e => e.Value.Contains("forældet", StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue