Some checks are pending
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:2017-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
- ASP.NET Core Blazor static SSR: overview with KPI tiles, instance page (findings, CPU/batch/PLE trends, top waits, file latency, blocking, deadlocks with .xdl download, not-measurable rules, resolved), finding detail with explanation, evidence, fix script and references - Light-first design (sidebar, cards, self-hosted Inter, teal accent), optional dark theme via toggle; strict CSP, no inline styles - Windows authentication (Negotiate/IIS) with optional group restriction; refuses to run without authentication outside Development - Hosting: IIS (in-process ASP.NET Core Module), Windows service with Kestrel, or systemd; web reads the repository with db_datareader only - docs/installation.md for repository, permissions, agent and web - Fix: CPU used by other processes is not derived on Linux (SystemIdle is unreliable there); deadlocks are not re-reported or stored twice after an agent restart; Dapper sends datetime2 so time comparisons are exact Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
160 lines
7 KiB
C#
160 lines
7 KiB
C#
using System.Net;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Core.Instances;
|
|
using SQLInsight.Repository;
|
|
using SQLInsight.Rules.Engine;
|
|
|
|
namespace SQLInsight.IntegrationTests;
|
|
|
|
[Collection(SqlServerTestGroup.Name)]
|
|
[Trait("Category", "Integration")]
|
|
public class WebTests(SqlServerFixture sql) : IAsyncLifetime
|
|
{
|
|
private readonly string _database = "SQLInsightWeb_" + Guid.NewGuid().ToString("N")[..8];
|
|
private WebApplicationFactory<Program> _factory = null!;
|
|
private InsightRepository _repository = null!;
|
|
private long _findingId;
|
|
private long _deadlockId;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];");
|
|
var connectionString = sql.DatabaseConnectionString(_database);
|
|
_repository = new InsightRepository(connectionString);
|
|
await _repository.MigrateAsync();
|
|
|
|
var info = new InstanceInfo
|
|
{
|
|
ServerName = "SQL01",
|
|
Version = SqlServerVersion.Parse("15.0.4355.3"),
|
|
ProductLevel = "RTM",
|
|
ProductUpdateLevel = "CU28",
|
|
Edition = "Standard Edition",
|
|
EngineEdition = EngineEdition.Standard,
|
|
};
|
|
var id = await _repository.UpsertInstanceAsync("prod-sql01", info, "SqlClient", false, DateTimeOffset.UtcNow);
|
|
await _repository.SaveRuleResultsAsync(id,
|
|
[
|
|
new RuleResult
|
|
{
|
|
RuleId = "SQLI-CFG-001",
|
|
RuleTitle = "max server memory er ikke sat",
|
|
Outcome = RuleOutcome.Findings,
|
|
Findings =
|
|
[
|
|
new Finding
|
|
{
|
|
RuleId = "SQLI-CFG-001",
|
|
Severity = Severity.High,
|
|
Title = "max server memory er ikke sat",
|
|
Explanation = "SQL Server må bruge al hukommelse <script>alert(1)</script>",
|
|
Recommendation = "Sæt max server memory.",
|
|
Evidence = [new Evidence("max server memory (MB)", "2147483647")],
|
|
FixScript = "EXEC sys.sp_configure N'max server memory (MB)', 58983;",
|
|
References = [new Reference("Server memory", new Uri("https://learn.microsoft.com/sql/"))],
|
|
},
|
|
],
|
|
},
|
|
new RuleResult { RuleId = "SQLI-DB-011", RuleTitle = "VLF'er", Outcome = RuleOutcome.NotMeasurable, Reason = "Kræver SQL Server 2016 SP2" },
|
|
], DateTimeOffset.UtcNow);
|
|
await _repository.SaveCollectedAsync(id, "deadlocks", new DeadlockSnapshot
|
|
{
|
|
CapturedAt = DateTimeOffset.UtcNow,
|
|
NewDeadlocks = [new DeadlockEvent { Time = DateTimeOffset.UtcNow, GraphXml = "<deadlock><victim-list /></deadlock>", Processes = [new DeadlockProcess { Id = "p1", IsVictim = true, SessionId = 60, ClientApp = "Sales.Api" }] }],
|
|
});
|
|
_findingId = (await _repository.GetActiveFindingsAsync(id)).Single().FindingId;
|
|
_deadlockId = (await _repository.GetDeadlocksAsync(id, DateTimeOffset.UtcNow.AddHours(-1))).Single().DeadlockId;
|
|
|
|
_factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
|
|
{
|
|
b.UseEnvironment("Development");
|
|
b.UseSetting("SQLInsight:Repository:ConnectionString", connectionString);
|
|
b.UseSetting("SQLInsight:Web:Authentication", "None");
|
|
b.UseSetting("SQLInsight:Web:TimeZone", "Europe/Copenhagen");
|
|
});
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await _factory.DisposeAsync();
|
|
await sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];");
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Overview_lists_instances_and_urgent_findings()
|
|
{
|
|
var html = await _factory.CreateClient().GetStringAsync("/");
|
|
|
|
Assert.Contains("prod-sql01", html, StringComparison.Ordinal);
|
|
Assert.Contains("SQL Server 2019", html, StringComparison.Ordinal);
|
|
Assert.Contains("max server memory er ikke sat", html, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Instance_page_shows_findings_and_not_measurable_rules()
|
|
{
|
|
var html = await _factory.CreateClient().GetStringAsync("/instanser/prod-sql01");
|
|
|
|
Assert.Contains("Aktive fund", html, StringComparison.Ordinal);
|
|
Assert.Contains("SQLI-DB-011", html, StringComparison.Ordinal);
|
|
Assert.Contains("Kræver SQL Server 2016 SP2", html, StringComparison.Ordinal);
|
|
Assert.Contains($"/deadlocks/{_deadlockId}.xdl", html, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Finding_page_shows_explanation_evidence_and_script_html_encoded()
|
|
{
|
|
var html = await _factory.CreateClient().GetStringAsync($"/fund/{_findingId}");
|
|
|
|
Assert.Contains("Hvorfor er det et problem?", html, StringComparison.Ordinal);
|
|
Assert.Contains("2147483647", html, StringComparison.Ordinal);
|
|
Assert.Contains("sp_configure", html, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("<script>alert(1)</script>", html, StringComparison.Ordinal);
|
|
Assert.Contains("<script>", html, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Deadlock_graph_downloads_as_xdl()
|
|
{
|
|
var response = await _factory.CreateClient().GetAsync($"/deadlocks/{_deadlockId}.xdl");
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.Equal($"deadlock-{_deadlockId}.xdl", response.Content.Headers.ContentDisposition?.FileName);
|
|
Assert.StartsWith("<deadlock>", await response.Content.ReadAsStringAsync(), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Security_headers_are_set()
|
|
{
|
|
var response = await _factory.CreateClient().GetAsync("/");
|
|
|
|
Assert.Contains("default-src 'self'", response.Headers.GetValues("Content-Security-Policy").Single(), StringComparison.Ordinal);
|
|
Assert.Equal("DENY", response.Headers.GetValues("X-Frame-Options").Single());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unknown_instance_and_finding_are_handled()
|
|
{
|
|
var client = _factory.CreateClient();
|
|
|
|
Assert.Contains("Ukendt instans", await client.GetStringAsync("/instanser/findes-ikke"), StringComparison.Ordinal);
|
|
Assert.Contains("Ukendt fund", await client.GetStringAsync("/fund/999999"), StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public void Refuses_to_run_without_authentication_outside_development()
|
|
{
|
|
using var production = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
|
|
{
|
|
b.UseEnvironment("Production");
|
|
b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(_database));
|
|
b.UseSetting("SQLInsight:Web:Authentication", "None");
|
|
});
|
|
|
|
var ex = Assert.ThrowsAny<Exception>(() => production.CreateClient());
|
|
Assert.Contains("kun tilladt i Development", ex.ToString(), StringComparison.Ordinal);
|
|
}
|
|
}
|