fix(security): import trust gate covers instance alarm-override trigger expressions — 5th call site is surface-complete (plan R2-05 T6)

This commit is contained in:
Joseph Doherty
2026-07-13 09:57:59 -04:00
parent ab77094a26
commit b02c15bbca
2 changed files with 221 additions and 4 deletions
@@ -946,8 +946,11 @@ public sealed class BundleImporter : IBundleImporter
/// <summary>
/// Enumerates every executable C# surface a bundle carries that the
/// trust gate must vet: non-Skip template scripts + their Expression-trigger
/// bodies, template alarm Expression-trigger bodies, shared scripts, and
/// ApiMethod scripts. Expression triggers compile and execute at the site
/// bodies, template alarm Expression-trigger bodies, shared scripts,
/// ApiMethod scripts, AND instance alarm-override trigger expressions
/// (<c>InstanceAlarmOverrideDto.TriggerConfigurationOverride</c>). Expression
/// triggers — template alarms AND the instance overrides that replace them in
/// the flattened config — compile and execute at the site
/// (`TriggerExpressionGlobals`), so they are a genuine trust surface, not just
/// script bodies. A null <paramref name="resolutionMap"/> (preview time, before
/// resolutions are chosen) gates everything. Yields <c>(kind, entityName,
@@ -998,6 +1001,25 @@ public sealed class BundleImporter : IBundleImporter
yield return ("ApiMethod", m.Name, m.Name, m.Script);
}
}
foreach (var i in content.Instances)
{
if (IsSkipResolution(resolutionMap, "Instance", i.UniqueName)) continue;
foreach (var o in i.AlarmOverrides)
{
// The DTO carries no trigger type (the type lives on the template
// alarm), so gate ANY override config carrying an {"expression":...}
// string body: if the overridden alarm is not Expression-triggered
// the body never executes, but vetting it anyway is fail-safe — the
// structured (HiLo/threshold) configs have no "expression" key, so
// there is no false-positive channel.
var expr = ExtractExpressionBody(o.TriggerConfigurationOverride);
if (!string.IsNullOrEmpty(expr))
{
yield return ("Instance", i.UniqueName,
$"{o.AlarmCanonicalName} (alarm trigger override expression)", expr);
}
}
}
}
/// <summary>
@@ -1009,8 +1031,24 @@ public sealed class BundleImporter : IBundleImporter
private static string? ExtractTriggerExpression(string? triggerType, string? triggerConfigJson)
{
if (triggerType is null
|| !triggerType.Equals("Expression", StringComparison.OrdinalIgnoreCase)
|| string.IsNullOrWhiteSpace(triggerConfigJson))
|| !triggerType.Equals("Expression", StringComparison.OrdinalIgnoreCase))
{
return null;
}
return ExtractExpressionBody(triggerConfigJson);
}
/// <summary>
/// Extracts the C# boolean expression from an <c>{"expression":"…"}</c> trigger
/// configuration, WITHOUT a trigger-type guard — used where the caller has no
/// trigger type to check (an instance alarm override; the type lives on the
/// template alarm). A config carrying no <c>"expression"</c> string key (the
/// structured HiLo/threshold shapes) or malformed JSON yields null — nothing to
/// gate.
/// </summary>
private static string? ExtractExpressionBody(string? triggerConfigJson)
{
if (string.IsNullOrWhiteSpace(triggerConfigJson))
{
return null;
}