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:
@@ -23,6 +23,7 @@ using ZB.MOM.WW.ScadaBridge.Commons.Types.Transport;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;
|
||||
using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase;
|
||||
using ZB.MOM.WW.ScadaBridge.ScriptAnalysis;
|
||||
using ZB.MOM.WW.ScadaBridge.TemplateEngine;
|
||||
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Validation;
|
||||
using ZB.MOM.WW.ScadaBridge.Transport.Encryption;
|
||||
@@ -846,6 +847,26 @@ public sealed class BundleImporter : IBundleImporter
|
||||
: $"Script references SharedScript or ExternalSystem '{candidate}' not present in bundle or target — advisory; the deploy-time gate re-validates."));
|
||||
}
|
||||
|
||||
// #05-T20 — script trust gate (preview parity with the apply-time gate).
|
||||
// A forbidden-API verdict is authoritative → a hard Blocker for every
|
||||
// script kind. No resolution map at preview time, so all scripts are
|
||||
// vetted. Blocker Name is entity-qualified so multiple offenders surface
|
||||
// as distinct rows.
|
||||
foreach (var (kind, entityName, scriptLabel, code) in EnumerateTrustGatedScripts(content, resolutionMap: null))
|
||||
{
|
||||
foreach (var violation in ScriptTrustValidator.FindViolations(code))
|
||||
{
|
||||
blockers.Add(new ImportPreviewItem(
|
||||
EntityType: kind,
|
||||
Name: $"{entityName}.{scriptLabel}",
|
||||
ExistingVersion: null,
|
||||
IncomingVersion: null,
|
||||
Kind: ConflictKind.Blocker,
|
||||
FieldDiffJson: null,
|
||||
BlockerReason: $"Script trust violation — {violation}"));
|
||||
}
|
||||
}
|
||||
|
||||
return blockers;
|
||||
}
|
||||
|
||||
@@ -906,6 +927,51 @@ public sealed class BundleImporter : IBundleImporter
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #05-T20 — enumerates every executable script body the trust gate must
|
||||
/// vet: non-Skip template scripts, shared scripts, and ApiMethod scripts.
|
||||
/// A null <paramref name="resolutionMap"/> (preview time, before resolutions
|
||||
/// are chosen) gates everything. Yields <c>(kind, entityName, scriptLabel,
|
||||
/// code)</c> so callers can format either an error string or a preview row.
|
||||
/// </summary>
|
||||
private static IEnumerable<(string Kind, string EntityName, string ScriptLabel, string Code)> EnumerateTrustGatedScripts(
|
||||
BundleContentDto content,
|
||||
Dictionary<(string, string), ImportResolution>? resolutionMap)
|
||||
{
|
||||
foreach (var t in content.Templates)
|
||||
{
|
||||
if (IsSkipResolution(resolutionMap, "Template", t.Name)) continue;
|
||||
foreach (var s in t.Scripts)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(s.Code))
|
||||
{
|
||||
yield return ("Template", t.Name, s.Name, s.Code);
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var s in content.SharedScripts)
|
||||
{
|
||||
if (IsSkipResolution(resolutionMap, "SharedScript", s.Name)) continue;
|
||||
if (!string.IsNullOrEmpty(s.Code))
|
||||
{
|
||||
yield return ("SharedScript", s.Name, s.Name, s.Code);
|
||||
}
|
||||
}
|
||||
foreach (var m in content.ApiMethods)
|
||||
{
|
||||
if (IsSkipResolution(resolutionMap, "ApiMethod", m.Name)) continue;
|
||||
if (!string.IsNullOrEmpty(m.Script))
|
||||
{
|
||||
yield return ("ApiMethod", m.Name, m.Name, m.Script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsSkipResolution(
|
||||
Dictionary<(string, string), ImportResolution>? resolutionMap, string entityType, string name)
|
||||
=> resolutionMap != null
|
||||
&& ResolveOrDefault(resolutionMap, entityType, name).Action == ResolutionAction.Skip;
|
||||
|
||||
/// <summary>
|
||||
/// Names that look like PascalCase references but are never user-defined
|
||||
/// SharedScripts or ExternalSystems. Filters the false-positive noise the
|
||||
@@ -4176,6 +4242,22 @@ public sealed class BundleImporter : IBundleImporter
|
||||
var errors = new List<string>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
// ---- Pass 0: script trust gate ----
|
||||
// Bundle import is the fifth script-trust call site. Every executable
|
||||
// script body (template, shared, ApiMethod) is run through the shared
|
||||
// ScriptTrustValidator BEFORE name resolution — a forbidden-API verdict
|
||||
// is authoritative (not the false-positive-prone name heuristic), so it
|
||||
// is a HARD error for all kinds, and it must not be masked by a Pass-1
|
||||
// name-resolution error. Task 15's verdict cache keeps repeat cost nil.
|
||||
foreach (var (kind, entityName, scriptLabel, code) in EnumerateTrustGatedScripts(content, resolutionMap))
|
||||
{
|
||||
foreach (var violation in ScriptTrustValidator.FindViolations(code))
|
||||
{
|
||||
errors.Add($"{kind} '{entityName}' script '{scriptLabel}': {violation}");
|
||||
}
|
||||
}
|
||||
if (errors.Count > 0) return (errors, warnings);
|
||||
|
||||
// ---- Pass 1: minimal name-resolution scan ----
|
||||
|
||||
// Build the known-resolvable set. For in-bundle entries, EXCLUDE the
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.Commons/ZB.MOM.WW.ScadaBridge.Commons.csproj" />
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.ConfigurationDatabase/ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.csproj" />
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.ScriptAnalysis/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.csproj" />
|
||||
<ProjectReference Include="../ZB.MOM.WW.ScadaBridge.TemplateEngine/ZB.MOM.WW.ScadaBridge.TemplateEngine.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user