fix(security): bundle import runs the script trust gate — forbidden-API scripts rejected at import review, not at runtime
#05-T20. Bundle import is now the fifth ScriptTrustValidator delegating call site. RunSemanticValidationAsync gains a Pass 0 that runs ScriptTrustValidator .FindViolations over every non-Skip template / shared / ApiMethod script body before name resolution; any violation is a HARD error for all kinds (a trust verdict is authoritative, not the false-positive-prone name heuristic) and short-circuits so it is never masked by a Pass-1 name error. DetectBlockersAsync runs the same gate for preview parity, emitting entity-qualified Blocker rows. Transport gains a direct ScriptAnalysis project reference. Docs: Component- ScriptAnalysis (fifth call site) + Component-Transport updated. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
+75
@@ -292,6 +292,81 @@ public sealed class SemanticValidatorImportTests : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Apply_TemplateScriptWithForbiddenApi_HardBlocks()
|
||||
{
|
||||
// #05-T20 — bundle import is the fifth script-trust call site. A template
|
||||
// script that reaches a forbidden API (Process.Start) must be rejected at
|
||||
// import review, NOT left to fail at runtime. Unlike the name-resolution
|
||||
// heuristic, a trust verdict is authoritative → hard error for ALL kinds.
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
var t = new Template("Malicious");
|
||||
t.Scripts.Add(new Commons.Entities.Templates.TemplateScript(
|
||||
"init",
|
||||
"System.Diagnostics.Process.Start(\"cmd\");"));
|
||||
ctx.Templates.Add(t);
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var sessionId = await ExportWipeAndLoadAsync();
|
||||
|
||||
SemanticValidationException ex = default!;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
ex = await Assert.ThrowsAsync<SemanticValidationException>(() =>
|
||||
importer.ApplyAsync(sessionId,
|
||||
new List<ImportResolution>
|
||||
{
|
||||
new("Template", "Malicious", ResolutionAction.Add, null),
|
||||
},
|
||||
user: "bob"));
|
||||
}
|
||||
|
||||
Assert.NotEmpty(ex.Errors);
|
||||
Assert.Contains(ex.Errors, err => err.Contains("Process", StringComparison.Ordinal));
|
||||
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
Assert.False(await ctx.Templates.AnyAsync(t => t.Name == "Malicious"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Apply_ApiMethodWithForbiddenApi_HardBlocks()
|
||||
{
|
||||
// #05-T20 — same trust gate over ApiMethod script bodies.
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
|
||||
ctx.ApiMethods.Add(new Commons.Entities.InboundApi.ApiMethod(
|
||||
"Ingest",
|
||||
"System.Diagnostics.Process.Start(\"cmd\"); return 1;"));
|
||||
await ctx.SaveChangesAsync();
|
||||
}
|
||||
|
||||
var sessionId = await ExportWipeAndLoadAsync();
|
||||
|
||||
SemanticValidationException ex = default!;
|
||||
await using (var scope = _provider.CreateAsyncScope())
|
||||
{
|
||||
var importer = scope.ServiceProvider.GetRequiredService<IBundleImporter>();
|
||||
ex = await Assert.ThrowsAsync<SemanticValidationException>(() =>
|
||||
importer.ApplyAsync(sessionId,
|
||||
new List<ImportResolution>
|
||||
{
|
||||
new("ApiMethod", "Ingest", ResolutionAction.Add, null),
|
||||
},
|
||||
user: "bob"));
|
||||
}
|
||||
|
||||
Assert.NotEmpty(ex.Errors);
|
||||
Assert.Contains(ex.Errors, err => err.Contains("Process", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SemanticValidator_catches_alarm_trigger_type_mismatch_at_import()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user