mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 17:08:53 +00:00
Extract metadata, detect platform for no-payload packages (#34099)
Implements #33750, also related to #33980
This commit is contained in:
parent
cc7062f8f0
commit
7382a0ef7f
3 changed files with 226 additions and 10 deletions
|
|
@ -6,6 +6,7 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
|
@ -78,7 +79,7 @@ func (svc *Service) UploadSoftwareInstaller(ctx context.Context, payload *fleet.
|
|||
//
|
||||
// For "msi", addMetadataToSoftwarePayload fails before this point if product code cannot be extracted.
|
||||
//
|
||||
case payload.Extension == "exe" || payload.Extension == "tar.gz":
|
||||
case payload.Extension == "exe" || payload.Extension == "tar.gz" || fleet.IsScriptPackage(payload.Extension):
|
||||
return nil, &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("Couldn't add. Fleet can't create a policy to detect existing installations for .%s packages. Please add the software, add a custom policy, and enable the install software policy automation.", payload.Extension),
|
||||
}
|
||||
|
|
@ -1537,6 +1538,16 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
|
|||
return "", ctxerr.New(ctx, "installer file is required")
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(payload.Filename))
|
||||
ext = strings.TrimPrefix(ext, ".")
|
||||
|
||||
if fleet.IsScriptPackage(ext) {
|
||||
if err := svc.addScriptPackageMetadata(ctx, payload, ext); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ext, nil
|
||||
}
|
||||
|
||||
meta, err := file.ExtractInstallerMetadata(payload.InstallerFile)
|
||||
if err != nil {
|
||||
if errors.Is(err, file.ErrUnsupportedType) {
|
||||
|
|
@ -1621,6 +1632,60 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
|
|||
return meta.Extension, nil
|
||||
}
|
||||
|
||||
func (svc *Service) addScriptPackageMetadata(ctx context.Context, payload *fleet.UploadSoftwareInstallerPayload, extension string) error {
|
||||
if payload == nil {
|
||||
return ctxerr.New(ctx, "payload is required")
|
||||
}
|
||||
|
||||
if payload.InstallerFile == nil {
|
||||
return ctxerr.New(ctx, "installer file is required")
|
||||
}
|
||||
|
||||
scriptBytes, err := io.ReadAll(payload.InstallerFile)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "reading script file")
|
||||
}
|
||||
|
||||
if err := payload.InstallerFile.Rewind(); err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "resetting script file reader")
|
||||
}
|
||||
|
||||
scriptContents := string(scriptBytes)
|
||||
|
||||
if err := fleet.ValidateHostScriptContents(scriptContents, false); err != nil {
|
||||
return &fleet.BadRequestError{
|
||||
Message: fmt.Sprintf("Couldn't add. Script validation failed: %s", err.Error()),
|
||||
InternalErr: ctxerr.Wrap(ctx, err, "validating script contents"),
|
||||
}
|
||||
}
|
||||
|
||||
shaSum, err := file.SHA256FromTempFileReader(payload.InstallerFile)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "calculating script SHA256")
|
||||
}
|
||||
|
||||
if payload.Title == "" {
|
||||
base := filepath.Base(payload.Filename)
|
||||
payload.Title = strings.TrimSuffix(base, filepath.Ext(base))
|
||||
}
|
||||
|
||||
payload.Version = ""
|
||||
payload.InstallScript = scriptContents
|
||||
payload.StorageID = shaSum
|
||||
payload.BundleIdentifier = ""
|
||||
payload.PackageIDs = nil
|
||||
payload.Extension = extension
|
||||
payload.Source = "scripts"
|
||||
|
||||
platform, err := fleet.SoftwareInstallerPlatformFromExtension(extension)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "determining platform from extension")
|
||||
}
|
||||
payload.Platform = platform
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
batchSoftwarePrefix = "software_batch_"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ func TestInstallUninstallAuth(t *testing.T) {
|
|||
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
ServerSettings: fleet.ServerSettings{ScriptsDisabled: true}, // global scripts being disabled shouldn't impact (un)installs
|
||||
ServerSettings: fleet.ServerSettings{ScriptsDisabled: true},
|
||||
}, nil
|
||||
}
|
||||
ds.HostFunc = func(ctx context.Context, id uint) (*fleet.Host, error) {
|
||||
|
|
@ -184,7 +184,6 @@ func TestInstallUninstallAuth(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestUninstallSoftwareTitle is mostly tested in enterprise integration test. This test hits a few edge cases.
|
||||
func TestUninstallSoftwareTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := new(mock.Store)
|
||||
|
|
@ -200,7 +199,6 @@ func TestUninstallSoftwareTitle(t *testing.T) {
|
|||
return host, nil
|
||||
}
|
||||
|
||||
// Global scripts disabled (doesn't matter)
|
||||
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
ServerSettings: fleet.ServerSettings{
|
||||
|
|
@ -209,12 +207,10 @@ func TestUninstallSoftwareTitle(t *testing.T) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Host scripts disabled
|
||||
host.ScriptsEnabled = ptr.Bool(false)
|
||||
require.ErrorContains(t, svc.UninstallSoftwareTitle(context.Background(), 1, 10), fleet.RunScriptsOrbitDisabledErrMsg)
|
||||
}
|
||||
|
||||
// TestInstallSoftwareTitle is mostly tested in enterprise integration test. This test hits is placed to hit some edge cases.
|
||||
func TestInstallSoftwareTitle(t *testing.T) {
|
||||
t.Parallel()
|
||||
ds := new(mock.Store)
|
||||
|
|
@ -312,7 +308,6 @@ func TestSoftwareInstallerPayloadFromSlug(t *testing.T) {
|
|||
assert.NotEmpty(t, payload.UninstallScript)
|
||||
assert.True(t, payload.FleetMaintained)
|
||||
|
||||
// when SHA256 is no_check
|
||||
ds.GetMaintainedAppBySlugFunc = func(ctx context.Context, slug string, teamID *uint) (*fleet.MaintainedApp, error) {
|
||||
return &fleet.MaintainedApp{
|
||||
ID: 1,
|
||||
|
|
@ -331,7 +326,6 @@ func TestSoftwareInstallerPayloadFromSlug(t *testing.T) {
|
|||
assert.NotEmpty(t, payload.UninstallScript)
|
||||
assert.True(t, payload.FleetMaintained)
|
||||
|
||||
// when a slug empty, we no-op and return the payload as is
|
||||
payload = fleet.SoftwareInstallerPayload{URL: "https://fleetdm.com"}
|
||||
err = svc.softwareInstallerPayloadFromSlug(context.Background(), &payload, nil)
|
||||
require.NoError(t, err)
|
||||
|
|
@ -377,3 +371,155 @@ func newTestServiceWithMock(t *testing.T, ds fleet.Datastore) (*Service, *svcmoc
|
|||
}
|
||||
return svc, baseSvc
|
||||
}
|
||||
|
||||
func TestAddScriptPackageMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
svc := newTestService(t, nil)
|
||||
|
||||
t.Run("valid shell script", func(t *testing.T) {
|
||||
scriptContents := "#!/bin/bash\necho 'Installing software'\n"
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.sh")
|
||||
require.NoError(t, err)
|
||||
defer tmpFile.Close()
|
||||
_, err = tmpFile.WriteString(scriptContents)
|
||||
require.NoError(t, err)
|
||||
|
||||
tfr, err := fleet.NewKeepFileReader(tmpFile.Name())
|
||||
require.NoError(t, err)
|
||||
defer tfr.Close()
|
||||
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallerFile: tfr,
|
||||
Filename: "install-app.sh",
|
||||
}
|
||||
|
||||
err = svc.addScriptPackageMetadata(ctx, payload, "sh")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "install-app", payload.Title)
|
||||
require.Equal(t, "", payload.Version)
|
||||
require.Equal(t, scriptContents, payload.InstallScript)
|
||||
require.Equal(t, "linux", payload.Platform)
|
||||
require.Equal(t, "scripts", payload.Source)
|
||||
require.Empty(t, payload.BundleIdentifier)
|
||||
require.Empty(t, payload.PackageIDs)
|
||||
require.NotEmpty(t, payload.StorageID)
|
||||
require.Equal(t, "sh", payload.Extension)
|
||||
})
|
||||
|
||||
t.Run("valid powershell script", func(t *testing.T) {
|
||||
scriptContents := "Write-Host 'Installing software'\n"
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.ps1")
|
||||
require.NoError(t, err)
|
||||
defer tmpFile.Close()
|
||||
_, err = tmpFile.WriteString(scriptContents)
|
||||
require.NoError(t, err)
|
||||
|
||||
tfr, err := fleet.NewKeepFileReader(tmpFile.Name())
|
||||
require.NoError(t, err)
|
||||
defer tfr.Close()
|
||||
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallerFile: tfr,
|
||||
Filename: "install-app.ps1",
|
||||
}
|
||||
|
||||
err = svc.addScriptPackageMetadata(ctx, payload, "ps1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "install-app", payload.Title)
|
||||
require.Equal(t, "", payload.Version)
|
||||
require.Equal(t, scriptContents, payload.InstallScript)
|
||||
require.Equal(t, "windows", payload.Platform)
|
||||
require.Equal(t, "scripts", payload.Source)
|
||||
require.Empty(t, payload.BundleIdentifier)
|
||||
require.Empty(t, payload.PackageIDs)
|
||||
require.NotEmpty(t, payload.StorageID)
|
||||
})
|
||||
|
||||
t.Run("invalid shebang", func(t *testing.T) {
|
||||
scriptContents := "#!/usr/bin/python\nprint('hello')\n"
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.sh")
|
||||
require.NoError(t, err)
|
||||
defer tmpFile.Close()
|
||||
_, err = tmpFile.WriteString(scriptContents)
|
||||
require.NoError(t, err)
|
||||
|
||||
tfr, err := fleet.NewKeepFileReader(tmpFile.Name())
|
||||
require.NoError(t, err)
|
||||
defer tfr.Close()
|
||||
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallerFile: tfr,
|
||||
Filename: "test.sh",
|
||||
}
|
||||
|
||||
err = svc.addScriptPackageMetadata(ctx, payload, "sh")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "Script validation failed")
|
||||
require.Contains(t, err.Error(), "Interpreter not supported")
|
||||
})
|
||||
|
||||
t.Run("empty script", func(t *testing.T) {
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.sh")
|
||||
require.NoError(t, err)
|
||||
defer tmpFile.Close()
|
||||
|
||||
tfr, err := fleet.NewKeepFileReader(tmpFile.Name())
|
||||
require.NoError(t, err)
|
||||
defer tfr.Close()
|
||||
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallerFile: tfr,
|
||||
Filename: "test.sh",
|
||||
}
|
||||
|
||||
err = svc.addScriptPackageMetadata(ctx, payload, "sh")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "must not be empty")
|
||||
})
|
||||
|
||||
t.Run("custom title preserved", func(t *testing.T) {
|
||||
scriptContents := "#!/bin/bash\necho 'test'\n"
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.sh")
|
||||
require.NoError(t, err)
|
||||
defer tmpFile.Close()
|
||||
_, err = tmpFile.WriteString(scriptContents)
|
||||
require.NoError(t, err)
|
||||
|
||||
tfr, err := fleet.NewKeepFileReader(tmpFile.Name())
|
||||
require.NoError(t, err)
|
||||
defer tfr.Close()
|
||||
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallerFile: tfr,
|
||||
Filename: "test.sh",
|
||||
Title: "My Custom Title",
|
||||
}
|
||||
|
||||
err = svc.addScriptPackageMetadata(ctx, payload, "sh")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "My Custom Title", payload.Title)
|
||||
})
|
||||
|
||||
t.Run("file contents preserved verbatim", func(t *testing.T) {
|
||||
scriptContents := "#!/bin/bash\necho \"Test's 'quotes'\"\necho $VAR\n\n"
|
||||
tmpFile, err := os.CreateTemp(t.TempDir(), "test-*.sh")
|
||||
require.NoError(t, err)
|
||||
defer tmpFile.Close()
|
||||
_, err = tmpFile.WriteString(scriptContents)
|
||||
require.NoError(t, err)
|
||||
|
||||
tfr, err := fleet.NewKeepFileReader(tmpFile.Name())
|
||||
require.NoError(t, err)
|
||||
defer tfr.Close()
|
||||
|
||||
payload := &fleet.UploadSoftwareInstallerPayload{
|
||||
InstallerFile: tfr,
|
||||
Filename: "test.sh",
|
||||
}
|
||||
|
||||
err = svc.addScriptPackageMetadata(ctx, payload, "sh")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, scriptContents, payload.InstallScript)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -568,6 +568,11 @@ type DownloadSoftwareInstallerPayload struct {
|
|||
Size int64
|
||||
}
|
||||
|
||||
func IsScriptPackage(ext string) bool {
|
||||
ext = strings.TrimPrefix(ext, ".")
|
||||
return ext == "sh" || ext == "ps1"
|
||||
}
|
||||
|
||||
func SofwareInstallerSourceFromExtensionAndName(ext, name string) (string, error) {
|
||||
ext = strings.TrimPrefix(ext, ".")
|
||||
switch ext {
|
||||
|
|
@ -594,9 +599,9 @@ func SofwareInstallerSourceFromExtensionAndName(ext, name string) (string, error
|
|||
func SoftwareInstallerPlatformFromExtension(ext string) (string, error) {
|
||||
ext = strings.TrimPrefix(ext, ".")
|
||||
switch ext {
|
||||
case "deb", "rpm", "tar.gz":
|
||||
case "deb", "rpm", "tar.gz", "sh":
|
||||
return "linux", nil
|
||||
case "exe", "msi":
|
||||
case "exe", "msi", "ps1":
|
||||
return "windows", nil
|
||||
case "pkg":
|
||||
return "darwin", nil
|
||||
|
|
|
|||
Loading…
Reference in a new issue