- The Sensitive helper (Value/Text) is shared (Api/Sensitive.cs) and gains ServerText ("Agentens konto (…)" and the
test's account), Url (query left out), ServerTest and CheckResult.
- Windows servers and checks: CreatedBy/UpdatedBy, RequestedBy, LastError, area details, rule-state reasons, the
check's SQL login and an HTTP address's query (settings.url and target) are "[udeladt]" unless
ApiIncludesSensitiveEvidence is on. The grant script endpoint then returns the shipped template, Account
"[udeladt]" and a note without the service account.
- GET /agent-requests/{id} rewrites ServerTest results (account and texts) and SyntheticCheckRun results (failure
message, sensitive details) like ResultJson does for connection tests, and names serverId/checkId.
- Tests and runs reuse a request already waiting (said in the descriptions); running a paused check is 409.
- GET /server-findings: limit (max 500) and continuation with X-Continuation, evidence's sensitive flag, status,
scopePaused/scopeRemoved; GET /server-findings/{id} has history. GET /windows-servers/{id}/events returns
{ capturedAt, method, ignored, coverage, events }. The server list/one server has latest, worstDisk, soonestFull
and findings per severity; checks have findings per severity.
- Admin texts as main (AdminGroups, else the local Administrators); service lists trimmed and deduplicated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
389 lines
27 KiB
C#
389 lines
27 KiB
C#
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;
|
|
|
|
/// <summary>Windows servers and synthetic checks through API v1, with Windows authentication on and tokens with and without config:write.</summary>
|
|
[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<Program> _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<Program>().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<JsonElement> 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);
|
|
}
|
|
}
|
|
}
|