Skip to content

Implement ML-based anomaly detection for device behavior monitoring#14

Open
robgrame with Copilot wants to merge 19 commits into
mainfrom
copilot/implement-anomaly-detection
Open

Implement ML-based anomaly detection for device behavior monitoring#14
robgrame with Copilot wants to merge 19 commits into
mainfrom
copilot/implement-anomaly-detection

Conversation

Copilot AI commented Nov 7, 2025

Copy link
Copy Markdown
Contributor

Adds automated anomaly detection to identify unusual device behavior patterns across the fleet. Uses statistical analysis (2.5σ threshold) to detect reporting anomalies, deployment failures, and inactive devices.

Changes

Backend

  • Anomaly detection service with three algorithms:
    • Reporting frequency: flags devices deviating >2.5σ from fleet mean
    • Deployment state: detects devices stuck in Pending (3+ reports) or Error (2+ reports)
    • Inactivity: identifies devices silent >14 days with severity scaling
  • Background worker runs hourly scans with 5min startup delay
  • REST API for listing, viewing, scanning, and resolving anomalies
  • EF Core migration adds Anomalies table with indexed foreign key to Devices

Frontend

  • Anomalies page (/Anomalies/List) with severity-based filtering and one-click resolution
  • Dashboard alert banner surfaces active anomalies with count
  • Navigation integration adds Anomalies menu item

Security

  • Input validation on ResolvedBy prevents empty audit trails
  • Log sanitization removes newlines to prevent log forging
  • All detection thresholds extracted as named constants for maintainability

API Example

// GET /api/Anomalies returns:
[
  {
    "id": "guid",
    "deviceId": "guid",
    "deviceName": "LAPTOP-01",
    "type": "ReportingFrequency",
    "description": "Excessive reporting detected. Device reported 42 times in 7 days (expected: ~12.3)",
    "severity": 0.85,
    "detectedAtUtc": "2025-11-07T10:30:00Z",
    "status": "Active"
  }
]

// POST /api/Anomalies/{id}/resolve
{ "resolvedBy": "admin@example.com" }

Migration Required

dotnet ef database update --project SecureBootDashboard.Api

No configuration changes needed—feature enabled by default with sensible thresholds.

Original prompt

This section details on the original issue you should resolve

<issue_title>Machine learning anomaly detection</issue_title>
<issue_description>Implement Machine learning anomaly detection as described in roadmap</issue_description>

Comments on the Issue (you are @copilot in this section)


✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI and others added 6 commits November 7, 2025 06:10
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>
…s to constants

Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
…prevent log forging

Co-authored-by: robgrame <12012136+robgrame@users.noreply.github.com>
Copilot AI changed the title [WIP] Implement machine learning anomaly detection Implement ML-based anomaly detection for device behavior monitoring Nov 7, 2025
Copilot AI requested a review from robgrame November 7, 2025 06:41
@robgrame robgrame marked this pull request as ready for review November 8, 2025 07:28
@robgrame robgrame requested a review from Copilot November 8, 2025 07:28

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces an anomaly detection system for the Secure Boot Certificate Watcher that identifies unusual patterns in device behavior. Despite being labeled as "machine learning" in the documentation, the implementation uses statistical analysis (mean and standard deviation calculations) rather than ML.NET.

Key changes:

  • New anomaly detection service using statistical thresholds (2.5σ) to identify reporting frequency, deployment state, and inactivity anomalies
  • Background service running hourly scans with manual trigger capability
  • Web dashboard UI for viewing, investigating, and resolving anomalies with severity-based prioritization
  • Database schema additions with new Anomalies table and EF Core migration

Reviewed Changes

Copilot reviewed 22 out of 24 changed files in this pull request and generated 29 comments.

