using Microsoft.Extensions.Logging.Abstractions; using ScadaLink.SiteRuntime.Scripts; namespace ScadaLink.SiteRuntime.Tests.Scripts; /// /// WP-6 (Phase 8): Script sandboxing verification. /// Adversarial tests that verify forbidden APIs are blocked at compilation time. /// public class SandboxTests { private readonly ScriptCompilationService _service; public SandboxTests() { _service = new ScriptCompilationService(NullLogger.Instance); } // ── System.IO forbidden ── [Fact] public void Sandbox_FileRead_Blocked() { var result = _service.Compile("evil", """System.IO.File.ReadAllText("/etc/passwd")"""); Assert.False(result.IsSuccess); Assert.Contains(result.Errors, e => e.Contains("System.IO")); } [Fact] public void Sandbox_FileWrite_Blocked() { var result = _service.Compile("evil", """System.IO.File.WriteAllText("/tmp/hack.txt", "pwned")"""); Assert.False(result.IsSuccess); } [Fact] public void Sandbox_DirectoryCreate_Blocked() { var result = _service.Compile("evil", """System.IO.Directory.CreateDirectory("/tmp/evil")"""); Assert.False(result.IsSuccess); } [Fact] public void Sandbox_FileStream_Blocked() { var result = _service.Compile("evil", """new System.IO.FileStream("/tmp/x", System.IO.FileMode.Create)"""); Assert.False(result.IsSuccess); } [Fact] public void Sandbox_StreamReader_Blocked() { var result = _service.Compile("evil", """new System.IO.StreamReader("/tmp/x")"""); Assert.False(result.IsSuccess); } // ── Process forbidden ── [Fact] public void Sandbox_ProcessStart_Blocked() { var result = _service.Compile("evil", """System.Diagnostics.Process.Start("cmd.exe", "/c dir")"""); Assert.False(result.IsSuccess); Assert.Contains(result.Errors, e => e.Contains("Process")); } [Fact] public void Sandbox_ProcessStartInfo_Blocked() { var code = """ var psi = new System.Diagnostics.Process(); psi.StartInfo.FileName = "bash"; """; var result = _service.Compile("evil", code); Assert.False(result.IsSuccess); } // ── Threading forbidden (except Tasks/CancellationToken) ── [Fact] public void Sandbox_ThreadCreate_Blocked() { var result = _service.Compile("evil", """new System.Threading.Thread(() => {}).Start()"""); Assert.False(result.IsSuccess); Assert.Contains(result.Errors, e => e.Contains("System.Threading")); } [Fact] public void Sandbox_Mutex_Blocked() { var result = _service.Compile("evil", """new System.Threading.Mutex()"""); Assert.False(result.IsSuccess); } [Fact] public void Sandbox_Semaphore_Blocked() { var result = _service.Compile("evil", """new System.Threading.Semaphore(1, 1)"""); Assert.False(result.IsSuccess); } [Fact] public void Sandbox_TaskDelay_Allowed() { // async/await and Tasks are explicitly allowed var violations = _service.ValidateTrustModel("await System.Threading.Tasks.Task.Delay(100)"); Assert.Empty(violations); } [Fact] public void Sandbox_CancellationToken_Allowed() { var violations = _service.ValidateTrustModel( "var ct = System.Threading.CancellationToken.None;"); Assert.Empty(violations); } [Fact] public void Sandbox_CancellationTokenSource_Allowed() { var violations = _service.ValidateTrustModel( "var cts = new System.Threading.CancellationTokenSource();"); Assert.Empty(violations); } // ── Reflection forbidden ── [Fact] public void Sandbox_GetType_Reflection_Blocked() { var result = _service.Compile("evil", """typeof(string).GetMethods(System.Reflection.BindingFlags.NonPublic)"""); Assert.False(result.IsSuccess); } [Fact] public void Sandbox_AssemblyLoad_Blocked() { var result = _service.Compile("evil", """System.Reflection.Assembly.Load("System.Runtime")"""); Assert.False(result.IsSuccess); } [Fact] public void Sandbox_ActivatorCreateInstance_ViaReflection_Blocked() { var result = _service.Compile("evil", """System.Reflection.Assembly.GetExecutingAssembly()"""); Assert.False(result.IsSuccess); } // ── Raw network forbidden ── [Fact] public void Sandbox_TcpClient_Blocked() { var result = _service.Compile("evil", """new System.Net.Sockets.TcpClient("evil.com", 80)"""); Assert.False(result.IsSuccess); Assert.Contains(result.Errors, e => e.Contains("System.Net.Sockets")); } [Fact] public void Sandbox_UdpClient_Blocked() { var result = _service.Compile("evil", """new System.Net.Sockets.UdpClient(1234)"""); Assert.False(result.IsSuccess); } [Fact] public void Sandbox_HttpClient_Blocked() { var result = _service.Compile("evil", """new System.Net.Http.HttpClient()"""); Assert.False(result.IsSuccess); Assert.Contains(result.Errors, e => e.Contains("System.Net.Http")); } [Fact] public void Sandbox_HttpRequestMessage_Blocked() { var result = _service.Compile("evil", """new System.Net.Http.HttpRequestMessage(System.Net.Http.HttpMethod.Get, "https://evil.com")"""); Assert.False(result.IsSuccess); } // ── Allowed operations ── [Fact] public void Sandbox_BasicMath_Allowed() { var result = _service.Compile("safe", "Math.Max(1, 2)"); Assert.True(result.IsSuccess); } [Fact] public void Sandbox_LinqOperations_Allowed() { var result = _service.Compile("safe", "new List { 1, 2, 3 }.Where(x => x > 1).Sum()"); Assert.True(result.IsSuccess); } [Fact] public void Sandbox_StringOperations_Allowed() { var result = _service.Compile("safe", """string.Join(", ", new[] { "a", "b", "c" })"""); Assert.True(result.IsSuccess); } [Fact] public void Sandbox_DateTimeOperations_Allowed() { var result = _service.Compile("safe", "DateTime.UtcNow.AddHours(1).ToString(\"o\")"); Assert.True(result.IsSuccess); } // ── Execution timeout ── [Fact] public async Task Sandbox_InfiniteLoop_CancelledByToken() { // Compile a script that loops forever var code = """ while (true) { CancellationToken.ThrowIfCancellationRequested(); } return null; """; var result = _service.Compile("infinite", code); Assert.True(result.IsSuccess, "Infinite loop compiles but should be cancelled at runtime"); // Execute with a short timeout using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500)); var globals = new ScriptGlobals { Instance = null!, Parameters = new Dictionary(), CancellationToken = cts.Token }; await Assert.ThrowsAnyAsync(async () => { await result.CompiledScript!.RunAsync(globals, cts.Token); }); } [Fact] public async Task Sandbox_LongRunningScript_TimesOut() { // A script that does heavy computation with cancellation checks var code = """ var sum = 0; for (var i = 0; i < 100_000_000; i++) { sum += i; if (i % 10000 == 0) CancellationToken.ThrowIfCancellationRequested(); } return sum; """; var result = _service.Compile("heavy", code); Assert.True(result.IsSuccess); using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(100)); var globals = new ScriptGlobals { Instance = null!, Parameters = new Dictionary(), CancellationToken = cts.Token }; await Assert.ThrowsAnyAsync(async () => { await result.CompiledScript!.RunAsync(globals, cts.Token); }); } // ── Combined adversarial attempts ── [Fact] public void Sandbox_MultipleViolationsInOneScript_AllDetected() { var code = """ System.IO.File.ReadAllText("/etc/passwd"); System.Diagnostics.Process.Start("cmd"); new System.Net.Sockets.TcpClient(); new System.Net.Http.HttpClient(); """; var violations = _service.ValidateTrustModel(code); Assert.True(violations.Count >= 4, $"Expected at least 4 violations but got {violations.Count}: {string.Join("; ", violations)}"); } [Fact] public void Sandbox_UsingDirective_StillDetected() { var code = """ // Even with using aliases, the namespace string is still detected var x = System.IO.Path.GetTempPath(); """; var violations = _service.ValidateTrustModel(code); Assert.NotEmpty(violations); } }