From 5df0d2ea8fd72b26d0324d368cef98f6d5249e82 Mon Sep 17 00:00:00 2001 From: Janik Besendorf Date: Sun, 26 Jul 2026 18:29:23 +0200 Subject: [PATCH] Collect full SELinux policies --- acquisition/acquisition.go | 52 ++++++- acquisition/acquisition_test.go | 78 ++++++++++ adb/adb.go | 175 +++++++++++++++++++++- adb/sync_test.go | 252 ++++++++++++++++++++++++++++++++ modules/selinux.go | 63 +++++++- modules/selinux_test.go | 143 ++++++++++++++++++ 6 files changed, 753 insertions(+), 10 deletions(-) create mode 100644 adb/sync_test.go create mode 100644 modules/selinux_test.go diff --git a/acquisition/acquisition.go b/acquisition/acquisition.go index c7a5cf6..43db6e9 100644 --- a/acquisition/acquisition.go +++ b/acquisition/acquisition.go @@ -160,9 +160,39 @@ func (a *Acquisition) PullToZipStaged(remotePath, zipPath string) error { if err := a.validateStreamingMode(); err != nil { return err } + if a.StreamingPuller == nil { + return fmt.Errorf("streaming puller cannot be nil") + } + if remotePath == "" { + return fmt.Errorf("remote path cannot be empty") + } + + return a.pullToZipStaged(zipPath, func(writer io.Writer) error { + return a.StreamingPuller.PullToWriter(remotePath, writer) + }) +} +// SyncPullToZipStaged retrieves a device file through ADB's sync service and +// adds it to the acquisition archive only after the complete pull succeeds. +func (a *Acquisition) SyncPullToZipStaged(remotePath, zipPath string) error { + if err := a.validateStreamingMode(); err != nil { + return err + } + if adb.Client == nil { + return fmt.Errorf("ADB client cannot be nil") + } + if remotePath == "" { + return fmt.Errorf("remote path cannot be empty") + } + + return a.pullToZipStaged(zipPath, func(writer io.Writer) error { + return adb.Client.SyncPullToWriter(remotePath, writer) + }) +} + +func (a *Acquisition) pullToZipStaged(zipPath string, pull func(io.Writer) error) error { if a.ZipWriter.IsEncrypted() { - staged, err := a.StreamingPuller.PullToEncryptedTempFile(remotePath) + staged, err := createEncryptedTempFile(pull) if err != nil { return err } @@ -176,12 +206,26 @@ func (a *Acquisition) PullToZipStaged(remotePath, zipPath string) error { return a.ZipWriter.CreateFileFromReader(zipPath, reader) } - tempPath, err := a.StreamingPuller.PullToTempFile(remotePath) + tempFile, err := os.CreateTemp("", "androidqf-pull-*") if err != nil { - return err + return fmt.Errorf("failed to create temporary file: %w", err) } + tempPath := tempFile.Name() defer os.Remove(tempPath) - return a.ZipWriter.CreateFileFromPath(zipPath, tempPath) + + if err := pull(tempFile); err != nil { + _ = tempFile.Close() + return err + } + if err := tempFile.Close(); err != nil { + return fmt.Errorf("failed to close temporary file: %w", err) + } + + err = a.ZipWriter.CreateFileFromPath(zipPath, tempPath) + if err != nil { + return err + } + return nil } func (a *Acquisition) GetSystemInformation() error { diff --git a/acquisition/acquisition_test.go b/acquisition/acquisition_test.go index c5dc58f..1e6d607 100644 --- a/acquisition/acquisition_test.go +++ b/acquisition/acquisition_test.go @@ -134,6 +134,84 @@ func TestCompleteReturnsArchiveFinalizationErrors(t *testing.T) { } } +func TestPullToZipStagedWithWriterSupportsEncryptedStaging(t *testing.T) { + var archive bytes.Buffer + writer := &StreamingZipWriter{ + zipWriter: zip.NewWriter(&archive), + encrypted: true, + } + acq := &Acquisition{ + ZipWriter: writer, + StreamingMode: true, + } + content := bytes.Repeat([]byte("sensitive policy data\n"), 4096) + + err := acq.pullToZipStaged("selinux/sys/fs/selinux/policy", func(destination io.Writer) error { + _, err := destination.Write(content) + return err + }) + if err != nil { + t.Fatalf("pullToZipStaged() error = %v", err) + } + if err := writer.zipWriter.Close(); err != nil { + t.Fatalf("zip Close() error = %v", err) + } + + reader, err := zip.NewReader(bytes.NewReader(archive.Bytes()), int64(archive.Len())) + if err != nil { + t.Fatalf("zip.NewReader() error = %v", err) + } + if len(reader.File) != 1 || reader.File[0].Name != "selinux/sys/fs/selinux/policy" { + t.Fatalf("archive entries = %#v, want active SELinux policy", reader.File) + } + fileReader, err := reader.File[0].Open() + if err != nil { + t.Fatalf("Open(policy) error = %v", err) + } + got, err := io.ReadAll(fileReader) + _ = fileReader.Close() + if err != nil { + t.Fatalf("ReadAll(policy) error = %v", err) + } + if !bytes.Equal(got, content) { + t.Fatal("archived policy does not match pulled content") + } +} + +func TestPullToZipStagedWithWriterDoesNotArchiveFailedEncryptedPull(t *testing.T) { + var archive bytes.Buffer + writer := &StreamingZipWriter{ + zipWriter: zip.NewWriter(&archive), + encrypted: true, + } + acq := &Acquisition{ + ZipWriter: writer, + StreamingMode: true, + } + pullErr := errors.New("partial sync failure") + + err := acq.pullToZipStaged("selinux/sys/fs/selinux/policy", func(destination io.Writer) error { + if _, err := io.WriteString(destination, "partial policy"); err != nil { + return err + } + return pullErr + }) + if !errors.Is(err, pullErr) { + t.Fatalf("pullToZipStaged() error = %v, want %v", err, pullErr) + } + if err := writer.zipWriter.Close(); err != nil { + t.Fatalf("zip Close() error = %v", err) + } + + reader, err := zip.NewReader(bytes.NewReader(archive.Bytes()), int64(archive.Len())) + if err != nil { + t.Fatalf("zip.NewReader() error = %v", err) + } + if len(reader.File) != 0 { + t.Fatalf("archive contains entries after failed pull: %#v", reader.File) + } +} + func readZipFiles(t *testing.T, archivePath string) map[string]string { t.Helper() diff --git a/adb/adb.go b/adb/adb.go index 3bdcf5f..27dda10 100644 --- a/adb/adb.go +++ b/adb/adb.go @@ -6,18 +6,24 @@ package adb import ( + "encoding/binary" "errors" "fmt" + "io" + "net" "os/exec" + "strconv" "strings" + "time" saveSlice "github.com/botherder/go-savetime/slice" "github.com/mvt-project/androidqf/log" ) type ADB struct { - ExePath string - Serial string + ExePath string + Serial string + serverAddress string } type DeviceInfo struct { @@ -30,6 +36,12 @@ type DeviceInfo struct { var Client *ADB +const ( + defaultADBServerAddress = "127.0.0.1:5037" + maxSyncFrameSize = 1024 * 1024 + adbDialTimeout = 10 * time.Second +) + // New returns a new ADB instance. func New() (*ADB, error) { adb := ADB{} @@ -200,6 +212,165 @@ func (a *ADB) Pull(remotePath, localPath string) (string, error) { return string(out), nil } +// SyncPullToWriter retrieves a device file through ADB's sync service and +// writes it directly to writer. Unlike shell-based streaming, the sync service +// can retrieve files such as /sys/fs/selinux/policy that are exposed to +// `adb pull` but cannot be read from an ADB shell. +func (a *ADB) SyncPullToWriter(remotePath string, writer io.Writer) error { + if remotePath == "" { + return fmt.Errorf("remote path cannot be empty") + } + if writer == nil { + return fmt.Errorf("writer cannot be nil") + } + + serverAddress := a.serverAddress + if serverAddress == "" { + serverAddress = defaultADBServerAddress + } + + conn, err := net.DialTimeout("tcp", serverAddress, adbDialTimeout) + if err != nil { + return fmt.Errorf("failed to connect to ADB server: %w", err) + } + defer conn.Close() + + transport := "host:transport-any" + if a.Serial != "" { + transport = "host:transport:" + a.Serial + } + if err := sendADBHostRequest(conn, transport); err != nil { + return fmt.Errorf("failed to select ADB transport: %w", err) + } + if err := sendADBHostRequest(conn, "sync:"); err != nil { + return fmt.Errorf("failed to start ADB sync service: %w", err) + } + + if err := writeSyncRequest(conn, "RECV", remotePath); err != nil { + return fmt.Errorf("failed to request %q from ADB sync service: %w", remotePath, err) + } + if err := receiveSyncFile(conn, writer); err != nil { + return fmt.Errorf("failed to pull %q through ADB sync service: %w", remotePath, err) + } + return nil +} + +func sendADBHostRequest(conn io.ReadWriter, request string) error { + if len(request) > 0xffff { + return fmt.Errorf("ADB request is too long") + } + header := fmt.Sprintf("%04x", len(request)) + if err := writeString(conn, header+request); err != nil { + return err + } + + status := make([]byte, 4) + if _, err := io.ReadFull(conn, status); err != nil { + return err + } + switch string(status) { + case "OKAY": + return nil + case "FAIL": + message, err := readADBHostFailure(conn) + if err != nil { + return err + } + return errors.New(message) + default: + return fmt.Errorf("unexpected ADB status %q", status) + } +} + +func readADBHostFailure(reader io.Reader) (string, error) { + lengthBytes := make([]byte, 4) + if _, err := io.ReadFull(reader, lengthBytes); err != nil { + return "", err + } + length, err := strconv.ParseUint(string(lengthBytes), 16, 16) + if err != nil { + return "", fmt.Errorf("invalid ADB failure length %q", lengthBytes) + } + message := make([]byte, int(length)) + if _, err := io.ReadFull(reader, message); err != nil { + return "", err + } + return string(message), nil +} + +func writeSyncRequest(writer io.Writer, id, path string) error { + if len(id) != 4 { + return fmt.Errorf("sync request ID must be four bytes") + } + if len(path) > maxSyncFrameSize { + return fmt.Errorf("sync request path is too long") + } + + header := make([]byte, 8) + copy(header[:4], id) + binary.LittleEndian.PutUint32(header[4:], uint32(len(path))) + if err := writeBytes(writer, header); err != nil { + return err + } + return writeString(writer, path) +} + +func writeBytes(writer io.Writer, data []byte) error { + written, err := writer.Write(data) + if err != nil { + return err + } + if written != len(data) { + return io.ErrShortWrite + } + return nil +} + +func writeString(writer io.Writer, data string) error { + written, err := io.WriteString(writer, data) + if err != nil { + return err + } + if written != len(data) { + return io.ErrShortWrite + } + return nil +} + +func receiveSyncFile(reader io.Reader, writer io.Writer) error { + header := make([]byte, 8) + for { + if _, err := io.ReadFull(reader, header); err != nil { + return err + } + + id := string(header[:4]) + length := binary.LittleEndian.Uint32(header[4:]) + switch id { + case "DATA": + if length > maxSyncFrameSize { + return fmt.Errorf("ADB sync DATA frame is too large: %d bytes", length) + } + if _, err := io.CopyN(writer, reader, int64(length)); err != nil { + return err + } + case "DONE": + return nil + case "FAIL": + if length > maxSyncFrameSize { + return fmt.Errorf("ADB sync FAIL frame is too large: %d bytes", length) + } + message := make([]byte, int(length)) + if _, err := io.ReadFull(reader, message); err != nil { + return err + } + return errors.New(string(message)) + default: + return fmt.Errorf("unexpected ADB sync response %q", id) + } + } +} + // Push a file on the phone func (a *ADB) Push(localPath, remotePath string) (string, error) { out, err := a.Exec("push", localPath, remotePath) diff --git a/adb/sync_test.go b/adb/sync_test.go new file mode 100644 index 0000000..54b2c36 --- /dev/null +++ b/adb/sync_test.go @@ -0,0 +1,252 @@ +package adb + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "net" + "strconv" + "strings" + "testing" +) + +func TestSyncPullToWriter(t *testing.T) { + address := startFakeADBServer(t, func(conn net.Conn) error { + if err := expectHostRequest(conn, "host:transport:device-1"); err != nil { + return err + } + if err := expectHostRequest(conn, "sync:"); err != nil { + return err + } + if err := expectSyncRequest(conn, "RECV", "/sys/fs/selinux/policy"); err != nil { + return err + } + if err := writeSyncFrame(conn, "DATA", []byte("active ")); err != nil { + return err + } + if err := writeSyncFrame(conn, "DATA", []byte("policy")); err != nil { + return err + } + return writeSyncFrame(conn, "DONE", nil) + }) + + client := &ADB{Serial: "device-1", serverAddress: address} + var output bytes.Buffer + if err := client.SyncPullToWriter("/sys/fs/selinux/policy", &output); err != nil { + t.Fatalf("SyncPullToWriter() error = %v", err) + } + if got := output.String(); got != "active policy" { + t.Fatalf("SyncPullToWriter() output = %q, want %q", got, "active policy") + } +} + +func TestSyncPullToWriterUsesAnyTransportWithoutSerial(t *testing.T) { + address := startFakeADBServer(t, func(conn net.Conn) error { + if err := expectHostRequest(conn, "host:transport-any"); err != nil { + return err + } + if err := expectHostRequest(conn, "sync:"); err != nil { + return err + } + if err := expectSyncRequest(conn, "RECV", "/vendor/policy"); err != nil { + return err + } + return writeSyncFrame(conn, "DONE", nil) + }) + + client := &ADB{serverAddress: address} + if err := client.SyncPullToWriter("/vendor/policy", io.Discard); err != nil { + t.Fatalf("SyncPullToWriter() error = %v", err) + } +} + +func TestSyncPullToWriterReportsHostFailure(t *testing.T) { + address := startFakeADBServer(t, func(conn net.Conn) error { + request, err := readHostRequest(conn) + if err != nil { + return err + } + if request != "host:transport:missing" { + return fmt.Errorf("host request = %q", request) + } + if _, err := io.WriteString(conn, "FAIL0010device not found"); err != nil { + return err + } + return nil + }) + + client := &ADB{Serial: "missing", serverAddress: address} + err := client.SyncPullToWriter("/policy", io.Discard) + if err == nil || !strings.Contains(err.Error(), "device not found") { + t.Fatalf("SyncPullToWriter() error = %v, want device-not-found failure", err) + } +} + +func TestSyncPullToWriterReportsSyncFailure(t *testing.T) { + address := startFakeADBServer(t, func(conn net.Conn) error { + if err := expectHostRequest(conn, "host:transport-any"); err != nil { + return err + } + if err := expectHostRequest(conn, "sync:"); err != nil { + return err + } + if err := expectSyncRequest(conn, "RECV", "/missing"); err != nil { + return err + } + return writeSyncFrame(conn, "FAIL", []byte("No such file")) + }) + + client := &ADB{serverAddress: address} + err := client.SyncPullToWriter("/missing", io.Discard) + if err == nil || !strings.Contains(err.Error(), "No such file") { + t.Fatalf("SyncPullToWriter() error = %v, want missing-file failure", err) + } +} + +func TestReceiveSyncFileRejectsInvalidResponses(t *testing.T) { + tests := []struct { + name string + data []byte + }{ + { + name: "unknown response", + data: syncFrame("NOPE", nil), + }, + { + name: "oversized data", + data: syncHeader("DATA", maxSyncFrameSize+1), + }, + { + name: "oversized failure", + data: syncHeader("FAIL", maxSyncFrameSize+1), + }, + { + name: "truncated data", + data: append(syncHeader("DATA", 8), []byte("short")...), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := receiveSyncFile(bytes.NewReader(tt.data), io.Discard); err == nil { + t.Fatal("receiveSyncFile() error = nil, want error") + } + }) + } +} + +func TestReceiveSyncFilePropagatesWriterFailure(t *testing.T) { + input := append(syncFrame("DATA", []byte("policy")), syncFrame("DONE", nil)...) + wantErr := errors.New("write failed") + writer := errorWriter{err: wantErr} + + err := receiveSyncFile(bytes.NewReader(input), writer) + if !errors.Is(err, wantErr) { + t.Fatalf("receiveSyncFile() error = %v, want %v", err, wantErr) + } +} + +type errorWriter struct { + err error +} + +func (w errorWriter) Write([]byte) (int, error) { + return 0, w.err +} + +func startFakeADBServer(t *testing.T, serve func(net.Conn) error) string { + t.Helper() + + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + t.Fatalf("net.Listen() error = %v", err) + } + t.Cleanup(func() { + _ = listener.Close() + }) + + done := make(chan error, 1) + go func() { + conn, err := listener.Accept() + if err != nil { + done <- err + return + } + defer conn.Close() + done <- serve(conn) + }() + t.Cleanup(func() { + if err := <-done; err != nil && !errors.Is(err, net.ErrClosed) { + t.Errorf("fake ADB server error: %v", err) + } + }) + + return listener.Addr().String() +} + +func expectHostRequest(conn net.Conn, want string) error { + got, err := readHostRequest(conn) + if err != nil { + return err + } + if got != want { + return fmt.Errorf("host request = %q, want %q", got, want) + } + _, err = io.WriteString(conn, "OKAY") + return err +} + +func readHostRequest(reader io.Reader) (string, error) { + lengthBytes := make([]byte, 4) + if _, err := io.ReadFull(reader, lengthBytes); err != nil { + return "", err + } + length, err := strconv.ParseUint(string(lengthBytes), 16, 16) + if err != nil { + return "", err + } + request := make([]byte, int(length)) + if _, err := io.ReadFull(reader, request); err != nil { + return "", err + } + return string(request), nil +} + +func expectSyncRequest(reader io.Reader, wantID, wantPath string) error { + header := make([]byte, 8) + if _, err := io.ReadFull(reader, header); err != nil { + return err + } + if got := string(header[:4]); got != wantID { + return fmt.Errorf("sync request ID = %q, want %q", got, wantID) + } + length := binary.LittleEndian.Uint32(header[4:]) + path := make([]byte, int(length)) + if _, err := io.ReadFull(reader, path); err != nil { + return err + } + if got := string(path); got != wantPath { + return fmt.Errorf("sync request path = %q, want %q", got, wantPath) + } + return nil +} + +func writeSyncFrame(writer io.Writer, id string, data []byte) error { + if err := writeBytes(writer, syncHeader(id, len(data))); err != nil { + return err + } + return writeBytes(writer, data) +} + +func syncFrame(id string, data []byte) []byte { + return append(syncHeader(id, len(data)), data...) +} + +func syncHeader(id string, length int) []byte { + header := make([]byte, 8) + copy(header[:4], id) + binary.LittleEndian.PutUint32(header[4:], uint32(length)) + return header +} diff --git a/modules/selinux.go b/modules/selinux.go index 7749ffe..1dcb113 100644 --- a/modules/selinux.go +++ b/modules/selinux.go @@ -5,6 +5,7 @@ package modules import ( + "errors" "fmt" "github.com/mvt-project/androidqf/acquisition" @@ -14,6 +15,29 @@ import ( type SELinux struct{} +type selinuxPolicyFile struct { + remotePath string + archivePath string + optional bool +} + +var selinuxPolicyFiles = []selinuxPolicyFile{ + { + remotePath: "/odm/etc/selinux/precompiled_sepolicy", + archivePath: "selinux/odm/etc/selinux/precompiled_sepolicy", + optional: true, + }, + { + remotePath: "/vendor/etc/selinux/precompiled_sepolicy", + archivePath: "selinux/vendor/etc/selinux/precompiled_sepolicy", + optional: true, + }, + { + remotePath: "/sys/fs/selinux/policy", + archivePath: "selinux/sys/fs/selinux/policy", + }, +} + func NewSELinux() *SELinux { return &SELinux{} } @@ -23,12 +47,43 @@ func (s *SELinux) Name() string { } func (s *SELinux) Run(acq *acquisition.Acquisition, opts *Options) error { - log.Info("Collecting SELinux status...") + return s.run(acq, adb.Client.Shell, adb.Client.FileExists, acq.SyncPullToZipStaged) +} - out, err := adb.Client.Shell("getenforce") +func (s *SELinux) run( + acq *acquisition.Acquisition, + shell func(...string) (string, error), + fileExists func(string) (bool, error), + pull func(string, string) error, +) error { + log.Info("Collecting SELinux status and policies...") + + var errs []error + out, err := shell("getenforce") if err != nil { - return fmt.Errorf("failed to run `adb shell getenforce`: %v", err) + errs = append(errs, fmt.Errorf("failed to run `adb shell getenforce`: %w", err)) + } else if err := saveStringToAcquisition(acq, "selinux.txt", out); err != nil { + errs = append(errs, fmt.Errorf("failed to save SELinux status: %w", err)) + } + + for _, policy := range selinuxPolicyFiles { + if policy.optional { + exists, err := fileExists(policy.remotePath) + if err == nil && !exists { + log.Debugf("SELinux policy not present at %s", policy.remotePath) + continue + } + if err != nil { + log.Debugf("Unable to check for SELinux policy at %s, attempting collection: %v", policy.remotePath, err) + } + } + + log.Debugf("Collecting SELinux policy from %s", policy.remotePath) + if err := pull(policy.remotePath, policy.archivePath); err != nil { + log.Warningf("Failed to collect SELinux policy from %s: %v", policy.remotePath, err) + errs = append(errs, fmt.Errorf("failed to collect SELinux policy from %s: %w", policy.remotePath, err)) + } } - return saveStringToAcquisition(acq, "selinux.txt", out) + return errors.Join(errs...) } diff --git a/modules/selinux_test.go b/modules/selinux_test.go new file mode 100644 index 0000000..2b1b052 --- /dev/null +++ b/modules/selinux_test.go @@ -0,0 +1,143 @@ +package modules + +import ( + "archive/zip" + "errors" + "io" + "testing" + + "github.com/mvt-project/androidqf/acquisition" +) + +func TestSELinuxRunCollectsStatusAndAvailablePolicies(t *testing.T) { + writer := newModuleTestZipWriter(t) + acq := &acquisition.Acquisition{ + ZipWriter: writer, + StreamingMode: true, + } + + existing := map[string]bool{ + "/odm/etc/selinux/precompiled_sepolicy": true, + } + var pulls []selinuxPolicyFile + err := NewSELinux().run( + acq, + func(command ...string) (string, error) { + if len(command) != 1 || command[0] != "getenforce" { + t.Fatalf("shell command = %#v, want getenforce", command) + } + return "Enforcing", nil + }, + func(path string) (bool, error) { + return existing[path], nil + }, + func(remotePath, archivePath string) error { + pulls = append(pulls, selinuxPolicyFile{ + remotePath: remotePath, + archivePath: archivePath, + }) + return nil + }, + ) + if err != nil { + t.Fatalf("SELinux.run() error = %v", err) + } + + if len(pulls) != 2 { + t.Fatalf("policy pulls = %#v, want ODM and active policies", pulls) + } + if pulls[0].remotePath != "/odm/etc/selinux/precompiled_sepolicy" || + pulls[0].archivePath != "selinux/odm/etc/selinux/precompiled_sepolicy" { + t.Fatalf("first policy pull = %#v, want ODM policy mapping", pulls[0]) + } + if pulls[1].remotePath != "/sys/fs/selinux/policy" || + pulls[1].archivePath != "selinux/sys/fs/selinux/policy" { + t.Fatalf("second policy pull = %#v, want active policy mapping", pulls[1]) + } + + if err := writer.Close(); err != nil { + t.Fatalf("Close() error = %v", err) + } + files := readModuleTestZip(t, writer.GetOutputPath()) + if got := files["selinux.txt"]; got != "Enforcing" { + t.Fatalf("selinux.txt = %q, want Enforcing", got) + } +} + +func TestSELinuxRunAttemptsAllPoliciesAndAggregatesFailures(t *testing.T) { + writer := newModuleTestZipWriter(t) + acq := &acquisition.Acquisition{ + ZipWriter: writer, + StreamingMode: true, + } + t.Cleanup(func() { + _ = writer.Close() + }) + + statusErr := errors.New("getenforce failed") + odmErr := errors.New("odm pull failed") + activeErr := errors.New("active pull failed") + var pulls []string + err := NewSELinux().run( + acq, + func(...string) (string, error) { + return "", statusErr + }, + func(string) (bool, error) { + return true, nil + }, + func(remotePath, archivePath string) error { + pulls = append(pulls, remotePath) + switch remotePath { + case "/odm/etc/selinux/precompiled_sepolicy": + return odmErr + case "/sys/fs/selinux/policy": + return activeErr + default: + return nil + } + }, + ) + + if !errors.Is(err, statusErr) || !errors.Is(err, odmErr) || !errors.Is(err, activeErr) { + t.Fatalf("SELinux.run() error = %v, want all failures", err) + } + if len(pulls) != len(selinuxPolicyFiles) { + t.Fatalf("attempted %d policy pulls, want %d", len(pulls), len(selinuxPolicyFiles)) + } +} + +func newModuleTestZipWriter(t *testing.T) *acquisition.StreamingZipWriter { + t.Helper() + + writer, err := acquisition.NewStreamingZipWriter("selinux-test", t.TempDir()) + if err != nil { + t.Fatalf("NewStreamingZipWriter() error = %v", err) + } + return writer +} + +func readModuleTestZip(t *testing.T, archivePath string) map[string]string { + t.Helper() + + reader, err := zip.OpenReader(archivePath) + if err != nil { + t.Fatalf("zip.OpenReader() error = %v", err) + } + defer reader.Close() + + files := make(map[string]string) + for _, file := range reader.File { + fileReader, err := file.Open() + if err != nil { + t.Fatalf("Open(%q) error = %v", file.Name, err) + } + content, err := io.ReadAll(fileReader) + _ = fileReader.Close() + if err != nil { + t.Fatalf("ReadAll(%q) error = %v", file.Name, err) + } + files[file.Name] = string(content) + } + return files +}