Editor/UI/ScanResultsWindow.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Editor;
using Sandbox.SecBox.Bridge.Dto;
using Sandbox.SecBox.Lifecycle;
using DiagnosticsLog = Sandbox.SecBox.Bridge.DiagnosticsLog;
namespace Sandbox.SecBox.UI;
// Results dialog for the manual "Scan now" menu action. Shows one card per
// library: a plain-language list of what the library can do (spawn processes,
// read and write files, call native code, ...), its finding counts, current
// trust decision, and a Review button that opens the full ReviewDialog.
//
// Opened in a "scanning" state by MenuItems.ScanNow, then populated via
// SetResults once BootAudit.ScanAllLibraries finishes off the UI thread.
public sealed class ScanResultsWindow : BaseWindow
{
static ScanResultsWindow _instance;
Label _subtitle;
ScrollArea _scroll;
const string CssH1 = "font-size: 18px; font-weight: 700; color: #ffffff;";
const string CssSubtle = "color: #9aa0a6; font-size: 11px;";
const string CssCard = "background-color: #2b2b2f; border-radius: 4px; padding: 10px 12px;";
const string CssIdent = "color: #ffffff; font-size: 14px; font-weight: 700;";
const string CssCounts = "color: #c5cad1; font-size: 11px; font-family: monospace;";
const string CssBody = "color: #e8eaee; font-size: 12px;";
const string CssDim = "color: #9aa0a6; font-size: 11px;";
const string CssError = "color: #ef9a9a; font-size: 12px;";
const string CssChip = "color: white; padding: 3px 9px; border-radius: 10px; font-size: 11px; font-weight: 700;";
public static ScanResultsWindow OpenScanning()
{
if (_instance == null)
_instance = new ScanResultsWindow();
try { _instance.Raise(); } catch { }
_instance.ShowScanning();
_instance.Show();
return _instance;
}
ScanResultsWindow() : base()
{
DeleteOnClose = true;
Size = new Vector2(720, 640);
WindowTitle = "secbox - scan results";
SetWindowIcon("radar");
Layout = Layout.Column();
Layout.Margin = 16;
Layout.Spacing = 12;
BuildHeader();
_scroll = Layout.Add(new ScrollArea(this), 1);
_scroll.Canvas = new Widget(_scroll);
_scroll.Canvas.Layout = Layout.Column();
_scroll.Canvas.Layout.Margin = 2;
_scroll.Canvas.Layout.Spacing = 8;
BuildFooter();
}
void BuildHeader()
{
var col = Layout.AddColumn();
col.Spacing = 4;
var title = new Label("Scan results");
title.SetStyles(CssH1);
col.Add(title);
_subtitle = new Label("");
_subtitle.SetStyles(CssSubtle);
_subtitle.WordWrap = true;
col.Add(_subtitle);
}
void BuildFooter()
{
var row = Layout.AddRow();
row.Spacing = 8;
var manage = new Button("Manage trust...");
manage.Icon = "verified_user";
manage.Clicked = () => { try { TrustManagerWindow.Open(); } catch { } };
row.Add(manage);
row.AddStretchCell();
var close = new Button.Primary("Close");
close.Clicked = () => Close();
row.Add(close);
}
void ShowScanning()
{
_subtitle.Text = "Scanning libraries...";
var layout = _scroll?.Canvas?.Layout;
if (layout == null) return;
layout.Clear(true);
var msg = new Label("Scanning installed libraries. This can take a few seconds...");
msg.SetStyles(CssBody);
msg.WordWrap = true;
layout.Add(msg);
layout.AddStretchCell();
}
// Called on the main thread once the scan completes. Null means the scan
// could not run (no project / core load failure).
public void SetResults(List<LibraryScanResult> results)
{
var layout = _scroll?.Canvas?.Layout;
if (layout == null) return;
layout.Clear(true);
if (results == null)
{
_subtitle.Text = "Scan could not run.";
var msg = new Label("The scan did not run. Make sure a project is open and Secbox.Core loaded, then try again. See secbox > Dev > Open Diagnostics Log.");
msg.SetStyles(CssBody);
msg.WordWrap = true;
layout.Add(msg);
layout.AddStretchCell();
return;
}
var flagged = results.Count(r => r.TotalFindings > 0);
var clean = results.Count(r => !r.HasError && r.TotalFindings == 0);
var errored = results.Count(r => r.HasError);
_subtitle.Text = $"{results.Count} library(ies) scanned - {flagged} with findings, {clean} clean"
+ (errored > 0 ? $", {errored} failed" : "");
if (results.Count == 0)
{
var msg = new Label("No libraries in scope. Install a library under Libraries/ and scan again.");
msg.SetStyles(CssBody);
msg.WordWrap = true;
layout.Add(msg);
layout.AddStretchCell();
return;
}
// Worst-first: errors, then by severity, then most findings.
var ordered = results
.OrderByDescending(r => r.HasError)
.ThenByDescending(r => (int)r.MaxSeverity)
.ThenByDescending(r => r.TotalFindings)
.ThenBy(r => r.PackageIdent, StringComparer.OrdinalIgnoreCase);
foreach (var r in ordered)
layout.Add(BuildResultCard(r));
layout.AddStretchCell();
}
Widget BuildResultCard(LibraryScanResult r)
{
var card = new Widget();
card.Layout = Layout.Column();
card.Layout.Spacing = 4;
card.Layout.Margin = 0;
card.SetStyles(CssCard);
// Header: ident + status chip + (decision chip when there are findings).
var headerRow = card.Layout.AddRow();
headerRow.Spacing = 8;
var ident = new Label(string.IsNullOrEmpty(r.PackageIdent) ? "(unknown package)" : r.PackageIdent);
ident.SetStyles(CssIdent);
ident.TextSelectable = true;
headerRow.Add(ident);
headerRow.AddStretchCell();
if (r.HasError)
{
headerRow.Add(Chip("Scan failed", "#e53935"));
}
else if (r.TotalFindings == 0)
{
headerRow.Add(Chip("Clean", "#43a047"));
}
else
{
headerRow.Add(Chip(r.MaxSeverity.ToString(), SeverityHex(r.MaxSeverity)));
headerRow.Add(Chip(DecisionLabel(r.Decision), DecisionHex(r.Decision)));
}
if (r.HasError)
{
var err = new Label($"Could not scan this library: {r.Error}");
err.SetStyles(CssError);
err.WordWrap = true;
card.Layout.Add(err);
return card;
}
if (r.TotalFindings == 0)
{
var ok = new Label("Nothing risky detected. This library stays inside the engine sandbox.");
ok.SetStyles(CssDim);
ok.WordWrap = true;
card.Layout.Add(ok);
return card;
}
// Counts.
var counts = new Label($"Crit={r.CriticalCount} High={r.HighCount} Med={r.MediumCount} Low={r.LowCount}");
counts.SetStyles(CssCounts);
card.Layout.Add(counts);
// Plain-language "what it can do" list, worst-severity first.
card.Layout.AddSpacingCell(2);
var what = new Label("What this library can do:");
what.SetStyles(CssDim);
card.Layout.Add(what);
foreach (var capability in Capabilities(r.Findings))
{
var line = new Label("- " + capability);
line.SetStyles(CssBody);
line.WordWrap = true;
card.Layout.Add(line);
}
// Action row.
card.Layout.AddSpacingCell(4);
var actionRow = card.Layout.AddRow();
actionRow.Spacing = 6;
actionRow.AddStretchCell();
var review = new Button("Review...");
review.Icon = "policy";
review.Clicked = () => OpenReview(r);
actionRow.Add(review);
return card;
}
// Distinct plain-language capability titles, worst-severity first. Built from
// the same FindingTranslator dictionary the review window uses.
static IEnumerable<string> Capabilities(List<Finding> findings)
{
var worstByTitle = new Dictionary<string, Severity>(StringComparer.Ordinal);
foreach (var f in findings)
{
var title = FindingTranslator.Translate(f.RuleId).Title;
if (string.IsNullOrEmpty(title)) continue;
if (!worstByTitle.TryGetValue(title, out var sev) || f.Severity > sev)
worstByTitle[title] = f.Severity;
}
return worstByTitle
.OrderByDescending(kv => (int)kv.Value)
.ThenBy(kv => kv.Key, StringComparer.Ordinal)
.Select(kv => kv.Key)
.Take(8);
}
void OpenReview(LibraryScanResult r)
{
try
{
var root = PackageLocator.CurrentProjectRoot();
if (string.IsNullOrEmpty(root))
{
EditorUtility.DisplayDialog("secbox", "No current project.");
return;
}
var store = TrustStore.Load(root);
ReviewDialog.Show(r.PackageIdent, r.ContentHash, r.Findings, store);
}
catch (Exception ex)
{
DiagnosticsLog.Error($"[secbox] scan results: open review for {r.PackageIdent} failed", ex);
EditorUtility.DisplayDialog("secbox", $"Could not open review: {ex.Message}");
}
}
Label Chip(string text, string hex)
{
var chip = new Label(text);
chip.SetStyles($"{CssChip} background-color: {hex};");
return chip;
}
static string SeverityHex(Severity s) => s switch
{
Severity.Critical => "#e53935",
Severity.High => "#fb8c00",
Severity.Medium => "#fdd835",
Severity.Low => "#90a4ae",
_ => "#607d8b",
};
static string DecisionLabel(Decision d) => d switch
{
Decision.TrustAlways => "Trusted",
Decision.AllowOnce => "Allowed (session)",
Decision.NotReviewed => "Not reviewed",
Decision.Block => "Blocked",
Decision.Quarantine => "Quarantined",
_ => d.ToString(),
};
static string DecisionHex(Decision d) => d switch
{
Decision.TrustAlways => "#43a047",
Decision.AllowOnce => "#29b6f6",
Decision.NotReviewed => "#90a4ae",
Decision.Block => "#e53935",
Decision.Quarantine => "#fb8c00",
_ => "#607d8b",
};
public override void OnDestroyed()
{
if (_instance == this) _instance = null;
base.OnDestroyed();
}
}