Show a summary per file
File Description
docs/ANOMALY_DETECTION.md Comprehensive documentation for the anomaly detection feature (misleadingly titled as ML-based)
SecureBootWatcher.Shared/Models/AnomalyDetectionResult.cs Data model for anomaly results with type, severity, and status enums
SecureBootDashboard.Api/Services/AnomalyDetectionService.cs Core service implementing statistical analysis for three anomaly types
SecureBootDashboard.Api/Services/AnomalyDetectionBackgroundService.cs Hosted service running hourly automatic scans
SecureBootDashboard.Api/Controllers/AnomaliesController.cs REST API endpoints for anomaly CRUD operations
SecureBootDashboard.Api/Data/AnomalyEntity.cs EF Core entity for persisting anomaly data
SecureBootDashboard.Api/Data/SecureBootDbContext.cs DbContext updates to include Anomalies table
SecureBootDashboard.Api/Data/Migrations/20251107061541_AddAnomalyDetection.cs Database migration creating Anomalies table with indexes
SecureBootDashboard.Web/Pages/Anomalies/List.cshtml Razor page displaying anomalies with statistics, filtering, and resolution actions
SecureBootDashboard.Web/Pages/Anomalies/List.cshtml.cs Page model handling anomaly list display, manual scans, and resolution
SecureBootDashboard.Web/Services/SecureBootApiClient.cs Client methods for anomaly API endpoints
SecureBootDashboard.Web/Services/ISecureBootApiClient.cs Interface additions for anomaly methods
SecureBootDashboard.Web/Pages/Shared/_Layout.cshtml Navigation menu update adding Anomalies link
SecureBootDashboard.Web/Pages/Index.cshtml Dashboard banner showing active anomaly count
SecureBootDashboard.Web/Pages/Index.cshtml.cs Logic to fetch and display anomaly count on main dashboard
SecureBootDashboard.Api/Program.cs Service registration for anomaly detection
SecureBootDashboard.Api/SecureBootDashboard.Api.csproj Added Microsoft.ML package (unused in implementation)
SecureBootDashboard.Api.Tests/SecureBootDashboard.Api.Tests.csproj Test dependencies for in-memory database and mocking
SecureBootDashboard.Api.Tests/AnomalyDetectionServiceTests.cs Unit tests covering inactivity, deployment state, and resolution scenarios
README.md Updated features section and roadmap to reflect anomaly detection completion
Files not reviewed (1)
  • SecureBootDashboard.Api/Data/Migrations/20251107061541_AddAnomalyDetection.Designer.cs: Language not supported

Comment thread SecureBootDashboard.Web/Pages/Anomalies/List.cshtml.cs Outdated
Comment thread SecureBootDashboard.Api/Services/AnomalyDetectionService.cs
Comment thread SecureBootDashboard.Api/Services/AnomalyDetectionService.cs
Comment thread docs/ANOMALY_DETECTION.md Outdated
Comment thread SecureBootDashboard.Api/Controllers/AnomaliesController.cs Outdated
Comment thread SecureBootDashboard.Web/Pages/Anomalies/List.cshtml.cs
Comment on lines +53 to +58
catch (Exception ex)
{
_logger.LogError(ex, "Error triggering anomaly scan");
ErrorMessage = "Error triggering anomaly scan. Please try again.";
Anomalies = await _apiClient.GetActiveAnomaliesAsync(HttpContext.RequestAborted);
}

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Copilot uses AI. Check for mistakes.
Comment thread SecureBootDashboard.Web/Pages/Anomalies/List.cshtml.cs
Comment thread SecureBootDashboard.Web/Pages/Index.cshtml.cs
Comment on lines +135 to +143
string description;
if (device.ReportCount > upperThreshold)
{
description = $"Excessive reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";
}
else
{
description = $"Insufficient reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";
}

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Suggested change
string description;
if (device.ReportCount > upperThreshold)
{
description = $"Excessive reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";
}
else
{
description = $"Insufficient reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";
}
string description = device.ReportCount > upperThreshold
? $"Excessive reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})"
: $"Insufficient reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";

Copilot uses AI. Check for mistakes.
robgrame and others added 12 commits November 8, 2025 08:56
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>
@robgrame robgrame requested a review from Copilot November 8, 2025 09:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Copilot reviewed 21 out of 23 changed files in this pull request and generated 22 comments.

Files not reviewed (1)
  • SecureBootDashboard.Api/Data/Migrations/20251107061541_AddAnomalyDetection.Designer.cs: Language not supported

Comment on lines +172 to +179
var stuckDevices = await _dbContext.Devices
.Include(d => d.Reports.Where(r => r.CreatedAtUtc > lookbackPeriod).OrderByDescending(r => r.CreatedAtUtc))
.Where(d => d.Reports.Any(r => r.CreatedAtUtc > lookbackPeriod))
.ToListAsync(cancellationToken);

