using ZB.MOM.WW.ScadaBridge.ScriptAnalysis; namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests; /// /// M3.1: adversarial + legitimate cases for the fused trust validator. Reject /// cases exercise the semantic pass (alias / using static / global::), the /// reflection-gateway hardening pass, and the namespace deny-list union; clean /// cases pin the allowed exceptions (Tasks, CancellationToken, Diagnostics /// other than Process). /// public class ScriptTrustValidatorTests { // ---- Reject (non-empty violations) -------------------------------------- [Fact] public void Rejects_SystemIo_Using() { var code = "using System.IO; var x = File.ReadAllText(\"/etc/passwd\");"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_SystemIo_GlobalQualified() { var code = "var x = global::System.IO.File.ReadAllText(\"x\");"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_SystemIo_Aliased() { var code = "using IO = System.IO; var f = IO.File.ReadAllText(\"x\");"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_SystemIo_UsingStatic() { var code = "using static System.IO.File; var s = ReadAllText(\"x\");"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_ReflectionGateway_OffPermittedType() { var code = "var t = typeof(string).Assembly.GetType(\"System.IO.File\");"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_Dynamic_Keyword() { var code = "dynamic d = 5; d.Foo();"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_Activator_CreateInstance() { var code = "var o = Activator.CreateInstance(typeof(string));"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_RuntimeInteropServices() { var code = "using System.Runtime.InteropServices; var h = Marshal.SizeOf();"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_MicrosoftWin32() { var code = "using Microsoft.Win32; var k = Registry.LocalMachine;"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_Threading_Thread_Sleep() { var code = "System.Threading.Thread.Sleep(10);"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_All_SystemNet() { var code = "System.Net.WebClient w = null;"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_ReflectionGateway_OffAllowedTaskType() { // Guards the reflection-first ordering: a gateway member hung off an // allowed System.Threading.Tasks type must still be rejected even though // the chain's namespace prefix is an allowed exception. var code = "var a = System.Threading.Tasks.Task.CompletedTask.GetType().Assembly;"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_ForbiddenIo_NestedInAllowedTaskRunLambda() { // A forbidden System.IO reference buried inside an allowed Task.Run lambda. // The allowed-exception prefix on the outer member access must NOT shadow // the nested forbidden reference — Pass 2 must descend into the lambda. var code = "await System.Threading.Tasks.Task.Run(() => System.IO.File.ReadAllText(\"x\"));"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_ForbiddenMutex_AsGenericArg_UnderAllowedTasksPrefix() { // System.Threading.Mutex (forbidden) appears as a generic argument of an // allowed System.Threading.Tasks.TaskCompletionSource. The allowed // outer name must not shadow the forbidden generic arg. var code = "System.Threading.Tasks.TaskCompletionSource tcs = null;"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_DirectThreadingMutex_NotThreadSleep() { // A direct forbidden System.Threading type (not Thread.Sleep) — pins that // the broad System.Threading deny-list catches more than the one cased test. var code = "var m = new System.Threading.Mutex();"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_ForbiddenFileInfo_AsGenericArg() { // System.IO.FileInfo (forbidden) as a generic argument of an allowed // System.Collections.Generic.List. var code = "System.Collections.Generic.List x = null;"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_NameOf_ForbiddenType() { // Conservative fail-safe: naming a forbidden type inside nameof(...) is // deliberately flagged (a script has no business naming it even there). var code = "var s = nameof(System.IO.File);"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_Process_QualifiedType() { var code = "var p = System.Diagnostics.Process.Start(\"x\");"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Rejects_Process_BareIdentifier_ViaUsing() { // System.Diagnostics is an ALLOWED namespace (Stopwatch/Debug ok), so the // using directive is not flagged; Process is a forbidden TYPE reached as a // bare identifier. This pins whether FindViolations resolves it. var code = "using System.Diagnostics; var p = Process.Start(\"x\");"; Assert.NotEmpty(ScriptTrustValidator.FindViolations(code)); } // ---- Clean (empty violations) ------------------------------------------- [Fact] public void Allows_TasksDelay() { var code = "await System.Threading.Tasks.Task.Delay(1);"; Assert.Empty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Allows_DiagnosticsStopwatch_NotProcess() { var code = "var sw = System.Diagnostics.Stopwatch.StartNew();"; Assert.Empty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Allows_CancellationTokenSource() { var code = "var c = new System.Threading.CancellationTokenSource();"; Assert.Empty(ScriptTrustValidator.FindViolations(code)); } [Fact] public void Allows_LinqAndMath() { var code = "var n = System.Linq.Enumerable.Range(0,3).Sum(); var m = System.Math.Max(1,2);"; Assert.Empty(ScriptTrustValidator.FindViolations(code)); } }