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) Successful in 59s
deploy community / deploy (push) Successful in 0s
deploy hub / deploy (push) Successful in 20s
CI / MSI-installer (push) Successful in 36s
The version moved to 1.0.1, so the metrics test failed. It now reads the informational version the endpoint itself reports. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
137 lines
6.5 KiB
C#
137 lines
6.5 KiB
C#
using System.Net;
|
|
using System.Net.Http.Headers;
|
|
using System.Text.Json;
|
|
using Microsoft.AspNetCore.Hosting;
|
|
using Microsoft.AspNetCore.Mvc.Testing;
|
|
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Core.Instances;
|
|
using SQLInsight.Repository;
|
|
using SQLInsight.Rules.Engine;
|
|
using SQLInsight.Web.Api;
|
|
|
|
namespace SQLInsight.IntegrationTests;
|
|
|
|
/// <summary>/api/v1 and /metrics with Windows authentication switched on and bearer tokens for machines.</summary>
|
|
[Collection(SqlServerTestGroup.Name)]
|
|
[Trait("Category", "Integration")]
|
|
public class WebApiTests(SqlServerFixture sql) : IAsyncLifetime
|
|
{
|
|
private const string Token = "sqli_test-token-for-prometheus";
|
|
|
|
private readonly string _database = "SQLInsightApi_" + Guid.NewGuid().ToString("N")[..8];
|
|
private WebApplicationFactory<Program> _factory = null!;
|
|
private int _port;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
using (var probe = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0))
|
|
{
|
|
probe.Start();
|
|
_port = ((IPEndPoint)probe.LocalEndpoint).Port;
|
|
}
|
|
|
|
await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];");
|
|
var repository = new InsightRepository(sql.DatabaseConnectionString(_database));
|
|
await repository.MigrateAsync();
|
|
var id = await repository.UpsertInstanceAsync("prod \"sql01\"", new InstanceInfo
|
|
{
|
|
ServerName = "SQL01", Version = SqlServerVersion.Parse("16.0.4175.1"), ProductLevel = "RTM", ProductUpdateLevel = "CU20",
|
|
Edition = "Enterprise Edition", EngineEdition = EngineEdition.Enterprise,
|
|
}, "SqlClient", false, DateTimeOffset.UtcNow);
|
|
await repository.SaveRuleResultsAsync(id,
|
|
[
|
|
new RuleResult
|
|
{
|
|
RuleId = "SQLI-BLK-001", RuleTitle = "Blocking", Outcome = RuleOutcome.Findings,
|
|
Findings =
|
|
[
|
|
new Finding
|
|
{
|
|
RuleId = "SQLI-BLK-001", Severity = Severity.High, Title = "Session 87 blokerer 14 sessioner", Explanation = "e", Recommendation = "r",
|
|
Subject = "session: 87",
|
|
Evidence = [new Evidence("Blokerede sessioner", "14"), new Evidence("Head blockers SQL", "UPDATE dbo.Kunder SET Cpr = '010101-1234'", Sensitive: true)],
|
|
},
|
|
],
|
|
},
|
|
], DateTimeOffset.UtcNow);
|
|
await sql.ExecuteAsSaAsync($"INSERT [{_database}].insight.MetricSamples (InstanceId, Metric, SampledAt, Value) VALUES ({id}, 'cpu.sqlserver', SYSUTCDATETIME(), 42.5);");
|
|
|
|
_factory = new WebApplicationFactory<Program>().WithWebHostBuilder(b =>
|
|
{
|
|
b.UseEnvironment("Development");
|
|
b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(_database));
|
|
b.UseSetting("SQLInsight:Web:Authentication", "Windows");
|
|
b.UseSetting("SQLInsight:Web:AllowedGroups:0", @"DOMÆNE\SQL-Admins");
|
|
b.UseSetting("SQLInsight:Web:ApiTokens:prometheus", "sha256:" + ApiTokenHandler.Hash(Token));
|
|
b.UseSetting("urls", $"http://127.0.0.1:{_port}");
|
|
});
|
|
|
|
// Negotiate needs a real server (Kestrel), not the in-memory test server.
|
|
_factory.UseKestrel();
|
|
_factory.StartServer();
|
|
|
|
}
|
|
|
|
public async Task DisposeAsync()
|
|
{
|
|
await _factory.DisposeAsync();
|
|
await sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];");
|
|
}
|
|
|
|
private HttpClient Client(string? token)
|
|
{
|
|
var client = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false, BaseAddress = new Uri($"http://127.0.0.1:{_port}") });
|
|
if (token is not null)
|
|
{
|
|
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
|
}
|
|
|
|
return client;
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Metrics_are_served_in_prometheus_format_with_a_valid_token()
|
|
{
|
|
var response = await Client(Token).GetAsync("/metrics");
|
|
var text = await response.Content.ReadAsStringAsync();
|
|
|
|
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
|
Assert.StartsWith("text/plain", response.Content.Headers.ContentType!.MediaType, StringComparison.Ordinal);
|
|
Assert.Contains("sqlinsight_instance_up{instance=\"prod \\\"sql01\\\"\"} 1", text, StringComparison.Ordinal);
|
|
Assert.Contains("sqlinsight_findings{instance=\"prod \\\"sql01\\\"\",severity=\"high\"} 1", text, StringComparison.Ordinal);
|
|
Assert.Contains("sqlinsight_rule_findings{instance=\"prod \\\"sql01\\\"\",rule=\"SQLI-BLK-001\",severity=\"high\"} 1", text, StringComparison.Ordinal);
|
|
Assert.Contains("sqlinsight_metric{instance=\"prod \\\"sql01\\\"\",metric=\"cpu.sqlserver\"} 42.5", text, StringComparison.Ordinal);
|
|
Assert.Contains("# TYPE sqlinsight_findings gauge", text, StringComparison.Ordinal);
|
|
// The version from Directory.Build.props (or the release tag), whatever it currently is.
|
|
var version = typeof(IntegrationEndpoints).Assembly
|
|
.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
|
|
.Cast<System.Reflection.AssemblyInformationalVersionAttribute>().Single().InformationalVersion.Split('+')[0];
|
|
Assert.Contains($"sqlinsight_build_info{{version=\"{version}\"", text, StringComparison.Ordinal);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Findings_api_leaves_out_sensitive_evidence()
|
|
{
|
|
var json = await Client(Token).GetStringAsync("/api/v1/findings?minSeverity=High");
|
|
|
|
using var document = JsonDocument.Parse(json);
|
|
var finding = Assert.Single(document.RootElement.EnumerateArray());
|
|
Assert.Equal("SQLI-BLK-001", finding.GetProperty("ruleId").GetString());
|
|
Assert.Contains("[udeladt]", json, StringComparison.Ordinal);
|
|
Assert.DoesNotContain("010101-1234", json, StringComparison.Ordinal);
|
|
Assert.Equal(0, JsonDocument.Parse(await Client(Token).GetStringAsync("/api/v1/findings?minSeverity=Critical")).RootElement.GetArrayLength());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(null, "/metrics")]
|
|
[InlineData("forkert-token", "/metrics")]
|
|
[InlineData("forkert-token", "/api/v1/instances")]
|
|
[InlineData(Token, "/")]
|
|
[InlineData(Token, "/fund")]
|
|
public async Task Without_a_valid_token_or_outside_the_api_access_is_refused(string? token, string path)
|
|
{
|
|
var response = await Client(token).GetAsync(path);
|
|
|
|
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
|
|
}
|
|
}
|