diff --git a/SecureBootDashboard.Api.Tests/Controllers/CertificateUpdateControllerTests.cs b/SecureBootDashboard.Api.Tests/Controllers/CertificateUpdateControllerTests.cs new file mode 100644 index 0000000..dae5cbe --- /dev/null +++ b/SecureBootDashboard.Api.Tests/Controllers/CertificateUpdateControllerTests.cs @@ -0,0 +1,152 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Moq; +using SecureBootDashboard.Api.Controllers; +using SecureBootDashboard.Api.Services; +using SecureBootWatcher.Shared.Models; +using Xunit; + +namespace SecureBootDashboard.Api.Tests.Controllers +{ + public class CertificateUpdateControllerTests + { + private readonly Mock _mockUpdateService; + private readonly Mock> _mockLogger; + private readonly CertificateUpdateController _controller; + + public CertificateUpdateControllerTests() + { + _mockUpdateService = new Mock(); + _mockLogger = new Mock>(); + _controller = new CertificateUpdateController(_mockUpdateService.Object, _mockLogger.Object); + } + + [Fact] + public async Task TriggerUpdateAsync_ValidRequest_ReturnsOkResult() + { + // Arrange + var request = new CertificateUpdateController.CertificateUpdateRequest + { + FleetId = "fleet-01", + IssuedBy = "admin@example.com", + Notes = "Test update" + }; + + var expectedResult = new CertificateUpdateResult( + Guid.NewGuid(), + 10, + "Update command sent successfully to 10 device(s)"); + + _mockUpdateService + .Setup(s => s.SendUpdateCommandAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(expectedResult); + + // Act + var result = await _controller.TriggerUpdateAsync(request, CancellationToken.None); + + // Assert + var okResult = Assert.IsType(result.Result); + var returnValue = Assert.IsType(okResult.Value); + Assert.Equal(expectedResult.TargetDeviceCount, returnValue.TargetDeviceCount); + } + + [Fact] + public async Task TriggerUpdateAsync_ServiceThrowsException_Returns500() + { + // Arrange + var request = new CertificateUpdateController.CertificateUpdateRequest + { + FleetId = "fleet-01", + IssuedBy = "admin@example.com" + }; + + _mockUpdateService + .Setup(s => s.SendUpdateCommandAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("Test exception")); + + // Act + var result = await _controller.TriggerUpdateAsync(request, CancellationToken.None); + + // Assert + var statusCodeResult = Assert.IsType(result.Result); + Assert.Equal(500, statusCodeResult.StatusCode); + } + + [Fact] + public async Task GetCommandStatusAsync_ValidCommandId_ReturnsOkResult() + { + // Arrange + var commandId = Guid.NewGuid(); + var expectedStatus = new CertificateUpdateCommandStatus( + commandId, + "fleet-01", + 10, + 5, + DateTimeOffset.UtcNow, + null, + "InProgress"); + + _mockUpdateService + .Setup(s => s.GetCommandStatusAsync(commandId, It.IsAny())) + .ReturnsAsync(expectedStatus); + + // Act + var result = await _controller.GetCommandStatusAsync(commandId, CancellationToken.None); + + // Assert + var okResult = Assert.IsType(result.Result); + var returnValue = Assert.IsType(okResult.Value); + Assert.Equal(commandId, returnValue.CommandId); + } + + [Fact] + public async Task GetCommandStatusAsync_CommandNotFound_ReturnsNotFound() + { + // Arrange + var commandId = Guid.NewGuid(); + + _mockUpdateService + .Setup(s => s.GetCommandStatusAsync(commandId, It.IsAny())) + .ReturnsAsync((CertificateUpdateCommandStatus?)null); + + // Act + var result = await _controller.GetCommandStatusAsync(commandId, CancellationToken.None); + + // Assert + Assert.IsType(result.Result); + } + + [Fact] + public async Task TriggerUpdateAsync_WithTargetDevices_SendsCorrectCommand() + { + // Arrange + var targetDevices = new[] { "DEVICE-01", "DEVICE-02" }; + var request = new CertificateUpdateController.CertificateUpdateRequest + { + FleetId = "fleet-01", + TargetDevices = targetDevices, + IssuedBy = "admin@example.com", + UpdateFlags = 0x5944 + }; + + CertificateUpdateCommand? capturedCommand = null; + _mockUpdateService + .Setup(s => s.SendUpdateCommandAsync(It.IsAny(), It.IsAny())) + .Callback((cmd, ct) => capturedCommand = cmd) + .ReturnsAsync(new CertificateUpdateResult(Guid.NewGuid(), 2, "Success")); + + // Act + await _controller.TriggerUpdateAsync(request, CancellationToken.None); + + // Assert + Assert.NotNull(capturedCommand); + Assert.Equal("fleet-01", capturedCommand.FleetId); + Assert.Equal(targetDevices, capturedCommand.TargetDevices); + Assert.Equal((uint)0x5944, capturedCommand.UpdateFlags); + Assert.Equal("admin@example.com", capturedCommand.IssuedBy); + } + } +} diff --git a/SecureBootDashboard.Api.Tests/SecureBootDashboard.Api.Tests.csproj b/SecureBootDashboard.Api.Tests/SecureBootDashboard.Api.Tests.csproj index 00391d6..e8635f4 100644 --- a/SecureBootDashboard.Api.Tests/SecureBootDashboard.Api.Tests.csproj +++ b/SecureBootDashboard.Api.Tests/SecureBootDashboard.Api.Tests.csproj @@ -12,6 +12,7 @@ + diff --git a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs deleted file mode 100644 index 2217181..0000000 --- a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.AssemblyInfo.cs b/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.AssemblyInfo.cs deleted file mode 100644 index 72c4929..0000000 --- a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("SecureBootDashboard.Api.Tests")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyProductAttribute("SecureBootDashboard.Api.Tests")] -[assembly: System.Reflection.AssemblyTitleAttribute("SecureBootDashboard.Api.Tests")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.AssemblyInfoInputs.cache b/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.AssemblyInfoInputs.cache deleted file mode 100644 index 9398e74..0000000 --- a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -f097ba72437f2013fdf8cd265429800cca4aa0861a04f9e9d95b8653ee2886e6 diff --git a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.GeneratedMSBuildEditorConfig.editorconfig b/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index 2ca8280..0000000 --- a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,15 +0,0 @@ -is_global = true -build_property.TargetFramework = net8.0 -build_property.TargetPlatformMinVersion = -build_property.UsingMicrosoftNETSdkWeb = -build_property.ProjectTypeGuids = -build_property.InvariantGlobalization = -build_property.PlatformNeutralAssembly = -build_property.EnforceExtendedAnalyzerRules = -build_property._SupportedPlatformList = Linux,macOS,Windows -build_property.RootNamespace = SecureBootDashboard.Api.Tests -build_property.ProjectDir = /home/runner/work/Nimbus.BootCertWatcher/Nimbus.BootCertWatcher/SecureBootDashboard.Api.Tests/ -build_property.EnableComHosting = -build_property.EnableGeneratedComInterfaceComImportInterop = -build_property.EffectiveAnalysisLevelStyle = 8.0 -build_property.EnableCodeStyleSeverity = diff --git a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.GlobalUsings.g.cs b/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.GlobalUsings.g.cs deleted file mode 100644 index 2cd3d38..0000000 --- a/SecureBootDashboard.Api.Tests/obj/Debug/net8.0/SecureBootDashboard.Api.Tests.GlobalUsings.g.cs +++ /dev/null @@ -1,9 +0,0 @@ -// -global using global::System; -global using global::System.Collections.Generic; -global using global::System.IO; -global using global::System.Linq; -global using global::System.Net.Http; -global using global::System.Threading; -global using global::System.Threading.Tasks; -global using global::Xunit; diff --git a/SecureBootDashboard.Api/Configuration/CertificateUpdateServiceOptions.cs b/SecureBootDashboard.Api/Configuration/CertificateUpdateServiceOptions.cs new file mode 100644 index 0000000..5b9cf66 --- /dev/null +++ b/SecureBootDashboard.Api/Configuration/CertificateUpdateServiceOptions.cs @@ -0,0 +1,75 @@ +using System; + +namespace SecureBootDashboard.Api.Configuration +{ + /// + /// Configuration options for the Certificate Update Service. + /// + public sealed class CertificateUpdateServiceOptions + { + /// + /// Whether the certificate update service is enabled. + /// + public bool Enabled { get; set; } = false; + + /// + /// Storage account queue service URI (e.g., https://mystorageaccount.queue.core.windows.net). + /// + public Uri? QueueServiceUri { get; set; } + + /// + /// Queue name for certificate update commands. + /// + public string CommandQueueName { get; set; } = "secureboot-update-commands"; + + /// + /// Authentication method: "ManagedIdentity", "AppRegistration", "Certificate", "DefaultAzureCredential", or "ConnectionString". + /// + public string AuthenticationMethod { get; set; } = "DefaultAzureCredential"; + + /// + /// Connection string (only used if AuthenticationMethod is "ConnectionString"). + /// + public string? ConnectionString { get; set; } + + /// + /// Application (Client) ID from Azure App Registration. + /// + public string? ClientId { get; set; } + + /// + /// Directory (Tenant) ID. + /// + public string? TenantId { get; set; } + + /// + /// Client Secret from App Registration. + /// + public string? ClientSecret { get; set; } + + /// + /// Path to certificate file (.pfx or .pem). + /// + public string? CertificatePath { get; set; } + + /// + /// Password for the certificate file. + /// + public string? CertificatePassword { get; set; } + + /// + /// Certificate thumbprint for certificate store. + /// + public string? CertificateThumbprint { get; set; } + + /// + /// Certificate store location. + /// + public string CertificateStoreLocation { get; set; } = "CurrentUser"; + + /// + /// Certificate store name. + /// + public string CertificateStoreName { get; set; } = "My"; + } +} diff --git a/SecureBootDashboard.Api/Controllers/CertificateUpdateController.cs b/SecureBootDashboard.Api/Controllers/CertificateUpdateController.cs new file mode 100644 index 0000000..1c0b83f --- /dev/null +++ b/SecureBootDashboard.Api/Controllers/CertificateUpdateController.cs @@ -0,0 +1,131 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using SecureBootDashboard.Api.Services; +using SecureBootWatcher.Shared.Models; + +namespace SecureBootDashboard.Api.Controllers +{ + /// + /// Controller for managing certificate update commands. + /// + [ApiController] + [Route("api/[controller]")] + public sealed class CertificateUpdateController : ControllerBase + { + private readonly ICertificateUpdateService _updateService; + private readonly ILogger _logger; + + public CertificateUpdateController( + ICertificateUpdateService updateService, + ILogger logger) + { + _updateService = updateService; + _logger = logger; + } + + /// + /// Trigger certificate update for a fleet or group of devices. + /// + /// The update command request. + /// Cancellation token. + /// Result of the update command. + [HttpPost("trigger")] + public async Task> TriggerUpdateAsync( + [FromBody] CertificateUpdateRequest request, + CancellationToken cancellationToken) + { + try + { + // Sanitize fleet ID for logging to prevent log forging + var sanitizedFleetId = request.FleetId?.Replace("\r", "").Replace("\n", "") ?? "ALL"; + _logger.LogInformation( + "Received certificate update request for fleet {FleetId}", + sanitizedFleetId); + + var command = new CertificateUpdateCommand + { + FleetId = request.FleetId, + TargetDevices = request.TargetDevices ?? Array.Empty(), + UpdateFlags = request.UpdateFlags, + IssuedBy = request.IssuedBy, + Notes = request.Notes, + ExpiresAtUtc = request.ExpiresAtUtc + }; + + var result = await _updateService.SendUpdateCommandAsync(command, cancellationToken); + + return Ok(result); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to trigger certificate update for fleet {FleetId}", request.FleetId); + return StatusCode(500, new { Error = "Failed to trigger certificate update" }); + } + } + + /// + /// Get the status of a certificate update command. + /// + /// The command ID. + /// Cancellation token. + /// Command status. + [HttpGet("status/{commandId:guid}")] + public async Task> GetCommandStatusAsync( + Guid commandId, + CancellationToken cancellationToken) + { + var status = await _updateService.GetCommandStatusAsync(commandId, cancellationToken); + + if (status == null) + { + return NotFound(new { Error = "Command not found" }); + } + + return Ok(status); + } + + /// + /// Request model for triggering certificate updates. + /// + public sealed record CertificateUpdateRequest + { + /// + /// The fleet ID to target. If null, targets all devices. + /// + public string? FleetId { get; init; } + + /// + /// Specific device machine names to target. If empty, targets all devices in fleet. + /// + [MaxLength(1000, ErrorMessage = "Cannot target more than 1000 devices at once")] + public string[]? TargetDevices { get; init; } + + /// + /// The update flags to apply. If null, uses Windows defaults. + /// + public uint? UpdateFlags { get; init; } + + /// + /// When this command expires (optional). + /// + public DateTimeOffset? ExpiresAtUtc { get; init; } + + /// + /// User or system that issued this command. + /// + [Required] + [MaxLength(100)] + [RegularExpression(@"^[\w\-.@]+$", ErrorMessage = "IssuedBy must be alphanumeric and may include dash, underscore, dot, or at-sign.")] + public string? IssuedBy { get; init; } + + /// + /// Additional notes or reason for this update command. + /// + public string? Notes { get; init; } + } + } +} diff --git a/SecureBootDashboard.Api/Program.cs b/SecureBootDashboard.Api/Program.cs index b6636cc..739acc6 100644 --- a/SecureBootDashboard.Api/Program.cs +++ b/SecureBootDashboard.Api/Program.cs @@ -138,6 +138,25 @@ Log.Information("Configuring Export Service..."); builder.Services.AddScoped(); + // Configure Certificate Update Service + Log.Information("Configuring Certificate Update Service..."); + var updateConfig = builder.Configuration.GetSection("CertificateUpdateService"); + var updateEnabled = updateConfig.GetValue("Enabled"); + Log.Information("Certificate Update Service Enabled: {Enabled}", updateEnabled); + + if (updateEnabled) + { + var updateQueueUri = updateConfig.GetValue("QueueServiceUri"); + var updateQueueName = updateConfig.GetValue("CommandQueueName"); + + Log.Information(" Command Queue URI: {QueueUri}", updateQueueUri); + Log.Information(" Command Queue Name: {QueueName}", updateQueueName); + Log.Information(" Auth Method is configured."); + + builder.Services.Configure(updateConfig); + builder.Services.AddScoped(); + } + // Configure Azure Queue Processor Log.Information("Configuring Queue Processor..."); var queueConfig = builder.Configuration.GetSection("QueueProcessor"); diff --git a/SecureBootDashboard.Api/Services/CertificateUpdateService.cs b/SecureBootDashboard.Api/Services/CertificateUpdateService.cs new file mode 100644 index 0000000..00d5842 --- /dev/null +++ b/SecureBootDashboard.Api/Services/CertificateUpdateService.cs @@ -0,0 +1,321 @@ +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Identity; +using Azure.Storage.Queues; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using SecureBootDashboard.Api.Configuration; +using SecureBootDashboard.Api.Data; +using SecureBootWatcher.Shared.Models; +using SecureBootWatcher.Shared.Transport; + +namespace SecureBootDashboard.Api.Services +{ + /// + /// Service for managing certificate update commands via Azure Queue Storage. + /// + public sealed class CertificateUpdateService : ICertificateUpdateService + { + private readonly SecureBootDbContext _dbContext; + private readonly ILogger _logger; + private readonly IOptionsMonitor _optionsMonitor; + private QueueClient? _commandQueueClient; + + public CertificateUpdateService( + SecureBootDbContext dbContext, + ILogger logger, + IOptionsMonitor optionsMonitor) + { + _dbContext = dbContext; + _logger = logger; + _optionsMonitor = optionsMonitor; + } + + public async Task SendUpdateCommandAsync( + CertificateUpdateCommand command, + CancellationToken cancellationToken = default) + { + var options = _optionsMonitor.CurrentValue; + + if (!options.Enabled) + { + _logger.LogWarning("Certificate update service is disabled"); + return new CertificateUpdateResult( + command.CommandId, + 0, + "Certificate update service is disabled"); + } + + // Determine target devices + var targetDevices = await GetTargetDevicesAsync(command, cancellationToken); + var targetCount = targetDevices.Count; + + // Sanitize fleet ID for logging to prevent log forging + var sanitizedFleetId = command.FleetId?.Replace("\r", "").Replace("\n", "") ?? "ALL"; + _logger.LogInformation( + "Certificate update command {CommandId} targeting {Count} devices in fleet {FleetId}", + command.CommandId, + targetCount, + sanitizedFleetId); + + if (targetCount == 0) + { + return new CertificateUpdateResult( + command.CommandId, + 0, + "No devices found matching the criteria"); + } + + // Initialize queue client if needed + if (_commandQueueClient == null) + { + _commandQueueClient = CreateQueueClient(options); + if (_commandQueueClient == null) + { + throw new InvalidOperationException("Failed to create queue client"); + } + } + + // Send command to queue + var envelope = new CertificateUpdateCommandEnvelope + { + Command = command, + EnqueuedAtUtc = DateTimeOffset.UtcNow + }; + + var messageJson = JsonSerializer.Serialize(envelope); + + try + { + await _commandQueueClient.SendMessageAsync(messageJson, cancellationToken); + + _logger.LogInformation( + "Certificate update command {CommandId} sent to queue for {Count} devices", + command.CommandId, + targetCount); + + return new CertificateUpdateResult( + command.CommandId, + targetCount, + $"Update command sent successfully to {targetCount} device(s)"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to send certificate update command {CommandId}", command.CommandId); + throw; + } + } + + public Task GetCommandStatusAsync( + Guid commandId, + CancellationToken cancellationToken = default) + { + // Note: This is a basic implementation. In a production system, you would: + // 1. Store command metadata in a database table + // 2. Track device acknowledgments and completion + // 3. Monitor queue message processing + + // For now, return a placeholder status + _logger.LogInformation("Getting status for command {CommandId}", commandId); + + // This would be enhanced to query a commands table in the database + var status = new CertificateUpdateCommandStatus( + commandId, + null, + 0, + 0, + DateTimeOffset.UtcNow, + null, + "Pending"); + + return Task.FromResult(status); + } + + private async Task> GetTargetDevicesAsync( + CertificateUpdateCommand command, + CancellationToken cancellationToken) + { + IQueryable query = _dbContext.Devices; + + // Filter by fleet if specified + if (!string.IsNullOrWhiteSpace(command.FleetId)) + { + query = query.Where(d => d.FleetId == command.FleetId); + } + + // Filter by specific devices if specified + if (command.TargetDevices != null && command.TargetDevices.Length > 0) + { + query = query.Where(d => command.TargetDevices.Contains(d.MachineName)); + } + + var devices = await query + .Select(d => d.MachineName) + .ToListAsync(cancellationToken); + + return devices; + } + + private QueueClient? CreateQueueClient(CertificateUpdateServiceOptions options) + { + try + { + // Connection String method + if (options.AuthenticationMethod.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase) && + !string.IsNullOrWhiteSpace(options.ConnectionString)) + { + _logger.LogWarning("Using Connection String authentication. NOT recommended for production."); + return new QueueClient(options.ConnectionString, options.CommandQueueName); + } + + // Validate QueueServiceUri for Entra ID auth + if (options.QueueServiceUri == null) + { + _logger.LogError("QueueServiceUri is required for Entra ID authentication."); + return null; + } + + var queueUri = new Uri(options.QueueServiceUri, options.CommandQueueName); + TokenCredential credential; + + // App Registration + Client Secret + if (options.AuthenticationMethod.Equals("AppRegistration", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(options.TenantId) || + string.IsNullOrWhiteSpace(options.ClientId) || + string.IsNullOrWhiteSpace(options.ClientSecret)) + { + _logger.LogError("TenantId, ClientId, and ClientSecret are required for AppRegistration authentication."); + return null; + } + + _logger.LogInformation("Using App Registration authentication for command queue"); + credential = new ClientSecretCredential(options.TenantId, options.ClientId, options.ClientSecret); + return new QueueClient(queueUri, credential); + } + + // Certificate-based authentication + if (options.AuthenticationMethod.Equals("Certificate", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(options.TenantId) || string.IsNullOrWhiteSpace(options.ClientId)) + { + _logger.LogError("TenantId and ClientId are required for Certificate authentication."); + return null; + } + + X509Certificate2? certificate = null; + + // From file + if (!string.IsNullOrWhiteSpace(options.CertificatePath)) + { + var password = options.CertificatePassword; + certificate = string.IsNullOrWhiteSpace(password) + ? new X509Certificate2(options.CertificatePath) + : new X509Certificate2(options.CertificatePath, password); + _logger.LogInformation("Loaded certificate from file for command queue"); + } + // From Windows Certificate Store + else if (!string.IsNullOrWhiteSpace(options.CertificateThumbprint)) + { + var storeLocation = options.CertificateStoreLocation.Equals("LocalMachine", StringComparison.OrdinalIgnoreCase) + ? StoreLocation.LocalMachine + : StoreLocation.CurrentUser; + + var storeName = Enum.TryParse(options.CertificateStoreName, true, out var parsed) + ? parsed + : StoreName.My; + + using var store = new X509Store(storeName, storeLocation); + store.Open(OpenFlags.ReadOnly); + + var certificates = store.Certificates.Find( + X509FindType.FindByThumbprint, + options.CertificateThumbprint.Replace(" ", "").Replace(":", ""), + validOnly: false); + + if (certificates.Count == 0) + { + _logger.LogError("Certificate not found in store"); + return null; + } + + certificate = certificates[0]; + _logger.LogInformation("Loaded certificate from store for command queue"); + } + + if (certificate == null) + { + _logger.LogError("Certificate could not be loaded."); + return null; + } + + credential = new ClientCertificateCredential(options.TenantId, options.ClientId, certificate); + return new QueueClient(queueUri, credential); + } + + // Managed Identity + if (options.AuthenticationMethod.Equals("ManagedIdentity", StringComparison.OrdinalIgnoreCase)) + { + credential = string.IsNullOrWhiteSpace(options.ClientId) + ? new ManagedIdentityCredential() + : new ManagedIdentityCredential(options.ClientId); + + _logger.LogInformation("Using Managed Identity for command queue"); + return new QueueClient(queueUri, credential); + } + + // DefaultAzureCredential + if (options.AuthenticationMethod.Equals("DefaultAzureCredential", StringComparison.OrdinalIgnoreCase) || + string.IsNullOrWhiteSpace(options.AuthenticationMethod)) + { + var credentialOptions = new DefaultAzureCredentialOptions(); + + if (!string.IsNullOrWhiteSpace(options.ClientId)) + { + credentialOptions.ManagedIdentityClientId = options.ClientId; + } + + credential = new DefaultAzureCredential(credentialOptions); + _logger.LogInformation("Using DefaultAzureCredential for command queue"); + return new QueueClient(queueUri, credential); + } + + _logger.LogError("Invalid AuthenticationMethod: {Method}", options.AuthenticationMethod); + return null; + } + catch (Azure.Identity.AuthenticationFailedException ex) + { + _logger.LogError(ex, "Authentication failed while creating command queue client"); + return null; + } + catch (Azure.RequestFailedException ex) + { + _logger.LogError(ex, "Azure request failed while creating command queue client"); + return null; + } + catch (UriFormatException ex) + { + _logger.LogError(ex, "Invalid queue URI format"); + return null; + } + catch (Exception ex) + { + // Rethrow critical exceptions + if (ex is OutOfMemoryException || + ex is StackOverflowException || + ex is ThreadAbortException) + { + throw; + } + _logger.LogError(ex, "Unexpected error while creating command queue client"); + return null; + } + } + } +} diff --git a/SecureBootDashboard.Api/Services/ICertificateUpdateService.cs b/SecureBootDashboard.Api/Services/ICertificateUpdateService.cs new file mode 100644 index 0000000..f836560 --- /dev/null +++ b/SecureBootDashboard.Api/Services/ICertificateUpdateService.cs @@ -0,0 +1,53 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using SecureBootWatcher.Shared.Models; + +namespace SecureBootDashboard.Api.Services +{ + /// + /// Service for managing certificate update commands. + /// + public interface ICertificateUpdateService + { + /// + /// Sends a certificate update command for a fleet or group of devices. + /// + /// The update command to send. + /// Cancellation token. + /// The command ID and number of target devices. + Task SendUpdateCommandAsync( + CertificateUpdateCommand command, + CancellationToken cancellationToken = default); + + /// + /// Gets the status of a certificate update command. + /// + /// The command ID. + /// Cancellation token. + /// The command status. + Task GetCommandStatusAsync( + Guid commandId, + CancellationToken cancellationToken = default); + } + + /// + /// Result of sending a certificate update command. + /// + public sealed record CertificateUpdateResult( + Guid CommandId, + int TargetDeviceCount, + string Message); + + /// + /// Status of a certificate update command. + /// + public sealed record CertificateUpdateCommandStatus( + Guid CommandId, + string? FleetId, + int TargetDeviceCount, + int ProcessedDeviceCount, + DateTimeOffset IssuedAtUtc, + DateTimeOffset? CompletedAtUtc, + string Status); +} diff --git a/SecureBootDashboard.Api/appsettings.Development.json b/SecureBootDashboard.Api/appsettings.Development.json index 0c208ae..07a7c18 100644 --- a/SecureBootDashboard.Api/appsettings.Development.json +++ b/SecureBootDashboard.Api/appsettings.Development.json @@ -4,5 +4,11 @@ "Default": "Information", "Microsoft.AspNetCore": "Warning" } + }, + "CertificateUpdateService": { + "Enabled": false, + "QueueServiceUri": "https://localhost:10001/devstoreaccount1", + "CommandQueueName": "secureboot-update-commands", + "AuthenticationMethod": "DefaultAzureCredential" } } diff --git a/SecureBootDashboard.Api/appsettings.json b/SecureBootDashboard.Api/appsettings.json index 5c63528..b0f7e58 100644 --- a/SecureBootDashboard.Api/appsettings.json +++ b/SecureBootDashboard.Api/appsettings.json @@ -64,5 +64,19 @@ "EmptyQueuePollInterval": "00:00:10", "VisibilityTimeout": "00:05:00", "MaxDequeueCount": 5 + }, + "CertificateUpdateService": { + "Enabled": false, + "QueueServiceUri": "https://secbootcert.queue.core.windows.net", + "CommandQueueName": "secureboot-update-commands", + "AuthenticationMethod": "Certificate", + // NOTE: These values are development/test configuration only. + // In production deployments, use Azure Key Vault or environment variables instead. + // These IDs are not sensitive secrets but should be overridden per environment. + "TenantId": "d6dbad84-5922-4700-a049-c7068c37c884", + "ClientId": "c8034569-4990-4823-9f1d-b46223789c35", + "CertificateThumbprint": "61FC110D5BABD61419B106862B304C2FFF57A262", + "CertificateStoreLocation": "LocalMachine", + "CertificateStoreName": "My" } } diff --git a/SecureBootDashboard.Api/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/SecureBootDashboard.Api/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs deleted file mode 100644 index 2217181..0000000 --- a/SecureBootDashboard.Api/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.AssemblyInfo.cs b/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.AssemblyInfo.cs deleted file mode 100644 index 2a163df..0000000 --- a/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("SecureBootDashboard.Api")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyProductAttribute("SecureBootDashboard.Api")] -[assembly: System.Reflection.AssemblyTitleAttribute("SecureBootDashboard.Api")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.AssemblyInfoInputs.cache b/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.AssemblyInfoInputs.cache deleted file mode 100644 index 38dfa60..0000000 --- a/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -330f15cde29ff0865162a8b698633027975c3326c7d37814fede7aa21de6f746 diff --git a/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GeneratedMSBuildEditorConfig.editorconfig b/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index b4de29c..0000000 --- a/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,31 +0,0 @@ -is_global = true -build_property.TargetFramework = net8.0 -build_property.TargetFramework = net8.0 -build_property.TargetPlatformMinVersion = -build_property.TargetPlatformMinVersion = -build_property.UsingMicrosoftNETSdkWeb = true -build_property.UsingMicrosoftNETSdkWeb = true -build_property.ProjectTypeGuids = -build_property.ProjectTypeGuids = -build_property.InvariantGlobalization = -build_property.InvariantGlobalization = -build_property.PlatformNeutralAssembly = -build_property.PlatformNeutralAssembly = -build_property.EnforceExtendedAnalyzerRules = -build_property.EnforceExtendedAnalyzerRules = -build_property._SupportedPlatformList = Linux,macOS,Windows -build_property._SupportedPlatformList = Linux,macOS,Windows -build_property.TargetFrameworkIdentifier = .NETCoreApp -build_property.TargetFrameworkVersion = v8.0 -build_property.RootNamespace = SecureBootDashboard.Api -build_property.RootNamespace = SecureBootDashboard.Api -build_property.ProjectDir = C:\Users\nefario\source\repos\robgrame\Nimbus.BootCertWatcher\SecureBootDashboard.Api\ -build_property.EnableComHosting = -build_property.EnableGeneratedComInterfaceComImportInterop = -build_property.RazorLangVersion = 8.0 -build_property.SupportLocalizedComponentNames = -build_property.GenerateRazorMetadataSourceChecksumAttributes = -build_property.MSBuildProjectDirectory = C:\Users\nefario\source\repos\robgrame\Nimbus.BootCertWatcher\SecureBootDashboard.Api -build_property._RazorSourceGeneratorDebug = -build_property.EffectiveAnalysisLevelStyle = 8.0 -build_property.EnableCodeStyleSeverity = diff --git a/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GlobalUsings.g.cs b/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GlobalUsings.g.cs deleted file mode 100644 index 5e6145d..0000000 --- a/SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GlobalUsings.g.cs +++ /dev/null @@ -1,17 +0,0 @@ -// -global using Microsoft.AspNetCore.Builder; -global using Microsoft.AspNetCore.Hosting; -global using Microsoft.AspNetCore.Http; -global using Microsoft.AspNetCore.Routing; -global using Microsoft.Extensions.Configuration; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; -global using System; -global using System.Collections.Generic; -global using System.IO; -global using System.Linq; -global using System.Net.Http; -global using System.Net.Http.Json; -global using System.Threading; -global using System.Threading.Tasks; diff --git a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs deleted file mode 100644 index 2217181..0000000 --- a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.AssemblyInfo.cs b/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.AssemblyInfo.cs deleted file mode 100644 index c448a48..0000000 --- a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("SecureBootDashboard.Web.Tests")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyProductAttribute("SecureBootDashboard.Web.Tests")] -[assembly: System.Reflection.AssemblyTitleAttribute("SecureBootDashboard.Web.Tests")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.AssemblyInfoInputs.cache b/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.AssemblyInfoInputs.cache deleted file mode 100644 index 8cac64a..0000000 --- a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -a3c8c3fe34666d672d32eb769544a3bd9af8560e521948600931faaeed007746 diff --git a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.GeneratedMSBuildEditorConfig.editorconfig b/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index 8466a2d..0000000 --- a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,15 +0,0 @@ -is_global = true -build_property.TargetFramework = net8.0 -build_property.TargetPlatformMinVersion = -build_property.UsingMicrosoftNETSdkWeb = -build_property.ProjectTypeGuids = -build_property.InvariantGlobalization = -build_property.PlatformNeutralAssembly = -build_property.EnforceExtendedAnalyzerRules = -build_property._SupportedPlatformList = Linux,macOS,Windows -build_property.RootNamespace = SecureBootDashboard.Web.Tests -build_property.ProjectDir = /home/runner/work/Nimbus.BootCertWatcher/Nimbus.BootCertWatcher/SecureBootDashboard.Web.Tests/ -build_property.EnableComHosting = -build_property.EnableGeneratedComInterfaceComImportInterop = -build_property.EffectiveAnalysisLevelStyle = 8.0 -build_property.EnableCodeStyleSeverity = diff --git a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.GlobalUsings.g.cs b/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.GlobalUsings.g.cs deleted file mode 100644 index 2cd3d38..0000000 --- a/SecureBootDashboard.Web.Tests/obj/Debug/net8.0/SecureBootDashboard.Web.Tests.GlobalUsings.g.cs +++ /dev/null @@ -1,9 +0,0 @@ -// -global using global::System; -global using global::System.Collections.Generic; -global using global::System.IO; -global using global::System.Linq; -global using global::System.Net.Http; -global using global::System.Threading; -global using global::System.Threading.Tasks; -global using global::Xunit; diff --git a/SecureBootDashboard.Web/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/SecureBootDashboard.Web/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs deleted file mode 100644 index 2217181..0000000 --- a/SecureBootDashboard.Web/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.AssemblyInfo.cs b/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.AssemblyInfo.cs deleted file mode 100644 index 0788dc0..0000000 --- a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("SecureBootDashboard.Web")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyProductAttribute("SecureBootDashboard.Web")] -[assembly: System.Reflection.AssemblyTitleAttribute("SecureBootDashboard.Web")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.AssemblyInfoInputs.cache b/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.AssemblyInfoInputs.cache deleted file mode 100644 index 650cf4b..0000000 --- a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -dd3abfe8967763194617910f3f645ffa942bed9a0c5cd7be6ca938e74a2b92bc diff --git a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GeneratedMSBuildEditorConfig.editorconfig b/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index 918f2ba..0000000 --- a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,83 +0,0 @@ -is_global = true -build_property.TargetFramework = net8.0 -build_property.TargetFrameworkIdentifier = .NETCoreApp -build_property.TargetFrameworkVersion = v8.0 -build_property.TargetPlatformMinVersion = -build_property.UsingMicrosoftNETSdkWeb = true -build_property.ProjectTypeGuids = -build_property.InvariantGlobalization = -build_property.PlatformNeutralAssembly = -build_property.EnforceExtendedAnalyzerRules = -build_property._SupportedPlatformList = Linux,macOS,Windows -build_property.RootNamespace = SecureBootDashboard.Web -build_property.RootNamespace = SecureBootDashboard.Web -build_property.ProjectDir = C:\Users\nefario\source\repos\robgrame\Nimbus.BootCertWatcher\SecureBootdashboard.web\ -build_property.EnableComHosting = -build_property.EnableGeneratedComInterfaceComImportInterop = -build_property.RazorLangVersion = 8.0 -build_property.SupportLocalizedComponentNames = -build_property.GenerateRazorMetadataSourceChecksumAttributes = -build_property.MSBuildProjectDirectory = C:\Users\nefario\source\repos\robgrame\Nimbus.BootCertWatcher\SecureBootdashboard.web -build_property._RazorSourceGeneratorDebug = -build_property.EffectiveAnalysisLevelStyle = 8.0 -build_property.EnableCodeStyleSeverity = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/About.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcQWJvdXQuY3NodG1s -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Account/Login.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcQWNjb3VudFxMb2dpbi5jc2h0bWw= -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Account/Logout.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcQWNjb3VudFxMb2dvdXQuY3NodG1s -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Devices/Details.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcRGV2aWNlc1xEZXRhaWxzLmNzaHRtbA== -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Devices/List.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcRGV2aWNlc1xMaXN0LmNzaHRtbA== -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Devices/Reports.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcRGV2aWNlc1xSZXBvcnRzLmNzaHRtbA== -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Error.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcRXJyb3IuY3NodG1s -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Index.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcSW5kZXguY3NodG1s -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Privacy.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcUHJpdmFjeS5jc2h0bWw= -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Reports/Details.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcUmVwb3J0c1xEZXRhaWxzLmNzaHRtbA== -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Shared/_ValidationScriptsPartial.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcU2hhcmVkXF9WYWxpZGF0aW9uU2NyaXB0c1BhcnRpYWwuY3NodG1s -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Welcome.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcV2VsY29tZS5jc2h0bWw= -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/_ViewImports.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcX1ZpZXdJbXBvcnRzLmNzaHRtbA== -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/_ViewStart.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcX1ZpZXdTdGFydC5jc2h0bWw= -build_metadata.AdditionalFiles.CssScope = - -[C:/Users/nefario/source/repos/robgrame/Nimbus.BootCertWatcher/SecureBootdashboard.web/Pages/Shared/_Layout.cshtml] -build_metadata.AdditionalFiles.TargetPath = UGFnZXNcU2hhcmVkXF9MYXlvdXQuY3NodG1s -build_metadata.AdditionalFiles.CssScope = b-g7ac228pnz diff --git a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GlobalUsings.g.cs b/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GlobalUsings.g.cs deleted file mode 100644 index 5e6145d..0000000 --- a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GlobalUsings.g.cs +++ /dev/null @@ -1,17 +0,0 @@ -// -global using Microsoft.AspNetCore.Builder; -global using Microsoft.AspNetCore.Hosting; -global using Microsoft.AspNetCore.Http; -global using Microsoft.AspNetCore.Routing; -global using Microsoft.Extensions.Configuration; -global using Microsoft.Extensions.DependencyInjection; -global using Microsoft.Extensions.Hosting; -global using Microsoft.Extensions.Logging; -global using System; -global using System.Collections.Generic; -global using System.IO; -global using System.Linq; -global using System.Net.Http; -global using System.Net.Http.Json; -global using System.Threading; -global using System.Threading.Tasks; diff --git a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.RazorAssemblyInfo.cache b/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.RazorAssemblyInfo.cache deleted file mode 100644 index ecb9c97..0000000 --- a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.RazorAssemblyInfo.cache +++ /dev/null @@ -1 +0,0 @@ -d5ac7ab69059af111e9d7125adeb7b174ca570725d4b64a544cca7bd11ac7ca0 diff --git a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.RazorAssemblyInfo.cs b/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.RazorAssemblyInfo.cs deleted file mode 100644 index cd3853c..0000000 --- a/SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.RazorAssemblyInfo.cs +++ /dev/null @@ -1,18 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: Microsoft.AspNetCore.Mvc.ApplicationParts.ProvideApplicationPartFactoryAttribute("Microsoft.AspNetCore.Mvc.ApplicationParts.ConsolidatedAssemblyApplicationPartFact" + - "ory, Microsoft.AspNetCore.Mvc.Razor")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/SecureBootWatcher.Client.Tests/obj/Debug/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs b/SecureBootWatcher.Client.Tests/obj/Debug/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs deleted file mode 100644 index 15efebf..0000000 --- a/SecureBootWatcher.Client.Tests/obj/Debug/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] diff --git a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.AssemblyInfo.cs b/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.AssemblyInfo.cs deleted file mode 100644 index d6d66d0..0000000 --- a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("SecureBootWatcher.Client.Tests")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyProductAttribute("SecureBootWatcher.Client.Tests")] -[assembly: System.Reflection.AssemblyTitleAttribute("SecureBootWatcher.Client.Tests")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.AssemblyInfoInputs.cache b/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.AssemblyInfoInputs.cache deleted file mode 100644 index 079b6da..0000000 --- a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -aedb671d9b6bf348ccbdc1e1007a49ab21b4241adadafad5084ec890a8954172 diff --git a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.GeneratedMSBuildEditorConfig.editorconfig b/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index 4d2c7b3..0000000 --- a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -is_global = true -build_property.IsMSTestTestAdapterReferenced = true -build_property.RootNamespace = SecureBootWatcher.Client.Tests -build_property.ProjectDir = C:\Users\nefario\source\repos\robgrame\Nimbus.BootCertWatcher\SecureBootWatcher.Client.Tests\ -build_property.EnableComHosting = -build_property.EnableGeneratedComInterfaceComImportInterop = -build_property.CsWinRTUseWindowsUIXamlProjections = false -build_property.EffectiveAnalysisLevelStyle = -build_property.EnableCodeStyleSeverity = diff --git a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.GlobalUsings.g.cs b/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.GlobalUsings.g.cs deleted file mode 100644 index 9ab263f..0000000 --- a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.GlobalUsings.g.cs +++ /dev/null @@ -1,8 +0,0 @@ -// -global using global::Microsoft.VisualStudio.TestTools.UnitTesting; -global using global::System; -global using global::System.Collections.Generic; -global using global::System.IO; -global using global::System.Linq; -global using global::System.Threading; -global using global::System.Threading.Tasks; diff --git a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.csproj.AssemblyReference.cache b/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.csproj.AssemblyReference.cache deleted file mode 100644 index fcda2a7..0000000 Binary files a/SecureBootWatcher.Client.Tests/obj/Debug/net48/SecureBootWatcher.Client.Tests.csproj.AssemblyReference.cache and /dev/null differ diff --git a/SecureBootWatcher.Client/obj/Debug/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs b/SecureBootWatcher.Client/obj/Debug/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs deleted file mode 100644 index 15efebf..0000000 --- a/SecureBootWatcher.Client/obj/Debug/net48/.NETFramework,Version=v4.8.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")] diff --git a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.AssemblyInfo.cs b/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.AssemblyInfo.cs deleted file mode 100644 index f6d4673..0000000 --- a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("SecureBootWatcher.Client")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyProductAttribute("SecureBootWatcher.Client")] -[assembly: System.Reflection.AssemblyTitleAttribute("SecureBootWatcher.Client")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.AssemblyInfoInputs.cache b/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.AssemblyInfoInputs.cache deleted file mode 100644 index 6428687..0000000 --- a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -ab0516815ddaa26328e0bcec9213f5fc50934fa26df4275c23d1eaf7be9df5ea diff --git a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.GeneratedMSBuildEditorConfig.editorconfig b/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index 3e369c5..0000000 --- a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,8 +0,0 @@ -is_global = true -build_property.RootNamespace = SecureBootWatcher.Client -build_property.ProjectDir = /home/runner/work/Nimbus.BootCertWatcher/Nimbus.BootCertWatcher/SecureBootWatcher.Client/ -build_property.EnableComHosting = -build_property.EnableGeneratedComInterfaceComImportInterop = -build_property.CsWinRTUseWindowsUIXamlProjections = false -build_property.EffectiveAnalysisLevelStyle = -build_property.EnableCodeStyleSeverity = diff --git a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.csproj.AssemblyReference.cache b/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.csproj.AssemblyReference.cache deleted file mode 100644 index 8ae0dc8..0000000 Binary files a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.csproj.AssemblyReference.cache and /dev/null differ diff --git a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.exe.withSupportedRuntime.config b/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.exe.withSupportedRuntime.config deleted file mode 100644 index 8e342a9..0000000 --- a/SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.exe.withSupportedRuntime.config +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs deleted file mode 100644 index 2217181..0000000 --- a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.AssemblyInfo.cs b/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.AssemblyInfo.cs deleted file mode 100644 index 378cfc4..0000000 --- a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("SecureBootWatcher.Shared.Tests")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyProductAttribute("SecureBootWatcher.Shared.Tests")] -[assembly: System.Reflection.AssemblyTitleAttribute("SecureBootWatcher.Shared.Tests")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.AssemblyInfoInputs.cache b/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.AssemblyInfoInputs.cache deleted file mode 100644 index 4034ab0..0000000 --- a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -869a4e2e3f7898d62d8fd35daf6b5a58cf587f8a85b7beca3caf4b6534bd7d0b diff --git a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.GeneratedMSBuildEditorConfig.editorconfig b/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index 0e61e0f..0000000 --- a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,15 +0,0 @@ -is_global = true -build_property.TargetFramework = net8.0 -build_property.TargetPlatformMinVersion = -build_property.UsingMicrosoftNETSdkWeb = -build_property.ProjectTypeGuids = -build_property.InvariantGlobalization = -build_property.PlatformNeutralAssembly = -build_property.EnforceExtendedAnalyzerRules = -build_property._SupportedPlatformList = Linux,macOS,Windows -build_property.RootNamespace = SecureBootWatcher.Shared.Tests -build_property.ProjectDir = /home/runner/work/Nimbus.BootCertWatcher/Nimbus.BootCertWatcher/SecureBootWatcher.Shared.Tests/ -build_property.EnableComHosting = -build_property.EnableGeneratedComInterfaceComImportInterop = -build_property.EffectiveAnalysisLevelStyle = 8.0 -build_property.EnableCodeStyleSeverity = diff --git a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.GlobalUsings.g.cs b/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.GlobalUsings.g.cs deleted file mode 100644 index 2cd3d38..0000000 --- a/SecureBootWatcher.Shared.Tests/obj/Debug/net8.0/SecureBootWatcher.Shared.Tests.GlobalUsings.g.cs +++ /dev/null @@ -1,9 +0,0 @@ -// -global using global::System; -global using global::System.Collections.Generic; -global using global::System.IO; -global using global::System.Linq; -global using global::System.Net.Http; -global using global::System.Threading; -global using global::System.Threading.Tasks; -global using global::Xunit; diff --git a/SecureBootWatcher.Shared/Models/CertificateUpdateCommand.cs b/SecureBootWatcher.Shared/Models/CertificateUpdateCommand.cs new file mode 100644 index 0000000..09fa653 --- /dev/null +++ b/SecureBootWatcher.Shared/Models/CertificateUpdateCommand.cs @@ -0,0 +1,50 @@ +using System; + +namespace SecureBootWatcher.Shared.Models +{ + /// + /// Represents a command to trigger certificate updates on client devices. + /// + public sealed class CertificateUpdateCommand + { + /// + /// Unique identifier for this command. + /// + public Guid CommandId { get; set; } = Guid.NewGuid(); + + /// + /// The fleet ID this command targets. If null, targets all devices. + /// + public string? FleetId { get; set; } + + /// + /// Specific device machine names to target. If empty, targets all devices in fleet. + /// + public string[] TargetDevices { get; set; } = Array.Empty(); + + /// + /// The update flags to apply. If null, uses Windows defaults. + /// + public uint? UpdateFlags { get; set; } + + /// + /// When this command was issued. + /// + public DateTimeOffset IssuedAtUtc { get; set; } = DateTimeOffset.UtcNow; + + /// + /// When this command expires (optional). + /// + public DateTimeOffset? ExpiresAtUtc { get; set; } + + /// + /// User or system that issued this command. + /// + public string? IssuedBy { get; set; } + + /// + /// Additional notes or reason for this update command. + /// + public string? Notes { get; set; } + } +} diff --git a/SecureBootWatcher.Shared/Transport/CertificateUpdateCommandEnvelope.cs b/SecureBootWatcher.Shared/Transport/CertificateUpdateCommandEnvelope.cs new file mode 100644 index 0000000..8d9fde7 --- /dev/null +++ b/SecureBootWatcher.Shared/Transport/CertificateUpdateCommandEnvelope.cs @@ -0,0 +1,19 @@ +using System; +using SecureBootWatcher.Shared.Models; + +namespace SecureBootWatcher.Shared.Transport +{ + /// + /// Represents the payload for certificate update commands in Azure Queue Storage. + /// + public sealed class CertificateUpdateCommandEnvelope + { + public string Version { get; set; } = "1.0"; + + public string MessageType { get; set; } = "CertificateUpdateCommand"; + + public CertificateUpdateCommand Command { get; set; } = new CertificateUpdateCommand(); + + public DateTimeOffset EnqueuedAtUtc { get; set; } = DateTimeOffset.UtcNow; + } +} diff --git a/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs b/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs deleted file mode 100644 index 8bf3a42..0000000 --- a/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/.NETStandard,Version=v2.0.AssemblyAttributes.cs +++ /dev/null @@ -1,4 +0,0 @@ -// -using System; -using System.Reflection; -[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETStandard,Version=v2.0", FrameworkDisplayName = ".NET Standard 2.0")] diff --git a/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.AssemblyInfo.cs b/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.AssemblyInfo.cs deleted file mode 100644 index 448768b..0000000 --- a/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.AssemblyInfo.cs +++ /dev/null @@ -1,20 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -using System; -using System.Reflection; - -[assembly: System.Reflection.AssemblyCompanyAttribute("SecureBootWatcher.Shared")] -[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] -[assembly: System.Reflection.AssemblyProductAttribute("SecureBootWatcher.Shared")] -[assembly: System.Reflection.AssemblyTitleAttribute("SecureBootWatcher.Shared")] - -// Generated by the MSBuild WriteCodeFragment class. - diff --git a/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.AssemblyInfoInputs.cache b/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.AssemblyInfoInputs.cache deleted file mode 100644 index a75bb89..0000000 --- a/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.AssemblyInfoInputs.cache +++ /dev/null @@ -1 +0,0 @@ -99a26cd5f17c6e39f218759d91f2d65edc0865fb9bd1f1981e7c2af01f6f0834 diff --git a/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.GeneratedMSBuildEditorConfig.editorconfig b/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.GeneratedMSBuildEditorConfig.editorconfig deleted file mode 100644 index b962f1c..0000000 --- a/SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.GeneratedMSBuildEditorConfig.editorconfig +++ /dev/null @@ -1,8 +0,0 @@ -is_global = true -build_property.RootNamespace = SecureBootWatcher.Shared -build_property.ProjectDir = C:\Users\nefario\source\repos\robgrame\Nimbus.BootCertWatcher\SecureBootWatcher.Shared\ -build_property.EnableComHosting = -build_property.EnableGeneratedComInterfaceComImportInterop = -build_property.CsWinRTUseWindowsUIXamlProjections = false -build_property.EffectiveAnalysisLevelStyle = -build_property.EnableCodeStyleSeverity = diff --git a/docs/CERTIFICATE_UPDATE_API.md b/docs/CERTIFICATE_UPDATE_API.md new file mode 100644 index 0000000..5f9988c --- /dev/null +++ b/docs/CERTIFICATE_UPDATE_API.md @@ -0,0 +1,457 @@ +# Certificate Update API + +## Overview + +The Certificate Update API allows administrators to remotely trigger Secure Boot certificate updates for groups of devices. This feature enables centralized management of certificate deployment across Windows fleets. + +## Architecture + +The certificate update feature uses a command queue pattern: + +``` +┌─────────────────────────────────────────────┐ +│ Dashboard API (ASP.NET Core 8) │ +│ ┌───────────────────────────────────────┐ │ +│ │ POST /api/CertificateUpdate/trigger │ │ +│ │ • Validates request │ │ +│ │ • Queries target devices │ │ +│ │ • Sends command to Azure Queue │ │ +│ └───────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ + │ + ▼ + ┌───────────────────────────────┐ + │ Azure Queue Storage │ + │ secureboot-update-commands │ + └───────────────────────────────┘ + │ + ▼ (clients poll) +┌─────────────────────────────────────────────┐ +│ Windows Devices (.NET Framework 4.8) │ +│ ┌───────────────────────────────────────┐ │ +│ │ SecureBootWatcher.Client │ │ +│ │ • Polls command queue │ │ +│ │ • Processes update commands │ │ +│ │ • Applies certificate updates │ │ +│ │ • Reports status │ │ +│ └───────────────────────────────────────┘ │ +└─────────────────────────────────────────────┘ +``` + +## API Endpoints + +### Trigger Certificate Update + +Sends a certificate update command for a fleet or specific devices. + +**Endpoint**: `POST /api/CertificateUpdate/trigger` + +**Request Body**: +```json +{ + "fleetId": "fleet-01", + "targetDevices": ["DEVICE-01", "DEVICE-02"], + "updateFlags": 23876, + "issuedBy": "admin@example.com", + "notes": "Deploying UEFI CA 2023 certificates", + "expiresAtUtc": "2025-12-31T23:59:59Z" +} +``` + +**Request Parameters**: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `fleetId` | string | No | The fleet ID to target. If null, targets all devices. | +| `targetDevices` | string[] | No | Specific device machine names. If empty, targets all devices in fleet. | +| `updateFlags` | uint | No | The update flags to apply. If null, uses Windows defaults. See [Update Flags](#update-flags) below. | +| `issuedBy` | string | Yes | User or system that issued this command. | +| `notes` | string | No | Additional notes or reason for this update. | +| `expiresAtUtc` | DateTimeOffset | No | When this command expires (optional). | + +**Response** (200 OK): +```json +{ + "commandId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "targetDeviceCount": 10, + "message": "Update command sent successfully to 10 device(s)" +} +``` + +**Response** (500 Internal Server Error): +```json +{ + "error": "Failed to trigger certificate update" +} +``` + +### Get Command Status + +Retrieves the status of a certificate update command. + +**Endpoint**: `GET /api/CertificateUpdate/status/{commandId}` + +**Response** (200 OK): +```json +{ + "commandId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "fleetId": "fleet-01", + "targetDeviceCount": 10, + "processedDeviceCount": 5, + "issuedAtUtc": "2025-11-07T23:00:00Z", + "completedAtUtc": null, + "status": "InProgress" +} +``` + +**Response** (404 Not Found): +```json +{ + "error": "Command not found" +} +``` + +## Update Flags + +The `updateFlags` parameter controls which certificates are deployed. Common values: + +| Value (Hex) | Value (Decimal) | Description | +|-------------|-----------------|-------------| +| `0x5944` | 23876 | Initial state - All updates pending | +| `0x5904` | 22788 | Windows UEFI CA 2023 applied | +| `0x5104` | 20740 | Microsoft UEFI CA 2023 applied | +| `0x4104` | 16644 | Microsoft Option ROM CA 2023 applied | +| `0x4100` | 16640 | Microsoft KEK 2023 applied | +| `0x4000` | 16384 | Deployment complete | +| `null` | - | Use Windows defaults | + +Individual flags: + +| Flag | Value (Hex) | Value (Decimal) | Description | +|------|-------------|-----------------|-------------| +| `WindowsUefiCA2023` | `0x0040` | 64 | Apply Windows UEFI CA 2023 certificate | +| `MicrosoftUefiCA2023` | `0x0800` | 2048 | Apply Microsoft UEFI CA 2023 | +| `MicrosoftOptionRomCA2023` | `0x1000` | 4096 | Apply Microsoft Option ROM CA 2023 | +| `MicrosoftKEK2023` | `0x0004` | 4 | Apply Microsoft KEK 2023 | +| `WindowsBootManager2023` | `0x0100` | 256 | Apply Windows Boot Manager 2023 | +| `ConditionalMicrosoftCAs` | `0x4000` | 16384 | Apply MS CAs only if MS UEFI CA 2011 exists | + +## Configuration + +### Enable the Service + +In `appsettings.json`: + +```json +{ + "CertificateUpdateService": { + "Enabled": true, + "QueueServiceUri": "https://yourstorageaccount.queue.core.windows.net", + "CommandQueueName": "secureboot-update-commands", + "AuthenticationMethod": "DefaultAzureCredential" + } +} +``` + +### Configuration Options + +| Setting | Description | Default | +|---------|-------------|---------| +| `Enabled` | Enable/disable the certificate update service | `false` | +| `QueueServiceUri` | Azure Storage Queue service URI | - | +| `CommandQueueName` | Queue name for update commands | `secureboot-update-commands` | +| `AuthenticationMethod` | Authentication method (see below) | `DefaultAzureCredential` | + +### Authentication Methods + +The service supports multiple authentication methods: + +#### 1. DefaultAzureCredential (Recommended for Development) +```json +{ + "AuthenticationMethod": "DefaultAzureCredential" +} +``` +Tries multiple credential sources automatically (environment variables, managed identity, Visual Studio, Azure CLI, etc.). + +#### 2. Managed Identity (Recommended for Production) +```json +{ + "AuthenticationMethod": "ManagedIdentity", + "ClientId": "optional-client-id-for-user-assigned-identity" +} +``` + +#### 3. App Registration (Client Secret) +```json +{ + "AuthenticationMethod": "AppRegistration", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "ClientSecret": "your-client-secret" +} +``` + +#### 4. Certificate-based Authentication +```json +{ + "AuthenticationMethod": "Certificate", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "CertificateThumbprint": "YOUR-CERT-THUMBPRINT", + "CertificateStoreLocation": "LocalMachine", + "CertificateStoreName": "My" +} +``` + +Or with certificate file: +```json +{ + "AuthenticationMethod": "Certificate", + "TenantId": "your-tenant-id", + "ClientId": "your-client-id", + "CertificatePath": "C:\\path\\to\\certificate.pfx", + "CertificatePassword": "certificate-password" +} +``` + +## Azure Setup + +### 1. Create Storage Account Queue + +```bash +# Create queue using Azure CLI +az storage queue create \ + --name secureboot-update-commands \ + --account-name yourstorageaccount +``` + +### 2. Assign Permissions + +For Managed Identity or App Registration: + +```bash +# Get the principal ID (managed identity or service principal) +PRINCIPAL_ID="your-principal-id" + +# Assign Storage Queue Data Contributor role +az role assignment create \ + --role "Storage Queue Data Contributor" \ + --assignee $PRINCIPAL_ID \ + --scope "/subscriptions/{subscription-id}/resourceGroups/{resource-group}/providers/Microsoft.Storage/storageAccounts/{storage-account}/queueServices/default/queues/secureboot-update-commands" +``` + +## Usage Examples + +### Example 1: Update All Devices in a Fleet + +```bash +curl -X POST https://your-api.azurewebsites.net/api/CertificateUpdate/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "fleetId": "fleet-production", + "issuedBy": "admin@contoso.com", + "notes": "Deploying UEFI CA 2023 to production fleet" + }' +``` + +### Example 2: Update Specific Devices + +```bash +curl -X POST https://your-api.azurewebsites.net/api/CertificateUpdate/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "targetDevices": ["WORKSTATION-01", "WORKSTATION-02", "WORKSTATION-03"], + "issuedBy": "admin@contoso.com", + "notes": "Test deployment to pilot group" + }' +``` + +### Example 3: Update with Custom Flags + +```bash +curl -X POST https://your-api.azurewebsites.net/api/CertificateUpdate/trigger \ + -H "Content-Type: application/json" \ + -d '{ + "fleetId": "fleet-test", + "updateFlags": 64, + "issuedBy": "admin@contoso.com", + "notes": "Deploy only Windows UEFI CA 2023 (0x0040)" + }' +``` + +### Example 4: Check Command Status + +```bash +curl https://your-api.azurewebsites.net/api/CertificateUpdate/status/3fa85f64-5717-4562-b3fc-2c963f66afa6 +``` + +## Client Implementation (Future) + +The client-side implementation to poll and process update commands is not yet implemented. The client will need to: + +1. Poll the `secureboot-update-commands` queue periodically +2. Receive and deserialize `CertificateUpdateCommandEnvelope` messages +3. Check if the command targets this device (by fleet ID or machine name) +4. Apply the certificate updates using Windows Update or registry manipulation +5. Send a status report back to the API +6. Delete the message from the queue + +**Client polling logic** (pseudocode): +```csharp +while (true) +{ + var messages = await queueClient.ReceiveMessagesAsync(); + + foreach (var message in messages) + { + var envelope = JsonSerializer.Deserialize(message.MessageText); + var command = envelope.Command; + + // Check if this device is targeted + if (ShouldProcessCommand(command)) + { + // Apply the update + await ApplyCertificateUpdateAsync(command); + + // Report status + await ReportUpdateStatusAsync(command.CommandId); + + // Delete message only if processed by this device + await queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt); + } + } + + await Task.Delay(TimeSpan.FromMinutes(5)); +} +``` + +## Security Considerations + +- **Authentication**: Add `[Authorize]` attribute to controller endpoints in production +- **Authorization**: Implement role-based access control (RBAC) to restrict who can trigger updates +- **Audit Logging**: All update commands are logged with issuer information +- **Command Expiration**: Use `expiresAtUtc` to prevent old commands from being processed +- **Queue Access**: Use managed identity or certificates instead of connection strings +- **TLS/HTTPS**: Ensure all API calls use HTTPS +- **Input Validation**: The API validates all input parameters + +## Monitoring + +### Application Insights Queries + +**Track update commands sent**: +```kusto +traces +| where message contains "Certificate update command" +| where message contains "sent to queue" +| project timestamp, commandId = extract("CommandId: ([a-f0-9-]+)", 1, message), deviceCount = extract("for ([0-9]+) devices", 1, message) +``` + +**Track update failures**: +```kusto +exceptions +| where outerMessage contains "CertificateUpdate" +| project timestamp, problemId, outerMessage, innermostMessage +``` + +### Metrics to Monitor + +- Number of update commands issued per day +- Success rate of command queue sends +- Average devices targeted per command +- Command processing latency +- Queue message age + +## Troubleshooting + +### Service is Disabled + +**Symptom**: API returns "Certificate update service is disabled" + +**Solution**: Set `CertificateUpdateService:Enabled` to `true` in configuration + +### Queue Client Creation Failed + +**Symptom**: Error "Failed to create queue client" + +**Solution**: +- Verify `QueueServiceUri` is correct +- Check authentication settings (TenantId, ClientId, etc.) +- Ensure queue exists in storage account +- Verify permissions on the queue + +### No Devices Found + +**Symptom**: Response shows `targetDeviceCount: 0` + +**Solution**: +- Verify fleet ID is correct +- Check device names are exact matches +- Ensure devices have reported to the API at least once + +### Authentication Failed + +**Symptom**: Azure authentication errors in logs + +**Solution**: +- For Managed Identity: Ensure identity is assigned to App Service +- For App Registration: Verify client secret hasn't expired +- For Certificate: Check certificate is installed and accessible +- Verify role assignments on storage queue + +## API Reference + +### Models + +#### CertificateUpdateCommand + +```csharp +public sealed class CertificateUpdateCommand +{ + public Guid CommandId { get; set; } + public string? FleetId { get; set; } + public string[] TargetDevices { get; set; } + public uint? UpdateFlags { get; set; } + public DateTimeOffset IssuedAtUtc { get; set; } + public DateTimeOffset? ExpiresAtUtc { get; set; } + public string? IssuedBy { get; set; } + public string? Notes { get; set; } +} +``` + +#### CertificateUpdateCommandEnvelope + +```csharp +public sealed class CertificateUpdateCommandEnvelope +{ + public string Version { get; set; } = "1.0"; + public string MessageType { get; set; } = "CertificateUpdateCommand"; + public CertificateUpdateCommand Command { get; set; } + public DateTimeOffset EnqueuedAtUtc { get; set; } +} +``` + +## Future Enhancements + +1. **Command Tracking**: Store commands in database with full audit trail +2. **Device Acknowledgment**: Track which devices have processed each command +3. **Retry Logic**: Automatic retry for failed updates +4. **Scheduling**: Schedule updates for specific date/time +5. **Staged Rollout**: Deploy to devices in batches over time +6. **Approval Workflow**: Multi-stage approval before sending commands +7. **PowerShell Integration**: PowerShell cmdlets for triggering updates +8. **Web UI**: Dashboard interface for managing update campaigns + +## Related Documentation + +- [Secure Boot Update Flags](../SecureBootWatcher.Shared/Models/SecureBootUpdateFlags.cs) +- [Azure Queue Entra ID Authentication](AZURE_QUEUE_ENTRA_ID_AUTH.md) +- [Queue Processor Service](QUEUE_PROCESSOR_MONITORING.md) +- [API Architecture](WEB_IMPLEMENTATION_SUMMARY.md) + +## Support + +For issues or questions: +- GitHub Issues: [Report bugs or request features](https://github.com/robgrame/Nimbus.BootCertWatcher/issues) +- Documentation: See [docs/](.) directory