//go:build darwin || windows package main import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "os" "path" "path/filepath" "regexp" "runtime" "slices" "strings" "time" maintained_apps "github.com/fleetdm/fleet/v4/ee/maintained-apps" "github.com/fleetdm/fleet/v4/pkg/file" "github.com/fleetdm/fleet/v4/server/fleet" mdm_maintained_apps "github.com/fleetdm/fleet/v4/server/mdm/maintainedapps" kitlog "github.com/go-kit/log" "github.com/go-kit/log/level" ) type Config struct { tmpDir string env []string installationSearchDirectory string operatingSystem string logger kitlog.Logger logLevel string inputsPath string outputsPath string } func run(cfg *Config) error { apps, err := getListOfApps(cfg.outputsPath) if err != nil { level.Error(cfg.logger).Log("msg", fmt.Sprintf("Error getting list of apps: %v", err)) return err } cfg.tmpDir, err = os.MkdirTemp("", "fma-validate-") if err != nil { level.Error(cfg.logger).Log("msg", fmt.Sprintf("Error creating temporary directory: %v", err)) return err } defer func() { err := os.RemoveAll(cfg.tmpDir) if err != nil { level.Error(cfg.logger).Log("msg", fmt.Sprintf("warning failed to remove temporary directory: %v", err)) } }() ctx := context.Background() totalApps := 0 successfulApps := 0 appWithError := []string{} appWithWarning := []string{} frozenApps := []string{} for _, app := range apps { if app.Platform != cfg.operatingSystem { continue } totalApps++ level.Info(cfg.logger).Log("msg", fmt.Sprintf("Validating app: %s (%s)", app.Name, app.Slug)) appLogger := kitlog.With(cfg.logger, "app", app.Name, ) ac := &AppCommander{cfg: cfg, appLogger: appLogger} appJson, err := getAppJson(cfg.outputsPath, app.Slug) if err != nil { level.Error(appLogger).Log("msg", fmt.Sprintf("Error getting app json manifest: %v", err)) appWithError = append(appWithError, app.Name) continue } maintainedApp := appFromJson(appJson) ac.Name = app.Name ac.Slug = app.Slug ac.UniqueIdentifier = app.UniqueIdentifier // default version to maintained app version ac.Version = maintainedApp.Version ac.InstallScript = maintainedApp.InstallScript ac.UninstallScript = maintainedApp.UninstallScript isFrozen, err := ac.isFrozen() if err != nil { level.Error(appLogger).Log("msg", fmt.Sprintf("Error checking if app is frozen: %v", err)) appWithError = append(appWithError, ac.Name) continue } if isFrozen { level.Info(appLogger).Log("msg", "App is frozen, skipping validation...") frozenApps = append(frozenApps, ac.Name) continue } installerTFR, err := DownloadMaintainedApp(cfg, maintainedApp) if err != nil { level.Error(appLogger).Log("msg", fmt.Sprintf("Error downloading maintained app: %v", err)) appWithError = append(appWithError, ac.Name) continue } err = ac.extractAppVersion(installerTFR) if err != nil { level.Error(appLogger).Log("msg", fmt.Sprintf("Error extracting installer version: %v. Using '%s'", err, ac.Version)) appWithError = append(appWithError, ac.Name) } hash, err := file.SHA256FromTempFileReader(installerTFR) installerTFR.Close() if err != nil { level.Error(appLogger).Log("msg", fmt.Sprintf("Error checking if sha256 hash matches: %v", err)) appWithError = append(appWithError, ac.Name) continue } if hash != maintainedApp.SHA256 && maintainedApp.SHA256 != "no_check" { level.Error(appLogger).Log("msg", "SHA256 hash in manifest does not match installer file hash") appWithError = append(appWithError, ac.Name) continue } // If application is already installed, attempt to uninstall it if slices.Contains(preInstalled, ac.Slug) { ac.uninstallPreInstalled(ctx) } appPath, changerError, listError := ac.expectToChangeFileSystem( func() error { level.Info(appLogger).Log("msg", "Executing install script...") output, err := executeScript(cfg, ac.InstallScript) if err != nil { level.Error(appLogger).Log("msg", fmt.Sprintf("Error executing install script: %v", err)) level.Error(appLogger).Log("msg", fmt.Sprintf("Output: %s", output)) return err } level.Debug(appLogger).Log("msg", fmt.Sprintf("Output: %s", output)) return nil }, ) if listError != nil { level.Error(appLogger).Log("msg", fmt.Sprintf("Error listing directory contents: %v", listError)) } if changerError != nil { appWithError = append(appWithError, ac.Name) continue } ac.AppPath = appPath if ac.AppPath == "" { appWithWarning = append(appWithWarning, ac.Name) } err = postApplicationInstall(appLogger, ac.AppPath) if err != nil { level.Warn(appLogger).Log("msg", fmt.Sprintf("Error detected in post-installation steps: %v", err)) appWithWarning = append(appWithWarning, ac.Name) } existance, err := appExists(ctx, appLogger, ac.Name, ac.UniqueIdentifier, ac.Version, ac.AppPath) if err != nil { level.Error(appLogger).Log("msg", fmt.Sprintf("Error checking if app exists: %v", err)) appWithError = append(appWithError, ac.Name) continue } if !existance { level.Error(appLogger).Log("msg", fmt.Sprintf("App version '%s' was not found by osquery", ac.Version)) appWithError = append(appWithError, ac.Name) continue } // Uninstall uninstalled := ac.uninstallApp(ctx) if !uninstalled { appWithError = append(appWithError, ac.Name) continue } level.Info(cfg.logger).Log("msg", fmt.Sprintf("All checks passed for app: %s (%s)", ac.Name, ac.Slug)) successfulApps++ } if len(frozenApps) > 0 { level.Info(cfg.logger).Log("msg", fmt.Sprintf("Some apps were skipped: %v", frozenApps)) } if len(appWithWarning) > 0 { level.Warn(cfg.logger).Log("msg", fmt.Sprintf("Some apps were validated with warnings: %v", appWithWarning)) } if successfulApps == totalApps-len(frozenApps) { // All apps were successfully validated! level.Info(cfg.logger).Log("msg", fmt.Sprintf("All %d apps were successfully validated.", totalApps)) return nil } level.Info(cfg.logger).Log("msg", fmt.Sprintf("Validated %d out of %d apps successfully.", successfulApps, totalApps)) level.Info(cfg.logger).Log("msg", fmt.Sprintf("Apps with errors: %v", appWithError)) return errors.New("Some maintained apps failed validation") } func main() { cfg := &Config{} // logger cfg.logger = kitlog.NewLogfmtLogger(os.Stderr) cfg.logLevel = os.Getenv("LOG_LEVEL") if cfg.logLevel == "" { cfg.logLevel = "info" } var lvl level.Option switch strings.ToLower(cfg.logLevel) { case "debug": lvl = level.AllowDebug() case "error": lvl = level.AllowError() default: lvl = level.AllowInfo() } cfg.logger = level.NewFilter(cfg.logger, lvl) cfg.logger = kitlog.With(cfg.logger, "ts", kitlog.DefaultTimestampUTC, "caller", kitlog.DefaultCaller, ) // os detection cfg.operatingSystem = strings.ToLower(os.Getenv("GOOS")) if cfg.operatingSystem == "" { cfg.operatingSystem = runtime.GOOS level.Info(cfg.logger).Log("msg", fmt.Sprintf("GOOS environment variable is not set. Using system detected: '%s'", cfg.operatingSystem)) } if cfg.operatingSystem != "darwin" && cfg.operatingSystem != "windows" { level.Error(cfg.logger).Log("msg", fmt.Sprintf("Unsupported operating system: %s", cfg.operatingSystem)) os.Exit(1) } // installation directory detection cfg.installationSearchDirectory = os.Getenv("INSTALLATION_SEARCH_DIRECTORY") if cfg.installationSearchDirectory == "" { switch cfg.operatingSystem { case "darwin": cfg.installationSearchDirectory = "/Applications" case "windows": cfg.installationSearchDirectory = "C:\\Program Files" } level.Info(cfg.logger).Log("msg", fmt.Sprintf("INSTALLATION_SEARCH_DIRECTORY environment variable is not set. Using default: '%s'", cfg.installationSearchDirectory)) } // paths switch cfg.operatingSystem { case "darwin": cfg.inputsPath = "ee/maintained-apps/inputs/homebrew" case "windows": cfg.inputsPath = "ee\\maintained-apps\\inputs\\winget" } cfg.outputsPath = maintained_apps.OutputPath err := run(cfg) if err != nil { os.Exit(1) } } func getListOfApps(outputPath string) ([]maintained_apps.FMAListFileApp, error) { appListFilePath := path.Join(outputPath, "apps.json") inputJson, err := os.ReadFile(appListFilePath) if err != nil { return nil, fmt.Errorf("reading output apps list file: %w", err) } var outputAppsFile maintained_apps.FMAListFile if err := json.Unmarshal(inputJson, &outputAppsFile); err != nil { return nil, fmt.Errorf("unmarshaling output apps list file: %w", err) } return outputAppsFile.Apps, nil } func getAppJson(outputPath string, slug string) (*maintained_apps.FMAManifestFile, error) { appJsonFilePath := path.Join(outputPath, fmt.Sprintf("%s.json", slug)) inputJson, err := os.ReadFile(appJsonFilePath) if err != nil { return nil, fmt.Errorf("reading app '%s' json manifest: %w", slug, err) } var manifest maintained_apps.FMAManifestFile if err := json.Unmarshal(inputJson, &manifest); err != nil { return nil, fmt.Errorf("unmarshaling app '%s' json manifest: %w", slug, err) } return &manifest, nil } func appFromJson(manifest *maintained_apps.FMAManifestFile) fleet.MaintainedApp { var app fleet.MaintainedApp app.Version = manifest.Versions[0].Version app.Platform = manifest.Versions[0].Platform() app.InstallerURL = manifest.Versions[0].InstallerURL app.SHA256 = manifest.Versions[0].SHA256 app.InstallScript = manifest.Refs[manifest.Versions[0].InstallScriptRef] app.UninstallScript = manifest.Refs[manifest.Versions[0].UninstallScriptRef] app.AutomaticInstallQuery = manifest.Versions[0].Queries.Exists app.Categories = manifest.Versions[0].DefaultCategories return app } func DownloadMaintainedApp(cfg *Config, app fleet.MaintainedApp) (*fleet.TempFileReader, error) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() level.Info(cfg.logger).Log("msg", "Downloading...") installerTFR, filename, err := mdm_maintained_apps.DownloadInstaller(ctx, app.InstallerURL, http.DefaultClient) if err != nil { return nil, fmt.Errorf("downloading installer: %w", err) } // Create a file in tmpDir for the installer cleanFilename := filepath.Base(filename) if cleanFilename == "." || cleanFilename == ".." { cleanFilename = fmt.Sprintf("installer_%d", time.Now().UnixNano()) } filePath := filepath.Join(cfg.tmpDir, cleanFilename) out, err := os.Create(filePath) if err != nil { return nil, fmt.Errorf("creating file: %w", err) } defer out.Close() // Copy from TempFileReader to our file _, err = io.Copy(out, installerTFR) if err != nil { return nil, fmt.Errorf("failed to save file: %w", err) } // Rewind the TempFileReader for future use err = installerTFR.Rewind() if err != nil { return nil, fmt.Errorf("rewinding temp file: %w", err) } cfg.env = os.Environ() installerPathEnv := fmt.Sprintf("INSTALLER_PATH=%s", filePath) cfg.env = append(cfg.env, installerPathEnv) return installerTFR, nil } func listDirectoryContents(dir string) (map[string]struct{}, error) { entries, err := os.ReadDir(dir) if err != nil { return nil, fmt.Errorf("reading directory %s: %w", dir, err) } contents := make(map[string]struct{}) for _, entry := range entries { if entry.IsDir() && entry.Type()&os.ModeSymlink == 0 { contents[entry.Name()] = struct{}{} } } return contents, nil } var pathJoin = filepath.Join func filepathJoin(parts ...string) string { return pathJoin(parts...) } func detectApplicationChange(installationSearchDirectory string, appListPre, appListPost map[string]struct{}) (string, bool) { // Check for added applications for app := range appListPost { if _, exists := appListPre[app]; !exists { return filepathJoin(installationSearchDirectory, app), true // true = added } } // Check for removed applications for app := range appListPre { if _, exists := appListPost[app]; !exists { return filepathJoin(installationSearchDirectory, app), false // false = removed } } return "", false // no change detected } func validateSqlInput(input string) error { // Allow alphanumeric, spaces, dots, hyphens, underscores, forward/back slashes, colons, parentheses, pluses. if matched, _ := regexp.MatchString(`^[a-zA-Z0-9\s.\-_/\\:()+]*$`, input); !matched { return fmt.Errorf("invalid characters in input: %s", input) } return nil }