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:
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
apt-get install -f "$INSTALLER_PATH"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
installer -pkg "$INSTALLER_PATH" -target /
|
||||
@@ -0,0 +1 @@
|
||||
apt-get remove -y $(dpkg -f "$INSTALLER_PATH" Package)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
apt-get install -f "./foo/bar baz.f"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
installer -pkg "./foo/bar baz.f" -target /
|
||||
@@ -0,0 +1 @@
|
||||
apt-get remove -y $(dpkg -f "./foo/bar baz.f" Package)
|
||||
+14
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -71,7 +71,10 @@ const config = {
|
||||
filename: "[name]@[hash][ext]",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
test: /\.(sh|ps1)$/,
|
||||
type: "asset/source",
|
||||
},
|
||||
{
|
||||
test: /(\.tsx?|\.jsx?)$/,
|
||||
exclude: /node_modules/,
|
||||
|
||||
Reference in New Issue
Block a user