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
19 changes: 19 additions & 0 deletions src/DockerUpdateGuard/Components/Pages/Dashboard.razor
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ else
</MudItem>
</MudGrid>

@if (_dashboard.VulnerabilityConfigurationHint == VulnerabilityConfigurationHintKind.ScanningDisabled)
{
<MudAlert Severity="Severity.Info" Dense="true">
Vulnerability scanning is disabled. Set <code>DockerUpdateGuard:Vulnerabilities:Enabled</code> to <code>true</code> and configure a provider (<code>Trivy</code> or <code>DockerScout</code>) to see security findings.
</MudAlert>
}
else if (_dashboard.VulnerabilityConfigurationHint == VulnerabilityConfigurationHintKind.TrivyBaseUrlMissing)
{
<MudAlert Severity="Severity.Warning" Dense="true">
The Trivy provider reported that it is not configured. Set <code>DockerUpdateGuard:Vulnerabilities:TrivyBaseUrl</code> to the address of your Trivy server.
</MudAlert>
}
else if (_dashboard.VulnerabilityConfigurationHint == VulnerabilityConfigurationHintKind.DockerHubCredentialsMissing)
{
<MudAlert Severity="Severity.Warning" Dense="true">
The Docker Scout provider reported that it is not configured. Set <code>DockerUpdateGuard:DockerHub:UserName</code> and <code>DockerUpdateGuard:DockerHub:Pat</code> to valid Docker Hub credentials.
</MudAlert>
}

<MudGrid Spacing="2">
<MudItem xs="12" lg="4">
<MudPaper Class="section-paper section-paper--accent" Elevation="0">
Expand Down
36 changes: 21 additions & 15 deletions src/DockerUpdateGuard/Components/Pages/ScanHistory.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,29 @@
</div>
@if (VulnerabilitiesEnabled)
{
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Disabled="@_isRefreshingVulnerabilities"
OnClick="TriggerVulnerabilityRefreshAsync">
@if (_isRefreshingVulnerabilities)
<MudStack Spacing="1" AlignItems="AlignItems.End">
<MudButton Variant="Variant.Filled"
Color="Color.Primary"
Disabled="@_isRefreshingVulnerabilities"
OnClick="TriggerVulnerabilityRefreshAsync">
@if (_isRefreshingVulnerabilities)
{
<MudProgressCircular Class="me-2"
Size="Size.Small"
Indeterminate="true"
Color="Color.Inherit" />
<span>Refreshing vulnerabilities...</span>
}
else
{
<span>Refresh vulnerabilities</span>
}
</MudButton>
@if (_schedule?.VulnerabilityScanningEnabled == true)
{
<MudProgressCircular Class="me-2"
Size="Size.Small"
Indeterminate="true"
Color="Color.Inherit" />
<span>Refreshing vulnerabilities...</span>
<MudText Typo="Typo.caption">Next scheduled vulnerability refresh: @FormatNextVulnerabilityRefresh(_schedule)</MudText>
}
else
{
<span>Refresh vulnerabilities</span>
}
</MudButton>
</MudStack>
}
</div>

Expand Down
36 changes: 36 additions & 0 deletions src/DockerUpdateGuard/Components/Pages/ScanHistory.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ public sealed partial class ScanHistory : IDisposable
/// </summary>
private const string ScanHistoryStateKey = "ScanHistory.List";

/// <summary>
/// Persistent-state key for the prerendered vulnerability refresh schedule
/// </summary>
private const string ScheduleStateKey = "ScanHistory.Schedule";

#endregion // Constants

#region Fields
Expand All @@ -36,6 +41,11 @@ public sealed partial class ScanHistory : IDisposable
/// </summary>
private IReadOnlyList<ScanHistoryItemData>? _scans;

/// <summary>
/// Estimated vulnerability refresh schedule
/// </summary>
private VulnerabilityScheduleViewData? _schedule;

/// <summary>
/// Current error message
/// </summary>
Expand Down Expand Up @@ -106,6 +116,18 @@ private static Color GetScanStatusColor(string status)
};
}

/// <summary>
/// Format the estimated next scheduled vulnerability refresh
/// </summary>
/// <param name="schedule">Vulnerability refresh schedule</param>
/// <returns>Formatted schedule value</returns>
private static string FormatNextVulnerabilityRefresh(VulnerabilityScheduleViewData schedule)
{
return schedule.NextVulnerabilityRefreshUtc is null
? "shortly (no run recorded yet)"
: schedule.NextVulnerabilityRefreshUtc.GetValueOrDefault().ToLocalTime().ToString("g");
}

#endregion // Static methods

