namespace ZB.MOM.WW.ScadaBridge.InboundAPI.Tests; /// /// Tests for the script-trust-model checker. /// /// The delegates to the shared /// ScriptTrustValidator in ZB.MOM.WW.ScadaBridge.ScriptAnalysis. /// The unified policy differs from the old local deny-list in one notable way: /// System.Diagnostics is loosened to System.Diagnostics.Process /// only — Stopwatch, Debug, and other non-Process Diagnostics /// types are now ALLOWED. /// /// Hardening (reflection-gateway / dynamic / Activator) is /// preserved unchanged via the shared validator. /// /// Violation message wording is intentionally not asserted — callers should /// use .Count / .Any() rather than exact strings. /// public class ForbiddenApiCheckerTests { private static bool IsRejected(string script) => ForbiddenApiChecker.FindViolations(script).Count > 0; // --- Baseline: legitimate scripts must still pass --- /// Verifies that a legitimate script is not rejected. /// The permitted script source. [Theory] [InlineData("return 1 + 1;")] [InlineData("var list = new List { 1, 2, 3 }; return list.Sum();")] [InlineData("return Parameters.Get(\"x\") * 2;")] [InlineData("await Task.Delay(1); return null;")] [InlineData("var r = await Route.To(\"inst\").Call(\"s\"); return r;")] [InlineData("Action a = () => {}; a.Invoke(); return null;")] public void PermittedScript_NotRejected(string script) { Assert.False(IsRejected(script), script); } // --- System.Diagnostics loosening: Process is still forbidden; non-Process types // (Stopwatch, Debug) are now ALLOWED by the unified policy. The old // InboundAPI checker blocked the entire System.Diagnostics namespace. --- /// Verifies that System.Diagnostics.Process is still forbidden. [Fact] public void Diagnostics_Process_StillForbidden() { // System.Diagnostics.Process is an explicit forbidden scope in ScriptTrustPolicy. Assert.True(IsRejected("System.Diagnostics.Process.Start(\"/bin/sh\"); return null;")); } /// Verifies that Stopwatch from System.Diagnostics is now allowed. [Fact] public void Diagnostics_Stopwatch_NowAllowed() { // System.Diagnostics.Stopwatch is NOT under the forbidden scope // "System.Diagnostics.Process" — the unified policy only forbids // System.Diagnostics.Process, not the whole System.Diagnostics namespace. Assert.False(IsRejected("var sw = new System.Diagnostics.Stopwatch(); sw.Start(); return null;")); } /// Verifies that Debug from System.Diagnostics is now allowed. [Fact] public void Diagnostics_Debug_NowAllowed() { // System.Diagnostics.Debug is also not covered by the Process-only scope. Assert.False(IsRejected("System.Diagnostics.Debug.WriteLine(\"hi\"); return null;")); } // --- Baseline: forbidden namespaces (textual) must still be rejected --- /// Verifies that a script using a forbidden namespace is rejected. /// The script source that uses a forbidden namespace. [Theory] [InlineData("System.IO.File.Delete(\"/tmp/x\"); return null;")] [InlineData("using System.Reflection; return null;")] [InlineData("var s = new System.Net.Sockets.Socket(default, default, default); return null;")] public void ForbiddenNamespace_Rejected(string script) { Assert.True(IsRejected(script), script); } // --- Reflection reachable without a forbidden namespace token --- /// Verifies that reflection through assembly property access is rejected. [Fact] public void Reflection_AssemblyPropertyAccess_Rejected() { // typeof(string).Assembly — .Assembly is a reflection gateway off a permitted type. Assert.True(IsRejected("var a = typeof(string).Assembly; return null;")); } /// Verifies that reflection through Assembly.GetType is rejected. [Fact] public void Reflection_AssemblyGetType_Rejected() { // The classic bypass: obtain System.IO.File as a Type via a string literal. Assert.True(IsRejected( "var t = typeof(string).Assembly.GetType(\"System.IO.File\"); return null;")); } /// Verifies that reflection through Object.GetType is rejected. [Fact] public void Reflection_ObjectGetType_Rejected() { // x.GetType() returns a System.Type — a reflection gateway. Assert.True(IsRejected("var t = \"\".GetType(); return null;")); } /// Verifies that the static Type.GetType reflection entry point is rejected. [Fact] public void Reflection_TypeGetTypeStatic_Rejected() { Assert.True(IsRejected("var t = Type.GetType(\"System.IO.File\"); return null;")); } /// Verifies that Activator.CreateInstance is rejected. [Fact] public void Reflection_ActivatorCreateInstance_Rejected() { Assert.True(IsRejected( "var o = Activator.CreateInstance(Type.GetType(\"System.IO.File\")); return null;")); } /// Verifies that Type.InvokeMember is rejected. [Fact] public void Reflection_InvokeMember_Rejected() { Assert.True(IsRejected( "typeof(object).InvokeMember(\"x\", default, null, null, null); return null;")); } /// Verifies that GetMethod followed by Invoke is rejected. [Fact] public void Reflection_GetMethodInvoke_Rejected() { Assert.True(IsRejected( "var m = typeof(object).GetMethod(\"ToString\"); return null;")); } /// Verifies that GetTypeInfo is rejected. [Fact] public void Reflection_GetTypeInfo_Rejected() { Assert.True(IsRejected("var ti = \"\".GetType().GetTypeInfo(); return null;")); } /// Verifies that the dynamic keyword is rejected. [Fact] public void DynamicKeyword_Rejected() { // dynamic widens late-bound member access the static walker cannot see through. Assert.True(IsRejected("dynamic d = Parameters; return null;")); } // --- FindViolations returns a non-null, non-throwing result for edge inputs --- /// Verifies that an empty, whitespace or null script yields an empty violation list. /// The empty, whitespace or null script source. [Theory] [InlineData("")] [InlineData(" ")] [InlineData(null!)] public void EmptyOrWhitespace_ReturnsEmptyList(string? script) { var result = ForbiddenApiChecker.FindViolations(script!); Assert.NotNull(result); Assert.Empty(result); } }