foreach (var device in stuckDevices)
{
var recentReports = device.Reports.Take(5).ToList();

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inefficient query pattern: The query loads devices with filtered reports using .Include(d => d.Reports.Where(...)) (line 173), but then iterates through devices and takes only 5 reports per device (line 179). For large datasets, this could load many reports into memory unnecessarily. Consider restructuring the query to limit reports per device at the database level or using a more efficient approach with separate queries.

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +88
[HttpPost("scan")]
[ProducesResponseType(typeof(IReadOnlyList<AnomalyDetectionResult>), 200)]
public async Task<IActionResult> TriggerScanAsync(CancellationToken cancellationToken)
{
try
{
_logger.LogInformation("Manual anomaly detection scan triggered");
var anomalies = await _anomalyService.DetectAnomaliesAsync(cancellationToken);
return Ok(anomalies);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during manual anomaly detection scan");
return StatusCode(500, "Error during anomaly detection");
}
}

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation: The manual scan trigger endpoint (line 73-88) accepts a POST request but doesn't validate any parameters or implement any rate limiting. A malicious or buggy client could repeatedly trigger expensive anomaly detection scans. Consider implementing rate limiting, authentication checks, or requiring administrator privileges for this endpoint.

Copilot uses AI. Check for mistakes.
foreach (var device in deviceReportStats.Where(d => d.ReportCount > upperThreshold || d.ReportCount < lowerThreshold))
{
var severity = Math.Abs(device.ReportCount - mean) / Math.Max(stdDev, 1);
severity = Math.Min(severity / 3.0, 1.0); // Normalize to 0-1

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded magic numbers: The severity calculation uses a hardcoded divisor of 3.0 (line 139) to normalize the severity score. This magic number should be extracted as a named constant (e.g., NormalizationFactor or MaxExpectedStandardDeviations) to improve code readability and maintainability.

Copilot uses AI. Check for mistakes.
Comment on lines +238 to +243
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Failed to resolve anomaly {AnomalyId}", id);
// Suppress exception for consistency with other methods
return;
}

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "Suppress exception for consistency with other methods" (line 241) suggests this is intentional behavior, but it creates an inconsistent API contract. The method signature returns Task, not Task<bool>, so callers have no way to know if the operation succeeded or failed. Consider either returning a boolean success indicator or using a result type that can convey success/failure status.

Copilot uses AI. Check for mistakes.
Comment on lines +225 to +226
var inactiveDevices = await _dbContext.Devices
.Where(d => d.LastSeenUtc < inactivityThreshold && d.Reports.Any())

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The DetectInactivityAnomaliesAsync method queries for devices where LastSeenUtc < inactivityThreshold (line 226), which means devices that haven't been seen in the last 14 days. However, it also requires d.Reports.Any() to ensure the device has reports. Consider whether this excludes newly registered devices that have never reported, which might be an anomaly worth detecting.

Copilot uses AI. Check for mistakes.
{
_logger.LogError(dbex, "Database update error while detecting anomalies");
}
catch (Exception ex)

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Suggested change
catch (Exception ex)
catch (Exception ex) when (
!(ex is OutOfMemoryException) &&
!(ex is StackOverflowException) &&
!(ex is ThreadAbortException)
)

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +59
catch (Exception ex)
{
_logger.LogError(ex, "Error triggering anomaly scan");
ErrorMessage = "Error triggering anomaly scan. Please try again.";
Anomalies = await _apiClient.GetActiveAnomaliesAsync(HttpContext.RequestAborted);
}

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Copilot uses AI. Check for mistakes.
Comment on lines +72 to +76
catch (Exception ex)
{
_logger.LogError(ex, "Error resolving anomaly {AnomalyId}", anomalyId);
ErrorMessage = "Error resolving anomaly. Please try again.";
}

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Copilot uses AI. Check for mistakes.
Comment on lines +67 to +71
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load anomaly count");
ActiveAnomaliesCount = 0;
}

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generic catch clause.

Suggested change
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load anomaly count");
ActiveAnomaliesCount = 0;
}
catch (HttpRequestException ex)
{
_logger.LogWarning(ex, "Failed to load anomaly count");
ActiveAnomaliesCount = 0;
}
catch (TaskCanceledException ex)
{
_logger.LogWarning(ex, "Failed to load anomaly count (request canceled)");
ActiveAnomaliesCount = 0;
}

Copilot uses AI. Check for mistakes.
Comment on lines +141 to +149
string description;
if (device.ReportCount > upperThreshold)
{
description = $"Excessive reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";
}
else
{
description = $"Insufficient reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";
}

Copilot AI Nov 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches of this 'if' statement write to the same variable - consider using '?' to express intent better.

Suggested change
string description;
if (device.ReportCount > upperThreshold)
{
description = $"Excessive reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";
}
else
{
description = $"Insufficient reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";
}
string description = device.ReportCount > upperThreshold
? $"Excessive reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})"
: $"Insufficient reporting detected. Device reported {device.ReportCount} times in {ReportingFrequencyLookbackDays} days (expected: ~{mean:F1})";

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Machine learning anomaly detection

3 participants