add scripts to add/remove software (#18649)

for:

- https://github.com/fleetdm/fleet/issues/18314
- https://github.com/fleetdm/fleet/issues/18315
- https://github.com/fleetdm/fleet/issues/18317
- https://github.com/fleetdm/fleet/issues/18316

# Checklist for submitter

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

<!-- Note that API documentation changes are now addressed by the
product design team. -->

- [x] Added/updated tests
- [x] Manual QA for all new/changed functionality
This commit is contained in:
Roberto Dip
2024-05-01 14:15:59 -03:00
committed by GitHub
parent 7d014f9fad
commit 92e540aee5
22 changed files with 359 additions and 1 deletions
+4
View File
@@ -133,3 +133,7 @@ export const formatSoftwareType = ({
}
return type;
};
// ISoftwareInstallerType defines the supported installer types for
// software uploaded by the IT admin.
export type ISoftwareInstallerType = "pkg" | "msi" | "deb" | "exe";
@@ -0,0 +1,50 @@
import { ISoftwareInstallerType } from "interfaces/software";
// @ts-ignore
import installPkg from "../../pkg/file/scripts/install_pkg.sh";
// @ts-ignore
import installMsi from "../../pkg/file/scripts/install_msi.ps1";
// @ts-ignore
import installExe from "../../pkg/file/scripts/install_exe.ps1";
// @ts-ignore
import installDeb from "../../pkg/file/scripts/install_deb.sh";
const replaceVariables = (rawScript: string, installerPath: string): string => {
return rawScript.replace("$INSTALLER_PATH", installerPath);
};
/*
* getInstallScript returns a string with a script to install the
* provided software.
*
* Note that we don't do any sanitization of the arguments here,
* delegating that to the caller which should have the right context
* about what should be escaped.
* */
const getInstallScript = (
filetype: ISoftwareInstallerType,
path: string
): string => {
let rawScript: string;
switch (filetype) {
case "pkg":
rawScript = installPkg;
break;
case "msi":
rawScript = installMsi;
break;
case "deb":
rawScript = installDeb;
break;
case "exe":
rawScript = installExe;
break;
default:
// this should never happen as this function is type-guarded
throw new Error(`unsupported file type: ${filetype}`);
}
return replaceVariables(rawScript, path);
};
export default getInstallScript;
+85
View File
@@ -0,0 +1,85 @@
package file
import (
_ "embed"
"strings"
)
type InstallerType string
const (
InstallerTypeMsi InstallerType = "msi"
InstallerTypeDeb InstallerType = "deb"
InstallerTypePkg InstallerType = "pkg"
InstallerTypeExe InstallerType = "exe"
)
//go:embed scripts/install_pkg.sh
var installPkgScript string
//go:embed scripts/install_msi.ps1
var installMsiScript string
//go:embed scripts/install_exe.ps1
var installExeScript string
//go:embed scripts/install_deb.sh
var installDebScript string
// GetInstallScript returns a script that can be used to install the given
// installer based on the provided type
func GetInstallScript(installerType InstallerType, installerPath string) string {
var rawScript string
switch installerType {
case InstallerTypeMsi:
rawScript = installMsiScript
case InstallerTypeDeb:
rawScript = installDebScript
case InstallerTypePkg:
rawScript = installPkgScript
case InstallerTypeExe:
rawScript = installExeScript
default:
return ""
}
return replaceVars(rawScript, installerPath)
}
//go:embed scripts/remove_exe.ps1
var removeExeScript string
//go:embed scripts/remove_pkg.sh
var removePkgScript string
//go:embed scripts/remove_msi.ps1
var removeMsiScript string
//go:embed scripts/remove_deb.sh
var removeDebScript string
// GetRemoveScript returns a script that can be used to remove the given
// installer based on the provided type
func GetRemoveScript(installerType InstallerType, installerPath string) string {
var rawScript string
switch installerType {
case InstallerTypeMsi:
rawScript = removeMsiScript
case InstallerTypeDeb:
rawScript = removeDebScript
case InstallerTypePkg:
rawScript = removePkgScript
case InstallerTypeExe:
rawScript = removeExeScript
default:
return ""
}
return replaceVars(rawScript, installerPath)
}
func replaceVars(rawScript string, installerPath string) string {
return strings.Replace(rawScript, "$INSTALLER_PATH", installerPath, -1)
}
+73
View File
@@ -0,0 +1,73 @@
package file
import (
"flag"
"io"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
)
var (
update = flag.Bool("update", false, "update the golden files of this test")
)
func TestMain(m *testing.M) {
flag.Parse()
os.Exit(m.Run())
}
// Note: to update the goldens, run the tests with `-update`:
//
// go test ./pkg/file/... -update
func TestGetInstallAndRemoveScript(t *testing.T) {
scriptsByType := map[InstallerType][2]string{
InstallerTypeMsi: {
"./scripts/install_msi.ps1",
"./scripts/remove_msi.ps1",
},
InstallerTypePkg: {
"./scripts/install_pkg.sh",
"./scripts/remove_pkg.sh",
},
InstallerTypeDeb: {
"./scripts/install_deb.sh",
"./scripts/remove_deb.sh",
},
InstallerTypeExe: {
"./scripts/install_exe.ps1",
"./scripts/remove_exe.ps1",
},
}
for itype, scripts := range scriptsByType {
installerPath := "./foo/bar baz.f"
gotScript := GetInstallScript(itype, installerPath)
assertGoldenMatches(t, scripts[0], gotScript, *update)
gotScript = GetRemoveScript(itype, installerPath)
assertGoldenMatches(t, scripts[1], gotScript, *update)
}
}
func assertGoldenMatches(t *testing.T, goldenFile string, actual string, update bool) {
t.Helper()
goldenPath := filepath.Join("testdata", goldenFile+".golden")
f, err := os.OpenFile(goldenPath, os.O_RDWR|os.O_CREATE, 0644)
require.NoError(t, err)
defer f.Close()
if update {
_, err := f.WriteString(actual)
require.NoError(t, err)
return
}
content, err := io.ReadAll(f)
require.NoError(t, err)
require.Equal(t, string(content), actual)
}
+15
View File
@@ -0,0 +1,15 @@
### File scripts
This folder contains scripts to install/remove software for different types of installers.
Scripts are stored on their own files for two reasons:
1. Some of them are read and displayed in the UI.
2. It's helpful to have good syntax highlighting and easy ways to run them.
#### Variables
Because the scripts are shared between Go and JS, the convention is to declare variables using `$VAR_NAME` and document its intended usage here.
- `$INSTALLER_PATH` path to the installer file.
+1
View File
@@ -0,0 +1 @@
apt-get install -f "$INSTALLER_PATH"
+21
View File
@@ -0,0 +1,21 @@
$exeFilePath = "$INSTALLER_PATH"
# extract the name of the executable to use as the sub-directory name
$exeName = [System.IO.Path]::GetFileName($exeFilePath)
$subDir = [System.IO.Path]::GetFileNameWithoutExtension($exeFilePath)
# Program Files is the recommended location for any third-party software on Windows.
#
# Note: a x86 binary on a x64 system is supposed to go in
# $env:ProgramFiles(x86) but I didn't find a reliable way to get this
# information from the exe file.
$destinationPath = Join-Path -Path $env:ProgramFiles -ChildPath $subDir
# check if the directory does not exist, and create it if necessary
if (-not (Test-Path -Path $destinationPath)) {
New-Item -ItemType Directory -Path $destinationPath
}
# copy the .exe file to the new sub-directory
$destinationExePath = Join-Path -Path $destinationPath -ChildPath $exeName
Copy-Item -Path $exeFilePath -Destination $destinationExePath
+9
View File
@@ -0,0 +1,9 @@
$logFile = "${env:TEMP}/fleet-install-software.log"
$installProcess = Start-Process msiexec.exe `
-ArgumentList "/quiet /norestart /lv ${logFile} /i `"$INSTALLER_PATH`"" `
-PassThru -Verb RunAs -Wait
Get-Content $logFile -Tail 500
exit $instalProcess.ExitCode
+1
View File
@@ -0,0 +1 @@
installer -pkg "$INSTALLER_PATH" -target /
+1
View File
@@ -0,0 +1 @@
apt-get remove -y $(dpkg -f "$INSTALLER_PATH" Package)
+14
View File
@@ -0,0 +1,14 @@
$exeFilePath = "$INSTALLER_PATH"
# extract the name of the executable to use as the sub-directory name
$exeName = [System.IO.Path]::GetFileName($exeFilePath)
$subDir = [System.IO.Path]::GetFileNameWithoutExtension($exeFilePath)
# determine the correct Program Files directory based on OS architecture
$destinationPath = Join-Path -Path $env:ProgramFiles -ChildPath $subDir
$destinationExePath = Join-Path -Path $destinationPath -ChildPath $exeName
# remove only the exe file, while at runtime other files could have been
# created in this folder, this is a naive approach to prevent forcing us to
# remove important folders by crafting a malicious file name.
Remove-Item -Path $destinationExePath
+9
View File
@@ -0,0 +1,9 @@
$logFile = "${env:TEMP}/fleet-remove-software.log"
$removeProcess = Start-Process msiexec.exe `
-ArgumentList "/quiet /norestart /lv ${logFile} /x `"$INSTALLER_PATH`"" `
-PassThru -Verb RunAs -Wait
Get-Content $logFile -Tail 500
exit $instalProcess.ExitCode
+8
View File
@@ -0,0 +1,8 @@
# grab the identifier from the first PackageInfo we find. Those are placed in different locations depending on the installer
pkg_id=$(tar xOvf "$INSTALLER_PATH" --include='*PackageInfo*' 2>/dev/null | sed -n 's/.*identifier="\([^"]*\)".*/\1/p')
# remove all the files and empty directories that were installed
pkgutil --files $pkg_id | tr '\n' '\0' | xargs -n 1 -0 rm -d
# remove the receipt
pkgutil --forget $pkg_id
+1
View File
@@ -0,0 +1 @@
apt-get install -f "./foo/bar baz.f"
+21
View File
@@ -0,0 +1,21 @@
$exeFilePath = "./foo/bar baz.f"
# extract the name of the executable to use as the sub-directory name
$exeName = [System.IO.Path]::GetFileName($exeFilePath)
$subDir = [System.IO.Path]::GetFileNameWithoutExtension($exeFilePath)
# Program Files is the recommended location for any third-party software on Windows.
#
# Note: a x86 binary on a x64 system is supposed to go in
# $env:ProgramFiles(x86) but I didn't find a reliable way to get this
# information from the exe file.
$destinationPath = Join-Path -Path $env:ProgramFiles -ChildPath $subDir
# check if the directory does not exist, and create it if necessary
if (-not (Test-Path -Path $destinationPath)) {
New-Item -ItemType Directory -Path $destinationPath
}
# copy the .exe file to the new sub-directory
$destinationExePath = Join-Path -Path $destinationPath -ChildPath $exeName
Copy-Item -Path $exeFilePath -Destination $destinationExePath
+9
View File
@@ -0,0 +1,9 @@
$logFile = "${env:TEMP}/fleet-install-software.log"
$installProcess = Start-Process msiexec.exe `
-ArgumentList "/quiet /norestart /lv ${logFile} /i `"./foo/bar baz.f`"" `
-PassThru -Verb RunAs -Wait
Get-Content $logFile -Tail 500
exit $instalProcess.ExitCode
+1
View File
@@ -0,0 +1 @@
installer -pkg "./foo/bar baz.f" -target /
+1
View File
@@ -0,0 +1 @@
apt-get remove -y $(dpkg -f "./foo/bar baz.f" Package)
+14
View File
@@ -0,0 +1,14 @@
$exeFilePath = "./foo/bar baz.f"
# extract the name of the executable to use as the sub-directory name
$exeName = [System.IO.Path]::GetFileName($exeFilePath)
$subDir = [System.IO.Path]::GetFileNameWithoutExtension($exeFilePath)
# determine the correct Program Files directory based on OS architecture
$destinationPath = Join-Path -Path $env:ProgramFiles -ChildPath $subDir
$destinationExePath = Join-Path -Path $destinationPath -ChildPath $exeName
# remove only the exe file, while at runtime other files could have been
# created in this folder, this is a naive approach to prevent forcing us to
# remove important folders by crafting a malicious file name.
Remove-Item -Path $destinationExePath
+9
View File
@@ -0,0 +1,9 @@
$logFile = "${env:TEMP}/fleet-remove-software.log"
$removeProcess = Start-Process msiexec.exe `
-ArgumentList "/quiet /norestart /lv ${logFile} /x `"./foo/bar baz.f`"" `
-PassThru -Verb RunAs -Wait
Get-Content $logFile -Tail 500
exit $instalProcess.ExitCode
+8
View File
@@ -0,0 +1,8 @@
# grab the identifier from the first PackageInfo we find. Those are placed in different locations depending on the installer
pkg_id=$(tar xOvf "./foo/bar baz.f" --include='*PackageInfo*' 2>/dev/null | sed -n 's/.*identifier="\([^"]*\)".*/\1/p')
# remove all the files and empty directories that were installed
pkgutil --files $pkg_id | tr '\n' '\0' | xargs -n 1 -0 rm -d
# remove the receipt
pkgutil --forget $pkg_id
+4 -1
View File
@@ -71,7 +71,10 @@ const config = {
filename: "[name]@[hash][ext]",
},
},
{
test: /\.(sh|ps1)$/,
type: "asset/source",
},
{
test: /(\.tsx?|\.jsx?)$/,
exclude: /node_modules/,