Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ScriptTrustPolicy.cs
T

197 lines
7.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.IO;
using System.Reflection;
using Microsoft.CodeAnalysis;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
/// <summary>
/// M3.1: the single authoritative source of truth for the ScadaBridge script
/// trust model. Previously the forbidden-API deny-list, allowed exceptions,
/// reflection-gateway member names, default metadata references, and default
/// imports were duplicated (and disagreed) across four call sites — the
/// SiteRuntime <c>ScriptCompilationService</c>, the InboundAPI
/// <c>ForbiddenApiChecker</c>, and the design-time deploy gate. This class
/// fuses them into one collection set that <see cref="ScriptTrustValidator"/>
/// and <see cref="RoslynScriptCompiler"/> consume; the four consumers delegate
/// here in later tasks (M3.2M3.5).
///
/// <para>
/// The deny-list is intentionally the UNION of the two existing
/// implementations — it forbids <c>System.Diagnostics.Process</c> (not all of
/// <c>System.Diagnostics</c>, so <c>Stopwatch</c> stays allowed), all of
/// <c>System.Net</c>, all of <c>System.Threading</c> except Tasks /
/// CancellationToken(Source), plus <c>System.Reflection</c>,
/// <c>System.Runtime.InteropServices</c>, and <c>Microsoft.Win32</c>.
/// </para>
/// </summary>
public static class ScriptTrustPolicy
{
/// <summary>
/// Forbidden API roots. Each entry is matched as a prefix against the
/// resolved symbol's containing namespace and fully-qualified containing
/// type — an entry may name a whole namespace ("System.IO") or a single
/// type ("System.Diagnostics.Process").
/// </summary>
public static readonly string[] ForbiddenScopes =
[
"System.IO",
"System.Diagnostics.Process",
"System.Threading",
"System.Reflection",
"System.Net",
"System.Runtime.InteropServices",
"Microsoft.Win32",
];
/// <summary>
/// Specific namespaces/types allowed even though they sit under a forbidden
/// root. async/await and cancellation tokens are OK despite
/// <c>System.Threading</c> being blocked.
/// </summary>
public static readonly string[] AllowedExceptions =
[
"System.Threading.Tasks",
"System.Threading.CancellationToken",
"System.Threading.CancellationTokenSource",
];
/// <summary>
/// Member names that are reflection gateways. Reaching any of these — even
/// off a permitted type such as <c>typeof(string)</c> — lets a script
/// escape the namespace deny-list (obtain an arbitrary <c>Type</c>, load an
/// assembly, late-bind a method). They are rejected regardless of the
/// receiver expression.
/// </summary>
public static readonly HashSet<string> ReflectionGatewayMembers = new(StringComparer.Ordinal)
{
"GetType",
"GetTypeInfo",
"Assembly",
"Module",
"CreateInstance",
"InvokeMember",
"GetMethod",
"GetMethods",
"GetConstructor",
"GetConstructors",
"GetField",
"GetFields",
"GetProperty",
"GetProperties",
"GetMember",
"GetMembers",
"GetRuntimeMethod",
"GetRuntimeMethods",
"MethodHandle",
"TypeHandle",
};
/// <summary>
/// Bare identifiers that are forbidden outright. <c>dynamic</c> widens
/// late-bound member access the static walker cannot see through;
/// <c>Activator</c> has no non-reflection use.
/// </summary>
public static readonly HashSet<string> ForbiddenIdentifiers = new(StringComparer.Ordinal)
{
"dynamic",
"Activator",
};
/// <summary>
/// Assemblies referenced by compiled scripts. Shared between the Roslyn
/// scripting options and the semantic-analysis compilation built for trust
/// validation, so the validator resolves symbols against exactly the same
/// metadata the script is compiled against.
/// </summary>
public static readonly IReadOnlyList<Assembly> DefaultAssemblies =
[
typeof(object).Assembly,
typeof(System.Linq.Enumerable).Assembly,
typeof(System.Math).Assembly,
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly,
typeof(DynamicJsonElement).Assembly,
];
/// <summary>
/// Metadata references for the trust-validation semantic compilation and
/// the design-time script compilation.
/// </summary>
public static readonly IReadOnlyList<MetadataReference> DefaultReferences =
DefaultAssemblies
.Select(a => (MetadataReference)MetadataReference.CreateFromFile(a.Location))
.ToList();
/// <summary>
/// The full trusted-platform reference set used ONLY by
/// <see cref="ScriptTrustValidator"/>'s semantic analysis — NOT by
/// <see cref="RoslynScriptCompiler"/>. Unlike <see cref="DefaultReferences"/>
/// (the minimal, runtime-fidelity set used to decide script <i>validity</i>,
/// which must mirror exactly what the site runtime compiles against), the
/// trust validator references the entire framework so that EVERY type a
/// script names resolves to a real symbol and is judged by its true
/// namespace. Without this, a forbidden TYPE that sits inside an ALLOWED
/// namespace and is reached as a bare identifier — the only such case in the
/// policy being <c>System.Diagnostics.Process</c> via
/// <c>using System.Diagnostics;</c> — would not resolve against a minimal
/// reference set and would slip past the semantic pass (still blocked
/// downstream as an undefined-symbol compile error, but with a misleading
/// message). Referencing the full framework lets the validator flag it
/// authoritatively as a forbidden API. Enriching the analysis reference set
/// can only IMPROVE detection — the verdict is by namespace/type, so more
/// resolvable symbols means more correct verdicts, never a false allow.
/// </summary>
public static readonly IReadOnlyList<MetadataReference> AnalysisReferences = BuildAnalysisReferences();
private static IReadOnlyList<MetadataReference> BuildAnalysisReferences()
{
var byPath = new Dictionary<string, MetadataReference>(StringComparer.OrdinalIgnoreCase);
// Trusted platform assemblies = the full framework reference set the host
// started with; lets the semantic pass resolve any BCL type.
if (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") is string tpa)
{
foreach (var path in tpa.Split(Path.PathSeparator))
{
if (path.Length == 0 ||
!path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) ||
byPath.ContainsKey(path) ||
!File.Exists(path))
{
continue;
}
try { byPath[path] = MetadataReference.CreateFromFile(path); }
catch { /* skip an unreadable assembly rather than fail validation */ }
}
}
// Ensure app assemblies the script API surface needs are present even if
// not in the TPA list (e.g. Commons / DynamicJsonElement).
foreach (var asm in DefaultAssemblies)
{
var loc = asm.Location;
if (loc.Length == 0 || byPath.ContainsKey(loc) || !File.Exists(loc))
continue;
try { byPath[loc] = MetadataReference.CreateFromFile(loc); }
catch { /* ignore */ }
}
// Fallback to the minimal set if the TPA list was unavailable (e.g. a
// single-file/AOT host) so validation still functions.
return byPath.Count > 0 ? byPath.Values.ToList() : DefaultReferences;
}
/// <summary>
/// Default namespace imports made available to compiled scripts.
/// </summary>
public static readonly string[] DefaultImports =
[
"System",
"System.Collections.Generic",
"System.Linq",
"System.Threading.Tasks",
];
}