security card component that shows all information of the current saved audit like activation state, policy and meta data

This commit is contained in:
nilsk 2026-03-31 14:44:30 +02:00
parent 75407a8e10
commit 5f94428204
2 changed files with 343 additions and 0 deletions

View File

@ -0,0 +1,191 @@
@using AIStudio.Agents.AssistantAudit
@inherits MSGComponentBase
@if (this.Plugin is not null)
{
var state = this.SecurityState;
<div class="d-flex">
<MudTooltip Text="@state.ActionLabel" Placement="Placement.Top">
<MudIconButton Icon="@state.BadgeIcon"
Color="@state.AuditColor"
Size="@(this.Compact ? Size.Small : Size.Medium)"
OnClick="@this.ToggleSecurityCard" />
</MudTooltip>
<MudPopover Open="@this.showSecurityCard"
AnchorOrigin="Origin.BottomRight"
TransformOrigin="Origin.BottomLeft"
OverflowBehavior="OverflowBehavior.FlipAlways"
DropShadow="@true"
Class="border-solid border-4 rounded-lg"
Style="@this.GetPopoverStyle()">
<MudCard Elevation="2" Outlined Style="max-width: min(42rem, 90vw);">
<MudCardHeader>
<CardHeaderAvatar>
<MudAvatar Color="@state.AuditColor" Variant="Variant.Filled" Size="Size.Large">
<MudIcon Icon="@state.AuditIcon" Size="Size.Medium" />
</MudAvatar>
</CardHeaderAvatar>
<CardHeaderContent>
<div class="d-flex align-center gap-2">
<MudText Typo="Typo.h6">@T("Assistant Security")</MudText>
<MudChip T="string" Size="Size.Small" Variant="Variant.Filled" Color="@state.AuditColor">
@state.AuditLabel
</MudChip>
@if (!string.IsNullOrWhiteSpace(state.AvailabilityLabel))
{
<MudChip T="string" Size="Size.Small" Variant="Variant.Outlined" Color="@state.AvailabilityColor" Icon="@state.AvailabilityIcon">
@state.AvailabilityLabel
</MudChip>
}
</div>
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@state.Headline
</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudTooltip Text="@T("Show or hide the detailed security information.")">
<MudIconButton Icon="@Icons.Material.Filled.ExpandMore" OnClick="@this.ToggleDetails" />
</MudTooltip>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent Class="pt-0 pb-2">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="4" Class="flex-wrap">
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Small" />
<MudText Typo="Typo.body2">@T("Confidence"):</MudText>
<MudProgressLinear Color="@state.AuditColor"
Value="@this.GetConfidencePercentage()"
Rounded="@true"
Size="Size.Medium"
Style="width: 80px; min-width: 80px;" />
<MudText Typo="Typo.caption" Class="mud-text-secondary">
@this.GetConfidenceLabel()
</MudText>
</MudStack>
<MudDivider Vertical="@true" FlexItem="@true" />
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.BugReport" Size="Size.Small" Color="@state.AuditColor" />
<MudText Typo="Typo.body2">@this.GetFindingSummary()</MudText>
</MudStack>
<MudDivider Vertical="@true" FlexItem="@true" />
<MudStack Row="true" AlignItems="AlignItems.Center" Spacing="1">
<MudIcon Icon="@Icons.Material.Filled.Schedule" Size="Size.Small" />
<MudText Typo="Typo.body2" Class="mud-text-secondary">
@this.GetAuditTimestampLabel()
</MudText>
</MudStack>
</MudStack>
</MudCardContent>
<MudCollapse Expanded="@this.showDetails">
<MudDivider />
<MudCardContent>
<MudStack Spacing="3">
<MudAlert Severity="@this.GetStatusSeverity()" Variant="Variant.Outlined" Dense="@true">
@state.Description
</MudAlert>
<MudSimpleTable Dense="@true" Hover="@true" Bordered="@true" Striped="@true" Style="overflow-x: auto;">
<tbody>
<tr>
<td style="width: 180px;">
<MudText Typo="Typo.body2"><b>@T("Plugin ID")</b></MudText>
</td>
<td><code style="font-size: 0.8rem;">@this.Plugin.Id</code></td>
</tr>
<tr>
<td>
<MudText Typo="Typo.body2"><b>@T("Current hash")</b></MudText>
</td>
<td><code style="font-size: 0.8rem;">@GetShortHash(state.CurrentHash)</code></td>
</tr>
@if (state.Audit is not null)
{
<tr>
<td>
<MudText Typo="Typo.body2"><b>@T("Audit hash")</b></MudText>
</td>
<td><code style="font-size: 0.8rem;">@GetShortHash(state.Audit.PluginHash)</code></td>
</tr>
<tr>
<td>
<MudText Typo="Typo.body2"><b>@T("Audit provider")</b></MudText>
</td>
<td><MudText Typo="Typo.body2">@this.GetAuditProviderLabel()</MudText></td>
</tr>
<tr>
<td>
<MudText Typo="Typo.body2"><b>@T("Audited at")</b></MudText>
</td>
<td><MudText Typo="Typo.body2">@this.FormatFileTimestamp(state.Audit.AuditedAtUtc.ToLocalTime().DateTime)</MudText></td>
</tr>
<tr>
<td>
<MudText Typo="Typo.body2"><b>@T("Audit level")</b></MudText>
</td>
<td><MudText Typo="Typo.body2">@state.AuditLabel</MudText></td>
</tr>
<tr>
<td>
<MudText Typo="Typo.body2"><b>@T("Availability")</b></MudText>
</td>
<td><MudText Typo="Typo.body2">@state.AvailabilityLabel</MudText></td>
</tr>
}
<tr>
<td>
<MudText Typo="Typo.body2"><b>@T("Required minimum")</b></MudText>
</td>
<td><MudText Typo="Typo.body2">@state.Settings.MinimumLevel.GetName()</MudText></td>
</tr>
</tbody>
</MudSimpleTable>
@if (state.Audit is null)
{
<MudAlert Severity="Severity.Info" Variant="Variant.Text" Dense="@true">
@T("No stored audit details are available yet.")
</MudAlert>
}
else if (state.Audit.Findings.Count == 0)
{
<MudAlert Severity="Severity.Success" Variant="Variant.Text" Dense="@true">
@T("No security findings were stored for this assistant plugin.")
</MudAlert>
}
else
{
<MudStack Spacing="2">
@foreach (var finding in state.Audit.Findings)
{
<MudAlert Severity="@finding.Severity.GetSeverity()" Variant="Variant.Text" Dense="@true">
<strong>@finding.Category</strong><span>: @finding.Description</span>
@if (!string.IsNullOrWhiteSpace(finding.Location))
{
<div>
<MudText Typo="Typo.caption">@finding.Location</MudText>
</div>
}
</MudAlert>
}
</MudStack>
}
</MudStack>
</MudCardContent>
</MudCollapse>
<MudCardActions>
<MudButton Variant="Variant.Outlined" Color="Color.Primary" OnClick="@this.OpenAuditDialogAsync">
@state.ActionLabel
</MudButton>
<MudButton Variant="Variant.Text" OnClick="@this.HideSecurityCard">
@T("Close")
</MudButton>
</MudCardActions>
</MudCard>
</MudPopover>
</div>
}

