Allow icon in team level yaml for script-only packages (#43783)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #43142
Since script-only packages have to be specified as a path, add some
logic to allow icon to be set as a path in that situation.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [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/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] Timeouts are implemented and retries are limited to avoid infinite
loops
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- `TestSoftwarePackagesPathWithInline` checks custom package yml path so
there is no regression, added `TestScriptOnlyPackagesPathWithInline` to
test script-only package path.
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually
- Tested .sh and .ps1 script-only packages with icon path specified in
the team level yaml.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Fixed custom icon handling for script-only packages (e.g., .sh and
.ps1), allowing icons to be set and resolved correctly for packages
referenced by path.
* **Tests**
* Added test coverage validating custom icon functionality and path
resolution for script-only packages; included a sample script used by
the test.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jonathan Katz 2026-04-20 15:56:23 -05:00 committed by GitHub
parent 308e5f3dc8
commit 7d9c134942
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 51 additions and 5 deletions

View file

@ -0,0 +1 @@
- Fixed an issue where adding a custom icon for a script-only package was not allowed in GitOps.

View file

@ -275,12 +275,19 @@ type SoftwarePackage struct {
fleet.SoftwarePackageSpec
}
func (spec SoftwarePackage) HydrateToPackageLevel(packageLevel fleet.SoftwarePackageSpec) (fleet.SoftwarePackageSpec, error) {
if spec.Icon.Path != "" || spec.InstallScript.Path != "" || spec.UninstallScript.Path != "" ||
func (spec SoftwarePackage) HydrateToPackageLevel(packageLevel fleet.SoftwarePackageSpec, ext string) (fleet.SoftwarePackageSpec, error) {
if spec.InstallScript.Path != "" || spec.UninstallScript.Path != "" ||
spec.PostInstallScript.Path != "" || spec.URL != "" || spec.SHA256 != "" || spec.PreInstallQuery.Path != "" {
return packageLevel, fmt.Errorf("the software package defined in %s must not have icons, scripts, queries, URL, or hash specified at the team level", *spec.Path)
}
// Icon should be allowed at the team level yaml for script packages which must be specified as a path
if spec.Icon.Path != "" {
if ext != ".sh" && ext != ".ps1" {
return packageLevel, fmt.Errorf("the software package defined in %s must not have icons, scripts, queries, URL, or hash specified at the team level", *spec.Path)
}
}
packageLevel.Categories = spec.Categories
packageLevel.LabelsIncludeAny = spec.LabelsIncludeAny
packageLevel.LabelsExcludeAny = spec.LabelsExcludeAny
@ -1877,10 +1884,15 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin
}
// Script file becomes the install script for a script-only package
scriptSpec := fleet.SoftwarePackageSpec{
InstallScript: fleet.TeamSpecSoftwareAsset{Path: resolvedPath},
ReferencedYamlPath: resolvedPath,
Icon: teamLevelPackage.Icon,
}
scriptSpec, err = teamLevelPackage.HydrateToPackageLevel(scriptSpec)
// Icon path needs to be resolved, but since this function will set
// the install script it needs to be set to the correct path again.
scriptSpec = scriptSpec.ResolveSoftwarePackagePaths(baseDir)
scriptSpec.InstallScript.Path = resolvedPath
scriptSpec, err = teamLevelPackage.HydrateToPackageLevel(scriptSpec, ext)
if err != nil {
multiError = multierror.Append(multiError, err)
continue
@ -1922,7 +1934,7 @@ func parseSoftware(top map[string]json.RawMessage, result *GitOps, baseDir strin
for i, spec := range softwarePackageSpecs {
softwarePackageSpec := spec.ResolveSoftwarePackagePaths(filepath.Dir(spec.ReferencedYamlPath))
softwarePackageSpec, err = teamLevelPackage.HydrateToPackageLevel(softwarePackageSpec)
softwarePackageSpec, err = teamLevelPackage.HydrateToPackageLevel(softwarePackageSpec, ext)
if err != nil {
multiError = multierror.Append(multiError, err)
continue

View file

@ -1524,6 +1524,36 @@ software:
assert.ErrorContains(t, err, "the software package defined in software/single-package.yml must not have icons, scripts, queries, URL, or hash specified at the team level")
}
func TestScriptOnlyPackagesPathWithInline(t *testing.T) {
t.Parallel()
config := getTeamConfig([]string{"software"})
config += `
software:
packages:
- path: software/script-only.sh
icon:
path: ./foo/bar.png
`
path, basePath := createTempFile(t, "", config)
err := file.Copy(
filepath.Join("testdata", "software", "script-only.sh"),
filepath.Join(basePath, "software", "script-only.sh"),
os.FileMode(0o755),
)
require.NoError(t, err)
appConfig := fleet.EnrichedAppConfig{}
appConfig.License = &fleet.LicenseInfo{
Tier: fleet.TierPremium,
}
gitops, err := GitOpsFromFile(path, basePath, &appConfig, nopLogf)
require.NoError(t, err)
require.Len(t, gitops.Software.Packages, 1)
assert.Equal(t, filepath.Join(basePath, "foo", "bar.png"), gitops.Software.Packages[0].Icon.Path)
}
func TestIllegalFleetSecret(t *testing.T) {
t.Parallel()
config := getGlobalConfig([]string{"policies"})

View file

@ -0,0 +1,3 @@
#!/bin/bash
echo "hello world"