fleet/cmd/fleetctl/fleetctl/generate_gitops_test.go
2026-03-13 16:47:09 -04:00

2064 lines
65 KiB
Go

// filepath: cmd/fleetctl/generate_gitops_test.go
package fleetctl
import (
"bytes"
"encoding/json"
"errors"
"flag"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"github.com/fleetdm/fleet/v4/pkg/optjson"
"github.com/fleetdm/fleet/v4/server/dev_mode"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/ghodss/yaml"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
ma "github.com/fleetdm/fleet/v4/ee/maintained-apps"
)
type MockClient struct {
IsFree bool
TeamNameOverride string
WithoutMDM bool
}
func (c *MockClient) GetAppConfig() (*fleet.EnrichedAppConfig, error) {
b, err := os.ReadFile("./testdata/generateGitops/appConfig.json")
if err != nil {
return nil, err
}
var appConfig fleet.EnrichedAppConfig
if err := json.Unmarshal(b, &appConfig); err != nil {
return nil, err
}
if c.IsFree == true {
appConfig.License.Tier = fleet.TierFree
}
if c.WithoutMDM {
appConfig.MDM.EnabledAndConfigured = false
appConfig.MDM.AndroidEnabledAndConfigured = false
appConfig.MDM.WindowsEnabledAndConfigured = false
}
return &appConfig, nil
}
func (MockClient) GetEnrollSecretSpec() (*fleet.EnrollSecretSpec, error) {
spec := &fleet.EnrollSecretSpec{
Secrets: []*fleet.EnrollSecret{
{
Secret: "some-secret-number-one",
},
{
Secret: "some-secret-number-two",
},
},
}
return spec, nil
}
func (c *MockClient) ListTeams(query string) ([]fleet.Team, error) {
var config fleet.TeamConfig
b, err := os.ReadFile("./testdata/generateGitops/teamConfig.json")
if err != nil {
return nil, err
}
if err := json.Unmarshal(b, &config); err != nil {
return nil, err
}
teams := []fleet.Team{
{
ID: 1,
Name: "Team A 👍",
Config: config,
Secrets: []*fleet.EnrollSecret{
{
Secret: "some-team-secret",
},
},
},
}
if c.TeamNameOverride != "" {
teams[0].Name = c.TeamNameOverride
}
return teams, nil
}
func (MockClient) ListScripts(query string) ([]*fleet.Script, error) {
switch query {
case "fleet_id=1":
return []*fleet.Script{{
ID: 2,
TeamID: ptr.Uint(1),
Name: "Script B.ps1",
ScriptContentID: 2,
}}, nil
case "fleet_id=0":
return []*fleet.Script{{
ID: 3,
TeamID: ptr.Uint(0),
Name: "Script Z.ps1",
ScriptContentID: 3,
}}, nil
case "fleet_id=2", "fleet_id=3", "fleet_id=4", "fleet_id=5":
return nil, nil
case "fleet_id=6":
return nil, nil
default:
return nil, fmt.Errorf("unexpected query: %s", query)
}
}
func (c MockClient) ListConfigurationProfiles(teamID *uint) ([]*fleet.MDMConfigProfilePayload, error) {
if c.WithoutMDM {
return nil, errors.New("should not have pulled configuration profiles endpoint")
}
if teamID == nil {
return []*fleet.MDMConfigProfilePayload{
{
ProfileUUID: "global-macos-mobileconfig-profile-uuid",
Name: "Global MacOS MobileConfig Profile",
Platform: "darwin",
Identifier: "com.example.global-macos-mobileconfig-profile",
LabelsIncludeAll: []fleet.ConfigurationProfileLabel{{
LabelName: "Label A",
}, {
LabelName: "Label B",
}},
},
{
ProfileUUID: "global-macos-json-profile-uuid",
Name: "Global MacOS JSON Profile",
Platform: "darwin",
Identifier: "com.example.global-macos-json-profile",
LabelsExcludeAny: []fleet.ConfigurationProfileLabel{{
LabelName: "Label C",
}},
},
{
ProfileUUID: "global-windows-profile-uuid",
Name: "Global Windows Profile",
Platform: "windows",
LabelsIncludeAny: []fleet.ConfigurationProfileLabel{{
LabelName: "Label D",
}},
},
{
ProfileUUID: "global-android-profile-uuid",
Name: "Global Android Profile",
Platform: "android",
},
}, nil
}
if *teamID == 1 {
return []*fleet.MDMConfigProfilePayload{
{
ProfileUUID: "test-mobileconfig-profile-uuid",
Name: "Team MacOS MobileConfig Profile",
Platform: "darwin",
Identifier: "com.example.team-macos-mobileconfig-profile",
},
}, nil
}
if *teamID == 0 || *teamID == 2 || *teamID == 3 || *teamID == 4 || *teamID == 5 || *teamID == 6 {
return nil, nil
}
return nil, fmt.Errorf("unexpected team ID: %v", *teamID)
}
func (MockClient) GetScriptContents(scriptID uint) ([]byte, error) {
if scriptID == 2 {
return []byte("pop goes the weasel!"), nil
}
if scriptID == 3 {
return []byte("#!/usr/bin/env pwsh\necho \"Hello from Script B!\""), nil
}
return nil, errors.New("script not found")
}
func (MockClient) GetProfileContents(profileID string) ([]byte, error) {
switch profileID {
case "global-macos-mobileconfig-profile-uuid":
return []byte("<xml>global macos mobileconfig profile</xml>"), nil
case "global-macos-json-profile-uuid":
return []byte(`{"profile": "global macos json profile"}`), nil
case "global-windows-profile-uuid":
return []byte("<xml>global windows profile</xml>"), nil
case "global-android-profile-uuid":
return []byte(`{"name": "Global Android Profile", "cameraDisabled": true}`), nil
case "test-mobileconfig-profile-uuid":
return []byte("<xml>test mobileconfig profile</xml>"), nil
}
return nil, errors.New("profile not found")
}
func (MockClient) GetTeam(teamID uint) (*fleet.Team, error) {
if teamID == 0 {
// Return "No Team" configuration with webhook settings
return &fleet.Team{
ID: 0,
Name: "No team",
Config: fleet.TeamConfig{
WebhookSettings: fleet.TeamWebhookSettings{
FailingPoliciesWebhook: fleet.FailingPoliciesWebhookSettings{
Enable: true,
DestinationURL: "https://example.com/no-team-webhook",
PolicyIDs: []uint{1, 2, 3},
HostBatchSize: 100,
},
},
},
}, nil
}
if teamID == 1 {
b, err := os.ReadFile("./testdata/generateGitops/teamConfig.json")
if err != nil {
return nil, err
}
var config fleet.TeamConfig
if err := json.Unmarshal(b, &config); err != nil {
return nil, err
}
return &fleet.Team{
ID: 1,
Name: "Test Team",
Config: config,
Secrets: []*fleet.EnrollSecret{
{
Secret: "some-team-secret",
},
},
}, nil
}
return nil, errors.New("team not found")
}
func (MockClient) ListSoftwareTitles(query string) ([]fleet.SoftwareTitleListResult, error) {
switch query {
case "available_for_install=1&fleet_id=1":
return []fleet.SoftwareTitleListResult{
{
ID: 1,
Name: "My Software Package",
HashSHA256: ptr.String("software-package-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "my-software.pkg",
Platform: "darwin",
Version: "13.37",
},
},
{
ID: 2,
Name: "My App Store App",
AppStoreApp: &fleet.SoftwarePackageOrApp{
AppStoreID: "com.example.team-software",
Platform: string(fleet.MacOSPlatform),
},
HashSHA256: ptr.String("app-store-app-hash"),
},
{
ID: 6,
Name: "My Setup Experience App",
AppStoreApp: &fleet.SoftwarePackageOrApp{
AppStoreID: "com.example.setup-experience-software",
Platform: string(fleet.AndroidPlatform),
InstallDuringSetup: ptr.Bool(true),
},
HashSHA256: ptr.String("app-setup-experience-hash"),
},
{
ID: 7,
Name: "My iOS Auto Update App",
AppStoreApp: &fleet.SoftwarePackageOrApp{
AppStoreID: "com.example.ios-auto-update",
Platform: string(fleet.IOSPlatform),
},
HashSHA256: ptr.String("ios-auto-update-hash"),
},
{
ID: 8,
Name: "My FMA",
HashSHA256: ptr.String("fma-package-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "my-fma.pkg",
Platform: "darwin",
Version: "1",
FleetMaintainedAppID: ptr.Uint(1),
},
},
{
ID: 9,
Name: "My Windows FMA",
HashSHA256: ptr.String("win-fma-package-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "my-fma.msi",
Platform: "windows",
Version: "1",
FleetMaintainedAppID: ptr.Uint(2),
},
},
}, nil
case "available_for_install=1&fleet_id=0":
return []fleet.SoftwareTitleListResult{}, nil
default:
return nil, fmt.Errorf("unexpected query: %s", query)
}
}
func (MockClient) GetFleetMaintainedApp(id uint) (*fleet.MaintainedApp, error) {
return &fleet.MaintainedApp{Slug: "foo/darwin"}, nil
}
func (MockClient) GetPolicies(teamID *uint) ([]*fleet.Policy, error) {
if teamID == nil {
return []*fleet.Policy{
{
PolicyData: fleet.PolicyData{
ID: 1,
Name: "Global Policy",
Query: "SELECT * FROM global_policy WHERE id = 1",
Resolution: ptr.String("Do a global thing"),
Description: "This is a global policy",
Platform: "darwin",
LabelsIncludeAny: []fleet.LabelIdent{{
LabelName: "Label A",
}, {
LabelName: "Label B",
}},
ConditionalAccessEnabled: true,
Type: fleet.PolicyTypeDynamic,
},
InstallSoftware: &fleet.PolicySoftwareTitle{
SoftwareTitleID: 1,
},
},
}, nil
}
return []*fleet.Policy{
{
PolicyData: fleet.PolicyData{
ID: 1,
Name: "Team Policy",
Query: "SELECT * FROM team_policy WHERE id = 1",
Resolution: ptr.String("Do a team thing"),
Description: "This is a team policy",
Platform: "linux,windows",
ConditionalAccessEnabled: true,
Type: fleet.PolicyTypeDynamic,
},
RunScript: &fleet.PolicyScript{
ID: 1,
},
},
{
PolicyData: fleet.PolicyData{
ID: 2,
Name: "Team patch policy",
Query: "SELECT * FROM team_policy WHERE id = 1",
Resolution: ptr.String("Do a team thing"),
Description: "This is a team patch policy",
Platform: "linux,windows",
ConditionalAccessEnabled: true,
Type: fleet.PolicyTypePatch,
},
PatchSoftware: &fleet.PolicySoftwareTitle{
SoftwareTitleID: 8,
},
},
}, nil
}
func (MockClient) GetQueries(teamID *uint, name *string) ([]fleet.Query, error) {
if teamID == nil {
return []fleet.Query{
{
ID: 1,
Name: "Global Query",
Query: "SELECT * FROM global_query WHERE id = 1",
Description: "This is a global query",
Platform: "darwin",
Interval: 3600,
ObserverCanRun: true,
AutomationsEnabled: true,
LabelsIncludeAny: []fleet.LabelIdent{{
LabelName: "Label A",
}, {
LabelName: "Label B",
}},
MinOsqueryVersion: "1.2.3",
Logging: "stdout",
},
}, nil
}
return []fleet.Query{
{
ID: 1,
Name: "Team Query",
Query: "SELECT * FROM team_query WHERE id = 1",
Description: "This is a team query",
Platform: "linux,windows",
Interval: 1800,
ObserverCanRun: false,
AutomationsEnabled: true,
MinOsqueryVersion: "4.5.6",
Logging: "stderr",
},
}, nil
}
//nolint:gocritic // ignore captLocal
func (MockClient) GetSoftwareTitleByID(ID uint, teamID *uint) (*fleet.SoftwareTitle, error) {
switch ID {
case 1:
if *teamID != 1 {
return nil, errors.New("team ID mismatch")
}
return &fleet.SoftwareTitle{
ID: 1,
SoftwarePackage: &fleet.SoftwareInstaller{
LabelsIncludeAny: []fleet.SoftwareScopeLabel{{
LabelName: "Label A",
}, {
LabelName: "Label B",
}},
PreInstallQuery: "SELECT * FROM pre_install_query",
InstallScript: "foo",
PostInstallScript: "bar",
UninstallScript: "baz",
SelfService: true,
Platform: "darwin",
URL: "https://example.com/download/my-software.pkg",
Categories: []string{"Browsers"},
},
IconUrl: ptr.String("/api/icon1.png"),
}, nil
case 2:
if *teamID != 1 {
return nil, errors.New("team ID mismatch")
}
return &fleet.SoftwareTitle{
ID: 2,
AppStoreApp: &fleet.VPPAppStoreApp{
VPPAppID: fleet.VPPAppID{Platform: fleet.MacOSPlatform},
LabelsExcludeAny: []fleet.SoftwareScopeLabel{{
LabelName: "Label C",
}, {
LabelName: "Label D",
}},
Categories: []string{"Productivity", "Utilities"},
SelfService: true,
},
IconUrl: ptr.String("/api/icon2.png"),
}, nil
case 6:
if *teamID != 1 {
return nil, errors.New("team ID mismatch")
}
return &fleet.SoftwareTitle{
ID: 6,
AppStoreApp: &fleet.VPPAppStoreApp{
VPPAppID: fleet.VPPAppID{AdamID: "com.example.setup-experience-software", Platform: fleet.AndroidPlatform},
LabelsExcludeAny: []fleet.SoftwareScopeLabel{},
SelfService: true,
},
IconUrl: ptr.String("/api/icon3.png"),
}, nil
case 7:
if *teamID != 1 {
return nil, errors.New("team ID mismatch")
}
return &fleet.SoftwareTitle{
ID: 7,
AppStoreApp: &fleet.VPPAppStoreApp{
VPPAppID: fleet.VPPAppID{AdamID: "com.example.ios-auto-update", Platform: fleet.IOSPlatform},
SelfService: false,
},
IconUrl: ptr.String("/api/icon4.png"),
SoftwareAutoUpdateConfig: fleet.SoftwareAutoUpdateConfig{
AutoUpdateEnabled: ptr.Bool(true),
AutoUpdateStartTime: ptr.String("01:00"),
AutoUpdateEndTime: ptr.String("03:00"),
},
}, nil
case 8:
return &fleet.SoftwareTitle{
ID: 8,
SoftwarePackage: &fleet.SoftwareInstaller{
LabelsIncludeAny: []fleet.SoftwareScopeLabel{{
LabelName: "Label A",
}, {
LabelName: "Label B",
}},
PreInstallQuery: "SELECT * FROM pre_install_query",
InstallScript: "install",
PostInstallScript: "postinstall",
UninstallScript: "uninstall",
SelfService: true,
Platform: "darwin",
URL: "https://example.com/download/my-software.pkg",
Categories: []string{"Browsers"},
BundleIdentifier: "com.my.fma",
FleetMaintainedAppID: ptr.Uint(1),
},
IconUrl: ptr.String("/api/icon1.png"),
BundleIdentifier: ptr.String("com.my.fma"),
}, nil
case 9:
return &fleet.SoftwareTitle{
ID: 9,
Name: "My Windows FMA",
SoftwarePackage: &fleet.SoftwareInstaller{
InstallScript: "install",
UninstallScript: "uninstall",
SelfService: true,
Platform: "windows",
FleetMaintainedAppID: ptr.Uint(2),
},
IconUrl: ptr.String("/api/icon5.png"),
}, nil
default:
return nil, errors.New("software title not found")
}
}
func (MockClient) GetSoftwareTitleIcon(titleID uint, teamID uint) ([]byte, error) {
return []byte(fmt.Sprintf("icon for title %d on team %d", titleID, teamID)), nil
}
func (MockClient) GetLabels(teamID uint) ([]*fleet.LabelSpec, error) {
if teamID != 0 {
return nil, nil // simulate no team-specific labels
}
return []*fleet.LabelSpec{{
Name: "Label A",
Platform: "linux,macos",
Description: "Label A description",
LabelMembershipType: fleet.LabelMembershipTypeDynamic,
Query: "SELECT * FROM osquery_info",
}, {
Name: "Label B",
Description: "Label B description",
LabelMembershipType: fleet.LabelMembershipTypeManual,
Hosts: []string{"1", "2"},
}, {
Name: "Label C",
Description: "Label C description",
LabelMembershipType: fleet.LabelMembershipTypeHostVitals,
HostVitalsCriteria: ptr.RawMessage(json.RawMessage(`{"vital": "end_user_idp_group", "value": "some-group"}`)),
}}, nil
}
func (MockClient) Me() (*fleet.User, error) {
return &fleet.User{
ID: 1,
Name: "Test User",
Email: "test@example.com",
GlobalRole: ptr.String("admin"),
}, nil
}
func (MockClient) GetEULAMetadata() (*fleet.MDMEULA, error) {
return &fleet.MDMEULA{
Name: "test.pdf",
Token: "test-eula-token",
}, nil
}
func (MockClient) GetEULAContent(token string) ([]byte, error) {
return []byte("This is the EULA content."), nil
}
func (MockClient) GetSetupExperienceSoftware(platform string, teamID uint) ([]fleet.SoftwareTitleListResult, error) {
if teamID == 1 {
return []fleet.SoftwareTitleListResult{
{
ID: 1,
Name: "My Software Package",
HashSHA256: ptr.String("software-package-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
InstallDuringSetup: ptr.Bool(true),
Name: "my-software.pkg",
Platform: "darwin",
Version: "13.37",
},
},
{
ID: 6,
Name: "My Setup Experience App",
AppStoreApp: &fleet.SoftwarePackageOrApp{
AppStoreID: "com.example.setup-experience-software",
Platform: string(fleet.AndroidPlatform),
InstallDuringSetup: ptr.Bool(true),
},
HashSHA256: ptr.String("app-setup-experience-hash"),
},
{
ID: 8,
Name: "My FMA",
SoftwarePackage: &fleet.SoftwarePackageOrApp{
InstallDuringSetup: ptr.Bool(true),
Name: "my-fma.pkg",
Platform: "darwin",
},
},
}, nil
}
if teamID == 2 {
return []fleet.SoftwareTitleListResult{
{
ID: 1,
Name: "My Software Package",
HashSHA256: ptr.String("software-package-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
InstallDuringSetup: ptr.Bool(false),
Name: "my-software.pkg",
Platform: "darwin",
Version: "13.37",
},
},
{
ID: 2,
Name: "My Other Software Package",
HashSHA256: ptr.String("software-package-hash"),
},
}, nil
}
if teamID == 0 || teamID == 3 || teamID == 4 || teamID == 5 {
return nil, nil
}
return nil, fmt.Errorf("unexpected team ID: %d", teamID)
}
func (MockClient) GetBootstrapPackageMetadata(teamID uint, forUpdate bool) (*fleet.MDMAppleBootstrapPackage, error) {
if teamID == 3 {
return &fleet.MDMAppleBootstrapPackage{
Name: "Bootstrap Package for Team 1",
}, nil
}
if teamID == 0 || teamID == 1 || teamID == 2 || teamID == 4 || teamID == 5 {
return nil, nil
}
return nil, fmt.Errorf("unexpected team ID: %d", teamID)
}
func (MockClient) GetSetupExperienceScript(teamID uint) (*fleet.Script, error) {
if teamID == 4 {
return &fleet.Script{
Name: "Setup Experience Script for Team 1",
}, nil
}
if teamID == 0 || teamID == 1 || teamID == 2 || teamID == 3 || teamID == 5 {
return nil, nil
}
return nil, fmt.Errorf("unexpected team ID: %d", teamID)
}
func (MockClient) GetAppleMDMEnrollmentProfile(teamID uint) (*fleet.MDMAppleSetupAssistant, error) {
if teamID == 5 {
return &fleet.MDMAppleSetupAssistant{
Name: "Apple MDM Enrollment Profile for Team 1",
}, nil
}
if teamID == 0 || teamID == 1 || teamID == 2 || teamID == 3 || teamID == 4 {
return nil, nil
}
return nil, fmt.Errorf("unexpected team ID: %d", teamID)
}
func (MockClient) GetCertificateAuthoritiesSpec(includeSecrets bool) (*fleet.GroupedCertificateAuthorities, error) {
res := fleet.GroupedCertificateAuthorities{
DigiCert: []fleet.DigiCertCA{
{
Name: "some-digicert-name",
URL: "https://some-digicert-url.com",
APIToken: maskSecret("some-digicert-api-token", includeSecrets),
ProfileID: "some-digicert-profile-id",
CertificateCommonName: "some-digicert-certificate-common-name",
CertificateUserPrincipalNames: []string{
"some-digicert-certificate-user-principal-name",
"some-other-digicert-certificate-user-principal-name",
},
CertificateSeatID: "some-digicert-certificate-seat-id",
},
},
NDESSCEP: &fleet.NDESSCEPProxyCA{
URL: "https://some-ndes-scep-proxy-url.com",
AdminURL: "https://some-ndes-admin-url.com",
Username: "some-ndes-username",
Password: maskSecret("some-ndes-password", includeSecrets),
},
CustomScepProxy: []fleet.CustomSCEPProxyCA{
{
Name: "some-custom-scep-proxy-name",
URL: "https://some-custom-scep-proxy-url.com",
Challenge: maskSecret("some-custom-scep-proxy-challenge", includeSecrets),
},
},
Hydrant: []fleet.HydrantCA{
{
Name: "some-hydrant-name",
URL: "https://some-hydrant-url.com",
ClientID: "some-hydrant-client-id",
ClientSecret: maskSecret("some-hydrant-client-secret", includeSecrets),
},
},
EST: []fleet.ESTProxyCA{
{
Name: "some-est-name",
URL: "https://some-est-url.example.com",
Username: "some-est-username",
Password: maskSecret("some-est-password", includeSecrets),
},
},
Smallstep: []fleet.SmallstepSCEPProxyCA{
{
Name: "some-smallstep-name",
URL: "https://some-smallstep-url.com",
ChallengeURL: "https://some-smallstep-challenge-url.com",
Username: "some-smallstep-username",
Password: maskSecret("some-smallstep-password", includeSecrets),
},
},
}
return &res, nil
}
func (MockClient) GetCertificateTemplates(teamID string) ([]*fleet.CertificateTemplateResponseSummary, error) {
var res []*fleet.CertificateTemplateResponseSummary
if teamID == "1" {
res = []*fleet.CertificateTemplateResponseSummary{
{
ID: 1,
CertificateAuthorityName: "DIGIDOO",
Name: "my_certypoo",
SubjectName: "CN=OU=$FLEET_VAR_HOST_UUID/ST=$FLEET_VAR_HOST_HARDWARE_SERIAL",
},
}
}
return res, nil
}
func maskSecret(value string, shouldShowSecret bool) string {
if shouldShowSecret {
return value
}
return "********"
}
func compareDirs(t *testing.T, sourceDir, targetDir string) {
err := filepath.WalkDir(sourceDir, func(srcPath string, d os.DirEntry, walkErr error) error {
if d.IsDir() {
return nil
}
relPath, err := filepath.Rel(sourceDir, srcPath)
require.NoError(t, err, "Error getting relative path: %v", err)
// Patch these files because Go can't zip the module with emojis in the filename.
// The actual outputted file will have the emoji, but the testdata file can't.
relPath = strings.Replace(relPath, "team-a-thumbsup", "team-a-👍", 1)
tgtPath := filepath.Join(targetDir, relPath)
targetInfo, err := os.Stat(tgtPath)
require.NoError(t, err, "Error getting target file info: %v", err)
require.False(t, targetInfo.IsDir(), "Expected file but found directory: %s", tgtPath)
srcData, err := os.ReadFile(srcPath)
require.NoError(t, err, "Error reading source file: %v", err)
tgtData, err := os.ReadFile(tgtPath)
require.NoError(t, err, "Error reading target file: %v", err)
require.Equal(t, string(srcData), string(tgtData), "File contents do not match for %s", relPath)
return nil
})
if err != nil {
t.Fatalf("Error walking source directory: %v", err)
}
}
func configureFMAManifestServer(t *testing.T) {
manifestServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.Contains(r.URL.Path, "apps.json") {
data := json.RawMessage(`{"version": 2, "apps": [{"name": "My FMA", "slug": "fma1/darwin", "platform": "darwin", "unique_identifier": "com.my.fma"}, {"name": "My Windows FMA", "slug": "fma2/windows", "platform": "windows", "unique_identifier": "My Windows FMA"}]}`)
err := json.NewEncoder(w).Encode(data)
require.NoError(t, err)
return
}
slug := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, ".json"), "/")
var versions []*ma.FMAManifestApp
versions = append(versions, &ma.FMAManifestApp{
Version: "1",
Queries: ma.FMAQueries{
Exists: "SELECT 1 FROM osquery_info;",
},
InstallScriptRef: "install_ref",
UninstallScriptRef: "uninstall_ref",
DefaultCategories: []string{"Productivity"},
Slug: slug,
})
manifest := ma.FMAManifestFile{
Versions: versions,
Refs: map[string]string{
"install_ref": "install",
"uninstall_ref": "uninstall",
},
}
err := json.NewEncoder(w).Encode(manifest)
require.NoError(t, err)
}))
t.Cleanup(manifestServer.Close)
dev_mode.SetOverride("FLEET_DEV_MAINTAINED_APPS_BASE_URL", manifestServer.URL, t)
}
func TestGenerateGitops(t *testing.T) {
configureFMAManifestServer(t)
fleetClient := &MockClient{}
action := createGenerateGitopsAction(fleetClient)
buf := new(bytes.Buffer)
tempDir := os.TempDir() + "/" + uuid.New().String()
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.String("dir", tempDir, "")
cliContext := cli.NewContext(&cli.App{
Name: "test",
Usage: "test",
Writer: buf,
ErrWriter: buf,
}, flagSet, nil)
err := action(cliContext)
require.NoError(t, err, buf.String())
compareDirs(t, "./testdata/generateGitops/test_dir_premium", tempDir)
t.Cleanup(func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatalf("failed to remove temp dir: %v", err)
}
})
}
func TestGenerateGitopsWithoutMDM(t *testing.T) {
configureFMAManifestServer(t)
fleetClient := &MockClient{WithoutMDM: true}
action := createGenerateGitopsAction(fleetClient)
buf := new(bytes.Buffer)
tempDir := os.TempDir() + "/" + uuid.New().String()
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.String("dir", tempDir, "")
cliContext := cli.NewContext(&cli.App{
Name: "test",
Usage: "test",
Writer: buf,
ErrWriter: buf,
}, flagSet, nil)
err := action(cliContext)
require.NoError(t, err, buf.String()) // just checking for success to verify #33667
t.Cleanup(func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatalf("failed to remove temp dir: %v", err)
}
})
}
func TestGenerateGitopsFree(t *testing.T) {
fleetClient := &MockClient{}
fleetClient.IsFree = true
action := createGenerateGitopsAction(fleetClient)
buf := new(bytes.Buffer)
tempDir := os.TempDir() + "/" + uuid.New().String()
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.String("dir", tempDir, "")
cliContext := cli.NewContext(&cli.App{
Name: "test",
Usage: "test",
Writer: buf,
ErrWriter: buf,
}, flagSet, nil)
err := action(cliContext)
require.NoError(t, err, buf.String())
compareDirs(t, "./testdata/generateGitops/test_dir_free", tempDir)
t.Cleanup(func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatalf("failed to remove temp dir: %v", err)
}
})
}
func TestGenerateOrgSettings(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
// Generate the org settings.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
orgSettingsRaw, err := cmd.generateOrgSettings()
require.NoError(t, err)
require.NotNil(t, orgSettingsRaw)
var orgSettings map[string]interface{}
b, err := yamlMarshalRenamed(orgSettingsRaw)
require.NoError(t, err)
fmt.Println("Org settings raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &orgSettings)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedOrgSettings.yaml")
require.NoError(t, err)
var expectedAppConfig map[string]interface{}
err = yaml.Unmarshal(b, &expectedAppConfig)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedAppConfig, orgSettings)
}
func TestGenerateOrgSettingsMaskedGoogleCalendarApiKey(t *testing.T) {
// This test verifies that generateOrgSettings handles the case where
// api_key_json is masked (returned as "********" string instead of a map).
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Set the Google Calendar API key to masked, which will serialize as "********"
require.NotEmpty(t, appConfig.Integrations.GoogleCalendar)
appConfig.Integrations.GoogleCalendar[0].ApiKey.SetMasked()
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]any),
AppConfig: appConfig,
}
// Generate the org settings - this should not panic.
orgSettingsRaw, err := cmd.generateOrgSettings()
require.NoError(t, err)
require.NotNil(t, orgSettingsRaw)
// Verify the result can be marshaled to YAML without error.
b, err := yamlMarshalRenamed(orgSettingsRaw)
require.NoError(t, err)
// Verify api_key_json was replaced with a comment placeholder (not "********")
var orgSettings map[string]any
err = yaml.Unmarshal(b, &orgSettings)
require.NoError(t, err)
integrations := orgSettings["integrations"].(map[string]any)
googleCalendar := integrations["google_calendar"].([]any)
intg := googleCalendar[0].(map[string]any)
apiKeyJson := intg["api_key_json"]
// Should be a comment placeholder string, not "********" or a map
apiKeyJsonStr, ok := apiKeyJson.(string)
require.True(t, ok, "api_key_json should be a string placeholder")
require.Contains(t, apiKeyJsonStr, "GITOPS_COMMENT", "api_key_json should be a comment placeholder")
// Verify SecretWarning was added for google_calendar.api_key_json
var foundWarning bool
for _, w := range cmd.Messages.SecretWarnings {
if w.Key == "integrations.google_calendar.api_key_json" {
foundWarning = true
break
}
}
require.True(t, foundWarning, "expected SecretWarning for integrations.google_calendar.api_key_json")
}
func TestGeneratedOrgSettingsNoSSO(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
appConfig.SSOSettings = nil
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
// Generate the org settings.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
orgSettingsRaw, err := cmd.generateOrgSettings()
require.NoError(t, err)
require.NotNil(t, orgSettingsRaw)
var orgSettings map[string]any
b, err := yamlMarshalRenamed(orgSettingsRaw)
require.NoError(t, err)
err = yaml.Unmarshal(b, &orgSettings)
require.NoError(t, err)
}
func TestGenerateSoftwareAutoUpdateSchedule(t *testing.T) {
configureFMAManifestServer(t)
fleetClient := &MockClient{}
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]any),
SoftwareList: make(map[uint]Software),
ScriptList: make(map[uint]string),
}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
cmd.AppConfig = appConfig
// prepare FilesToWrite entry like Run() would
cmd.FilesToWrite["fleets/test.yml"] = map[string]any{}
// generate software for team 1
res, err := cmd.generateSoftware("fleets/test.yml", 1, "team-a", false)
require.NoError(t, err)
require.NotNil(t, res)
apps, ok := res["app_store_apps"]
require.True(t, ok, "expected app_store_apps in result")
appsList, ok := apps.([]map[string]any)
if !ok {
// yaml/unmarshal may produce []interface{}
rawList, ok := apps.([]any)
require.True(t, ok, "unexpected app_store_apps type")
appsList = make([]map[string]any, 0, len(rawList))
for _, v := range rawList {
m, ok := v.(map[string]any)
require.True(t, ok)
appsList = append(appsList, m)
}
}
var found bool
for _, a := range appsList {
if a["app_store_id"] == "com.example.ios-auto-update" {
found = true
// auto update keys should be present
val, ok := a["auto_update_enabled"]
require.True(t, ok)
assert.Equal(t, true, val)
start, ok := a["auto_update_window_start"]
require.True(t, ok)
assert.Equal(t, "01:00", start)
end, ok := a["auto_update_window_end"]
require.True(t, ok)
assert.Equal(t, "03:00", end)
}
}
require.True(t, found, "did not find iOS auto-update app in generated software")
}
func TestGeneratedOrgSettingsOktaConditionalAccessNotIncluded(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Set Okta conditional access fields (these should NOT appear in GitOps output)
appConfig.ConditionalAccess = &fleet.ConditionalAccessSettings{
MicrosoftEntraTenantID: "test-tenant-id",
MicrosoftEntraConnectionConfigured: true,
OktaIDPID: optjson.SetString("https://okta.example.com/idp"),
OktaAssertionConsumerServiceURL: optjson.SetString("https://okta.example.com/acs"),
OktaAudienceURI: optjson.SetString("https://okta.example.com/audience"),
OktaCertificate: optjson.SetString("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----"),
}
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
// Generate the org settings.
orgSettingsRaw, err := cmd.generateOrgSettings()
require.NoError(t, err)
require.NotNil(t, orgSettingsRaw)
var orgSettings map[string]any
b, err := yamlMarshalRenamed(orgSettingsRaw)
require.NoError(t, err)
err = yaml.Unmarshal(b, &orgSettings)
require.NoError(t, err)
// Verify that conditional_access section does not exist in the output
// (Okta configs are not supported in GitOps)
_, hasConditionalAccess := orgSettings["conditional_access"]
assert.False(t, hasConditionalAccess, "conditional_access section should not be present in GitOps output as Okta configs are not supported")
// Also verify by checking the YAML string directly
yamlStr := string(b)
assert.NotContains(t, yamlStr, "okta_idp_id", "Okta IDP ID should not be in GitOps output")
assert.NotContains(t, yamlStr, "okta_assertion_consumer_service_url", "Okta ACS URL should not be in GitOps output")
assert.NotContains(t, yamlStr, "okta_audience_uri", "Okta Audience URI should not be in GitOps output")
assert.NotContains(t, yamlStr, "okta_certificate", "Okta Certificate should not be in GitOps output")
assert.NotContains(t, yamlStr, "microsoft_entra_tenant_id", "Microsoft Entra Tenant ID should not be in GitOps output")
assert.NotContains(t, yamlStr, "microsoft_entra_connection_configured", "Microsoft Entra connection status should not be in GitOps output")
}
func TestGenerateOrgSettingsInsecure(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.Bool("insecure", true, "Output sensitive information in plaintext.")
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, flagSet, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
// Generate the org settings.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
orgSettingsRaw, err := cmd.generateOrgSettings()
require.NoError(t, err)
require.NotNil(t, orgSettingsRaw)
var orgSettings map[string]interface{}
b, err := yamlMarshalRenamed(orgSettingsRaw)
require.NoError(t, err)
fmt.Println("Org settings raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &orgSettings)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedOrgSettings-insecure.yaml")
require.NoError(t, err)
var expectedAppConfig map[string]interface{}
err = yaml.Unmarshal(b, &expectedAppConfig)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedAppConfig, orgSettings)
}
func TestGenerateTeamSettings(t *testing.T) {
// Get the test team.
fleetClient := &MockClient{}
team, err := fleetClient.GetTeam(1)
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: nil,
}
// Generate the org settings.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
TeamSettingsRaw, err := cmd.generateTeamSettings("team.yml", team)
require.NoError(t, err)
require.NotNil(t, TeamSettingsRaw)
var TeamSettings map[string]interface{}
b, err := yamlMarshalRenamed(TeamSettingsRaw)
require.NoError(t, err)
fmt.Println("Team settings raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &TeamSettings)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamSettings.yaml")
require.NoError(t, err)
var expectedAppConfig map[string]interface{}
err = yaml.Unmarshal(b, &expectedAppConfig)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedAppConfig, TeamSettings)
}
func TestGenerateTeamSettingsInsecure(t *testing.T) {
// Get the test team.
fleetClient := &MockClient{}
team, err := fleetClient.GetTeam(1)
require.NoError(t, err)
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.Bool("insecure", true, "Output sensitive information in plaintext.")
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(&cli.App{}, flagSet, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: nil,
}
// Generate the org settings.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
TeamSettingsRaw, err := cmd.generateTeamSettings("team.yml", team)
require.NoError(t, err)
require.NotNil(t, TeamSettingsRaw)
var TeamSettings map[string]interface{}
b, err := yamlMarshalRenamed(TeamSettingsRaw)
require.NoError(t, err)
fmt.Println("Team settings raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &TeamSettings)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamSettings-insecure.yaml")
require.NoError(t, err)
var expectedAppConfig map[string]interface{}
err = yaml.Unmarshal(b, &expectedAppConfig)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedAppConfig, TeamSettings)
}
// For the purpose of testing macos_setup generation,
// Team 1 has setup experience software with InstallDuringSetup enabled,
// Team 2 has setup experience software with InstallDuringSetup disabled,
// Team 3 has a bootstrap package,
// Team 4 has a setup experience script,
// Team 5 has an Apple MDM enrollment profile.
func TestGenerateControls(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
ScriptList: make(map[uint]string),
}
// Generate global controls.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
mdmConfig := fleet.TeamMDM{
EnableDiskEncryption: appConfig.MDM.EnableDiskEncryption.Value,
MacOSUpdates: appConfig.MDM.MacOSUpdates,
IOSUpdates: appConfig.MDM.IOSUpdates,
IPadOSUpdates: appConfig.MDM.IPadOSUpdates,
WindowsUpdates: appConfig.MDM.WindowsUpdates,
MacOSSetup: appConfig.MDM.MacOSSetup,
}
controlsRaw, err := cmd.generateControls(nil, "", &mdmConfig)
require.NoError(t, err)
require.NotNil(t, controlsRaw)
var controls map[string]interface{}
b, err := yaml.Marshal(controlsRaw)
require.NoError(t, err)
fmt.Println("Controls raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &controls)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedGlobalControls.yaml")
require.NoError(t, err)
var expectedControls map[string]interface{}
err = yaml.Unmarshal(b, &expectedControls)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedControls, controls)
if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-macos-mobileconfig-profile.mobileconfig"]; ok {
require.Equal(t, "<xml>global macos mobileconfig profile</xml>", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-macos-json-profile.json"]; ok {
require.Equal(t, `{"profile": "global macos json profile"}`, fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-windows-profile.xml"]; ok {
require.Equal(t, "<xml>global windows profile</xml>", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/profiles/global-android-profile.json"]; ok {
require.Equal(t, `{"name": "Global Android Profile", "cameraDisabled": true}`, fileContents)
} else {
t.Fatalf("Expected file not found")
}
// Generate controls for no-team, sending in an MDM config with "EndUserAuthentication" disabled.
// Mocks for no team don't return any scripts, bootstrap, software, or profiles,
// so it should _not_ generate a macos_setup config.
mdmConfig = fleet.TeamMDM{
MacOSSetup: fleet.MacOSSetup{
EnableEndUserAuthentication: false,
},
}
controlsRaw, err = cmd.generateControls(ptr.Uint(0), "no_team", &mdmConfig)
require.NoError(t, err)
// Check that the controls do not contain a setup_experience section
_, ok := controlsRaw["setup_experience"]
require.False(t, ok, "Expected no setup_experience section for no-team controls")
// Try that again, but with an MDM config that has "EndUserAuthentication" enabled.
mdmConfig = fleet.TeamMDM{
MacOSSetup: fleet.MacOSSetup{
EnableEndUserAuthentication: true,
},
}
controlsRaw, err = cmd.generateControls(ptr.Uint(0), "no_team", &mdmConfig)
require.NoError(t, err)
// Check that the controls do contain a macos_setup section
verifyControlsHasMacosSetup(t, controlsRaw)
// Generate controls for a team.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
// Note that this team has setup experience software, so we expect a macos_setup section.
controlsRaw, err = cmd.generateControls(ptr.Uint(1), "some_team", nil)
require.NoError(t, err)
require.NotNil(t, controlsRaw)
b, err = yaml.Marshal(controlsRaw)
require.NoError(t, err)
fmt.Println("Controls raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &controls)
require.NoError(t, err)
// Get the expected controls YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamControls.yaml")
require.NoError(t, err)
err = yaml.Unmarshal(b, &expectedControls)
require.NoError(t, err)
if fileContents, ok := cmd.FilesToWrite["lib/some_team/profiles/team-macos-mobileconfig-profile.mobileconfig"]; ok {
require.Equal(t, "<xml>test mobileconfig profile</xml>", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some_team/scripts/Script B.ps1"]; ok {
require.Equal(t, "pop goes the weasel!", fileContents)
} else {
t.Fatalf("Expected file not found")
}
// Compare.
require.Equal(t, expectedControls, controls)
// Generate controls for a team with software but none that installs during setup.
controlsRaw, err = cmd.generateControls(ptr.Uint(2), "some_team", nil)
require.NoError(t, err)
require.NotNil(t, controlsRaw)
_, ok = controlsRaw["setup_experience"]
require.False(t, ok, "Expected no setup_experience section")
// Generate controls for a team with a bootstrap pacakge.
controlsRaw, err = cmd.generateControls(ptr.Uint(3), "some_team", nil)
require.NoError(t, err)
verifyControlsHasMacosSetup(t, controlsRaw)
// Generate controls for a team with a setup experience script.
controlsRaw, err = cmd.generateControls(ptr.Uint(4), "some_team", nil)
require.NoError(t, err)
verifyControlsHasMacosSetup(t, controlsRaw)
// Generate controls for a team with a setup experience profile.
controlsRaw, err = cmd.generateControls(ptr.Uint(5), "some_team", nil)
require.NoError(t, err)
verifyControlsHasMacosSetup(t, controlsRaw)
}
func TestGenerateSoftware(t *testing.T) {
configureFMAManifestServer(t)
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
SoftwareList: make(map[uint]Software),
}
softwareRaw, err := cmd.generateSoftware("team.yml", 1, "some-team", false)
require.NoError(t, err)
require.NotNil(t, softwareRaw)
var software map[string]any
b, err := yaml.Marshal(softwareRaw)
require.NoError(t, err)
fmt.Println("software raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &software)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamSoftware.yaml")
require.NoError(t, err)
var expectedSoftware map[string]any
err = yaml.Unmarshal(b, &expectedSoftware)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedSoftware, software)
if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-software-package-darwin-install"]; ok {
require.Equal(t, "foo", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-software-package-darwin-postinstall"]; ok {
require.Equal(t, "bar", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-software-package-darwin-uninstall"]; ok {
require.Equal(t, "baz", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some-team/queries/my-software-package-darwin-preinstallquery.yml"]; ok {
require.Equal(t, []map[string]any{{
"query": "SELECT * FROM pre_install_query",
}}, fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some-team/scripts/my-fma-darwin-postinstall"]; ok {
require.Equal(t, "postinstall", fileContents)
} else {
t.Fatalf("Expected file not found")
}
if fileContents, ok := cmd.FilesToWrite["lib/some-team/queries/my-fma-darwin-preinstallquery.yml"]; ok {
require.Equal(t, []map[string]any{{
"query": "SELECT * FROM pre_install_query",
}}, fileContents)
} else {
t.Fatalf("Expected file not found")
}
}
// TestGenerateSoftwareScriptPackages tests that script packages (.sh and .ps1)
// do not output install_script, post_install_script, uninstall_script, or
// pre_install_query fields in GitOps YAML, even though these fields may be
// populated internally (the file contents become the install script).
func TestGenerateSoftwareScriptPackages(t *testing.T) {
fleetClient := &MockClientWithScriptPackage{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
SoftwareList: make(map[uint]Software),
}
softwareRaw, err := cmd.generateSoftware("team.yml", 2, "script-team", false)
require.NoError(t, err)
require.NotNil(t, softwareRaw)
var software map[string]interface{}
b, err := yaml.Marshal(softwareRaw)
require.NoError(t, err)
fmt.Println("software with script packages:\n", string(b))
err = yaml.Unmarshal(b, &software)
require.NoError(t, err)
packages, ok := software["packages"].([]interface{})
require.True(t, ok, "packages should be an array")
require.Len(t, packages, 3, "should have 3 packages: 1 regular + 2 scripts (.sh and .ps1)")
// Identify by URL since hash_sha256 includes comment tokens
var shScriptPkg, ps1ScriptPkg, regularPkg map[string]interface{}
for _, pkg := range packages {
p := pkg.(map[string]interface{})
url, ok := p["url"].(string)
if !ok {
continue
}
switch url {
case "https://example.com/download/my-script.sh":
shScriptPkg = p
case "https://example.com/download/setup.ps1":
ps1ScriptPkg = p
case "https://example.com/download/regular-package.deb":
regularPkg = p
}
}
require.NotNil(t, shScriptPkg, ".sh script package should exist")
require.NotNil(t, ps1ScriptPkg, ".ps1 script package should exist")
require.NotNil(t, regularPkg, "regular package should exist")
_, hasInstallScript := shScriptPkg["install_script"]
require.False(t, hasInstallScript, ".sh script package should NOT have install_script in YAML output")
_, hasPostInstallScript := shScriptPkg["post_install_script"]
require.False(t, hasPostInstallScript, ".sh script package should NOT have post_install_script in YAML output")
_, hasUninstallScript := shScriptPkg["uninstall_script"]
require.False(t, hasUninstallScript, ".sh script package should NOT have uninstall_script in YAML output")
_, hasPreInstallQuery := shScriptPkg["pre_install_query"]
require.False(t, hasPreInstallQuery, ".sh script package should NOT have pre_install_query in YAML output")
_, hasInstallScript = ps1ScriptPkg["install_script"]
require.False(t, hasInstallScript, ".ps1 script package should NOT have install_script in YAML output")
_, hasPostInstallScript = ps1ScriptPkg["post_install_script"]
require.False(t, hasPostInstallScript, ".ps1 script package should NOT have post_install_script in YAML output")
_, hasUninstallScript = ps1ScriptPkg["uninstall_script"]
require.False(t, hasUninstallScript, ".ps1 script package should NOT have uninstall_script in YAML output")
_, hasPreInstallQuery = ps1ScriptPkg["pre_install_query"]
require.False(t, hasPreInstallQuery, ".ps1 script package should NOT have pre_install_query in YAML output")
require.Contains(t, shScriptPkg, "url", ".sh script package should have url")
require.Contains(t, shScriptPkg, "hash_sha256", ".sh script package should have hash_sha256")
require.Contains(t, ps1ScriptPkg, "url", ".ps1 script package should have url")
require.Contains(t, ps1ScriptPkg, "hash_sha256", ".ps1 script package should have hash_sha256")
require.Contains(t, regularPkg, "install_script", "regular package should have install_script")
require.Contains(t, regularPkg, "post_install_script", "regular package should have post_install_script")
require.Contains(t, regularPkg, "uninstall_script", "regular package should have uninstall_script")
require.Contains(t, regularPkg, "pre_install_query", "regular package should have pre_install_query")
for filename := range cmd.FilesToWrite {
require.NotContains(t, filename, "my-script-linux-install", "should not write install script file for .sh script package")
require.NotContains(t, filename, "my-script-linux-postinstall", "should not write post-install script file for .sh script package")
require.NotContains(t, filename, "my-script-linux-uninstall", "should not write uninstall script file for .sh script package")
require.NotContains(t, filename, "my-script-linux-preinstallquery", "should not write pre-install query file for .sh script package")
require.NotContains(t, filename, "powershell-script-windows-install", "should not write install script file for .ps1 script package")
require.NotContains(t, filename, "powershell-script-windows-postinstall", "should not write post-install script file for .ps1 script package")
require.NotContains(t, filename, "powershell-script-windows-uninstall", "should not write uninstall script file for .ps1 script package")
require.NotContains(t, filename, "powershell-script-windows-preinstallquery", "should not write pre-install query file for .ps1 script package")
}
}
type MockClientWithScriptPackage struct {
MockClient
}
func (c *MockClientWithScriptPackage) ListSoftwareTitles(query string) ([]fleet.SoftwareTitleListResult, error) {
switch query {
case "available_for_install=1&fleet_id=2":
return []fleet.SoftwareTitleListResult{
{
ID: 3,
Name: "My Script",
HashSHA256: ptr.String("script-package-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "my-script.sh",
Platform: "linux",
Version: "1.0",
},
},
{
ID: 4,
Name: "Regular Package",
HashSHA256: ptr.String("regular-package-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "regular-package.deb",
Platform: "linux",
Version: "2.0",
},
},
{
ID: 5,
Name: "PowerShell Script",
HashSHA256: ptr.String("ps1-script-hash"),
SoftwarePackage: &fleet.SoftwarePackageOrApp{
Name: "setup.ps1",
Platform: "windows",
Version: "1.5",
},
},
}, nil
default:
return c.MockClient.ListSoftwareTitles(query)
}
}
func (c *MockClientWithScriptPackage) GetSoftwareTitleByID(id uint, teamID *uint) (*fleet.SoftwareTitle, error) {
switch id {
case 3:
if *teamID != 2 {
return nil, errors.New("team ID mismatch")
}
// InstallScript is populated internally from file contents, but these fields
// should NOT be output in GitOps YAML
return &fleet.SoftwareTitle{
ID: 3,
SoftwarePackage: &fleet.SoftwareInstaller{
InstallScript: "#!/bin/bash\necho 'This is the script content'",
PostInstallScript: "",
UninstallScript: "",
PreInstallQuery: "",
SelfService: true,
Platform: "linux",
URL: "https://example.com/download/my-script.sh",
Name: "my-script.sh",
},
}, nil
case 4:
if *teamID != 2 {
return nil, errors.New("team ID mismatch")
}
return &fleet.SoftwareTitle{
ID: 4,
SoftwarePackage: &fleet.SoftwareInstaller{
InstallScript: "install script content",
PostInstallScript: "post-install script content",
UninstallScript: "uninstall script content",
PreInstallQuery: "SELECT 1",
SelfService: false,
Platform: "linux",
URL: "https://example.com/download/regular-package.deb",
Name: "regular-package.deb",
},
}, nil
case 5:
if *teamID != 2 {
return nil, errors.New("team ID mismatch")
}
// InstallScript is populated internally from file contents, but these fields
// should NOT be output in GitOps YAML
return &fleet.SoftwareTitle{
ID: 5,
SoftwarePackage: &fleet.SoftwareInstaller{
InstallScript: "Write-Host 'This is the PowerShell script content'",
PostInstallScript: "",
UninstallScript: "",
PreInstallQuery: "",
SelfService: true,
Platform: "windows",
URL: "https://example.com/download/setup.ps1",
Name: "setup.ps1",
},
}, nil
default:
return c.MockClient.GetSoftwareTitleByID(id, teamID)
}
}
func (c *MockClientWithScriptPackage) GetSetupExperienceSoftware(platform string, teamID uint) ([]fleet.SoftwareTitleListResult, error) {
if teamID == 2 {
return []fleet.SoftwareTitleListResult{}, nil
}
return c.MockClient.GetSetupExperienceSoftware(platform, teamID)
}
func TestGeneratePolicies(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]any),
AppConfig: appConfig,
SoftwareList: map[uint]Software{
1: {
Hash: "team-software-hash",
Comment: "__TEAM_SOFTWARE_COMMENT_TOKEN__",
},
},
ScriptList: map[uint]string{
1: "/path/to/script1.sh",
},
}
policiesRaw, err := cmd.generatePolicies(nil, "default.yml", nil)
require.NoError(t, err)
require.NotNil(t, policiesRaw)
var generatedPolicies []map[string]any
b, err := yaml.Marshal(policiesRaw)
require.NoError(t, err)
fmt.Println("generated global policies raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &generatedPolicies)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedGlobalPolicies.yaml")
require.NoError(t, err)
var expectedPolicies []map[string]any
err = yaml.Unmarshal(b, &expectedPolicies)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedPolicies, generatedPolicies)
// Generate policies for a team.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
var expectedTeamPolicies []map[string]any
var generatedTeamPolicies []map[string]any
policiesRaw, err = cmd.generatePolicies(ptr.Uint(1), "some_team", nil)
require.NoError(t, err)
require.NotNil(t, policiesRaw)
b, err = yaml.Marshal(policiesRaw)
require.NoError(t, err)
fmt.Println("generated team policies raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &generatedTeamPolicies)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamPolicies.yaml")
require.NoError(t, err)
err = yaml.Unmarshal(b, &expectedTeamPolicies)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedPolicies, generatedPolicies)
}
func TestGenerateQueries(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
queriesRaw, err := cmd.generateQueries(nil)
require.NoError(t, err)
require.NotNil(t, queriesRaw)
var queries []map[string]interface{}
b, err := yaml.Marshal(queriesRaw)
require.NoError(t, err)
fmt.Println("queries raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &queries)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedGlobalReports.yaml")
require.NoError(t, err)
var expectedQueries []map[string]interface{}
err = yaml.Unmarshal(b, &expectedQueries)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedQueries, queries)
// Generate queries for a team.
// Note that nested keys here may be strings,
// so we'll JSON marshal and unmarshal to a map for comparison.
queriesRaw, err = cmd.generateQueries(ptr.Uint(1))
require.NoError(t, err)
require.NotNil(t, queriesRaw)
b, err = yaml.Marshal(queriesRaw)
require.NoError(t, err)
fmt.Println("queries raw:\n", string(b)) // Debugging line
err = yaml.Unmarshal(b, &queries)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedTeamReports.yaml")
require.NoError(t, err)
err = yaml.Unmarshal(b, &expectedQueries)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedQueries, queries)
}
func TestGenerateLabels(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
}
labelsRaw, err := cmd.generateLabels(nil)
require.NoError(t, err)
require.NotNil(t, labelsRaw)
var labels []map[string]interface{}
b, err := yaml.Marshal(labelsRaw)
require.NoError(t, err)
err = yaml.Unmarshal(b, &labels)
require.NoError(t, err)
// Get the expected org settings YAML.
b, err = os.ReadFile("./testdata/generateGitops/expectedLabels.yaml")
require.NoError(t, err)
var expectedLabels []map[string]any
err = yaml.Unmarshal(b, &expectedLabels)
require.NoError(t, err)
// Compare.
require.Equal(t, expectedLabels, labels)
teamLabels, err := cmd.generateLabels(&fleet.Team{ID: 1})
require.NoError(t, err)
require.Empty(t, teamLabels)
}
func verifyControlsHasMacosSetup(t *testing.T, controlsRaw map[string]interface{}) {
macosSetup, ok := controlsRaw["setup_experience"].(string)
require.True(t, ok, "Expected setup_experience section to be a string")
require.Equal(t, macosSetup, "TODO: update with your setup_experience configuration")
}
func TestGenerateControlsAndMDMWithoutMDMEnabledAndConfigured(t *testing.T) {
// Get the test app config.
fleetClient := &MockClient{}
appConfig, err := fleetClient.GetAppConfig()
require.NoError(t, err)
appConfig.MDM.EnabledAndConfigured = false
appConfig.MDM.WindowsEnabledAndConfigured = false
appConfig.MDM.AppleBusinessManager = optjson.Slice[fleet.MDMAppleABMAssignmentInfo]{}
appConfig.MDM.AppleServerURL = ""
appConfig.MDM.EndUserAuthentication = fleet.MDMEndUserAuthentication{}
appConfig.MDM.VolumePurchasingProgram = optjson.Slice[fleet.MDMAppleVolumePurchasingProgramInfo]{}
// Create the command.
cmd := &GenerateGitopsCommand{
Client: fleetClient,
CLI: cli.NewContext(cli.NewApp(), nil, nil),
Messages: Messages{},
FilesToWrite: make(map[string]interface{}),
AppConfig: appConfig,
ScriptList: make(map[uint]string),
}
// teamId=6 is unhandled for MDM APIs to make sure the APIs to get MDM stuff
// aren't called if MDM is not enabled and configured.
controlsRaw, err := cmd.generateControls(ptr.Uint(6), "some_team", nil)
require.NoError(t, err)
require.Contains(t, controlsRaw, "scripts") // Uploading scripts does not require MDM turned on.
require.Empty(t, controlsRaw["scripts"])
// teamId=6 is unhandled for MDM APIs to make sure the APIs to get MDM stuff
// aren't called if MDM is not enabled and configured.
mdmRaw, err := cmd.generateMDM(&appConfig.MDM)
require.NoError(t, err)
// Verify all keys are set to empty.
for _, key := range []string{
"apple_business_manager",
"apple_server_url",
"end_user_authentication",
"end_user_license_agreement",
"volume_purchasing_program",
} {
require.Contains(t, mdmRaw, key)
require.Empty(t, mdmRaw[key])
}
}
func TestSillyTeamNames(t *testing.T) {
configureFMAManifestServer(t)
sillyTeamNames := map[string]string{
"🫆": "🫆.yml",
"🪾": "🪾.yml",
"🫜": "🫜.yml",
"🪉": "🪉.yml",
"🪏": "🪏.yml",
"🫟": "🫟.yml",
"👍": "👍.yml",
"a/team\\with:all*the?footguns\"in<it>omg|": "aateambwithcalldtheefootgunsfingithomgi.yml",
}
fleetClient := &MockClient{}
tempDir := os.TempDir() + "/" + uuid.New().String()
t.Cleanup(func() {
if err := os.RemoveAll(tempDir); err != nil {
t.Fatalf("failed to remove temp dir: %v", err)
}
})
for name, expectedFilename := range sillyTeamNames {
t.Run(name, func(t *testing.T) {
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
flagSet.String("dir", tempDir, "")
flagSet.Bool("force", true, "")
fleetClient.TeamNameOverride = name
action := createGenerateGitopsAction(fleetClient)
buf := new(bytes.Buffer)
cliContext := cli.NewContext(&cli.App{
Name: "test",
Usage: "test",
Writer: buf,
ErrWriter: buf,
}, flagSet, nil)
// Get the test app config.
err := action(cliContext)
require.NoError(t, err, buf.String())
// Expect a correctly-named .yaml
tgtPath := filepath.Join(tempDir, "fleets", expectedFilename)
_, err = os.Stat(tgtPath)
require.NoError(t, err)
})
}
}
func TestReplaceAliasKeys(t *testing.T) {
rules := map[string]string{
"old_key": "new_key",
"nested_old": "nested_new",
}
t.Run("deleteOld=true removes old keys", func(t *testing.T) {
data := map[string]any{
"old_key": "value1",
"other_key": "value2",
}
replaceAliasKeys(data, rules, true)
require.Equal(t, "value1", data["new_key"])
_, exists := data["old_key"]
require.False(t, exists, "old key should be removed when deleteOld=true")
require.Equal(t, "value2", data["other_key"])
})
t.Run("deleteOld=false keeps both keys", func(t *testing.T) {
data := map[string]any{
"old_key": "value1",
"other_key": "value2",
}
replaceAliasKeys(data, rules, false)
require.Equal(t, "value1", data["new_key"])
require.Equal(t, "value1", data["old_key"])
require.Equal(t, "value2", data["other_key"])
})
t.Run("nested maps and arrays", func(t *testing.T) {
data := map[string]any{
"outer": map[string]any{
"nested_old": "nested_value",
},
"list": []any{
map[string]any{"old_key": "in_array"},
},
}
// deleteOld=true: old keys removed recursively
replaceAliasKeys(data, rules, true)
outer := data["outer"].(map[string]any)
require.Equal(t, "nested_value", outer["nested_new"])
_, exists := outer["nested_old"]
require.False(t, exists)
item := data["list"].([]any)[0].(map[string]any)
require.Equal(t, "in_array", item["new_key"])
_, exists = item["old_key"]
require.False(t, exists)
})
t.Run("nested maps and arrays deleteOld=false", func(t *testing.T) {
data := map[string]any{
"outer": map[string]any{
"nested_old": "nested_value",
},
"list": []any{
map[string]any{"old_key": "in_array"},
},
}
replaceAliasKeys(data, rules, false)
outer := data["outer"].(map[string]any)
require.Equal(t, "nested_value", outer["nested_new"])
require.Equal(t, "nested_value", outer["nested_old"])
item := data["list"].([]any)[0].(map[string]any)
require.Equal(t, "in_array", item["new_key"])
require.Equal(t, "in_array", item["old_key"])
})
t.Run("nil map is safe", func(t *testing.T) {
replaceAliasKeys(nil, rules, true)
replaceAliasKeys(nil, rules, false)
var m map[string]any
replaceAliasKeys(m, rules, true)
})
}