Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ Interactive Chart.js visualizations showing compliance trends and deployment sta
- **Microsoft Detection**: Identifies Microsoft-issued certificates vs. third-party
- **Certificate Details**: Complete X.509 properties including algorithms, key sizes, validity periods

### 🤖 Automated Remediation
- **Workflow Engine**: Define automated responses to device conditions and alerts
- **Flexible Triggers**: Based on deployment state, certificate expiration, fleet, manufacturer, and more
- **Multiple Actions**: Log entries, device tag updates, notifications, webhooks
- **Execution History**: Track all workflow executions with detailed results
- **Priority-based**: Control execution order with configurable priorities

### 📡 Flexible Architecture
- **Multiple Sinks**: File share, Azure Queue Storage, or direct HTTP API ingestion
- **Hybrid Deployment**: Supports cloud (Azure App Service) and on-premises hosting
Expand Down Expand Up @@ -294,6 +301,7 @@ Comprehensive documentation is available in the [`docs/`](docs/) directory:
- **[Device List Separation](docs/DEVICE_LIST_SEPARATION.md)** - UI reorganization details
- **[Certificate Enumeration](docs/CERTIFICATE_ENUMERATION.md)** - UEFI certificate tracking
- **[Logo & Banner Implementation](docs/LOGO_BANNER_IMPLEMENTATION.md)** - Branding assets
- **[Automated Remediation Workflows](docs/AUTOMATED_REMEDIATION_WORKFLOWS.md)** - Workflow automation guide

### Operations & Troubleshooting
- **[Logging Guide](docs/LOGGING_GUIDE.md)** - Serilog configuration and best practices
Expand Down Expand Up @@ -487,7 +495,7 @@ For questions, issues, or support:
### v1.2 (Q2 2025)
- [ ] Multi-tenant support with RBAC
- [ ] Certificate compliance policies
- [ ] Automated remediation workflows
- [x] Automated remediation workflows
- [ ] Enhanced analytics (30/60/90 day trends)

### v2.0 (Q3 2025)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="8.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
Expand Down
233 changes: 233 additions & 0 deletions SecureBootDashboard.Api.Tests/Services/WorkflowEngineTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SecureBootDashboard.Api.Data;
using SecureBootDashboard.Api.Services;
using SecureBootWatcher.Shared.Models;
using Xunit;

