feat(scriptanalysis): M3.1 shared trust validator + compiler + compile surfaces + tests
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// M3.1: parse + compile gate tests. The "representative real script" corpus is
|
||||
/// the PRIMARY guard that <see cref="ScriptCompileSurface"/> faithfully mirrors
|
||||
/// the runtime <c>ScriptGlobals</c> surface — if a member or signature drifts,
|
||||
/// the corpus stops binding and this test fails.
|
||||
/// </summary>
|
||||
public class RoslynScriptCompilerTests
|
||||
{
|
||||
[Fact]
|
||||
public void ParseDiagnostics_NonEmpty_ForSyntaxError()
|
||||
{
|
||||
Assert.NotEmpty(RoslynScriptCompiler.ParseDiagnostics("var x = ;"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParseDiagnostics_Empty_ForValidSyntax()
|
||||
{
|
||||
Assert.Empty(RoslynScriptCompiler.ParseDiagnostics("var x = 1;"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compile_NonEmpty_ForUndefinedSymbol()
|
||||
{
|
||||
var code = "var x = NoSuchThing.Foo();";
|
||||
Assert.NotEmpty(RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compile_Empty_ForRepresentativeRealScript()
|
||||
{
|
||||
const string code = """
|
||||
var temp = Attributes["Temperature"];
|
||||
Attributes["Setpoint"] = 42;
|
||||
var r = await ExternalSystem.Call("erp", "sync");
|
||||
var op = await Database.CachedWrite("hist", "INSERT ...");
|
||||
await Notify.To("ops").Send("subj", "msg");
|
||||
var shared = await Scripts.CallShared("Helper");
|
||||
var child = Children["Pump"].Attributes["Speed"];
|
||||
|
||||
// Widen coverage across the rest of the surface.
|
||||
var attr = await Instance.GetAttribute("Temperature");
|
||||
await Instance.SetAttribute("Setpoint", "43");
|
||||
var track = await Instance.Tracking.Status(op);
|
||||
var parentSpeed = Parent?.Attributes["Speed"];
|
||||
var alarmName = Alarm?.Name;
|
||||
var p = Parameters;
|
||||
var ct = CancellationToken;
|
||||
var status = await Notify.Status("notif-id");
|
||||
var cachedCall = await ExternalSystem.CachedCall("erp", "ping");
|
||||
var resolved = Attributes.Resolve("Temperature");
|
||||
var conn = await Database.Connection("hist");
|
||||
var scope = Scope;
|
||||
""";
|
||||
|
||||
var diagnostics = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface));
|
||||
Assert.Empty(diagnostics);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Compile_Empty_ForTriggerExpression()
|
||||
{
|
||||
const string expr =
|
||||
"Attributes[\"Temp\"] != null && (int)(Children[\"P\"].Attributes[\"S\"] ?? 0) > 5";
|
||||
|
||||
var diagnostics = RoslynScriptCompiler.Compile(expr, typeof(TriggerCompileSurface));
|
||||
Assert.Empty(diagnostics);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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<int>();";
|
||||
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));
|
||||
}
|
||||
|
||||
// ---- 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));
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user