sqlinsight-pro/tests/SQLInsight.Agent.Tests/InstanceConfigurationAgentTests.cs
Søren Eggert Lundsteen Olsen 81b8376026 Agent: instances from the repository, request queue, sealed passwords, AX hub opt-in
The agent mirrors SQLInsight:Instances into the repository at start (read-only
"fra konfigurationsfil" rows, every database monitored as before) and reads the
configuration on every tick: instances added, paused, removed or changed in
SQLInsight Web, and database choices, take effect without a restart. If the
repository cannot be read before the first load, the configuration file alone
is used.

AgentRequestService runs connection tests (with the permission check), database
discovery and instance discovery (SQL Server Browser, registry) as the agent's
own identity, and refreshes every instance's database list hourly. The agent
keeps an RSA key (DPAPI LocalMachine, restricted folder) and publishes its public
key and identity; SQL login passwords sealed by the web UI are opened only here.

Dynamics AX findings and AX rule outcomes are sent to SQLInsight Hub only for
databases where it is turned on (default off).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-02 13:22:41 +02:00

147 lines
5.7 KiB
C#

using System.Text;
using SQLInsight.Agent.Configuration;
using SQLInsight.Agent.Hub;
using SQLInsight.Core.Configuration;
using SQLInsight.Core.Findings;
using SQLInsight.Core.Instances;
using SQLInsight.Repository;
using SQLInsight.Rules.Engine;
namespace SQLInsight.Agent.Tests;
public sealed class AgentCredentialKeyTests : IDisposable
{
private readonly string _directory = Path.Combine(Path.GetTempPath(), "sqlinsight-key-" + Guid.NewGuid().ToString("N"));
public void Dispose()
{
if (Directory.Exists(_directory))
{
Directory.Delete(_directory, recursive: true);
}
}
[Fact]
public void Key_is_created_once_kept_and_opens_what_the_web_ui_sealed_with_its_public_key()
{
string keyId;
byte[] envelope;
using (var first = AgentCredentialKey.LoadOrCreate(_directory))
{
keyId = first.KeyId;
envelope = CredentialEnvelope.Seal("Hemmelig-1!", first.KeyId, first.Describe(DateTimeOffset.UtcNow).PublicKey, CredentialEnvelope.Context("sql01,1450", "mon"));
}
using var again = AgentCredentialKey.LoadOrCreate(_directory);
Assert.Equal(keyId, again.KeyId);
Assert.StartsWith("agent-", again.KeyId, StringComparison.Ordinal);
Assert.Equal("Hemmelig-1!", again.Open(envelope, "sql01,1450", "mon"));
Assert.Throws<CredentialException>(() => again.Open(envelope, "other", "mon"));
var stored = File.ReadAllBytes(Path.Combine(_directory, AgentCredentialKey.FileName));
Assert.DoesNotContain("PRIVATE KEY", Encoding.ASCII.GetString(stored), StringComparison.Ordinal);
}
[Fact]
public void An_unreadable_key_file_is_replaced_and_old_passwords_must_be_entered_again()
{
string oldId;
using (var first = AgentCredentialKey.LoadOrCreate(_directory))
{
oldId = first.KeyId;
}
File.WriteAllBytes(Path.Combine(_directory, AgentCredentialKey.FileName), [1, 2, 3, 4]);
using var replaced = AgentCredentialKey.LoadOrCreate(_directory);
Assert.NotEqual(oldId, replaced.KeyId);
}
[Fact]
public void The_published_identity_is_the_account_and_computer_the_agent_runs_as()
{
using var key = AgentCredentialKey.LoadOrCreate(_directory);
var row = key.Describe(DateTimeOffset.UtcNow);
Assert.Equal(Environment.MachineName, row.MachineName);
Assert.False(string.IsNullOrEmpty(row.ServiceAccount));
Assert.Equal(key.KeyId, row.KeyId);
}
}
public sealed class InstanceFinderTests
{
private static byte[] Response(string text)
{
var body = Encoding.ASCII.GetBytes(text);
return [0x05, (byte)(body.Length & 0xFF), (byte)(body.Length >> 8), .. body];
}
[Fact]
public void Browser_answer_lists_default_and_named_instances()
{
var instances = InstanceFinder.Parse(Response(
"ServerName;SQL01;InstanceName;MSSQLSERVER;IsClustered;No;Version;15.0.2000.5;tcp;1433;;" +
"ServerName;SQL01;InstanceName;AX30;IsClustered;Yes;Version;9.00.5000.00;tcp;1450;np;\\\\SQL01\\pipe\\MSSQL$AX30\\sql\\query;;"), "sql01");
Assert.Equal(2, instances.Count);
Assert.Equal(("SQL01", "MSSQLSERVER", 1433, false), (instances[0].Server, instances[0].InstanceName, instances[0].TcpPort, instances[0].IsClustered));
Assert.Equal(("SQL01\\AX30", "9.00.5000.00", 1450, true), (instances[1].Server, instances[1].Version, instances[1].TcpPort, instances[1].IsClustered));
Assert.All(instances, i => Assert.Equal("SQL Server Browser", i.Source));
}
[Fact]
public void Anything_else_is_ignored()
{
Assert.Empty(InstanceFinder.Parse([0x04, 0, 0], "x"));
Assert.Empty(InstanceFinder.Parse([0x05], "x"));
Assert.Empty(InstanceFinder.Parse(Response("ServerName;SQL01;;"), "x"));
}
}
public sealed class DynamicsAxHubTests
{
private static readonly DateTimeOffset Now = new(2026, 10, 2, 12, 0, 0, TimeSpan.Zero);
private static readonly InstanceInfo Info = new()
{
ServerName = "AX01", Version = SqlServerVersion.Parse("9.0.5000.0"), Edition = "Standard Edition", EngineEdition = EngineEdition.Standard,
};
private static StoredFinding Finding(string ruleId, string subject) => new()
{
FindingId = 1, RuleId = ruleId, Subject = subject, Severity = Severity.Medium, Title = "t", Explanation = "e", Recommendation = "r",
FirstSeenAt = Now.UtcDateTime, Evidence = [],
};
private static readonly RuleResult[] Results =
[
new() { RuleId = "COM-AX-001", RuleTitle = "AX 3.0", Outcome = RuleOutcome.Findings },
new() { RuleId = "SQLI-DB-001", RuleTitle = "Backup", Outcome = RuleOutcome.Findings },
];
private static readonly StoredFinding[] Findings =
[
Finding("COM-AX-001", "database: AX30"), Finding("COM-AX-001", "database: AXTEST"), Finding("SQLI-DB-001", "database: AX30"),
];
[Fact]
public void Dynamics_ax_findings_stay_in_the_network_by_default()
{
var submission = SubmissionBuilder.Build("ax", Info, Results, Findings, new HubOptions(), Now);
Assert.Equal(["SQLI-DB-001"], submission.Findings.Select(f => f.RuleId));
Assert.Equal(["SQLI-DB-001"], submission.Rules.Select(r => r.RuleId));
}
[Fact]
public void Only_the_databases_that_allow_it_send_dynamics_ax_findings()
{
var selection = new DatabaseSelection(true, ["AX30", "AXTEST"], [], ["AX30"]);
var submission = SubmissionBuilder.Build("ax", Info, Results, Findings, new HubOptions(), Now, selection);
Assert.Equal(["database: AX30", "database: AX30"], submission.Findings.Select(f => f.Subject));
Assert.Contains(submission.Rules, r => r.RuleId == "COM-AX-001");
}
}