feat: add audited one-shot prepare gate
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyGateAAppIntegrationContractTests
|
||||
{
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string AppRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyParityApp");
|
||||
private static readonly string Window = Read(AppRoot, "MainWindow.xaml.cs");
|
||||
private static readonly string Playout = Read(AppRoot, "MainWindow.Playout.cs");
|
||||
private static readonly string IntentParser = Read(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.LegacyBridge",
|
||||
"LegacyPlayoutUiIntents.cs");
|
||||
private static readonly string Factory = Read(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.Playout",
|
||||
"PlayoutEngineFactory.cs");
|
||||
private static readonly string LaunchAuthorization = Read(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.Playout",
|
||||
"Safety",
|
||||
"PlayoutLaunchAuthorization.cs");
|
||||
private static readonly string InfrastructureRoot = Path.Combine(
|
||||
RepositoryRoot,
|
||||
"src",
|
||||
"MBN_STOCK_WEBVIEW.Infrastructure",
|
||||
"Execution");
|
||||
|
||||
[Fact]
|
||||
public void Launch_authorization_is_captured_before_WebView_creation_and_forwarded_unchanged()
|
||||
{
|
||||
var constructor = Slice(
|
||||
Window,
|
||||
" public MainWindow()",
|
||||
" private async void OnRootLoaded(");
|
||||
var capture = RequiredIndex(
|
||||
constructor,
|
||||
"PlayoutLaunchAuthorization.CaptureFromEnvironment()");
|
||||
var initializeComponent = RequiredIndex(constructor, "InitializeComponent();");
|
||||
|
||||
Assert.True(capture < initializeComponent);
|
||||
Assert.Contains(
|
||||
"_playoutLaunchAuthorization =\n" +
|
||||
" PlayoutLaunchAuthorization.CaptureFromEnvironment();",
|
||||
Normalize(constructor),
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var initialization = Normalize(Slice(
|
||||
Playout,
|
||||
" private void InitializePlayoutRuntime()",
|
||||
" private async Task ConnectPlayoutAsync("));
|
||||
Assert.Contains(
|
||||
"_playoutEngine = PlayoutEngineFactory.Create(\n" +
|
||||
" _playoutOptions,\n" +
|
||||
" _playoutLaunchAuthorization);",
|
||||
initialization,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var factoryCreate = Slice(
|
||||
Normalize(Factory),
|
||||
" public static IPlayoutEngine Create(\n" +
|
||||
" PlayoutOptions options,\n" +
|
||||
" PlayoutLaunchAuthorization launchAuthorization)",
|
||||
" internal static void ValidateGateAOptions(");
|
||||
Assert.Contains("dispatcher,\n launchAuthorization,", factoryCreate,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("launchAuthorization: launchAuthorization", factoryCreate,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate_A_has_a_dedicated_capability_intent_and_exact_Samsung_5001_prepare_handler()
|
||||
{
|
||||
Assert.Contains(
|
||||
"public sealed record LegacyGateAPrepareIntent(string Capability)",
|
||||
IntentParser,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("\"gate-a-prepare\" => ParseGateAPrepare(payload)", IntentParser,
|
||||
StringComparison.Ordinal);
|
||||
var parser = Slice(
|
||||
IntentParser,
|
||||
" private static LegacyUiIntent? ParseGateAPrepare(JsonElement payload)",
|
||||
" private static LegacyUiIntent? ParseFadeDuration(JsonElement payload)");
|
||||
Assert.Contains("payload.EnumerateObject().Count() != 1", parser,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("capability.Length != 64", parser, StringComparison.Ordinal);
|
||||
Assert.Contains("not (>= 'A' and <= 'F')", parser, StringComparison.Ordinal);
|
||||
|
||||
var dispatch = Normalize(Slice(
|
||||
Window,
|
||||
" private async Task<LegacyOperatorSnapshot> HandleIntentAsync(",
|
||||
" private async Task<LegacyOperatorSnapshot> RefreshNamedPlaylistsForDispatchAsync("));
|
||||
Assert.Contains(
|
||||
"LegacyGateAPrepareIntent prepare =>\n" +
|
||||
" await ExecuteGateAPrepareAsync(\n" +
|
||||
" prepare.Capability,",
|
||||
dispatch,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var handler = Slice(
|
||||
Playout,
|
||||
" private async Task<LegacyOperatorSnapshot> ExecuteGateAPrepareAsync(",
|
||||
" private bool TryCreateExactGateAPrepareRequest(");
|
||||
Assert.Contains("TryClaimGateAPrepare(capability)", handler,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("TryCreateExactGateAPrepareRequest(out var request)", handler,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("gateAPrepareClaimed: true", handler, StringComparison.Ordinal);
|
||||
Assert.Contains("gateAPrepareRequest: request", handler, StringComparison.Ordinal);
|
||||
Assert.Contains("CompleteGateAPrepare()", handler, StringComparison.Ordinal);
|
||||
|
||||
var exact = Slice(
|
||||
Playout,
|
||||
" private bool TryCreateExactGateAPrepareRequest(",
|
||||
" private async Task<LegacyOperatorSnapshot> SetFadeDurationAsync(");
|
||||
foreach (var required in new[]
|
||||
{
|
||||
"snapshot.Playlist.Count != 1",
|
||||
"composition.FadeDuration != 6",
|
||||
"composition.BackgroundKind != LegacySceneBackgroundKind.None",
|
||||
"row.MarketText, \"코스피\"",
|
||||
"row.StockName, \"삼성전자\"",
|
||||
"row.PageText, \"1/1\"",
|
||||
"row.StockCode, \"005930\"",
|
||||
"_controller.CreatePlayoutRequest(6)",
|
||||
"candidate.SelectedIndexZeroBased != 0",
|
||||
"entry.CutCode, \"5001\"",
|
||||
"entry.FadeDuration != 6",
|
||||
"entry.PageNavigation?.IsEnabled == true",
|
||||
"selection.GroupCode, \"코스피\"",
|
||||
"selection.Subject, \"삼성전자\"",
|
||||
"selection.DataCode, \"005930\""
|
||||
})
|
||||
{
|
||||
Assert.Contains(required, exact, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ordinary_playout_intents_are_rejected_in_Gate_A_before_workflow_access()
|
||||
{
|
||||
var execute = Slice(
|
||||
Playout,
|
||||
" private async Task<LegacyOperatorSnapshot> ExecuteOperatorPlayoutAsync(",
|
||||
" private async Task<LegacyOperatorSnapshot> ExecuteGateAPrepareAsync(");
|
||||
var workflowRead = RequiredIndex(execute, "var workflow = _playoutWorkflow;");
|
||||
var preWorkflowGuard = execute[..workflowRead];
|
||||
|
||||
Assert.Contains("_playoutLaunchAuthorization.IsGateA", preWorkflowGuard,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("!gateAPrepareClaimed", preWorkflowGuard, StringComparison.Ordinal);
|
||||
Assert.Contains("command != LegacyOperatorPlayoutCommand.Prepare", preWorkflowGuard,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("gateAPrepareRequest is null", preWorkflowGuard,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("return _controller.ReportError(", preWorkflowGuard,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_playoutWorkflow", preWorkflowGuard,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("CreatePlayoutRequest", preWorkflowGuard,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("_playoutCommandGate.WaitAsync", preWorkflowGuard,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var dispatch = Slice(
|
||||
Window,
|
||||
" private async Task<LegacyOperatorSnapshot> HandleIntentAsync(",
|
||||
" private async Task<LegacyOperatorSnapshot> RefreshNamedPlaylistsForDispatchAsync(");
|
||||
Assert.Contains("LegacyExecutePlayoutIntent playout =>", dispatch,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("ExecuteOperatorPlayoutAsync(\n playout.Command,",
|
||||
Normalize(dispatch), StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_database_mutation_executors_receive_the_same_launch_denial_policy()
|
||||
{
|
||||
var constructor = Slice(
|
||||
Window,
|
||||
" public MainWindow()",
|
||||
" private async void OnRootLoaded(");
|
||||
Assert.Contains(
|
||||
"new LaunchDatabaseMutationAuthorization(_playoutLaunchAuthorization)",
|
||||
constructor,
|
||||
StringComparison.Ordinal);
|
||||
Assert.True(
|
||||
CountOccurrences(
|
||||
constructor,
|
||||
"mutationAuthorization: databaseMutationAuthorization") == 3,
|
||||
"The same launch authorization must be injected into all three DB mutation executors.");
|
||||
|
||||
var adapter = Slice(
|
||||
Window,
|
||||
" private sealed class LaunchDatabaseMutationAuthorization",
|
||||
" private void PostState(");
|
||||
Assert.Contains(": IDatabaseMutationAuthorization", adapter,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("return _authorization.AllowsDatabaseWrites;", adapter,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("_authorization = authorization ??", adapter,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
foreach (var executorFile in new[]
|
||||
{
|
||||
"OracleManualFinancialMutationExecutor.cs",
|
||||
"OracleNamedPlaylistMutationExecutor.cs",
|
||||
"OperatorCatalogMutationExecutor.cs"
|
||||
})
|
||||
{
|
||||
var executor = File.ReadAllText(Path.Combine(InfrastructureRoot, executorFile));
|
||||
var demand = RequiredIndex(executor, "DatabaseMutationAuthorizationGuard.Demand(");
|
||||
var open = RequiredIndex(executor, "await connection.OpenAsync(");
|
||||
Assert.True(demand < open, $"{executorFile} must deny before opening a DB connection.");
|
||||
Assert.Contains("_mutationAuthorization", executor, StringComparison.Ordinal);
|
||||
Assert.Contains("IDatabaseMutationAuthorization mutationAuthorization", executor,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate_A_blocks_persistent_write_intents_before_dispatch()
|
||||
{
|
||||
var process = Normalize(Slice(
|
||||
Window,
|
||||
" private async Task ProcessIntentAsync(LegacyUiIntent intent)",
|
||||
" private static bool IsNamedPlaylistModalIntent("));
|
||||
var guard = RequiredIndex(
|
||||
process,
|
||||
"_playoutLaunchAuthorization.IsGateA &&\n IsPersistentWriteIntent(intent)");
|
||||
var dispatchStarted = RequiredIndex(process, "dispatchStarted = true;");
|
||||
var dispatch = RequiredIndex(process, "HandleIntentAsync(intent");
|
||||
Assert.True(guard < dispatchStarted);
|
||||
Assert.True(guard < dispatch);
|
||||
|
||||
var guardEnd = process.IndexOf("return;", guard, StringComparison.Ordinal);
|
||||
Assert.True(guardEnd > guard);
|
||||
Assert.Contains("ReportIntentFailure(", process[guard..guardEnd],
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var persistent = Slice(
|
||||
Playout,
|
||||
" private static bool IsPersistentWriteIntent(LegacyUiIntent intent)",
|
||||
" private static bool IsFixedSectionBatchMutationIntent(");
|
||||
foreach (var intent in new[]
|
||||
{
|
||||
"LegacyDeleteUc4SelectedThemeIntent",
|
||||
"LegacyDeleteUc6SelectedExpertIntent",
|
||||
"LegacySaveOperatorCatalogIntent",
|
||||
"LegacyConfirmNativeDialogIntent",
|
||||
"LegacyAddComparisonPairIntent",
|
||||
"LegacySaveManualFinancialIntent",
|
||||
"LegacySaveManualNetSellIntent",
|
||||
"LegacySaveManualViIntent",
|
||||
"LegacyImportManualListsIntent",
|
||||
"LegacyCreateNamedPlaylistIntent",
|
||||
"LegacySaveCurrentNamedPlaylistIntent",
|
||||
"LegacyDeleteSelectedNamedPlaylistIntent"
|
||||
})
|
||||
{
|
||||
Assert.Contains(intent, persistent, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Gate_A_blocks_fade_and_background_mutation_before_any_command_gate()
|
||||
{
|
||||
Assert.Contains("public bool AllowsCompositionMutation => !IsGateA;",
|
||||
LaunchAuthorization, StringComparison.Ordinal);
|
||||
|
||||
var fade = Slice(
|
||||
Playout,
|
||||
" private async Task<LegacyOperatorSnapshot> SetFadeDurationAsync(",
|
||||
" private LegacyOperatorSnapshot SelectPlaylistBoundaryAndStopRefresh(");
|
||||
AssertCompositionMutationGuard(
|
||||
fade,
|
||||
"Gate A 검증 회차에서는 Fade 설정을 변경할 수 없습니다.");
|
||||
|
||||
var toggle = Slice(
|
||||
Playout,
|
||||
" private async Task<LegacyOperatorSnapshot> ToggleBackgroundAsync(",
|
||||
" private async Task<LegacyOperatorSnapshot> ChooseBackgroundAsync(");
|
||||
AssertCompositionMutationGuard(
|
||||
toggle,
|
||||
"Gate A 검증 회차에서는 배경 설정을 변경할 수 없습니다.");
|
||||
|
||||
var choose = Slice(
|
||||
Playout,
|
||||
" private async Task<LegacyOperatorSnapshot> ChooseBackgroundAsync(",
|
||||
" private bool CanChangeComposition()");
|
||||
AssertCompositionMutationGuard(
|
||||
choose,
|
||||
"Gate A 검증 회차에서는 배경 파일을 선택할 수 없습니다.");
|
||||
}
|
||||
|
||||
private static void AssertCompositionMutationGuard(string method, string message)
|
||||
{
|
||||
var authorization = RequiredIndex(
|
||||
method,
|
||||
"if (!_playoutLaunchAuthorization.AllowsCompositionMutation)");
|
||||
var commandGate = RequiredIndex(method, "_playoutCommandGate.WaitAsync");
|
||||
Assert.True(authorization < commandGate);
|
||||
Assert.Contains(message, method[authorization..commandGate],
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("return _controller.ReportError(", method[authorization..commandGate],
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static int CountOccurrences(string source, string marker)
|
||||
{
|
||||
var count = 0;
|
||||
var index = 0;
|
||||
while ((index = source.IndexOf(marker, index, StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
count++;
|
||||
index += marker.Length;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static string Slice(string source, string startMarker, string endMarker)
|
||||
{
|
||||
var start = RequiredIndex(source, startMarker);
|
||||
var end = RequiredIndex(source, endMarker, start + startMarker.Length);
|
||||
return source[start..end];
|
||||
}
|
||||
|
||||
private static int RequiredIndex(string source, string marker, int startIndex = 0)
|
||||
{
|
||||
var index = source.IndexOf(marker, startIndex, StringComparison.Ordinal);
|
||||
Assert.True(index >= 0, $"Required source marker was not found: {marker}");
|
||||
return index;
|
||||
}
|
||||
|
||||
private static string Normalize(string source) =>
|
||||
source.Replace("\r\n", "\n", StringComparison.Ordinal);
|
||||
|
||||
private static string Read(params string[] parts) =>
|
||||
File.ReadAllText(Path.Combine(parts));
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root was not found.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
namespace MBN_STOCK_WEBVIEW.LegacyWeb.Tests;
|
||||
|
||||
public sealed class LegacyGateAHarnessContractTests
|
||||
{
|
||||
private const string CurrentMsixSha256 =
|
||||
"0E44F87FDD31852B7DDFCD6CF3A6325DA674F1F5A23AD39CB262A725AEB84367";
|
||||
|
||||
private static readonly string RepositoryRoot = FindRepositoryRoot();
|
||||
private static readonly string Runner = ReadScript("Invoke-LegacyPgmPrepareGate.ps1");
|
||||
private static readonly string PlanBuilder = ReadScript("New-LegacyPgmPreparePlan.ps1");
|
||||
private static readonly string NodeHelper = ReadScript("Test-LegacyPgmPrepareGate.mjs");
|
||||
|
||||
[Fact]
|
||||
public void Node_receives_the_bearer_only_from_the_parent_owned_JIT_pipe()
|
||||
{
|
||||
Assert.Contains("import net from \"node:net\";", NodeHelper, StringComparison.Ordinal);
|
||||
Assert.Contains("\"capability-sha256\", \"permit-pipe\"", NodeHelper,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"name.startsWith(\"MBN_LEGACY_PGM_GATE_A_\")",
|
||||
NodeHelper,
|
||||
StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"process.env[capabilityName]",
|
||||
NodeHelper,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var receive = Slice(
|
||||
NodeHelper,
|
||||
"async function receiveJitCapability(expiresAt)",
|
||||
"async function prepareOnce(expiresAt)");
|
||||
Assert.Contains("net.createConnection(pipePath)", receive, StringComparison.Ordinal);
|
||||
Assert.Contains("bytes.length !== 64", receive, StringComparison.Ordinal);
|
||||
Assert.Contains("sha256(bytes) !== configuration.capabilitySha256", receive,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("jitPermitReceived = true", receive, StringComparison.Ordinal);
|
||||
|
||||
var prepare = Slice(
|
||||
NodeHelper,
|
||||
"async function prepareOnce(expiresAt)",
|
||||
"async function captureScreenshot()");
|
||||
var boundary = RequiredIndex(prepare, "const preRevision = before.state.revision;");
|
||||
var permit = RequiredIndex(prepare, "await receiveJitCapability(expiresAt)");
|
||||
var arm = RequiredIndex(prepare, "__legacyPgmPrepareGate.arm(");
|
||||
var dispatchPossible = RequiredIndex(prepare, "prepareDispatchPossible = true;");
|
||||
Assert.True(boundary < permit && permit < arm && arm < dispatchPossible);
|
||||
|
||||
var execute = Slice(NodeHelper, "async function execute()", "try {");
|
||||
Assert.Contains(
|
||||
"installGate(configuration.planSha256, configuration.capabilitySha256)",
|
||||
execute,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Runner_authenticates_the_exact_Node_client_before_one_JIT_delivery()
|
||||
{
|
||||
var start = Slice(
|
||||
Runner,
|
||||
"function Start-NodeHelper([object] $Plan)",
|
||||
"function Write-Evidence([string] $Path)");
|
||||
Assert.Contains("New-CurrentUserOneShotPipe 'Node'", start, StringComparison.Ordinal);
|
||||
Assert.Contains("'--capability-sha256'", start, StringComparison.Ordinal);
|
||||
Assert.Contains("'--permit-pipe'", start, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(
|
||||
"$info.EnvironmentVariables[$capabilityName]",
|
||||
start,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("MBN_LEGACY_PGM_GATE_A_*", start, StringComparison.Ordinal);
|
||||
Assert.Contains("Assert-NodePermitClient", start, StringComparison.Ordinal);
|
||||
|
||||
var client = Slice(
|
||||
Runner,
|
||||
"function Assert-NodePermitClient(",
|
||||
"function Stop-NodeHelperFailStop(");
|
||||
Assert.Contains("ReadNamedPipeClientProcessId", client, StringComparison.Ordinal);
|
||||
Assert.Contains("$clientProcessId -ne $Process.Id", client, StringComparison.Ordinal);
|
||||
Assert.Contains("$Process.StartTime.ToUniversalTime().Ticks", client,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("[int]$row.ParentProcessId -ne $PID", client,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("SamePath ([string]$row.ExecutablePath) $ExpectedExecutable", client,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var delivery = RequiredIndex(start, "$permitPipe.Stream.Write($bytes,0,$bytes.Length)");
|
||||
foreach (var marker in new[]
|
||||
{
|
||||
"Assert-NodePermitClient",
|
||||
"Assert-Authorization $script:authorization $Plan",
|
||||
"Assert-Application $script:application",
|
||||
"Assert-Pgm $Plan.pgm",
|
||||
"Assert-Cdp ([int]$Plan.webView.port)"
|
||||
})
|
||||
{
|
||||
Assert.True(RequiredIndex(start, marker) < delivery, marker);
|
||||
}
|
||||
|
||||
var stop = Slice(
|
||||
Runner,
|
||||
"function Stop-NodeHelperFailStop(",
|
||||
"function Start-NodeHelper([object] $Plan)");
|
||||
Assert.Contains("Close-OneShotPipe $Pipe", stop, StringComparison.Ordinal);
|
||||
Assert.Contains("$Process.Kill()", stop, StringComparison.Ordinal);
|
||||
Assert.Contains("$Process.WaitForExit(0)", stop, StringComparison.Ordinal);
|
||||
Assert.Contains("$Process.WaitForExit(5000)", stop, StringComparison.Ordinal);
|
||||
Assert.Contains("was not proven stopped", stop, StringComparison.Ordinal);
|
||||
Assert.Contains("only the control-plane Node helper", stop, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("$script:application", stop, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Assert-Pgm", stop, StringComparison.Ordinal);
|
||||
|
||||
var catchBlock = start[RequiredIndex(start, " catch {")..];
|
||||
Assert.Contains("Stop-NodeHelperFailStop $process $permitPipe", catchBlock,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("exceeded its outer timeout; parent fail-stop is required", start,
|
||||
StringComparison.Ordinal);
|
||||
var monitorTry = Slice(start, " try {\n $process.Refresh()", " catch {");
|
||||
Assert.True(
|
||||
RequiredIndex(monitorTry, "$process.StartTime.ToUniversalTime().Ticks") <
|
||||
RequiredIndex(monitorTry, "while (-not $process.HasExited"));
|
||||
Assert.Contains("if (-not $process.HasExited)", monitorTry, StringComparison.Ordinal);
|
||||
Assert.Contains("$process.WaitForExit(0)", monitorTry, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Package_wrapper_receives_launch_environment_over_an_authenticated_pipe_not_its_command_line()
|
||||
{
|
||||
var package = Slice(
|
||||
Runner,
|
||||
"function Start-Package([object] $Plan)",
|
||||
"function Quote-Argument([string] $Value)");
|
||||
Assert.DoesNotContain("$encodedEnvironment", package, StringComparison.Ordinal);
|
||||
Assert.Contains("New-CurrentUserOneShotPipe 'Package'", package,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("Assert-PackagePermitClient", package, StringComparison.Ordinal);
|
||||
Assert.Contains("[LegacyPgmPreparePackageIdentity]::Read($process.Handle)",
|
||||
Runner,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("$payload = $utf8NoBom.GetBytes(($environment | ConvertTo-Json -Compress))",
|
||||
package,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("$launchPipe.Stream.Write($payload,0,$payload.Length)", package,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("$client.ReadByte() -ne -1", package, StringComparison.Ordinal);
|
||||
var wrapperStop = Slice(
|
||||
Runner,
|
||||
"function Stop-PackageWrapperFailStop(",
|
||||
"function Start-Package([object] $Plan)");
|
||||
Assert.Contains("$Process.Kill()", wrapperStop, StringComparison.Ordinal);
|
||||
Assert.Contains("$Process.WaitForExit(0)", wrapperStop, StringComparison.Ordinal);
|
||||
Assert.Contains("$Process.WaitForExit(5000)", wrapperStop, StringComparison.Ordinal);
|
||||
Assert.Contains("was not proven stopped", wrapperStop, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("catch { return }", wrapperStop, StringComparison.Ordinal);
|
||||
Assert.Contains("$wrapper.WaitForExit(0)", package, StringComparison.Ordinal);
|
||||
Assert.Contains("without proving the exact wrapper exited", package,
|
||||
StringComparison.Ordinal);
|
||||
var child = Slice(package, " $child = @\"", "\"@\n $encodedChild");
|
||||
Assert.DoesNotContain("$encodedEnvironment", child, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("$script:gateCapability", child, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("$capabilityName", child, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("$environment", child, StringComparison.Ordinal);
|
||||
|
||||
var pipes = Slice(
|
||||
Runner,
|
||||
"function New-CurrentUserOneShotPipe([string] $Purpose)",
|
||||
"function Close-OneShotPipe([object] $Pipe)");
|
||||
Assert.Contains("[IO.Pipes.PipeDirection]::InOut", pipes, StringComparison.Ordinal);
|
||||
Assert.Contains("[IO.Pipes.PipeSecurity]::new()", pipes, StringComparison.Ordinal);
|
||||
Assert.Contains("$identity.User", pipes, StringComparison.Ordinal);
|
||||
Assert.Contains("SetAccessRuleProtection($true, $false)", pipes,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Managed_runtime_injection_environment_is_rejected_and_removed_from_children()
|
||||
{
|
||||
var preflight = Slice(
|
||||
Runner,
|
||||
"function Assert-NoUnsafeEnvironment",
|
||||
"function Assert-Pgm([object] $Pgm)");
|
||||
var package = Slice(
|
||||
Runner,
|
||||
"function Start-Package([object] $Plan)",
|
||||
"function Quote-Argument([string] $Value)");
|
||||
var child = Slice(package, " $child = @\"", "\"@\n $encodedChild");
|
||||
var node = Slice(
|
||||
Runner,
|
||||
"function Start-NodeHelper([object] $Plan)",
|
||||
"function Write-Evidence([string] $Path)");
|
||||
|
||||
foreach (var prefix in new[] { "DOTNET_*", "CORECLR_*", "COR_*", "COMPlus_*" })
|
||||
{
|
||||
Assert.Contains(prefix, preflight, StringComparison.Ordinal);
|
||||
Assert.True(Count(child, prefix) >= 2, $"{prefix} must be removed before and after app launch.");
|
||||
Assert.Contains(prefix, node, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
foreach (var scope in new[]
|
||||
{
|
||||
"[EnvironmentVariableTarget]::Process",
|
||||
"[EnvironmentVariableTarget]::User",
|
||||
"[EnvironmentVariableTarget]::Machine"
|
||||
})
|
||||
{
|
||||
Assert.Contains(scope, preflight, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Database_source_is_pinned_leased_and_cannot_be_overridden_by_environment()
|
||||
{
|
||||
Assert.Contains("approvedDatabaseConfigPath", PlanBuilder, StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"database = New-FilePin $approvedDatabaseConfigPath 'database config'",
|
||||
PlanBuilder,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Assert-FilePin $script:plan.configuration.database 'database config'",
|
||||
Runner,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Add-ReadLease ([string]$script:plan.configuration.database.path) 'database config'",
|
||||
Runner,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("filePinsValidated = 12", Runner, StringComparison.Ordinal);
|
||||
Assert.Contains("const databasePin = plan?.configuration?.database;", NodeHelper,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("!isSha256(databasePin?.sha256)", NodeHelper,
|
||||
StringComparison.Ordinal);
|
||||
|
||||
var preflight = Slice(
|
||||
Runner,
|
||||
"function Assert-NoUnsafeEnvironment",
|
||||
"function Assert-Pgm([object] $Pgm)");
|
||||
var package = Slice(
|
||||
Runner,
|
||||
"function Start-Package([object] $Plan)",
|
||||
"function Quote-Argument([string] $Value)");
|
||||
var child = Slice(package, " $child = @\"", "\"@\n $encodedChild");
|
||||
foreach (var prefix in new[]
|
||||
{
|
||||
"MBN_STOCK_ORACLE_*", "MBN_STOCK_MARIADB_*", "MBN_STOCK_DB_*"
|
||||
})
|
||||
{
|
||||
Assert.Contains(prefix, preflight, StringComparison.Ordinal);
|
||||
Assert.True(Count(child, prefix) >= 2, $"{prefix} must be removed before and after app launch.");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plan_contract_requires_helper_fail_stop_and_the_current_package_pin()
|
||||
{
|
||||
foreach (var source in new[] { Runner, PlanBuilder })
|
||||
{
|
||||
Assert.Contains(CurrentMsixSha256, source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Contains("nodeHelperFailStopRequired = $true", PlanBuilder,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("packageWrapperFailStopRequired = $true", PlanBuilder,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("'nodeHelperFailStopRequired'", Runner, StringComparison.Ordinal);
|
||||
Assert.Contains("'packageWrapperFailStopRequired'", Runner, StringComparison.Ordinal);
|
||||
Assert.Contains("plan?.safety?.nodeHelperFailStopRequired !== true", NodeHelper,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains("plan?.safety?.packageWrapperFailStopRequired !== true", NodeHelper,
|
||||
StringComparison.Ordinal);
|
||||
Assert.Contains(
|
||||
"Assert-JsonInt $Plan.safety.nodeHelperInvocationMaximum 1",
|
||||
Runner,
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string Slice(string source, string startMarker, string endMarker)
|
||||
{
|
||||
var start = RequiredIndex(source, startMarker);
|
||||
var end = RequiredIndex(source, endMarker, start + startMarker.Length);
|
||||
return source[start..end];
|
||||
}
|
||||
|
||||
private static int RequiredIndex(string source, string marker, int startIndex = 0)
|
||||
{
|
||||
var index = source.IndexOf(marker, startIndex, StringComparison.Ordinal);
|
||||
Assert.True(index >= 0, $"Required source marker was not found: {marker}");
|
||||
return index;
|
||||
}
|
||||
|
||||
private static int Count(string source, string marker)
|
||||
{
|
||||
var count = 0;
|
||||
var index = 0;
|
||||
while ((index = source.IndexOf(marker, index, StringComparison.Ordinal)) >= 0)
|
||||
{
|
||||
count++;
|
||||
index += marker.Length;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
private static string ReadScript(string fileName) =>
|
||||
File.ReadAllText(Path.Combine(RepositoryRoot, "scripts", fileName));
|
||||
|
||||
private static string FindRepositoryRoot()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "MBN_STOCK_WEBVIEW.sln")))
|
||||
{
|
||||
return current.FullName;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Repository root was not found.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user