From d89e3f0b48a45938f487ccc3cd2662b86f162c70 Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Wed, 26 Aug 2026 13:54:58 +0200 Subject: [PATCH] fix(backend): cap vulnerability_report severity at Warning A completed scan is a finding about the software inventory, not an enrollment failure. Mapping the CVE risk onto Error/Critical made the report read as a failed enrollment: the diagnosis page collected it as failure evidence, MCP search lifted it on failure-intent queries, and the timeline's Error filter filled up with clean sessions. critical/high now map to Warning, everything else to Info; the CVE severity itself stays in scan_summary and on the report panel's own scale. Co-Authored-By: Claude Opus 5 (1M context) --- .../VulnerabilityReportEventSequenceTests.cs | 31 ++++++++++++++++++- .../VulnerabilityCorrelateHandler.cs | 20 ++++++++++-- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/Backend/AutopilotMonitor.Functions.Tests/VulnerabilityReportEventSequenceTests.cs b/src/Backend/AutopilotMonitor.Functions.Tests/VulnerabilityReportEventSequenceTests.cs index a2a726e2..33acb83e 100644 --- a/src/Backend/AutopilotMonitor.Functions.Tests/VulnerabilityReportEventSequenceTests.cs +++ b/src/Backend/AutopilotMonitor.Functions.Tests/VulnerabilityReportEventSequenceTests.cs @@ -115,7 +115,7 @@ public class VulnerabilityReportEventSequenceTests Assert.Equal("vulnerability_report", evt.EventType); Assert.Equal(AutopilotMonitor.Shared.Constants.EventTypes.VulnerabilityReport, evt.EventType); Assert.Equal("VulnerabilityCorrelationService", evt.Source); - Assert.Equal(EventSeverity.Error, evt.Severity); // risk=high → Error + Assert.Equal(EventSeverity.Warning, evt.Severity); // risk=high → Warning (severity is capped) // Non-phase event: must stay Unknown so the UI timeline doesn't render a phantom phase boundary. Assert.Equal(EnrollmentPhase.Unknown, evt.Phase); Assert.Equal(NowUtc, evt.Timestamp); @@ -127,6 +127,35 @@ public class VulnerabilityReportEventSequenceTests Assert.IsType>(evt.Data["scan_summary"]); } + [Theory] + // The scan reports on the software inventory, not on the enrollment: a finding is never an + // enrollment failure, so severity is capped at Warning. Error/Critical would put the report + // into the diagnosis page's failure evidence and into MCP's failure-intent ranking. + [InlineData("critical", EventSeverity.Warning)] + [InlineData("high", EventSeverity.Warning)] + [InlineData("medium", EventSeverity.Info)] + [InlineData("low", EventSeverity.Info)] + [InlineData("none", EventSeverity.Info)] + [InlineData("HIGH", EventSeverity.Warning)] // case-insensitive + [InlineData("", EventSeverity.Info)] + public void Severity_IsCappedAtWarning(string risk, EventSeverity expected) + { + var evt = Build(null, new List(), Report(risk)); + + Assert.Equal(expected, evt.Severity); + } + + [Fact] + public void Severity_IsInfo_WhenRiskIsMissing() + { + var report = new Dictionary + { + ["scan_summary"] = new Dictionary(), + }; + + Assert.Equal(EventSeverity.Info, Build(null, new List(), report).Severity); + } + [Fact] public void Data_Phase_CarriesTheLabel() { diff --git a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelateHandler.cs b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelateHandler.cs index 093d2c3b..b747dd24 100644 --- a/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelateHandler.cs +++ b/src/Backend/AutopilotMonitor.Functions/Services/Vulnerability/VulnerabilityCorrelateHandler.cs @@ -541,12 +541,26 @@ namespace AutopilotMonitor.Functions.Services.Vulnerability string.Equals(phase, "user_enrollment", StringComparison.Ordinal) || string.Equals(phase, "both", StringComparison.Ordinal); + /// + /// Maps the scan's overall CVE risk onto the event severity, capped at + /// . + /// + /// Why the cap: EventSeverity on the timeline states how badly the + /// enrollment went, not how severe a CVE is. A completed scan is a finding about + /// the software inventory — nothing failed. Emitting Error/Critical made the report read + /// as an enrollment failure: the diagnosis page collects Error/Critical events as failure + /// evidence, MCP search lifts high-severity events on failure-intent queries, and the + /// timeline's Error filter — the one used to hunt for problems — filled up with clean + /// sessions. The CVE severity itself is not lost: it rides in scan_summary + /// (overall_risk, critical_cves, …) and the report panel renders it on its + /// own colour scale. Warning keeps the report visible without claiming a failure. + /// + /// private static EventSeverity MapVulnerabilityRiskToSeverity(string? overallRisk) => (overallRisk ?? string.Empty).ToLowerInvariant() switch { - "critical" => EventSeverity.Critical, - "high" => EventSeverity.Error, - "medium" => EventSeverity.Warning, + "critical" => EventSeverity.Warning, + "high" => EventSeverity.Warning, _ => EventSeverity.Info, }; }