Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
{
<MudPaper Class="summary-card" Elevation="0">
<span class="summary-label">Vulnerability assessment</span>
@if (RemediationSummary is not null)
{
<span class="@RemediationCssClass">@RemediationSummary</span>
}
<MudChip T="string" Size="Size.Small" Color="@VulnerabilityDisplayFormatter.GetStatusColor(Assessment.Status, Assessment.SeveritySummary)">@Assessment.Status</MudChip>
<VulnerabilitySeverityChips Summary="Assessment.SeveritySummary" />
@if (Assessment.NewFindingCount > 0)
Expand All @@ -13,15 +17,11 @@
<MudChip T="string" Size="Size.Small" Color="Color.Success">@($"−{Assessment.ResolvedFindingCount} resolved")</MudChip>
}
<span class="table-secondary">@($"{Assessment.Source} · {Assessment.ActiveFindingCount} active")</span>
@if (Assessment.ActiveFindingCount > 0)
{
<span class="table-secondary">@($"{Assessment.FixableFindingCount} of {Assessment.ActiveFindingCount} active findings have a fix available")</span>
}
@if (ShowCheckedAt && Assessment.CheckedAtUtc is DateTimeOffset checkedAtUtc)
{
<span class="table-secondary">Checked @checkedAtUtc.ToLocalTime().ToString("g")</span>
}
@if (string.IsNullOrWhiteSpace(Assessment.Message) == false)
@if (ShowMessage)
{
<span class="table-secondary">@Assessment.Message</span>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ namespace DockerUpdateGuard.Components.Shared;
/// </summary>
public partial class VulnerabilityAssessmentCard
{
#region Constants

/// <summary>
/// Status label of an assessment that reported findings
/// </summary>
private const string FindingsDetectedStatus = "Findings detected";

/// <summary>
/// Status label of an assessment that reported no findings
/// </summary>
private const string NoFindingsStatus = "No findings";

#endregion // Constants

#region Properties

/// <summary>
Expand All @@ -23,5 +37,65 @@ public partial class VulnerabilityAssessmentCard
[Parameter]
public bool ShowCheckedAt { get; set; }

/// <summary>
/// Leading line that states whether updating the image can remedy the known findings,
/// or <c>null</c> when the assessment has no active findings
/// </summary>
private string? RemediationSummary
{
get
{
if (Assessment is null || Assessment.ActiveFindingCount == 0)
{
return null;
}

if (Assessment.FixableFindingCount > 0)
{
return $"{Assessment.FixableFindingCount} of {Assessment.ActiveFindingCount} findings can be fixed by updating this image";
}

return $"Nothing to fix right now — no upstream fix is available for the {Assessment.ActiveFindingCount} known issues in this image";
}
}

/// <summary>
/// CSS classes of the leading remediation line, using the warning tone only when an update actually helps
/// </summary>
private string RemediationCssClass
{
get
{
if (Assessment is not null && Assessment.FixableFindingCount > 0)
{
return "summary-remediation summary-remediation--warning";
}

return "summary-remediation";
}
}

/// <summary>
/// Whether the persisted assessment message is rendered, which is suppressed for the
/// completed scan states because there the message only restates the finding count
/// </summary>
private bool ShowMessage
{
get
{
if (Assessment is null || string.IsNullOrWhiteSpace(Assessment.Message) == true)
{
return false;
}

if (string.Equals(Assessment.Status, FindingsDetectedStatus, StringComparison.OrdinalIgnoreCase) == true)
{
return false;
}

return string.Equals(Assessment.Status, NoFindingsStatus, StringComparison.OrdinalIgnoreCase) == false;
}
}

#endregion // Properties
}
18 changes: 18 additions & 0 deletions src/DockerUpdateGuard/wwwroot/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,24 @@ a {
overflow-wrap: anywhere;
}

.summary-remediation {
display: block;
margin: 0.35rem 0 0.55rem;
color: var(--dug-text-muted);
font-size: 0.88rem;
font-weight: 600;
line-height: 1.35;
overflow-wrap: anywhere;
}

.summary-remediation--warning {
color: var(--dug-warning);
}

.dug-dark .summary-remediation--warning {
color: #f59e0b;
}

.dug-chart {
position: relative;
min-height: 200px;
Expand Down
6 changes: 3 additions & 3 deletions src/Tests/DockerUpdateGuard.Tests/MyImageDetailTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public async Task MyImageDetailWithFixableFindingsShowsFixableSummaryAndUpdateHi

var component = testContext.Render<MyImageDetail>(parameters => parameters.Add(page => page.ObservedImageId, observedImageId));

Assert.Contains("1 of 2 active findings have a fix available",
Assert.Contains("1 of 2 findings can be fixed by updating this image",
component.Markup,
"The vulnerability assessment card must show the fixable finding count");
Assert.Contains("Updating may resolve up to 1 of 2 active findings",
Expand Down Expand Up @@ -150,9 +150,9 @@ public async Task MyImageDetailWithoutFixableFindingsHidesUpdateHint()

var component = testContext.Render<MyImageDetail>(parameters => parameters.Add(page => page.ObservedImageId, observedImageId));

Assert.DoesNotContain("active findings have a fix available",
Assert.DoesNotContain("summary-remediation",
component.Markup,
"The fixable summary line must be hidden when there are no active findings");
"The remediation line must be hidden when there are no active findings");
Assert.DoesNotContain("Updating may resolve",
component.Markup,
"The update findings hint chip must be hidden when no active finding has a fix available");
Expand Down
4 changes: 2 additions & 2 deletions src/Tests/DockerUpdateGuard.Tests/ObservedImageDetailTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public async Task ObservedImageDetailWithFixableFindingsShowsFixableSummaryAndUp

var component = testContext.Render<ObservedImageDetail>(parameters => parameters.Add(page => page.ObservedImageId, observedImageId));

Assert.Contains("2 of 3 active findings have a fix available",
Assert.Contains("2 of 3 findings can be fixed by updating this image",
component.Markup,
"The vulnerability assessment card must show the fixable finding count");
Assert.Contains("Updating may resolve up to 2 of 3 active findings",
Expand Down Expand Up @@ -100,7 +100,7 @@ public async Task ObservedImageDetailWithoutActiveUpdateFindingsHidesUpdateHint(

var component = testContext.Render<ObservedImageDetail>(parameters => parameters.Add(page => page.ObservedImageId, observedImageId));

Assert.Contains("1 of 1 active findings have a fix available",
Assert.Contains("1 of 1 findings can be fixed by updating this image",
component.Markup,
"The vulnerability assessment card must still show the fixable finding count");
Assert.DoesNotContain("Updating may resolve",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public async Task RuntimeContainerDetailWithFixableFindingsShowsFixableSummaryAn
var component = testContext.Render<RuntimeContainerDetail>(parameters => parameters.Add(page => page.DockerInstanceId, dockerInstanceId)
.Add(page => page.ContainerId, containerId));

Assert.Contains("1 of 2 active findings have a fix available",
Assert.Contains("1 of 2 findings can be fixed by updating this image",
component.Markup,
"The vulnerability assessment card must show the fixable finding count");
Assert.Contains("Updating may resolve up to 1 of 2 active findings",
Expand Down Expand Up @@ -123,9 +123,9 @@ public async Task RuntimeContainerDetailWithoutFixableFindingsHidesFixableSummar
var component = testContext.Render<RuntimeContainerDetail>(parameters => parameters.Add(page => page.DockerInstanceId, dockerInstanceId)
.Add(page => page.ContainerId, containerId));

Assert.DoesNotContain("active findings have a fix available",
Assert.DoesNotContain("summary-remediation",
component.Markup,
"The fixable summary line must be hidden when there are no active findings");
"The remediation line must be hidden when there are no active findings");
Assert.DoesNotContain("Updating may resolve",
component.Markup,
"The update hint chip must be hidden when no active finding has a fix available");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,76 @@ public async Task VulnerabilityAssessmentCardPopulatedAssessmentRendersStatusChi
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");
Assert.Contains("4 of 6 findings can be fixed by updating this image", markup, "The card must lead with the fixable finding statement");
Assert.DoesNotContain("Scan completed with findings", markup, "The card must suppress the provider message for a completed scan with findings");
}
}

/// <summary>
/// Verify the leading line names the fixable findings and uses the warning tone
/// </summary>
/// <returns>Task</returns>
[TestMethod]
public async Task VulnerabilityAssessmentCardFixableFindingsRendersWarningRemediationLine()
{
var testContext = BlazorTestContextFactory.Create();

await using (testContext)
{
var component = testContext.Render<VulnerabilityAssessmentCard>(parameters => parameters.Add(card => card.Assessment, CreateAssessment()));
var markup = component.Markup;

Assert.Contains("4 of 6 findings can be fixed by updating this image", markup, "The card must state how many findings an update fixes");
Assert.Contains("summary-remediation--warning", markup, "The fixable statement must use the warning tone");
Assert.DoesNotContain("Nothing to fix right now", markup, "The card must not state that nothing can be fixed while findings are fixable");
}
}

/// <summary>
/// Verify the leading line reports the neutral no-upstream-fix statement when nothing is fixable
/// </summary>
/// <returns>Task</returns>
[TestMethod]
public async Task VulnerabilityAssessmentCardWithoutFixableFindingsRendersNeutralRemediationLine()
{
var testContext = BlazorTestContextFactory.Create();

await using (testContext)
{
var assessment = CreateAssessment();
assessment.FixableFindingCount = 0;

var component = testContext.Render<VulnerabilityAssessmentCard>(parameters => parameters.Add(card => card.Assessment, assessment));
var markup = component.Markup;

Assert.Contains("Nothing to fix right now — no upstream fix is available for the 6 known issues in this image",
markup,
"The card must state that no upstream fix is available");
Assert.DoesNotContain("summary-remediation--warning", markup, "The neutral statement must not use the warning tone");
}
}

/// <summary>
/// Verify a failed assessment still renders the message of the vulnerability provider
/// </summary>
/// <returns>Task</returns>
[TestMethod]
public async Task VulnerabilityAssessmentCardFailedAssessmentRendersProviderMessage()
{
var testContext = BlazorTestContextFactory.Create();

await using (testContext)
{
var assessment = new VulnerabilityAssessmentViewData
{
Status = "Failed",
Source = "Trivy",
Message = "The vulnerability scan timed out",
};

var component = testContext.Render<VulnerabilityAssessmentCard>(parameters => parameters.Add(card => card.Assessment, assessment));

Assert.Contains("The vulnerability scan timed out", component.Markup, "A failed assessment must still render the provider message");
}
}

Expand Down Expand Up @@ -129,7 +197,8 @@ public async Task VulnerabilityAssessmentCardEmptyAssessmentOmitsOptionalLines()
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("summary-remediation", markup, "The card must omit the remediation statement when there are no active findings");
Assert.DoesNotContain("Nothing to fix right now", markup, "The card must omit the remediation statement when there are no active findings");
Assert.DoesNotContain("Checked ", markup, "The card must omit the check timestamp when it is not set");
}
}
Expand Down
Loading