fix(transport): close code-review gaps in the import trust gate + severity split (T19/T20)

Addresses the T20 security code-review findings:
- Gate template script + alarm Expression-trigger bodies too (they compile and
  execute at the site), not just script bodies — EnumerateTrustGatedScripts now
  extracts and vets the {"expression":"..."} C# via the same trust validator.
- Per-origin local-declaration exclusion: a helper declared in a template script
  no longer suppresses a genuinely-missing reference of the same name in an
  ApiMethod (a global set silently defeated the ApiMethod hard-blocker).
- Fail-closed: a ScriptTrustValidator throw on one pathological body is caught
  and surfaced as a hard blocker FOR THAT script (apply) / Blocker row (preview)
  instead of aborting the whole import.
- Corrected the misleading attribute-Value comment (typed literal, not a compiled
  expression) and documented the Pass-0 fail-fast rationale.

Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
Joseph Doherty
2026-07-10 02:32:28 -04:00
parent 92e713f1a1
commit 039cf69cbb
2 changed files with 233 additions and 45 deletions
@@ -367,6 +367,86 @@ public sealed class SemanticValidatorImportTests : IDisposable
Assert.Contains(ex.Errors, err => err.Contains("Process", StringComparison.Ordinal));
}
[Fact]
public async Task Apply_AlarmExpressionTriggerWithForbiddenApi_HardBlocks()
{
// #05-T20 (code-review fix): an alarm's Expression trigger compiles and
// executes at the site, so a forbidden API in the trigger expression must
// be caught at import — not just in template/shared/ApiMethod script bodies.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var t = new Template("AlarmedTank");
t.Alarms.Add(new TemplateAlarm("Bad")
{
TriggerType = AlarmTriggerType.Expression,
TriggerConfiguration = "{\"expression\":\"System.Diagnostics.Process.GetCurrentProcess() != null\"}",
PriorityLevel = 1,
});
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", "AlarmedTank", ResolutionAction.Add, null),
},
user: "bob"));
}
Assert.NotEmpty(ex.Errors);
Assert.Contains(ex.Errors, err => err.Contains("Process", StringComparison.Ordinal));
}
[Fact]
public async Task Apply_ApiMethodReference_NotSuppressedByTemplateLocalFunction()
{
// #05-T19 (code-review fix): a local function declared in a TEMPLATE script
// must not suppress a genuinely-missing reference of the same name in an
// ApiMethod — the ApiMethod finding stays a HARD error. A pre-fix global
// locallyDeclared set silently defeated this.
await using (var scope = _provider.CreateAsyncScope())
{
var ctx = scope.ServiceProvider.GetRequiredService<ScadaBridgeDbContext>();
var t = new Template("Helper");
t.Scripts.Add(new Commons.Entities.Templates.TemplateScript(
"init",
"decimal SharedHelper(decimal x) => x * 2; return SharedHelper(1m);"));
ctx.Templates.Add(t);
ctx.ApiMethods.Add(new Commons.Entities.InboundApi.ApiMethod(
"Ingest",
"var x = SharedHelper(); return x;"));
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", "Helper", ResolutionAction.Add, null),
new("ApiMethod", "Ingest", ResolutionAction.Add, null),
},
user: "bob"));
}
Assert.NotEmpty(ex.Errors);
Assert.Contains(ex.Errors, err => err.Contains("SharedHelper", StringComparison.Ordinal));
}
[Fact]
public async Task SemanticValidator_catches_alarm_trigger_type_mismatch_at_import()
{