diff --git a/src/DockerUpdateGuard/Components/Pages/MyImageDetail.razor b/src/DockerUpdateGuard/Components/Pages/MyImageDetail.razor index 5c6db13..17ad928 100644 --- a/src/DockerUpdateGuard/Components/Pages/MyImageDetail.razor +++ b/src/DockerUpdateGuard/Components/Pages/MyImageDetail.razor @@ -47,28 +47,7 @@ - - Vulnerability assessment - @_detail.VulnerabilityAssessment.Status - - @if (_detail.VulnerabilityAssessment.NewFindingCount > 0) - { - @($"+{_detail.VulnerabilityAssessment.NewFindingCount} new") - } - @if (_detail.VulnerabilityAssessment.ResolvedFindingCount > 0) - { - @($"−{_detail.VulnerabilityAssessment.ResolvedFindingCount} resolved") - } - @($"{_detail.VulnerabilityAssessment.Source} · {_detail.VulnerabilityAssessment.ActiveFindingCount} active") - @if (_detail.VulnerabilityAssessment.ActiveFindingCount > 0) - { - @($"{_detail.VulnerabilityAssessment.FixableFindingCount} of {_detail.VulnerabilityAssessment.ActiveFindingCount} active findings have a fix available") - } - @if (string.IsNullOrWhiteSpace(_detail.VulnerabilityAssessment.Message) == false) - { - @_detail.VulnerabilityAssessment.Message - } - + @if (string.IsNullOrWhiteSpace(_detail.BaseRuntimeAlertSummary) == false) { @@ -155,7 +134,7 @@ @ImageReferenceFormatter.GetReference(context.ImageReference) @ImageReferenceFormatter.GetReference(context.SourceReference ?? string.Empty) - @context.VulnerabilityAssessment.Status + @context.VulnerabilityAssessment.Status @($"{context.VulnerabilityAssessment.ActiveFindingCount} active · {context.VulnerabilityAssessment.Source}") diff --git a/src/DockerUpdateGuard/Components/Pages/MyImageDetail.razor.cs b/src/DockerUpdateGuard/Components/Pages/MyImageDetail.razor.cs index 49144f8..ac909ba 100644 --- a/src/DockerUpdateGuard/Components/Pages/MyImageDetail.razor.cs +++ b/src/DockerUpdateGuard/Components/Pages/MyImageDetail.razor.cs @@ -82,17 +82,6 @@ internal static Color GetScanStatusColor(string status) }; } - /// - /// Resolve the chip color for a vulnerability assessment status - /// - /// Status label - /// Severity summary of the active findings - /// Chip color - internal static Color GetVulnerabilityAssessmentColor(string status, VulnerabilitySeveritySummaryViewData? severitySummary) - { - return VulnerabilityDisplayFormatter.GetStatusColor(status, severitySummary); - } - #endregion // Static methods #region Methods diff --git a/src/DockerUpdateGuard/Components/Pages/ObservedImageDetail.razor b/src/DockerUpdateGuard/Components/Pages/ObservedImageDetail.razor index e91d675..7b8cba8 100644 --- a/src/DockerUpdateGuard/Components/Pages/ObservedImageDetail.razor +++ b/src/DockerUpdateGuard/Components/Pages/ObservedImageDetail.razor @@ -50,28 +50,7 @@ - - Vulnerability assessment - @_detail.VulnerabilityAssessment.Status - - @if (_detail.VulnerabilityAssessment.NewFindingCount > 0) - { - @($"+{_detail.VulnerabilityAssessment.NewFindingCount} new") - } - @if (_detail.VulnerabilityAssessment.ResolvedFindingCount > 0) - { - @($"−{_detail.VulnerabilityAssessment.ResolvedFindingCount} resolved") - } - @($"{_detail.VulnerabilityAssessment.Source} · {_detail.VulnerabilityAssessment.ActiveFindingCount} active") - @if (_detail.VulnerabilityAssessment.ActiveFindingCount > 0) - { - @($"{_detail.VulnerabilityAssessment.FixableFindingCount} of {_detail.VulnerabilityAssessment.ActiveFindingCount} active findings have a fix available") - } - @if (string.IsNullOrWhiteSpace(_detail.VulnerabilityAssessment.Message) == false) - { - @_detail.VulnerabilityAssessment.Message - } - + @if (string.IsNullOrWhiteSpace(_detail.BaseRuntimeAlertSummary) == false) { @@ -163,7 +142,7 @@ @ImageReferenceFormatter.GetReference(context.ImageReference) @ImageReferenceFormatter.GetReference(context.SourceReference ?? string.Empty) - @context.VulnerabilityAssessment.Status + @context.VulnerabilityAssessment.Status @($"{context.VulnerabilityAssessment.ActiveFindingCount} active · {context.VulnerabilityAssessment.Source}") diff --git a/src/DockerUpdateGuard/Components/Pages/ObservedImageDetail.razor.cs b/src/DockerUpdateGuard/Components/Pages/ObservedImageDetail.razor.cs index 0da2584..3575f58 100644 --- a/src/DockerUpdateGuard/Components/Pages/ObservedImageDetail.razor.cs +++ b/src/DockerUpdateGuard/Components/Pages/ObservedImageDetail.razor.cs @@ -82,17 +82,6 @@ private static Color GetScanStatusColor(string status) }; } - /// - /// Resolve the chip color for a vulnerability assessment status - /// - /// Status label - /// Severity summary of the active findings - /// Chip color - private static Color GetVulnerabilityAssessmentColor(string status, VulnerabilitySeveritySummaryViewData? severitySummary) - { - return VulnerabilityDisplayFormatter.GetStatusColor(status, severitySummary); - } - /// /// Resolve the chip color for a base-runtime alert /// diff --git a/src/DockerUpdateGuard/Components/Pages/RuntimeContainerDetail.razor b/src/DockerUpdateGuard/Components/Pages/RuntimeContainerDetail.razor index 96b6659..a45cd24 100644 --- a/src/DockerUpdateGuard/Components/Pages/RuntimeContainerDetail.razor +++ b/src/DockerUpdateGuard/Components/Pages/RuntimeContainerDetail.razor @@ -102,32 +102,7 @@ } - - Vulnerability assessment - @_detail.VulnerabilityAssessment.Status - - @if (_detail.VulnerabilityAssessment.NewFindingCount > 0) - { - @($"+{_detail.VulnerabilityAssessment.NewFindingCount} new") - } - @if (_detail.VulnerabilityAssessment.ResolvedFindingCount > 0) - { - @($"−{_detail.VulnerabilityAssessment.ResolvedFindingCount} resolved") - } - @($"{_detail.VulnerabilityAssessment.Source} · {_detail.VulnerabilityAssessment.ActiveFindingCount} active") - @if (_detail.VulnerabilityAssessment.ActiveFindingCount > 0) - { - @($"{_detail.VulnerabilityAssessment.FixableFindingCount} of {_detail.VulnerabilityAssessment.ActiveFindingCount} active findings have a fix available") - } - @if (_detail.VulnerabilityAssessment.CheckedAtUtc is DateTimeOffset checkedAtUtc) - { - Checked @checkedAtUtc.ToLocalTime().ToString("g") - } - @if (string.IsNullOrWhiteSpace(_detail.VulnerabilityAssessment.Message) == false) - { - @_detail.VulnerabilityAssessment.Message - } - + @if (_detail.ActiveBaseImageVulnerabilityFindingCount > 0) { @@ -167,7 +142,7 @@ @ImageReferenceFormatter.GetReference(context.ImageReference) @ImageReferenceFormatter.GetReference(context.SourceReference ?? string.Empty) - @context.VulnerabilityAssessment.Status + @context.VulnerabilityAssessment.Status @($"{context.VulnerabilityAssessment.ActiveFindingCount} active · {context.VulnerabilityAssessment.Source}") diff --git a/src/DockerUpdateGuard/Components/Pages/RuntimeContainerDetail.razor.cs b/src/DockerUpdateGuard/Components/Pages/RuntimeContainerDetail.razor.cs index 8443c80..3d2b513 100644 --- a/src/DockerUpdateGuard/Components/Pages/RuntimeContainerDetail.razor.cs +++ b/src/DockerUpdateGuard/Components/Pages/RuntimeContainerDetail.razor.cs @@ -108,22 +108,6 @@ private static Color GetUpdateStateColor(string? status) }; } - /// - /// Resolve the chip color for a vulnerability status - /// - /// Vulnerability status - /// Severity summary of the active findings - /// Chip color - private static Color GetVulnerabilityStatusColor(string? status, VulnerabilitySeveritySummaryViewData? severitySummary) - { - if (string.IsNullOrWhiteSpace(status)) - { - return Color.Default; - } - - return VulnerabilityDisplayFormatter.GetStatusColor(status, severitySummary); - } - /// /// Resolve the chip color for a scan status /// diff --git a/src/DockerUpdateGuard/Components/Shared/VulnerabilityAssessmentCard.razor b/src/DockerUpdateGuard/Components/Shared/VulnerabilityAssessmentCard.razor new file mode 100644 index 0000000..f5847f8 --- /dev/null +++ b/src/DockerUpdateGuard/Components/Shared/VulnerabilityAssessmentCard.razor @@ -0,0 +1,29 @@ +@if (Assessment is not null) +{ + + Vulnerability assessment + @Assessment.Status + + @if (Assessment.NewFindingCount > 0) + { + @($"+{Assessment.NewFindingCount} new") + } + @if (Assessment.ResolvedFindingCount > 0) + { + @($"−{Assessment.ResolvedFindingCount} resolved") + } + @($"{Assessment.Source} · {Assessment.ActiveFindingCount} active") + @if (Assessment.ActiveFindingCount > 0) + { + @($"{Assessment.FixableFindingCount} of {Assessment.ActiveFindingCount} active findings have a fix available") + } + @if (ShowCheckedAt && Assessment.CheckedAtUtc is DateTimeOffset checkedAtUtc) + { + Checked @checkedAtUtc.ToLocalTime().ToString("g") + } + @if (string.IsNullOrWhiteSpace(Assessment.Message) == false) + { + @Assessment.Message + } + +} \ No newline at end of file diff --git a/src/DockerUpdateGuard/Components/Shared/VulnerabilityAssessmentCard.razor.cs b/src/DockerUpdateGuard/Components/Shared/VulnerabilityAssessmentCard.razor.cs new file mode 100644 index 0000000..6a0fa67 --- /dev/null +++ b/src/DockerUpdateGuard/Components/Shared/VulnerabilityAssessmentCard.razor.cs @@ -0,0 +1,27 @@ +using DockerUpdateGuard.UI; + +using Microsoft.AspNetCore.Components; + +namespace DockerUpdateGuard.Components.Shared; + +/// +/// Summary card that renders the vulnerability assessment of a container or an image +/// +public partial class VulnerabilityAssessmentCard +{ + #region Properties + + /// + /// Vulnerability assessment to render + /// + [Parameter] + public VulnerabilityAssessmentViewData? Assessment { get; set; } + + /// + /// Whether the timestamp of the last check is rendered + /// + [Parameter] + public bool ShowCheckedAt { get; set; } + + #endregion // Properties +} \ No newline at end of file diff --git a/src/Tests/DockerUpdateGuard.Tests/MyImageDetailTests.cs b/src/Tests/DockerUpdateGuard.Tests/MyImageDetailTests.cs index 8f8aac6..6f263f8 100644 --- a/src/Tests/DockerUpdateGuard.Tests/MyImageDetailTests.cs +++ b/src/Tests/DockerUpdateGuard.Tests/MyImageDetailTests.cs @@ -28,12 +28,6 @@ public class MyImageDetailTests private static readonly MethodInfo _getScanStatusColorMethod = typeof(MyImageDetail).GetMethod("GetScanStatusColor", BindingFlags.NonPublic | BindingFlags.Static) ?? throw new InvalidOperationException("MyImageDetail must expose the non-public static GetScanStatusColor method"); - /// - /// Non-public vulnerability-assessment chip color resolver - /// - private static readonly MethodInfo _getVulnerabilityAssessmentColorMethod = typeof(MyImageDetail).GetMethod("GetVulnerabilityAssessmentColor", BindingFlags.NonPublic | BindingFlags.Static) - ?? throw new InvalidOperationException("MyImageDetail must expose the non-public static GetVulnerabilityAssessmentColor method"); - #endregion // Fields #region Methods @@ -67,32 +61,6 @@ public void MyImageDetailGetScanStatusColorKnownStatusReturnsExpectedColor(strin $"Scan status '{status}' must map to Color.{expectedColor}"); } - /// - /// Verify vulnerability assessment chip colors map to expected MudBlazor color values - /// - /// Vulnerability assessment status string - /// Expected chip color - [TestMethod] - [DataRow("Findings Detected", Color.Warning)] - [DataRow("FINDINGS DETECTED", Color.Warning)] - [DataRow("No Findings", Color.Success)] - [DataRow("NO FINDINGS", Color.Success)] - [DataRow("Failed", Color.Error)] - [DataRow("FAILED", Color.Error)] - [DataRow("Not Configured", Color.Default)] - [DataRow("NOT CONFIGURED", Color.Default)] - [DataRow("Unsupported", Color.Default)] - [DataRow("UNSUPPORTED", Color.Default)] - [DataRow("anything-else", Color.Info)] - public void MyImageDetailGetVulnerabilityAssessmentColorKnownStatusReturnsExpectedColor(string status, Color expectedColor) - { - var color = (Color)_getVulnerabilityAssessmentColorMethod.Invoke(null, [status, null])!; - - Assert.AreEqual(expectedColor, - color, - $"Vulnerability assessment status '{status}' must map to Color.{expectedColor}"); - } - /// /// Verify the vulnerability assessment card shows the fixable finding count and the update-finding hint chip /// diff --git a/src/Tests/DockerUpdateGuard.Tests/VulnerabilityAssessmentCardTests.cs b/src/Tests/DockerUpdateGuard.Tests/VulnerabilityAssessmentCardTests.cs new file mode 100644 index 0000000..cf5a1f4 --- /dev/null +++ b/src/Tests/DockerUpdateGuard.Tests/VulnerabilityAssessmentCardTests.cs @@ -0,0 +1,155 @@ +using Bunit; + +using DockerUpdateGuard.Components.Shared; +using DockerUpdateGuard.Tests.Helper; +using DockerUpdateGuard.UI; + +namespace DockerUpdateGuard.Tests; + +/// +/// Tests for +/// +[TestClass] +public class VulnerabilityAssessmentCardTests +{ + #region Static methods + + /// + /// Create a populated vulnerability assessment for the card + /// + /// Vulnerability assessment view data + private static VulnerabilityAssessmentViewData CreateAssessment() + { + return new VulnerabilityAssessmentViewData + { + Status = "Findings detected", + Source = "Trivy", + Message = "Scan completed with findings", + CheckedAtUtc = new DateTimeOffset(2026, 3, 14, 10, 30, 0, TimeSpan.Zero), + ActiveFindingCount = 6, + FixableFindingCount = 4, + NewFindingCount = 2, + ResolvedFindingCount = 3, + SeveritySummary = new VulnerabilitySeveritySummaryViewData + { + CriticalCount = 1, + HighCount = 2, + }, + }; + } + + #endregion // Static methods + + #region Methods + + /// + /// Verify the card renders the status, the severity chips and the secondary summary lines + /// + /// Task + [TestMethod] + public async Task VulnerabilityAssessmentCardPopulatedAssessmentRendersStatusChipsAndSummary() + { + var testContext = BlazorTestContextFactory.Create(); + + await using (testContext) + { + var component = testContext.Render(parameters => parameters.Add(card => card.Assessment, CreateAssessment())); + var markup = component.Markup; + + Assert.Contains("Vulnerability assessment", markup, "The card must render its label"); + Assert.Contains("Findings detected", markup, "The card must render the assessment status"); + Assert.Contains("1 Critical", markup, "The card must render the severity chips"); + Assert.Contains("2 High", markup, "The card must render the severity chips"); + Assert.Contains("+2 new", markup, "The card must render the new finding count"); + Assert.Contains("−3 resolved", markup, "The card must render the resolved finding count"); + Assert.Contains("Trivy · 6 active", markup, "The card must render the source and the active finding count"); + Assert.Contains("4 of 6 active findings have a fix available", markup, "The card must render the fixable finding summary"); + Assert.Contains("Scan completed with findings", markup, "The card must render the assessment message"); + } + } + + /// + /// Verify the check timestamp is rendered when the card is asked to show it + /// + /// Task + [TestMethod] + public async Task VulnerabilityAssessmentCardShowCheckedAtEnabledRendersCheckedLine() + { + var testContext = BlazorTestContextFactory.Create(); + + await using (testContext) + { + var assessment = CreateAssessment(); + var component = testContext.Render(parameters => parameters.Add(card => card.Assessment, assessment) + .Add(card => card.ShowCheckedAt, true)); + var expectedTimestamp = assessment.CheckedAtUtc!.Value.ToLocalTime().ToString("g"); + + Assert.Contains($"Checked {expectedTimestamp}", component.Markup, "The card must render the check timestamp when ShowCheckedAt is enabled"); + } + } + + /// + /// Verify the check timestamp is omitted by default + /// + /// Task + [TestMethod] + public async Task VulnerabilityAssessmentCardShowCheckedAtDisabledOmitsCheckedLine() + { + var testContext = BlazorTestContextFactory.Create(); + + await using (testContext) + { + var component = testContext.Render(parameters => parameters.Add(card => card.Assessment, CreateAssessment())); + + Assert.DoesNotContain("Checked ", component.Markup, "The card must omit the check timestamp when ShowCheckedAt is disabled"); + } + } + + /// + /// Verify counters and the message are omitted when they carry no information + /// + /// Task + [TestMethod] + public async Task VulnerabilityAssessmentCardEmptyAssessmentOmitsOptionalLines() + { + var testContext = BlazorTestContextFactory.Create(); + + await using (testContext) + { + var assessment = new VulnerabilityAssessmentViewData + { + Status = "No findings", + Source = "Trivy", + }; + + var component = testContext.Render(parameters => parameters.Add(card => card.Assessment, assessment) + .Add(card => card.ShowCheckedAt, true)); + var markup = component.Markup; + + Assert.Contains("Trivy · 0 active", markup, "The card must render the source and the active finding count"); + Assert.DoesNotContain("new", markup, "The card must omit the new finding chip when no findings are new"); + Assert.DoesNotContain("resolved", markup, "The card must omit the resolved finding chip when no findings were resolved"); + Assert.DoesNotContain("have a fix available", markup, "The card must omit the fixable summary when there are no active findings"); + Assert.DoesNotContain("Checked ", markup, "The card must omit the check timestamp when it is not set"); + } + } + + /// + /// Verify a missing assessment renders nothing + /// + /// Task + [TestMethod] + public async Task VulnerabilityAssessmentCardNullAssessmentRendersNothing() + { + var testContext = BlazorTestContextFactory.Create(); + + await using (testContext) + { + var component = testContext.Render(parameters => parameters.Add(card => card.Assessment, null)); + + Assert.IsEmpty(component.Markup, "A missing assessment must not render the card"); + } + } + + #endregion // Methods +} \ No newline at end of file