diff --git a/tables/mdm/mdm.go b/tables/mdm/mdm.go index 3271443..c782ae0 100644 --- a/tables/mdm/mdm.go +++ b/tables/mdm/mdm.go @@ -86,16 +86,16 @@ func MDMInfoGenerate( ) ([]map[string]string, error) { fs := utils.OSFileSystem{} // There might not be any profiles installed, but we still care if the device is DEP capable, so discard the error - profiles, _ := getMDMProfile() + profiles, _ := getMDMProfileFunc() depEnrolled, userApproved := "unknown", "unknown" - status, err := getMDMProfileStatus(fs) + status, err := getMDMProfileStatusFunc(fs) if err == nil { // only supported on 10.13.4+ depEnrolled = strconv.FormatBool(status.DEPEnrolled) userApproved = strconv.FormatBool(status.UserApproved) } - depstatus := getDEPStatus(status, fs) + depstatus := getDEPStatusFunc(status, fs) depCapable := strconv.FormatBool(depstatus.DEPCapable) var enrollProfileItems []profileItem @@ -142,9 +142,12 @@ func MDMInfoGenerate( return results, nil } +var getMDMProfileFunc = getMDMProfile +var getMDMProfileStatusFunc = getMDMProfileStatus +var getDEPStatusFunc = getDEPStatus + func getMDMProfile() (*profilesOutput, error) { - cmd := exec.Command("/usr/bin/profiles", "-L", "-o", "stdout-xml") - out, err := cmd.Output() + out, err := runProfilesListCmd() if err != nil { return nil, errors.Wrap(err, "calling /usr/bin/profiles to get MDM profile payload") } @@ -157,19 +160,38 @@ func getMDMProfile() (*profilesOutput, error) { return &profiles, nil } +var runProfilesListCmd = func() ([]byte, error) { + cmd := exec.Command("/usr/bin/profiles", "-L", "-o", "stdout-xml") + return cmd.Output() +} + func getMDMProfileStatus(fs utils.FileSystem) (profileStatus, error) { - if !utils.FileExists(fs, "/usr/bin/profiles") { + if _, err := fs.Stat("/usr/bin/profiles"); err != nil { return profileStatus{}, errors.New("mdm: /usr/bin/profiles does not exist") } - cmd := exec.Command("/usr/bin/profiles", "status", "-type", "enrollment") - out, err := cmd.Output() + out, err := runProfilesStatusCmd() if err != nil { return profileStatus{}, errors.Wrap( err, "calling /usr/bin/profiles to get MDM profile status", ) } + return parseMDMProfileStatus(out) +} + +var runProfilesStatusCmd = func() ([]byte, error) { + cmd := exec.Command("/usr/bin/profiles", "status", "-type", "enrollment") + return cmd.Output() +} + +func parseMDMProfileStatus(out []byte) (profileStatus, error) { lines := bytes.Split(out, []byte("\n")) + if len(lines) < 2 { + return profileStatus{}, errors.Errorf( + "mdm: could not split the DEP Enrollment status %s", + string(out), + ) + } depEnrollmentParts := bytes.SplitN(lines[0], []byte(":"), 2) if len(depEnrollmentParts) < 2 { return profileStatus{}, errors.Errorf( diff --git a/tables/mdm/mdm_test.go b/tables/mdm/mdm_test.go index c484011..ea26597 100644 --- a/tables/mdm/mdm_test.go +++ b/tables/mdm/mdm_test.go @@ -2,6 +2,7 @@ package mdm import ( "context" + "errors" "fmt" "os" "testing" @@ -12,6 +13,51 @@ import ( "github.com/stretchr/testify/assert" ) +func withMDMProfileFunc(t *testing.T, fn func() (*profilesOutput, error)) { + t.Helper() + original := getMDMProfileFunc + getMDMProfileFunc = fn + t.Cleanup(func() { + getMDMProfileFunc = original + }) +} + +func withMDMProfileStatusFunc(t *testing.T, fn func(utils.FileSystem) (profileStatus, error)) { + t.Helper() + original := getMDMProfileStatusFunc + getMDMProfileStatusFunc = fn + t.Cleanup(func() { + getMDMProfileStatusFunc = original + }) +} + +func withDEPStatusFunc(t *testing.T, fn func(profileStatus, utils.FileSystem) depStatus) { + t.Helper() + original := getDEPStatusFunc + getDEPStatusFunc = fn + t.Cleanup(func() { + getDEPStatusFunc = original + }) +} + +func withRunProfilesListCmd(t *testing.T, fn func() ([]byte, error)) { + t.Helper() + original := runProfilesListCmd + runProfilesListCmd = fn + t.Cleanup(func() { + runProfilesListCmd = original + }) +} + +func withRunProfilesStatusCmd(t *testing.T, fn func() ([]byte, error)) { + t.Helper() + original := runProfilesStatusCmd + runProfilesStatusCmd = fn + t.Cleanup(func() { + runProfilesStatusCmd = original + }) +} + // TestMDMInfoColumns tests if the MDMInfoColumns function returns the correct columns func TestMDMInfoColumns(t *testing.T) { columns := MDMInfoColumns() @@ -36,38 +82,136 @@ func TestMDMInfoColumns(t *testing.T) { // TestMDMInfoGenerate tests the MDMInfoGenerate function func TestMDMInfoGenerate(t *testing.T) { - ctx := context.Background() - queryContext := table.QueryContext{} + withMDMProfileFunc(t, func() (*profilesOutput, error) { + return &profilesOutput{ComputerLevel: []profilePayload{{ + ProfileIdentifier: "com.example.mdm", + ProfileInstallDate: "2026-01-01 00:00:00 +0000", + ProfileItems: []profileItem{ + { + PayloadType: "com.apple.mdm", + PayloadContent: &payloadContent{ + AccessRights: 8191, + CheckInURL: "https://mdm.example.com/checkin", + ServerURL: "https://mdm.example.com/server", + Topic: "com.apple.mgmt.External.example", + IdentityCertificateUUID: "identity-uuid", + SignMessage: true, + }, + }, + {PayloadType: "com.apple.security.scep"}, + }, + }}}, nil + }) + withMDMProfileStatusFunc(t, func(fs utils.FileSystem) (profileStatus, error) { + return profileStatus{DEPEnrolled: true, UserApproved: true}, nil + }) + withDEPStatusFunc(t, func(status profileStatus, fs utils.FileSystem) depStatus { + return depStatus{DEPCapable: true} + }) + + results, err := MDMInfoGenerate(context.Background(), table.QueryContext{}) + assert.NoError(t, err) + assert.Equal(t, []map[string]string{{ + "enrolled": "true", + "server_url": "https://mdm.example.com/server", + "checkin_url": "https://mdm.example.com/checkin", + "access_rights": "8191", + "install_date": "2026-01-01 00:00:00 +0000", + "payload_identifier": "com.example.mdm", + "sign_message": "true", + "topic": "com.apple.mgmt.External.example", + "identity_certificate_uuid": "identity-uuid", + "installed_from_dep": "true", + "user_approved": "true", + "has_scep_payload": "true", + "dep_capable": "true", + }}, results) +} - results, err := MDMInfoGenerate(ctx, queryContext) +func TestMDMInfoGenerateUnenrolledWhenProfileMissing(t *testing.T) { + withMDMProfileFunc(t, func() (*profilesOutput, error) { + return nil, errors.New("profiles failed") + }) + withMDMProfileStatusFunc(t, func(fs utils.FileSystem) (profileStatus, error) { + return profileStatus{}, errors.New("status unsupported") + }) + withDEPStatusFunc(t, func(status profileStatus, fs utils.FileSystem) depStatus { + return depStatus{} + }) + results, err := MDMInfoGenerate(context.Background(), table.QueryContext{}) assert.NoError(t, err) - assert.NotNil(t, results) + assert.Equal(t, []map[string]string{{ + "enrolled": "false", + "dep_capable": "false", + }}, results) } -// TestGetMDMProfile tests the getMDMProfile function func TestGetMDMProfile(t *testing.T) { + withRunProfilesListCmd(t, func() ([]byte, error) { + return []byte(` + + +_computerlevel + +ProfileIdentifiercom.example.mdm +ProfileInstallDate2026-01-01 +ProfileItems + + +`), nil + }) + profiles, err := getMDMProfile() + assert.NoError(t, err) + assert.Equal(t, "com.example.mdm", profiles.ComputerLevel[0].ProfileIdentifier) +} + +func TestGetMDMProfileErrors(t *testing.T) { + t.Run("command error", func(t *testing.T) { + withRunProfilesListCmd(t, func() ([]byte, error) { + return nil, errors.New("profiles failed") + }) + profiles, err := getMDMProfile() + assert.Error(t, err) + assert.Nil(t, profiles) + assert.ErrorContains(t, err, "calling /usr/bin/profiles") + }) - // Since profiles isn't present on non-macOS, the test should handle both cases - if err == nil { - assert.NotNil(t, profiles) - } else { + t.Run("invalid plist", func(t *testing.T) { + withRunProfilesListCmd(t, func() ([]byte, error) { + return []byte("not plist"), nil + }) + profiles, err := getMDMProfile() assert.Error(t, err) - } + assert.Nil(t, profiles) + assert.ErrorContains(t, err, "unmarshal profiles output") + }) } -// TestGetMDMProfileStatus tests the getMDMProfileStatus function func TestGetMDMProfileStatus(t *testing.T) { + withRunProfilesStatusCmd(t, func() ([]byte, error) { + return []byte("Enrolled via DEP: Yes\nMDM enrollment: User Approved\n"), nil + }) fs := utils.MockFileSystem{FileExists: true, Err: nil} status, err := getMDMProfileStatus(fs) - // Since the status is only supported on 10.13.4+, the test should handle both cases - if err == nil { - assert.NotNil(t, status) - } else { - assert.Error(t, err) - } + assert.NoError(t, err) + assert.Equal(t, profileStatus{DEPEnrolled: true, UserApproved: true}, status) +} + +func TestParseMDMProfileStatusErrors(t *testing.T) { + _, err := parseMDMProfileStatus([]byte("Enrolled via DEP Yes\nMDM enrollment: User Approved\n")) + assert.Error(t, err) + assert.ErrorContains(t, err, "could not split the DEP Enrollment source") + + _, err = parseMDMProfileStatus([]byte("Enrolled via DEP: Yes\nMDM enrollment User Approved\n")) + assert.Error(t, err) + assert.ErrorContains(t, err, "could not split the DEP Enrollment status") + + _, err = parseMDMProfileStatus([]byte("Enrolled via DEP: Yes")) + assert.Error(t, err) + assert.ErrorContains(t, err, "could not split the DEP Enrollment status") } // TestGetDEPStatus tests the getDEPStatus function