feat(templateengine): M3.2 deploy gate delegates to shared ScriptAnalysis (real compile + authoritative forbidden-API)

This commit is contained in:
Joseph Doherty
2026-06-16 19:36:03 -04:00
parent 784fee7b07
commit 14bd25196a
4 changed files with 119 additions and 298 deletions
@@ -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;
}