using ZB.MOM.WW.ScadaBridge.Commons.Types; namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation; /// /// 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. /// /// /// SECURITY LIMITATION (TemplateEngine-006): the forbidden-API check below /// is an interim, advisory text scan — it is NOT an authoritative trust-model /// boundary. 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, /// using static, or global::-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. /// /// public class ScriptCompiler { /// /// Forbidden namespace patterns — scripts (and trigger expressions, via /// ) must not use these. Trigger expressions run /// under the same trust model as scripts, so the list is shared from here rather /// than duplicated. /// /// /// Matched with 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. /// /// internal static readonly string[] ForbiddenPatterns = [ "System.IO.", "System.Diagnostics.Process", "System.Threading.", "System.Reflection.", "System.Net.Sockets.", "System.Net.Http.", ]; /// /// Attempts to compile a script and returns success or a compilation error. /// /// The C# script code. /// The canonical name of the script (for error messages). /// Success if the script compiles, or Failure with the error message. public Result TryCompile(string code, string scriptName) { if (string.IsNullOrWhiteSpace(code)) return Result.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.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.Success(true), CSharpDelimiterScanner.Mismatch.UnexpectedCloseBrace => Result.Failure($"Script '{scriptName}' has mismatched braces (unexpected closing brace)."), CSharpDelimiterScanner.Mismatch.UnclosedBrace => Result.Failure($"Script '{scriptName}' has mismatched braces (unclosed opening brace)."), CSharpDelimiterScanner.Mismatch.UnexpectedCloseBracket => Result.Failure($"Script '{scriptName}' has mismatched brackets (unexpected closing bracket)."), CSharpDelimiterScanner.Mismatch.UnclosedBracket => Result.Failure($"Script '{scriptName}' has mismatched brackets (unclosed opening bracket)."), CSharpDelimiterScanner.Mismatch.UnexpectedCloseParen => Result.Failure($"Script '{scriptName}' has mismatched parentheses (unexpected closing parenthesis)."), CSharpDelimiterScanner.Mismatch.UnclosedParen => Result.Failure($"Script '{scriptName}' has mismatched parentheses (unclosed opening parenthesis)."), CSharpDelimiterScanner.Mismatch.UnclosedBlockComment => Result.Failure($"Script '{scriptName}' has an unclosed block comment."), CSharpDelimiterScanner.Mismatch.UnterminatedString => Result.Failure($"Script '{scriptName}' has an unterminated string literal."), CSharpDelimiterScanner.Mismatch.UnterminatedChar => Result.Failure($"Script '{scriptName}' has an unterminated character literal."), _ => Result.Success(true), }; } }