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.14.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://tuf.fleetctl.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) 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.OpenFile(path, os.O_RDONLY, 0o755) 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) } } }