feat(templateengine): M3.2 deploy gate delegates to shared ScriptAnalysis (real compile + authoritative forbidden-API)
This commit is contained in:
@@ -1,105 +1,51 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types;
|
||||
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
|
||||
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.
|
||||
/// The authoritative design-time / deploy-blocking script gate. Delegates to the
|
||||
/// shared <see cref="ZB.MOM.WW.ScadaBridge.ScriptAnalysis"/> analyzer (M3.2): a
|
||||
/// real Roslyn semantic forbidden-API verdict
|
||||
/// (<see cref="ScriptTrustValidator.FindViolations(string, System.Collections.Generic.IEnumerable{Microsoft.CodeAnalysis.MetadataReference})"/>)
|
||||
/// followed by a real CSharpScript compile against the
|
||||
/// <see cref="ScriptCompileSurface"/> globals
|
||||
/// (<see cref="RoslynScriptCompiler.Compile(string, System.Type, System.Collections.Generic.IEnumerable{Microsoft.CodeAnalysis.MetadataReference}, System.Collections.Generic.IEnumerable{string})"/>).
|
||||
///
|
||||
/// <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.
|
||||
/// This is a real trust-model boundary, not the previous advisory substring +
|
||||
/// brace-balance scan: the forbidden-API check resolves symbols (so namespace
|
||||
/// aliases, <c>using static</c>, <c>global::</c>-qualified references, and the
|
||||
/// reflection gateway are all caught), and the compile catches undefined symbols
|
||||
/// and signature mismatches that a structural scan could never see.
|
||||
/// </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.
|
||||
/// The security check runs first: a script that uses a forbidden API is
|
||||
/// rejected before any compile diagnostics are considered.
|
||||
/// </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>
|
||||
/// <returns>Success if the script is clean and 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.");
|
||||
}
|
||||
}
|
||||
// Authoritative forbidden-API verdict first — a security violation must
|
||||
// gate the script regardless of whether it otherwise compiles.
|
||||
var violations = ScriptTrustValidator.FindViolations(code);
|
||||
if (violations.Count > 0)
|
||||
return Result<bool>.Failure($"Script '{scriptName}' uses forbidden API: {violations[0]}");
|
||||
|
||||
// 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),
|
||||
};
|
||||
// Real CSharpScript compile against the runtime-mirroring globals surface.
|
||||
var errors = RoslynScriptCompiler.Compile(code, typeof(ScriptCompileSurface));
|
||||
if (errors.Count > 0)
|
||||
return Result<bool>.Failure($"Script '{scriptName}' failed to compile: {errors[0]}");
|
||||
|
||||
return Result<bool>.Success(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
||||
|
||||
@@ -247,10 +248,11 @@ public class ValidationService
|
||||
/// checks against the <c>{ "expression": "..." }</c> trigger configuration:
|
||||
/// <list type="bullet">
|
||||
/// <item>Blank expression → warning (the trigger will never fire).</item>
|
||||
/// <item>Syntax check → error if the expression uses a forbidden API or has
|
||||
/// unbalanced brackets/quotes. The TemplateEngine project does not reference a
|
||||
/// Roslyn compiler (see <see cref="ScriptCompiler"/>), so this mirrors that
|
||||
/// string-based syntax check rather than a full compile.</item>
|
||||
/// <item>Syntax/compile check → error if the expression uses a forbidden API
|
||||
/// or does not compile as a bare boolean expression. Delegates to the shared
|
||||
/// authoritative analyzer (see <see cref="ScriptCompiler"/> and
|
||||
/// <see cref="CheckExpressionSyntax"/>) — a real Roslyn compile against the
|
||||
/// <see cref="TriggerCompileSurface"/>, not a string scan.</item>
|
||||
/// <item>Attribute-reference scan → error for any <c>Attributes["X"]</c> literal
|
||||
/// whose key is absent from the flattened configuration, mirroring
|
||||
/// <see cref="ValidateScriptTriggerReferences"/> for the structured triggers.</item>
|
||||
@@ -373,113 +375,27 @@ public class ValidationService
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lightweight string-based syntax check for a trigger expression. Mirrors the
|
||||
/// approach in <see cref="ScriptCompiler"/> (the TemplateEngine project has no
|
||||
/// Roslyn compiler reference): rejects forbidden APIs and unbalanced
|
||||
/// brackets/quotes. Returns an error message, or <c>null</c> when the expression
|
||||
/// looks well-formed.
|
||||
/// Authoritative syntax/trust check for a trigger expression. Delegates to the
|
||||
/// shared <see cref="ZB.MOM.WW.ScadaBridge.ScriptAnalysis"/> analyzer (same gate
|
||||
/// as <see cref="ScriptCompiler"/>): a real forbidden-API verdict
|
||||
/// (<see cref="ScriptTrustValidator.FindViolations(string, System.Collections.Generic.IEnumerable{Microsoft.CodeAnalysis.MetadataReference})"/>)
|
||||
/// followed by a real CSharpScript compile of the bare boolean expression against
|
||||
/// the <see cref="TriggerCompileSurface"/> globals. Returns an error message, or
|
||||
/// <c>null</c> when the expression is clean and compiles.
|
||||
/// </summary>
|
||||
/// <param name="expression">The expression to check for syntax errors.</param>
|
||||
/// <param name="expression">The expression to check.</param>
|
||||
/// <returns>A human-readable error message if the expression is invalid; <c>null</c> if well-formed.</returns>
|
||||
internal static string? CheckExpressionSyntax(string expression)
|
||||
{
|
||||
// Advisory forbidden-API scan (TemplateEngine-006): code-region-aware so
|
||||
// the inert text inside a string/comment is not flagged, but still a
|
||||
// substring match — not an authoritative boundary. See ScriptCompiler.
|
||||
foreach (var pattern in ScriptCompiler.ForbiddenPatterns)
|
||||
{
|
||||
if (CSharpDelimiterScanner.ContainsInCode(expression, pattern))
|
||||
{
|
||||
return $"uses forbidden API '{pattern.TrimEnd('.')}'. " +
|
||||
"Trigger expressions cannot use System.IO, Process, Threading, Reflection, or raw network APIs.";
|
||||
}
|
||||
}
|
||||
// Authoritative forbidden-API verdict first.
|
||||
var violations = ScriptTrustValidator.FindViolations(expression);
|
||||
if (violations.Count > 0)
|
||||
return $"uses forbidden API: {violations[0]}";
|
||||
|
||||
var parenDepth = 0;
|
||||
var bracketDepth = 0;
|
||||
var braceDepth = 0;
|
||||
var inString = false;
|
||||
var inChar = false;
|
||||
var inLineComment = false;
|
||||
var inBlockComment = false;
|
||||
|
||||
for (int i = 0; i < expression.Length; i++)
|
||||
{
|
||||
var c = expression[i];
|
||||
var next = i + 1 < expression.Length ? expression[i + 1] : '\0';
|
||||
|
||||
if (inLineComment)
|
||||
{
|
||||
if (c == '\n') inLineComment = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inBlockComment)
|
||||
{
|
||||
if (c == '*' && next == '/')
|
||||
{
|
||||
inBlockComment = false;
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inString)
|
||||
{
|
||||
if (c == '\\') { i++; continue; }
|
||||
if (c == '"') inString = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inChar)
|
||||
{
|
||||
if (c == '\\') { i++; continue; }
|
||||
if (c == '\'') inChar = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' && next == '/')
|
||||
{
|
||||
inLineComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '/' && next == '*')
|
||||
{
|
||||
inBlockComment = true;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (c)
|
||||
{
|
||||
case '"': inString = true; break;
|
||||
case '\'': inChar = true; break;
|
||||
case '(': parenDepth++; break;
|
||||
case ')':
|
||||
parenDepth--;
|
||||
if (parenDepth < 0) return "mismatched parentheses (unexpected ')').";
|
||||
break;
|
||||
case '[': bracketDepth++; break;
|
||||
case ']':
|
||||
bracketDepth--;
|
||||
if (bracketDepth < 0) return "mismatched brackets (unexpected ']').";
|
||||
break;
|
||||
case '{': braceDepth++; break;
|
||||
case '}':
|
||||
braceDepth--;
|
||||
if (braceDepth < 0) return "mismatched braces (unexpected '}').";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (inBlockComment) return "unterminated block comment.";
|
||||
if (inString) return "unterminated string literal.";
|
||||
if (inChar) return "unterminated character literal.";
|
||||
if (parenDepth != 0) return $"mismatched parentheses ({parenDepth} unclosed).";
|
||||
if (bracketDepth != 0) return $"mismatched brackets ({bracketDepth} unclosed).";
|
||||
if (braceDepth != 0) return $"mismatched braces ({braceDepth} unclosed).";
|
||||
// Real compile of the bare boolean expression against the trigger globals.
|
||||
var errors = RoslynScriptCompiler.Compile(expression, typeof(TriggerCompileSurface));
|
||||
if (errors.Count > 0)
|
||||
return $"is not a valid expression: {errors[0]}";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,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" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user