using System.IO;
using System.Reflection;
using Microsoft.CodeAnalysis;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
///
/// 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 ScriptCompilationService, the InboundAPI
/// ForbiddenApiChecker, and the design-time deploy gate. This class
/// fuses them into one collection set that
/// and consume; the four consumers delegate
/// here in later tasks (M3.2–M3.5).
///
///
/// The deny-list is intentionally the UNION of the two existing
/// implementations — it forbids System.Diagnostics.Process (not all of
/// System.Diagnostics, so Stopwatch stays allowed), all of
/// System.Net, all of System.Threading except Tasks /
/// CancellationToken(Source), plus System.Reflection,
/// System.Runtime.InteropServices, and Microsoft.Win32.
///
///
public static class ScriptTrustPolicy
{
///
/// 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").
///
public static readonly string[] ForbiddenScopes =
[
"System.IO",
"System.Diagnostics.Process",
"System.Threading",
"System.Reflection",
"System.Net",
"System.Runtime.InteropServices",
"Microsoft.Win32",
];
///
/// Specific namespaces/types allowed even though they sit under a forbidden
/// root. async/await and cancellation tokens are OK despite
/// System.Threading being blocked.
///
public static readonly string[] AllowedExceptions =
[
"System.Threading.Tasks",
"System.Threading.CancellationToken",
"System.Threading.CancellationTokenSource",
];
///
/// Member names that are reflection gateways. Reaching any of these — even
/// off a permitted type such as typeof(string) — lets a script
/// escape the namespace deny-list (obtain an arbitrary Type, load an
/// assembly, late-bind a method). They are rejected regardless of the
/// receiver expression.
///
public static readonly HashSet ReflectionGatewayMembers = new(StringComparer.Ordinal)
{
"GetType",
"GetTypeInfo",
"Assembly",
"Module",
"CreateInstance",
"InvokeMember",
"GetMethod",
"GetMethods",
"GetConstructor",
"GetConstructors",
"GetField",
"GetFields",
"GetProperty",
"GetProperties",
"GetMember",
"GetMembers",
"GetRuntimeMethod",
"GetRuntimeMethods",
"MethodHandle",
"TypeHandle",
};
///
/// Bare identifiers that are forbidden outright. dynamic widens
/// late-bound member access the static walker cannot see through;
/// Activator has no non-reflection use.
///
public static readonly HashSet ForbiddenIdentifiers = new(StringComparer.Ordinal)
{
"dynamic",
"Activator",
};
///
/// 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.
///
public static readonly IReadOnlyList DefaultAssemblies =
[
typeof(object).Assembly,
typeof(System.Linq.Enumerable).Assembly,
typeof(System.Math).Assembly,
typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly,
typeof(DynamicJsonElement).Assembly,
];
///
/// Metadata references for the trust-validation semantic compilation and
/// the design-time script compilation.
///
public static readonly IReadOnlyList DefaultReferences =
DefaultAssemblies
.Select(a => (MetadataReference)MetadataReference.CreateFromFile(a.Location))
.ToList();
///
/// The full trusted-platform reference set used ONLY by
/// 's semantic analysis — NOT by
/// . Unlike
/// (the minimal, runtime-fidelity set used to decide script validity,
/// 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 System.Diagnostics.Process via
/// using System.Diagnostics; — 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.
///
public static readonly IReadOnlyList AnalysisReferences = BuildAnalysisReferences();
private static IReadOnlyList BuildAnalysisReferences()
{
var byPath = new Dictionary(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;
}
///
/// Default namespace imports made available to compiled scripts.
///
public static readonly string[] DefaultImports =
[
"System",
"System.Collections.Generic",
"System.Linq",
"System.Threading.Tasks",
];
}