Initial harness scaffolding per SCOPE.md. Drives the win11 analyzer VM via qemu-guest-agent (runs as NT AUTHORITY\SYSTEM, same context as GE-Enforce in production - see reference-vm-qga-as-system memory note for why this is preferred over WinRM). Pieces: - lib/qga.sh - host-side helpers (qga round-trip, snapshot revert, share mount via cmdkey + net use, file upload). Source from any harness script. - lib/verify-state.ps1 - VM-side detection runner. Parses matrix.json, walks each app's verify block, prints PASS/FAIL with detail, exits 0 only if every check passes. Methods: Registry, File, FileVersion, Hash, FileGrep. - matrix.json - PC-type matrix data. Currently only Standard/Machine rows populated (apps + drift scenarios). Extending to other PC types is just adding rows. - B-enforce/run.sh - 5-phase orchestrator (stage / baseline / tamper / heal / idempotent). Defaults to Standard/Machine. SKIP_REVERT=1 for faster iteration without burning the snapshot revert. - B-enforce/tamper.ps1 - applies driftScenarios from matrix.json. Methods: RegRemove, RegSet, FileDelete, FileOverwrite, FileGrepDelete. Path A (imaging-time install) and remaining 8 PC-type rows are next. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
118 lines
4.4 KiB
Bash
Executable File
118 lines
4.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# qga.sh - host-side helpers for driving the win11 VM via qemu-guest-agent.
|
|
#
|
|
# Source this from harness scripts: `source "$(dirname "$0")/../lib/qga.sh"`
|
|
#
|
|
# All commands run as NT AUTHORITY\SYSTEM inside the VM (qemu-ga's service
|
|
# context). See reference-vm-qga-as-system memory note for why this is
|
|
# preferred over WinRM for dispatcher / manifest-engine tests.
|
|
|
|
set -euo pipefail
|
|
|
|
VM_DOMAIN="${VM_DOMAIN:-win11}"
|
|
VM_IP="${VM_IP:-192.168.122.225}"
|
|
HOST_SAMBA_USER="${HOST_SAMBA_USER:-camp}"
|
|
HOST_SAMBA_PASS="${HOST_SAMBA_PASS:-vos313}"
|
|
HOST_SAMBA_HOST="${HOST_SAMBA_HOST:-192.168.122.1}"
|
|
|
|
# Helper: fail loud
|
|
die() { echo "[ERROR] $*" >&2; exit 1; }
|
|
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*"; }
|
|
|
|
# Send a JSON command to qemu-ga, return parsed return field
|
|
qga() {
|
|
local payload="$1"
|
|
virsh -c qemu:///system qemu-agent-command "$VM_DOMAIN" "$payload" 2>/dev/null \
|
|
|| die "qga call failed: $payload"
|
|
}
|
|
|
|
# Run a PowerShell snippet inside the VM as SYSTEM. Stdin = snippet,
|
|
# stdout = exit-decorated combined stdout+stderr from the PS process.
|
|
qga_run_ps() {
|
|
python3 - <<'PY'
|
|
import base64, json, subprocess, sys, time, os
|
|
DOMAIN = os.environ.get('VM_DOMAIN', 'win11')
|
|
TIMEOUT = int(os.environ.get('QGA_TIMEOUT', '300'))
|
|
def virsh(cmd):
|
|
p = subprocess.run(['virsh','-c','qemu:///system','qemu-agent-command',DOMAIN, json.dumps(cmd)],
|
|
capture_output=True, text=True, timeout=120)
|
|
if p.returncode != 0:
|
|
sys.exit(f"virsh err: {p.stderr.strip()}")
|
|
return json.loads(p.stdout)['return']
|
|
snippet = sys.stdin.read()
|
|
args = ['-NoProfile','-ExecutionPolicy','Bypass','-Command', snippet]
|
|
pid = virsh({'execute':'guest-exec','arguments':{'path':'powershell.exe','arg':args,'capture-output':True}})['pid']
|
|
deadline = time.time() + TIMEOUT
|
|
while time.time() < deadline:
|
|
st = virsh({'execute':'guest-exec-status','arguments':{'pid':pid}})
|
|
if st.get('exited'):
|
|
out = base64.b64decode(st.get('out-data','')).decode('utf-8','replace')
|
|
err = base64.b64decode(st.get('err-data','')).decode('utf-8','replace')
|
|
rc = st.get('exitcode')
|
|
sys.stdout.write(out)
|
|
if err:
|
|
sys.stdout.write('\n--- STDERR ---\n')
|
|
sys.stdout.write(err)
|
|
sys.stdout.write(f'\n--- exit {rc} ---\n')
|
|
sys.exit(0 if rc == 0 else 1)
|
|
time.sleep(0.5)
|
|
sys.exit(f"timeout waiting for pid {pid}")
|
|
PY
|
|
}
|
|
|
|
# Wait until qemu-ga responds to guest-ping
|
|
vm_wait_for_ready() {
|
|
local max=${1:-90}
|
|
local i=0
|
|
while ! virsh -c qemu:///system qemu-agent-command "$VM_DOMAIN" '{"execute":"guest-ping"}' >/dev/null 2>&1; do
|
|
((i++))
|
|
if (( i > max )); then die "VM never became ready ($max attempts)"; fi
|
|
sleep 1
|
|
done
|
|
}
|
|
|
|
# Make sure VM is up and qga is alive. Start if not.
|
|
vm_start_if_needed() {
|
|
local state
|
|
state=$(virsh -c qemu:///system domstate "$VM_DOMAIN" 2>/dev/null || echo unknown)
|
|
if [[ "$state" != "running" ]]; then
|
|
log "VM is $state - starting"
|
|
virsh -c qemu:///system start "$VM_DOMAIN" >/dev/null
|
|
fi
|
|
vm_wait_for_ready 90
|
|
}
|
|
|
|
# Mount the host samba share inside the VM as SYSTEM. Idempotent.
|
|
# After this call Z:\ inside the VM points at /home/camp/pxe-images/.
|
|
vm_mount_share() {
|
|
qga_run_ps <<PS
|
|
cmdkey /add:$HOST_SAMBA_HOST /user:$HOST_SAMBA_USER /pass:$HOST_SAMBA_PASS | Out-Null
|
|
net use Z: \\\\$HOST_SAMBA_HOST\\pxe-images /persistent:no 2>&1 | Out-Null
|
|
"Z reachable: \$(Test-Path 'Z:\\')"
|
|
PS
|
|
}
|
|
|
|
# Upload a single file from host to VM by reading it into VM via WriteAllBytes.
|
|
# Best for tiny files (config, single ps1). For bigger files use vm_mount_share + Copy-Item.
|
|
vm_put_file() {
|
|
local local_path="$1" remote_path="$2"
|
|
[[ -f "$local_path" ]] || die "no such file: $local_path"
|
|
local b64
|
|
b64=$(base64 -w0 < "$local_path")
|
|
qga_run_ps <<PS
|
|
\$bytes = [System.Convert]::FromBase64String('$b64')
|
|
\$dst = '$remote_path'
|
|
New-Item -ItemType Directory -Force -Path (Split-Path \$dst -Parent) -ErrorAction SilentlyContinue | Out-Null
|
|
[System.IO.File]::WriteAllBytes(\$dst, \$bytes)
|
|
"wrote \$dst (\$(\$bytes.Length) bytes)"
|
|
PS
|
|
}
|
|
|
|
# Revert the VM to the blank-slate snapshot, then start + wait.
|
|
vm_revert_to_blank_slate() {
|
|
log "reverting to blank-slate snapshot..."
|
|
sudo virsh -c qemu:///system snapshot-revert "$VM_DOMAIN" blank-slate \
|
|
|| die "snapshot-revert failed (need sudo without password, or run script as root)"
|
|
vm_start_if_needed
|
|
}
|