refactor(siteruntime): M3.3 ValidateTrustModel delegates to shared ScriptAnalysis + compile-surface parity test
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
using System.Reflection;
|
||||
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
using ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// M3.3: compile-surface parity guard. The shared
|
||||
/// <see cref="ScriptCompileSurface"/> / <see cref="TriggerCompileSurface"/> are
|
||||
/// compile-only stubs the design-time deploy gate binds candidate scripts
|
||||
/// against; they must mirror the real SiteRuntime <see cref="ScriptGlobals"/> /
|
||||
/// <see cref="TriggerExpressionGlobals"/> bind surfaces. If a member a script can
|
||||
/// reference on the real globals is missing from the compile surface, a script
|
||||
/// that uses it would pass the design-time gate but fail at the site — so the
|
||||
/// compile surface's public top-level member NAMES must be a SUPERSET of the
|
||||
/// real globals' names. This test fails loudly (listing the missing names) when
|
||||
/// the surface drifts behind the runtime globals.
|
||||
///
|
||||
/// <para>
|
||||
/// Top-level member-name parity is sufficient: the design-time compile against
|
||||
/// the surface catches deeper signature mismatches itself; this guard only
|
||||
/// ensures every entry point a script can name on the real globals exists on the
|
||||
/// stub.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class CompileSurfaceParityTests
|
||||
{
|
||||
[Fact]
|
||||
public void ScriptCompileSurface_MemberNames_AreSupersetOf_ScriptGlobals()
|
||||
{
|
||||
AssertSurfaceCoversGlobals(
|
||||
surface: typeof(ScriptCompileSurface),
|
||||
globals: typeof(ScriptGlobals));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TriggerCompileSurface_MemberNames_AreSupersetOf_TriggerExpressionGlobals()
|
||||
{
|
||||
AssertSurfaceCoversGlobals(
|
||||
surface: typeof(TriggerCompileSurface),
|
||||
globals: typeof(TriggerExpressionGlobals));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asserts that the public instance property + method member names of
|
||||
/// <paramref name="surface"/> are a superset of those of
|
||||
/// <paramref name="globals"/>. Inherited <see cref="object"/> members
|
||||
/// (ToString/GetHashCode/Equals/GetType) are excluded. Fails with the exact
|
||||
/// list of missing names when the surface does not cover the globals.
|
||||
/// </summary>
|
||||
private static void AssertSurfaceCoversGlobals(Type surface, Type globals)
|
||||
{
|
||||
var surfaceNames = PublicInstanceMemberNames(surface);
|
||||
var globalsNames = PublicInstanceMemberNames(globals);
|
||||
|
||||
var missing = globalsNames
|
||||
.Where(name => !surfaceNames.Contains(name))
|
||||
.OrderBy(name => name, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
|
||||
Assert.True(
|
||||
missing.Count == 0,
|
||||
$"Compile surface '{surface.Name}' is missing {missing.Count} member name(s) "
|
||||
+ $"present on '{globals.Name}': {string.Join(", ", missing)}. "
|
||||
+ "The compile-only surface must mirror the runtime globals — add the "
|
||||
+ "missing member(s) to the ScriptAnalysis surface type.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public instance property + method member names declared on or inherited by
|
||||
/// <paramref name="type"/>, excluding the inherited <see cref="object"/>
|
||||
/// members (ToString, GetHashCode, Equals, GetType) so only script-reachable
|
||||
/// API names remain. Property/method names are compared (no signatures).
|
||||
/// </summary>
|
||||
private static HashSet<string> PublicInstanceMemberNames(Type type)
|
||||
{
|
||||
const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance;
|
||||
|
||||
var objectMembers = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
nameof(ToString),
|
||||
nameof(GetHashCode),
|
||||
nameof(Equals),
|
||||
nameof(GetType),
|
||||
};
|
||||
|
||||
var names = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var property in type.GetProperties(flags))
|
||||
names.Add(property.Name);
|
||||
|
||||
foreach (var method in type.GetMethods(flags))
|
||||
{
|
||||
// Skip compiler-generated property accessors (get_X / set_X) — the
|
||||
// property itself is already counted by name above.
|
||||
if (method.IsSpecialName)
|
||||
continue;
|
||||
if (objectMembers.Contains(method.Name))
|
||||
continue;
|
||||
names.Add(method.Name);
|
||||
}
|
||||
|
||||
return names;
|
||||
}
|
||||
}
|
||||
+67
@@ -5,6 +5,16 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
||||
|
||||
/// <summary>
|
||||
/// WP-19: Script Trust Model tests — validates forbidden API detection and compilation.
|
||||
///
|
||||
/// As of the M3.3 consolidation, <c>ScriptCompilationService.ValidateTrustModel</c>
|
||||
/// delegates its forbidden-API verdict to the shared authoritative
|
||||
/// <c>ScriptAnalysis.ScriptTrustValidator</c>, which is stricter than SiteRuntime's
|
||||
/// original deny-list: ALL of <c>System.Net</c> is forbidden (not just Sockets/Http),
|
||||
/// plus reflection gateways, <c>dynamic</c>, <c>Activator</c>,
|
||||
/// <c>System.Runtime.InteropServices</c> and <c>Microsoft.Win32</c>. Only
|
||||
/// <c>System.Diagnostics.Process</c> is blocked under System.Diagnostics —
|
||||
/// <c>Stopwatch</c> stays allowed. The real execution-path compile against
|
||||
/// <c>ScriptGlobals</c> / <c>TriggerExpressionGlobals</c> is unchanged.
|
||||
/// </summary>
|
||||
public class ScriptCompilationServiceTests
|
||||
{
|
||||
@@ -108,4 +118,61 @@ public class ScriptCompilationServiceTests
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.NotEmpty(result.Errors);
|
||||
}
|
||||
|
||||
// ── M3.3: stricter shared-validator behavior ──
|
||||
|
||||
[Fact]
|
||||
public void ValidateTrustModel_SystemNetDns_Forbidden()
|
||||
{
|
||||
// The shared validator forbids ALL of System.Net — not just Sockets/Http.
|
||||
// System.Net.Dns was allowed under the old SiteRuntime list; now blocked.
|
||||
var violations = _service.ValidateTrustModel(
|
||||
"System.Net.Dns.GetHostName()");
|
||||
Assert.NotEmpty(violations);
|
||||
Assert.Contains(violations, v => v.Contains("System.Net"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateTrustModel_ReflectionGatewayViaPermittedType_Forbidden()
|
||||
{
|
||||
// typeof(x).Assembly.GetType(...) never spells a forbidden namespace, but
|
||||
// the shared validator rejects the reflection-gateway members regardless of
|
||||
// receiver — this was NOT caught by the old SiteRuntime list.
|
||||
var violations = _service.ValidateTrustModel(
|
||||
"typeof(string).Assembly.GetType(\"System.IO.File\")");
|
||||
Assert.NotEmpty(violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateTrustModel_Dynamic_Forbidden()
|
||||
{
|
||||
var violations = _service.ValidateTrustModel("dynamic d = 1; return d;");
|
||||
Assert.NotEmpty(violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateTrustModel_Activator_Forbidden()
|
||||
{
|
||||
var violations = _service.ValidateTrustModel(
|
||||
"Activator.CreateInstance(typeof(string))");
|
||||
Assert.NotEmpty(violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateTrustModel_InteropServices_Forbidden()
|
||||
{
|
||||
var violations = _service.ValidateTrustModel(
|
||||
"System.Runtime.InteropServices.Marshal.SizeOf<int>()");
|
||||
Assert.NotEmpty(violations);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateTrustModel_Stopwatch_Allowed()
|
||||
{
|
||||
// Only System.Diagnostics.Process is blocked under System.Diagnostics —
|
||||
// Stopwatch stays allowed.
|
||||
var violations = _service.ValidateTrustModel(
|
||||
"var sw = System.Diagnostics.Stopwatch.StartNew(); return sw.ElapsedMilliseconds;");
|
||||
Assert.Empty(violations);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Tests.Scripts;
|
||||
/// The previous implementation was a raw substring scan of the source text — it both
|
||||
/// missed forbidden APIs (no literal namespace string) and raised false positives on
|
||||
/// the namespace string appearing in comments, string literals or unrelated identifiers.
|
||||
///
|
||||
/// As of M3.3, <c>ValidateTrustModel</c> delegates to the shared authoritative
|
||||
/// <c>ScriptAnalysis.ScriptTrustValidator</c>, which retains this same Roslyn
|
||||
/// semantic-symbol analysis (plus reflection-gateway hardening), so these bypass /
|
||||
/// false-positive / allowed-exception regressions continue to hold through the
|
||||
/// delegating service.
|
||||
/// </summary>
|
||||
public class TrustModelSemanticTests
|
||||
{
|
||||
|
||||
+1
@@ -25,6 +25,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../src/ZB.MOM.WW.ScadaBridge.SiteRuntime/ZB.MOM.WW.ScadaBridge.SiteRuntime.csproj" />
|
||||
<ProjectReference Include="../../src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.csproj" />
|
||||
<ProjectReference Include="../../src/ZB.MOM.WW.ScadaBridge.Commons/ZB.MOM.WW.ScadaBridge.Commons.csproj" />
|
||||
<ProjectReference Include="../../src/ZB.MOM.WW.ScadaBridge.HealthMonitoring/ZB.MOM.WW.ScadaBridge.HealthMonitoring.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user