mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
300 lines
8.1 KiB
Go
300 lines
8.1 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bufio"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
|
|
"github.com/fleetdm/fleet/v4/pkg/download"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
)
|
|
|
|
var (
|
|
rxOption = regexp.MustCompile(`\-\-(\w+)\s`)
|
|
osqueryVersion = "5.22.1"
|
|
|
|
structTpl = template.Must(template.New("struct").Funcs(template.FuncMap{
|
|
"camelCase": camelCaseOptionName,
|
|
}).Parse(`// Automatically generated by tools/osquery-agent-options for osquery {{ .OsqueryVersion }}. DO NOT EDIT!
|
|
// To update flags for a new osquery version, update the osqueryVersion variable in
|
|
// "tools/osquery-agent-options/main.go" and run "cd server/fleet/ && go generate".
|
|
package fleet
|
|
|
|
type osqueryOptions struct { {{ range $name, $type := .Options }}
|
|
{{camelCase $name}} {{$type}} ` + "`json:\"{{$name}}\"`" + `{{end}}
|
|
|
|
// embed the os-specific structs
|
|
OsqueryCommandLineFlagsLinux
|
|
OsqueryCommandLineFlagsWindows
|
|
OsqueryCommandLineFlagsMacOS
|
|
OsqueryCommandLineFlagsHidden
|
|
}
|
|
|
|
type osqueryCommandLineFlags struct { {{ range $name, $type := .Flags }}
|
|
{{camelCase $name}} {{$type}} ` + "`json:\"{{$name}}\"`" + `{{end}}
|
|
|
|
// embed the os-specific structs
|
|
OsqueryCommandLineFlagsLinux
|
|
OsqueryCommandLineFlagsWindows
|
|
OsqueryCommandLineFlagsMacOS
|
|
OsqueryCommandLineFlagsHidden
|
|
}
|
|
`))
|
|
)
|
|
|
|
type templateData struct {
|
|
OsqueryVersion string
|
|
Options map[string]string
|
|
Flags map[string]string
|
|
}
|
|
|
|
func main() {
|
|
fmt.Printf("Generating osquery flags for version: %s\n", osqueryVersion)
|
|
if runtime.GOOS != "darwin" {
|
|
log.Fatal("Currently only supported on macOS")
|
|
}
|
|
urlStr := fmt.Sprintf("https://updates.fleetdm.com/targets/osqueryd/macos-app/%s/osqueryd.app.tar.gz", osqueryVersion)
|
|
osqueryTUFURL, err := url.Parse(urlStr)
|
|
if err != nil {
|
|
log.Fatalf("parse osquery TUF URL: %q: %s", urlStr, err)
|
|
}
|
|
tmpDir, err := os.MkdirTemp("", "")
|
|
if err != nil {
|
|
log.Fatalf("create temp dir: %s", err)
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
osquerydAppTarGzPath := filepath.Join(tmpDir, "osqueryd.app.tar.gz")
|
|
if err := download.Download(http.DefaultClient, osqueryTUFURL, osquerydAppTarGzPath); err != nil {
|
|
log.Fatalf("download osqueryd.app.tar.gz to %s: %s", osquerydAppTarGzPath, err) //nolint:gocritic // ignore exitAfterDefer
|
|
}
|
|
if err := extractTarGz(osquerydAppTarGzPath); err != nil {
|
|
log.Fatalf("extract tar.gz %q: %s", osquerydAppTarGzPath, err)
|
|
}
|
|
osquerydPath := filepath.Join(filepath.Dir(osquerydAppTarGzPath), "osquery.app", "Contents", "MacOS", "osqueryd")
|
|
|
|
// marshal/unmarshal the OS-specific structs into a map so we have all their
|
|
// keys and we can ignore them in the auto-generated structs (because we
|
|
// can't auto- generate those, we'd only see the ones that exist on the
|
|
// current OS)
|
|
var allOSSpecific struct {
|
|
fleet.OsqueryCommandLineFlagsLinux
|
|
fleet.OsqueryCommandLineFlagsWindows
|
|
fleet.OsqueryCommandLineFlagsMacOS
|
|
fleet.OsqueryCommandLineFlagsHidden
|
|
}
|
|
b, err := json.Marshal(allOSSpecific)
|
|
if err != nil {
|
|
log.Fatalf("failed to marshal os-specific structs: %v", err)
|
|
}
|
|
|
|
var osSpecificNames map[string]interface{}
|
|
if err := json.Unmarshal(b, &osSpecificNames); err != nil {
|
|
log.Fatalf("failed to unmarshal os-specific structs to get the list of keys: %v", err)
|
|
}
|
|
|
|
// get the list of flags that are valid as configuration options
|
|
b, err = exec.Command(osquerydPath, "--help").Output()
|
|
if err != nil {
|
|
log.Fatalf("failed to run osqueryd --help: %v", err)
|
|
}
|
|
|
|
var optionsStarted, optionsSeen, optionsDone bool
|
|
var optionNames, allNames []string
|
|
|
|
s := bufio.NewScanner(bytes.NewReader(b))
|
|
for s.Scan() {
|
|
line := s.Text()
|
|
|
|
if !optionsStarted {
|
|
if strings.Contains(line, "osquery configuration options") {
|
|
optionsStarted = true
|
|
continue
|
|
}
|
|
}
|
|
|
|
if line == "" {
|
|
if optionsSeen {
|
|
// we're done for options, empty line after an option has been seen
|
|
optionsDone = true
|
|
}
|
|
continue
|
|
}
|
|
|
|
matches := rxOption.FindStringSubmatch(line)
|
|
if matches == nil {
|
|
continue
|
|
}
|
|
|
|
if optionsStarted && !optionsDone {
|
|
optionsSeen = true
|
|
optionNames = append(optionNames, matches[1])
|
|
}
|
|
allNames = append(allNames, matches[1])
|
|
}
|
|
if err := s.Err(); err != nil {
|
|
log.Fatalf("failed to read osqueryd --help output: %v", err)
|
|
}
|
|
|
|
// find the data type for each option
|
|
var optionTypes []struct {
|
|
Name string
|
|
Type string
|
|
}
|
|
b, err = exec.Command(osquerydPath, "-S", "--json", "SELECT name, type FROM osquery_flags").Output()
|
|
if err != nil {
|
|
log.Fatalf("failed to run osqueryi query: %v", err)
|
|
}
|
|
if err := json.Unmarshal(b, &optionTypes); err != nil {
|
|
log.Fatalf("failed to unmarshal osqueryi query output: %v", err)
|
|
}
|
|
|
|
// index the results by name
|
|
allOptions := make(map[string]string, len(optionTypes))
|
|
for _, nt := range optionTypes {
|
|
allOptions[nt.Name] = nt.Type
|
|
}
|
|
|
|
// identify the valid config options
|
|
validOptions := make(map[string]string, len(optionNames))
|
|
for _, nm := range optionNames {
|
|
// ignore the os-specific options
|
|
if _, ok := osSpecificNames[nm]; ok {
|
|
continue
|
|
}
|
|
|
|
ot, ok := allOptions[nm]
|
|
if ok {
|
|
validOptions[nm] = ot
|
|
}
|
|
}
|
|
// identify the valid command-line flags
|
|
validFlags := make(map[string]string, len(allNames))
|
|
for _, nm := range allNames {
|
|
// ignore the os-specific options
|
|
if _, ok := osSpecificNames[nm]; ok {
|
|
continue
|
|
}
|
|
|
|
ot, ok := allOptions[nm]
|
|
if ok {
|
|
validFlags[nm] = ot
|
|
}
|
|
}
|
|
|
|
outputFilePath := os.Args[1]
|
|
outputFile, err := os.OpenFile(outputFilePath, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0o644) // nolint:gosec // G302
|
|
if err != nil {
|
|
log.Fatalf("open output file %q: %s", outputFilePath, err)
|
|
}
|
|
defer outputFile.Close()
|
|
|
|
if err := structTpl.Execute(outputFile, templateData{
|
|
OsqueryVersion: osqueryVersion,
|
|
Options: validOptions,
|
|
Flags: validFlags,
|
|
}); err != nil {
|
|
log.Fatalf("failed to execute template: %v", err)
|
|
}
|
|
|
|
if err := outputFile.Close(); err != nil {
|
|
log.Fatalf("close file %q: %s", outputFilePath, err)
|
|
}
|
|
}
|
|
|
|
func camelCaseOptionName(s string) string {
|
|
parts := strings.Split(s, "_")
|
|
for i, p := range parts {
|
|
parts[i] = strings.Title(p)
|
|
}
|
|
return strings.Join(parts, "")
|
|
}
|
|
|
|
// sanitizeArchivePath sanitizes the archive file pathing from "G305: Zip Slip vulnerability"
|
|
func sanitizeArchivePath(d, t string) (string, error) {
|
|
v := filepath.Join(d, t)
|
|
if strings.HasPrefix(v, filepath.Clean(d)) {
|
|
return v, nil
|
|
}
|
|
|
|
return "", fmt.Errorf("%s: %s", "content filepath is tainted", t)
|
|
}
|
|
|
|
// extractTagGz extracts the contents of the provided tar.gz file.
|
|
func extractTarGz(path string) error {
|
|
tarGzFile, err := os.Open(path)
|
|
if err != nil {
|
|
return fmt.Errorf("open %q: %w", path, err)
|
|
}
|
|
defer tarGzFile.Close()
|
|
|
|
gzipReader, err := gzip.NewReader(tarGzFile)
|
|
if err != nil {
|
|
return fmt.Errorf("gzip reader %q: %w", path, err)
|
|
}
|
|
defer gzipReader.Close()
|
|
|
|
tarReader := tar.NewReader(gzipReader)
|
|
for {
|
|
header, err := tarReader.Next()
|
|
switch {
|
|
case err == nil:
|
|
// OK
|
|
case errors.Is(err, io.EOF):
|
|
return nil
|
|
default:
|
|
return fmt.Errorf("tar reader %q: %w", path, err)
|
|
}
|
|
|
|
// Prevent zip-slip attack.
|
|
if strings.Contains(header.Name, "..") {
|
|
return fmt.Errorf("invalid path in tar.gz: %q", header.Name)
|
|
}
|
|
|
|
targetPath, err := sanitizeArchivePath(filepath.Dir(path), header.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("sanitize failed: %s", err)
|
|
}
|
|
|
|
switch header.Typeflag {
|
|
case tar.TypeDir:
|
|
if err := os.MkdirAll(targetPath, constant.DefaultDirMode); err != nil {
|
|
return fmt.Errorf("mkdir %q: %w", header.Name, err)
|
|
}
|
|
case tar.TypeReg:
|
|
err := func() error {
|
|
outFile, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY, header.FileInfo().Mode())
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create %q: %w", header.Name, err)
|
|
}
|
|
defer outFile.Close()
|
|
|
|
// Ignoring G110 because we are using this on tooling.
|
|
if _, err := io.Copy(outFile, tarReader); err != nil { //nolint:gosec
|
|
return fmt.Errorf("failed to copy %q: %w", header.Name, err)
|
|
}
|
|
return nil
|
|
}()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown flag type %q: %d", header.Name, header.Typeflag)
|
|
}
|
|
}
|
|
}
|