Files
pulse/scripts/installtests/secure_runtime_rootful_qualification_test.go
rcourtman e93ee38084 Tighten rootful runtime cleanup proof
Rootful evidence must reject non-running systemd states and prove no unrelated container runtime survives. Require exact manager state, mask distro containerd, recheck readiness after scenarios, and remove the complete Podman socket boundary.
2026-09-02 02:52:13 +01:00

1147 lines
56 KiB
Go

//go:build !windows
package installtests
// This file is a standalone rootful-container qualification packet. The live
// test is opt-in and must run only inside the disposable Ubuntu/systemd hosts
// created by scripts/run-secure-runtime-rootful-qualification.sh.
import (
"context"
"debug/buildinfo"
"encoding/json"
"errors"
"fmt"
"net"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
"syscall"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/agenthelper"
agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
)
const (
rootfulQualOptIn = "PULSE_SECURE_RUNTIME_ROOTFUL_QUALIFICATION"
rootfulQualOptInValue = "disposable-v1"
rootfulQualMarker = "/etc/pulse-secure-runtime-rootful-qualification"
rootfulQualReceiptPath = "/opt/pulse/result/rootful-receipt.json"
rootfulQualResultDir = "/opt/pulse/result"
rootfulQualFixture = "pulse-rootful-qualification-fixture:v1"
rootfulQualRunningName = "pulse-rootful-running"
rootfulQualExitedName = "pulse-rootful-exited"
rootfulQualHelperSock = "/run/pulse-agent/helper.sock"
rootfulQualBoundProbe = "/usr/local/libexec/pulse-rootful-qualification/dockeragent.test"
)
var (
rootfulQualBaseImagePattern = regexp.MustCompile(`^ubuntu@sha256:[0-9a-f]{64}$`)
rootfulQualScenarioOrder = []string{
"fresh_install",
"legacy_migration",
"collector_restart",
"helper_restart",
"helper_loss",
"helper_recovery",
"operation_bounds",
"update_preservation",
"authority_isolation",
"cleanup",
}
)
type rootfulQualReceipt struct {
SchemaVersion int `json:"schema_version"`
Kind string `json:"kind"`
Result string `json:"result"`
SourceCommit string `json:"source_commit"`
BaseImage string `json:"base_image"`
StartedAt string `json:"started_at"`
CompletedAt string `json:"completed_at"`
SourceHashes map[string]string `json:"source_hashes"`
Artifacts rootlessQualArtifacts `json:"artifacts"`
Runs []rootfulQualRun `json:"runs"`
}
type rootfulQualRun struct {
Host rootlessQualHost `json:"host"`
Runtime rootfulQualRuntime `json:"runtime"`
Scenarios []rootlessQualScenario `json:"scenarios"`
}
type rootfulQualRuntime struct {
Runtime string `json:"runtime"`
RuntimeVersion string `json:"runtime_version"`
DaemonID string `json:"daemon_id"`
DaemonRootless bool `json:"daemon_rootless"`
SocketPath string `json:"socket_path"`
SocketUID int `json:"socket_uid"`
SocketGID int `json:"socket_gid"`
SocketMode string `json:"socket_mode"`
SocketType string `json:"socket_type"`
SocketSymlink bool `json:"socket_symlink"`
}
type rootfulQualDaemon struct {
runtime string
unit string
socket string
dataRoot string
runRoot string
fixture string
}
func TestSecureRuntimeRootfulQualification(t *testing.T) {
if os.Getenv(rootfulQualOptIn) != rootfulQualOptInValue {
t.Skip("run through scripts/run-secure-runtime-rootful-qualification.sh inside its disposable systemd container")
}
runtimeKind := strings.TrimSpace(os.Getenv("PULSE_ROOTFUL_RUNTIME"))
receiptPath := strings.TrimSpace(os.Getenv("PULSE_ROOTFUL_RECEIPT"))
rootfulQualRequireDisposableHost(t, runtimeKind, receiptPath)
rootfulQualAssertSystemContainerdDisabled(t)
collector := secureRuntimeReadArtifact(t, "PULSE_SECURE_RUNTIME_COLLECTOR")
collectorSignature := secureRuntimeReadSignature(t, "PULSE_SECURE_RUNTIME_COLLECTOR_SIGNATURE")
helper := secureRuntimeReadArtifact(t, "PULSE_SECURE_RUNTIME_HELPER")
collectorVersion := secureRuntimeArtifactVersion(t, "PULSE_SECURE_RUNTIME_COLLECTOR")
installerPath := strings.TrimSpace(os.Getenv("PULSE_SECURE_RUNTIME_INSTALLER"))
if !filepath.IsAbs(installerPath) {
t.Fatalf("PULSE_SECURE_RUNTIME_INSTALLER must be absolute: %q", installerPath)
}
started := time.Now().UTC()
fixture := newSecureRuntimeLabFixture(collector, collectorSignature, helper, nil, collectorVersion)
defer fixture.actionServer.Shutdown()
server := httptestNewServer(t, fixture)
defer server.Close()
collectorCredential := secureRuntimeLabToken
daemon := rootfulQualDaemonFor(runtimeKind)
defer rootlessQualBestEffortStop(daemon.unit, rootfulQualHungUnit(runtimeKind))
rootfulQualPrepareFixture(t, daemon)
rootfulQualStartDaemon(t, daemon)
rootfulQualCreateFixtures(t, daemon)
baseline := rootfulQualRuntimeBaseline(t, daemon)
if baseline.Count != 2 {
t.Fatalf("rootful %s baseline count = %d, want 2", runtimeKind, baseline.Count)
}
runtimeVersion := rootfulQualRuntimeVersion(t, daemon)
daemonID := rootfulQualDaemonID(t, daemon)
socketUID, socketGID, socketMode := rootfulQualSocketIdentity(t, daemon.socket)
if socketUID != 0 {
t.Fatalf("rootful runtime socket UID = %d, want 0", socketUID)
}
var scenarios []rootlessQualScenario
appendScenario := func(name string, began time.Time, report *agentsdocker.Report, evidence map[string]any) {
scenario := rootlessQualScenario{
Name: name, Result: "passed", StartedAt: began.Format(time.RFC3339Nano),
CompletedAt: time.Now().UTC().Format(time.RFC3339Nano), Evidence: evidence,
}
if report != nil {
stream, sequence, ok := agentshost.ParseReportSequenceID(report.SequenceID)
if !ok {
t.Fatalf("scenario %s received invalid sequence ID %q", name, report.SequenceID)
}
scenario.ReportStreamID = &stream
scenario.ReportSequence = &sequence
}
scenarios = append(scenarios, scenario)
}
freshStarted := time.Now().UTC()
secureRuntimeRunInstaller(t, installerPath, server.URL,
"--least-privilege", "--enable-privileged-helper", "--enable-docker")
fresh := rootfulQualWaitSummary(t, fixture, freshStarted, runtimeKind, baseline.SemanticDigest, 75*time.Second)
secureRuntimeAssertSafeProfile(t)
secureRuntimeAssertHelperProtocol(t)
rootfulQualAssertCollectorSocketDenied(t, daemon.socket)
freshPID := secureRuntimeCollectorMainPID(t)
freshHelperPID, _ := rootfulQualUnitIdentity(t, "pulse-agent-helper.service")
appendScenario("fresh_install", freshStarted, &fresh.Report,
rootfulQualSummaryEvidence(freshPID, freshHelperPID, daemonID, fresh.Report))
rootlessQualUninstallPulse(t, installerPath, server.URL, collectorCredential)
rootlessQualAssertPulseRemoved(t)
if registered, revoked, uninstalls := fixture.collectorLifecycleSnapshot(); registered || !revoked || uninstalls != 1 {
t.Fatalf("fresh collector uninstall was not durably modeled: registered=%t revoked=%t uninstalls=%d", registered, revoked, uninstalls)
}
fixture.replaceCollectorCredential(secureRuntimeLabTokenV2, secureRuntimeCollectorBindingV2)
collectorCredential = secureRuntimeLabTokenV2
migrationStarted := time.Now().UTC()
secureRuntimeRunInstallerWithCollectorCredential(t, installerPath, server.URL, collectorCredential,
"--enable-commands", "--command-authority", "command-capable", "--enable-docker")
legacy := rootlessQualWaitReport(t, fixture, migrationStarted, 75*time.Second, func(report agentsdocker.Report) bool {
return rootlessQualComplete(report) && report.Host.CollectionMode == "" && report.Host.Runtime == runtimeKind && rootlessQualSemanticDigest(report) == baseline.SemanticDigest
})
secureRuntimeAssertRootCommandProfile(t)
legacyPID := secureRuntimeCollectorMainPID(t)
applyStarted := time.Now().UTC()
secureRuntimeRunInstallerWithCollectorCredential(t, installerPath, server.URL, collectorCredential, "--safe-profile-apply")
migrated := rootfulQualWaitSummary(t, fixture, applyStarted, runtimeKind, baseline.SemanticDigest, 75*time.Second)
secureRuntimeAssertSafeProfile(t)
secureRuntimeAssertHelperProtocol(t)
rootfulQualAssertCollectorSocketDenied(t, daemon.socket)
migratedPID := secureRuntimeCollectorMainPID(t)
if legacyPID == migratedPID || fixture.authorityReductionCount() < 1 || rootlessQualSemanticDigest(legacy.Report) != rootlessQualSemanticDigest(migrated.Report) {
t.Fatalf("rootful migration did not replace/reduce the collector with summary parity")
}
migratedHelperPID, _ := rootfulQualUnitIdentity(t, "pulse-agent-helper.service")
migrationEvidence := rootfulQualSummaryEvidence(migratedPID, migratedHelperPID, daemonID, migrated.Report)
migrationEvidence["legacy_profile"] = "root-command-capable"
migrationEvidence["target_profile"] = "typed-helper-monitoring-only"
migrationEvidence["authority_reduced"] = true
migrationEvidence["legacy_collector_pid"] = legacyPID
appendScenario("legacy_migration", migrationStarted, &migrated.Report, migrationEvidence)
collectorRestartStarted := time.Now().UTC()
previousStream, _, _ := agentshost.ParseReportSequenceID(migrated.Report.SequenceID)
secureRuntimeCommand(t, 20*time.Second, "systemctl", "restart", "pulse-agent.service")
collectorPID := secureRuntimeCollectorMainPID(t)
collectorRestart := rootfulQualWaitSummary(t, fixture, collectorRestartStarted, runtimeKind, baseline.SemanticDigest, 75*time.Second)
collectorStream, _, _ := agentshost.ParseReportSequenceID(collectorRestart.Report.SequenceID)
if collectorPID == migratedPID || collectorStream == previousStream {
t.Fatalf("collector restart did not replace PID/report stream: pid=%d/%d stream=%s/%s", migratedPID, collectorPID, previousStream, collectorStream)
}
helperPID, _ := rootfulQualUnitIdentity(t, "pulse-agent-helper.service")
collectorRestartEvidence := rootfulQualSummaryEvidence(collectorPID, helperPID, daemonID, collectorRestart.Report)
collectorRestartEvidence["previous_collector_pid"] = migratedPID
collectorRestartEvidence["previous_report_stream_id"] = previousStream
appendScenario("collector_restart", collectorRestartStarted, &collectorRestart.Report, collectorRestartEvidence)
helperPIDBefore, helperInvocationBefore := rootfulQualUnitIdentity(t, "pulse-agent-helper.service")
secureRuntimeCommand(t, 20*time.Second, "systemctl", "restart", "pulse-agent-helper.service")
helperRestartStarted := time.Now().UTC()
helperPIDAfter, helperInvocationAfter := rootfulQualUnitIdentity(t, "pulse-agent-helper.service")
if helperPIDBefore == helperPIDAfter || helperInvocationBefore == helperInvocationAfter {
t.Fatalf("helper restart did not replace exact service identity")
}
helperRestart := rootfulQualWaitSummary(t, fixture, helperRestartStarted, runtimeKind, baseline.SemanticDigest, 75*time.Second)
helperRestartEvidence := rootfulQualSummaryEvidence(collectorPID, helperPIDAfter, daemonID, helperRestart.Report)
helperRestartEvidence["previous_helper_pid"] = helperPIDBefore
helperRestartEvidence["previous_helper_invocation_id"] = helperInvocationBefore
helperRestartEvidence["helper_invocation_id"] = helperInvocationAfter
appendScenario("helper_restart", helperRestartStarted, &helperRestart.Report, helperRestartEvidence)
lossStarted := time.Now().UTC()
secureRuntimeCommand(t, 20*time.Second, "systemctl", "stop", "pulse-agent-helper.socket", "pulse-agent-helper.service")
loss := rootfulQualWaitStatusOnly(t, fixture, lossStarted, runtimeKind, 75*time.Second)
lossStream, lossSequence, _ := agentshost.ParseReportSequenceID(loss.Report.SequenceID)
if len(loss.Report.Containers) != 0 || secureRuntimeCollectorMainPID(t) != collectorPID {
t.Fatal("helper loss emitted an authoritative empty inventory or replaced the collector")
}
appendScenario("helper_loss", lossStarted, &loss.Report, map[string]any{
"collector_pid": collectorPID, "previous_helper_pid": helperPIDAfter,
"collection_mode": "typed-helper-unavailable-status-only", "helper_available": false,
"status_only": true, "inventory_complete": false, "inventory_present": false,
"authoritative_inventory_replacement": false,
"previous_authoritative_inventory_count": baseline.Count,
"previous_authoritative_semantic_sha256": baseline.SemanticDigest,
"operation_status": "degraded", "operation": agenthelper.OperationContainerInventory,
"container_updates_enabled": false, "container_actions_enabled": false, "direct_socket_access": false,
})
recoveryStarted := time.Now().UTC()
secureRuntimeCommand(t, 20*time.Second, "systemctl", "start", "pulse-agent-helper.socket")
recovered := rootfulQualWaitSummary(t, fixture, recoveryStarted, runtimeKind, baseline.SemanticDigest, 75*time.Second)
secureRuntimeAssertHelperProtocol(t)
helperRecoveryPID, _ := rootfulQualUnitIdentity(t, "pulse-agent-helper.service")
recoveryStream, recoverySequence, _ := agentshost.ParseReportSequenceID(recovered.Report.SequenceID)
if recoveryStream != lossStream || recoverySequence <= lossSequence {
t.Fatalf("helper recovery did not advance the same report stream")
}
recoveryEvidence := rootfulQualSummaryEvidence(collectorPID, helperRecoveryPID, daemonID, recovered.Report)
recoveryEvidence["previous_helper_pid"] = helperPIDAfter
recoveryEvidence["previous_status_report_sequence"] = lossSequence
appendScenario("helper_recovery", recoveryStarted, &recovered.Report, recoveryEvidence)
boundStarted := time.Now().UTC()
rootfulQualStopDaemon(t, daemon)
rootfulQualStartHungDaemon(t, daemon)
probeElapsed := rootfulQualRunBoundProbe(t, 2*time.Second)
probeCompletedAt := time.Now().UTC()
boundStatus := rootfulQualWaitStatusOnly(t, fixture, probeCompletedAt, runtimeKind, 45*time.Second)
boundStatusStream, boundStatusSequence, _ := agentshost.ParseReportSequenceID(boundStatus.Report.SequenceID)
rootlessQualBestEffortStop(rootfulQualHungUnit(runtimeKind))
_ = os.Remove(daemon.socket)
rootfulQualStartDaemon(t, daemon)
boundRecoveryStarted := time.Now().UTC()
boundRecovery := rootfulQualWaitSummary(t, fixture, boundRecoveryStarted, runtimeKind, baseline.SemanticDigest, 75*time.Second)
boundRecoveryStream, boundRecoverySequence, _ := agentshost.ParseReportSequenceID(boundRecovery.Report.SequenceID)
if boundRecoveryStream != boundStatusStream || boundRecoverySequence <= boundStatusSequence {
t.Fatalf("bounded operation recovery did not advance the same report stream")
}
appendScenario("operation_bounds", boundStarted, &boundRecovery.Report, map[string]any{
"collector_pid": collectorPID, "helper_pid": helperRecoveryPID,
"operation": agenthelper.OperationContainerInventory, "failure_class": "bounded-timeout",
"collection_mode": agentsdocker.CollectionModeTypedHelperSummary,
"inventory_complete": true, "full_fields_present": false, "stats_present": false,
"secondary_structure_sha256": "",
"deadline_ms": 2000, "elapsed_ms": probeElapsed.Milliseconds(), "bounded_failure_observed": true,
"status_only_report_sequence": boundStatusSequence, "recovery_report_sequence": boundRecoverySequence,
"previous_authoritative_inventory_count": baseline.Count,
"previous_authoritative_semantic_sha256": baseline.SemanticDigest,
"recovery_inventory_count": baseline.Count, "recovery_semantic_sha256": rootlessQualSemanticDigest(boundRecovery.Report),
"authoritative_empty_replacement": false, "collector_alive": true, "helper_alive": true,
"container_updates_enabled": false, "container_actions_enabled": false, "direct_socket_access": false,
})
updateStarted := time.Now().UTC()
preUpdatePID := secureRuntimeCollectorMainPID(t)
preUpdateStream, _, _ := agentshost.ParseReportSequenceID(boundRecovery.Report.SequenceID)
secureRuntimeRunInstallerWithCollectorCredential(t, installerPath, server.URL, collectorCredential, "--update")
postUpdatePID := secureRuntimeCollectorMainPID(t)
updated := rootfulQualWaitSummary(t, fixture, updateStarted, runtimeKind, baseline.SemanticDigest, 90*time.Second)
postUpdateStream, _, _ := agentshost.ParseReportSequenceID(updated.Report.SequenceID)
if postUpdatePID == preUpdatePID || postUpdateStream == preUpdateStream {
t.Fatalf("ordinary update did not restart the safe collector: pid=%d/%d stream=%s/%s", preUpdatePID, postUpdatePID, preUpdateStream, postUpdateStream)
}
postUpdateHelperPID, _ := rootfulQualUnitIdentity(t, "pulse-agent-helper.service")
if postUpdateHelperPID != helperRecoveryPID {
t.Fatalf("ordinary collector update replaced the independent helper process: pid=%d/%d", helperRecoveryPID, postUpdateHelperPID)
}
updateEvidence := rootfulQualSummaryEvidence(postUpdatePID, postUpdateHelperPID, daemonID, updated.Report)
updateEvidence["previous_collector_pid"] = preUpdatePID
updateEvidence["previous_helper_pid"] = helperRecoveryPID
updateEvidence["previous_report_stream_id"] = preUpdateStream
updateEvidence["update_applied"] = true
updateEvidence["collector_binary_sha256"] = secureRuntimeHash(secureRuntimeReadFile(t, "/usr/local/bin/pulse-agent"))
updateEvidence["helper_binary_sha256"] = secureRuntimeHash(secureRuntimeReadFile(t, "/usr/local/lib/pulse-agent/pulse-agent-helper"))
appendScenario("update_preservation", updateStarted, &updated.Report, updateEvidence)
authorityStarted := time.Now().UTC()
collectorUID := rootlessQualUID(t, "pulse-agent")
groups := strings.Fields(rootlessQualCommand(t, 10*time.Second, "id", "-nG", "pulse-agent"))
if slices.Contains(groups, "docker") || slices.Contains(groups, "podman") {
t.Fatalf("safe collector retained a rootful daemon group: %v", groups)
}
rootfulQualAssertCollectorSocketDenied(t, daemon.socket)
helperNetworkDenied := rootlessQualAssertHelperNetworkDenied(t)
commandSessionPresent := fixture.actionServer.IsAgentConnectedForOrganization(secureRuntimeLabOrgID, secureRuntimeLabAgentID)
if !helperNetworkDenied || commandSessionPresent || secureRuntimeCollectorHasArgument("--enable-commands") || secureRuntimeCollectorProcessUID(t) != collectorUID {
t.Fatal("rootful authority isolation did not remain exact")
}
appendScenario("authority_isolation", authorityStarted, nil, map[string]any{
"collector_pid": postUpdatePID, "collector_uid": collectorUID, "effective_uid": collectorUID,
"effective_root": false, "safe_profile_enabled": true, "commands_enabled": false,
"privileged_helper_enabled": true, "reduction_request_observed": true,
"collector_command_transport_present": false, "collector_command_session_present": false,
"container_actions_enabled": false, "container_updates_enabled": false,
"rootful_socket_access": false, "direct_socket_access": false, "helper_network_access": false,
})
cleanupStarted := time.Now().UTC()
rootlessQualUninstallPulse(t, installerPath, server.URL, collectorCredential)
rootfulQualRemoveFixtures(t, daemon)
rootfulQualStopDaemon(t, daemon)
rootfulQualRemoveRuntimeState(t, daemon)
rootlessQualAssertPulseRemoved(t)
if registered, revoked, uninstalls := fixture.collectorLifecycleSnapshot(); registered || !revoked || uninstalls != 2 {
t.Fatalf("final collector uninstall was not durably modeled: registered=%t revoked=%t uninstalls=%d", registered, revoked, uninstalls)
}
if _, err := os.Lstat(daemon.socket); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("cleanup left rootful runtime socket %s: %v", daemon.socket, err)
}
stateClean := rootfulQualRuntimeStateClean(daemon)
if !stateClean {
t.Fatal("cleanup left rootful runtime state")
}
appendScenario("cleanup", cleanupStarted, nil, map[string]any{
"collector_stopped": true, "helper_stopped": true, "runtime_stopped": true,
"socket_absent": true, "fixtures_removed": true, "state_clean": true,
})
receipt := rootfulQualReceipt{
SchemaVersion: 1, Kind: "pulse-secure-runtime-rootful-qualification", Result: "passed",
SourceCommit: strings.TrimSpace(os.Getenv("PULSE_ROOTFUL_SOURCE_COMMIT")),
BaseImage: strings.TrimSpace(os.Getenv("PULSE_ROOTFUL_UBUNTU_IMAGE")),
StartedAt: started.Format(time.RFC3339Nano), CompletedAt: time.Now().UTC().Format(time.RFC3339Nano),
SourceHashes: rootfulQualSourceHashes(t), Artifacts: rootfulQualArtifactIdentities(t, installerPath),
Runs: []rootfulQualRun{{
Host: rootlessQualHost{
MachineID: strings.TrimSpace(string(rootlessQualReadFile(t, "/etc/machine-id"))), Architecture: runtime.GOARCH,
Kernel: rootlessQualCommand(t, 10*time.Second, "uname", "-srvmo"),
SystemdVersion: strings.SplitN(rootlessQualCommand(t, 10*time.Second, "systemctl", "--version"), "\n", 2)[0],
},
Runtime: rootfulQualRuntime{
Runtime: runtimeKind, RuntimeVersion: runtimeVersion, DaemonID: daemonID, DaemonRootless: false,
SocketPath: daemon.socket, SocketUID: socketUID, SocketGID: socketGID, SocketMode: socketMode,
SocketType: "unix", SocketSymlink: false,
},
Scenarios: scenarios,
}},
}
if err := rootfulQualValidateReceipt(receipt, 1); err != nil {
t.Fatalf("generated rootful receipt failed validation: %v", err)
}
rootlessQualWriteJSON(t, receiptPath, receipt)
}
// TestSecureRuntimeRootfulBoundProbe is invoked as the installed collector UID
// by the live qualification test. It proves the real helper and provider honor
// a caller-supplied bounded deadline against an accepted but unresponsive
// rootful daemon connection.
func TestSecureRuntimeRootfulBoundProbe(t *testing.T) {
if os.Getenv("PULSE_ROOTFUL_BOUND_PROBE") != "1" {
t.Skip("internal rootful qualification subprocess")
}
deadlineMillis, err := strconv.Atoi(os.Getenv("PULSE_ROOTFUL_BOUND_DEADLINE_MS"))
if err != nil || deadlineMillis < 1 {
t.Fatalf("invalid bound-probe deadline: %v", err)
}
deadline := time.Duration(deadlineMillis) * time.Millisecond
client, err := agenthelper.NewClient(agenthelper.ClientConfig{SocketPath: rootfulQualHelperSock, MaxDeadline: deadline})
if err != nil {
t.Fatal(err)
}
var response agenthelper.ContainerInventoryResult
_, err = client.Call(context.Background(), agenthelper.OperationContainerInventory, agenthelper.OperationVersion1, deadline, struct{}{}, &response)
var remote *agenthelper.RemoteError
var networkError net.Error
typedDeadline := errors.As(err, &remote) && remote.Code == agenthelper.ErrorDeadlineExceeded
localDeadline := errors.As(err, &networkError) && networkError.Timeout()
if !typedDeadline && !localDeadline {
t.Fatalf("bounded helper operation error = %T %v", err, err)
}
fmt.Println("ROOTFUL_BOUND_RESULT=deadline_exceeded")
}
func httptestNewServer(t *testing.T, handler *secureRuntimeLabFixture) *httptest.Server {
t.Helper()
return httptest.NewServer(handler)
}
func rootfulQualRequireDisposableHost(t *testing.T, runtimeKind, receiptPath string) {
t.Helper()
if os.Geteuid() != 0 || (runtimeKind != "docker" && runtimeKind != "podman") {
t.Fatalf("qualification requires root and PULSE_ROOTFUL_RUNTIME=docker|podman")
}
marker, err := os.ReadFile(rootfulQualMarker)
if err != nil || strings.TrimSpace(string(marker)) != rootfulQualOptInValue {
t.Fatalf("disposable marker is absent or invalid: %v", err)
}
if receiptPath != rootfulQualReceiptPath {
t.Fatalf("PULSE_ROOTFUL_RECEIPT must use %q: %q", rootfulQualReceiptPath, receiptPath)
}
info, err := os.Lstat(rootfulQualResultDir)
if err != nil {
t.Fatal(err)
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 || info.Mode().Perm() != 0o700 || stat.Uid != 0 {
t.Fatalf("rootful result directory must be root-owned mode 0700: mode=%s stat=%#v", info.Mode(), info.Sys())
}
osRelease := string(rootlessQualReadFile(t, "/etc/os-release"))
if !strings.Contains(osRelease, "VERSION_ID=\"24.04\"") && !strings.Contains(osRelease, "VERSION_ID=24.04") {
t.Fatal("qualification host is not Ubuntu 24.04")
}
if _, err := os.Stat("/run/systemd/system"); err != nil {
t.Fatalf("qualification host is not booted under systemd: %v", err)
}
if rootlessQualHasDefaultRoute(string(rootlessQualReadFile(t, "/proc/net/route"))) {
t.Fatal("qualification workload must run with outer-container networking disabled")
}
}
func rootfulQualDaemonFor(runtimeKind string) rootfulQualDaemon {
if runtimeKind == "docker" {
return rootfulQualDaemon{runtime: runtimeKind, unit: "pulse-rootful-docker", socket: "/var/run/docker.sock", dataRoot: "/var/lib/pulse-rootful-docker", runRoot: "/run/pulse-rootful-docker", fixture: "/opt/pulse/rootful-fixture"}
}
return rootfulQualDaemon{runtime: runtimeKind, unit: "pulse-rootful-podman", socket: "/run/podman/podman.sock", dataRoot: "/var/lib/pulse-rootful-podman", runRoot: "/run/pulse-rootful-podman", fixture: "/opt/pulse/rootful-fixture"}
}
func rootfulQualPrepareFixture(t *testing.T, daemon rootfulQualDaemon) {
t.Helper()
rootlessQualCommand(t, 10*time.Second, "install", "-d", "-o", "root", "-g", "root", "-m", "0700", daemon.fixture)
containerfile := "FROM scratch\nCOPY busybox /busybox\nENTRYPOINT [\"/busybox\"]\n"
if err := os.WriteFile(filepath.Join(daemon.fixture, "Containerfile"), []byte(containerfile), 0o600); err != nil {
t.Fatal(err)
}
busybox, err := os.ReadFile("/bin/busybox")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(daemon.fixture, "busybox"), busybox, 0o700); err != nil {
t.Fatal(err)
}
}
func rootfulQualStartDaemon(t *testing.T, daemon rootfulQualDaemon) {
t.Helper()
rootlessQualBestEffortStop(daemon.unit)
_ = os.Remove(daemon.socket)
rootlessQualCommand(t, 10*time.Second, "install", "-d", "-o", "root", "-g", "root", "-m", "0755", filepath.Dir(daemon.socket))
if daemon.runtime == "docker" {
rootlessQualCommand(t, 20*time.Second, "systemd-run", "--quiet", "--collect", "--unit", daemon.unit, "--property=Type=exec", "--",
"/usr/bin/dockerd", "--host=unix://"+daemon.socket, "--data-root="+daemon.dataRoot,
"--exec-root=/run/pulse-rootful-docker", "--pidfile=/run/pulse-rootful-docker.pid", "--storage-driver=vfs", "--iptables=false", "--bridge=none")
} else {
rootlessQualCommand(t, 20*time.Second, "systemd-run", "--quiet", "--collect", "--unit", daemon.unit, "--property=Type=exec", "--",
"/usr/bin/podman", "--storage-driver=vfs", "--root="+daemon.dataRoot, "--runroot="+daemon.runRoot,
"system", "service", "--time=0", "unix://"+daemon.socket)
}
rootlessQualWaitSocket(t, daemon.socket)
rootlessQualCommand(t, 10*time.Second, "chmod", "0660", daemon.socket)
rootfulQualRuntimeCommand(t, daemon, 30*time.Second, "info")
if driver := rootfulQualRuntimeStorageDriver(t, daemon); driver != "vfs" {
t.Fatalf("rootful %s storage driver = %q, want vfs", daemon.runtime, driver)
}
}
func rootfulQualRuntimeStorageDriver(t *testing.T, daemon rootfulQualDaemon) string {
t.Helper()
if daemon.runtime == "docker" {
return rootfulQualRuntimeCommand(t, daemon, 30*time.Second, "info", "--format", "{{.Driver}}")
}
return rootfulQualRuntimeCommand(t, daemon, 30*time.Second, "info", "--format", "{{.Store.GraphDriverName}}")
}
func rootfulQualStopDaemon(t *testing.T, daemon rootfulQualDaemon) {
t.Helper()
rootlessQualStopUnit(t, daemon.unit)
_ = os.Remove(daemon.socket)
if daemon.runtime == "docker" {
rootfulQualWaitNoContainerd(t, 20*time.Second)
}
}
func rootfulQualAssertSystemContainerdDisabled(t *testing.T) {
t.Helper()
unitState := rootlessQualCommand(t, 10*time.Second, "systemctl", "show", "containerd.service", "--property=UnitFileState", "--value")
activeState := rootlessQualCommand(t, 10*time.Second, "systemctl", "show", "containerd.service", "--property=ActiveState", "--value")
if unitState != "masked" || activeState != "inactive" {
t.Fatalf("distro containerd service must be masked and inactive: UnitFileState=%q ActiveState=%q", unitState, activeState)
}
rootfulQualWaitNoContainerd(t, 5*time.Second)
}
func rootfulQualWaitNoContainerd(t *testing.T, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
for {
output, err := rootlessQualCommandError(3*time.Second, "pgrep", "-x", "containerd")
if err != nil {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
return
}
t.Fatalf("inspect containerd processes: %v\n%s", err, output)
}
if time.Now().After(deadline) {
t.Fatalf("containerd process remained after explicit Docker daemon stop: %s", strings.TrimSpace(output))
}
time.Sleep(100 * time.Millisecond)
}
}
func rootfulQualRuntimeCommand(t *testing.T, daemon rootfulQualDaemon, timeout time.Duration, args ...string) string {
t.Helper()
if daemon.runtime == "docker" {
return rootlessQualCommand(t, timeout, "docker", append([]string{"--host", "unix://" + daemon.socket}, args...)...)
}
return rootlessQualCommand(t, timeout, "podman", append([]string{"--url", "unix://" + daemon.socket}, args...)...)
}
func rootfulQualCreateFixtures(t *testing.T, daemon rootfulQualDaemon) {
t.Helper()
rootfulQualRuntimeCommand(t, daemon, 2*time.Minute, "build", "--network=none", "-t", rootfulQualFixture, "-f", filepath.Join(daemon.fixture, "Containerfile"), daemon.fixture)
rootfulQualRuntimeCommand(t, daemon, 30*time.Second, "run", "-d", "--restart=always", "--name", rootfulQualRunningName, rootfulQualFixture, "sleep", "3600")
if out, err := rootfulQualRuntimeCommandError(daemon, 30*time.Second, "run", "--name", rootfulQualExitedName, rootfulQualFixture, "true"); err != nil {
t.Fatalf("create exited fixture: %v\n%s", err, out)
}
}
func rootfulQualRemoveFixtures(t *testing.T, daemon rootfulQualDaemon) {
t.Helper()
for _, name := range []string{rootfulQualRunningName, rootfulQualExitedName} {
_, _ = rootfulQualRuntimeCommandError(daemon, 30*time.Second, "rm", "-f", name)
}
_, _ = rootfulQualRuntimeCommandError(daemon, 30*time.Second, "rmi", "-f", rootfulQualFixture)
}
func rootfulQualRuntimeCommandError(daemon rootfulQualDaemon, timeout time.Duration, args ...string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
name := "podman"
prefix := []string{"--url", "unix://" + daemon.socket}
if daemon.runtime == "docker" {
name = "docker"
prefix = []string{"--host", "unix://" + daemon.socket}
}
output, err := exec.CommandContext(ctx, name, append(prefix, args...)...).CombinedOutput()
return strings.TrimSpace(string(output)), err
}
func rootfulQualRuntimeBaseline(t *testing.T, daemon rootfulQualDaemon) rootlessQualBaseline {
t.Helper()
return rootlessQualBaselineFromPSOutput(rootfulQualRuntimeCommand(t, daemon, 30*time.Second, "ps", "-a", "--format", "{{.Names}}|{{.Image}}|{{.State}}"))
}
func rootfulQualRuntimeVersion(t *testing.T, daemon rootfulQualDaemon) string {
t.Helper()
return rootfulQualRuntimeCommand(t, daemon, 30*time.Second, "version", "--format", "{{.Server.Version}}")
}
func rootfulQualDaemonID(t *testing.T, daemon rootfulQualDaemon) string {
t.Helper()
d := rootlessQualDaemon{runtime: daemon.runtime, rootfulSock: daemon.socket}
return rootlessQualDaemonID(t, d, false)
}
func rootfulQualSocketIdentity(t *testing.T, path string) (int, int, string) {
t.Helper()
info, err := os.Lstat(path)
if err != nil {
t.Fatal(err)
}
if info.Mode()&os.ModeSocket == 0 || info.Mode()&os.ModeSymlink != 0 || info.Mode().Perm() != 0o660 {
t.Fatalf("unsafe rootful runtime socket %s mode=%s", path, info.Mode())
}
stat, ok := info.Sys().(*syscall.Stat_t)
if !ok {
t.Fatalf("socket %s lacks Unix stat identity", path)
}
return int(stat.Uid), int(stat.Gid), fmt.Sprintf("%04o", info.Mode().Perm())
}
func rootfulQualWaitSummary(t *testing.T, fixture *secureRuntimeLabFixture, after time.Time, runtimeKind, digest string, timeout time.Duration) secureRuntimeDockerReport {
t.Helper()
report := rootlessQualWaitReport(t, fixture, after, timeout, func(report agentsdocker.Report) bool {
return rootlessQualComplete(report) && report.InventoryComplete != nil && *report.InventoryComplete &&
report.Host.CollectionMode == agentsdocker.CollectionModeTypedHelperSummary && report.Host.Runtime == runtimeKind &&
rootlessQualSemanticDigest(report) == digest && len(report.Containers) > 0
})
rootlessQualAssertHelperSummaryOnly(t, report.Report, runtimeKind)
return report
}
func rootfulQualWaitStatusOnly(t *testing.T, fixture *secureRuntimeLabFixture, after time.Time, runtimeKind string, timeout time.Duration) secureRuntimeDockerReport {
t.Helper()
return rootlessQualWaitReport(t, fixture, after, timeout, func(report agentsdocker.Report) bool {
return report.InventoryComplete != nil && !*report.InventoryComplete && report.Host.Runtime == runtimeKind &&
report.Host.CollectionMode == agentsdocker.CollectionModeTypedHelperSummary && len(report.Containers) == 0 &&
secureRuntimeDockerHelperModuleState(report) == "degraded"
})
}
func rootfulQualSummaryEvidence(collectorPID, helperPID int, daemonID string, report agentsdocker.Report) map[string]any {
digest := rootlessQualDigestReport(report)
return map[string]any{
"collector_pid": collectorPID, "helper_pid": helperPID,
"collection_mode": agentsdocker.CollectionModeTypedHelperSummary,
"inventory_complete": true, "inventory_count": digest.Count,
"semantic_sha256": digest.SemanticDigest, "full_fields_present": false,
"stats_present": false, "secondary_structure_sha256": "",
"container_updates_enabled": false, "container_actions_enabled": false,
"direct_socket_access": false, "daemon_id": daemonID, "daemon_rootless": false,
}
}
func rootfulQualUnitIdentity(t *testing.T, unit string) (int, string) {
t.Helper()
pidText := rootlessQualCommand(t, 10*time.Second, "systemctl", "show", unit, "--property=MainPID", "--value")
pid, err := strconv.Atoi(pidText)
if err != nil || pid <= 0 {
t.Fatalf("invalid %s MainPID %q", unit, pidText)
}
invocation := rootlessQualCommand(t, 10*time.Second, "systemctl", "show", unit, "--property=InvocationID", "--value")
if len(invocation) != 32 {
t.Fatalf("invalid %s InvocationID %q", unit, invocation)
}
return pid, invocation
}
func rootfulQualAssertCollectorSocketDenied(t *testing.T, socket string) {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
command := exec.CommandContext(ctx, "runuser", "-u", "pulse-agent", "--", "curl", "-fsS", "--max-time", "2", "--unix-socket", socket, "http://runtime/_ping")
if output, err := command.CombinedOutput(); err == nil {
t.Fatalf("safe collector unexpectedly reached rootful socket %s: %s", socket, strings.TrimSpace(string(output)))
}
}
func rootfulQualHungUnit(runtimeKind string) string { return "pulse-rootful-" + runtimeKind + "-hung" }
func rootfulQualStartHungDaemon(t *testing.T, daemon rootfulQualDaemon) {
t.Helper()
scriptPath := filepath.Join(daemon.fixture, "hung-runtime.py")
script := `import os, socket, threading, time, sys
path = sys.argv[1]
try: os.unlink(path)
except FileNotFoundError: pass
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.bind(path)
os.chmod(path, 0o660)
s.listen(64)
def hold(c):
try: time.sleep(120)
finally: c.close()
while True:
c, _ = s.accept()
threading.Thread(target=hold, args=(c,), daemon=True).start()
`
if err := os.WriteFile(scriptPath, []byte(script), 0o700); err != nil {
t.Fatal(err)
}
rootlessQualCommand(t, 20*time.Second, "systemd-run", "--quiet", "--collect", "--unit", rootfulQualHungUnit(daemon.runtime), "--property=Type=exec", "--",
"/usr/bin/python3", scriptPath, daemon.socket)
rootlessQualWaitSocket(t, daemon.socket)
}
func rootfulQualRunBoundProbe(t *testing.T, deadline time.Duration) time.Duration {
t.Helper()
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
boundProbe := strings.TrimSpace(os.Getenv("PULSE_ROOTFUL_BOUND_PROBE_BINARY"))
if boundProbe != rootfulQualBoundProbe {
t.Fatalf("PULSE_ROOTFUL_BOUND_PROBE_BINARY must use %q: %q", rootfulQualBoundProbe, boundProbe)
}
if secureRuntimeHash(rootlessQualReadFile(t, boundProbe)) != secureRuntimeHash(rootlessQualReadFile(t, executable)) {
t.Fatal("collector-executable bound probe differs from the qualification binary")
}
for _, path := range []string{filepath.Dir(boundProbe), boundProbe} {
info, statErr := os.Lstat(path)
if statErr != nil {
t.Fatalf("stat bound-probe path %s: %v", path, statErr)
}
statInfo, ok := info.Sys().(*syscall.Stat_t)
isProbe := path == boundProbe
if !ok || statInfo.Uid != 0 || info.Mode().Perm() != 0o755 || isProbe && !info.Mode().IsRegular() || !isProbe && !info.IsDir() {
t.Fatalf("bound-probe path is not root-owned mode 0755 with a regular executable: %s %+v", path, info)
}
}
ctx, cancel := context.WithTimeout(context.Background(), deadline+3*time.Second)
defer cancel()
started := time.Now()
cmd := exec.CommandContext(ctx, "runuser", "-u", "pulse-agent", "--", "env",
"PULSE_ROOTFUL_BOUND_PROBE=1", fmt.Sprintf("PULSE_ROOTFUL_BOUND_DEADLINE_MS=%d", deadline.Milliseconds()),
boundProbe, "-test.run", "^TestSecureRuntimeRootfulBoundProbe$", "-test.count=1", "-test.v", "-test.timeout=10s")
output, err := cmd.CombinedOutput()
elapsed := time.Since(started)
if err != nil || !strings.Contains(string(output), "ROOTFUL_BOUND_RESULT=deadline_exceeded") {
t.Fatalf("bounded helper probe failed after %s: %v\n%s", elapsed, err, output)
}
if elapsed < deadline/2 || elapsed > deadline+time.Second {
t.Fatalf("bounded helper probe elapsed %s outside expected interval", elapsed)
}
return elapsed
}
func rootfulQualRemoveRuntimeState(t *testing.T, daemon rootfulQualDaemon) {
t.Helper()
roots := []string{daemon.dataRoot, daemon.runRoot}
deadline := time.Now().Add(30 * time.Second)
for {
mountInfo := string(rootlessQualReadFile(t, "/proc/self/mountinfo"))
remaining, err := rootlessQualMountPointsBelow(mountInfo, roots)
if err != nil {
t.Fatalf("inspect disposable runtime mounts: %v", err)
}
if len(remaining) == 0 {
break
}
if time.Now().After(deadline) {
t.Fatalf("runtime mounts remain after service shutdown: %q", remaining)
}
time.Sleep(100 * time.Millisecond)
}
for _, path := range []string{daemon.dataRoot, daemon.runRoot, daemon.fixture} {
if err := os.RemoveAll(path); err != nil {
t.Fatalf("remove disposable runtime path %s: %v", path, err)
}
}
if daemon.runtime == "podman" {
if err := os.Remove(filepath.Dir(daemon.socket)); err != nil && !errors.Is(err, os.ErrNotExist) {
t.Fatalf("remove disposable Podman socket directory: %v", err)
}
}
}
func rootfulQualRuntimeStateClean(daemon rootfulQualDaemon) bool {
paths := []string{daemon.socket, daemon.dataRoot, daemon.runRoot, daemon.fixture}
if daemon.runtime == "podman" {
paths = append(paths, filepath.Dir(daemon.socket))
}
for _, path := range paths {
if _, err := os.Lstat(path); !errors.Is(err, os.ErrNotExist) {
return false
}
}
return true
}
func rootfulQualArtifactIdentities(t *testing.T, installerPath string) rootlessQualArtifacts {
t.Helper()
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
return rootlessQualArtifacts{
QualificationTest: rootfulQualGoArtifact(t, executable, "dockeragent.test"),
Collector: rootfulQualGoArtifact(t, strings.TrimSpace(os.Getenv("PULSE_SECURE_RUNTIME_COLLECTOR")), "pulse-agent"),
Helper: rootfulQualGoArtifact(t, strings.TrimSpace(os.Getenv("PULSE_SECURE_RUNTIME_HELPER")), "pulse-agent-helper"),
Installer: rootlessQualInstallerArtifact{PathBasename: filepath.Base(installerPath), SHA256: secureRuntimeHash(rootlessQualReadFile(t, installerPath))},
}
}
func rootfulQualGoArtifact(t *testing.T, path, basename string) rootlessQualArtifact {
t.Helper()
info, err := buildinfo.ReadFile(path)
if err != nil {
t.Fatalf("read Go build metadata for %s: %v", path, err)
}
artifact := rootlessQualArtifact{PathBasename: filepath.Base(path), SHA256: secureRuntimeHash(rootlessQualReadFile(t, path)), Package: info.Path, GoVersion: info.GoVersion}
for _, setting := range info.Settings {
switch setting.Key {
case "vcs.revision":
artifact.VCSRevision = setting.Value
case "vcs.modified":
artifact.VCSModified = setting.Value == "true"
}
}
wantCommit := strings.TrimSpace(os.Getenv("PULSE_ROOTFUL_SOURCE_COMMIT"))
if artifact.PathBasename != basename || artifact.Package == "" || artifact.VCSRevision != wantCommit || artifact.VCSModified {
t.Fatalf("rootful qualification artifact is not an exact clean source build: %+v", artifact)
}
return artifact
}
func rootfulQualSourceHashes(t *testing.T) map[string]string {
t.Helper()
path := strings.TrimSpace(os.Getenv("PULSE_ROOTFUL_SOURCE_HASHES"))
if !filepath.IsAbs(path) {
t.Fatalf("PULSE_ROOTFUL_SOURCE_HASHES must be absolute: %q", path)
}
var hashes map[string]string
if err := json.Unmarshal(rootlessQualReadFile(t, path), &hashes); err != nil {
t.Fatal(err)
}
if len(hashes) == 0 {
t.Fatal("rootful source hash map is empty")
}
return hashes
}
func rootfulQualValidateReceipt(receipt rootfulQualReceipt, expectedRuns int) error {
if receipt.SchemaVersion != 1 || receipt.Kind != "pulse-secure-runtime-rootful-qualification" || receipt.Result != "passed" {
return errors.New("invalid rootful qualification identity")
}
if len(receipt.SourceCommit) != 40 || !rootfulQualBaseImagePattern.MatchString(receipt.BaseImage) || receipt.StartedAt == "" || receipt.CompletedAt == "" || len(receipt.SourceHashes) == 0 || len(receipt.Runs) != expectedRuns {
return errors.New("incomplete rootful qualification envelope")
}
for _, run := range receipt.Runs {
if run.Runtime.Runtime != "docker" && run.Runtime.Runtime != "podman" {
return fmt.Errorf("unsupported runtime %q", run.Runtime.Runtime)
}
if run.Runtime.DaemonRootless || run.Runtime.DaemonID == "" || run.Runtime.SocketUID != 0 || run.Runtime.SocketPath == "" || run.Runtime.SocketMode != "0660" || run.Runtime.SocketType != "unix" || run.Runtime.SocketSymlink {
return errors.New("invalid rootful runtime identity")
}
if run.Host.MachineID == "" || len(run.Scenarios) != len(rootfulQualScenarioOrder) {
return errors.New("incomplete rootful host/scenario evidence")
}
for index, scenario := range run.Scenarios {
if scenario.Name != rootfulQualScenarioOrder[index] || scenario.Result != "passed" || scenario.StartedAt == "" || scenario.CompletedAt == "" || scenario.Evidence == nil {
return fmt.Errorf("invalid scenario %d", index)
}
isReporting := scenario.Name != "authority_isolation" && scenario.Name != "cleanup"
if isReporting != (scenario.ReportSequence != nil && scenario.ReportStreamID != nil) {
return fmt.Errorf("scenario %s report binding mismatch", scenario.Name)
}
}
}
return nil
}
func TestRootfulQualificationReceiptContract(t *testing.T) {
stream := "stream"
sequence := uint64(1)
scenarios := make([]rootlessQualScenario, 0, len(rootfulQualScenarioOrder))
for _, name := range rootfulQualScenarioOrder {
scenario := rootlessQualScenario{Name: name, Result: "passed", StartedAt: time.Now().UTC().Format(time.RFC3339Nano), CompletedAt: time.Now().UTC().Format(time.RFC3339Nano), Evidence: map[string]any{"observed": true}}
if name != "authority_isolation" && name != "cleanup" {
scenario.ReportStreamID = &stream
scenario.ReportSequence = &sequence
}
scenarios = append(scenarios, scenario)
}
receipt := rootfulQualReceipt{
SchemaVersion: 1, Kind: "pulse-secure-runtime-rootful-qualification", Result: "passed",
SourceCommit: strings.Repeat("a", 40), StartedAt: time.Now().UTC().Format(time.RFC3339Nano), CompletedAt: time.Now().UTC().Format(time.RFC3339Nano),
BaseImage: "ubuntu@sha256:" + strings.Repeat("c", 64),
SourceHashes: map[string]string{"go.mod": strings.Repeat("b", 64)},
Runs: []rootfulQualRun{{Host: rootlessQualHost{MachineID: strings.Repeat("1", 32)}, Runtime: rootfulQualRuntime{Runtime: "docker", RuntimeVersion: "1", DaemonID: "daemon", SocketPath: "/var/run/docker.sock", SocketUID: 0, SocketGID: 999, SocketMode: "0660", SocketType: "unix"}, Scenarios: scenarios}},
}
if err := rootfulQualValidateReceipt(receipt, 1); err != nil {
t.Fatal(err)
}
receipt.Runs[0].Scenarios[3], receipt.Runs[0].Scenarios[4] = receipt.Runs[0].Scenarios[4], receipt.Runs[0].Scenarios[3]
if err := rootfulQualValidateReceipt(receipt, 1); err == nil {
t.Fatal("validator accepted reordered rootful scenarios")
}
}
func TestRootfulQualificationGoSchemaPassesPythonValidator(t *testing.T) {
commit := strings.Repeat("a", 40)
digest := strings.Repeat("b", 64)
started := time.Date(2026, 9, 1, 10, 0, 0, 0, time.UTC)
receipt := rootfulQualReceipt{
SchemaVersion: 1, Kind: "pulse-secure-runtime-rootful-qualification", Result: "passed",
SourceCommit: commit, StartedAt: started.Format(time.RFC3339Nano),
BaseImage: "ubuntu@sha256:" + strings.Repeat("c", 64),
CompletedAt: started.Add(2 * time.Minute).Format(time.RFC3339Nano),
SourceHashes: map[string]string{"internal/agenthelper/container_inventory.go": digest, "scripts/install.sh": digest},
Artifacts: rootlessQualArtifacts{
QualificationTest: rootlessQualArtifact{PathBasename: "dockeragent.test", SHA256: digest, Package: "github.com/rcourtman/pulse-go-rewrite/scripts/installtests.test", GoVersion: "go1.25.0", VCSRevision: commit},
Collector: rootlessQualArtifact{PathBasename: "pulse-agent", SHA256: digest, Package: "github.com/rcourtman/pulse-go-rewrite/cmd/pulse-agent", GoVersion: "go1.25.0", VCSRevision: commit},
Helper: rootlessQualArtifact{PathBasename: "pulse-agent-helper", SHA256: digest, Package: "github.com/rcourtman/pulse-go-rewrite/cmd/pulse-agent-helper", GoVersion: "go1.25.0", VCSRevision: commit},
Installer: rootlessQualInstallerArtifact{PathBasename: "install.sh", SHA256: digest},
},
}
for index, runtimeKind := range []string{"docker", "podman"} {
receipt.Runs = append(receipt.Runs, rootfulQualValidatorFixtureRun(runtimeKind, index, started.Add(time.Duration(index)*30*time.Second), digest))
}
path := filepath.Join(t.TempDir(), "receipt.json")
rootlessQualWriteJSON(t, path, receipt)
validator := repoFile("scripts", "release_control", "secure_runtime_rootful_attestation_v1.py")
program := `import importlib.util, pathlib, sys
path=pathlib.Path(sys.argv[1]).resolve()
sys.path.insert(0, str(path.parent))
spec=importlib.util.spec_from_file_location("validator", path)
module=importlib.util.module_from_spec(spec); spec.loader.exec_module(module)
module.parse_receipt_bytes(pathlib.Path(sys.argv[2]).read_bytes())
`
cmd := exec.Command("python3", "-I", "-c", program, validator, path)
if output, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("Go rootful receipt schema failed the real Python validator: %v\n%s", err, output)
}
}
func rootfulQualValidatorFixtureRun(runtimeKind string, index int, began time.Time, digest string) rootfulQualRun {
base := index * 100
daemonID := runtimeKind + "-daemon"
socketPath := "/var/run/docker.sock"
socketGID := 999
if runtimeKind == "podman" {
socketPath = "/run/podman/podman.sock"
socketGID = 0
}
stream := func(value string) *string { return &value }
sequence := func(value uint64) *uint64 { return &value }
makeScenario := func(offset int, name string, streamID *string, seq *uint64, evidence map[string]any) rootlessQualScenario {
start := began.Add(time.Duration(offset) * time.Second)
return rootlessQualScenario{
Name: name, Result: "passed", StartedAt: start.Format(time.RFC3339Nano),
CompletedAt: start.Add(time.Second).Format(time.RFC3339Nano),
ReportStreamID: streamID, ReportSequence: seq, Evidence: evidence,
}
}
summary := func(collectorPID, helperPID int) map[string]any {
return map[string]any{
"collector_pid": collectorPID, "helper_pid": helperPID,
"collection_mode": "typed-helper-summary", "inventory_complete": true,
"inventory_count": 2, "semantic_sha256": digest, "full_fields_present": false,
"stats_present": false, "secondary_structure_sha256": "",
"container_updates_enabled": false, "container_actions_enabled": false,
"direct_socket_access": false, "daemon_id": daemonID, "daemon_rootless": false,
}
}
fresh := summary(base+100, base+200)
migration := summary(base+110, base+210)
migration["legacy_profile"] = "root-command-capable"
migration["target_profile"] = "typed-helper-monitoring-only"
migration["authority_reduced"] = true
migration["legacy_collector_pid"] = base + 90
collectorRestart := summary(base+120, base+210)
collectorRestart["previous_collector_pid"] = base + 110
collectorRestart["previous_report_stream_id"] = runtimeKind + "-migration"
helperRestart := summary(base+120, base+220)
helperRestart["previous_helper_pid"] = base + 210
helperRestart["previous_helper_invocation_id"] = runtimeKind + "-helper-old"
helperRestart["helper_invocation_id"] = runtimeKind + "-helper-new"
loss := map[string]any{
"collector_pid": base + 120, "previous_helper_pid": base + 220,
"collection_mode": "typed-helper-unavailable-status-only", "helper_available": false,
"status_only": true, "inventory_complete": false, "inventory_present": false,
"authoritative_inventory_replacement": false,
"previous_authoritative_inventory_count": 2, "previous_authoritative_semantic_sha256": digest,
"operation_status": "degraded", "operation": "container.inventory",
"container_updates_enabled": false, "container_actions_enabled": false, "direct_socket_access": false,
}
recovery := summary(base+120, base+230)
recovery["previous_helper_pid"] = base + 220
recovery["previous_status_report_sequence"] = uint64(3)
bounds := map[string]any{
"collector_pid": base + 120, "helper_pid": base + 230,
"operation": "container.inventory", "failure_class": "bounded-timeout",
"deadline_ms": 2000, "elapsed_ms": 2000, "bounded_failure_observed": true,
"status_only_report_sequence": uint64(5), "recovery_report_sequence": uint64(6),
"collection_mode": "typed-helper-summary", "inventory_complete": true,
"previous_authoritative_inventory_count": 2, "previous_authoritative_semantic_sha256": digest,
"recovery_inventory_count": 2, "recovery_semantic_sha256": digest,
"full_fields_present": false, "stats_present": false, "secondary_structure_sha256": "",
"authoritative_empty_replacement": false, "collector_alive": true, "helper_alive": true,
"container_updates_enabled": false, "container_actions_enabled": false, "direct_socket_access": false,
}
update := summary(base+130, base+230)
update["previous_collector_pid"] = base + 120
update["previous_helper_pid"] = base + 230
update["previous_report_stream_id"] = runtimeKind + "-steady"
update["update_applied"] = true
update["collector_binary_sha256"] = digest
update["helper_binary_sha256"] = digest
authority := map[string]any{
"collector_pid": base + 130, "collector_uid": 1000 + index, "effective_uid": 1000 + index,
"effective_root": false, "safe_profile_enabled": true, "commands_enabled": false,
"privileged_helper_enabled": true, "reduction_request_observed": true,
"collector_command_transport_present": false, "collector_command_session_present": false,
"container_actions_enabled": false, "container_updates_enabled": false,
"rootful_socket_access": false, "direct_socket_access": false, "helper_network_access": false,
}
return rootfulQualRun{
Host: rootlessQualHost{
MachineID: "machine-" + runtimeKind, Architecture: "amd64",
Kernel: "Linux fixture", SystemdVersion: "systemd 255",
},
Runtime: rootfulQualRuntime{
Runtime: runtimeKind, RuntimeVersion: "1.0.0", DaemonID: daemonID,
DaemonRootless: false, SocketPath: socketPath, SocketUID: 0, SocketGID: socketGID,
SocketMode: "0660", SocketType: "unix", SocketSymlink: false,
},
Scenarios: []rootlessQualScenario{
makeScenario(0, "fresh_install", stream(runtimeKind+"-fresh"), sequence(1), fresh),
makeScenario(2, "legacy_migration", stream(runtimeKind+"-migration"), sequence(1), migration),
makeScenario(4, "collector_restart", stream(runtimeKind+"-steady"), sequence(1), collectorRestart),
makeScenario(6, "helper_restart", stream(runtimeKind+"-steady"), sequence(2), helperRestart),
makeScenario(8, "helper_loss", stream(runtimeKind+"-steady"), sequence(3), loss),
makeScenario(10, "helper_recovery", stream(runtimeKind+"-steady"), sequence(4), recovery),
makeScenario(12, "operation_bounds", stream(runtimeKind+"-steady"), sequence(6), bounds),
makeScenario(14, "update_preservation", stream(runtimeKind+"-update"), sequence(1), update),
makeScenario(16, "authority_isolation", nil, nil, authority),
makeScenario(18, "cleanup", nil, nil, map[string]any{
"collector_stopped": true, "helper_stopped": true, "runtime_stopped": true,
"socket_absent": true, "fixtures_removed": true, "state_clean": true,
}),
},
}
}
func TestRootfulQualificationWrapperInvariants(t *testing.T) {
raw, err := os.ReadFile(repoFile("scripts", "run-secure-runtime-rootful-qualification.sh"))
if err != nil {
t.Fatal(err)
}
script := string(raw)
for _, required := range []string{
"pulse-secure-runtime-rootful-qualification", "PULSE_ROOTFUL_QUALIFICATION_CONFIRM",
"--network none", "--cgroupns=private", "docker-receipt.json", "podman-receipt.json",
"secure_runtime_rootful_attestation_v1.py", "qualification output directory must have exact mode 0700",
"capture_qualification_container_diagnostics", "journalctl --no-pager -n 2000",
"org.pulse.rootful-qualification.run", "-buildvcs=true",
"github.com/rcourtman/pulse-go-rewrite/scripts/installtests.test",
"https://github.com/rcourtman/Pulse.git", "refs/remotes/origin/main", "refs/heads/main",
"PULSE_ROOTFUL_UBUNTU_IMAGE", "PULSE_ROOTFUL_BOUND_PROBE_BINARY",
"rootful_qualification_systemd_readiness",
"containerd.service",
"podman-auto-update.service", "podman-auto-update.timer",
"podman-clean-transient.service", "podman-restart.service",
rootfulQualBoundProbe,
} {
if !strings.Contains(script, required) {
t.Fatalf("rootful qualification wrapper missing %q", required)
}
}
if count := strings.Count(script, "-buildvcs=true"); count != 3 {
t.Fatalf("rootful wrapper must require VCS metadata for exactly three Go artifacts: got %d", count)
}
for _, forbidden := range []string{"/var/run/docker.sock:/", "/run/docker.sock:/", "/run/podman/podman.sock:/", "--pid=host", "--cgroupns=host"} {
if strings.Contains(script, forbidden) {
t.Fatalf("rootful qualification wrapper contains forbidden host boundary %q", forbidden)
}
}
}
func TestRootfulQualificationSystemdReadiness(t *testing.T) {
runtimeScript := repoFile("scripts", "secure-runtime-rootful-runtime.sh")
binDir := t.TempDir()
fakeDocker := filepath.Join(binDir, "docker")
fake := `#!/bin/sh
case "$*" in
*"systemctl is-system-running"*)
printf '%s\n' "${FAKE_SYSTEMD_MANAGER_STATE:-starting}"
[ "${FAKE_SYSTEMD_MANAGER_STATE:-starting}" = running ]
;;
*"systemctl show --property=ActiveState --value multi-user.target"*)
printf '%s\n' "${FAKE_SYSTEMD_TARGET_STATE:-inactive}"
;;
*"systemctl list-units --state=failed --no-legend --no-pager --plain"*)
printf '%s' "${FAKE_SYSTEMD_FAILED_UNITS:-}"
;;
*)
printf 'unexpected docker arguments: %s\n' "$*" >&2
exit 99
;;
esac
`
if err := os.WriteFile(fakeDocker, []byte(fake), 0o700); err != nil {
t.Fatal(err)
}
tests := []struct {
name string
managerState string
targetState string
failedUnits string
wantExit int
wantOutput string
}{
{name: "manager starting", managerState: "starting", targetState: "active", wantExit: 1},
{name: "target inactive", managerState: "running", targetState: "inactive", wantExit: 1},
{name: "clean running manager", managerState: "running", targetState: "active", wantExit: 0},
{name: "degraded manager", managerState: "degraded", targetState: "active", wantExit: 2, wantOutput: "degraded"},
{name: "maintenance manager", managerState: "maintenance", targetState: "active", wantExit: 2, wantOutput: "maintenance"},
{name: "failed unit", managerState: "running", targetState: "active", failedUnits: "podman-restart.service loaded failed failed", wantExit: 2, wantOutput: "podman-restart.service"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
cmd := exec.Command("bash", "-c", `source "$1"; rootful_qualification_systemd_readiness fixture`, "bash", runtimeScript)
cmd.Env = append(os.Environ(),
"PATH="+binDir+":"+os.Getenv("PATH"),
"FAKE_SYSTEMD_MANAGER_STATE="+test.managerState,
"FAKE_SYSTEMD_TARGET_STATE="+test.targetState,
"FAKE_SYSTEMD_FAILED_UNITS="+test.failedUnits,
)
output, err := cmd.CombinedOutput()
gotExit := 0
if err != nil {
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
t.Fatalf("readiness helper failed without an exit status: %v", err)
}
gotExit = exitErr.ExitCode()
}
if gotExit != test.wantExit {
t.Fatalf("readiness exit = %d, want %d\n%s", gotExit, test.wantExit, output)
}
if test.wantOutput != "" && !strings.Contains(string(output), test.wantOutput) {
t.Fatalf("readiness output missing %q: %s", test.wantOutput, output)
}
})
}
}