mirror of
https://gitcode.com/ageerle/ruoyi-ai.git
synced 2026-11-13 22:40:42 +00:00
298 lines
14 KiB
PowerShell
298 lines
14 KiB
PowerShell
#requires -Version 7.0
|
|
[CmdletBinding()]
|
|
param(
|
|
[string]$DockerExecutable = "C:/Program Files/Docker/Docker/resources/bin/docker.exe",
|
|
[string]$ExpectedDockerExecutableSha256 = "83f31577a39b1c9f48c571cc2c31c99d74f74df1ab44003b4022301ed30a9831",
|
|
[string]$ExpectedDockerfileSha256 = "72c262c9396e59359e52edddb9963360da4c581298247231f6ab2e369ce43eef",
|
|
[string]$ExpectedDockerignoreSha256 = "16150e43ce98c333f93ad82a4991269a89bdd5596c427b4f5066ad266d6f871d",
|
|
[string]$BuildDockerConfigDirectory = "C:/Users/wl/.ruoyi-harness/evaluator/docker-build-config",
|
|
[string]$RuntimeDockerConfigDirectory = "C:/Users/wl/.ruoyi-harness/evaluator/docker-runtime-empty-config",
|
|
[string]$DockerHost = "npipe:////./pipe/docker_engine",
|
|
[string]$SourceContextDirectory = $PSScriptRoot,
|
|
[string]$ImageTag = "ruoyi-harness-toolchain:local"
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = "Stop"
|
|
$sourceContext = [System.IO.Path]::GetFullPath($SourceContextDirectory)
|
|
if (-not [System.IO.Directory]::Exists($sourceContext) -or
|
|
([System.IO.DirectoryInfo]::new($sourceContext).Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 -or
|
|
-not [System.IO.File]::Exists((Join-Path $sourceContext "Dockerfile")) -or
|
|
-not [System.IO.File]::Exists((Join-Path $sourceContext ".dockerignore"))) {
|
|
throw "BUILD_SOURCE_CONTEXT_INVALID"
|
|
}
|
|
|
|
function Assert-NoBroadWriteAcl {
|
|
param([Parameter(Mandatory)][string]$LiteralPath)
|
|
|
|
$trustedWriterSids = [System.Collections.Generic.HashSet[string]]::new(
|
|
[System.StringComparer]::OrdinalIgnoreCase)
|
|
$null = $trustedWriterSids.Add("S-1-5-18") # SYSTEM
|
|
$null = $trustedWriterSids.Add("S-1-5-32-544") # BUILTIN\Administrators
|
|
$null = $trustedWriterSids.Add([System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value)
|
|
try {
|
|
$trustedInstallerSid = ([System.Security.Principal.NTAccount]::new(
|
|
"NT SERVICE", "TrustedInstaller")).Translate(
|
|
[System.Security.Principal.SecurityIdentifier]).Value
|
|
$null = $trustedWriterSids.Add($trustedInstallerSid)
|
|
} catch {
|
|
throw "TRUSTED_INSTALLER_IDENTITY_UNRESOLVED"
|
|
}
|
|
$writeMask = [System.Security.AccessControl.FileSystemRights]::WriteData -bor
|
|
[System.Security.AccessControl.FileSystemRights]::AppendData -bor
|
|
[System.Security.AccessControl.FileSystemRights]::WriteExtendedAttributes -bor
|
|
[System.Security.AccessControl.FileSystemRights]::WriteAttributes -bor
|
|
[System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
|
|
[System.Security.AccessControl.FileSystemRights]::Delete -bor
|
|
[System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
|
|
[System.Security.AccessControl.FileSystemRights]::TakeOwnership
|
|
$target = [System.IO.Path]::GetFullPath($LiteralPath)
|
|
$paths = @($target)
|
|
$parent = [System.IO.Directory]::GetParent($target)
|
|
if ($null -ne $parent) { $paths += $parent.FullName }
|
|
foreach ($path in $paths) {
|
|
$acl = Get-Acl -LiteralPath $path
|
|
$ownerSid = ([System.Security.Principal.NTAccount]$acl.Owner).Translate(
|
|
[System.Security.Principal.SecurityIdentifier]).Value
|
|
if (-not $trustedWriterSids.Contains($ownerSid)) {
|
|
throw "UNTRUSTED_EVALUATOR_ACL_OWNER"
|
|
}
|
|
foreach ($rule in $acl.Access) {
|
|
if ($rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
|
|
($rule.FileSystemRights -band $writeMask) -eq 0) {
|
|
continue
|
|
}
|
|
try {
|
|
$sid = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
|
|
} catch {
|
|
throw "UNRESOLVED_EVALUATOR_ACL_IDENTITY"
|
|
}
|
|
if (-not $trustedWriterSids.Contains($sid)) {
|
|
throw "UNTRUSTED_WRITE_EVALUATOR_ACL_DENIED"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function Invoke-IsolatedDocker {
|
|
param(
|
|
[Parameter(Mandatory)][string]$Executable,
|
|
[Parameter(Mandatory)][string[]]$ArgumentList,
|
|
[Parameter(Mandatory)][string]$TempDirectory,
|
|
[ValidateRange(1, 600000)][int]$TimeoutMs = 120000,
|
|
[switch]$EchoOutput
|
|
)
|
|
|
|
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
|
$startInfo.FileName = $Executable
|
|
$startInfo.UseShellExecute = $false
|
|
$startInfo.CreateNoWindow = $true
|
|
$startInfo.RedirectStandardOutput = $true
|
|
$startInfo.RedirectStandardError = $true
|
|
foreach ($argument in $ArgumentList) {
|
|
$startInfo.ArgumentList.Add($argument)
|
|
}
|
|
$startInfo.Environment.Clear()
|
|
$windowsDirectory = [Environment]::GetFolderPath([Environment+SpecialFolder]::Windows)
|
|
$startInfo.Environment["SystemRoot"] = $windowsDirectory
|
|
$startInfo.Environment["WINDIR"] = $windowsDirectory
|
|
$startInfo.Environment["TEMP"] = $TempDirectory
|
|
$startInfo.Environment["TMP"] = $TempDirectory
|
|
|
|
$process = [System.Diagnostics.Process]::new()
|
|
$process.StartInfo = $startInfo
|
|
if (-not $process.Start()) {
|
|
throw "TRUSTED_DOCKER_PROCESS_START_FAILED"
|
|
}
|
|
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
|
|
$stderrTask = $process.StandardError.ReadToEndAsync()
|
|
if (-not $process.WaitForExit($TimeoutMs)) {
|
|
try {
|
|
$process.Kill($true)
|
|
$null = $process.WaitForExit(5000)
|
|
} finally {
|
|
$process.Dispose()
|
|
}
|
|
throw "TRUSTED_DOCKER_PROCESS_TIMEOUT"
|
|
}
|
|
$stdout = $stdoutTask.GetAwaiter().GetResult()
|
|
$stderr = $stderrTask.GetAwaiter().GetResult()
|
|
if ($EchoOutput) {
|
|
if (-not [string]::IsNullOrEmpty($stdout)) { [Console]::Out.Write($stdout) }
|
|
if (-not [string]::IsNullOrEmpty($stderr)) { [Console]::Error.Write($stderr) }
|
|
}
|
|
$result = [pscustomobject]@{
|
|
ExitCode = $process.ExitCode
|
|
Stdout = $stdout
|
|
Stderr = $stderr
|
|
}
|
|
$process.Dispose()
|
|
$result
|
|
}
|
|
|
|
$docker = [System.IO.Path]::GetFullPath($DockerExecutable)
|
|
if (-not [System.IO.File]::Exists($docker) -or
|
|
-not [System.IO.Path]::GetFileName($docker).Equals("docker.exe", [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "TRUSTED_DOCKER_CLI_REQUIRED"
|
|
}
|
|
$dockerInfo = [System.IO.FileInfo]::new($docker)
|
|
if (($dockerInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
throw "TRUSTED_DOCKER_CLI_REPARSE_DENIED"
|
|
}
|
|
if ($ExpectedDockerExecutableSha256 -notmatch '^[0-9a-fA-F]{64}$') {
|
|
throw "TRUSTED_DOCKER_CLI_DIGEST_REQUIRED"
|
|
}
|
|
$dockerSha256 = (Get-FileHash -LiteralPath $docker -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
if (-not $dockerSha256.Equals($ExpectedDockerExecutableSha256.ToLowerInvariant(), [System.StringComparison]::Ordinal)) {
|
|
throw "TRUSTED_DOCKER_CLI_DIGEST_MISMATCH"
|
|
}
|
|
Assert-NoBroadWriteAcl -LiteralPath $docker
|
|
|
|
$buildConfigRoot = [System.IO.Path]::GetFullPath($BuildDockerConfigDirectory)
|
|
$runtimeConfig = [System.IO.Path]::GetFullPath($RuntimeDockerConfigDirectory)
|
|
[System.IO.Directory]::CreateDirectory($buildConfigRoot) | Out-Null
|
|
[System.IO.Directory]::CreateDirectory($runtimeConfig) | Out-Null
|
|
$buildConfigRootInfo = [System.IO.DirectoryInfo]::new($buildConfigRoot)
|
|
$runtimeConfigInfo = [System.IO.DirectoryInfo]::new($runtimeConfig)
|
|
if (($buildConfigRootInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
|
|
throw "CREDENTIAL_FREE_BUILD_DOCKER_CONFIG_REQUIRED"
|
|
}
|
|
if (($runtimeConfigInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 -or
|
|
@($runtimeConfigInfo.EnumerateFileSystemInfos()).Count -ne 0) {
|
|
throw "EMPTY_LINK_FREE_RUNTIME_DOCKER_CONFIG_REQUIRED"
|
|
}
|
|
Assert-NoBroadWriteAcl -LiteralPath $buildConfigRoot
|
|
Assert-NoBroadWriteAcl -LiteralPath $runtimeConfig
|
|
$buildConfig = [System.IO.Path]::GetFullPath((Join-Path $buildConfigRoot ("session-" + [guid]::NewGuid().ToString("N"))))
|
|
if (-not ([System.IO.DirectoryInfo]::new($buildConfig).Parent.FullName).Equals($buildConfigRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "BUILD_DOCKER_CONFIG_BOUNDARY_INVALID"
|
|
}
|
|
[System.IO.Directory]::CreateDirectory($buildConfig) | Out-Null
|
|
$buildConfigInfo = [System.IO.DirectoryInfo]::new($buildConfig)
|
|
if (($buildConfigInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 -or
|
|
@($buildConfigInfo.EnumerateFileSystemInfos()).Count -ne 0) {
|
|
throw "EMPTY_LINK_FREE_BUILD_DOCKER_CONFIG_REQUIRED"
|
|
}
|
|
Assert-NoBroadWriteAcl -LiteralPath $buildConfig
|
|
if ($DockerHost -ne "npipe:////./pipe/docker_engine") {
|
|
throw "LOCAL_DOCKER_ENGINE_REQUIRED"
|
|
}
|
|
|
|
$processRoot = [System.IO.Path]::GetFullPath((Join-Path $buildConfigRoot ("process-" + [guid]::NewGuid().ToString("N"))))
|
|
try {
|
|
if (-not ([System.IO.DirectoryInfo]::new($processRoot).Parent.FullName).Equals($buildConfigRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
throw "BUILD_PROCESS_BOUNDARY_INVALID"
|
|
}
|
|
$context = Join-Path $processRoot "context"
|
|
$processTemp = Join-Path $processRoot "temp"
|
|
[System.IO.Directory]::CreateDirectory($context) | Out-Null
|
|
[System.IO.Directory]::CreateDirectory($processTemp) | Out-Null
|
|
Assert-NoBroadWriteAcl -LiteralPath $processRoot
|
|
Assert-NoBroadWriteAcl -LiteralPath $context
|
|
Assert-NoBroadWriteAcl -LiteralPath $processTemp
|
|
|
|
if ($ExpectedDockerfileSha256 -notmatch '^[0-9a-fA-F]{64}$' -or
|
|
$ExpectedDockerignoreSha256 -notmatch '^[0-9a-fA-F]{64}$') {
|
|
throw "APPROVED_BUILD_CONTEXT_DIGESTS_REQUIRED"
|
|
}
|
|
$dockerfile = Join-Path $context "Dockerfile"
|
|
$dockerignore = Join-Path $context ".dockerignore"
|
|
[System.IO.File]::Copy((Join-Path $sourceContext "Dockerfile"), $dockerfile, $false)
|
|
[System.IO.File]::Copy((Join-Path $sourceContext ".dockerignore"), $dockerignore, $false)
|
|
$dockerfileSha256 = (Get-FileHash -LiteralPath $dockerfile -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
$dockerignoreSha256 = (Get-FileHash -LiteralPath $dockerignore -Algorithm SHA256).Hash.ToLowerInvariant()
|
|
if (-not $dockerfileSha256.Equals($ExpectedDockerfileSha256.ToLowerInvariant(), [System.StringComparison]::Ordinal) -or
|
|
-not $dockerignoreSha256.Equals($ExpectedDockerignoreSha256.ToLowerInvariant(), [System.StringComparison]::Ordinal)) {
|
|
throw "BUILD_CONTEXT_DIGEST_MISMATCH"
|
|
}
|
|
$buildResult = Invoke-IsolatedDocker -Executable $docker -TempDirectory $processTemp -EchoOutput `
|
|
-ArgumentList @("--config", $buildConfig, "--host", $DockerHost, "build",
|
|
"--platform", "linux/amd64", "--pull", "--tag", $ImageTag,
|
|
"--file", $dockerfile, $context)
|
|
if ($buildResult.ExitCode -ne 0) {
|
|
throw "SANDBOX_IMAGE_BUILD_FAILED"
|
|
}
|
|
|
|
$inspectResult = Invoke-IsolatedDocker -Executable $docker -TempDirectory $processTemp -TimeoutMs 10000 `
|
|
-ArgumentList @("--config", $runtimeConfig, "--host", $DockerHost,
|
|
"image", "inspect", $ImageTag)
|
|
if ($inspectResult.ExitCode -ne 0) {
|
|
throw "SANDBOX_IMAGE_INSPECT_FAILED"
|
|
}
|
|
$inspectJson = $inspectResult.Stdout
|
|
$records = @($inspectJson | ConvertFrom-Json -Depth 32)
|
|
if ($records.Count -ne 1) {
|
|
throw "SANDBOX_IMAGE_IDENTITY_AMBIGUOUS"
|
|
}
|
|
$image = $records[0]
|
|
$volumesProperty = $image.Config.PSObject.Properties['Volumes']
|
|
$cmdProperty = $image.Config.PSObject.Properties['Cmd']
|
|
$healthcheckProperty = $image.Config.PSObject.Properties['Healthcheck']
|
|
$entrypointProperty = $image.Config.PSObject.Properties['Entrypoint']
|
|
$volumes = if ($null -eq $volumesProperty) { $null } else { $volumesProperty.Value }
|
|
$commandValue = if ($null -eq $cmdProperty) { $null } else { $cmdProperty.Value }
|
|
$healthcheck = if ($null -eq $healthcheckProperty) { $null } else { $healthcheckProperty.Value }
|
|
$entrypoint = if ($null -eq $entrypointProperty) { $null } else { $entrypointProperty.Value }
|
|
if ([string]$image.Id -notmatch '^sha256:[0-9a-f]{64}$' -or
|
|
[string]$image.Os -ne "linux" -or [string]$image.Architecture -ne "amd64" -or
|
|
[string]$image.Config.User -ne "65532:65532" -or
|
|
[string]$image.Config.WorkingDir -ne "/workspace" -or
|
|
$null -ne $volumes -or $null -ne $commandValue -or
|
|
$null -ne $healthcheck -or $null -ne $entrypoint) {
|
|
throw "SANDBOX_IMAGE_METADATA_UNTRUSTED"
|
|
}
|
|
|
|
$approvedEnvironment = @{
|
|
PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
JAVA_HOME = "/opt/java/openjdk"
|
|
MAVEN_HOME = "/usr/share/maven"
|
|
HOME = "/tmp"
|
|
LANG = "C.UTF-8"
|
|
}
|
|
$seenEnvironment = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
|
|
foreach ($entry in @($image.Config.Env)) {
|
|
$separator = ([string]$entry).IndexOf('=')
|
|
if ($separator -le 0) {
|
|
throw "SANDBOX_IMAGE_ENVIRONMENT_UNTRUSTED"
|
|
}
|
|
$name = ([string]$entry).Substring(0, $separator)
|
|
$value = ([string]$entry).Substring($separator + 1)
|
|
if (-not $approvedEnvironment.ContainsKey($name) -or
|
|
-not ([string]$approvedEnvironment[$name]).Equals($value, [System.StringComparison]::Ordinal) -or
|
|
-not $seenEnvironment.Add($name)) {
|
|
throw "SANDBOX_IMAGE_ENVIRONMENT_UNTRUSTED"
|
|
}
|
|
}
|
|
if ($seenEnvironment.Count -ne $approvedEnvironment.Count) {
|
|
throw "SANDBOX_IMAGE_ENVIRONMENT_UNTRUSTED"
|
|
}
|
|
|
|
[ordered]@{
|
|
schemaVersion = 1
|
|
status = "BUILT_AND_INSPECTED"
|
|
imageId = [string]$image.Id
|
|
dockerExecutableSha256 = $dockerSha256
|
|
dockerfileSha256 = $dockerfileSha256
|
|
dockerignoreSha256 = $dockerignoreSha256
|
|
dockerConfigDirectory = $runtimeConfig
|
|
dockerHost = $DockerHost
|
|
baseManifests = @(
|
|
"maven@sha256:edf045813426842617b1667456ddec0026885146465e890b897655d877ba3386"
|
|
"node@sha256:9137a20e25879e0b557227b57e3ee4e9af4bde29eb3db66134cd1723e84f830b"
|
|
)
|
|
} | ConvertTo-Json -Depth 6 -Compress
|
|
} finally {
|
|
$sessionInfo = [System.IO.DirectoryInfo]::new($buildConfig)
|
|
if ([System.IO.Directory]::Exists($buildConfig) -and
|
|
$sessionInfo.Parent.FullName.Equals($buildConfigRoot, [System.StringComparison]::OrdinalIgnoreCase) -and
|
|
$sessionInfo.Name.StartsWith("session-", [System.StringComparison]::Ordinal)) {
|
|
Remove-Item -LiteralPath $buildConfig -Recurse -Force
|
|
}
|
|
$processInfo = [System.IO.DirectoryInfo]::new($processRoot)
|
|
if ([System.IO.Directory]::Exists($processRoot) -and
|
|
$processInfo.Parent.FullName.Equals($buildConfigRoot, [System.StringComparison]::OrdinalIgnoreCase) -and
|
|
$processInfo.Name.StartsWith("process-", [System.StringComparison]::Ordinal)) {
|
|
Remove-Item -LiteralPath $processRoot -Recurse -Force
|
|
}
|
|
}
|