319 lines
15 KiB
C#
319 lines
15 KiB
C#
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.");
|
|
}
|
|
}
|