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