Add Linux client support (.NET 8) for cross-platform Secure Boot monitoring#10
Add Linux client support (.NET 8) for cross-platform Secure Boot monitoring#10Copilot wants to merge 50 commits into
Conversation
…T Framework 4.8 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>
Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
There was a problem hiding this comment.
Pull Request Overview
This PR implements Linux client support (.NET 8) for the SecureBootWatcher solution, enabling cross-platform Secure Boot certificate monitoring. The implementation creates a new .NET 8 console application that uses Linux-specific APIs to achieve feature parity with the existing Windows client.
Key changes:
- New Linux client project with platform-specific EFI variable access, journald event logging, and DMI/SMBIOS hardware detection
- Shared sink implementations (File, Azure Queue, Web API) reused across platforms
- Comprehensive documentation and unit tests
- Bug fix for .NET Framework 4.8 compatibility in existing Windows client
Reviewed Changes
Copilot reviewed 31 out of 33 changed files in this pull request and generated 38 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/LINUX_CLIENT_IMPLEMENTATION_SUMMARY.md | Comprehensive implementation summary documenting architecture, decisions, and deliverables |
| SecureBootWatcher.sln | Added Linux client and test projects to solution |
| SecureBootWatcher.LinuxClient/*.cs | New Linux client implementation with platform-specific services |
| SecureBootWatcher.LinuxClient/README.md | Detailed documentation for Linux client setup, configuration, and troubleshooting |
| SecureBootWatcher.LinuxClient.Tests/*.cs | Unit tests for Linux-specific components |
| README.md | Updated with Linux client information and prerequisites |
| SecureBootWatcher.Client/Services/PowerShellSecureBootCertificateEnumerator.cs | Fixed .NET Framework 4.8 compatibility issue |
Comments suppressed due to low confidence (3)
SecureBootWatcher.LinuxClient/Sinks/AzureQueueReportSink.cs:1
- Comment is in Italian. Should be translated to English: 'Method 3: Certificate-based authentication (MORE SECURE - recommended for production)'.
using System;
SecureBootWatcher.LinuxClient/Sinks/AzureQueueReportSink.cs:1
- Comment is in Italian. Should be translated to English: 'Method 5: DefaultAzureCredential (tries multiple methods automatically)'.
using System;
SecureBootWatcher.LinuxClient/Sinks/AzureQueueReportSink.cs:1
- Comment is in Italian. Should be translated to English: 'If a ClientId is specified, use it for Managed Identity'.
using System;
| /// </summary> | ||
| internal interface ISecureBootCertificateEnumerator | ||
| { | ||
| /// <summary> | ||
| /// Enumerates all Secure Boot certificates from UEFI firmware. | ||
| /// </summary> |
There was a problem hiding this comment.
XML documentation comments have inconsistent indentation. Lines 9 and 14 use 2 spaces instead of the standard 4 spaces used elsewhere. This should be corrected for consistency.
| /// </summary> | |
| internal interface ISecureBootCertificateEnumerator | |
| { | |
| /// <summary> | |
| /// Enumerates all Secure Boot certificates from UEFI firmware. | |
| /// </summary> | |
| /// </summary> | |
| internal interface ISecureBootCertificateEnumerator | |
| { | |
| /// <summary> | |
| /// Enumerates all Secure Boot certificates from UEFI firmware. | |
| /// </summary> |
| { | ||
| private readonly ILogger<AzureQueueReportSink> _logger; | ||
| private readonly IOptionsMonitor<SecureBootWatcherOptions> _options; | ||
| private readonly AsyncRetryPolicy _retryPolicy; | ||
|
|
||
| public AzureQueueReportSink(ILogger<AzureQueueReportSink> logger, IOptionsMonitor<SecureBootWatcherOptions> options) | ||
| { | ||
| _logger = logger; | ||
| _options = options; | ||
| _retryPolicy = Policy | ||
| .Handle<RequestFailedException>() | ||
| .Or<TimeoutException>() | ||
| .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)), (ex, span, attempt, _) => | ||
| { | ||
| _logger.LogWarning(ex, "Retrying Azure Queue send attempt {Attempt} after {Delay}.", attempt, span); | ||
| }); | ||
| } | ||
|
|
||
| public async Task EmitAsync(SecureBootStatusReport report, CancellationToken cancellationToken) | ||
| { | ||
| var sinkOptions = _options.CurrentValue.Sinks.AzureQueue; | ||
| if (string.IsNullOrWhiteSpace(sinkOptions.QueueName)) | ||
| { | ||
| _logger.LogDebug("Azure Queue sink is disabled because QueueName is not configured."); | ||
| return; | ||
| } | ||
|
|
||
| var queueClient = CreateQueueClient(sinkOptions); | ||
| if (queueClient == null) | ||
| { | ||
| _logger.LogWarning("Azure Queue sink skipped because required configuration is missing."); | ||
| return; | ||
| } | ||
|
|
||
| await queueClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); | ||
|
|
||
| var envelope = new SecureBootQueueEnvelope | ||
| { | ||
| Report = report, | ||
| EnqueuedAtUtc = DateTimeOffset.UtcNow | ||
| }; | ||
|
|
||
| var payload = JsonSerializer.Serialize(envelope, new JsonSerializerOptions | ||
| { | ||
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | ||
| WriteIndented = false | ||
| }); | ||
|
|
||
| await _retryPolicy.ExecuteAsync(async token => | ||
| { | ||
| await queueClient.SendMessageAsync( | ||
| BinaryData.FromString(payload), | ||
| visibilityTimeout: sinkOptions.VisibilityTimeout, | ||
| cancellationToken: token).ConfigureAwait(false); | ||
|
|
||
| _logger.LogInformation("Secure Boot report enqueued to {QueueName} using {AuthMethod} authentication.", | ||
| queueClient.Name, sinkOptions.AuthenticationMethod); | ||
| }, cancellationToken).ConfigureAwait(false); | ||
| } | ||
|
|
||
| private QueueClient? CreateQueueClient(AzureQueueSinkOptions options) | ||
| { | ||
| try | ||
| { | ||
| // Metodo 1: Connection String (non raccomandato per produzione) | ||
| if (options.AuthenticationMethod.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase) && | ||
| !string.IsNullOrWhiteSpace(options.ConnectionString)) | ||
| { | ||
| _logger.LogWarning("Using Connection String authentication. This is NOT recommended for production. Use App Registration or Managed Identity instead."); | ||
| return new QueueClient(options.ConnectionString, options.QueueName); | ||
| } | ||
|
|
||
| // Valida che QueueServiceUri sia configurato per autenticazione Entra ID | ||
| if (options.QueueServiceUri == null) | ||
| { | ||
| _logger.LogError("QueueServiceUri is required for Entra ID authentication."); | ||
| return null; | ||
| } | ||
|
|
||
| var queueUri = new Uri(options.QueueServiceUri, options.QueueName); | ||
| TokenCredential credential; | ||
|
|
||
| // Metodo 2: App Registration con Client Secret (raccomandato per service-to-service) | ||
| 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 App Registration authentication."); | ||
| return null; |
There was a problem hiding this comment.
Inconsistent and incorrect indentation throughout the file. The class, fields, and methods use non-standard whitespace (mixing tabs and spaces). Should use consistent 4-space indentation according to C# coding conventions.
| { | |
| private readonly ILogger<AzureQueueReportSink> _logger; | |
| private readonly IOptionsMonitor<SecureBootWatcherOptions> _options; | |
| private readonly AsyncRetryPolicy _retryPolicy; | |
| public AzureQueueReportSink(ILogger<AzureQueueReportSink> logger, IOptionsMonitor<SecureBootWatcherOptions> options) | |
| { | |
| _logger = logger; | |
| _options = options; | |
| _retryPolicy = Policy | |
| .Handle<RequestFailedException>() | |
| .Or<TimeoutException>() | |
| .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)), (ex, span, attempt, _) => | |
| { | |
| _logger.LogWarning(ex, "Retrying Azure Queue send attempt {Attempt} after {Delay}.", attempt, span); | |
| }); | |
| } | |
| public async Task EmitAsync(SecureBootStatusReport report, CancellationToken cancellationToken) | |
| { | |
| var sinkOptions = _options.CurrentValue.Sinks.AzureQueue; | |
| if (string.IsNullOrWhiteSpace(sinkOptions.QueueName)) | |
| { | |
| _logger.LogDebug("Azure Queue sink is disabled because QueueName is not configured."); | |
| return; | |
| } | |
| var queueClient = CreateQueueClient(sinkOptions); | |
| if (queueClient == null) | |
| { | |
| _logger.LogWarning("Azure Queue sink skipped because required configuration is missing."); | |
| return; | |
| } | |
| await queueClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); | |
| var envelope = new SecureBootQueueEnvelope | |
| { | |
| Report = report, | |
| EnqueuedAtUtc = DateTimeOffset.UtcNow | |
| }; | |
| var payload = JsonSerializer.Serialize(envelope, new JsonSerializerOptions | |
| { | |
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | |
| WriteIndented = false | |
| }); | |
| await _retryPolicy.ExecuteAsync(async token => | |
| { | |
| await queueClient.SendMessageAsync( | |
| BinaryData.FromString(payload), | |
| visibilityTimeout: sinkOptions.VisibilityTimeout, | |
| cancellationToken: token).ConfigureAwait(false); | |
| _logger.LogInformation("Secure Boot report enqueued to {QueueName} using {AuthMethod} authentication.", | |
| queueClient.Name, sinkOptions.AuthenticationMethod); | |
| }, cancellationToken).ConfigureAwait(false); | |
| } | |
| private QueueClient? CreateQueueClient(AzureQueueSinkOptions options) | |
| { | |
| try | |
| { | |
| // Metodo 1: Connection String (non raccomandato per produzione) | |
| if (options.AuthenticationMethod.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase) && | |
| !string.IsNullOrWhiteSpace(options.ConnectionString)) | |
| { | |
| _logger.LogWarning("Using Connection String authentication. This is NOT recommended for production. Use App Registration or Managed Identity instead."); | |
| return new QueueClient(options.ConnectionString, options.QueueName); | |
| } | |
| // Valida che QueueServiceUri sia configurato per autenticazione Entra ID | |
| if (options.QueueServiceUri == null) | |
| { | |
| _logger.LogError("QueueServiceUri is required for Entra ID authentication."); | |
| return null; | |
| } | |
| var queueUri = new Uri(options.QueueServiceUri, options.QueueName); | |
| TokenCredential credential; | |
| // Metodo 2: App Registration con Client Secret (raccomandato per service-to-service) | |
| 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 App Registration authentication."); | |
| return null; | |
| { | |
| private readonly ILogger<AzureQueueReportSink> _logger; | |
| private readonly IOptionsMonitor<SecureBootWatcherOptions> _options; | |
| private readonly AsyncRetryPolicy _retryPolicy; | |
| public AzureQueueReportSink(ILogger<AzureQueueReportSink> logger, IOptionsMonitor<SecureBootWatcherOptions> options) | |
| { | |
| _logger = logger; | |
| _options = options; | |
| _retryPolicy = Policy | |
| .Handle<RequestFailedException>() | |
| .Or<TimeoutException>() | |
| .WaitAndRetryAsync(3, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt)), (ex, span, attempt, _) => | |
| { | |
| _logger.LogWarning(ex, "Retrying Azure Queue send attempt {Attempt} after {Delay}.", attempt, span); | |
| }); | |
| } | |
| public async Task EmitAsync(SecureBootStatusReport report, CancellationToken cancellationToken) | |
| { | |
| var sinkOptions = _options.CurrentValue.Sinks.AzureQueue; | |
| if (string.IsNullOrWhiteSpace(sinkOptions.QueueName)) | |
| { | |
| _logger.LogDebug("Azure Queue sink is disabled because QueueName is not configured."); | |
| return; | |
| } | |
| var queueClient = CreateQueueClient(sinkOptions); | |
| if (queueClient == null) | |
| { | |
| _logger.LogWarning("Azure Queue sink skipped because required configuration is missing."); | |
| return; | |
| } | |
| await queueClient.CreateIfNotExistsAsync(cancellationToken: cancellationToken).ConfigureAwait(false); | |
| var envelope = new SecureBootQueueEnvelope | |
| { | |
| Report = report, | |
| EnqueuedAtUtc = DateTimeOffset.UtcNow | |
| }; | |
| var payload = JsonSerializer.Serialize(envelope, new JsonSerializerOptions | |
| { | |
| PropertyNamingPolicy = JsonNamingPolicy.CamelCase, | |
| WriteIndented = false | |
| }); | |
| await _retryPolicy.ExecuteAsync(async token => | |
| { | |
| await queueClient.SendMessageAsync( | |
| BinaryData.FromString(payload), | |
| visibilityTimeout: sinkOptions.VisibilityTimeout, | |
| cancellationToken: token).ConfigureAwait(false); | |
| _logger.LogInformation("Secure Boot report enqueued to {QueueName} using {AuthMethod} authentication.", | |
| queueClient.Name, sinkOptions.AuthenticationMethod); | |
| }, cancellationToken).ConfigureAwait(false); | |
| } | |
| private QueueClient? CreateQueueClient(AzureQueueSinkOptions options) | |
| { | |
| try | |
| { | |
| // Metodo 1: Connection String (non raccomandato per produzione) | |
| if (options.AuthenticationMethod.Equals("ConnectionString", StringComparison.OrdinalIgnoreCase) && | |
| !string.IsNullOrWhiteSpace(options.ConnectionString)) | |
| { | |
| _logger.LogWarning("Using Connection String authentication. This is NOT recommended for production. Use App Registration or Managed Identity instead."); | |
| return new QueueClient(options.ConnectionString, options.QueueName); | |
| } | |
| // Valida che QueueServiceUri sia configurato per autenticazione Entra ID | |
| if (options.QueueServiceUri == null) | |
| { | |
| _logger.LogError("QueueServiceUri is required for Entra ID authentication."); | |
| return null; | |
| } | |
| var queueUri = new Uri(options.QueueServiceUri, options.QueueName); | |
| TokenCredential credential; | |
| // Metodo 2: App Registration con Client Secret (raccomandato per service-to-service) | |
| 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 App Registration authentication."); | |
| return null; |
| { | ||
| try | ||
| { | ||
| // Metodo 1: Connection String (non raccomandato per produzione) |
There was a problem hiding this comment.
Comment is in Italian. Should be translated to English: 'Method 1: Connection String (not recommended for production)'.
| // Metodo 1: Connection String (non raccomandato per produzione) | |
| // Method 1: Connection String (not recommended for production) |
| return new QueueClient(options.ConnectionString, options.QueueName); | ||
| } | ||
|
|
||
| // Valida che QueueServiceUri sia configurato per autenticazione Entra ID |
There was a problem hiding this comment.
Comment is in Italian. Should be translated to English: 'Validate that QueueServiceUri is configured for Entra ID authentication'.
| // Valida che QueueServiceUri sia configurato per autenticazione Entra ID | |
| // Validate that QueueServiceUri is configured for Entra ID authentication |
| catch (Exception ex) | ||
| { | ||
| _logger.LogDebug(ex, "Failed to read file {Path}", path); | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | |
| { | |
| _logger.LogDebug(ex, "Failed to read file {Path}", path); | |
| } | |
| catch (IOException ex) | |
| { | |
| _logger.LogDebug(ex, "Failed to read file {Path}", path); | |
| } | |
| catch (UnauthorizedAccessException ex) | |
| { | |
| _logger.LogDebug(ex, "Failed to read file {Path}", path); | |
| } |
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to create Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | |
| { | |
| _logger.LogError(ex, "Failed to create Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } | |
| catch (Azure.Identity.AuthenticationFailedException ex) | |
| { | |
| _logger.LogError(ex, "Authentication failed while creating Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } | |
| catch (Azure.RequestFailedException ex) | |
| { | |
| _logger.LogError(ex, "Request to Azure Queue failed with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } | |
| catch (ArgumentException ex) | |
| { | |
| _logger.LogError(ex, "Invalid argument while creating Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } | |
| catch (InvalidOperationException ex) | |
| { | |
| _logger.LogError(ex, "Invalid operation while creating Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } |
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to load certificate from file: {Path}", options.CertificatePath); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to load certificate from store. Thumbprint: {Thumbprint}", options.CertificateThumbprint); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | ||
| { | ||
| lastException = ex; | ||
|
|
||
| if (attemptNumber <= maxRetries) | ||
| { | ||
| // Calculate delay for next retry | ||
| var delay = GetCurrentDelay(attemptNumber, retryDelay, useExponentialBackoff); | ||
|
|
||
| _logger.LogWarning( | ||
| "? Attempt {Attempt}/{MaxRetries} failed for {SinkName}: {ErrorMessage}. Retrying in {Delay}...", | ||
| attemptNumber, | ||
| maxRetries + 1, | ||
| sinkName, | ||
| ex.Message, | ||
| delay); | ||
|
|
||
| // Wait before retry (unless it's the last attempt) | ||
| if (attemptNumber <= maxRetries) | ||
| { | ||
| try | ||
| { | ||
| await Task.Delay(delay, cancellationToken).ConfigureAwait(false); | ||
| } | ||
| catch (OperationCanceledException) | ||
| { | ||
| _logger.LogInformation("Retry cancelled for {SinkName}", sinkName); | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| else | ||
| { | ||
| // Log error without full stack trace since we'll try next sink | ||
| _logger.LogWarning( | ||
| "? All {TotalAttempts} attempts failed for {SinkName}: {ErrorMessage}. Moving to next sink.", | ||
| attemptNumber, | ||
| sinkName, | ||
| lastException?.Message ?? "Unknown error"); | ||
| } | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| return new QueueClient(queueUri, credential); | ||
| } | ||
|
|
||
| // Metodo 3: Certificate-based authentication (PI� SICURO - raccomandato per produzione) |
There was a problem hiding this comment.
Comments are written in Italian instead of English. All code comments should be in English for consistency with the rest of the codebase. For example, 'Metodo 1' should be 'Method 1', 'non raccomandato' should be 'not recommended', 'Valida che' should be 'Validate that', 'Opzione A' should be 'Option A', 'Carica certificato' should be 'Load certificate', etc.
| // Metodo 3: Certificate-based authentication (PI� SICURO - raccomandato per produzione) | |
| // Method 3: Certificate-based authentication (MORE SECURE - recommended for production) |
| catch (Exception ex) | ||
| { | ||
| _logger.LogDebug(ex, "Failed to parse certificate from {Database}", databaseName); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
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>
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>
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>
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>
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>
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>
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>
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>
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>
| return null; | ||
| } | ||
|
|
||
| certificate = certificates[0]; |
There was a problem hiding this comment.
Variable certificate is not declared in this scope. The code attempts to assign to certificate without declaring it first. This should be declared earlier in the method or the certificate should be used directly within the using block.
| return null; | ||
| } | ||
| } | ||
| // Option B: Load certificate from Windows Certificate Store |
There was a problem hiding this comment.
Comment references 'Windows Certificate Store' in the Linux client. While the X509Store API is cross-platform in .NET, this comment is misleading since it suggests Windows-specific functionality. Consider updating to 'Option B: Load certificate from system certificate store' or clarify that this uses the system's certificate store on Linux.
| // Option B: Load certificate from Windows Certificate Store | |
| // Option B: Load certificate from system certificate store (cross-platform: Windows, Linux, macOS) |
| _logger.LogWarning( | ||
| "? Attempt {Attempt}/{MaxRetries} failed for {SinkName}: {ErrorMessage}. Retrying in {Delay}...", |
There was a problem hiding this comment.
The log message contains a '?' character that appears to be a placeholder or unintended character. This should likely be removed or replaced with an appropriate emoji/icon if intended.
| _logger.LogWarning( | ||
| "? All {TotalAttempts} attempts failed for {SinkName}: {ErrorMessage}. Moving to next sink.", |
There was a problem hiding this comment.
The log message contains a '?' character that appears to be a placeholder or unintended character. This should likely be removed or replaced with an appropriate emoji/icon if intended.
| var events = await QueryJournalctlAsync(since, cancellationToken).ConfigureAwait(false); | ||
| records.AddRange(events); | ||
| } | ||
| catch (Exception ex) |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | |
| catch (Exception ex) when (ex is not OperationCanceledException && ex is not TaskCanceledException) |
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to create Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | ||
| return null; | ||
| } | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | |
| { | |
| _logger.LogError(ex, "Failed to create Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } | |
| } | |
| catch (AuthenticationFailedException ex) | |
| { | |
| _logger.LogError(ex, "Authentication failed when creating Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } | |
| catch (RequestFailedException ex) | |
| { | |
| _logger.LogError(ex, "Azure request failed when creating Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } | |
| catch (OperationCanceledException ex) | |
| { | |
| _logger.LogError(ex, "Operation was canceled when creating Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } | |
| // Optionally catch other non-critical exceptions | |
| catch (Exception ex) when (!(ex is OutOfMemoryException) && !(ex is StackOverflowException) && !(ex is ThreadAbortException)) | |
| { | |
| _logger.LogError(ex, "Unexpected error when creating Azure Queue client with authentication method: {Method}", options.AuthenticationMethod); | |
| return null; | |
| } | |
| } |
| catch (Exception ex) | ||
| { | ||
| _logger.LogError(ex, "Failed to load certificate from store. Thumbprint: {Thumbprint}", options.CertificateThumbprint); | ||
| return null; | ||
| } |
There was a problem hiding this comment.
Generic catch clause.
| catch (Exception ex) | |
| { | |
| _logger.LogError(ex, "Failed to load certificate from store. Thumbprint: {Thumbprint}", options.CertificateThumbprint); | |
| return null; | |
| } | |
| catch (CryptographicException ex) | |
| { | |
| _logger.LogError(ex, "Cryptographic error loading certificate from store. Thumbprint: {Thumbprint}", options.CertificateThumbprint); | |
| return null; | |
| } | |
| catch (ArgumentException ex) | |
| { | |
| _logger.LogError(ex, "Argument error loading certificate from store. Thumbprint: {Thumbprint}", options.CertificateThumbprint); | |
| return null; | |
| } | |
| catch (InvalidOperationException ex) | |
| { | |
| _logger.LogError(ex, "Invalid operation loading certificate from store. Thumbprint: {Thumbprint}", options.CertificateThumbprint); | |
| return null; | |
| } |
| return; | ||
| } | ||
| catch (Exception ex) | ||
| { |
There was a problem hiding this comment.
Generic catch clause.
| { | |
| { | |
| // Rethrow critical exceptions | |
| if (ex is OutOfMemoryException || | |
| ex is StackOverflowException || | |
| ex is ThreadAbortException || | |
| ex is AccessViolationException) | |
| { | |
| throw; | |
| } |
Implements Linux client as specified in v2.0 roadmap to enable Secure Boot certificate monitoring on UEFI-enabled Linux systems.
New Components
SecureBootWatcher.LinuxClient(.NET 8)LinuxRegistrySnapshotProvider: Reads EFI variable status from/sys/firmware/efi/efivarsLinuxEventLogReader: Queries systemd journald viajournalctlfor boot eventsLinuxSecureBootCertificateEnumerator: Parses X.509 certificates directly from EFI variables (db, dbx, KEK, PK)ReportBuilder: Reads hardware metadata from DMI/SMBIOS (/sys/class/dmi/id/)All existing sinks (File Share, Azure Queue, Web API) and storage components work unchanged on Linux.
SecureBootWatcher.LinuxClient.TestsBasic unit test coverage for Linux-specific providers.
Architecture
Separate project from Windows client due to incompatible frameworks (.NET Framework 4.8 vs .NET 8) and platform APIs (Registry/EventLog vs EFI vars/journald).
System Requirements
/sys/firmware/efi/efivars)Documentation
SecureBootWatcher.LinuxClient/README.mdwith installation, systemd service setup, and troubleshootingdocs/LINUX_CLIENT_IMPLEMENTATION_SUMMARY.mdBug Fix
Fixed .NET Framework 4.8 compilation error in
PowerShellSecureBootCertificateEnumeratorwhereString.Contains(string, StringComparison)overload is unavailable. Changed toIndexOf(string, StringComparison) >= 0.Original prompt
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.