93 lines
3.4 KiB
C#
93 lines
3.4 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using ScadaLink.SiteRuntime.Scripts;
|
|
|
|
namespace ScadaLink.SiteRuntime.Tests.Scripts;
|
|
|
|
/// <summary>
|
|
/// SiteRuntime-011: regression tests for the semantic-analysis trust-model validation.
|
|
/// The previous implementation was a raw substring scan of the source text — it both
|
|
/// missed forbidden APIs (no literal namespace string) and raised false positives on
|
|
/// the namespace string appearing in comments, string literals or unrelated identifiers.
|
|
/// </summary>
|
|
public class TrustModelSemanticTests
|
|
{
|
|
private readonly ScriptCompilationService _service =
|
|
new(NullLogger<ScriptCompilationService>.Instance);
|
|
|
|
// ── Bypass cases (under-inclusive substring scan would MISS these) ──
|
|
|
|
[Fact]
|
|
public void TrustModel_GlobalQualifiedForbiddenType_IsDetected()
|
|
{
|
|
// `global::`-prefixed name — the literal "System.IO" substring is still present
|
|
// here, but the resolved-symbol approach catches it regardless of spelling.
|
|
var violations = _service.ValidateTrustModel(
|
|
"global::System.IO.File.ReadAllText(\"/etc/passwd\")");
|
|
Assert.NotEmpty(violations);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrustModel_ForbiddenTypeViaUsingAlias_IsDetected()
|
|
{
|
|
// A using-alias hides the forbidden namespace from a substring scan entirely:
|
|
// the script body never writes "System.IO". Semantic resolution still sees that
|
|
// the alias resolves to System.IO.File.
|
|
var code = """
|
|
using F = System.IO.File;
|
|
F.ReadAllText("/etc/passwd");
|
|
""";
|
|
var violations = _service.ValidateTrustModel(code);
|
|
Assert.NotEmpty(violations);
|
|
Assert.Contains(violations, v => v.Contains("System.IO"));
|
|
}
|
|
|
|
// ── False-positive cases (over-inclusive substring scan would WRONGLY flag these) ──
|
|
|
|
[Fact]
|
|
public void TrustModel_ForbiddenNamespaceInStringLiteral_IsNotFlagged()
|
|
{
|
|
// "System.IO" appears only inside a string literal — not an API reference.
|
|
var violations = _service.ValidateTrustModel(
|
|
"var label = \"System.IO is blocked\"; return label;");
|
|
Assert.Empty(violations);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrustModel_ForbiddenNamespaceInComment_IsNotFlagged()
|
|
{
|
|
var code = """
|
|
// This script does not use System.IO or System.Reflection at all.
|
|
var x = 1 + 2;
|
|
return x;
|
|
""";
|
|
var violations = _service.ValidateTrustModel(code);
|
|
Assert.Empty(violations);
|
|
}
|
|
|
|
[Fact]
|
|
public void TrustModel_UnrelatedIdentifierContainingForbiddenSubstring_IsNotFlagged()
|
|
{
|
|
// A local variable whose name merely contains "Threading" is harmless.
|
|
var code = """
|
|
var ProcessThreadingCount = 5;
|
|
return ProcessThreadingCount + 1;
|
|
""";
|
|
var violations = _service.ValidateTrustModel(code);
|
|
Assert.Empty(violations);
|
|
}
|
|
|
|
// ── Allowed exceptions still resolve correctly ──
|
|
|
|
[Fact]
|
|
public void TrustModel_TaskAndCancellationToken_RemainAllowed()
|
|
{
|
|
var code = """
|
|
var cts = new System.Threading.CancellationTokenSource();
|
|
await System.Threading.Tasks.Task.Delay(1, cts.Token);
|
|
return 0;
|
|
""";
|
|
var violations = _service.ValidateTrustModel(code);
|
|
Assert.Empty(violations);
|
|
}
|
|
}
|