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

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:
Claude 2026-09-29 07:03:25 +00:00
parent 5bb1c37fef
commit 3c35f92785
No known key found for this signature in database
7 changed files with 190 additions and 9 deletions

View file

@ -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.
> **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
### Fase 1: Community MVP
- [ ] Agent som Windows Service med forbindelse til en eller flere SQL Server-instanser
- [ ] Collectors: wait stats, blocking, deadlocks (XE), CPU/memory, I/O-latens og konfiguration
- [ ] Regelmotor med de første ~30 regler, som hver har forklaring og anbefaling
- [ ] Lokalt repository med retention-politik
- [ ] Web-UI: serveroversigt, liste over fund, detaljevisning og simple trends
- [ ] Registrering af version og CU-niveau med advarsel om manglende opdateringer
- [ ] Legacy-collectors til 2000/2005/2008: blocking, waits, konfiguration, backup og version
- [ ] MSI-installer og dokumentation
- [x] Agent som Windows Service (og systemd) med forbindelse til en eller flere SQL Server-instanser, parallelt og isoleret
- [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
- [x] Regelmotor med 42 regler, som hver har forklaring, evidens, anbefaling og eventuelt et fix-script
- [x] Lokalt repository med migrationer, sporing af fund (set første gang, senest, løst), timeaggregering og retention
- [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
- [x] Registrering af version og CU-niveau med advarsel om manglende opdateringer (katalog pr. september 2026)
- [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)
- [x] Installationsvejledning ([docs/installation.md](docs/installation.md))
- [ ] MSI-installer (kræver Windows-byggemiljø, WiX)
### Fase 2: Dyb analyse
- [ ] Integration med Query Store: regressioner, analyse af planændringer og parameter sniffing

View file

@ -13,6 +13,7 @@ public static class BuiltInRules
[
// Version
new SupportLifecycleRule(SupportLifecycleCatalog.LoadEmbedded()),
new PatchLevelRule(BuildCatalog.LoadEmbedded(), SupportLifecycleCatalog.LoadEmbedded()),
// Configuration
new MaxServerMemoryDefaultRule(),

View 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);
}

View 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"))));
}
}

View 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 }
]
}

View file

@ -10,5 +10,6 @@
</ItemGroup>
<ItemGroup>
<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>
</Project>

View 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));
}
}