diff options
Diffstat (limited to 'src/Agent.Windows/Recovery')
| -rw-r--r-- | src/Agent.Windows/Recovery/RecoveryManager.cs | 52 |
1 files changed, 40 insertions, 12 deletions
diff --git a/src/Agent.Windows/Recovery/RecoveryManager.cs b/src/Agent.Windows/Recovery/RecoveryManager.cs index 9dfeef9..40a7923 100644 --- a/src/Agent.Windows/Recovery/RecoveryManager.cs +++ b/src/Agent.Windows/Recovery/RecoveryManager.cs @@ -2,6 +2,7 @@ using System.Security.AccessControl; using System.Security.Cryptography; using Agent.Windows.Security; using Agent.Windows.Network; +using Agent.Admin.Client; namespace Agent.Windows.Recovery; @@ -10,15 +11,39 @@ public sealed record RecoveryResult(int JournalsRecovered, int ResourcesRecovere public bool IsClean => Errors.Count == 0; } -public sealed class RecoveryManager(string journalDirectory, ISecurityAuditSink audit) +public sealed class RecoveryManager(string journalDirectory, ISecurityAuditSink audit, IElevatedNetworkSessionFactory? elevatedNetwork = null) { - public RecoveryResult RecoverAll() + public RecoveryResult RecoverAll() => RecoverAllAsync(CancellationToken.None).GetAwaiter().GetResult(); + + public async Task<RecoveryResult> RecoverAllAsync(CancellationToken cancellationToken) { if (!Directory.Exists(journalDirectory)) return new(0, 0, []); int journals = 0; int resources = 0; var errors = new List<string>(); - foreach (string path in Directory.EnumerateFiles(journalDirectory, "*.journal.json").Order(StringComparer.Ordinal)) + string[] paths = Directory.EnumerateFiles(journalDirectory, "*.journal.json").Order(StringComparer.Ordinal).ToArray(); + bool requiresElevation = paths.Select(TaskResourceJournal.Open).SelectMany(static item => item.Entries) + .Any(static entry => entry.State != JournalEntryState.Cleaned && entry.Kind == "network.loopback_exemption"); + IElevatedNetworkSession? elevated = null; + if (requiresElevation) + { + if (elevatedNetwork is null) return new(0, 0, ["Persistent network Recovery requires the elevated helper."]); + audit.Write(new(DateTimeOffset.UtcNow, Guid.Empty, "sandbox.elevation.request", "requested", + new Dictionary<string, string> { ["reason"] = "startup_recovery" })); + try { elevated = await elevatedNetwork.StartAsync(Guid.Empty, cancellationToken).ConfigureAwait(false); } + catch (AdminOperationException ex) + { + audit.Write(new(DateTimeOffset.UtcNow, Guid.Empty, + ex.Code == "sandbox.ipc_rejected" ? "sandbox.ipc.reject" : "sandbox.elevation.deny", "failed", + new Dictionary<string, string> { ["reason"] = "startup_recovery", ["message"] = ex.Message }, ErrorCode: ex.Code)); + throw; + } + audit.Write(new(DateTimeOffset.UtcNow, Guid.Empty, "sandbox.elevation.success", "connected", + new Dictionary<string, string> { ["reason"] = "startup_recovery" })); + } + try + { + foreach (string path in paths) { journals++; try @@ -28,8 +53,8 @@ public sealed class RecoveryManager(string journalDirectory, ISecurityAuditSink foreach (JournalEntry entry in journal.Entries.Reverse()) { if (entry.State == JournalEntryState.Cleaned) continue; - if (entry.State == JournalEntryState.Applied) RecoverEntry(journal.TaskId, entry); - CleanupPrepared(entry); + if (entry.State == JournalEntryState.Applied) await RecoverEntryAsync(journal.TaskId, entry, elevated, cancellationToken).ConfigureAwait(false); + await CleanupPreparedAsync(entry, elevated, cancellationToken).ConfigureAwait(false); journal.MarkCleaned(entry.Id); resources++; } @@ -41,10 +66,12 @@ public sealed class RecoveryManager(string journalDirectory, ISecurityAuditSink errors.Add($"{path}: {ex.Message}"); } } + } + finally { if (elevated is not null) await elevated.DisposeAsync().ConfigureAwait(false); } return new(journals, resources, errors); } - private static void RecoverEntry(Guid taskId, JournalEntry entry) + private static async Task RecoverEntryAsync(Guid taskId, JournalEntry entry, IElevatedNetworkSession? elevated, CancellationToken cancellationToken) { switch (entry.Kind) { @@ -55,14 +82,15 @@ public sealed class RecoveryManager(string journalDirectory, ISecurityAuditSink case "dir.rmdir": RecoverRmdir(entry); break; case "acl": RecoverAcl(entry); break; case "wfp.dynamic": break; // BFE removes every object when the originating dynamic session ends. - case "network.loopback_exemption": RecoverLoopback(entry); break; + case "network.loopback_exemption": await RecoverLoopbackAsync(entry, elevated, cancellationToken).ConfigureAwait(false); break; default: throw new InvalidDataException($"Unknown journal resource kind '{entry.Kind}' for task '{taskId}'."); } } - private static void RecoverLoopback(JournalEntry entry) + private static async Task RecoverLoopbackAsync(JournalEntry entry, IElevatedNetworkSession? elevated, CancellationToken cancellationToken) { - if (!bool.Parse(entry.Data["originally_present"])) LoopbackExemption.Remove(new System.Security.Principal.SecurityIdentifier(entry.Target)); + if (elevated is null) throw new InvalidOperationException("Persistent network Recovery requires the elevated helper."); + await elevated.RecoverLoopbackAsync(entry.Target, bool.Parse(entry.Data["originally_present"]), cancellationToken).ConfigureAwait(false); } private static void RecoverFile(JournalEntry entry) @@ -124,10 +152,10 @@ public sealed class RecoveryManager(string journalDirectory, ISecurityAuditSink Directory.CreateDirectory(entry.Target); } - private static void CleanupPrepared(JournalEntry entry) + private static async Task CleanupPreparedAsync(JournalEntry entry, IElevatedNetworkSession? elevated, CancellationToken cancellationToken) { - if (entry.Kind == "network.loopback_exemption" && !bool.Parse(entry.Data["originally_present"])) - LoopbackExemption.Remove(new System.Security.Principal.SecurityIdentifier(entry.Target)); + if (entry.Kind == "network.loopback_exemption" && entry.State != JournalEntryState.Applied) + await RecoverLoopbackAsync(entry, elevated, cancellationToken).ConfigureAwait(false); if (entry.Data.TryGetValue("temporary_path", out string? temporary) && temporary.Length > 0) File.Delete(temporary); if (entry.Data.TryGetValue("backup_path", out string? backup) && backup.Length > 0) { |
