Add API endpoint for remote certificate update commands#18
Conversation
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
robgrame
left a comment
There was a problem hiding this comment.
Dobbiamo fare in modo che negli scenari in cui gli storage queue non sono utilizzati questi funziona. Il Clint oltre a mandare il report deve fare un controllo se ha suoni da fare di servizio API soltanto
There was a problem hiding this comment.
Pull Request Overview
This PR adds a Certificate Update API that enables administrators to remotely trigger Secure Boot certificate updates for Windows device fleets. The implementation uses Azure Queue Storage to send update commands that client devices can poll and execute.
Key Changes:
- Introduced REST API endpoints for triggering certificate updates and checking command status
- Added Azure Queue Storage integration with multiple authentication methods (Managed Identity, App Registration, Certificate, DefaultAzureCredential)
- Created comprehensive API documentation with usage examples and troubleshooting guides
Reviewed Changes
Copilot reviewed 19 out of 20 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/CERTIFICATE_UPDATE_API.md | Complete API documentation including architecture diagrams, endpoint specifications, authentication methods, and usage examples |
| SecureBootDashboard.Api/Controllers/CertificateUpdateController.cs | REST API controller exposing POST /trigger and GET /status endpoints for certificate updates |
| SecureBootDashboard.Api/Services/ICertificateUpdateService.cs | Service interface defining certificate update operations and result models |
| SecureBootDashboard.Api/Services/CertificateUpdateService.cs | Service implementation handling Azure Queue integration, device targeting, and authentication |
| SecureBootDashboard.Api/Configuration/CertificateUpdateServiceOptions.cs | Configuration model for certificate update service settings including authentication options |
| SecureBootDashboard.Api/Program.cs | Service registration and configuration logging for the certificate update feature |
| SecureBootDashboard.Api/appsettings.json | Production configuration settings for the certificate update service |
| SecureBootDashboard.Api/appsettings.Development.json | Development configuration using DefaultAzureCredential and local storage |
| SecureBootWatcher.Shared/Models/CertificateUpdateCommand.cs | Command model containing update parameters, target devices, and metadata |
| SecureBootWatcher.Shared/Transport/CertificateUpdateCommandEnvelope.cs | Queue message envelope wrapping certificate update commands with versioning |
| SecureBootDashboard.Api.Tests/SecureBootDashboard.Api.Tests.csproj | Added Moq package reference for unit testing |
| SecureBootDashboard.Api.Tests/Controllers/CertificateUpdateControllerTests.cs | Unit tests for controller endpoints covering success and error scenarios |
| SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.GeneratedMSBuildEditorConfig.editorconfig | Build configuration update reflecting CI/CD environment path changes |
| SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.AssemblyInfo.cs | Auto-generated assembly info with runtime version removed |
| SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.csproj.AssemblyReference.cache | Binary assembly reference cache updated for CI/CD build |
| SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GlobalUsings.g.cs | Auto-generated global usings updated with global:: prefix |
| SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GeneratedMSBuildEditorConfig.editorconfig | Build configuration updated for Linux CI/CD environment |
| SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GlobalUsings.g.cs | Auto-generated global usings updated with global:: prefix |
| SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GeneratedMSBuildEditorConfig.editorconfig | Build configuration updated for Linux CI/CD environment |
| SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.AssemblyInfo.cs | Auto-generated assembly info with runtime version removed |
Comments suppressed due to low confidence (2)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:219
- Disposable 'X509Certificate2' is created but not disposed.
? new X509Certificate2(options.CertificatePath)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:220
- Disposable 'X509Certificate2' is created but not disposed.
: new X509Certificate2(options.CertificatePath, password);
| if (_commandQueueClient == null) | ||
| { | ||
| _commandQueueClient = CreateQueueClient(options); | ||
| if (_commandQueueClient == null) | ||
| { | ||
| throw new InvalidOperationException("Failed to create queue client"); | ||
| } | ||
| } |
There was a problem hiding this comment.
The _commandQueueClient field is accessed without thread-safety considerations. If SendUpdateCommandAsync is called concurrently from multiple threads, there's a race condition where multiple threads could all see _commandQueueClient == null and create multiple queue clients. Consider using lazy initialization with proper synchronization or making the service scoped to avoid this issue.
| /// User or system that issued this command. | ||
| /// </summary> | ||
| [Required] | ||
| public string? IssuedBy { get; init; } |
There was a problem hiding this comment.
The IssuedBy property is marked as [Required] but is nullable (string?). This creates ambiguity - either remove the nullable annotation to make it truly required, or remove the [Required] attribute if null values are acceptable. The inconsistency could lead to confusion about the validation behavior.
| public string? IssuedBy { get; init; } | |
| public string IssuedBy { get; init; } |
| return StatusCode(500, new { Error = "Failed to trigger certificate update" }); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Get the status of a certificate update command. | ||
| /// </summary> | ||
| /// <param name="commandId">The command ID.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Command status.</returns> | ||
| [HttpGet("status/{commandId:guid}")] | ||
| public async Task<ActionResult<CertificateUpdateCommandStatus>> GetCommandStatusAsync( | ||
| Guid commandId, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| try | ||
| { | ||
| var status = await _updateService.GetCommandStatusAsync(commandId, cancellationToken); | ||
|
|
||
| if (status == null) | ||
| { | ||
| return NotFound(new { Error = "Command not found" }); |
There was a problem hiding this comment.
The error response casing is inconsistent. On line 84, the error response uses "error" (lowercase), while on line 87 it uses "Error" (uppercase). Similarly, line 110 uses lowercase "error". This inconsistency in the API contract should be standardized to either use PascalCase or camelCase consistently.
| [HttpPost("trigger")] | ||
| public async Task<ActionResult<CertificateUpdateResult>> 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<string>(), | ||
| 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"); | ||
| return StatusCode(500, new { Error = "Failed to trigger certificate update" }); | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Get the status of a certificate update command. | ||
| /// </summary> | ||
| /// <param name="commandId">The command ID.</param> | ||
| /// <param name="cancellationToken">Cancellation token.</param> | ||
| /// <returns>Command status.</returns> | ||
| [HttpGet("status/{commandId:guid}")] | ||
| public async Task<ActionResult<CertificateUpdateCommandStatus>> GetCommandStatusAsync( | ||
| Guid commandId, | ||
| CancellationToken cancellationToken) | ||
| { | ||
| try | ||
| { | ||
| var status = await _updateService.GetCommandStatusAsync(commandId, cancellationToken); | ||
|
|
||
| if (status == null) | ||
| { | ||
| return NotFound(new { Error = "Command not found" }); | ||
| } | ||
|
|
||
| return Ok(status); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to get command status for {CommandId}", commandId); | ||
| return StatusCode(500, new { Error = "Failed to get command status" }); | ||
| } | ||
| } |
There was a problem hiding this comment.
Missing authorization on critical API endpoints. The controller endpoints for triggering certificate updates and checking command status lack authentication and authorization attributes. As noted in the documentation's security section (line 331), the [Authorize] attribute should be added to these endpoints in production to prevent unauthorized access to this sensitive functionality.
| public sealed record CertificateUpdateRequest | ||
| { | ||
| /// <summary> | ||
| /// The fleet ID to target. If null, targets all devices. | ||
| /// </summary> | ||
| public string? FleetId { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// Specific device machine names to target. If empty, targets all devices in fleet. | ||
| /// </summary> | ||
| public string[]? TargetDevices { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// The update flags to apply. If null, uses Windows defaults. | ||
| /// </summary> | ||
| public uint? UpdateFlags { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// When this command expires (optional). | ||
| /// </summary> | ||
| public DateTimeOffset? ExpiresAtUtc { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// User or system that issued this command. | ||
| /// </summary> | ||
| [Required] | ||
| public string? IssuedBy { get; init; } | ||
|
|
||
| /// <summary> | ||
| /// Additional notes or reason for this update command. | ||
| /// </summary> | ||
| public string? Notes { get; init; } | ||
| } |
There was a problem hiding this comment.
Input validation is missing for user-provided strings. The Notes, FleetId, and TargetDevices fields lack validation for maximum length, malicious content, or SQL injection patterns. Consider adding validation attributes like [MaxLength] and sanitizing inputs before storing them in the database or sending them to the queue.
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to trigger certificate update"); | ||
| return StatusCode(500, new { Error = "Failed to trigger certificate update" }); | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to get command status for {CommandId}", commandId); | ||
| return StatusCode(500, new { Error = "Failed to get command status" }); | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to create command queue client"); | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | |
| { | |
| _logger.LogError(ex, "Failed to create command queue client"); | |
| return null; | |
| } | |
| } | |
| catch (ArgumentException ex) | |
| { | |
| _logger.LogError(ex, "Invalid argument when creating command queue client"); | |
| return null; | |
| } | |
| catch (UriFormatException ex) | |
| { | |
| _logger.LogError(ex, "Invalid URI format for command queue client"); | |
| return null; | |
| } | |
| catch (Azure.Identity.AuthenticationFailedException ex) | |
| { | |
| _logger.LogError(ex, "Authentication failed when creating command queue client"); | |
| return null; | |
| } | |
| } | |
| } |
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated 7 comments.
Comments suppressed due to low confidence (2)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:219
- Disposable 'X509Certificate2' is created but not disposed.
? new X509Certificate2(options.CertificatePath)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:220
- Disposable 'X509Certificate2' is created but not disposed.
: new X509Certificate2(options.CertificatePath, password);
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (1)
SecureBootDashboard.Api/Controllers/CertificateUpdateController.cs:46
- This assignment to sanitizedFleetId is useless, since its value is never read.
var sanitizedFleetId = request.FleetId?.Replace("\r", "").Replace("\n", "") ?? "ALL";
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated 5 comments.
Comments suppressed due to low confidence (3)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:219
- Disposable 'X509Certificate2' is created but not disposed.
? new X509Certificate2(options.CertificatePath)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:220
- Disposable 'X509Certificate2' is created but not disposed.
: new X509Certificate2(options.CertificatePath, password);
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:248
- Disposable 'X509Certificate2' is created but not disposed.
certificate = new X509Certificate2(certificates[0].RawData);
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:248
- Disposable 'X509Certificate2' is created but not disposed.
certificate = new X509Certificate2(certificates[0].RawData);
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
Copilot reviewed 19 out of 20 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (2)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:219
- Disposable 'X509Certificate2' is created but not disposed.
? new X509Certificate2(options.CertificatePath)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:220
- Disposable 'X509Certificate2' is created but not disposed.
: new X509Certificate2(options.CertificatePath, password);
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
| is_global = true | ||
| build_property.RootNamespace = SecureBootWatcher.Shared | ||
| build_property.ProjectDir = C:\Users\nefario\source\repos\robgrame\Nimbus.BootCertWatcher\SecureBootWatcher.Shared\ | ||
| build_property.ProjectDir = /home/runner/work/Nimbus.BootCertWatcher/Nimbus.BootCertWatcher/SecureBootWatcher.Shared/ |
There was a problem hiding this comment.
The auto-generated files in obj/ directories should not be included in the pull request. These files are build artifacts that should be excluded via .gitignore. Consider removing these files from version control:
- SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.GeneratedMSBuildEditorConfig.editorconfig
- SecureBootWatcher.Shared/obj/Debug/netstandard2.0/SecureBootWatcher.Shared.AssemblyInfo.cs
- SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GlobalUsings.g.cs
- SecureBootDashboard.Web/obj/Debug/net8.0/SecureBootDashboard.Web.GeneratedMSBuildEditorConfig.editorconfig
- SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GlobalUsings.g.cs
- SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.GeneratedMSBuildEditorConfig.editorconfig
- SecureBootDashboard.Api/obj/Debug/net8.0/SecureBootDashboard.Api.AssemblyInfo.cs
- SecureBootWatcher.Client/obj/Debug/net48/SecureBootWatcher.Client.csproj.AssemblyReference.cache
The .gitignore already excludes obj/ but these files appear to have been committed.
There was a problem hiding this comment.
Removed all build artifact files from obj/Debug directories from version control. These files are now properly excluded via .gitignore. Changes in commit 81df76a.
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to create command queue client"); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
There was a problem hiding this comment.
Replaced generic catch clause with specific exception handling for Azure authentication, request failures, and URI format errors. Changes in commit 81df76a.
|
@robgrame Unfortunately I hit an unexpected error while processing your comment. I've automatically reported this to GitHub. You can ask me to try again later by mentioning me in a new comment. If you want to contact GitHub about this error, please mention the following identifier so they can better serve you: Sorry for the inconvenience! |
…r.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
@copilot apply changes based on the comments in this thread |
…ion, remove build artifacts Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Completed final cleanup of all build artifacts. Removed the remaining 16 obj/Debug files from test projects that were missed in the initial cleanup. All review feedback has been fully addressed. Changes in commit c04dcde. |
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 53 out of 55 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:219
- Disposable 'X509Certificate2' is created but not disposed.
? new X509Certificate2(options.CertificatePath)
SecureBootDashboard.Api/Services/CertificateUpdateService.cs:220
- Disposable 'X509Certificate2' is created but not disposed.
: new X509Certificate2(options.CertificatePath, password);
| certificate = string.IsNullOrWhiteSpace(password) | ||
| ? new X509Certificate2(options.CertificatePath) | ||
| : new X509Certificate2(options.CertificatePath, password); |
There was a problem hiding this comment.
The X509Certificate2 loaded from file is not disposed, causing a resource leak. X509Certificate2 implements IDisposable and should be properly disposed.
However, since this certificate is passed to ClientCertificateCredential and used beyond this method's scope, consider documenting that the credential takes ownership of the certificate, or refactor to ensure proper lifecycle management.
| using var store = new X509Store(storeName, storeLocation); | ||
| store.Open(OpenFlags.ReadOnly); | ||
|
|
||
| var certificates = store.Certificates.Find( |
There was a problem hiding this comment.
The certificates collection returned by store.Certificates.Find() needs to be disposed to prevent a resource leak. X509Certificate2Collection implements IDisposable.
Wrap the collection in a using statement:
using var certificates = store.Certificates.Find(
X509FindType.FindByThumbprint,
options.CertificateThumbprint.Replace(" ", "").Replace(":", ""),
validOnly: false);| var certificates = store.Certificates.Find( | |
| using var certificates = store.Certificates.Find( |
Certificate Update API Implementation
Implementation of API method to command certificate updates for groups of devices:
Recent Changes (Final Cleanup):
All Code Review Items Addressed:
Implementation Details:
Original prompt
💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.