using System.Net; using System.Net.Http.Headers; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using SQLInsight.Core.Configuration; using SQLInsight.Core.Findings; using SQLInsight.Core.Servers; using SQLInsight.Core.SyntheticChecks; using SQLInsight.Repository; using SQLInsight.Rules.Engine; using SQLInsight.Web.Api; namespace SQLInsight.IntegrationTests; /// Windows servers and synthetic checks through API v1, with Windows authentication on and tokens with and without config:write. [Collection(SqlServerTestGroup.Name)] [Trait("Category", "Integration")] public sealed class ServerApiTests(SqlServerFixture sql) : IAsyncLifetime { private const string ReadToken = "sqli_test-server-read"; private const string WriteToken = "sqli_test-server-write"; private readonly string _database = "SQLInsightSrvApi_" + Guid.NewGuid().ToString("N")[..8]; private WebApplicationFactory _factory = null!; private InsightRepository _repository = 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}];"); _repository = new InsightRepository(sql.DatabaseConnectionString(_database)); await _repository.MigrateAsync(); using var rsa = RSA.Create(3072); await _repository.PublishAgentKeyAsync(new AgentKeyRow { KeyId = "agent-api", PublicKey = rsa.ExportSubjectPublicKeyInfo(), MachineName = "AGENT01", ServiceAccount = @"FIRMA\gmsa-sqlinsight$", Domain = "FIRMA", CreatedAt = DateTime.UtcNow, LastSeenAt = DateTime.UtcNow, }); _factory = new WebApplicationFactory().WithWebHostBuilder(b => { b.UseEnvironment("Development"); b.UseSetting("SQLInsight:Repository:ConnectionString", sql.DatabaseConnectionString(_database)); b.UseSetting("SQLInsight:Web:Authentication", "Windows"); b.UseSetting("SQLInsight:Web:AdminGroups:0", @"FIRMA\SQL-Admins"); b.UseSetting("SQLInsight:Web:ApiTokens:grafana", "sha256:" + ApiTokenHandler.Hash(ReadToken)); b.UseSetting("SQLInsight:Web:ApiTokens:automation:hash", "sha256:" + ApiTokenHandler.Hash(WriteToken)); b.UseSetting("SQLInsight:Web:ApiTokens:automation:scopes:0", "read"); b.UseSetting("SQLInsight:Web:ApiTokens:automation:scopes:1", "config:write"); b.UseSetting("urls", $"http://127.0.0.1:{_port}"); }); _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; } private static StringContent Body(object value) => new(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json"); private static async Task JsonAsync(HttpResponseMessage response) => JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; [Fact] public async Task Windows_servers_in_the_api_and_changing_needs_config_write() { var write = Client(WriteToken); var read = Client(ReadToken); var server = new { host = "aos01.firma.local", role = "Application", watchedServices = new[] { "AOS60$01" } }; Assert.Equal(HttpStatusCode.Unauthorized, (await Client(null).GetAsync("/api/v1/windows-servers")).StatusCode); Assert.Equal(HttpStatusCode.Forbidden, (await read.PostAsync("/api/v1/windows-servers", Body(server))).StatusCode); Assert.Equal(HttpStatusCode.UnprocessableEntity, (await write.PostAsync("/api/v1/windows-servers", Body(new { host = @"aos01\x" }))).StatusCode); var created = await write.PostAsync("/api/v1/windows-servers", Body(server)); Assert.Equal(HttpStatusCode.Created, created.StatusCode); var body = await JsonAsync(created); var id = body.GetProperty("server").GetProperty("id").GetInt32(); Assert.Equal("AOS01", body.GetProperty("server").GetProperty("name").GetString()); Assert.True(body.GetProperty("serverTestRequestId").GetInt64() > 0); var now = DateTimeOffset.UtcNow; await _repository.SaveServerPerformanceAsync(id, new ServerPerformanceSample(now.AddMinutes(-1), 42, 6000, 16384, 3, 50)); await _repository.SaveServerSnapshotAsync(id, ServerArea.Disks, new ServerDiskSnapshot { CapturedAt = now, Disks = [new ServerDisk(@"D:\", "Data", "NTFS", 500, 40)], Forecasts = [new DiskForecast(@"D:\", 12, 3.3, 300, 14, 0.9)], }, now); await _repository.SaveServerSnapshotAsync(id, ServerArea.Services, new ServerServiceSnapshot { CapturedAt = now, Services = [new ServerService("AOS60$01", "AOS", "Running", "Auto")] }, now); await _repository.SaveServerSnapshotAsync(id, ServerArea.Tasks, new ServerTaskSnapshot { CapturedAt = now, Visible = 120, Tasks = [new ServerTask { Path = @"\", Name = "AX export", State = "Ready", LastTaskResult = 1 }], }, now); var (groups, _) = ServerEventAnalysis.Group([new ServerEventEntry("System", "disk", 153, 2, now.AddMinutes(-3), @"Disk 1 på \\fil01\hemmeligt")], []); await _repository.SaveServerEventsAsync(id, new ServerEventSnapshot { CapturedAt = now, ReadUntil = now, Groups = groups }); await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, id, [ new RuleResult { RuleId = "SQLI-WIN-009", RuleTitle = "Fejl i Windows' hændelseslog", Outcome = RuleOutcome.Findings, Findings = [new Finding { RuleId = "SQLI-WIN-009", Severity = Severity.High, Title = "Disk og filsystem: disk 153", Explanation = "e", Recommendation = "r", Subject = "event: System/disk/153", Evidence = [new Evidence("Kilde", "disk"), new Evidence("Eksempel", @"Disk 1 på \\fil01\hemmeligt", Sensitive: true)], }], }, ], now); var list = await JsonAsync(await read.GetAsync("/api/v1/windows-servers")); var listed = Assert.Single(list.EnumerateArray()); Assert.Equal("aos01.firma.local", listed.GetProperty("host").GetString()); Assert.Equal(42, listed.GetProperty("latest").GetProperty("cpuPercent").GetDouble(), 3); Assert.Equal(@"D:\", listed.GetProperty("worstDisk").GetProperty("volume").GetString()); Assert.Equal("fuld om ca. 12 dage", listed.GetProperty("soonestFull").GetProperty("text").GetString()); Assert.Equal(1, listed.GetProperty("findings").GetProperty("high").GetInt32()); Assert.Equal(1, (await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/performance"))).GetArrayLength()); var disks = await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/disks")); Assert.Equal("fuld om ca. 12 dage", disks.GetProperty("disks")[0].GetProperty("forecast").GetProperty("text").GetString()); Assert.Equal("AOS60$01", (await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/services"))).GetProperty("services")[0].GetProperty("name").GetString()); var tasks = await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/tasks")); Assert.True(tasks.GetProperty("tasks")[0].GetProperty("failed").GetBoolean()); var events = await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/events")); Assert.Equal(ApiV1Endpoints.Redacted, events.GetProperty("events")[0].GetProperty("sample").GetString()); var findings = await JsonAsync(await read.GetAsync($"/api/v1/server-findings?scope=server&id={id}")); var finding = Assert.Single(findings.EnumerateArray()); Assert.DoesNotContain("hemmeligt", finding.ToString(), StringComparison.Ordinal); Assert.True(finding.GetProperty("evidence")[1].GetProperty("sensitive").GetBoolean()); var findingId = finding.GetProperty("id").GetInt64(); Assert.Equal("SQLI-WIN-009", (await JsonAsync(await read.GetAsync($"/api/v1/server-findings/{findingId}"))).GetProperty("ruleId").GetString()); Assert.Equal(HttpStatusCode.BadRequest, (await read.GetAsync("/api/v1/server-findings?id=1")).StatusCode); var states = await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/rule-states")); Assert.Equal("findings", Assert.Single(states.EnumerateArray()).GetProperty("outcome").GetString()); // The agent's account is customer data: without sensitive data the shipped script with its placeholder. var grant = await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/grant-script")); Assert.Equal(ApiV1Endpoints.Redacted, grant.GetProperty("account").GetString()); Assert.Equal(ServerGrantScript.Template(), grant.GetProperty("script").GetString()); Assert.DoesNotContain("gmsa-sqlinsight", grant.ToString(), StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("AGENT01", grant.ToString(), StringComparison.Ordinal); Assert.Equal(HttpStatusCode.Forbidden, (await read.PatchAsync($"/api/v1/windows-servers/{id}", Body(new { paused = true }))).StatusCode); var patched = await JsonAsync(await write.PatchAsync($"/api/v1/windows-servers/{id}", Body(new { paused = true, ignoredServices = Enumerable.Repeat("MyBatch", 1) }))); Assert.True(patched.GetProperty("paused").GetBoolean()); Assert.Equal("MyBatch", patched.GetProperty("ignoredServices")[0].GetString()); Assert.Equal("AOS60$01", patched.GetProperty("watchedServices")[0].GetString()); Assert.Equal(HttpStatusCode.Accepted, (await write.PostAsync($"/api/v1/windows-servers/{id}/test", Body(new { }))).StatusCode); Assert.Equal("Pending", (await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/test"))).GetProperty("request").GetProperty("status").GetString()); await _repository.SyncSqlHostServersAsync([("sql01.firma.local", "SQL01")], now); var sqlHost = (await _repository.GetWindowsServersAsync()).Single(s => s.IsSqlHost).ServerId; Assert.Equal(HttpStatusCode.Conflict, (await write.DeleteAsync($"/api/v1/windows-servers/{sqlHost}")).StatusCode); Assert.Equal(HttpStatusCode.NoContent, (await write.DeleteAsync($"/api/v1/windows-servers/{id}")).StatusCode); Assert.Equal(HttpStatusCode.NotFound, (await read.GetAsync($"/api/v1/windows-servers/{id}")).StatusCode); } [Fact] public async Task Synthetic_checks_in_the_api_with_write_only_passwords_and_redacted_failures() { var write = Client(WriteToken); var read = Client(ReadToken); var check = new { name = "SQL01 login", kind = "Sql", intervalSeconds = 120, settings = new { server = @"sql01\AX", authentication = "SqlLogin", userName = "axapp", database = "AX2012" }, password = "Api-Hemmelig-2!", }; Assert.Equal(HttpStatusCode.Forbidden, (await read.PostAsync("/api/v1/synthetic-checks", Body(check))).StatusCode); Assert.Equal(HttpStatusCode.UnprocessableEntity, (await write.PostAsync("/api/v1/synthetic-checks", Body(new { name = "x", kind = "Sql", settings = new { server = "sql01", query = "DELETE FROM dbo.x" } }))).StatusCode); var created = await write.PostAsync("/api/v1/synthetic-checks", Body(check)); Assert.Equal(HttpStatusCode.Created, created.StatusCode); var body = await JsonAsync(created); Assert.DoesNotContain("Api-Hemmelig", body.ToString(), StringComparison.Ordinal); var id = body.GetProperty("check").GetProperty("id").GetInt32(); Assert.True(body.GetProperty("check").GetProperty("hasPassword").GetBoolean()); Assert.Equal(@"sql01\AX / AX2012", body.GetProperty("check").GetProperty("target").GetString()); Assert.True(body.GetProperty("runRequestId").GetInt64() > 0); await _repository.RecordSyntheticCheckResultAsync(id, new SyntheticCheckResult { At = DateTimeOffset.UtcNow, Success = false, DurationMs = 40, Message = "Login på SQL Server fejlede: Login failed for user 'axapp'.", Details = [new SyntheticCheckDetail("Server", @"sql01\AX"), new SyntheticCheckDetail("Fejl", "axapp", Sensitive: true)], }); var results = await JsonAsync(await read.GetAsync($"/api/v1/synthetic-checks/{id}/results")); var result = Assert.Single(results.EnumerateArray()); Assert.Equal(ApiV1Endpoints.Redacted, result.GetProperty("message").GetString()); Assert.Equal(@"sql01\AX", result.GetProperty("details")[0].GetProperty("value").GetString()); Assert.Equal(ApiV1Endpoints.Redacted, result.GetProperty("details")[1].GetProperty("value").GetString()); Assert.Equal(ApiV1Endpoints.Redacted, (await JsonAsync(await read.GetAsync($"/api/v1/synthetic-checks/{id}"))).GetProperty("lastMessage").GetString()); var changed = await write.PutAsync($"/api/v1/synthetic-checks/{id}", Body(new { name = "SQL01 login", settings = new { server = @"sql01\AX", authentication = "SqlLogin", userName = "axapp", database = "AX2012", warnMilliseconds = 500 }, })); Assert.Equal(HttpStatusCode.OK, changed.StatusCode); Assert.Equal(500, (await JsonAsync(changed)).GetProperty("settings").GetProperty("warnMilliseconds").GetInt32()); Assert.Equal(HttpStatusCode.UnprocessableEntity, (await write.PutAsync($"/api/v1/synthetic-checks/{id}", Body(new { name = "SQL01 login", settings = new { server = "sql02", authentication = "SqlLogin", userName = "axapp" }, }))).StatusCode); Assert.Equal(HttpStatusCode.UnprocessableEntity, (await write.PutAsync($"/api/v1/synthetic-checks/{id}", Body(new { name = "SQL01 login", kind = "Tcp" }))).StatusCode); Assert.Equal(HttpStatusCode.Accepted, (await write.PostAsync($"/api/v1/synthetic-checks/{id}/run", Body(new { }))).StatusCode); Assert.True((await JsonAsync(await write.PatchAsync($"/api/v1/synthetic-checks/{id}", Body(new { paused = true })))).GetProperty("paused").GetBoolean()); var paused = await write.PostAsync($"/api/v1/synthetic-checks/{id}/run", Body(new { })); Assert.Equal(HttpStatusCode.Conflict, paused.StatusCode); Assert.Contains("pause", (await JsonAsync(paused)).GetProperty("detail").GetString(), StringComparison.Ordinal); Assert.Equal(HttpStatusCode.OK, (await read.GetAsync($"/api/v1/synthetic-checks/{id}/rule-states")).StatusCode); Assert.Single((await JsonAsync(await read.GetAsync("/api/v1/synthetic-checks"))).EnumerateArray()); Assert.Contains((await JsonAsync(await read.GetAsync("/api/v1/rules"))).EnumerateArray(), r => r.GetProperty("id").GetString() == "SQLI-SYN-001"); Assert.Equal(HttpStatusCode.Forbidden, (await read.DeleteAsync($"/api/v1/synthetic-checks/{id}")).StatusCode); Assert.Equal(HttpStatusCode.NoContent, (await write.DeleteAsync($"/api/v1/synthetic-checks/{id}")).StatusCode); } [Fact] public async Task Repeated_tests_and_runs_reuse_the_request_waiting_for_the_agent() { var write = Client(WriteToken); var server = (await JsonAsync(await write.PostAsync("/api/v1/windows-servers", Body(new { host = "fil01.firma.local", role = "File" })))).GetProperty("server"); var serverId = server.GetProperty("id").GetInt32(); var created = await JsonAsync(await write.PostAsync("/api/v1/synthetic-checks", Body(new { name = "FIL01 SMB", kind = "Tcp", settings = new { host = "fil01", port = 445 } }))); var checkId = created.GetProperty("check").GetProperty("id").GetInt32(); var firstRun = created.GetProperty("runRequestId").GetInt64(); var test1 = await JsonAsync(await write.PostAsync($"/api/v1/windows-servers/{serverId}/test", Body(new { }))); var test2 = await JsonAsync(await write.PostAsync($"/api/v1/windows-servers/{serverId}/test", Body(new { }))); var run1 = await JsonAsync(await write.PostAsync($"/api/v1/synthetic-checks/{checkId}/run", Body(new { }))); var run2 = await JsonAsync(await write.PostAsync($"/api/v1/synthetic-checks/{checkId}/run", Body(new { }))); Assert.Equal(test1.GetProperty("id").GetInt64(), test2.GetProperty("id").GetInt64()); Assert.Equal(serverId, test1.GetProperty("serverId").GetInt32()); Assert.Equal(firstRun, run1.GetProperty("id").GetInt64()); Assert.Equal(firstRun, run2.GetProperty("id").GetInt64()); Assert.Equal(checkId, run2.GetProperty("checkId").GetInt32()); // Taken by the agent: the next click queues a new one. while (await _repository.ClaimAgentRequestAsync(DateTimeOffset.UtcNow) is { } claimed) { await _repository.CompleteAgentRequestAsync(claimed.RequestId, true, null, null, DateTimeOffset.UtcNow); } Assert.NotEqual(firstRun, (await JsonAsync(await write.PostAsync($"/api/v1/synthetic-checks/{checkId}/run", Body(new { })))).GetProperty("id").GetInt64()); } [Fact] public async Task Accounts_logins_user_names_and_url_queries_are_left_out_of_the_api() { var read = Client(ReadToken); var now = DateTimeOffset.UtcNow; const string account = @"FIRMA\gmsa-sqlinsight$"; var id = await _repository.AddWindowsServerAsync(new NewWindowsServer("AOS01", "aos01", ServerRoles.Application, [], []), @"FIRMA\anna", now); var denied = $"Adgang nægtet til hændelseslog på aos01 over DCOM: Agentens konto ({account}) mangler Event Log Readers."; await _repository.RecordServerContactAsync(id, new ServerOsInfo { Caption = "Windows Server 2019" }, "DCOM", [new ServerAreaStatus(ServerArea.Events, false, denied, ServerArea.Right(ServerArea.Events))], now.AddMinutes(-1)); await _repository.RecordServerErrorAsync(id, $"Adgang nægtet til serveren på aos01 over WS-Man: Agentens konto ({account}) mangler Remote Enable.", now); await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, id, [new RuleResult { RuleId = "SQLI-WIN-009", RuleTitle = "Hændelseslog", Outcome = RuleOutcome.NotMeasurable, Reason = denied }], now); var test = await _repository.AddServerRequestAsync(ServerRequestKind.ServerTest, id, @"FIRMA\anna", now); await _repository.ClaimAgentRequestAsync(now); await _repository.CompleteAgentRequestAsync(test, true, AgentRequestJson.Serialize(new WindowsServerTestResult { Reachable = true, Protocol = "DCOM", Account = account, Areas = [new ServerAreaStatus(ServerArea.Events, false, denied, ServerArea.Right(ServerArea.Events))], Notes = [$"WS-Man blev afvist for {account}; DCOM bruges."], }), null, now); var check = await _repository.AddSyntheticCheckAsync(new SyntheticCheckInput("EP", SyntheticCheckKind.Http, null, new SyntheticCheckSettings { Url = new Uri("https://ep.firma.local/sites/ax?token=hemmelig-noegle&x=1") }, null, null, 300, 15), @"FIRMA\bo", now); var sql = await _repository.AddSyntheticCheckAsync(new SyntheticCheckInput("SQL", SyntheticCheckKind.Sql, null, new SyntheticCheckSettings { Server = "sql01", Authentication = "SqlLogin", UserName = "axapp" }, [1, 2, 3], "agent-api", 300, 15), @"FIRMA\bo", now); var run = await _repository.AddServerRequestAsync(ServerRequestKind.SyntheticCheckRun, sql, @"FIRMA\bo", now); await _repository.ClaimAgentRequestAsync(now); await _repository.CompleteAgentRequestAsync(run, true, AgentRequestJson.Serialize(new SyntheticCheckResult { At = now, Success = false, Message = "Login failed for user 'axapp'.", Details = [new SyntheticCheckDetail("Server", "sql01"), new SyntheticCheckDetail("Login", "axapp", Sensitive: true)], }), null, now); var serverJson = (await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}"))).ToString(); var testJson = (await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/test"))).ToString(); var requestJson = (await JsonAsync(await read.GetAsync($"/api/v1/agent-requests/{test}"))).ToString(); var statesJson = (await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/rule-states"))).ToString(); var httpJson = await JsonAsync(await read.GetAsync($"/api/v1/synthetic-checks/{check}")); var sqlJson = await JsonAsync(await read.GetAsync($"/api/v1/synthetic-checks/{sql}")); var runJson = await JsonAsync(await read.GetAsync($"/api/v1/agent-requests/{run}")); var listJson = (await JsonAsync(await read.GetAsync("/api/v1/synthetic-checks"))).ToString(); foreach (var json in new[] { serverJson, testJson, requestJson, statesJson, httpJson.ToString(), sqlJson.ToString(), runJson.ToString(), listJson }) { Assert.DoesNotContain("gmsa", json, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("anna", json, StringComparison.Ordinal); Assert.DoesNotContain(@"FIRMA\\bo", json, StringComparison.Ordinal); Assert.DoesNotContain("axapp", json, StringComparison.Ordinal); Assert.DoesNotContain("hemmelig-noegle", json, StringComparison.Ordinal); } Assert.Contains("Event Log Readers", serverJson, StringComparison.Ordinal); Assert.Contains("Agentens konto ([udeladt])", serverJson, StringComparison.Ordinal); Assert.Contains("Agentens konto ([udeladt])", statesJson, StringComparison.Ordinal); Assert.Contains("\"account\":\"[udeladt]\"", requestJson, StringComparison.Ordinal); Assert.Contains("\"serverId\":" + id.ToString(System.Globalization.CultureInfo.InvariantCulture), requestJson, StringComparison.Ordinal); Assert.Equal("https://ep.firma.local/sites/ax?[udeladt]", httpJson.GetProperty("target").GetString()); Assert.Equal("https://ep.firma.local/sites/ax?[udeladt]", httpJson.GetProperty("settings").GetProperty("url").GetString()); Assert.Equal(ApiV1Endpoints.Redacted, httpJson.GetProperty("createdBy").GetString()); Assert.Equal(ApiV1Endpoints.Redacted, sqlJson.GetProperty("settings").GetProperty("userName").GetString()); var result = runJson.GetProperty("result"); Assert.Equal(ApiV1Endpoints.Redacted, result.GetProperty("message").GetString()); Assert.Equal("sql01", result.GetProperty("details")[0].GetProperty("value").GetString()); Assert.Equal(ApiV1Endpoints.Redacted, result.GetProperty("details")[1].GetProperty("value").GetString()); Assert.Equal(ApiV1Endpoints.Redacted, runJson.GetProperty("requestedBy").GetString()); } [Fact] public async Task Server_findings_come_in_pages_with_history_and_the_event_log_with_how_it_was_read() { var read = Client(ReadToken); var now = DateTimeOffset.UtcNow; var id = await _repository.AddWindowsServerAsync(new NewWindowsServer("FIL01", "fil01", ServerRoles.File, [], []), null, now); var paused = await _repository.AddWindowsServerAsync(new NewWindowsServer("FIL02", "fil02", ServerRoles.File, [], []), null, now); static RuleResult Disks(params string[] volumes) => new() { RuleId = "SQLI-WIN-005", RuleTitle = "Disk", Outcome = volumes.Length == 0 ? RuleOutcome.Passed : RuleOutcome.Findings, Findings = volumes.Select(v => new Finding { RuleId = "SQLI-WIN-005", Severity = Severity.High, Title = "Disk " + v, Explanation = "e", Recommendation = "r", Subject = "disk: " + v }) .ToList(), }; await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, id, [Disks("C:", "D:", "E:")], now.AddMinutes(-3)); await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, id, [Disks("D:", "E:")], now.AddMinutes(-2)); await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, id, [Disks("C:", "D:", "E:")], now.AddMinutes(-1)); await _repository.SaveScopedRuleResultsAsync(FindingScope.Server, paused, [Disks("C:")], now); await _repository.SetWindowsServerPausedAsync(paused, true, null, now); var page1 = await read.GetAsync("/api/v1/server-findings?limit=2"); var continuation = Assert.Single(page1.Headers.GetValues("X-Continuation")); var page2 = await read.GetAsync("/api/v1/server-findings?limit=2&continuation=" + continuation); Assert.Equal(2, (await JsonAsync(page1)).GetArrayLength()); var last = Assert.Single((await JsonAsync(page2)).EnumerateArray()); Assert.False(page2.Headers.Contains("X-Continuation")); Assert.Equal(id, last.GetProperty("scopeId").GetInt32()); Assert.Equal(HttpStatusCode.BadRequest, (await read.GetAsync("/api/v1/server-findings?continuation=x")).StatusCode); var pausedOwn = Assert.Single((await JsonAsync(await read.GetAsync($"/api/v1/server-findings?scope=server&id={paused}"))).EnumerateArray()); Assert.True(pausedOwn.GetProperty("scopePaused").GetBoolean()); var reopened = (await _repository.GetServerFindingsAsync(FindingScope.Server, id)).Single(f => f.Subject == "disk: C:"); var detail = await JsonAsync(await read.GetAsync($"/api/v1/server-findings/{reopened.FindingId}")); var earlier = Assert.Single(detail.GetProperty("history").EnumerateArray()); Assert.NotEqual(JsonValueKind.Null, earlier.GetProperty("resolvedAt").ValueKind); Assert.Equal("active", detail.GetProperty("status").GetString()); await _repository.SaveServerSnapshotAsync(id, ServerArea.Events, new ServerEventSnapshot { CapturedAt = now, ReadUntil = now, Method = "Event Log (RPC)", Ignored = 7, Coverage = "Kun de nyeste 500 hændelser er læst.", }, now); var events = await JsonAsync(await read.GetAsync($"/api/v1/windows-servers/{id}/events")); Assert.Equal("Event Log (RPC)", events.GetProperty("method").GetString()); Assert.Equal(7, events.GetProperty("ignored").GetInt32()); Assert.Equal("Kun de nyeste 500 hændelser er læst.", events.GetProperty("coverage").GetString()); Assert.Equal(0, events.GetProperty("events").GetArrayLength()); } [Fact] public async Task Openapi_describes_the_server_and_check_endpoints() { var document = await JsonAsync(await Client(ReadToken).GetAsync("/api/v1/openapi.json")); var paths = document.GetProperty("paths").EnumerateObject().Select(p => p.Name).ToList(); foreach (var path in new[] { "/api/v1/windows-servers", "/api/v1/windows-servers/{id}/disks", "/api/v1/server-findings", "/api/v1/synthetic-checks/{id}/run", "/api/v1/synthetic-checks/{id}/results" }) { Assert.Contains(path, paths); } } }