feat: Fleet Forge UEM - device management, agent, remote desktop (VNC), TLS reverse proxy
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
//go:build windows
|
||||
|
||||
package pkgmanager
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows/registry"
|
||||
)
|
||||
|
||||
func listWindowsPackages() ([]PackageInfo, error) {
|
||||
seen := make(map[string]PackageInfo)
|
||||
|
||||
if err := enumerateRegistryUninstall(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, seen); err != nil {
|
||||
return nil, fmt.Errorf("enumerate current user uninstall failed: %w", err)
|
||||
}
|
||||
if err := enumerateRegistryUninstall(registry.LOCAL_MACHINE, `SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall`, seen); err != nil {
|
||||
// non-fatal; 32-bit view may not exist
|
||||
_ = err
|
||||
}
|
||||
if err := enumerateRegistryUninstall(registry.CURRENT_USER, `SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`, seen); err != nil {
|
||||
_ = err
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath(resolveManagerPath("winget", interactiveProfile())); err == nil {
|
||||
if wingetPkgs, err := listWingetPackages(); err == nil {
|
||||
for _, p := range wingetPkgs {
|
||||
key := p.Name + "@" + p.Manager
|
||||
if _, ok := seen[key]; !ok {
|
||||
seen[key] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packages := make([]PackageInfo, 0, len(seen))
|
||||
for _, p := range seen {
|
||||
packages = append(packages, p)
|
||||
}
|
||||
return packages, nil
|
||||
}
|
||||
|
||||
func enumerateRegistryUninstall(root registry.Key, path string, seen map[string]PackageInfo) error {
|
||||
k, err := registry.OpenKey(root, path, registry.READ)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer k.Close()
|
||||
|
||||
subKeys, err := k.ReadSubKeyNames(0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, name := range subKeys {
|
||||
sk, err := registry.OpenKey(k, name, registry.READ)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
displayName, _, err := sk.GetStringValue("DisplayName")
|
||||
if err != nil || displayName == "" {
|
||||
sk.Close()
|
||||
continue
|
||||
}
|
||||
displayVersion, _, _ := sk.GetStringValue("DisplayVersion")
|
||||
version := strings.TrimSpace(displayVersion)
|
||||
if version == "" {
|
||||
version = "unknown"
|
||||
}
|
||||
|
||||
key := displayName + "@windows"
|
||||
if _, ok := seen[key]; !ok {
|
||||
seen[key] = PackageInfo{
|
||||
Name: displayName,
|
||||
Version: version,
|
||||
Manager: "windows",
|
||||
}
|
||||
}
|
||||
sk.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BootstrapManagers() error {
|
||||
var errs []string
|
||||
|
||||
if !lookupWinget() {
|
||||
if err := installWingetViaAppInstaller(); err != nil {
|
||||
errs = append(errs, "winget: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("choco"); err != nil {
|
||||
if err := installChocolatey(); err != nil {
|
||||
errs = append(errs, "chocolatey: "+err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("bootstrap failed: %s", strings.Join(errs, "; "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func installChocolatey() error {
|
||||
// Official Chocolatey install script (requires admin; agent runs as LocalSystem).
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command",
|
||||
"Set-ExecutionPolicy Bypass -Scope Process -Force; "+
|
||||
"[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; "+
|
||||
"iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("chocolatey install script failed: %w\nOutput: %s", err, string(out))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func lookupWinget() bool {
|
||||
if _, err := exec.LookPath(resolveManagerPath("winget", interactiveProfile())); err == nil {
|
||||
return true
|
||||
}
|
||||
appInstaller := `Microsoft.DesktopAppInstaller_8wekyb3d8bbwe`
|
||||
appxReg, err := registry.OpenKey(registry.LOCAL_MACHINE, `SOFTWARE\Microsoft\Windows\CurrentVersion\Appx\AppxAllUserStore`,
|
||||
registry.READ|registry.ENUMERATE_SUB_KEYS)
|
||||
if err == nil {
|
||||
defer appxReg.Close()
|
||||
if keys, err := appxReg.ReadSubKeyNames(0); err == nil {
|
||||
for _, keyName := range keys {
|
||||
if strings.Contains(keyName, "DesktopAppInstaller") {
|
||||
_ = appInstaller
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func installWingetViaAppInstaller() error {
|
||||
// Microsoft App Installer download link (redirects to latest winget bundle)
|
||||
installerURL := "https://aka.ms/getwinget"
|
||||
appInstallerPath := filepath.Join(os.TempDir(), "DesktopAppInstaller.msixbundle")
|
||||
|
||||
// Download App Installer bundle
|
||||
downloadCmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command",
|
||||
"[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; "+
|
||||
"Invoke-WebRequest -Uri '"+installerURL+"' -OutFile '"+appInstallerPath+"' -UseBasicParsing")
|
||||
if out, err := downloadCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("download App Installer failed: %w\nOutput: %s", err, string(out))
|
||||
}
|
||||
|
||||
// Install bundle (dependencies may need to be allowed; add-AppxPackage on msixbundle handles it)
|
||||
installCmd := exec.Command("powershell", "-NoProfile", "-NonInteractive", "-Command",
|
||||
"Add-AppxPackage -Path '"+appInstallerPath+"' -ForceUpdateFromAnyVersion")
|
||||
if out, err := installCmd.CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("install App Installer failed: %w\nOutput: %s", err, string(out))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user