Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
67e10fb
Initial plan
Copilot Nov 7, 2025
bb326f4
Initial plan for certificate update API endpoint
Copilot Nov 7, 2025
c28aa13
Add certificate update API endpoint with tests
Copilot Nov 8, 2025
a5b931a
Add comprehensive documentation for certificate update API
Copilot Nov 8, 2025
5a368dc
Fix log forging security vulnerabilities
Copilot Nov 8, 2025
ecc7047
Update CertificateUpdateController.cs
robgrame Nov 8, 2025
c816fad
Update CERTIFICATE_UPDATE_API.md
robgrame Nov 8, 2025
9e9d315
Update CertificateUpdateController.cs
robgrame Nov 8, 2025
a1fa9d8
Update CertificateUpdateController.cs
robgrame Nov 8, 2025
8d29623
Update CertificateUpdateService.cs
robgrame Nov 8, 2025
31430ae
Update Program.cs
robgrame Nov 8, 2025
804ee9e
Update CertificateUpdateController.cs
robgrame Nov 8, 2025
21020e2
Update CertificateUpdateController.cs
robgrame Nov 8, 2025
aac2168
Update Program.cs
robgrame Nov 8, 2025
f0fd1e9
Update CertificateUpdateController.cs
robgrame Nov 8, 2025
386f0d7
Update CertificateUpdateService.cs
robgrame Nov 8, 2025
1cefd18
Update CertificateUpdateService.cs
robgrame Nov 8, 2025
e6f1461
Update Program.cs
robgrame Nov 8, 2025
7a38509
Update SecureBootDashboard.Api/Controllers/CertificateUpdateControlle…
robgrame Nov 8, 2025
81df76a
Address code review feedback: improve exception handling, add validat…
Copilot Nov 8, 2025
c04dcde
Complete removal of all build artifact files from version control
Copilot Nov 8, 2025
7487034
Update CertificateUpdateController.cs
robgrame Nov 9, 2025
59fb7d5
Update CertificateUpdateService.cs
robgrame Nov 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<ICertificateUpdateService> _mockUpdateService;
private readonly Mock<ILogger<CertificateUpdateController>> _mockLogger;
private readonly CertificateUpdateController _controller;

public CertificateUpdateControllerTests()
{
_mockUpdateService = new Mock<ICertificateUpdateService>();
_mockLogger = new Mock<ILogger<CertificateUpdateController>>();
_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<CertificateUpdateCommand>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(expectedResult);

// Act
var result = await _controller.TriggerUpdateAsync(request, CancellationToken.None);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnValue = Assert.IsType<CertificateUpdateResult>(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<CertificateUpdateCommand>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception("Test exception"));

// Act
var result = await _controller.TriggerUpdateAsync(request, CancellationToken.None);

// Assert
var statusCodeResult = Assert.IsType<ObjectResult>(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<CancellationToken>()))
.ReturnsAsync(expectedStatus);

// Act
var result = await _controller.GetCommandStatusAsync(commandId, CancellationToken.None);

// Assert
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var returnValue = Assert.IsType<CertificateUpdateCommandStatus>(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<CancellationToken>()))
.ReturnsAsync((CertificateUpdateCommandStatus?)null);

// Act
var result = await _controller.GetCommandStatusAsync(commandId, CancellationToken.None);

// Assert
Assert.IsType<NotFoundObjectResult>(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<CertificateUpdateCommand>(), It.IsAny<CancellationToken>()))
.Callback<CertificateUpdateCommand, CancellationToken>((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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using System;

namespace SecureBootDashboard.Api.Configuration
{
/// <summary>
/// Configuration options for the Certificate Update Service.
/// </summary>
public sealed class CertificateUpdateServiceOptions
{
/// <summary>
/// Whether the certificate update service is enabled.
/// </summary>
public bool Enabled { get; set; } = false;

/// <summary>
/// Storage account queue service URI (e.g., https://mystorageaccount.queue.core.windows.net).
/// </summary>
public Uri? QueueServiceUri { get; set; }

/// <summary>
/// Queue name for certificate update commands.
/// </summary>
public string CommandQueueName { get; set; } = "secureboot-update-commands";

/// <summary>
/// Authentication method: "ManagedIdentity", "AppRegistration", "Certificate", "DefaultAzureCredential", or "ConnectionString".
/// </summary>
public string AuthenticationMethod { get; set; } = "DefaultAzureCredential";

/// <summary>
/// Connection string (only used if AuthenticationMethod is "ConnectionString").
/// </summary>
public string? ConnectionString { get; set; }

/// <summary>
/// Application (Client) ID from Azure App Registration.
/// </summary>
public string? ClientId { get; set; }

/// <summary>
/// Directory (Tenant) ID.
/// </summary>
public string? TenantId { get; set; }

/// <summary>
/// Client Secret from App Registration.
/// </summary>
public string? ClientSecret { get; set; }

/// <summary>
/// Path to certificate file (.pfx or .pem).
/// </summary>
public string? CertificatePath { get; set; }

/// <summary>
/// Password for the certificate file.
/// </summary>
public string? CertificatePassword { get; set; }

/// <summary>
/// Certificate thumbprint for certificate store.
/// </summary>
public string? CertificateThumbprint { get; set; }

/// <summary>
/// Certificate store location.
/// </summary>
public string CertificateStoreLocation { get; set; } = "CurrentUser";

/// <summary>
/// Certificate store name.
/// </summary>
public string CertificateStoreName { get; set; } = "My";
}
Comment thread
robgrame marked this conversation as resolved.
}
Loading
Loading