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(() => 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); // The unreadable file is kept beside the new key, not overwritten, in case it can be restored. Assert.Single(Directory.GetFiles(_directory, AgentCredentialKey.FileName + ".unreadable-*")); } [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"); } [Fact] public void The_built_in_ax_rules_follow_the_same_opt_in_and_keep_their_reasons_local() { var selection = new DatabaseSelection(true, ["AX30", "AXTEST"], [], ["AX30"]); RuleResult[] results = [ new() { RuleId = "SQLI-AX-001", RuleTitle = "Batchjob fejlet", Outcome = RuleOutcome.Findings }, new() { RuleId = "SQLI-AX-005", RuleTitle = "Nummerserie", Outcome = RuleOutcome.NotMeasurable, Reason = "Ikke målbar: GRANT SELECT ON [bmssa].[NUMBERSEQUENCETABLE] TO [svc]" }, ]; StoredFinding[] findings = [ Finding("SQLI-AX-001", "database: AX30 · batch: 1234|dat|NAT"), Finding("SQLI-AX-001", "database: AXTEST · batch: 99"), ]; var none = SubmissionBuilder.Build("ax", Info, results, findings, new HubOptions(), Now); var allowed = SubmissionBuilder.Build("ax", Info, results, findings, new HubOptions(), Now, selection); Assert.Empty(none.Findings); Assert.Empty(none.Rules); Assert.Equal(["database: AX30 · batch: 1234|dat|NAT"], allowed.Findings.Select(f => f.Subject)); Assert.Equal(SubmissionBuilder.AxReasonKeptLocally, allowed.Rules.Single(r => r.RuleId == "SQLI-AX-005").Reason); } }