mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
For #27476 # Checklist for submitter - [X] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [X] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) # Details This PR adds a new command `generate-gitops` to the `fleetctl` tool. The purpose of this command is to output GitOps-ready files that can then be used with `fleetctl-gitops`. The general usage of the command is: ``` fleectl generate-gitops --dir /path/to/dir/to/add/files/to ``` By default, the outputted files will not contain sensitive data, but will instead add comments where the data needs to be replaced by a user. In cases where sensitive data is redacted, the tool outputs warnings to the user indicating which keys need to be updated. The tool uses existing APIs to gather data for use in generating configuration files. In some cases new API client methods needed to be added to support the tool: * ListConfigurationProfiles * GetProfileContents * GetScriptContents * GetSoftwareTitleByID Additionally, the response for the /api/latest/fleet/software/batch endpoint was updated slightly to return `HashSHA256` for the software installers. This allows policies that automatically install software to refer to that software by hash. Other options that we may or may not choose to document at this time: * `--insecure`: outputs sensitive data in plaintext instead of leaving comments * `--print`: prints the output to stdout instead of writing files * `--key`: outputs the value at a keypath to stdout, e.g. `--key agent_options.config` * `--team`: only generates config for the specified team name * `--force`: overwrites files in the given directory (defaults to false, which errors if the dir is not empty) # Technical notes The command is implemented using a `GenerateGitopsCommand` type which holds some state (like a list of software and scripts encountered) as well as a Fleet client instance (which may be a mock instance for tests) and the CLI context (containing things like flags and output writers). The actual "action" of the CLI command calls the `Run()` method of the `GenerateGitopsCommand` var, which delegates most of the work to other methods like `generateOrgSettings()`, `generateControls()`, etc. Wherever possible, the subroutines use reflection to translate Go struct fields into JSON property names. This guarantees that the correct keys are written to config files, and protects against the unlikely event of keys changing. When sensitive data is encountered, the subroutines call `AddComment()` to get a new token to add to the config files. These tokens are replaced with comments like `# TODO - Add your enrollment secrets here` in the final output. # Known issues / TODOs: * The `macos_setup` configuration is not output by this tool yet. More planning is required for this. In the meantime, if the tool detects that `macos_setup` is configured on the server, it outputs a key with an invalid value and prints a warning to the user that they'll need to configure it themselves. * `yara_rules` are not output yet. The tool adds a warning that if you have Yara rules (which you can only upload via GitOps right now) that you'll have to migrate them manually. Supporting this will require a new API that we'll have to discuss the authz for, so punting on it for now. * Fleet maintained apps are not supported by GitOps yet (coming in https://github.com/fleetdm/fleet/issues/24469). In the meantime, this tool will output a `fleet_maintained_apps` key and trigger a warning, and GitOps will fail if that key is present. --------- Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com> Co-authored-by: Noah Talerman <47070608+noahtalerman@users.noreply.github.com>
1184 lines
40 KiB
Go
1184 lines
40 KiB
Go
package spec
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/fleetdm/fleet/v4/pkg/file"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var topLevelOptions = map[string]string{
|
|
"controls": "controls:",
|
|
"queries": "queries:",
|
|
"policies": "policies:",
|
|
"agent_options": "agent_options:",
|
|
"org_settings": `
|
|
org_settings:
|
|
server_settings:
|
|
server_url: https://fleet.example.com
|
|
org_info:
|
|
contact_url: https://example.com/contact
|
|
org_logo_url: ""
|
|
org_logo_url_light_background: ""
|
|
org_name: Test Org
|
|
secrets:
|
|
`,
|
|
}
|
|
|
|
var teamLevelOptions = map[string]string{
|
|
"controls": "controls:",
|
|
"queries": "queries:",
|
|
"policies": "policies:",
|
|
"agent_options": "agent_options:",
|
|
"name": "name: TeamName",
|
|
"team_settings": `
|
|
team_settings:
|
|
secrets:
|
|
`,
|
|
}
|
|
|
|
func createTempFile(t *testing.T, pattern, contents string) (filePath string, baseDir string) {
|
|
tmpFile, err := os.CreateTemp(t.TempDir(), pattern)
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString(contents)
|
|
require.NoError(t, err)
|
|
require.NoError(t, tmpFile.Close())
|
|
return tmpFile.Name(), filepath.Dir(tmpFile.Name())
|
|
}
|
|
|
|
func createNamedFileOnTempDir(t *testing.T, name string, contents string) (filePath string, baseDir string) {
|
|
tmpFilePath := filepath.Join(t.TempDir(), name)
|
|
tmpFile, err := os.Create(tmpFilePath)
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString(contents)
|
|
require.NoError(t, err)
|
|
require.NoError(t, tmpFile.Close())
|
|
return tmpFile.Name(), filepath.Dir(tmpFile.Name())
|
|
}
|
|
|
|
func gitOpsFromString(t *testing.T, s string) (*GitOps, error) {
|
|
path, basePath := createTempFile(t, "", s)
|
|
return GitOpsFromFile(path, basePath, nil, nopLogf)
|
|
}
|
|
|
|
func nopLogf(_ string, _ ...interface{}) {
|
|
}
|
|
|
|
func TestValidGitOpsYaml(t *testing.T) {
|
|
t.Parallel()
|
|
tests := map[string]struct {
|
|
environment map[string]string
|
|
filePath string
|
|
isTeam bool
|
|
}{
|
|
"global_config_no_paths": {
|
|
environment: map[string]string{
|
|
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
|
|
"FLEET_SECRET_NAME": "secret_name",
|
|
"FLEET_SECRET_length": "10",
|
|
"FLEET_SECRET_BANANA": "bread",
|
|
},
|
|
filePath: "testdata/global_config_no_paths.yml",
|
|
},
|
|
"global_config_with_paths": {
|
|
environment: map[string]string{
|
|
"LINUX_OS": "linux",
|
|
"DISTRIBUTED_DENYLIST_DURATION": "0",
|
|
"ORG_NAME": "Fleet Device Management",
|
|
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
|
|
"FLEET_SECRET_NAME": "secret_name",
|
|
"FLEET_SECRET_length": "10",
|
|
"FLEET_SECRET_BANANA": "bread",
|
|
},
|
|
filePath: "testdata/global_config.yml",
|
|
},
|
|
"team_config_no_paths": {
|
|
environment: map[string]string{
|
|
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
|
|
"FLEET_SECRET_NAME": "secret_name",
|
|
"FLEET_SECRET_length": "10",
|
|
"FLEET_SECRET_BANANA": "bread",
|
|
},
|
|
filePath: "testdata/team_config_no_paths.yml",
|
|
isTeam: true,
|
|
},
|
|
"team_config_with_paths": {
|
|
environment: map[string]string{
|
|
"POLICY": "policy",
|
|
"LINUX_OS": "linux",
|
|
"DISTRIBUTED_DENYLIST_DURATION": "0",
|
|
"ENABLE_FAILING_POLICIES_WEBHOOK": "true",
|
|
"FLEET_SECRET_FLEET_SECRET_": "fleet_secret",
|
|
"FLEET_SECRET_NAME": "secret_name",
|
|
"FLEET_SECRET_length": "10",
|
|
"FLEET_SECRET_BANANA": "bread",
|
|
},
|
|
filePath: "testdata/team_config.yml",
|
|
isTeam: true,
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
test := test
|
|
name := name
|
|
t.Run(
|
|
name, func(t *testing.T) {
|
|
if len(test.environment) > 0 {
|
|
for k, v := range test.environment {
|
|
os.Setenv(k, v)
|
|
}
|
|
t.Cleanup(func() {
|
|
for k := range test.environment {
|
|
os.Unsetenv(k)
|
|
}
|
|
})
|
|
}
|
|
|
|
var appConfig *fleet.EnrichedAppConfig
|
|
if test.isTeam {
|
|
appConfig = &fleet.EnrichedAppConfig{}
|
|
appConfig.License = &fleet.LicenseInfo{
|
|
Tier: fleet.TierPremium,
|
|
}
|
|
}
|
|
|
|
gitops, err := GitOpsFromFile(test.filePath, "./testdata", appConfig, nopLogf)
|
|
require.NoError(t, err)
|
|
|
|
if test.isTeam {
|
|
// Check team settings
|
|
assert.Equal(t, "Team1", *gitops.TeamName)
|
|
assert.Contains(t, gitops.TeamSettings, "webhook_settings")
|
|
webhookSettings, ok := gitops.TeamSettings["webhook_settings"].(map[string]interface{})
|
|
assert.True(t, ok, "webhook_settings not found")
|
|
assert.Contains(t, webhookSettings, "failing_policies_webhook")
|
|
failingPoliciesWebhook, ok := webhookSettings["failing_policies_webhook"].(map[string]interface{})
|
|
assert.True(t, ok, "webhook_settings not found")
|
|
assert.Contains(t, failingPoliciesWebhook, "enable_failing_policies_webhook")
|
|
enableFailingPoliciesWebhook, ok := failingPoliciesWebhook["enable_failing_policies_webhook"].(bool)
|
|
assert.True(t, ok)
|
|
assert.True(t, enableFailingPoliciesWebhook)
|
|
assert.Contains(t, gitops.TeamSettings, "host_expiry_settings")
|
|
assert.Contains(t, gitops.TeamSettings, "features")
|
|
assert.Contains(t, gitops.TeamSettings, "secrets")
|
|
secrets, ok := gitops.TeamSettings["secrets"]
|
|
assert.True(t, ok, "secrets not found")
|
|
require.Len(t, secrets.([]*fleet.EnrollSecret), 2)
|
|
assert.Equal(t, "SampleSecret123", secrets.([]*fleet.EnrollSecret)[0].Secret)
|
|
assert.Equal(t, "ABC", secrets.([]*fleet.EnrollSecret)[1].Secret)
|
|
require.Len(t, gitops.Software.Packages, 2)
|
|
for _, pkg := range gitops.Software.Packages {
|
|
if strings.Contains(pkg.URL, "MicrosoftTeams") {
|
|
assert.Equal(t, "testdata/lib/uninstall.sh", pkg.UninstallScript.Path)
|
|
} else {
|
|
assert.Empty(t, pkg.UninstallScript.Path)
|
|
}
|
|
}
|
|
} else {
|
|
// Check org settings
|
|
serverSettings, ok := gitops.OrgSettings["server_settings"]
|
|
assert.True(t, ok, "server_settings not found")
|
|
assert.Equal(t, "https://fleet.example.com", serverSettings.(map[string]interface{})["server_url"])
|
|
assert.EqualValues(t, 2000, serverSettings.(map[string]interface{})["query_report_cap"])
|
|
assert.Contains(t, gitops.OrgSettings, "org_info")
|
|
orgInfo, ok := gitops.OrgSettings["org_info"].(map[string]interface{})
|
|
assert.True(t, ok)
|
|
assert.Equal(t, "Fleet Device Management", orgInfo["org_name"])
|
|
assert.Contains(t, gitops.OrgSettings, "smtp_settings")
|
|
assert.Contains(t, gitops.OrgSettings, "sso_settings")
|
|
assert.Contains(t, gitops.OrgSettings, "integrations")
|
|
assert.Contains(t, gitops.OrgSettings, "mdm")
|
|
assert.Contains(t, gitops.OrgSettings, "webhook_settings")
|
|
assert.Contains(t, gitops.OrgSettings, "fleet_desktop")
|
|
assert.Contains(t, gitops.OrgSettings, "host_expiry_settings")
|
|
assert.Contains(t, gitops.OrgSettings, "activity_expiry_settings")
|
|
assert.Contains(t, gitops.OrgSettings, "features")
|
|
assert.Contains(t, gitops.OrgSettings, "vulnerability_settings")
|
|
assert.Contains(t, gitops.OrgSettings, "secrets")
|
|
secrets, ok := gitops.OrgSettings["secrets"]
|
|
assert.True(t, ok, "secrets not found")
|
|
require.Len(t, secrets.([]*fleet.EnrollSecret), 2)
|
|
assert.Equal(t, "SampleSecret123", secrets.([]*fleet.EnrollSecret)[0].Secret)
|
|
assert.Equal(t, "ABC", secrets.([]*fleet.EnrollSecret)[1].Secret)
|
|
activityExpirySettings, ok := gitops.OrgSettings["activity_expiry_settings"].(map[string]interface{})
|
|
require.True(t, ok)
|
|
activityExpiryEnabled, ok := activityExpirySettings["activity_expiry_enabled"].(bool)
|
|
require.True(t, ok)
|
|
require.True(t, activityExpiryEnabled)
|
|
activityExpiryWindow, ok := activityExpirySettings["activity_expiry_window"].(float64)
|
|
require.True(t, ok)
|
|
require.Equal(t, 30, int(activityExpiryWindow))
|
|
|
|
// Check labels
|
|
require.Len(t, gitops.Labels, 2)
|
|
assert.Equal(t, "Global label numero uno", gitops.Labels[0].Name)
|
|
assert.Equal(t, "Global label numero dos", gitops.Labels[1].Name)
|
|
assert.Equal(t, "SELECT 1 FROM osquery_info", gitops.Labels[0].Query)
|
|
require.Len(t, gitops.Labels[1].Hosts, 2)
|
|
assert.Equal(t, "host1", gitops.Labels[1].Hosts[0])
|
|
assert.Equal(t, "host2", gitops.Labels[1].Hosts[1])
|
|
|
|
}
|
|
|
|
// Check controls
|
|
_, ok := gitops.Controls.MacOSSettings.(fleet.MacOSSettings)
|
|
assert.True(t, ok, "macos_settings not found")
|
|
_, ok = gitops.Controls.WindowsSettings.(fleet.WindowsSettings)
|
|
assert.True(t, ok, "windows_settings not found")
|
|
_, ok = gitops.Controls.EnableDiskEncryption.(bool)
|
|
assert.True(t, ok, "enable_disk_encryption not found")
|
|
_, ok = gitops.Controls.MacOSMigration.(map[string]interface{})
|
|
assert.True(t, ok, "macos_migration not found")
|
|
assert.NotNil(t, gitops.Controls.MacOSSetup, "macos_setup not found")
|
|
_, ok = gitops.Controls.MacOSUpdates.(map[string]interface{})
|
|
assert.True(t, ok, "macos_updates not found")
|
|
_, ok = gitops.Controls.IOSUpdates.(map[string]interface{})
|
|
assert.True(t, ok, "ios_updates not found")
|
|
_, ok = gitops.Controls.IPadOSUpdates.(map[string]interface{})
|
|
assert.True(t, ok, "ipados_updates not found")
|
|
_, ok = gitops.Controls.WindowsEnabledAndConfigured.(bool)
|
|
assert.True(t, ok, "windows_enabled_and_configured not found")
|
|
_, ok = gitops.Controls.WindowsMigrationEnabled.(bool)
|
|
assert.True(t, ok, "windows_migration_enabled not found")
|
|
_, ok = gitops.Controls.WindowsUpdates.(map[string]interface{})
|
|
assert.True(t, ok, "windows_updates not found")
|
|
require.Len(t, gitops.FleetSecrets, 4)
|
|
assert.Equal(t, "fleet_secret", gitops.FleetSecrets["FLEET_SECRET_FLEET_SECRET_"])
|
|
assert.Equal(t, "secret_name", gitops.FleetSecrets["FLEET_SECRET_NAME"])
|
|
assert.Equal(t, "10", gitops.FleetSecrets["FLEET_SECRET_length"])
|
|
assert.Equal(t, "bread", gitops.FleetSecrets["FLEET_SECRET_BANANA"])
|
|
|
|
// Check agent options
|
|
assert.NotNil(t, gitops.AgentOptions)
|
|
assert.Contains(t, string(*gitops.AgentOptions), "\"distributed_denylist_duration\":0")
|
|
|
|
// Check queries
|
|
require.Len(t, gitops.Queries, 3)
|
|
assert.Equal(t, "Scheduled query stats", gitops.Queries[0].Name)
|
|
assert.Equal(t, "orbit_info", gitops.Queries[1].Name)
|
|
assert.Equal(t, "darwin,linux,windows", gitops.Queries[1].Platform)
|
|
assert.Equal(t, "osquery_info", gitops.Queries[2].Name)
|
|
|
|
// Check software
|
|
if test.isTeam {
|
|
require.Len(t, gitops.Software.Packages, 2)
|
|
require.Equal(t, "https://statics.teams.cdn.office.net/production-osx/enterprise/webview2/lkg/MicrosoftTeams.pkg", gitops.Software.Packages[0].URL)
|
|
require.Equal(t, "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", gitops.Software.Packages[0].SHA256)
|
|
require.False(t, gitops.Software.Packages[0].SelfService)
|
|
require.Equal(t, "https://ftp.mozilla.org/pub/firefox/releases/129.0.2/mac/en-US/Firefox%20129.0.2.pkg", gitops.Software.Packages[1].URL)
|
|
require.True(t, gitops.Software.Packages[1].SelfService)
|
|
|
|
require.Len(t, gitops.Software.AppStoreApps, 1)
|
|
require.Equal(t, gitops.Software.AppStoreApps[0].AppStoreID, "123456")
|
|
require.False(t, gitops.Software.AppStoreApps[0].SelfService)
|
|
}
|
|
|
|
// Check policies
|
|
expectedPoliciesCount := 5
|
|
if test.isTeam {
|
|
expectedPoliciesCount = 9
|
|
}
|
|
require.Len(t, gitops.Policies, expectedPoliciesCount)
|
|
assert.Equal(t, "😊 Failing policy", gitops.Policies[0].Name)
|
|
assert.Equal(t, "Passing policy", gitops.Policies[1].Name)
|
|
assert.Equal(t, "No root logins (macOS, Linux)", gitops.Policies[2].Name)
|
|
assert.Equal(t, "🔥 Failing policy", gitops.Policies[3].Name)
|
|
assert.Equal(t, "linux", gitops.Policies[3].Platform)
|
|
assert.Equal(t, "😊😊 Failing policy", gitops.Policies[4].Name)
|
|
if test.isTeam {
|
|
assert.Equal(t, "Microsoft Teams on macOS installed and up to date", gitops.Policies[5].Name)
|
|
assert.NotNil(t, gitops.Policies[5].InstallSoftware)
|
|
assert.Equal(t, "./microsoft-teams.pkg.software.yml", gitops.Policies[5].InstallSoftware.PackagePath)
|
|
|
|
assert.Equal(t, "Slack on macOS is installed", gitops.Policies[6].Name)
|
|
assert.NotNil(t, gitops.Policies[6].InstallSoftware)
|
|
assert.Equal(t, "123456", gitops.Policies[6].InstallSoftware.AppStoreID)
|
|
|
|
assert.Equal(t, "Script run policy", gitops.Policies[7].Name)
|
|
assert.NotNil(t, gitops.Policies[7].RunScript)
|
|
assert.Equal(t, "./lib/collect-fleetd-logs.sh", gitops.Policies[7].RunScript.Path)
|
|
|
|
assert.Equal(t, "🔥 Failing policy with script", gitops.Policies[8].Name)
|
|
assert.NotNil(t, gitops.Policies[8].RunScript)
|
|
// . or .. depending on whether with paths or without
|
|
assert.Contains(t, gitops.Policies[8].RunScript.Path, "./lib/collect-fleetd-logs.sh")
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestDuplicatePolicyNames(t *testing.T) {
|
|
t.Parallel()
|
|
config := getGlobalConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: My policy
|
|
platform: linux
|
|
query: SELECT 1 FROM osquery_info WHERE start_time < 0;
|
|
- name: My policy
|
|
platform: windows
|
|
query: SELECT 1;
|
|
`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "duplicate policy names")
|
|
}
|
|
|
|
func TestDuplicateQueryNames(t *testing.T) {
|
|
t.Parallel()
|
|
config := getGlobalConfig([]string{"queries"})
|
|
config += `
|
|
queries:
|
|
- name: orbit_info
|
|
query: SELECT * from orbit_info;
|
|
interval: 0
|
|
platform: darwin,linux,windows
|
|
min_osquery_version: all
|
|
observer_can_run: false
|
|
automations_enabled: true
|
|
logging: snapshot
|
|
- name: orbit_info
|
|
query: SELECT 1;
|
|
interval: 300
|
|
platform: windows
|
|
min_osquery_version: all
|
|
observer_can_run: false
|
|
automations_enabled: true
|
|
logging: snapshot
|
|
`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "duplicate query names")
|
|
}
|
|
|
|
func TestUnicodeQueryNames(t *testing.T) {
|
|
t.Parallel()
|
|
config := getGlobalConfig([]string{"queries"})
|
|
config += `
|
|
queries:
|
|
- name: 😊 orbit_info
|
|
query: SELECT * from orbit_info;
|
|
interval: 0
|
|
platform: darwin,linux,windows
|
|
min_osquery_version: all
|
|
observer_can_run: false
|
|
automations_enabled: true
|
|
logging: snapshot
|
|
`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "query name must be in ASCII")
|
|
}
|
|
|
|
func TestUnicodeTeamName(t *testing.T) {
|
|
t.Parallel()
|
|
config := getTeamConfig([]string{"name"})
|
|
config += `name: 😊 TeamName`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.NoError(t, err)
|
|
}
|
|
|
|
func TestVarExpansion(t *testing.T) {
|
|
os.Setenv("MACOS_OS", "darwin")
|
|
os.Setenv("LINUX_OS", "linux")
|
|
os.Setenv("EMPTY_VAR", "")
|
|
t.Cleanup(func() {
|
|
os.Unsetenv("MACOS_OS")
|
|
os.Unsetenv("LINUX_OS")
|
|
os.Unsetenv("EMPTY_VAR")
|
|
})
|
|
config := getGlobalConfig([]string{"queries"})
|
|
config += `
|
|
queries:
|
|
- name: orbit_info \$NOT_EXPANDED \\\$ALSO_NOT_EXPANDED
|
|
query: "SELECT * from orbit_info; -- double quotes are escaped by YAML after Fleet's escaping of backslashes \\\\\$NOT_EXPANDED"
|
|
interval: 0
|
|
platform: $MACOS_OS,${LINUX_OS},windows$EMPTY_VAR
|
|
min_osquery_version: all
|
|
observer_can_run: false
|
|
automations_enabled: true
|
|
logging: snapshot
|
|
description: 'single quotes are not escaped by YAML \\\$NOT_EXPANDED'
|
|
`
|
|
gitOps, err := gitOpsFromString(t, config)
|
|
require.NoError(t, err)
|
|
require.Len(t, gitOps.Queries, 1)
|
|
require.Equal(t, "darwin,linux,windows", gitOps.Queries[0].Platform)
|
|
require.Equal(t, `orbit_info $NOT_EXPANDED \$ALSO_NOT_EXPANDED`, gitOps.Queries[0].Name)
|
|
require.Equal(t, `single quotes are not escaped by YAML \$NOT_EXPANDED`, gitOps.Queries[0].Description)
|
|
require.Equal(t, `SELECT * from orbit_info; -- double quotes are escaped by YAML after Fleet's escaping of backslashes \$NOT_EXPANDED`, gitOps.Queries[0].Query)
|
|
|
|
config = getGlobalConfig([]string{"queries"})
|
|
config += `
|
|
queries:
|
|
- name: orbit_info $NOT_DEFINED
|
|
query: SELECT * from orbit_info;
|
|
interval: 0
|
|
platform: darwin,linux,windows
|
|
min_osquery_version: all
|
|
observer_can_run: false
|
|
automations_enabled: true
|
|
logging: snapshot
|
|
`
|
|
_, err = gitOpsFromString(t, config)
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "environment variable \"NOT_DEFINED\" not set")
|
|
}
|
|
|
|
func TestMixingGlobalAndTeamConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Mixing org_settings and team name
|
|
config := getGlobalConfig(nil)
|
|
config += "name: TeamName\n"
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "'org_settings' cannot be used with 'name', 'team_settings'")
|
|
|
|
// Mixing org_settings and team_settings
|
|
config = getGlobalConfig(nil)
|
|
config += "team_settings:\n secrets: []\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "'org_settings' cannot be used with 'name', 'team_settings'")
|
|
|
|
// Mixing org_settings and team name and team_settings
|
|
config = getGlobalConfig(nil)
|
|
config += "name: TeamName\n"
|
|
config += "team_settings:\n secrets: []\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "'org_settings' cannot be used with 'name', 'team_settings'")
|
|
}
|
|
|
|
func TestInvalidGitOpsYaml(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Bad YAML
|
|
_, err := gitOpsFromString(t, "bad:\nbad")
|
|
assert.ErrorContains(t, err, "failed to unmarshal")
|
|
|
|
for _, name := range []string{"global", "team"} {
|
|
t.Run(
|
|
name, func(t *testing.T) {
|
|
isTeam := name == "team"
|
|
getConfig := getGlobalConfig
|
|
if isTeam {
|
|
getConfig = getTeamConfig
|
|
}
|
|
|
|
if isTeam {
|
|
// Invalid top level key
|
|
config := getConfig(nil)
|
|
config += "unknown_key:\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "unknown top-level field")
|
|
|
|
// Invalid team name
|
|
config = getConfig([]string{"name"})
|
|
config += "name: [2]\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal name")
|
|
|
|
// Missing team name
|
|
config = getConfig([]string{"name"})
|
|
config += "name:\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "'name' is required")
|
|
|
|
// Invalid team_settings
|
|
config = getConfig([]string{"team_settings"})
|
|
config += "team_settings:\n path: [2]\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal team_settings")
|
|
|
|
// Invalid team_settings in a separate file
|
|
tmpFile, err := os.CreateTemp(t.TempDir(), "*team_settings.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString("[2]")
|
|
require.NoError(t, err)
|
|
config = getConfig([]string{"team_settings"})
|
|
config += fmt.Sprintf("%s:\n path: %s\n", "team_settings", tmpFile.Name())
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal team settings file")
|
|
|
|
// Invalid secrets 1
|
|
config = getConfig([]string{"team_settings"})
|
|
config += "team_settings:\n secrets: bad\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "must be a list of secret items")
|
|
|
|
// Invalid secrets 2
|
|
config = getConfig([]string{"team_settings"})
|
|
config += "team_settings:\n secrets: [2]\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "must have a 'secret' key")
|
|
|
|
// Missing team_settings.
|
|
config = getConfig([]string{"team_settings"})
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "'team_settings' is required when 'name' is provided")
|
|
|
|
// team_settings set on a "no-team.yml".
|
|
config = getConfig([]string{"name"})
|
|
config += "name: No team\n"
|
|
noTeamPath1, noTeamBasePath1 := createNamedFileOnTempDir(t, "no-team.yml", config)
|
|
_, err = GitOpsFromFile(noTeamPath1, noTeamBasePath1, nil, nopLogf)
|
|
assert.ErrorContains(t, err, fmt.Sprintf("cannot set 'team_settings' on 'No team' file: %q", noTeamPath1))
|
|
|
|
// 'No team' file with invalid name.
|
|
config = getConfig([]string{"name", "team_settings"})
|
|
config += "name: No team\n"
|
|
noTeamPath2, noTeamBasePath2 := createNamedFileOnTempDir(t, "foobar.yml", config)
|
|
_, err = GitOpsFromFile(noTeamPath2, noTeamBasePath2, nil, nopLogf)
|
|
assert.ErrorContains(t, err, fmt.Sprintf("file %q for 'No team' must be named 'no-team.yml'", noTeamPath2))
|
|
|
|
// Missing secrets
|
|
config = getConfig([]string{"team_settings"})
|
|
config += "team_settings:\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "'team_settings.secrets' is required")
|
|
} else {
|
|
// 'software' is not allowed in global config
|
|
config := getConfig(nil)
|
|
config += "software:\n packages:\n - url: https://example.com\n"
|
|
path1, basePath1 := createTempFile(t, "", config)
|
|
appConfig := fleet.EnrichedAppConfig{}
|
|
appConfig.License = &fleet.LicenseInfo{
|
|
Tier: fleet.TierPremium,
|
|
}
|
|
_, err = GitOpsFromFile(path1, basePath1, &appConfig, nopLogf)
|
|
assert.ErrorContains(t, err, "'software' cannot be set on global file")
|
|
|
|
// Invalid org_settings
|
|
config = getConfig([]string{"org_settings"})
|
|
config += "org_settings:\n path: [2]\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal org_settings")
|
|
|
|
// Invalid org_settings in a separate file
|
|
tmpFile, err := os.CreateTemp(t.TempDir(), "*org_settings.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString("[2]")
|
|
require.NoError(t, err)
|
|
config = getConfig([]string{"org_settings"})
|
|
config += fmt.Sprintf("%s:\n path: %s\n", "org_settings", tmpFile.Name())
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal org settings file")
|
|
|
|
// Invalid secrets 1
|
|
config = getConfig([]string{"org_settings"})
|
|
config += "org_settings:\n secrets: bad\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "must be a list of secret items")
|
|
|
|
// Invalid secrets 2
|
|
config = getConfig([]string{"org_settings"})
|
|
config += "org_settings:\n secrets: [2]\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "must have a 'secret' key")
|
|
|
|
// Missing secrets
|
|
config = getConfig([]string{"org_settings"})
|
|
config += "org_settings:\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "'org_settings.secrets' is required")
|
|
}
|
|
|
|
// Invalid agent_options
|
|
config := getConfig([]string{"agent_options"})
|
|
config += "agent_options:\n path: [2]\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal agent_options")
|
|
|
|
// Invalid agent_options in a separate file
|
|
tmpFile, err := os.CreateTemp(t.TempDir(), "*agent_options.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString("[2]")
|
|
require.NoError(t, err)
|
|
config = getConfig([]string{"agent_options"})
|
|
config += fmt.Sprintf("%s:\n path: %s\n", "agent_options", tmpFile.Name())
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal agent options file")
|
|
|
|
// Invalid controls
|
|
config = getConfig([]string{"controls"})
|
|
config += "controls:\n path: [2]\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal controls")
|
|
|
|
// Invalid controls in a separate file
|
|
tmpFile, err = os.CreateTemp(t.TempDir(), "*controls.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString("[2]")
|
|
require.NoError(t, err)
|
|
config = getConfig([]string{"controls"})
|
|
config += fmt.Sprintf("%s:\n path: %s\n", "controls", tmpFile.Name())
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal controls file")
|
|
|
|
// Invalid policies
|
|
config = getConfig([]string{"policies"})
|
|
config += "policies:\n path: [2]\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal policies")
|
|
|
|
// Invalid policies in a separate file
|
|
tmpFile, err = os.CreateTemp(t.TempDir(), "*policies.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString("[2]")
|
|
require.NoError(t, err)
|
|
config = getConfig([]string{"policies"})
|
|
config += fmt.Sprintf("%s:\n - path: %s\n", "policies", tmpFile.Name())
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal policies file")
|
|
|
|
// Policy name missing
|
|
config = getConfig([]string{"policies"})
|
|
config += "policies:\n - query: SELECT 1;\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "name is required")
|
|
|
|
// Policy query missing
|
|
config = getConfig([]string{"policies"})
|
|
config += "policies:\n - name: Test Policy\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "query is required")
|
|
|
|
// Invalid queries
|
|
config = getConfig([]string{"queries"})
|
|
config += "queries:\n path: [2]\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal queries")
|
|
|
|
// Invalid policies in a separate file
|
|
tmpFile, err = os.CreateTemp(t.TempDir(), "*queries.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString("[2]")
|
|
require.NoError(t, err)
|
|
config = getConfig([]string{"queries"})
|
|
config += fmt.Sprintf("%s:\n - path: %s\n", "queries", tmpFile.Name())
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal queries file")
|
|
|
|
// Query name missing
|
|
config = getConfig([]string{"queries"})
|
|
config += "queries:\n - query: SELECT 1;\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "name is required")
|
|
|
|
// Query SQL query missing
|
|
config = getConfig([]string{"queries"})
|
|
config += "queries:\n - name: Test Query\n"
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "query is required")
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestTopLevelGitOpsValidation(t *testing.T) {
|
|
t.Parallel()
|
|
tests := map[string]struct {
|
|
optsToExclude []string
|
|
shouldPass bool
|
|
isTeam bool
|
|
}{
|
|
"all_present_global": {
|
|
optsToExclude: []string{},
|
|
shouldPass: true,
|
|
},
|
|
"all_present_team": {
|
|
optsToExclude: []string{},
|
|
shouldPass: true,
|
|
isTeam: true,
|
|
},
|
|
"missing_all": {
|
|
optsToExclude: []string{"controls", "queries", "policies", "agent_options", "org_settings"},
|
|
},
|
|
"missing_queries": {
|
|
optsToExclude: []string{"queries"},
|
|
},
|
|
"missing_policies": {
|
|
optsToExclude: []string{"policies"},
|
|
},
|
|
"missing_agent_options": {
|
|
optsToExclude: []string{"agent_options"},
|
|
},
|
|
"missing_org_settings": {
|
|
optsToExclude: []string{"org_settings"},
|
|
},
|
|
"missing_name": {
|
|
optsToExclude: []string{"name"},
|
|
isTeam: true,
|
|
},
|
|
"missing_team_settings": {
|
|
optsToExclude: []string{"team_settings"},
|
|
isTeam: true,
|
|
},
|
|
}
|
|
for name, test := range tests {
|
|
t.Run(
|
|
name, func(t *testing.T) {
|
|
var config string
|
|
if test.isTeam {
|
|
config = getTeamConfig(test.optsToExclude)
|
|
} else {
|
|
config = getGlobalConfig(test.optsToExclude)
|
|
}
|
|
_, err := gitOpsFromString(t, config)
|
|
if test.shouldPass {
|
|
assert.NoError(t, err)
|
|
} else {
|
|
assert.ErrorContains(t, err, "is required")
|
|
}
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestGitOpsNullArrays(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
config := getGlobalConfig([]string{"queries", "policies"})
|
|
config += "queries: null\npolicies: ~\n"
|
|
gitops, err := gitOpsFromString(t, config)
|
|
assert.NoError(t, err)
|
|
assert.Nil(t, gitops.Queries)
|
|
assert.Nil(t, gitops.Policies)
|
|
}
|
|
|
|
func TestGitOpsPaths(t *testing.T) {
|
|
t.Parallel()
|
|
tests := map[string]struct {
|
|
isArray bool
|
|
isTeam bool
|
|
goodConfig string
|
|
}{
|
|
"org_settings": {
|
|
isArray: false,
|
|
goodConfig: "secrets: []\n",
|
|
},
|
|
"team_settings": {
|
|
isArray: false,
|
|
isTeam: true,
|
|
goodConfig: "secrets: []\n",
|
|
},
|
|
"controls": {
|
|
isArray: false,
|
|
goodConfig: "windows_enabled_and_configured: true\n",
|
|
},
|
|
"queries": {
|
|
isArray: true,
|
|
goodConfig: "[]",
|
|
},
|
|
"policies": {
|
|
isArray: true,
|
|
goodConfig: "[]",
|
|
},
|
|
"agent_options": {
|
|
isArray: false,
|
|
goodConfig: "name: value\n",
|
|
},
|
|
}
|
|
|
|
for name, test := range tests {
|
|
test := test
|
|
name := name
|
|
t.Run(
|
|
name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
getConfig := getGlobalConfig
|
|
if test.isTeam {
|
|
getConfig = getTeamConfig
|
|
}
|
|
|
|
// Test an absolute top level path
|
|
tmpDir := t.TempDir()
|
|
tmpFile, err := os.CreateTemp(tmpDir, "*good.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFile.WriteString(test.goodConfig)
|
|
require.NoError(t, err)
|
|
config := getConfig([]string{name})
|
|
if test.isArray {
|
|
config += fmt.Sprintf("%s:\n - path: %s\n", name, tmpFile.Name())
|
|
} else {
|
|
config += fmt.Sprintf("%s:\n path: %s\n", name, tmpFile.Name())
|
|
}
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.NoError(t, err)
|
|
|
|
// Test a relative top level path
|
|
config = getConfig([]string{name})
|
|
mainTmpFile, err := os.CreateTemp(tmpDir, "*main.yml")
|
|
require.NoError(t, err)
|
|
dir, file := filepath.Split(tmpFile.Name())
|
|
if test.isArray {
|
|
config += fmt.Sprintf("%s:\n - path: ./%s\n", name, file)
|
|
} else {
|
|
config += fmt.Sprintf("%s:\n path: ./%s\n", name, file)
|
|
}
|
|
err = os.WriteFile(mainTmpFile.Name(), []byte(config), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
_, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil, nopLogf)
|
|
assert.NoError(t, err)
|
|
|
|
// Test a bad path
|
|
config = getConfig([]string{name})
|
|
if test.isArray {
|
|
config += fmt.Sprintf("%s:\n - path: ./%s\n", name, "doesNotExist.yml")
|
|
} else {
|
|
config += fmt.Sprintf("%s:\n path: ./%s\n", name, "doesNotExist.yml")
|
|
}
|
|
err = os.WriteFile(mainTmpFile.Name(), []byte(config), 0o644)
|
|
require.NoError(t, err)
|
|
|
|
_, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil, nopLogf)
|
|
assert.ErrorContains(t, err, "no such file or directory")
|
|
|
|
// Test a bad file -- cannot be unmarshalled
|
|
tmpFileBad, err := os.CreateTemp(t.TempDir(), "*invalid.yml")
|
|
require.NoError(t, err)
|
|
_, err = tmpFileBad.WriteString("bad:\nbad")
|
|
require.NoError(t, err)
|
|
config = getConfig([]string{name})
|
|
if test.isArray {
|
|
config += fmt.Sprintf("%s:\n - path: %s\n", name, tmpFileBad.Name())
|
|
} else {
|
|
config += fmt.Sprintf("%s:\n path: %s\n", name, tmpFileBad.Name())
|
|
}
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to unmarshal")
|
|
|
|
// Test a nested path -- bad
|
|
tmpFileBad, err = os.CreateTemp(filepath.Dir(mainTmpFile.Name()), "*bad.yml")
|
|
require.NoError(t, err)
|
|
if test.isArray {
|
|
_, err = tmpFileBad.WriteString(fmt.Sprintf("- path: %s\n", tmpFile.Name()))
|
|
} else {
|
|
_, err = tmpFileBad.WriteString(fmt.Sprintf("path: %s\n", tmpFile.Name()))
|
|
}
|
|
require.NoError(t, err)
|
|
config = getConfig([]string{name})
|
|
dir, file = filepath.Split(tmpFileBad.Name())
|
|
if test.isArray {
|
|
config += fmt.Sprintf("%s:\n - path: ./%s\n", name, file)
|
|
} else {
|
|
config += fmt.Sprintf("%s:\n path: ./%s\n", name, file)
|
|
}
|
|
err = os.WriteFile(mainTmpFile.Name(), []byte(config), 0o644)
|
|
require.NoError(t, err)
|
|
_, err = GitOpsFromFile(mainTmpFile.Name(), dir, nil, nopLogf)
|
|
assert.ErrorContains(t, err, "nested paths are not supported")
|
|
},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestGitOpsGlobalPolicyWithInstallSoftware(t *testing.T) {
|
|
t.Parallel()
|
|
config := getGlobalConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: Some policy
|
|
query: SELECT 1;
|
|
install_software:
|
|
package_path: ./some_path.yml
|
|
`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "install_software can only be set on team policies")
|
|
}
|
|
|
|
func TestGitOpsGlobalPolicyWithRunScript(t *testing.T) {
|
|
t.Parallel()
|
|
config := getGlobalConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: Some policy
|
|
query: SELECT 1;
|
|
run_script:
|
|
path: ./some_path.sh
|
|
`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "run_script can only be set on team policies")
|
|
}
|
|
|
|
func TestGitOpsTeamPolicyWithInvalidInstallSoftware(t *testing.T) {
|
|
t.Parallel()
|
|
config := getTeamConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: Some policy
|
|
query: SELECT 1;
|
|
install_software:
|
|
package_path: ./some_path.yml
|
|
`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "failed to read install_software.package_path file")
|
|
|
|
config = getTeamConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: Some policy
|
|
query: SELECT 1;
|
|
install_software:
|
|
package_path:
|
|
`
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "install_software must include either a package_path, an app_store_id or a hash_sha256")
|
|
|
|
config = getTeamConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: Some policy
|
|
query: SELECT 1;
|
|
install_software:
|
|
package_path: ./some_path.yml
|
|
app_store_id: "123456"
|
|
`
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "must have only one of package_path or app_store_id")
|
|
|
|
// Software has a URL that's too big
|
|
tooBigURL := fmt.Sprintf("https://ftp.mozilla.org/%s", strings.Repeat("a", 4000-23))
|
|
config = getTeamConfig([]string{"software"})
|
|
config += fmt.Sprintf(`
|
|
software:
|
|
packages:
|
|
- url: %s
|
|
`, tooBigURL)
|
|
appConfig := fleet.EnrichedAppConfig{}
|
|
appConfig.License = &fleet.LicenseInfo{
|
|
Tier: fleet.TierPremium,
|
|
}
|
|
path, basePath := createTempFile(t, "", config)
|
|
_, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf)
|
|
assert.ErrorContains(t, err, fmt.Sprintf("software URL \"%s\" is too long, must be 4000 characters or less", tooBigURL))
|
|
|
|
// Software URL isn't a valid URL
|
|
config = getTeamConfig([]string{"software"})
|
|
invalidURL := "1.2.3://"
|
|
config += fmt.Sprintf(`
|
|
software:
|
|
packages:
|
|
- url: %s
|
|
`, invalidURL)
|
|
|
|
path, basePath = createTempFile(t, "", config)
|
|
_, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf)
|
|
assert.ErrorContains(t, err, fmt.Sprintf("%s is not a valid URL", invalidURL))
|
|
|
|
// Software URL refers to a .exe but doesn't have (un)install scripts specified
|
|
config = getTeamConfig([]string{"software"})
|
|
exeURL := "https://download-installer.cdn.mozilla.net/pub/firefox/releases/136.0.4/win64/en-US/Firefox%20Setup%20136.0.4.exe?foo=bar"
|
|
config += fmt.Sprintf(`
|
|
software:
|
|
packages:
|
|
- url: %s
|
|
`, exeURL)
|
|
|
|
path, basePath = createTempFile(t, "", config)
|
|
_, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf)
|
|
assert.ErrorContains(t, err, fmt.Sprintf("software URL %s refers to an .exe package, which requires both install_script and uninstall_script", exeURL))
|
|
|
|
// Software URL refers to a .tar.gz but doesn't have (un)install scripts specified (URL doesn't exist as Firefox is all .tar.xz)
|
|
config = getTeamConfig([]string{"software"})
|
|
tgzURL := "https://download-installer.cdn.mozilla.net/pub/firefox/releases/137.0.2/linux-x86_64/en-US/firefox-137.0.2.tar.gz?foo=baz"
|
|
config += fmt.Sprintf(`
|
|
software:
|
|
packages:
|
|
- url: %s
|
|
`, tgzURL)
|
|
|
|
path, basePath = createTempFile(t, "", config)
|
|
_, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf)
|
|
assert.ErrorContains(t, err, fmt.Sprintf("software URL %s refers to a .tar.gz archive, which requires both install_script and uninstall_script", tgzURL))
|
|
|
|
// Policy references a VPP app not present on the team
|
|
config = getTeamConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: Some policy
|
|
query: SELECT 1;
|
|
install_software:
|
|
app_store_id: "123456"
|
|
`
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "not found on team")
|
|
|
|
// Policy references a software installer not present in the team.
|
|
config = getTeamConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- path: ./team_install_software.policies.yml
|
|
software:
|
|
packages:
|
|
- url: https://ftp.mozilla.org/pub/firefox/releases/129.0.2/mac/en-US/Firefox%20129.0.2.pkg
|
|
self_service: true
|
|
|
|
`
|
|
path, basePath = createTempFile(t, "", config)
|
|
err = file.Copy(
|
|
filepath.Join("testdata", "team_install_software.policies.yml"),
|
|
filepath.Join(basePath, "team_install_software.policies.yml"),
|
|
0o755,
|
|
)
|
|
require.NoError(t, err)
|
|
err = file.Copy(
|
|
filepath.Join("testdata", "microsoft-teams.pkg.software.yml"),
|
|
filepath.Join(basePath, "microsoft-teams.pkg.software.yml"),
|
|
0o755,
|
|
)
|
|
require.NoError(t, err)
|
|
_, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf)
|
|
assert.ErrorContains(t, err,
|
|
"install_software.package_path URL https://statics.teams.cdn.office.net/production-osx/enterprise/webview2/lkg/MicrosoftTeams.pkg not found on team",
|
|
)
|
|
|
|
// Policy references a software installer file that has an invalid yaml.
|
|
config = getTeamConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- path: ./team_install_software.policies.yml
|
|
software:
|
|
packages:
|
|
- url: https://ftp.mozilla.org/pub/firefox/releases/129.0.2/mac/en-US/Firefox%20129.0.2.pkg
|
|
self_service: true
|
|
`
|
|
path, basePath = createTempFile(t, "", config)
|
|
err = file.Copy(
|
|
filepath.Join("testdata", "team_install_software.policies.yml"),
|
|
filepath.Join(basePath, "team_install_software.policies.yml"),
|
|
0o755,
|
|
)
|
|
require.NoError(t, err)
|
|
err = os.WriteFile( // nolint:gosec
|
|
filepath.Join(basePath, "microsoft-teams.pkg.software.yml"),
|
|
[]byte("invalid yaml"),
|
|
0o755,
|
|
)
|
|
require.NoError(t, err)
|
|
appConfig = fleet.EnrichedAppConfig{}
|
|
appConfig.License = &fleet.LicenseInfo{
|
|
Tier: fleet.TierPremium,
|
|
}
|
|
_, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf)
|
|
assert.ErrorContains(t, err, "failed to unmarshal install_software.package_path file")
|
|
}
|
|
|
|
func TestGitOpsWithStrayScriptEntryWithNoPath(t *testing.T) {
|
|
t.Parallel()
|
|
config := getTeamConfig([]string{"controls"})
|
|
config += `
|
|
controls:
|
|
scripts:
|
|
-
|
|
`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, `check for a stray "-"`)
|
|
}
|
|
|
|
func TestGitOpsTeamPolicyWithInvalidRunScript(t *testing.T) {
|
|
t.Parallel()
|
|
config := getTeamConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: Some policy
|
|
query: SELECT 1;
|
|
run_script:
|
|
path: ./some_path.sh
|
|
`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "script file does not exist")
|
|
|
|
config = getTeamConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: Some policy
|
|
query: SELECT 1;
|
|
run_script:
|
|
path:
|
|
`
|
|
_, err = gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "empty run_script path")
|
|
|
|
// Policy references a script not present in the team.
|
|
config = getTeamConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- path: ./policies/script-policy.yml
|
|
software:
|
|
controls:
|
|
scripts:
|
|
- path: ./policies/policies2.yml
|
|
|
|
`
|
|
path, basePath := createTempFile(t, "", config)
|
|
err = file.Copy(
|
|
filepath.Join("testdata", "policies", "script-policy.yml"),
|
|
filepath.Join(basePath, "policies", "script-policy.yml"),
|
|
0o755,
|
|
)
|
|
require.NoError(t, err)
|
|
err = file.Copy(
|
|
filepath.Join("testdata", "lib", "collect-fleetd-logs.sh"),
|
|
filepath.Join(basePath, "lib", "collect-fleetd-logs.sh"),
|
|
0o755,
|
|
)
|
|
require.NoError(t, err)
|
|
appConfig := fleet.EnrichedAppConfig{}
|
|
appConfig.License = &fleet.LicenseInfo{
|
|
Tier: fleet.TierPremium,
|
|
}
|
|
_, err = GitOpsFromFile(path, basePath, &appConfig, nopLogf)
|
|
assert.ErrorContains(t, err,
|
|
"was not defined in controls for TeamName",
|
|
)
|
|
}
|
|
|
|
func getGlobalConfig(optsToExclude []string) string {
|
|
return getBaseConfig(topLevelOptions, optsToExclude)
|
|
}
|
|
|
|
func getTeamConfig(optsToExclude []string) string {
|
|
return getBaseConfig(teamLevelOptions, optsToExclude)
|
|
}
|
|
|
|
func getBaseConfig(options map[string]string, optsToExclude []string) string {
|
|
var config string
|
|
for key, value := range options {
|
|
if !slices.Contains(optsToExclude, key) {
|
|
config += value + "\n"
|
|
}
|
|
}
|
|
return config
|
|
}
|
|
|
|
func TestIllegalFleetSecret(t *testing.T) {
|
|
t.Parallel()
|
|
config := getGlobalConfig([]string{"policies"})
|
|
config += `
|
|
policies:
|
|
- name: $FLEET_SECRET_POLICY
|
|
platform: linux
|
|
query: SELECT 1 FROM osquery_info WHERE start_time < 0;
|
|
- name: My policy
|
|
platform: windows
|
|
query: SELECT 1;
|
|
`
|
|
_, err := gitOpsFromString(t, config)
|
|
assert.ErrorContains(t, err, "variables with \"FLEET_SECRET_\" prefix are only allowed")
|
|
}
|
|
|
|
func TestInvalidSoftwareInstallerHash(t *testing.T) {
|
|
appConfig := &fleet.EnrichedAppConfig{}
|
|
appConfig.License = &fleet.LicenseInfo{
|
|
Tier: fleet.TierPremium,
|
|
}
|
|
_, err := GitOpsFromFile("testdata/team_config_invalid_sha.yml", "./testdata", appConfig, nopLogf)
|
|
assert.ErrorContains(t, err, "must be a valid lower-case hex-encoded (64-character) SHA-256 hash value")
|
|
}
|