#region Methods
Expand All @@ -118,10 +140,13 @@ private async Task LoadAsync()
{
var scans = await ViewService.GetScanHistoryAsync()
.ConfigureAwait(false);
var schedule = await ViewService.GetVulnerabilityScheduleAsync()
.ConfigureAwait(false);

await InvokeAsync(() =>
{
_scans = scans;
_schedule = schedule;

StateHasChanged();
}).ConfigureAwait(false);
Expand All @@ -138,6 +163,11 @@ private Task PersistScanHistory()
PersistentState.PersistAsJson(ScanHistoryStateKey, _scans);
}

if (_schedule is not null)
{
PersistentState.PersistAsJson(ScheduleStateKey, _schedule);
}

return Task.CompletedTask;
}

Expand Down Expand Up @@ -196,6 +226,12 @@ protected override async Task OnInitializedAsync()
{
_persistingSubscription = PersistentState.RegisterOnPersisting(PersistScanHistory);

if (PersistentState.TryTakeFromJson<VulnerabilityScheduleViewData>(ScheduleStateKey, out var restoredSchedule)
&& restoredSchedule is not null)
{
_schedule = restoredSchedule;
}

if (PersistentState.TryTakeFromJson<IReadOnlyList<ScanHistoryItemData>>(ScanHistoryStateKey, out var restoredScans)
&& restoredScans is not null)
{
Expand Down
84 changes: 84 additions & 0 deletions src/DockerUpdateGuard/UI/ApplicationViewService.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using DockerUpdateGuard.Configuration;
using DockerUpdateGuard.Data;
using DockerUpdateGuard.Data.Entities;
using DockerUpdateGuard.Data.Queries;
Expand All @@ -6,6 +7,7 @@
using DockerUpdateGuard.Images.Interfaces;

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;

namespace DockerUpdateGuard.UI;

Expand Down Expand Up @@ -46,6 +48,11 @@ public sealed class ApplicationViewService : IApplicationViewService, IDisposabl
/// </summary>
private readonly IImageReferenceParser _imageReferenceParser;

/// <summary>
/// Application options monitor
/// </summary>
private readonly IOptionsMonitor<DockerUpdateGuardOptions> _optionsMonitor;

/// <summary>
/// Shared-base-image query service
/// </summary>
Expand All @@ -60,13 +67,16 @@ public sealed class ApplicationViewService : IApplicationViewService, IDisposabl
/// </summary>
/// <param name="dbContext">Database context</param>
/// <param name="imageReferenceParser">Image reference parser</param>
/// <param name="optionsMonitor">Application options monitor</param>
/// <param name="sharedBaseImageQueryService">Shared base image query service</param>
public ApplicationViewService(DockerUpdateGuardDbContext dbContext,
IImageReferenceParser imageReferenceParser,
IOptionsMonitor<DockerUpdateGuardOptions> optionsMonitor,
ISharedBaseImageQueryService sharedBaseImageQueryService)
{
_dbContext = dbContext;
_imageReferenceParser = imageReferenceParser;
_optionsMonitor = optionsMonitor;
_sharedBaseImageQueryService = sharedBaseImageQueryService;
}

Expand Down Expand Up @@ -956,6 +966,71 @@ private async Task<IReadOnlyList<ScanHistoryItemData>> GetScanHistoryCoreAsync(i
.ToList();
}

/// <summary>
/// Resolve the vulnerability configuration hint without re-entering the service gate
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Configuration hint kind</returns>
private async Task<VulnerabilityConfigurationHintKind> GetVulnerabilityConfigurationHintCoreAsync(CancellationToken cancellationToken)
{
var vulnerabilityOptions = _optionsMonitor.CurrentValue.Vulnerabilities;

if (vulnerabilityOptions.Enabled == false)
{
return VulnerabilityConfigurationHintKind.ScanningDisabled;
}

var latestRun = await _dbContext.ScanRuns.Where(entity => entity.Type == ScanRunType.Vulnerability
&& entity.CompletedAtUtc != null)
.OrderByDescending(entity => entity.StartedAtUtc)
.Select(entity => new
{
entity.Status,
entity.ErrorMessage,
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);

return VulnerabilityConfigurationHintResolver.Resolve(vulnerabilityOptions, latestRun?.Status, latestRun?.ErrorMessage);
}

/// <summary>
/// Estimate the next scheduled vulnerability refresh without re-entering the service gate
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Vulnerability schedule view data</returns>
private async Task<VulnerabilityScheduleViewData> GetVulnerabilityScheduleCoreAsync(CancellationToken cancellationToken)
{
var options = _optionsMonitor.CurrentValue;

if (options.Vulnerabilities.Enabled == false)
{
return new VulnerabilityScheduleViewData();
}

var lastStartedAtUtc = await _dbContext.ScanRuns.Where(entity => entity.Type == ScanRunType.Vulnerability)
.OrderByDescending(entity => entity.StartedAtUtc)
.Select(entity => (DateTimeOffset?)entity.StartedAtUtc)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);

if (lastStartedAtUtc is null)
{
return new VulnerabilityScheduleViewData
{
VulnerabilityScanningEnabled = true,
};
}

var refreshInterval = TimeSpan.FromMinutes(options.Scanning.VulnerabilityRefreshIntervalMinutes);

return new VulnerabilityScheduleViewData
{
VulnerabilityScanningEnabled = true,
NextVulnerabilityRefreshUtc = lastStartedAtUtc.GetValueOrDefault() + refreshInterval,
};
}

/// <summary>
/// Map a linked runtime container projection
/// </summary>
Expand Down Expand Up @@ -1698,6 +1773,7 @@ public async Task<DashboardViewData> GetDashboardAsync(CancellationToken cancell
.ToListAsync(cancellationToken)
.ConfigureAwait(false);
var vulnerabilitySeveritySummary = CreateSeveritySummary(activeVulnerabilitySeverityCounts.Select(entity => new KeyValuePair<VulnerabilitySeverity, int>(entity.Severity, entity.ActiveFindingCount)));
var vulnerabilityConfigurationHint = await GetVulnerabilityConfigurationHintCoreAsync(cancellationToken).ConfigureAwait(false);

return new DashboardViewData
{
Expand All @@ -1710,6 +1786,8 @@ public async Task<DashboardViewData> GetDashboardAsync(CancellationToken cancell
OwnImageBaseRuntimeWarningCount = ownImageBaseRuntimeWarningCount,
ActiveVulnerabilityFindingCount = vulnerabilitySeveritySummary.TotalCount,
VulnerabilitySeveritySummary = vulnerabilitySeveritySummary,
VulnerabilityScanningEnabled = _optionsMonitor.CurrentValue.Vulnerabilities.Enabled,
VulnerabilityConfigurationHint = vulnerabilityConfigurationHint,
RecentScans = recentScans,
};
},
Expand Down Expand Up @@ -2097,6 +2175,12 @@ public async Task<IReadOnlyList<ScanHistoryItemData>> GetScanHistoryAsync(int ta
return await ExecuteSerializedAsync(() => GetScanHistoryCoreAsync(take, cancellationToken), cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public async Task<VulnerabilityScheduleViewData> GetVulnerabilityScheduleAsync(CancellationToken cancellationToken = default)
{
return await ExecuteSerializedAsync(() => GetVulnerabilityScheduleCoreAsync(cancellationToken), cancellationToken).ConfigureAwait(false);
}

/// <inheritdoc/>
public async Task<IReadOnlyList<VulnerabilityOverviewItemData>> GetVulnerabilityOverviewAsync(CancellationToken cancellationToken = default)
{
Expand Down
10 changes: 10 additions & 0 deletions src/DockerUpdateGuard/UI/DashboardViewData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@ public class DashboardViewData
/// </summary>
public VulnerabilitySeveritySummaryViewData VulnerabilitySeveritySummary { get; set; } = new();

/// <summary>
/// Indicates whether vulnerability scanning is enabled
/// </summary>
public bool VulnerabilityScanningEnabled { get; set; }

/// <summary>
/// Configuration hint that should be surfaced for the vulnerability setup
/// </summary>
public VulnerabilityConfigurationHintKind VulnerabilityConfigurationHint { get; set; }

/// <summary>
/// Recent scan list
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/DockerUpdateGuard/UI/IApplicationViewService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ public interface IApplicationViewService
/// <returns>Scan history entries</returns>
Task<IReadOnlyList<ScanHistoryItemData>> GetScanHistoryAsync(int take = 20, CancellationToken cancellationToken = default);

/// <summary>
/// Read the estimated vulnerability refresh schedule for the scan-history page
/// </summary>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Vulnerability schedule view data</returns>
Task<VulnerabilityScheduleViewData> GetVulnerabilityScheduleAsync(CancellationToken cancellationToken = default);

/// <summary>
/// Read the fleet-wide vulnerability overview grouped by advisory
/// </summary>
Expand Down
27 changes: 27 additions & 0 deletions src/DockerUpdateGuard/UI/VulnerabilityConfigurationHintKind.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace DockerUpdateGuard.UI;

/// <summary>
/// Configuration hint surfaced when vulnerability scanning cannot produce findings
/// </summary>
public enum VulnerabilityConfigurationHintKind
{
/// <summary>
/// Nothing to report
/// </summary>
None = 0,

/// <summary>
/// Vulnerability scanning is switched off
/// </summary>
ScanningDisabled = 1,

/// <summary>
/// The Trivy provider is selected but its base URL is missing
/// </summary>
TrivyBaseUrlMissing = 2,

/// <summary>
/// The Docker Scout provider is selected but the Docker Hub credentials are missing
/// </summary>
DockerHubCredentialsMissing = 3
}
Loading
Loading