View File

@ -0,0 +1,152 @@
using System.Globalization;
using AIStudio.Dialogs;
using AIStudio.Tools.PluginSystem.Assistants;
using Microsoft.AspNetCore.Components;
using DialogOptions = AIStudio.Dialogs.DialogOptions;
namespace AIStudio.Components;
public partial class AssistantPluginSecurityCard : MSGComponentBase
{
[Parameter]
public PluginAssistants? Plugin { get; set; }
[Parameter]
public bool Compact { get; set; }
[Inject]
private IDialogService DialogService { get; init; } = null!;
private PluginAssistantSecurityState SecurityState => this.Plugin is null
? new PluginAssistantSecurityState()
: PluginAssistantSecurityResolver.Resolve(this.SettingsManager, this.Plugin);
private CultureInfo currentCultureInfo = CultureInfo.InvariantCulture;
private bool showSecurityCard;
private bool showDetails;
protected override async Task OnInitializedAsync()
{
var activeLanguagePlugin = await this.SettingsManager.GetActiveLanguagePlugin();
this.currentCultureInfo = CommonTools.DeriveActiveCultureOrInvariant(activeLanguagePlugin.IETFTag);
this.showDetails = !this.Compact;
this.ApplyFilters([], [ Event.CONFIGURATION_CHANGED, Event.PLUGINS_RELOADED ]);
await base.OnInitializedAsync();
}
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
}
return base.OnAfterRenderAsync(firstRender);
}
private async Task OpenAuditDialogAsync()
{
if (this.Plugin is null)
return;
var parameters = new DialogParameters<AssistantPluginAuditDialog>
{
{ x => x.PluginId, this.Plugin.Id },
};
var dialog = await this.DialogService.ShowAsync<AssistantPluginAuditDialog>(this.T("Assistant Audit"), parameters, DialogOptions.FULLSCREEN);
var result = await dialog.Result;
if (result is null || result.Canceled || result.Data is not AssistantPluginAuditDialogResult auditResult)
return;
if (auditResult.Audit is not null)
UpsertAudit(this.SettingsManager.ConfigurationData.AssistantPluginAudits, auditResult.Audit);
if (auditResult.ActivatePlugin && !this.SettingsManager.ConfigurationData.EnabledPlugins.Contains(this.Plugin.Id))
this.SettingsManager.ConfigurationData.EnabledPlugins.Add(this.Plugin.Id);
await this.SettingsManager.StoreSettings();
await this.SendMessage(Event.CONFIGURATION_CHANGED, true);
}
protected override Task ProcessIncomingMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
{
if (triggeredEvent is Event.CONFIGURATION_CHANGED or Event.PLUGINS_RELOADED)
return this.InvokeAsync(this.StateHasChanged);
return Task.CompletedTask;
}
private void ToggleSecurityCard() => this.showSecurityCard = !this.showSecurityCard;
private void HideSecurityCard() => this.showSecurityCard = false;
private void ToggleDetails() => this.showDetails = !this.showDetails;
private static void UpsertAudit(List<PluginAssistantAudit> audits, PluginAssistantAudit audit)
{
var existingIndex = audits.FindIndex(x => x.PluginId == audit.PluginId);
if (existingIndex >= 0)
audits[existingIndex] = audit;
else
audits.Add(audit);
}
private string FormatFileTimestamp(DateTime timestamp) => CommonTools.FormatTimestampToGeneral(timestamp, this.currentCultureInfo);
private string GetPopoverStyle() => $"border-color: {this.GetStatusBorderColor()};";
private double GetConfidencePercentage()
{
var confidence = this.SecurityState.Audit?.Confidence ?? 0f;
if (confidence <= 1)
confidence *= 100;
return Math.Clamp(confidence, 0, 100);
}
private string GetConfidenceLabel() => $"{this.GetConfidencePercentage():0}%";
private string GetFindingSummary()
{
var count = this.SecurityState.Audit?.Findings.Count ?? 0;
return string.Format(this.T("{0} Finding(s)"), count);
}
private string GetAuditTimestampLabel()
{
var auditedAt = this.SecurityState.Audit?.AuditedAtUtc;
return auditedAt is null
? this.T("No audit yet")
: this.FormatFileTimestamp(auditedAt.Value.ToLocalTime().DateTime);
}
private string GetAuditProviderLabel()
{
var providerName = this.SecurityState.Audit?.AuditProviderName;
return string.IsNullOrWhiteSpace(providerName) ? this.T("Unknown") : providerName;
}
private static string GetShortHash(string hash)
{
if (string.IsNullOrWhiteSpace(hash) || hash.Length <= 16)
return hash;
return $"{hash[..8]}...{hash[^8..]}";
}
private Severity GetStatusSeverity() => this.SecurityState.AuditColor switch
{
Color.Success => Severity.Success,
Color.Warning => Severity.Warning,
Color.Error => Severity.Error,
_ => Severity.Info,
};
private string GetStatusBorderColor() => this.SecurityState.AuditColor switch
{
Color.Success => "var(--mud-palette-success)",
Color.Warning => "var(--mud-palette-warning)",
Color.Error => "var(--mud-palette-error)",
_ => "var(--mud-palette-info)",
};
}