using BuildFlow.Shared.Domain.Common; using BuildFlow.Shared.Domain.Errors; using BuildFlow.Shared.Domain.Exceptions; using FluentAssertions; using MediatR; using Microsoft.EntityFrameworkCore; using NSubstitute; using Scripting.Application.Services; using Scripting.Domain.Entities.ScriptFunction; using Scripting.Domain.Enums; using Scripting.Infrastructure.Persistence; using Xunit; namespace BuildFlow.Scripting.Tests; /// /// 沙箱语义白名单测试:验证文本正则黑名单覆盖不到的绕过路径被语义分析拦截 /// (已导入命名空间下裸写类型名、反射成员入口、精确禁用类型), /// 并验证正常语法糖(插值字符串、await、LINQ、JSON)不被误伤。 /// public class SandboxSemanticWhitelistTests { private static (ScriptingDbContext db, ScriptExecutionService svc) CreateService() { var db = ScriptingTestDbFactory.Create(); var sender = Substitute.For(); var currentUser = Substitute.For(); currentUser.UserId.Returns("test-user"); currentUser.IPAddress.Returns("127.0.0.1"); var svc = new ScriptExecutionService(db, sender, currentUser); return (db, svc); } private static ScriptFunctionEntity SeedScript(ScriptingDbContext db, string code) { var script = new ScriptFunctionEntity { Id = Guid.NewGuid(), Name = $"sandbox-{Guid.NewGuid():N}", DisplayName = "sandbox-test", Code = code, Status = ScriptStatusEnum.Published, TimeoutMs = 5000, CreatedBy = "test", UpdatedBy = "test", OperatorIP = "127.0.0.1", }; db.Scripts.Add(script); db.SaveChanges(); return script; } // ── 绕过路径:全部应被语义白名单拦截 ────────────────────────────────── [Theory] [InlineData("""Environment.GetEnvironmentVariable("PATH"); return 1;""", "System.Environment")] [InlineData("var x = System.Environment.MachineName; return 1;", "System.Environment")] [InlineData("_ = new Thread(() => { }); return 1;", "System.Threading.Thread")] [InlineData("\"abc\".GetType(); return 1;", "GetType")] [InlineData("var t = typeof(string); return 1;", "System.Type")] [InlineData("var x = System.AppDomain.CurrentDomain.FriendlyName; return 1;", "System.AppDomain")] [InlineData("var x = Activator.CreateInstance(typeof(int)); return 1;", "Activator")] public async Task Execute_BypassAttempt_IsRejectedBySemanticWhitelist(string code, string expectedFragment) { var (db, svc) = CreateService(); var script = SeedScript(db, code); await using var _ = db; var act = () => svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None); var exception = (await act.Should().ThrowAsync()) .WithMessage("*禁用 API*") .Which; exception.Code.Should().Be(ErrorCode.Business); exception.Message.Should().Contain(expectedFragment); } // ── 正常语法糖:不应误伤 ───────────────────────────────────────────── [Fact] public async Task Execute_InterpolatedString_Succeeds() { var (db, svc) = CreateService(); // 插值字符串编译期依赖 System.Runtime.CompilerServices.DefaultInterpolatedStringHandler, // 白名单必须放行该命名空间(Unsafe 已单独精确封禁) var script = SeedScript(db, """return $"x={Variables["a"]}"; """); await using var _ = db; var result = await svc.ExecuteByIdAsync(script.Id, """{"a":7}""", CancellationToken.None); result.Status.Should().Be(ScriptExecutionStatusEnum.Success); result.ResultJson.Should().Be("\"x=7\""); } [Fact] public async Task Execute_AwaitAndLinq_Succeeds() { var (db, svc) = CreateService(); // await 编译期依赖 CompilerServices.TaskAwaiter;LINQ 依赖 System.Linq —— 均应在白名单内 var script = SeedScript(db, "await Task.Delay(1, CancellationToken); var sum = new[] { 1L, 2L, 3L }.Sum(); return sum;"); await using var _ = db; var result = await svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None); result.Status.Should().Be(ScriptExecutionStatusEnum.Success); result.ResultJson.Should().Be("6"); } [Fact] public async Task Execute_JsonProcessing_Succeeds() { var (db, svc) = CreateService(); var script = SeedScript(db, """var doc = JsonDocument.Parse(InputJson); return doc.RootElement.GetProperty("k").GetString();"""); await using var _ = db; var result = await svc.ExecuteByIdAsync(script.Id, """{"k":"v"}""", CancellationToken.None); result.Status.Should().Be(ScriptExecutionStatusEnum.Success); result.ResultJson.Should().Be("\"v\""); } [Fact] public async Task Execute_ScriptDefinedLocalClass_Succeeds() { var (db, svc) = CreateService(); // 脚本自定义类型位于全局命名空间,必须放行 var script = SeedScript(db, "class P { public long V { get; set; } } return new P { V = 5 }.V;"); await using var _ = db; var result = await svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None); result.Status.Should().Be(ScriptExecutionStatusEnum.Success); result.ResultJson.Should().Be("5"); } // ── 同一代码只校验一次(缓存命中后仍保持拦截语义由缓存键保证) ──────── [Fact] public async Task Execute_ForbiddenCodeTwice_SecondAlsoRejected() { var (db, svc) = CreateService(); var script = SeedScript(db, "\"abc\".GetType(); return 1;"); await using var _ = db; await FluentActions.Awaiting(() => svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None)) .Should().ThrowAsync(); await FluentActions.Awaiting(() => svc.ExecuteByIdAsync(script.Id, "{}", CancellationToken.None)) .Should().ThrowAsync() .WithMessage("*禁用 API*"); } }