fleet/cmd/maintained-apps/validate/windows.go
Konstantin Sykulev b1a392d672
FMA test automation (#31210)
For #29183

# Checklist for submitter

- [x] Added/updated automated tests
- [x] Manual QA for all new/changed functionality


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced automated validation workflows for maintained applications
on both macOS and Windows, ensuring apps can be installed, verified, and
uninstalled as expected.
* Added new command-line tool to validate maintained apps, providing
detailed reporting on validation results.
* Enhanced detection and handling of pre-installed applications during
validation.
* Improved post-installation steps for macOS, including quarantine
removal and system refresh.

* **Chores**
* Added new continuous integration workflows to automate application
validation on pull requests for relevant files.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-07-31 15:23:36 -05:00

126 lines
3.7 KiB
Go

//go:build windows
package main
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"time"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
"github.com/fleetdm/fleet/v4/server/fleet"
queries "github.com/fleetdm/fleet/v4/server/service/osquery_utils"
kitlog "github.com/go-kit/log"
"github.com/go-kit/log/level"
)
var preInstalled = []string{}
func postApplicationInstall(_ kitlog.Logger, _ string) error {
return nil
}
func appExists(ctx context.Context, logger kitlog.Logger, appName, _, appVersion, appPath string) (bool, error) {
execTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
if err := validateSqlInput(appName); err != nil {
return false, fmt.Errorf("Invalid character found in appName: '%w'. Not executing query...", err)
}
if err := validateSqlInput(appPath); err != nil {
return false, fmt.Errorf("Invalid character found in appPath: '%w'. Not executing query...", err)
}
level.Info(logger).Log("msg", fmt.Sprintf("Looking for app: %s, version: %s", appName, appVersion))
query := `
SELECT name, install_location, version
FROM programs
WHERE
LOWER(name) LIKE LOWER('%` + appName + `%')
`
if appPath != "" {
query += fmt.Sprintf(" OR install_location LIKE '%%%s%%'", appPath)
}
cmd := exec.CommandContext(execTimeout, "osqueryi", "--json", query)
output, err := cmd.CombinedOutput()
if err != nil {
level.Error(logger).Log("msg", fmt.Sprintf("osquery output: %s", string(output)))
return false, fmt.Errorf("executing osquery command: %w", err)
}
type AppResult struct {
Name string `json:"name"`
InstallLocation string `json:"install_location"`
Version string `json:"version"`
}
var results []AppResult
if err := json.Unmarshal(output, &results); err != nil {
level.Error(logger).Log("msg", fmt.Sprintf("osquery output: %s", string(output)))
return false, fmt.Errorf("parsing osquery JSON output: %w", err)
}
if len(results) > 0 {
for _, result := range results {
software := &fleet.Software{
Name: result.Name,
Version: result.Version,
Source: "programs",
}
queries.MutateSoftwareOnIngestion(software, logger)
result.Version = software.Version
result.Name = software.Name
level.Info(logger).Log("msg", fmt.Sprintf("Found app: '%s' at %s, Version: %s", result.Name, result.InstallLocation, result.Version))
if result.Version == appVersion {
return true, nil
}
}
}
return false, nil
}
func executeScript(cfg *Config, scriptContents string) (string, error) {
scriptExtension := ".ps1"
scriptPath := filepath.Join(cfg.tmpDir, "script"+scriptExtension)
if err := os.WriteFile(scriptPath, []byte(scriptContents), constant.DefaultFileMode); err != nil {
return "", fmt.Errorf("writing script: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
// Use custom execution with non-interactive flags for Windows
cmd := exec.CommandContext(ctx, "powershell", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath)
cmd.WaitDelay = 1 * time.Minute
cmd.Env = cfg.env
cmd.Dir = filepath.Dir(scriptPath)
output, err := cmd.CombinedOutput()
exitCode := -1
// Only set exitCode if process completed and context wasn't cancelled
if cmd.ProcessState != nil {
// see orbit/pkg/scripts/exec_windows.go
// https://en.wikipedia.org/wiki/Exit_status#Windows
exitCode = int(int32(cmd.ProcessState.ExitCode())) // nolint:gosec
}
result := fmt.Sprintf(`
--------------------
%s
--------------------`, string(output))
if err != nil {
return result, err
}
if exitCode != 0 {
return result, fmt.Errorf("script execution failed with exit code %d: %s", exitCode, string(output))
}
return result, nil
}