Editor/Lifecycle/BootAudit.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Editor;
using Sandbox.SecBox.Bridge;
using Sandbox.SecBox.Bridge.Dto;
using DiagnosticsLog = Sandbox.SecBox.Bridge.DiagnosticsLog;
namespace Sandbox.SecBox.Lifecycle;
// Walks LibrarySystem.All, scans every library that doesn't already have a
// TrustAlways or Block decision recorded for its current content hash.
// Hits Secbox.Core via SecboxCoreClient.
public static class BootAudit
{
static bool ranThisSession;
[Event("editor.created")]
public static void OnEditorCreated(object _)
{
if (ranThisSession) return;
ranThisSession = true;
Run();
}
public static void Run()
{
DiagnosticsLog.Trace("BootAudit.Run: begin");
try { RunImpl(); }
catch (Exception ex) { DiagnosticsLog.Error("boot audit threw", ex); }
DiagnosticsLog.Trace("BootAudit.Run: end");
}
// Manual "Scan now" path. Unlike RunImpl this ignores the ScanOnBoot policy
// (the user asked for it explicitly) and scans every in-scope library
// regardless of an existing decision, so the results dialog can show findings
// for all of them - including ones already trusted or blocked. Trust entries
// are refreshed (counts + ReviewedAt) but existing decisions are preserved.
public static List<LibraryScanResult> ScanAllLibraries()
{
var results = new List<LibraryScanResult>();
var projectRoot = PackageLocator.CurrentProjectRoot();
if (string.IsNullOrEmpty(projectRoot))
{
DiagnosticsLog.Warn("manual scan: no current project - abort");
return results;
}
var store = TrustStore.Load(projectRoot);
var libraries = LibrarySystem.All?.ToList();
if (libraries == null || libraries.Count == 0)
{
DiagnosticsLog.Info("manual scan: no libraries to scan");
return results;
}
try
{
Task.Run(() => SecboxCoreClient.EnsureReadyAsync()).GetAwaiter().GetResult();
}
catch (Exception ex)
{
DiagnosticsLog.Error("manual scan: core load failed - abort", ex);
return results;
}
// Only scan paths under the open project's Libraries/ folder.
var librariesRoot = System.IO.Path.GetFullPath(System.IO.Path.Combine(projectRoot, "Libraries"))
.TrimEnd(System.IO.Path.DirectorySeparatorChar)
+ System.IO.Path.DirectorySeparatorChar;
bool anyFlagged = false;
foreach (var lib in libraries)
{
string ident = null;
string libRootPath = null;
try
{
var proj = lib?.Project;
ident = proj?.Package?.FullIdent ?? proj?.Package?.Ident;
libRootPath = proj?.RootDirectory?.FullName;
}
catch (Exception ex)
{
DiagnosticsLog.Warn($"manual scan: failed to introspect library: {ex.Message}");
continue;
}
if (string.IsNullOrEmpty(ident)) continue;
// Skip secbox itself (sbproj presence is ground truth, see RunImpl).
if (!string.IsNullOrEmpty(libRootPath)
&& System.IO.File.Exists(System.IO.Path.Combine(libRootPath, "secbox.sbproj")))
continue;
if (string.IsNullOrEmpty(libRootPath) || !System.IO.Directory.Exists(libRootPath)) continue;
var fullLibPath = System.IO.Path.GetFullPath(libRootPath);
if (!fullLibPath.StartsWith(librariesRoot, StringComparison.OrdinalIgnoreCase)) continue;
var hash = PackageHasher.HashFolder(libRootPath);
var existing = store.Find(hash);
var result = new LibraryScanResult
{
PackageIdent = ident,
Folder = libRootPath,
ContentHash = hash,
Decision = existing?.Decision ?? Decision.NotReviewed,
};
DiagnosticsLog.Info($"manual scan: scanning {ident} at {libRootPath}");
try
{
var report = Task.Run(() => SecboxCoreClient.ScanFolder(libRootPath)).GetAwaiter().GetResult();
result.Findings = report.Findings ?? new List<Finding>();
}
catch (Exception ex)
{
result.Error = ex.Message;
DiagnosticsLog.Warn($"manual scan: scan of {ident} failed: {ex.Message}");
results.Add(result);
continue;
}
result.CriticalCount = result.Findings.Count(f => f.Severity == Severity.Critical);
result.HighCount = result.Findings.Count(f => f.Severity == Severity.High);
result.MediumCount = result.Findings.Count(f => f.Severity == Severity.Medium);
result.LowCount = result.Findings.Count(f => f.Severity == Severity.Low);
DiagnosticsLog.Info($"manual scan: {ident}: Critical={result.CriticalCount} High={result.HighCount} Medium={result.MediumCount} Low={result.LowCount}");
var maxSeverity = result.Findings.Count == 0 ? Severity.Info : result.Findings.Max(f => f.Severity);
if (maxSeverity >= store.Policy.PromptThreshold)
{
store.Upsert(new TrustEntry
{
PackageIdent = ident,
Version = existing?.Version,
ContentHash = hash,
Decision = existing?.Decision ?? Decision.NotReviewed,
ReviewedAt = DateTime.UtcNow,
CriticalCount = result.CriticalCount,
HighCount = result.HighCount,
MediumCount = result.MediumCount,
LowCount = result.LowCount,
Notes = "Manual scan. First 5 findings:\n"
+ string.Join("\n", result.Findings.Take(5).Select(f => " " + f)),
});
anyFlagged = true;
}
results.Add(result);
}
if (anyFlagged) store.Save();
DiagnosticsLog.Info($"manual scan done: {results.Count} library result(s)");
return results;
}
static void RunImpl()
{
var projectRoot = PackageLocator.CurrentProjectRoot();
if (string.IsNullOrEmpty(projectRoot))
{
DiagnosticsLog.Warn("boot audit: no current project - abort");
return;
}
DiagnosticsLog.Trace($"boot audit: projectRoot={projectRoot}");
var store = TrustStore.Load(projectRoot);
if (!store.Policy.ScanOnBoot)
{
DiagnosticsLog.Info("boot audit: ScanOnBoot policy is OFF - abort");
return;
}
var libraries = LibrarySystem.All?.ToList();
if (libraries == null || libraries.Count == 0)
{
DiagnosticsLog.Info("boot audit: LibrarySystem.All is empty - no libraries to scan");
return;
}
DiagnosticsLog.Info($"boot audit: walking {libraries.Count} library projects");
try
{
Task.Run(() => SecboxCoreClient.EnsureReadyAsync()).GetAwaiter().GetResult();
}
catch (Exception ex)
{
DiagnosticsLog.Error("boot audit: core load failed - abort", ex);
return;
}
int newlyFlagged = 0;
int reviewedSkipped = 0;
int scanned = 0;
int skippedNoIdent = 0;
int skippedSelf = 0;
int skippedNoFolder = 0;
int skippedNotInLibraries = 0;
foreach (var lib in libraries)
{
string ident = null;
string libRootPath = null;
try
{
// LibraryProject.Project is a public property - use it directly.
var proj = lib?.Project;
ident = proj?.Package?.FullIdent ?? proj?.Package?.Ident;
libRootPath = proj?.RootDirectory?.FullName;
}
catch (Exception ex)
{
DiagnosticsLog.Warn($"boot audit: failed to introspect library: {ex.Message}");
continue;
}
if (string.IsNullOrEmpty(ident))
{
DiagnosticsLog.Trace($"boot audit: skip lib with empty ident (path={libRootPath ?? "<null>"})");
skippedNoIdent++;
continue;
}
// Self-skip is content-based: don't rely on ident shape. Engine's
// Package.FormatIdent emits "{org}.{ident}#local" for local packages
// (e.g. "f4industries.secbox#local"), which no prefix filter would
// reliably catch across forks/renames. The sbproj presence is the
// ground truth.
if (!string.IsNullOrEmpty(libRootPath)
&& System.IO.File.Exists(System.IO.Path.Combine(libRootPath, "secbox.sbproj")))
{
DiagnosticsLog.Trace($"boot audit: skip secbox-own ({ident}) at {libRootPath}");
skippedSelf++;
continue;
}
if (string.IsNullOrEmpty(libRootPath) || !System.IO.Directory.Exists(libRootPath))
{
DiagnosticsLog.Trace($"boot audit: skip {ident} - no folder on disk (path={libRootPath ?? "<null>"})");
skippedNoFolder++;
continue;
}
// Defence-in-depth: only scan paths under the open project's Libraries/.
var libRoot = System.IO.Path.GetFullPath(System.IO.Path.Combine(projectRoot, "Libraries"))
.TrimEnd(System.IO.Path.DirectorySeparatorChar)
+ System.IO.Path.DirectorySeparatorChar;
var fullLibPath = System.IO.Path.GetFullPath(libRootPath);
if (!fullLibPath.StartsWith(libRoot, StringComparison.OrdinalIgnoreCase))
{
DiagnosticsLog.Trace($"boot audit: skip {ident} - not under {libRoot} (was {fullLibPath})");
skippedNotInLibraries++;
continue;
}
var hash = PackageHasher.HashFolder(libRootPath);
var existing = store.Find(hash);
if (existing != null && existing.Decision is Decision.TrustAlways or Decision.Block)
{
DiagnosticsLog.Trace($"boot audit: skip {ident} - already decided ({existing.Decision})");
reviewedSkipped++;
continue;
}
DiagnosticsLog.Info($"boot audit: scanning {ident} at {libRootPath}");
ScanReport report;
try
{
report = Task.Run(() => SecboxCoreClient.ScanFolder(libRootPath)).GetAwaiter().GetResult();
}
catch (Exception ex)
{
DiagnosticsLog.Warn($"boot audit: scan of {ident} failed: {ex.Message}");
continue;
}
scanned++;
var critical = report.Findings.Count(f => f.Severity == Severity.Critical);
var high = report.Findings.Count(f => f.Severity == Severity.High);
var medium = report.Findings.Count(f => f.Severity == Severity.Medium);
var low = report.Findings.Count(f => f.Severity == Severity.Low);
DiagnosticsLog.Info($"boot audit: {ident}: Critical={critical} High={high} Medium={medium} Low={low}");
var maxSeverity = report.Findings.Count == 0 ? Severity.Info : report.Findings.Max(f => f.Severity);
if (maxSeverity >= store.Policy.PromptThreshold)
{
store.Upsert(new TrustEntry
{
PackageIdent = ident,
Version = null,
ContentHash = hash,
Decision = existing?.Decision ?? Decision.NotReviewed,
ReviewedAt = DateTime.UtcNow,
CriticalCount = critical,
HighCount = high,
MediumCount = medium,
LowCount = low,
Notes = $"Boot audit. First 5 findings:\n"
+ string.Join("\n", report.Findings.Take(5).Select(f => " " + f)),
});
newlyFlagged++;
}
}
if (newlyFlagged > 0) store.Save();
DiagnosticsLog.Info(
$"boot audit done: total={libraries.Count} scanned={scanned} newlyFlagged={newlyFlagged} "
+ $"alreadyDecided={reviewedSkipped} "
+ $"skippedNoIdent={skippedNoIdent} skippedSelf={skippedSelf} "
+ $"skippedNoFolder={skippedNoFolder} skippedNotInLibraries={skippedNotInLibraries}");
}
}