Files
ScadaBridge/src/ZB.MOM.WW.ScadaBridge.TemplateEngine/Validation/ScriptCompiler.cs
T
Joseph Doherty 7b0b9c7365 refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj,
namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated.
ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated.
SQL roles/logins, LDAP domains, CLI command name, and CLI config dir
(~/.scadalink → ~/.scadabridge) also renamed.

Build green; 5 Host.Tests fail awaiting SQL login rename in next commit.
Pre-existing StaleTagMonitor timing flakes unchanged.

Rename script committed at tools/rename-to-scadabridge.sh.
2026-05-28 09:37:45 -04:00

106 lines
5.6 KiB
C#

using ZB.MOM.WW.ScadaBridge.Commons.Types;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
/// <summary>
/// Validates script code by attempting to compile it using Roslyn.
/// In production, this would compile C# scripts against a stub ScriptApi assembly
/// that provides the allowed API surface (attribute read/write, CallScript, CallShared, etc.)
/// and enforces the forbidden API list (System.IO, Process, Threading, Reflection, raw network).
///
/// For now, this implementation performs basic syntax validation.
///
/// <para>
/// <b>SECURITY LIMITATION (TemplateEngine-006):</b> the forbidden-API check below
/// is an interim, <i>advisory</i> text scan — it is NOT an authoritative trust-model
/// boundary. <see cref="CSharpDelimiterScanner.ContainsInCode"/> removes the
/// false-positive half (forbidden text inside a string/comment is ignored), but a
/// determined script can still bypass the literal patterns via namespace aliases,
/// <c>using static</c>, or <c>global::</c>-qualified references. Authoritative
/// enforcement requires Roslyn semantic symbol analysis of the referenced
/// types/namespaces and is the responsibility of the real script compiler and the
/// Site Runtime sandbox. Do not rely on this class as the sole trust-model gate.
/// </para>
/// </summary>
public class ScriptCompiler
{
/// <summary>
/// Forbidden namespace patterns — scripts (and trigger expressions, via
/// <see cref="ValidationService"/>) must not use these. Trigger expressions run
/// under the same trust model as scripts, so the list is shared from here rather
/// than duplicated.
///
/// <para>
/// Matched with <see cref="CSharpDelimiterScanner.ContainsInCode"/> against code
/// regions only. This is advisory — see the class summary's SECURITY LIMITATION
/// note; the substring patterns are bypassable and the authoritative check is
/// deferred to Roslyn semantic analysis.
/// </para>
/// </summary>
internal static readonly string[] ForbiddenPatterns =
[
"System.IO.",
"System.Diagnostics.Process",
"System.Threading.",
"System.Reflection.",
"System.Net.Sockets.",
"System.Net.Http.",
];
/// <summary>
/// Attempts to compile a script and returns success or a compilation error.
/// </summary>
/// <param name="code">The C# script code.</param>
/// <param name="scriptName">The canonical name of the script (for error messages).</param>
/// <returns>Success if the script compiles, or Failure with the error message.</returns>
public Result<bool> TryCompile(string code, string scriptName)
{
if (string.IsNullOrWhiteSpace(code))
return Result<bool>.Failure($"Script '{scriptName}' has empty code.");
// Check for forbidden APIs. Advisory only (see class summary): the scan is
// code-region-aware so forbidden text inside a string/comment is ignored,
// but it remains a substring match and is not an authoritative boundary.
foreach (var pattern in ForbiddenPatterns)
{
if (CSharpDelimiterScanner.ContainsInCode(code, pattern))
{
return Result<bool>.Failure(
$"Script '{scriptName}' uses forbidden API: '{pattern.TrimEnd('.')}'. " +
"Scripts cannot use System.IO, Process, Threading, Reflection, or raw network APIs.");
}
}
// Basic structural validation: balanced braces/brackets/parens. The scan
// is string- and comment-aware (see CSharpDelimiterScanner) so a delimiter
// inside a regular/verbatim/interpolated/raw string, a char literal, or a
// comment does not produce a false mismatch. This remains an interim check
// until the Roslyn-based compiler is wired in.
var mismatch = CSharpDelimiterScanner.Scan(code);
return mismatch switch
{
CSharpDelimiterScanner.Mismatch.None =>
Result<bool>.Success(true),
CSharpDelimiterScanner.Mismatch.UnexpectedCloseBrace =>
Result<bool>.Failure($"Script '{scriptName}' has mismatched braces (unexpected closing brace)."),
CSharpDelimiterScanner.Mismatch.UnclosedBrace =>
Result<bool>.Failure($"Script '{scriptName}' has mismatched braces (unclosed opening brace)."),
CSharpDelimiterScanner.Mismatch.UnexpectedCloseBracket =>
Result<bool>.Failure($"Script '{scriptName}' has mismatched brackets (unexpected closing bracket)."),
CSharpDelimiterScanner.Mismatch.UnclosedBracket =>
Result<bool>.Failure($"Script '{scriptName}' has mismatched brackets (unclosed opening bracket)."),
CSharpDelimiterScanner.Mismatch.UnexpectedCloseParen =>
Result<bool>.Failure($"Script '{scriptName}' has mismatched parentheses (unexpected closing parenthesis)."),
CSharpDelimiterScanner.Mismatch.UnclosedParen =>
Result<bool>.Failure($"Script '{scriptName}' has mismatched parentheses (unclosed opening parenthesis)."),
CSharpDelimiterScanner.Mismatch.UnclosedBlockComment =>
Result<bool>.Failure($"Script '{scriptName}' has an unclosed block comment."),
CSharpDelimiterScanner.Mismatch.UnterminatedString =>
Result<bool>.Failure($"Script '{scriptName}' has an unterminated string literal."),
CSharpDelimiterScanner.Mismatch.UnterminatedChar =>
Result<bool>.Failure($"Script '{scriptName}' has an unterminated character literal."),
_ => Result<bool>.Success(true),
};
}
}