mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Implements patch policies #31914 - https://github.com/fleetdm/fleet/pull/40816 - https://github.com/fleetdm/fleet/pull/41248 - https://github.com/fleetdm/fleet/pull/41276 - https://github.com/fleetdm/fleet/pull/40948 - https://github.com/fleetdm/fleet/pull/40837 - https://github.com/fleetdm/fleet/pull/40956 - https://github.com/fleetdm/fleet/pull/41168 - https://github.com/fleetdm/fleet/pull/41171 - https://github.com/fleetdm/fleet/pull/40691 - https://github.com/fleetdm/fleet/pull/41524 - https://github.com/fleetdm/fleet/pull/41674 --------- Co-authored-by: Jonathan Katz <44128041+jkatz01@users.noreply.github.com> Co-authored-by: jkatz01 <yehonatankatz@gmail.com> Co-authored-by: RachelElysia <71795832+RachelElysia@users.noreply.github.com> Co-authored-by: Jahziel Villasana-Espinoza <jahziel@fleetdm.com>
2064 lines
65 KiB
Go
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)
|
|
})
|
|
}
|