refactor(centralui): M3.5 ScriptAnalysisService uses shared deny-list + delegates trust verdict
This commit is contained in:
@@ -12,6 +12,7 @@ using Microsoft.CodeAnalysis.Text;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services;
|
||||
using SharedTrust = ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;
|
||||
|
||||
@@ -56,18 +57,6 @@ public class ScriptAnalysisService
|
||||
"System.Text",
|
||||
"System.Threading.Tasks");
|
||||
|
||||
// Namespaces and types banned by the script trust model.
|
||||
// Tasks live under System.Threading.Tasks and remain allowed.
|
||||
private static readonly string[] ForbiddenNamespacePrefixes =
|
||||
{
|
||||
"System.IO",
|
||||
"System.Diagnostics",
|
||||
"System.Reflection",
|
||||
"System.Net",
|
||||
"System.Threading.Thread",
|
||||
"System.Threading.Tasks.Sources",
|
||||
};
|
||||
|
||||
private readonly ISharedScriptCatalog _sharedScripts;
|
||||
private readonly IMemoryCache _cache;
|
||||
private readonly IServiceProvider _services;
|
||||
@@ -1152,14 +1141,22 @@ public class ScriptAnalysisService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds every reference to a forbidden API — the documented script trust model,
|
||||
/// see <see cref="ForbiddenNamespacePrefixes"/>. Identifiers are resolved against
|
||||
/// the semantic model, so a forbidden type or member is caught however it is
|
||||
/// written: bare (<c>File</c>), fully qualified
|
||||
/// (<c>System.IO.File.WriteAllText</c>), or via an alias — while a user identifier
|
||||
/// that merely shares a name with a forbidden type (<c>var File = …</c>) does not
|
||||
/// false-positive. Used both for editor diagnostics and as the pre-execution
|
||||
/// trust-model gate (see <see cref="EnforceTrustModel"/>).
|
||||
/// Finds every reference to a forbidden API for the editor squiggle — the
|
||||
/// documented script trust model, sourced from the shared
|
||||
/// <see cref="SharedTrust.ScriptTrustPolicy.ForbiddenScopes"/> /
|
||||
/// <see cref="SharedTrust.ScriptTrustPolicy.AllowedExceptions"/> deny-list via
|
||||
/// <see cref="IsForbiddenName"/>. Identifiers are resolved against the semantic
|
||||
/// model, so a forbidden type or member is caught however it is written: bare
|
||||
/// (<c>File</c>), fully qualified (<c>System.IO.File.WriteAllText</c>), or via an
|
||||
/// alias — while a user identifier that merely shares a name with a forbidden type
|
||||
/// (<c>var File = …</c>) does not false-positive.
|
||||
///
|
||||
/// This is the position-aware, namespace-level editor advisory: it keeps the
|
||||
/// line/column markers Monaco needs. The authoritative run verdict lives in
|
||||
/// <see cref="EnforceTrustModel"/>, which delegates to the shared
|
||||
/// <see cref="SharedTrust.ScriptTrustValidator"/> (and additionally covers
|
||||
/// reflection-gateway members + dynamic/Activator hardening that the editor
|
||||
/// advisory does not mark).
|
||||
/// </summary>
|
||||
private static IEnumerable<DiagnosticMarker> FindForbiddenApiUsages(SyntaxTree tree, SemanticModel model)
|
||||
{
|
||||
@@ -1208,25 +1205,45 @@ public class ScriptAnalysisService
|
||||
/// <summary>
|
||||
/// The forbidden namespace/type a symbol implicates, or null if it is allowed.
|
||||
/// Checks the symbol's namespace and — for a type or member — the type's full
|
||||
/// name, so an entry like <c>System.Threading.Thread</c> bans that exact type
|
||||
/// while <c>System.Threading</c> (e.g. <c>CancellationToken</c>) stays allowed.
|
||||
/// name. M3.5: the allowed-exception carve-out is applied at the WHOLE-SYMBOL
|
||||
/// level (mirroring the shared <see cref="SharedTrust.ScriptTrustValidator"/>
|
||||
/// semantic pass), so a type whose full name is an allowed exception
|
||||
/// (e.g. <c>System.Threading.CancellationTokenSource</c>) is NOT flagged even
|
||||
/// though its bare containing namespace <c>System.Threading</c> is a forbidden
|
||||
/// root. Without the symbol-level check the namespace scope alone would
|
||||
/// false-positive the allowed cancellation types.
|
||||
/// </summary>
|
||||
private static string? ForbiddenNameFor(ISymbol? symbol)
|
||||
{
|
||||
if (symbol == null) return null;
|
||||
foreach (var name in QualifiedNamesOf(symbol))
|
||||
if (IsForbiddenName(name))
|
||||
return name;
|
||||
return null;
|
||||
var names = QualifiedNamesOf(symbol).ToList();
|
||||
if (names.Count == 0) return null;
|
||||
|
||||
// Allowed exception on ANY of the symbol's qualified names wins outright —
|
||||
// the rest of System.Threading stays forbidden, but Tasks +
|
||||
// CancellationToken(Source) survive.
|
||||
if (names.Any(n =>
|
||||
SharedTrust.ScriptTrustPolicy.AllowedExceptions.Any(a => IsUnderScope(n, a))))
|
||||
return null;
|
||||
|
||||
return names.FirstOrDefault(IsForbiddenName);
|
||||
}
|
||||
|
||||
/// <summary>Fully-qualified names a symbol reference implicates for trust-model checking.</summary>
|
||||
/// <summary>
|
||||
/// Fully-qualified names a symbol reference implicates for trust-model checking.
|
||||
/// M3.5: a bare namespace symbol is intentionally ignored — mirroring the shared
|
||||
/// <see cref="SharedTrust.ScriptTrustValidator"/> semantic pass. A namespace name
|
||||
/// on its own performs no action; harm requires referencing a type or member, so
|
||||
/// flagging the bare <c>System.Threading</c> qualifier of an otherwise-allowed
|
||||
/// type (<c>System.Threading.CancellationTokenSource</c>) would be a false
|
||||
/// positive. The forbidden-<c>using</c>-directive path is checked separately on
|
||||
/// the directive text, so namespace imports are still caught.
|
||||
/// </summary>
|
||||
private static IEnumerable<string> QualifiedNamesOf(ISymbol symbol)
|
||||
{
|
||||
switch (symbol)
|
||||
{
|
||||
case INamespaceSymbol { IsGlobalNamespace: false } ns:
|
||||
yield return ns.ToDisplayString();
|
||||
case INamespaceSymbol:
|
||||
break;
|
||||
case ITypeSymbol type:
|
||||
if (type.ContainingNamespace is { IsGlobalNamespace: false } tn)
|
||||
@@ -1249,23 +1266,74 @@ public class ScriptAnalysisService
|
||||
? ns.ToDisplayString() + "." + type.Name
|
||||
: type.Name;
|
||||
|
||||
private static bool IsForbiddenName(string qualifiedName) =>
|
||||
ForbiddenNamespacePrefixes.Any(p =>
|
||||
qualifiedName == p || qualifiedName.StartsWith(p + ".", StringComparison.Ordinal));
|
||||
/// <summary>
|
||||
/// M3.5: the forbidden-namespace verdict for the editor walker is sourced
|
||||
/// from the shared <see cref="SharedTrust.ScriptTrustPolicy"/> — the single
|
||||
/// deny-list the run gate (<see cref="ScriptTrustValidator"/>) also uses, so
|
||||
/// the editor squiggle and the run gate agree on which namespaces are
|
||||
/// forbidden. A name under a <see cref="SharedTrust.ScriptTrustPolicy.ForbiddenScopes"/>
|
||||
/// root is forbidden UNLESS it is also under a
|
||||
/// <see cref="SharedTrust.ScriptTrustPolicy.AllowedExceptions"/> entry
|
||||
/// (async/await + cancellation tokens survive the System.Threading block).
|
||||
/// </summary>
|
||||
private static bool IsForbiddenName(string qualifiedName)
|
||||
{
|
||||
if (SharedTrust.ScriptTrustPolicy.AllowedExceptions.Any(a => IsUnderScope(qualifiedName, a)))
|
||||
return false;
|
||||
return SharedTrust.ScriptTrustPolicy.ForbiddenScopes.Any(f => IsUnderScope(qualifiedName, f));
|
||||
}
|
||||
|
||||
private static bool IsUnderScope(string actual, string root) =>
|
||||
actual.Equals(root, StringComparison.Ordinal)
|
||||
|| actual.StartsWith(root + ".", StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
/// Pre-execution trust-model gate (CentralUI-001). Returns the forbidden-API
|
||||
/// markers (SCADA001/SCADA002) for a compiled script; an empty list means the
|
||||
/// script is clear to run. This is a static semantic gate, not a process
|
||||
/// sandbox — reflection-based indirection is still out of its reach; full
|
||||
/// isolation would require running scripts in a separate constrained process.
|
||||
/// Pre-execution trust-model gate (CentralUI-001). M3.5: the VERDICT is
|
||||
/// delegated to the shared <see cref="SharedTrust.ScriptTrustValidator"/> —
|
||||
/// the single source of truth the editor squiggle deny-list also derives from,
|
||||
/// so the run gate and the editor agree. The shared validator additionally
|
||||
/// enforces reflection-gateway members + dynamic/Activator hardening beyond the
|
||||
/// editor's namespace-level advisory. An empty result means clear to run.
|
||||
///
|
||||
/// The compilation's own metadata references are forwarded to the shared
|
||||
/// validator as extra references so its semantic pass resolves symbols against
|
||||
/// the SAME metadata the script was compiled against (the full BCL surface,
|
||||
/// including the SandboxScriptHost globals assembly). This keeps the gate's
|
||||
/// resolution identical to the editor walker's — e.g. a bare <c>Process</c>
|
||||
/// reached through <c>using System.Diagnostics;</c> resolves to
|
||||
/// <c>System.Diagnostics.Process</c> and is caught. Forwarding references never
|
||||
/// whitelists anything: the deny-list still applies regardless of what is
|
||||
/// referenced.
|
||||
///
|
||||
/// The shared validator returns positionless messages; the run gate doesn't
|
||||
/// need precise positions (only the verdict), so each violation is surfaced as a
|
||||
/// single error marker anchored at the script start. This is a static semantic
|
||||
/// gate, not a process sandbox — full isolation would require a separate
|
||||
/// constrained process.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<DiagnosticMarker> EnforceTrustModel(Compilation compilation)
|
||||
{
|
||||
var tree = compilation.SyntaxTrees.FirstOrDefault();
|
||||
if (tree == null) return Array.Empty<DiagnosticMarker>();
|
||||
var model = compilation.GetSemanticModel(tree);
|
||||
return FindForbiddenApiUsages(tree, model).ToList();
|
||||
|
||||
var violations = SharedTrust.ScriptTrustValidator.FindViolations(
|
||||
tree.ToString(), compilation.References);
|
||||
if (violations.Count == 0) return Array.Empty<DiagnosticMarker>();
|
||||
|
||||
// Positionless verdict → one error marker per violation at the script
|
||||
// start. Code SCADA002 (forbidden API reference) matches the editor's
|
||||
// semantic-marker code, so existing gate assertions ("SCADA001"/"SCADA002")
|
||||
// continue to hold.
|
||||
return violations
|
||||
.Select(v => new DiagnosticMarker(
|
||||
Severity: 8,
|
||||
StartLineNumber: 1,
|
||||
StartColumn: 1,
|
||||
EndLineNumber: 1,
|
||||
EndColumn: 2,
|
||||
Message: $"{v} — not allowed in scripts (script trust model).",
|
||||
Code: "SCADA002"))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static CompletionItem ToCompletionItem(ISymbol symbol)
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.Commons/ZB.MOM.WW.ScadaBridge.Commons.csproj" />
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.csproj" />
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.Security/ZB.MOM.WW.ScadaBridge.Security.csproj" />
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.TemplateEngine/ZB.MOM.WW.ScadaBridge.TemplateEngine.csproj" />
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.DeploymentManager/ZB.MOM.WW.ScadaBridge.DeploymentManager.csproj" />
|
||||
|
||||
Reference in New Issue
Block a user