namespace SecureBootDashboard.Api.Tests.Services
{
public sealed class WorkflowEngineTests : IDisposable
{
private readonly SecureBootDbContext _dbContext;
private readonly WorkflowEngine _workflowEngine;
private readonly IServiceProvider _serviceProvider;

public WorkflowEngineTests()
{
var services = new ServiceCollection();

services.AddDbContext<SecureBootDbContext>(options =>
options.UseInMemoryDatabase($"TestDb_{Guid.NewGuid()}"));

services.AddLogging(builder => builder.AddConsole());
services.AddScoped<WorkflowEngine>();

_serviceProvider = services.BuildServiceProvider();
_dbContext = _serviceProvider.GetRequiredService<SecureBootDbContext>();
_workflowEngine = _serviceProvider.GetRequiredService<WorkflowEngine>();
}

[Fact]
public async Task EvaluateAndExecuteAsync_NoWorkflows_ReturnsEmptyList()
{
// Arrange
var deviceId = Guid.NewGuid();
var reportId = Guid.NewGuid();

var device = new DeviceEntity
{
Id = deviceId,
MachineName = "TEST-DEVICE",
CreatedAtUtc = DateTimeOffset.UtcNow,
LastSeenUtc = DateTimeOffset.UtcNow
};

var report = new SecureBootReportEntity
{
Id = reportId,
DeviceId = deviceId,
Device = device,
RegistryStateJson = "{}",
CreatedAtUtc = DateTimeOffset.UtcNow
};

_dbContext.Devices.Add(device);
_dbContext.Reports.Add(report);
await _dbContext.SaveChangesAsync();

// Act
var executions = await _workflowEngine.EvaluateAndExecuteAsync(deviceId, reportId);

// Assert
Assert.Empty(executions);
}

[Fact]
public async Task EvaluateAndExecuteAsync_WorkflowWithLogAction_ExecutesSuccessfully()
{
// Arrange
var deviceId = Guid.NewGuid();
var reportId = Guid.NewGuid();

var device = new DeviceEntity
{
Id = deviceId,
MachineName = "TEST-DEVICE",
FleetId = "test-fleet",
CreatedAtUtc = DateTimeOffset.UtcNow,
LastSeenUtc = DateTimeOffset.UtcNow
};

var report = new SecureBootReportEntity
{
Id = reportId,
DeviceId = deviceId,
Device = device,
RegistryStateJson = "{}",
DeploymentState = "Error",
AlertsJson = "[\"Test alert\"]",
CreatedAtUtc = DateTimeOffset.UtcNow
};

var workflow = new RemediationWorkflowEntity
{
Id = Guid.NewGuid(),
Name = "Test Workflow",
IsEnabled = true,
Priority = 100,
TriggerJson = "{\"DeploymentState\":\"Error\",\"FleetIdMatches\":\"test-fleet\"}",
ActionsJson = "[{\"ActionType\":3,\"ConfigurationJson\":\"{\\\"message\\\":\\\"Test log\\\"}\",\"Order\":1}]",
CreatedAtUtc = DateTimeOffset.UtcNow,
UpdatedAtUtc = DateTimeOffset.UtcNow
};

_dbContext.Devices.Add(device);
_dbContext.Reports.Add(report);
_dbContext.RemediationWorkflows.Add(workflow);
await _dbContext.SaveChangesAsync();

// Act
var executions = await _workflowEngine.EvaluateAndExecuteAsync(deviceId, reportId);

// Assert
Assert.Single(executions);
var execution = executions.First();
Assert.Equal(WorkflowExecutionStatus.Completed, execution.Status);
Assert.Equal(workflow.Id, execution.WorkflowId);
Assert.Equal(deviceId, execution.DeviceId);
Assert.Equal(reportId, execution.ReportId);
}

[Fact]
public async Task EvaluateAndExecuteAsync_WorkflowWithNonMatchingTrigger_DoesNotExecute()
{
// Arrange
var deviceId = Guid.NewGuid();
var reportId = Guid.NewGuid();

var device = new DeviceEntity
{
Id = deviceId,
MachineName = "TEST-DEVICE",
FleetId = "other-fleet",
CreatedAtUtc = DateTimeOffset.UtcNow,
LastSeenUtc = DateTimeOffset.UtcNow
};

var report = new SecureBootReportEntity
{
Id = reportId,
DeviceId = deviceId,
Device = device,
RegistryStateJson = "{}",
DeploymentState = "Updated",
CreatedAtUtc = DateTimeOffset.UtcNow
};

var workflow = new RemediationWorkflowEntity
{
Id = Guid.NewGuid(),
Name = "Test Workflow",
IsEnabled = true,
Priority = 100,
TriggerJson = "{\"DeploymentState\":\"Error\",\"FleetIdMatches\":\"test-fleet\"}",
ActionsJson = "[{\"ActionType\":3,\"ConfigurationJson\":\"{\\\"message\\\":\\\"Test log\\\"}\",\"Order\":1}]",
CreatedAtUtc = DateTimeOffset.UtcNow,
UpdatedAtUtc = DateTimeOffset.UtcNow
};

_dbContext.Devices.Add(device);
_dbContext.Reports.Add(report);
_dbContext.RemediationWorkflows.Add(workflow);
await _dbContext.SaveChangesAsync();

// Act
var executions = await _workflowEngine.EvaluateAndExecuteAsync(deviceId, reportId);

// Assert
Assert.Empty(executions);
}

[Fact]
public async Task EvaluateAndExecuteAsync_DisabledWorkflow_DoesNotExecute()
{
// Arrange
var deviceId = Guid.NewGuid();
var reportId = Guid.NewGuid();

var device = new DeviceEntity
{
Id = deviceId,
MachineName = "TEST-DEVICE",
CreatedAtUtc = DateTimeOffset.UtcNow,
LastSeenUtc = DateTimeOffset.UtcNow
};

var report = new SecureBootReportEntity
{
Id = reportId,
DeviceId = deviceId,
Device = device,
RegistryStateJson = "{}",
DeploymentState = "Error",
CreatedAtUtc = DateTimeOffset.UtcNow
};

var workflow = new RemediationWorkflowEntity
{
Id = Guid.NewGuid(),
Name = "Test Workflow",
IsEnabled = false, // Disabled
Priority = 100,
TriggerJson = "{\"DeploymentState\":\"Error\"}",
ActionsJson = "[{\"ActionType\":3,\"ConfigurationJson\":\"{\\\"message\\\":\\\"Test log\\\"}\",\"Order\":1}]",
CreatedAtUtc = DateTimeOffset.UtcNow,
UpdatedAtUtc = DateTimeOffset.UtcNow
};

_dbContext.Devices.Add(device);
_dbContext.Reports.Add(report);
_dbContext.RemediationWorkflows.Add(workflow);
await _dbContext.SaveChangesAsync();

// Act
var executions = await _workflowEngine.EvaluateAndExecuteAsync(deviceId, reportId);

// Assert
Assert.Empty(executions);
}

public void Dispose()
{
_dbContext?.Dispose();
(_serviceProvider as IDisposable)?.Dispose();
}
}
}
Loading
Loading