122 lines
4.3 KiB
C#
122 lines
4.3 KiB
C#
using Microsoft.CodeAnalysis;
|
|
using Microsoft.CodeAnalysis.CSharp;
|
|
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
|
|
|
namespace ScadaLink.InboundAPI;
|
|
|
|
/// <summary>
|
|
/// InboundAPI-005: Enforces the ScadaLink script trust model on inbound API method
|
|
/// scripts before they are compiled into executable handlers.
|
|
///
|
|
/// The trust model (CLAUDE.md, Akka.NET conventions) forbids scripts from reaching
|
|
/// <c>System.IO</c>, <c>System.Diagnostics.Process</c>, <c>System.Threading</c>,
|
|
/// <c>System.Reflection</c>, and raw network APIs. Roslyn scripting performs no
|
|
/// API allow/deny-listing — restricting default imports is a convenience, not a
|
|
/// sandbox — so a script can fully-qualify any referenced type. This static check
|
|
/// walks the script syntax tree and rejects any reference to a forbidden namespace,
|
|
/// whether reached through a <c>using</c> directive or a fully-qualified name.
|
|
/// </summary>
|
|
public static class ForbiddenApiChecker
|
|
{
|
|
/// <summary>
|
|
/// Namespace prefixes the trust model forbids. A script segment matches if it is
|
|
/// equal to one of these or is a child namespace of it.
|
|
/// </summary>
|
|
private static readonly string[] ForbiddenNamespaces =
|
|
{
|
|
"System.IO",
|
|
"System.Diagnostics", // covers Process
|
|
"System.Threading", // Task/Tasks is explicitly re-allowed below
|
|
"System.Reflection",
|
|
"System.Net", // raw network (Sockets, HttpClient, etc.)
|
|
"System.Runtime.InteropServices",
|
|
"Microsoft.Win32",
|
|
};
|
|
|
|
/// <summary>
|
|
/// Namespaces that would otherwise be caught by a forbidden prefix but are
|
|
/// required for normal async script authoring and carry no host-access risk.
|
|
/// </summary>
|
|
private static readonly string[] AllowedExceptions =
|
|
{
|
|
"System.Threading.Tasks",
|
|
};
|
|
|
|
/// <summary>
|
|
/// Analyses the script source and returns the list of trust-model violations.
|
|
/// An empty list means the script is acceptable.
|
|
/// </summary>
|
|
public static IReadOnlyList<string> FindViolations(string scriptCode)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(scriptCode))
|
|
return Array.Empty<string>();
|
|
|
|
var tree = CSharpSyntaxTree.ParseText(
|
|
scriptCode,
|
|
new CSharpParseOptions(kind: SourceCodeKind.Script));
|
|
|
|
var walker = new ForbiddenApiWalker();
|
|
walker.Visit(tree.GetRoot());
|
|
return walker.Violations;
|
|
}
|
|
|
|
private static bool IsForbidden(string dottedName)
|
|
{
|
|
foreach (var allowed in AllowedExceptions)
|
|
{
|
|
if (dottedName == allowed || dottedName.StartsWith(allowed + ".", StringComparison.Ordinal))
|
|
return false;
|
|
}
|
|
|
|
foreach (var forbidden in ForbiddenNamespaces)
|
|
{
|
|
if (dottedName == forbidden || dottedName.StartsWith(forbidden + ".", StringComparison.Ordinal))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private sealed class ForbiddenApiWalker : CSharpSyntaxWalker
|
|
{
|
|
private readonly List<string> _violations = new();
|
|
|
|
public IReadOnlyList<string> Violations => _violations;
|
|
|
|
public override void VisitUsingDirective(UsingDirectiveSyntax node)
|
|
{
|
|
if (node.Name is not null && IsForbidden(node.Name.ToString()))
|
|
_violations.Add($"forbidden namespace import '{node.Name}'");
|
|
|
|
base.VisitUsingDirective(node);
|
|
}
|
|
|
|
public override void VisitQualifiedName(QualifiedNameSyntax node)
|
|
{
|
|
// Check the longest qualified name; do not descend so a single
|
|
// System.IO.File reference is reported once, not three times.
|
|
var text = node.ToString();
|
|
if (IsForbidden(text))
|
|
{
|
|
_violations.Add($"forbidden type reference '{text}'");
|
|
return;
|
|
}
|
|
|
|
base.VisitQualifiedName(node);
|
|
}
|
|
|
|
public override void VisitMemberAccessExpression(MemberAccessExpressionSyntax node)
|
|
{
|
|
// Catches fully-qualified expressions such as System.IO.File.Delete(...).
|
|
var text = node.ToString();
|
|
if (IsForbidden(text))
|
|
{
|
|
_violations.Add($"forbidden API access '{text}'");
|
|
return;
|
|
}
|
|
|
|
base.VisitMemberAccessExpression(node);
|
|
}
|
|
}
|
|
}
|