fix(security): /raw credential banner keeps running findings when the ConfigDb scan fails (#847)

This commit is contained in:
Joseph Doherty
2026-11-11 03:42:01 -05:00
parent 038aa007ac
commit c6c243cc06
3 changed files with 151 additions and 35 deletions
@@ -8,9 +8,11 @@
@* lmxopcua#815 upgrade preflight surface: stored / deployed configs whose credential references sit outside their
driver's namespace (the boot scan, re-runnable here) plus running drivers that reported resolving one through the
deprecated warn-only compatibility path (driver-health diagnostics, fleet-wide). Never shows a value. *@
deprecated warn-only compatibility path (driver-health diagnostics, fleet-wide). Never shows a value.
The two sources are independent (lmxopcua#847): a failed ConfigDb scan shows a note, and the running findings
— read from the in-process health store — still render. See CredentialReferenceBannerModel. *@
@if (_findings.Count > 0 || _error is not null)
@if (_model.Visible)
{
<div class="alert alert-warning small mb-3" role="alert">
<div class="d-flex justify-content-between align-items-start gap-2">
@@ -22,61 +24,93 @@
</div>
<button class="btn btn-sm btn-outline-secondary" disabled="@_busy" @onclick="RecheckAsync">Re-check</button>
</div>
@if (_error is not null)
@if (_model.ScanError is not null)
{
<div class="text-danger mt-1">@_error</div>
<div class="text-danger mt-1" data-testid="credential-scan-error">
@_model.ScanError Stored and deployed configs were not checked
@(_model.ScanFindings.Count > 0 ? "(the list below is from the last successful scan)" : "")
— running-driver findings below are still live.
</div>
}
<ul class="mb-0 mt-2">
@foreach (var f in _findings.Take(MaxRows))
{
<li>
<span class="badge bg-secondary me-1">@f.Source</span>
@if (!string.IsNullOrEmpty(f.DriverType)) { <span>@f.DriverType </span> }
driver <span class="mono">@f.DriverName</span>
@if (f.DeviceName is not null) { <span> device <span class="mono">@f.DeviceName</span></span> }
— @f.Problem
</li>
}
</ul>
@if (_findings.Count > MaxRows)
@if (_model.ScanFindings.Count > 0)
{
<div class="mt-1 text-muted">…and @(_findings.Count - MaxRows) more (see the node log).</div>
<ul class="mb-0 mt-2">
@foreach (var f in _model.ScanFindings.Take(MaxRows))
{
<li>
<span class="badge bg-secondary me-1">@f.Source</span>
@if (!string.IsNullOrEmpty(f.DriverType)) { <span>@f.DriverType </span> }
driver <span class="mono">@f.DriverName</span>
@if (f.DeviceName is not null) { <span> device <span class="mono">@f.DeviceName</span></span> }
— @f.Problem
</li>
}
</ul>
@if (_model.ScanFindings.Count > MaxRows)
{
<div class="mt-1 text-muted">…and @(_model.ScanFindings.Count - MaxRows) more (see the node log).</div>
}
}
@if (_model.RunningFindings.Count > 0)
{
<ul class="mb-0 mt-2" data-testid="credential-running-findings">
@foreach (var f in _model.RunningFindings.Take(MaxRows))
{
<li>
<span class="badge bg-secondary me-1">@f.Source</span>
driver <span class="mono">@f.DriverName</span>
— @f.Problem
</li>
}
</ul>
@if (_model.RunningFindings.Count > MaxRows)
{
<div class="mt-1 text-muted">…and @(_model.RunningFindings.Count - MaxRows) more running.</div>
}
}
</div>
}
@code {
private const int MaxRows = 25;
private IReadOnlyList<CredentialReferenceFinding> _findings = [];
private string? _error;
private CredentialReferenceBannerModel _model = new([], [], null, null);
private bool _busy;
protected override void OnInitialized() => Compose(State.Current);
private void Compose(CredentialReferencePreflightState.Snapshot snapshot)
{
_error = snapshot.Error;
_findings = snapshot.Findings
.Concat(CredentialReferencePreflight.RunningFindings(DriverStatus.GetAll()))
.ToList();
}
=> _model = CredentialReferenceBannerModel.Compose(snapshot, DriverStatus.GetAll());
private async Task RecheckAsync()
{
var factory = Services.GetService(typeof(IDbContextFactory<OtOpcUaConfigDbContext>)) as IDbContextFactory<OtOpcUaConfigDbContext>;
if (factory is null) return;
_busy = true;
try
{
var findings = await new CredentialReferencePreflight(factory).ScanAsync(CancellationToken.None);
var snapshot = new CredentialReferencePreflightState.Snapshot(findings, DateTime.UtcNow, null);
State.Set(snapshot);
var factory = Services.GetService(typeof(IDbContextFactory<OtOpcUaConfigDbContext>)) as IDbContextFactory<OtOpcUaConfigDbContext>;
CredentialReferencePreflightState.Snapshot snapshot;
if (factory is null)
{
snapshot = State.Current with { Error = "No config DB on this node." };
}
else
{
try
{
var findings = await new CredentialReferencePreflight(factory).ScanAsync(CancellationToken.None);
snapshot = new CredentialReferencePreflightState.Snapshot(findings, DateTime.UtcNow, null);
State.Set(snapshot);
}
catch (Exception)
{
// Keep the last successful scan's findings (flagged as such by the note) — and, above all,
// recompose the RUNNING findings from the live health store rather than leaving whatever the
// last render had (lmxopcua#847).
snapshot = State.Current with { Error = "The credential-reference preflight could not read the config DB." };
}
}
Compose(snapshot);
}
catch (Exception)
{
_error = "The credential-reference preflight could not read the config DB.";
}
finally
{
_busy = false;