mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-26 06:52:57 +08:00
fix(mxc): repair audit configuration and qualification runner
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
This commit is contained in:
@@ -544,3 +544,11 @@ The MSVC wrapper selects native test architecture from the machine environment,
|
||||
so an emulated SSH PowerShell does not incorrectly reject ARM64 qualification.
|
||||
Keep Windows kernel calls in the boundary library and retain shared supervisor
|
||||
unsafe-code bans, per-generation proxy authentication, and executable digests.
|
||||
|
||||
Use `windows:test:mxc-audit` to validate the shipped audit gateway config with
|
||||
its real parser, test JSONL coverage classification, and run the live seven-event
|
||||
ETW qualification. Build native release binaries first and set
|
||||
`OPENSHELL_WXC_EXEC_PATH`. The runner requires elevation or Performance Log Users
|
||||
membership and leaves a results bundle under the target directory. Ordinary MXC
|
||||
E2E success does not establish ETW audit coverage. Coverage must use structured
|
||||
JSONL and fail when an expected event is absent or has the wrong OCSF class.
|
||||
|
||||
@@ -321,3 +321,17 @@ workaround has been established.
|
||||
Provider environment publications reject case-insensitive duplicate names,
|
||||
invalid environment entries, and names reserved for the six injected TLS trust
|
||||
variables before installing them. Rejected updates retain the prior revision.
|
||||
|
||||
For live ETW audit coverage, set `OPENSHELL_WXC_EXEC_PATH` and run
|
||||
`mise run --skip-tools windows:test:mxc-audit` after building native release
|
||||
binaries. This requires elevation or Performance Log Users membership. It checks
|
||||
the shipped config and coverage classifier before requiring all seven event types
|
||||
in the durable JSONL output; the ordinary E2E scenarios do not check this path.
|
||||
|
||||
On the ARM64 test host (Windows build 28120, MXC 1.0.0), Sandboxing ETW records
|
||||
come from the shared Appinfo broker rather than the driver-owned executor.
|
||||
The consumer cannot correlate these records using the executor's PID and process
|
||||
generation, so live audit qualification currently fails even though the workload
|
||||
completes. The executor diagnostic log redacts its container identity. A trusted
|
||||
broker-to-launch correlation is needed; treating the shared broker PID as a
|
||||
sandbox identity would misattribute concurrent sandboxes and is not supported.
|
||||
|
||||
@@ -12,7 +12,7 @@ WHAT THIS PROVES / PRODUCES
|
||||
|
||||
The deliverable is the OCSF log: openshell-ocsf.<date>.log, one OCSF event
|
||||
object per line - the same schema and medium the Linux OpenShell pipeline
|
||||
produces (Windows is at functional parity).
|
||||
produces.
|
||||
|
||||
OCSF classes you will see:
|
||||
[6002] Application Lifecycle - sandbox created
|
||||
@@ -22,7 +22,8 @@ WHAT THIS PROVES / PRODUCES
|
||||
|
||||
PREREQUISITES (on this test box)
|
||||
- wxc-exec.exe present (default expected: C:\mxc-kit\bin\wxc-exec.exe)
|
||||
- process_container backend live (it was for our earlier runs)
|
||||
- process_container backend live
|
||||
- OpenSSL on PATH to generate disposable supervisor signing keys
|
||||
- Run ELEVATED (Run as administrator) OR from an account in the
|
||||
'Performance Log Users' group. Opening the real-time ETW session needs this;
|
||||
without it the run fails fast with a clear message.
|
||||
@@ -46,6 +47,9 @@ WHAT YOU GET BACK
|
||||
FILES IN THIS PACKAGE
|
||||
openshell-gateway.exe the gateway (self-contained; needs only VC++ runtime)
|
||||
openshell.exe the CLI
|
||||
openshell-supervisor.exe host supervisor
|
||||
openshell-windows-sandbox.exe sandbox runtime
|
||||
libz3.dll matching native Z3 runtime
|
||||
mxc-ocsf-audit.toml gateway/driver config (process_container, etw_audit=true, egress proxy)
|
||||
ocsf-audit.yaml sandbox policy (read-write grant to the share dir)
|
||||
run-ocsf-audit.ps1 the orchestrator you run
|
||||
@@ -54,9 +58,6 @@ FILES IN THIS PACKAGE
|
||||
|
||||
USEFUL OPTIONS
|
||||
-SandboxCount <n> Create n sandboxes (default 2). More sandboxes = more events.
|
||||
-NoProxy Skip the per-sandbox egress proxy. This omits ONLY the
|
||||
SandboxProxyConfigured config event; everything else is
|
||||
still produced. (Default is proxy ON for the full set.)
|
||||
-ShareDir <path> Host folder granted read-write to the workload. The script
|
||||
derives a disposable policy and per-sandbox config for it.
|
||||
-WxcExecPath <path> Path to wxc-exec.exe on this box.
|
||||
@@ -65,10 +66,10 @@ USEFUL OPTIONS
|
||||
-KeepRunning Leave the gateway running afterward for inspection.
|
||||
|
||||
NOTES
|
||||
- Disposable supervisor signing keys are protected outside the results bundle
|
||||
and removed after the gateway stops (retained with -KeepRunning).
|
||||
- The control plane between CLI and gateway runs with --disable-tls on loopback;
|
||||
that is unrelated to the OCSF audit path this example exercises.
|
||||
- A "supervisor session not connected" / ssh 255 message during sandbox create
|
||||
is EXPECTED on MXC and harmless - the agent already ran in-driver.
|
||||
- The proxy path requires the host-side CONNECT proxy and an absolute agent
|
||||
binary (the packaged config uses C:\Windows\System32\cmd.exe); the run script
|
||||
handles this for you.
|
||||
@@ -78,3 +79,15 @@ NOTES
|
||||
- MXC process audit events record the executable basename only. Command-line
|
||||
arguments are omitted from OCSF JSON and shorthand because they can contain
|
||||
credentials, signed URLs, or PII.
|
||||
|
||||
COVERAGE
|
||||
All seven required ETW event types must be present in structured JSONL.
|
||||
Console shorthand formatting does not affect coverage. The supervisor proxy
|
||||
is always provisioned; an OS proxy-configuration event is not required.
|
||||
|
||||
CURRENT QUALIFICATION LIMITATION
|
||||
On the ARM64 Windows build 28120 / MXC 1.0.0 test host, the Sandboxing provider
|
||||
emits from the shared Appinfo broker. The consumer's executor PID/generation
|
||||
anchor cannot correlate those records, so this qualification remains FAIL.
|
||||
Captured records are not sufficient: all seven attributed JSONL event types
|
||||
are still required. Do not use the shared broker PID as a sandbox identity.
|
||||
|
||||
@@ -13,7 +13,12 @@
|
||||
# disposable copy of this file. Workload command and
|
||||
# cwd are sandbox-scoped and passed separately through --driver-config-json.
|
||||
|
||||
[openshell]
|
||||
version = 2
|
||||
|
||||
[openshell.drivers.mxc]
|
||||
# This disposable example supplies its workload through --driver-config-json.
|
||||
allow_driver_config = true
|
||||
# Path to wxc-exec.exe on the box (patched by the run script; default is the
|
||||
# location observed on the MXC test boxes).
|
||||
wxc_exec_path = "C:\\mxc-kit\\bin\\wxc-exec.exe"
|
||||
|
||||
@@ -53,6 +53,88 @@ $PSNativeCommandUseErrorActionPreference = $false
|
||||
try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {}
|
||||
$OutputEncoding = [System.Text.Encoding]::UTF8
|
||||
|
||||
function Quote-NativeArgument([string]$value) {
|
||||
if ($value.Length -gt 0 -and $value -notmatch '[\s"]') { return $value }
|
||||
|
||||
$quoted = New-Object System.Text.StringBuilder
|
||||
[void]$quoted.Append('"')
|
||||
$backslashes = 0
|
||||
foreach ($ch in $value.ToCharArray()) {
|
||||
if ($ch -eq '\') {
|
||||
$backslashes++
|
||||
continue
|
||||
}
|
||||
if ($ch -eq '"') {
|
||||
[void]$quoted.Append(('\' * (2 * $backslashes + 1)))
|
||||
[void]$quoted.Append('"')
|
||||
} else {
|
||||
if ($backslashes -gt 0) { [void]$quoted.Append(('\' * $backslashes)) }
|
||||
[void]$quoted.Append($ch)
|
||||
}
|
||||
$backslashes = 0
|
||||
}
|
||||
if ($backslashes -gt 0) { [void]$quoted.Append(('\' * (2 * $backslashes))) }
|
||||
[void]$quoted.Append('"')
|
||||
return $quoted.ToString()
|
||||
}
|
||||
|
||||
function Invoke-NativeCaptured([string]$filePath, [string[]]$argumentList, [int]$timeoutSeconds = 0) {
|
||||
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$startInfo.FileName = $filePath
|
||||
$startInfo.Arguments = (($argumentList | ForEach-Object { Quote-NativeArgument $_ }) -join ' ')
|
||||
$startInfo.UseShellExecute = $false
|
||||
$startInfo.CreateNoWindow = $true
|
||||
$startInfo.RedirectStandardOutput = $true
|
||||
$startInfo.RedirectStandardError = $true
|
||||
|
||||
$process = New-Object System.Diagnostics.Process
|
||||
$process.StartInfo = $startInfo
|
||||
try {
|
||||
if (-not $process.Start()) { throw "failed to start $filePath" }
|
||||
} catch {
|
||||
$message = $_.Exception.Message
|
||||
if ($message -match '(?i)Application Control policy has blocked this file') {
|
||||
$sha256 = try { (Get-FileHash -LiteralPath $filePath -Algorithm SHA256).Hash } catch { "unavailable" }
|
||||
$signature = try { (Get-AuthenticodeSignature -LiteralPath $filePath).Status } catch { "unavailable" }
|
||||
throw "Application Control blocked CLI launch '$filePath' (SHA256=$sha256; Authenticode=$signature). Review CodeIntegrity/Operational event 3077 to identify the blocking policy, then deploy or allow an approved binary. Original error: $message"
|
||||
}
|
||||
throw
|
||||
}
|
||||
$stdout = $process.StandardOutput.ReadToEndAsync()
|
||||
$stderr = $process.StandardError.ReadToEndAsync()
|
||||
if ($timeoutSeconds -gt 0 -and -not $process.WaitForExit($timeoutSeconds * 1000)) {
|
||||
$process.Kill()
|
||||
}
|
||||
$process.WaitForExit()
|
||||
|
||||
return @{
|
||||
ExitCode = $process.ExitCode
|
||||
Output = @($stdout.Result, $stderr.Result) | Where-Object { $_ }
|
||||
}
|
||||
}
|
||||
|
||||
# Coverage comes from structured ETW-origin event messages, independent of the
|
||||
# console formatter. Class checks prevent unrelated log text from satisfying it.
|
||||
function Get-MxcAuditCoverage([object[]] $Events) {
|
||||
$rules = [ordered]@{
|
||||
"sandbox lifecycle (start)" = @(6002, "MXC sandbox '*' created with policy enforcement")
|
||||
"OS policy enforced" = @(5019, "MXC sandbox OS policy enforced")
|
||||
"OS policy configured" = @(5019, "MXC sandbox OS policy configured")
|
||||
"win32k lockdown applied" = @(5019, "MXC sandbox win32k lockdown applied")
|
||||
"UI restrictions applied" = @(5019, "MXC sandbox UI restrictions applied")
|
||||
"console reference plumbed" = @(5019, "MXC sandbox console reference plumbed")
|
||||
"process launch (executable identity)" = @(1007, "MXC sandbox launched process: *")
|
||||
}
|
||||
$coverage = [ordered]@{}
|
||||
foreach ($name in $rules.Keys) {
|
||||
$rule = $rules[$name]
|
||||
$coverage[$name] = @($Events | Where-Object {
|
||||
$_.class_uid -eq $rule[0] -and $_.message -like $rule[1]
|
||||
}).Count -gt 0
|
||||
}
|
||||
return $coverage
|
||||
}
|
||||
|
||||
$here = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
|
||||
|
||||
# Results bundle (everything we hand back) ------------------------------------
|
||||
@@ -192,8 +274,10 @@ $tomlSrc = Join-Path $here "mxc-ocsf-audit.toml"
|
||||
$toml = Join-Path $resultDir "mxc-ocsf-audit.used.toml" # disposable patched copy (bundled)
|
||||
$helloPath = Join-Path $ShareDir "hello.txt"
|
||||
|
||||
$secretDir = Join-Path $here ('audit-keys-' + [guid]::NewGuid().ToString('N'))
|
||||
$gw = $null
|
||||
$gatewayEtwSessions = @()
|
||||
$createdSandboxes = @()
|
||||
$passed = $true
|
||||
|
||||
try {
|
||||
@@ -221,6 +305,21 @@ try {
|
||||
}
|
||||
Info "wxc-exec: $WxcExecPath"
|
||||
|
||||
# Supervisor sessions require gateway-minted JWTs. Keep disposable private
|
||||
# keys outside the results bundle and restrict them to this test account.
|
||||
$openssl = (Get-Command openssl -ErrorAction Stop).Source
|
||||
New-Item -ItemType Directory $secretDir | Out-Null
|
||||
$ownerSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
||||
& icacls.exe $secretDir /inheritance:r /grant:r "*$($ownerSid):(OI)(CI)F" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw 'cannot protect disposable audit signing keys' }
|
||||
foreach ($keyArgs in @(
|
||||
@('genpkey', '-algorithm', 'ED25519', '-out', (Join-Path $secretDir 'signing.pem')),
|
||||
@('pkey', '-in', (Join-Path $secretDir 'signing.pem'), '-pubout', '-out', (Join-Path $secretDir 'public.pem'))
|
||||
)) {
|
||||
if ((Invoke-NativeCaptured $openssl $keyArgs).ExitCode -ne 0) { throw 'audit signing-key generation failed' }
|
||||
}
|
||||
Set-Content (Join-Path $secretDir 'kid') -Value 'mxc-ocsf-audit' -Encoding ASCII
|
||||
|
||||
# 2. Patch the disposable gateway config and derive a sandbox policy whose
|
||||
# filesystem grant matches -ShareDir.
|
||||
Step "Prepare gateway config and sandbox policy (disposable copies)"
|
||||
@@ -233,6 +332,17 @@ try {
|
||||
} else {
|
||||
$tomlText = [regex]::Replace($tomlText, '(?m)^\[openshell\.drivers\.mxc\]\s*$', "[openshell.drivers.mxc]`r`netw_audit = true")
|
||||
}
|
||||
$tomlText += @"
|
||||
|
||||
[openshell.gateway.auth]
|
||||
allow_unauthenticated_users = true
|
||||
|
||||
[openshell.gateway.gateway_jwt]
|
||||
signing_key_path = '$(Join-Path $secretDir 'signing.pem')'
|
||||
public_key_path = '$(Join-Path $secretDir 'public.pem')'
|
||||
kid_path = '$(Join-Path $secretDir 'kid')'
|
||||
gateway_id = 'mxc-ocsf-audit'
|
||||
"@
|
||||
Set-Content $toml -Value $tomlText -Encoding UTF8
|
||||
|
||||
$shareDirPolicy = $ShareDir.Replace('\', '/')
|
||||
@@ -248,20 +358,15 @@ try {
|
||||
$cmdExe = Join-Path $env:SystemRoot "System32\cmd.exe"
|
||||
if (-not (Test-Path $cmdExe -PathType Leaf)) { throw "cmd.exe not found at '$cmdExe'" }
|
||||
$helloPathCommand = $helloPath.Replace('\', '/')
|
||||
New-Item -ItemType Directory -Force $ShareDir | Out-Null
|
||||
$batch = Join-Path $ShareDir 'audit-workload.cmd'
|
||||
Set-Content -LiteralPath $batch -Encoding ASCII -Value ('@echo hello from openshell ocsf audit > "{0}"' -f $helloPathCommand)
|
||||
$driverConfig = @{
|
||||
mxc = @{
|
||||
command = @($cmdExe, "/c", "echo hello from openshell ocsf audit 1>`"$helloPathCommand`"")
|
||||
command = @($cmdExe, '/d', '/c', $batch)
|
||||
cwd = $shareDirPolicy
|
||||
}
|
||||
} | ConvertTo-Json -Compress -Depth 4
|
||||
# Windows PowerShell 5.1 strips embedded quotes when constructing a native
|
||||
# command line. Escape them so openshell.exe receives valid JSON.
|
||||
$driverConfigArg = if ($PSVersionTable.PSVersion.Major -lt 7) {
|
||||
$driverConfig.Replace('"', '\"')
|
||||
} else {
|
||||
$driverConfig
|
||||
}
|
||||
|
||||
Info "backend=process_container etw_audit=true runtime=supervisor+sandbox"
|
||||
Info "workload cwd=$shareDirPolicy policy grant=$shareDirPolicy"
|
||||
|
||||
@@ -342,23 +447,38 @@ try {
|
||||
|
||||
# 9. Register CLI -> gateway.
|
||||
Step "Register CLI -> gateway"
|
||||
& $cli gateway add "http://127.0.0.1:$Port" --local --name $GatewayName 2>&1 | ForEach-Object { Info $_ }
|
||||
if ($LASTEXITCODE -ne 0) { throw "gateway add failed (exit $LASTEXITCODE)" }
|
||||
& $cli gateway select $GatewayName 2>&1 | ForEach-Object { Info $_ }
|
||||
if ($LASTEXITCODE -ne 0) { throw "gateway select failed (exit $LASTEXITCODE)" }
|
||||
foreach ($cliArgs in @(
|
||||
@('gateway', 'add', "http://127.0.0.1:$Port", '--local', '--name', $GatewayName),
|
||||
@('gateway', 'select', $GatewayName)
|
||||
)) {
|
||||
$cliResult = Invoke-NativeCaptured $cli $cliArgs 30
|
||||
$cliResult.Output | ForEach-Object { Info $_ }
|
||||
if ($cliResult.ExitCode -ne 0) { throw "gateway registration failed (exit $($cliResult.ExitCode))" }
|
||||
}
|
||||
Ok "selected gateway '$GatewayName'"
|
||||
|
||||
# 10. Create N sandboxes. Each drives the Sandboxing provider -> a full OCSF
|
||||
# event burst. The post-create interactive attach failure is EXPECTED on
|
||||
# MXC (no in-sandbox supervisor) and harmless - the agent already ran.
|
||||
# event burst through the authenticated supervisor and sandbox runtime.
|
||||
Step "Create $SandboxCount sandbox(es) (drives the Sandboxing provider)"
|
||||
for ($i = 1; $i -le $SandboxCount; $i++) {
|
||||
$name = "ocsf$i"
|
||||
Info "-- creating $name --"
|
||||
try { & $cli sandbox create --name $name --policy $policy --driver-config-json $driverConfigArg --no-tty 2>&1 | ForEach-Object { Info $_ } }
|
||||
catch { Info "sandbox create attach: $($_.Exception.Message) (expected on MXC - agent ran in-driver; continuing)" }
|
||||
Remove-Item -LiteralPath $helloPath -Force -ErrorAction SilentlyContinue
|
||||
$created = Invoke-NativeCaptured $cli @('sandbox', 'create', '--name', $name,
|
||||
'--policy', $policy, '--driver-config-json', $driverConfig, '--detach') 90
|
||||
$created.Output | ForEach-Object { Info $_ }
|
||||
if ($created.ExitCode -ne 0) { throw "sandbox create failed (exit $($created.ExitCode))" }
|
||||
$createdSandboxes += $name
|
||||
$workloadDeadline = (Get-Date).AddSeconds(30)
|
||||
while (-not (Test-Path -LiteralPath $helloPath -PathType Leaf) -and (Get-Date) -lt $workloadDeadline) {
|
||||
Start-Sleep -Milliseconds 200
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $helloPath -PathType Leaf)) { throw 'audit workload did not produce its artifact' }
|
||||
# Give real-time ETW delivery time to complete before retiring attribution.
|
||||
Start-Sleep -Seconds 3
|
||||
try { & $cli sandbox delete $name 2>&1 | Out-Null } catch {}
|
||||
$deleted = Invoke-NativeCaptured $cli @('sandbox', 'delete', $name) 30
|
||||
if ($deleted.ExitCode -ne 0) { throw "sandbox delete failed (exit $($deleted.ExitCode)): $($deleted.Output)" }
|
||||
$createdSandboxes = @($createdSandboxes | Where-Object { $_ -ne $name })
|
||||
}
|
||||
}
|
||||
catch {
|
||||
@@ -366,6 +486,14 @@ catch {
|
||||
$passed = $false
|
||||
}
|
||||
finally {
|
||||
# A failed artifact/coverage check must not orphan a created runtime or leave
|
||||
# inherited log handles open when the gateway is stopped and results zipped.
|
||||
if (-not $KeepRunning -and $gw -and -not $gw.HasExited) {
|
||||
foreach ($name in $createdSandboxes) {
|
||||
try { $null = Invoke-NativeCaptured $cli @('sandbox', 'delete', $name) 30 }
|
||||
catch { Info "cleanup of $name failed: $($_.Exception.Message)" }
|
||||
}
|
||||
}
|
||||
if (-not $KeepRunning -and $gw -and $gatewayEtwSessions.Count -eq 0) {
|
||||
$gatewayEtwSessions = @(Get-MxcEtwSessions | Where-Object { $_.Pid -eq $gw.Id })
|
||||
}
|
||||
@@ -386,6 +514,13 @@ finally {
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $KeepRunning -and (Test-Path -LiteralPath $secretDir)) {
|
||||
foreach ($keyFile in @('signing.pem', 'public.pem', 'kid')) {
|
||||
Remove-Item -LiteralPath (Join-Path $secretDir $keyFile) -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Remove-Item -LiteralPath $secretDir -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# ---- summarise the OCSF audit trail --------------------------------------
|
||||
Exit-IsolatedCliEnvironment
|
||||
$logText = @()
|
||||
@@ -405,13 +540,14 @@ finally {
|
||||
$jsonlPath = if ($jsonlFiles.Count) { $jsonlFiles[0].FullName } else { $null }
|
||||
$classNames = @{ 6002 = "Application Lifecycle"; 5019 = "Device Config State Change"; 1007 = "Process Activity"; 2004 = "Detection Finding" }
|
||||
$classCounts = @{ 6002 = 0; 5019 = 0; 1007 = 0; 2004 = 0 }
|
||||
$jsonlCount = 0; $jsonlBad = 0; $sids = @(); $hosts = @()
|
||||
$jsonlCount = 0; $jsonlBad = 0; $sids = @(); $hosts = @(); $auditEvents = @()
|
||||
if ($jsonlPath) {
|
||||
$raw = @(Get-Content $jsonlPath -ErrorAction SilentlyContinue | Where-Object { $_.Trim() -ne "" })
|
||||
$jsonlCount = $raw.Count
|
||||
foreach ($line in $raw) {
|
||||
try {
|
||||
$o = $line | ConvertFrom-Json
|
||||
$auditEvents += $o
|
||||
if ($o.class_uid -ne $null -and $classCounts.ContainsKey([int]$o.class_uid)) { $classCounts[[int]$o.class_uid]++ }
|
||||
if ($o.metadata -and $o.metadata.uid) { $sids += [string]$o.metadata.uid }
|
||||
if ($o.device -and $o.device.hostname) { $hosts += [string]$o.device.hostname }
|
||||
@@ -421,28 +557,19 @@ finally {
|
||||
$hosts = @($hosts | Select-Object -Unique)
|
||||
}
|
||||
|
||||
# Event-type coverage (detected from the human-readable shorthand lines).
|
||||
function Seen([string]$pat) { [bool]($logText | Select-String -Pattern $pat -Quiet) }
|
||||
|
||||
# Expected happy-path ETW->OCSF event types for THIS run. The egress-proxy
|
||||
# event only fires when the proxy is enabled, so it only counts toward the
|
||||
# expected total when -NoProxy was NOT passed.
|
||||
$coreEvents = [ordered]@{
|
||||
"sandbox lifecycle (start)" = Seen "(?i)ocsf:.*LIFECYCLE:"
|
||||
"OS policy enforced" = Seen "(?i)ocsf:.*OS policy enforced"
|
||||
"OS policy configured" = Seen "(?i)ocsf:.*OS policy configured"
|
||||
"win32k lockdown applied" = Seen "(?i)ocsf:.*win32k lockdown"
|
||||
"UI restrictions applied" = Seen "(?i)ocsf:.*UI restrictions"
|
||||
"console reference plumbed" = Seen "(?i)ocsf:.*console reference plumbed"
|
||||
"process launch (executable identity)" = Seen "(?i)ocsf:.*PROC:LAUNCH"
|
||||
# Require all seven ETW event types in durable structured output.
|
||||
$coreEvents = Get-MxcAuditCoverage $auditEvents
|
||||
function SeenFinding([string] $Kind) {
|
||||
@($auditEvents | Where-Object {
|
||||
$_.class_uid -eq 2004 -and $_.message -like "*$Kind*"
|
||||
}).Count -gt 0
|
||||
}
|
||||
if ($proxyOn) { $coreEvents["egress proxy configured"] = Seen "(?i)ocsf:.*proxy configured" }
|
||||
|
||||
# Findings are anomaly / fallback signals - reported separately, NOT part of
|
||||
# the expected-coverage denominator (a clean run may emit none).
|
||||
$findingEvents = [ordered]@{
|
||||
"ActivityError finding" = Seen "(?i)ocsf:.*ActivityError"
|
||||
"FallbackError finding" = Seen "(?i)ocsf:.*FallbackError"
|
||||
"ActivityError finding" = SeenFinding "ActivityError"
|
||||
"FallbackError finding" = SeenFinding "FallbackError"
|
||||
}
|
||||
|
||||
$coreExpected = $coreEvents.Count
|
||||
@@ -467,7 +594,7 @@ user : $env:USERNAME (admin=$admin perfLogUsers=$plu)
|
||||
verdict : $verdict
|
||||
event coverage : $coreObserved of $coreExpected expected event types fired (+ $findingsObserved anomaly finding(s))
|
||||
queue overload : $(if ($consumerOverloaded) { 'YES - ETW records dropped; audit coverage gap' } else { 'no dropped ETW records observed' })
|
||||
proxy : $(if ($proxyOn) { 'on (full event set)' } else { 'off (-NoProxy; omits egress proxy event)' })
|
||||
proxy : authenticated supervisor proxy (ETW proxy event is not required)
|
||||
workload output : $(if ($workloadCompleted) { $helloPath } else { '(missing)' })
|
||||
wxc_exec : $WxcExecPath
|
||||
backend : process_container
|
||||
@@ -499,7 +626,7 @@ started, decoded the Sandboxing provider, attributed each event to a sandbox_id,
|
||||
reported no callback-queue overload, mapped events to OCSF, and wrote a durable
|
||||
JSONL audit log covering all $coreExpected
|
||||
expected event types across $classesSeen OCSF class(es) - the full Windows OCSF path
|
||||
end-to-end, at parity with the Linux pipeline.
|
||||
end-to-end for the Sandboxing provider event types listed above.
|
||||
"@
|
||||
Set-Content -Path (Join-Path $resultDir "summary.txt") -Value $summary -Encoding UTF8
|
||||
Write-Host $summary -ForegroundColor ($(if ($passed) { "Green" } else { "Red" }))
|
||||
|
||||
@@ -767,17 +767,30 @@ async fn run_lifecycle_inner(
|
||||
.map_err(|error| format!("start MXC ProcessContainer: {error}"))?;
|
||||
attach_child_logs(&context.sandbox.name, "sandbox", &mut boundary);
|
||||
attach_child_logs(&context.sandbox.name, "supervisor", &mut supervisor);
|
||||
if context.config.etw_audit
|
||||
&& let Some(pid) = boundary.id()
|
||||
&& let Ok(process_start_key) = crate::etw_consumer::child_process_start_key(&boundary)
|
||||
&& let Ok(mut attribution) = context.attribution.lock()
|
||||
{
|
||||
attribution.register_launch(
|
||||
&context.sandbox.id,
|
||||
&context.sandbox.name,
|
||||
pid,
|
||||
process_start_key,
|
||||
);
|
||||
if context.config.etw_audit {
|
||||
match boundary
|
||||
.id()
|
||||
.ok_or_else(|| "MXC child PID is unavailable".to_string())
|
||||
.and_then(|pid| {
|
||||
crate::etw_consumer::child_process_start_key(&boundary).map(|key| (pid, key))
|
||||
}) {
|
||||
Ok((pid, process_start_key)) => {
|
||||
let mut attribution = context
|
||||
.attribution
|
||||
.lock()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
attribution.register_launch(
|
||||
&context.sandbox.id,
|
||||
&context.sandbox.name,
|
||||
pid,
|
||||
process_start_key,
|
||||
);
|
||||
tracing::debug!(target: "mxc_etw", pid, process_start_key,
|
||||
sandbox_id = %context.sandbox.id, "registered driver-owned MXC audit launch");
|
||||
}
|
||||
Err(error) => tracing::warn!(sandbox_id = %context.sandbox.id, %error,
|
||||
"MXC ETW audit launch registration failed"),
|
||||
}
|
||||
}
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel();
|
||||
let (terminated_tx, terminated_rx) = watch::channel(false);
|
||||
|
||||
@@ -1567,6 +1567,9 @@ impl EtwEventProcessor {
|
||||
// instant `wxc-exec` starts, which can beat the driver's
|
||||
// `register_launch`. Hold the event for replay instead of dropping
|
||||
// it (see `drain_and_emit`).
|
||||
tracing::debug!(target: "mxc_etw", pid = ev.process_id,
|
||||
process_start_key = ?ev.process_start_key,
|
||||
"buffering uncorrelated MXC audit event");
|
||||
idx.buffer_unresolved(ev);
|
||||
None
|
||||
}
|
||||
@@ -2069,6 +2072,30 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn live_child_process_generation_is_stable_and_distinct() {
|
||||
let mut keys = Vec::new();
|
||||
for _ in 0..2 {
|
||||
let mut child = tokio::process::Command::new("powershell.exe")
|
||||
.args(["-NoProfile", "-Command", "Start-Sleep -Seconds 30"])
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.spawn()
|
||||
.expect("spawn owned telemetry probe");
|
||||
let first = child_process_start_key(&child);
|
||||
let second = child_process_start_key(&child);
|
||||
child.kill().await.expect("stop owned telemetry probe");
|
||||
let first = first.expect("live driver-owned handle has a generation key");
|
||||
assert_eq!(first, second.unwrap());
|
||||
keys.push(first);
|
||||
}
|
||||
assert_ne!(
|
||||
keys[0], keys[1],
|
||||
"different children require different generation keys"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extracts_process_start_key_from_etw_extended_data() {
|
||||
let unrelated = EVENT_HEADER_EXTENDED_DATA_ITEM {
|
||||
|
||||
@@ -266,3 +266,74 @@ if (-not $rejected) { throw 'conflicting runtimes accepted' }
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn audit_coverage_uses_structured_events_and_rejects_missing_or_wrong_classes() {
|
||||
let script = r#"
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$tokens = $null; $errors = $null
|
||||
$ast = [System.Management.Automation.Language.Parser]::ParseFile(
|
||||
$env:OPENSHELL_MXC_RUNNER, [ref]$tokens, [ref]$errors)
|
||||
if ($errors.Count) { throw "parse errors: $errors" }
|
||||
$function = $ast.Find({ param($node)
|
||||
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
|
||||
$node.Name -eq 'Get-MxcAuditCoverage'
|
||||
}, $true)
|
||||
if (!$function) { throw 'missing coverage function' }
|
||||
Invoke-Expression $function.Extent.Text
|
||||
foreach ($name in @('Quote-NativeArgument', 'Invoke-NativeCaptured')) {
|
||||
$helper = $ast.Find({ param($node)
|
||||
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $node.Name -eq $name
|
||||
}, $true)
|
||||
if (!$helper) { throw "missing $name" }
|
||||
Invoke-Expression $helper.Extent.Text
|
||||
}
|
||||
foreach ($code in @(0, 7)) {
|
||||
$result = Invoke-NativeCaptured 'powershell.exe' @('-NoProfile', '-Command',
|
||||
"[Console]::Error.WriteLine('status on stderr'); exit $code") 10
|
||||
if ($result.ExitCode -ne $code -or ($result.Output -join '') -notmatch 'status on stderr') {
|
||||
throw 'native stderr must be captured without hiding the exit code'
|
||||
}
|
||||
}
|
||||
$events = @(
|
||||
@{ class_uid=6002; message="MXC sandbox 'test' created with policy enforcement" },
|
||||
@{ class_uid=5019; message='MXC sandbox OS policy enforced' },
|
||||
@{ class_uid=5019; message='MXC sandbox OS policy configured' },
|
||||
@{ class_uid=5019; message='MXC sandbox win32k lockdown applied' },
|
||||
@{ class_uid=5019; message='MXC sandbox UI restrictions applied' },
|
||||
@{ class_uid=5019; message='MXC sandbox console reference plumbed' },
|
||||
@{ class_uid=1007; message='MXC sandbox launched process: cmd.exe' }
|
||||
) | ForEach-Object { $_ | ConvertTo-Json -Compress | ConvertFrom-Json }
|
||||
$coverage = Get-MxcAuditCoverage $events
|
||||
if ($coverage.Count -ne 7 -or @($coverage.Values | Where-Object { $_ }).Count -ne 7) {
|
||||
throw 'complete structured audit must satisfy seven event types'
|
||||
}
|
||||
foreach ($index in 0..6) {
|
||||
$subset = @(for ($i=0; $i -lt 7; $i++) { if ($i -ne $index) { $events[$i] } })
|
||||
if (@((Get-MxcAuditCoverage $subset).Values | Where-Object { $_ }).Count -ne 6) {
|
||||
throw "missing event $index must fail coverage"
|
||||
}
|
||||
}
|
||||
$events[6].class_uid = 5019
|
||||
if ((Get-MxcAuditCoverage $events)['process launch (executable identity)']) {
|
||||
throw 'matching text with wrong class must not pass'
|
||||
}
|
||||
if (@((Get-MxcAuditCoverage @()).Values | Where-Object { $_ }).Count -ne 0) {
|
||||
throw 'empty audit must not pass'
|
||||
}
|
||||
"#;
|
||||
let output = Command::new("powershell.exe")
|
||||
.args(["-NoProfile", "-Command", script])
|
||||
.env(
|
||||
"OPENSHELL_MXC_RUNNER",
|
||||
examples().join("run-ocsf-audit.ps1"),
|
||||
)
|
||||
.output()
|
||||
.expect("launch PowerShell coverage regression");
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{} {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1376,6 +1376,18 @@ ssh_gateway_port = 8080
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mxc_audit_example_loads_with_workload_opt_in() {
|
||||
let path = Path::new(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../openshell-driver-mxc/examples/mxc-ocsf-audit.toml"
|
||||
));
|
||||
let file = load(path).expect("shipped MXC audit config must load");
|
||||
let driver = &file.openshell.drivers["mxc"];
|
||||
assert_eq!(driver["allow_driver_config"].as_bool(), Some(true));
|
||||
assert_eq!(driver["etw_audit"].as_bool(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_version_in_nonempty_file() {
|
||||
let tmp = write_raw_tmp("[openshell]\n\n[openshell.gateway]\nname = \"test\"\n");
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true, Position = 0)]
|
||||
[ValidateSet("check", "lint", "build", "build-mxc-fixtures", "test", "test-precommit", "test-unsupported", "test-mxc-real", "test-mxc-boundary", "test-mxc-loopback", "test-mxc-network", "artifacts", "ci")]
|
||||
[ValidateSet("check", "lint", "build", "build-mxc-fixtures", "test", "test-precommit", "test-unsupported", "test-mxc-real", "test-mxc-boundary", "test-mxc-loopback", "test-mxc-network", "test-mxc-audit", "artifacts", "ci")]
|
||||
[string] $Action,
|
||||
|
||||
[Parameter(Position = 1)]
|
||||
@@ -657,7 +657,7 @@ if ($Action -in @("test", "test-precommit", "test-unsupported", "test-mxc-real")
|
||||
}
|
||||
}
|
||||
|
||||
if ($Action -in @("check", "lint", "build", "build-mxc-fixtures", "test", "test-precommit", "test-unsupported", "test-mxc-real", "test-mxc-boundary", "test-mxc-loopback", "test-mxc-network", "ci")) {
|
||||
if ($Action -in @("check", "lint", "build", "build-mxc-fixtures", "test", "test-precommit", "test-unsupported", "test-mxc-real", "test-mxc-boundary", "test-mxc-loopback", "test-mxc-network", "test-mxc-audit", "ci")) {
|
||||
$z3Features = Configure-Z3
|
||||
$Z3WorkspaceFeatures = $z3Features.WorkspaceFeatures
|
||||
$Z3ServerFeatures = $z3Features.ServerFeatures
|
||||
@@ -733,6 +733,33 @@ switch ($Action) {
|
||||
}
|
||||
}
|
||||
}
|
||||
"test-mxc-audit" {
|
||||
foreach ($rustTarget in $targets) {
|
||||
Assert-NativeTestTarget $rustTarget
|
||||
Invoke-VsCargo -RustTarget $rustTarget `
|
||||
-CargoArgs "cargo test --release -p openshell-driver-mxc --lib --target $rustTarget etw_consumer::tests -- --test-threads=1 --nocapture" `
|
||||
-LogName "test-$rustTarget-mxc-audit-attribution.log"
|
||||
Invoke-VsCargo -RustTarget $rustTarget `
|
||||
-CargoArgs "cargo test --release -p openshell-server --lib --target $rustTarget $Z3ServerFeatures mxc_audit_example -- --nocapture" `
|
||||
-LogName "test-$rustTarget-mxc-audit-config.log"
|
||||
Invoke-VsCargo -RustTarget $rustTarget `
|
||||
-CargoArgs "cargo test --release -p openshell-driver-mxc --test windows_demo_examples --target $rustTarget -- --test-threads=1 --nocapture" `
|
||||
-LogName "test-$rustTarget-mxc-audit-examples.log"
|
||||
$package = Join-Path $TargetDir "mxc-ocsf-qualification-$rustTarget"
|
||||
New-Item -ItemType Directory -Force $package | Out-Null
|
||||
foreach ($binary in @('openshell-gateway.exe', 'openshell.exe', 'openshell-supervisor.exe', 'openshell-windows-sandbox.exe', 'libz3.dll')) {
|
||||
Copy-Item -LiteralPath (Join-Path $TargetDir "$rustTarget/release/$binary") -Destination $package -Force
|
||||
}
|
||||
foreach ($artifact in @('run-ocsf-audit.ps1', 'mxc-ocsf-audit.toml', 'ocsf-audit.yaml')) {
|
||||
Copy-Item -LiteralPath (Join-Path $RepoRoot "crates/openshell-driver-mxc/examples/$artifact") -Destination $package -Force
|
||||
}
|
||||
$wxc = $env:OPENSHELL_WXC_EXEC_PATH
|
||||
if (-not $wxc) { throw 'Set OPENSHELL_WXC_EXEC_PATH to the real MXC executor.' }
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $package 'run-ocsf-audit.ps1') `
|
||||
-WxcExecPath $wxc -ShareDir (Join-Path $package 'work')
|
||||
if ($LASTEXITCODE -ne 0) { throw "Live MXC audit qualification failed; results are in $package" }
|
||||
}
|
||||
}
|
||||
"test-mxc-real" {
|
||||
foreach ($rustTarget in $targets) {
|
||||
Invoke-MxcRealTests $rustTarget
|
||||
|
||||
@@ -120,3 +120,8 @@ run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts
|
||||
description = "Qualify real HTTPS and child socket-owner policy against native MXC (requires Internet)"
|
||||
run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1"
|
||||
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-mxc-network native"
|
||||
|
||||
["windows:test:mxc-audit"]
|
||||
description = "Validate MXC audit fixtures and real seven-event JSONL coverage (requires ETW privileges and native release binaries)"
|
||||
run = "echo 'windows:* tasks require a Windows MSVC host' && exit 1"
|
||||
run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 test-mxc-audit native"
|
||||
|
||||
Reference in New Issue
Block a user