Editor/UI/ConcernMapper.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox.SecBox.Bridge.Dto;
namespace Sandbox.SecBox.UI;
/// <summary>
/// Static utility that maps raw findings into human-readable concern categories.
/// Mapping is based on the RuleId prefix pattern.
/// </summary>
public static class ConcernMapper
{
// Category definitions: key → (statement, default icon)
private static readonly Dictionary<string, (string Statement, string DefaultIcon)> _categories = new()
{
{ "filesystem", ("This library wants to **read and write files**", "\u26a0\u00ef") },
{ "process", ("This library wants to **run programs**", "\u26a0\u00ef") },
{ "interop", ("This library wants to call **native system code**", "\u26a0\u00ef") },
{ "dynamicCode",("This library wants to **download and run code**", "\u26a0\u00ef") },
{ "rawNetwork", ("This library wants to **access the internet**", "\u2139\u00ef") },
{ "environment",("This library wants to **modify system settings**", "\u2139\u00ef") },
{ "unsafeCode", ("This library uses **unsafe memory operations**", "\u2139\u00ef") },
{ "other", ("This library contains patterns we cannot classify", "\u2139\u00ef") },
};
// RuleId prefix → category key mapping
private static readonly Dictionary<string, string> _prefixMap = new()
{
{ "critical.filesystem.", "filesystem" },
{ "critical.process.", "process" },
{ "critical.interop.", "interop" },
{ "critical.dynamicCode.", "dynamicCode" },
{ "critical.rawNetwork.", "rawNetwork" },
{ "critical.environment.", "environment" },
{ "critical.reflection.", "unsafeCode" },
};
// Severity ranking for comparison
private static readonly Dictionary<Severity, int> _severityRank = new()
{
{ Severity.Info, 0 },
{ Severity.Low, 1 },
{ Severity.Medium, 2 },
{ Severity.High, 3 },
{ Severity.Critical, 4 },
};
/// <summary>
/// Maps raw findings into concern categories.
/// Only categories with at least one finding are returned.
/// Sorted by severity (Critical first), then alphabetically by category key.
/// </summary>
public static Concern[] Map(Finding[] findings)
{
if (findings == null || findings.Length == 0)
return Array.Empty<Concern>();
// Group findings by category
var groups = new Dictionary<string, List<Finding>>();
foreach (var f in findings)
{
var category = ResolveCategory(f.RuleId);
if (!groups.TryGetValue(category, out var list))
{
list = new List<Finding>();
groups[category] = list;
}
list.Add(f);
}
// Build concerns
var concerns = new List<Concern>();
foreach (var kvp in groups)
{
var category = kvp.Key;
var list = kvp.Value;
if (!_categories.TryGetValue(category, out var def))
continue; // Should not happen
var highest = list.Max(f => _severityRank.GetValueOrDefault(f.Severity, 0));
var highestSeverity = _severityRank.FirstOrDefault(x => x.Value == highest).Key;
var ruleIds = list.Select(f => f.RuleId).Distinct().OrderBy(r => r).ToArray();
concerns.Add(new Concern
{
Category = category,
Statement = def.Statement,
FindingCount = list.Count,
HighestSeverity = highestSeverity,
RuleIds = ruleIds,
Selected = highestSeverity == Severity.Critical || highestSeverity == Severity.High,
});
}
// Sort: Critical first, then High, Medium, Low; within same severity, alphabetical by category
concerns.Sort((a, b) =>
{
var rankA = _severityRank.GetValueOrDefault(a.HighestSeverity, 0);
var rankB = _severityRank.GetValueOrDefault(b.HighestSeverity, 0);
if (rankA != rankB)
return rankB.CompareTo(rankA); // Descending severity
return string.Compare(a.Category, b.Category, StringComparison.Ordinal);
});
return concerns.ToArray();
}
/// <summary>
/// Resolves a RuleId to a category key based on prefix matching.
/// Falls back to "other" if no prefix matches.
/// </summary>
private static string ResolveCategory(string ruleId)
{
if (string.IsNullOrEmpty(ruleId))
return "other";
foreach (var kvp in _prefixMap)
{
if (ruleId.StartsWith(kvp.Key, StringComparison.OrdinalIgnoreCase))
return kvp.Value;
}
return "other";
}
/// <summary>
/// Returns the human-readable statement for a category key.
/// </summary>
public static string GetStatement(string categoryKey)
{
if (string.IsNullOrEmpty(categoryKey))
return "Unknown concern";
if (_categories.TryGetValue(categoryKey, out var def))
return def.Statement;
return "Unknown concern";
}
/// <summary>
/// Returns the icon for a category key based on whether it has Critical/High severity.
/// </summary>
public static string GetIcon(string categoryKey, Severity severity)
{
if (severity == Severity.Critical || severity == Severity.High)
return "\u26a0\u00ef"; // Warning
return "\u2139\u00ef"; // Info
}
/// <summary>
/// Helper for icon selection - returns true if the concern has Critical or High severity.
/// </summary>
public static bool HasCriticalOrHigh(Concern concern)
{
return concern?.HighestSeverity == Severity.Critical || concern?.HighestSeverity == Severity.High;
}
}