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 / Build og unit tests (push) Waiting to run
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
- ASP.NET Core API POST /api/v1/findings with EF Core (Npgsql, snake_case) and a generated initial migration - The hub is the authority on licenses: verifies the signature itself, rejects invalid (401), community/expired past grace (403), revoked (403, re-checked under the lock) and over the instance limit (403) - Full-state reconciliation per instance: new findings inserted, existing updated, missing ones resolved, rule states upserted; idempotent by submission id; late older submissions are recorded but not applied - Serialized per customer with a PostgreSQL advisory lock (found by a concurrency test: parallel first submissions raced on the license row) - Size and schema validation; tests on real PostgreSQL, including the full path agent → queue → hub → PostgreSQL with evidence redaction Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
94 lines
4.8 KiB
C#
94 lines
4.8 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Logging.Abstractions;
|
|
using Microsoft.Extensions.Options;
|
|
using SQLInsight.Agent;
|
|
using SQLInsight.Agent.Hub;
|
|
using SQLInsight.Agent.Monitoring;
|
|
using SQLInsight.Collectors;
|
|
using SQLInsight.Connectivity;
|
|
using SQLInsight.Contracts;
|
|
using SQLInsight.Hub.Data;
|
|
using SQLInsight.Repository;
|
|
using SQLInsight.Rules;
|
|
using SQLInsight.Rules.Engine;
|
|
|
|
namespace SQLInsight.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// The whole paid path: agent monitors real SQL Server, queues its state, delivers it to the hub, hub stores it in PostgreSQL.
|
|
/// </summary>
|
|
[Collection(SqlServerTestGroup.Name)]
|
|
[Trait("Category", "Integration")]
|
|
public class HubEndToEndTests(SqlServerFixture sql, HubFixture hub) : IClassFixture<HubFixture>, IAsyncLifetime
|
|
{
|
|
private readonly string _database = "SQLInsightE2E_" + Guid.NewGuid().ToString("N")[..8];
|
|
private InsightRepository _repository = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];");
|
|
_repository = new InsightRepository(sql.DatabaseConnectionString(_database));
|
|
await _repository.MigrateAsync();
|
|
}
|
|
|
|
public Task DisposeAsync() => sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];");
|
|
|
|
private sealed class TestServerClients(Func<HttpClient> create) : IHttpClientFactory
|
|
{
|
|
public HttpClient CreateClient(string name) => create();
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Agent_state_reaches_the_hub_with_sensitive_evidence_redacted()
|
|
{
|
|
var options = Options.Create(new HubOptions { Enabled = true, Url = new Uri("http://localhost/"), License = hub.License("E2E A/S") });
|
|
options.Value.TrustedKeys[HubFixture.KeyId] = hub.PublicKey;
|
|
var outbox = new HubOutbox(options, _repository);
|
|
var monitor = new InstanceMonitor(
|
|
new MonitoredInstance
|
|
{
|
|
Name = "e2e-sql01", Server = sql.Server, Authentication = AuthenticationMode.SqlLogin, UserName = SqlServerFixture.AgentLogin,
|
|
Password = SqlServerFixture.AgentPassword, Provider = ConnectionProvider.SqlClient, TrustServerCertificate = true,
|
|
},
|
|
BuiltInCollectors.Create(), new Dictionary<string, TimeSpan>(), new SqlConnectionFactory(), new RuleEngine(BuiltInRules.Create()),
|
|
_repository, NullLogger.Instance, hubOutbox: outbox);
|
|
var sync = new HubSyncService(options, _repository, new TestServerClients(() => hub.Factory.CreateClient()), TimeProvider.System, NullLogger<HubSyncService>.Instance);
|
|
|
|
await monitor.RunCycleAsync(DateTimeOffset.UtcNow, CancellationToken.None);
|
|
var sent = await sync.RunOnceAsync(CancellationToken.None);
|
|
|
|
Assert.Equal(1, sent);
|
|
var status = await _repository.GetHubStatusAsync();
|
|
Assert.Equal("Valid", status!.LicenseState);
|
|
Assert.Equal(0, status.Pending);
|
|
Assert.NotNull(status.LastSentAt);
|
|
|
|
await using var scope = hub.Factory.Services.CreateAsyncScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<HubDbContext>();
|
|
var instance = await db.Instances.Include(i => i.Customer).SingleAsync(i => i.InstanceKey == "e2e-sql01");
|
|
Assert.Equal("E2E A/S", instance.Customer!.Name);
|
|
Assert.Null(instance.ServerName); // not sent by default
|
|
var findings = await db.Findings.Where(f => f.InstanceId == instance.Id && f.ResolvedAt == null).ToListAsync();
|
|
Assert.Contains(findings, f => f.RuleId == "SQLI-CFG-001");
|
|
Assert.True(await db.RuleStates.CountAsync(r => r.InstanceId == instance.Id) >= BuiltInRules.Create().Count);
|
|
|
|
var latest = Assert.Single(await _repository.GetLatestSubmissionsAsync());
|
|
Assert.Equal("e2e-sql01", latest.InstanceName);
|
|
Assert.NotNull(latest.SentAt);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Rejected_license_is_given_up_not_retried_forever()
|
|
{
|
|
var community = hub.License("Nope A/S", SQLInsight.Licensing.LicensePlan.Professional, expires: DateTimeOffset.UtcNow.AddDays(-45));
|
|
var options = Options.Create(new HubOptions { Enabled = true, Url = new Uri("http://localhost/"), License = community });
|
|
options.Value.TrustedKeys[HubFixture.KeyId] = hub.PublicKey;
|
|
await _repository.EnqueueSubmissionAsync("x", Guid.NewGuid(), "{}", DateTimeOffset.UtcNow);
|
|
var sync = new HubSyncService(options, _repository, new TestServerClients(() => hub.Factory.CreateClient()), TimeProvider.System, NullLogger<HubSyncService>.Instance);
|
|
|
|
// Locally the agent sees the license is long expired and does not even try.
|
|
Assert.Equal(0, await sync.RunOnceAsync(CancellationToken.None));
|
|
Assert.Equal("Expired", (await _repository.GetHubStatusAsync())!.LicenseState);
|
|
}
|
|
}
|