Files
Scott GressandIan Littman 8e98a1b65b Add aliases to fleetctl commands and flags (#40548)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40598

# Details

This PR updates `fleetctl` with new commands and flag names with "team"
and "query" terminology replaced with "fleet" and "report", using
aliases for backwards compatibility and logging deprecation warnings
when the old terminology is used.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
n/a

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually
- [x] `fleetctl query` -> `fleetctl report`
- [x] using `fleetctl query` logs a deprecation warning
---
- [x] `fleetctl get queries` -> `fleetctl get reports`
- [x] using `fleetctl get queries` logs a deprecation warning
---
- [x] `fleetctl get teams` -> `fleetctl get fleets`
- [x] using `fleetctl get teams` logs a deprecation warning
---
- [ ] `fleetctl apply --policies-teams` -> `fleetctl apply
--policies-fleets`
- [ ] using `fleetctl apply --policies-teams` logs a deprecation warning
---
- [x] `fleetctl get --with-queries` -> `fleetctl get --with-reports`
- [x] using `fleetctl get --with-queries` logs a deprecation warning
---
- [x] `fleetctl gitops --delete-other-teams` -> `fleetctl gitops
--delete-other-fleets`
- [x] using `fleetctl gitops --delete-other-teams` logs a deprecation
warning
---
- [x] `fleetctl report --query-name` -> `fleetctl report --report-name`
- [x] using `fleetctl report --query-name` logs a deprecation warning


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

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Deprecation warnings for legacy command and flag names to help users
transition to current terminology.

* **Changes**
* "Team" terminology updated to "Fleet" across commands and associated
flags throughout the CLI.
* "Queries" terminology updated to "Reports" in get and related
commands.
* All flag name changes maintain backward compatibility through aliases
for existing automation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Ian Littman <iansltx@gmail.com>
2026-02-27 08:38:29 -06:00

240 lines
5.4 KiB
Go

package fleetctl
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"github.com/fleetdm/fleet/v4/pkg/spec"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/ghodss/yaml"
"github.com/urfave/cli/v2"
)
// mappings based on https://github.com/osquery/osquery/blob/b87a4b5f1567415a72acd5ecd0e9e7ab75754959/tools/codegen/genwebsitejson.py#L38C18-L38C18
var platformMapping = map[string][]string{
"darwin": {"darwin"},
"linux": {"linux"},
"windows": {"windows"},
"chrome": {"chrome"},
"specs": {"darwin", "linux", "windows"},
"utility": {"darwin", "linux", "windows"},
"yara": {"darwin", "linux", "windows"},
"smart": {"darwin", "linux"},
"kernel": {"darwin"},
"linwin": {"linux", "windows"},
"macwin": {"darwin", "windows"},
"posix": {"darwin", "linux"},
"sleuthkit": {"darwin", "linux"},
"any": {""},
"all": {""},
"": {""},
}
func convertPlatforms(platformsIn string) (string, error) {
splitPlatformsIn := strings.Split(platformsIn, ",")
// validate and convert each substring
mapped := map[string]struct{}{} // use a set to dedupe
for _, substring := range splitPlatformsIn {
mappedSubstring, ok := platformMapping[substring]
// validate substring
if !ok {
return "", fmt.Errorf("unsupported platform: %s", substring)
}
for _, p := range mappedSubstring {
mapped[p] = struct{}{}
}
}
// convert set to slice
result := make([]string, 0, len(mapped))
for p := range mapped {
result = append(result, p)
}
// sort for deterministic output
sort.Strings(result)
resultString := strings.Join(result, ",")
return resultString, nil
}
func specGroupFromPack(name string, inputPack fleet.PermissivePackContent) (*spec.Group, error) {
specs := &spec.Group{
Queries: []*fleet.QuerySpec{},
}
// this ensures order is consistent in output
keys := make([]string, len(inputPack.Queries))
i := 0
for k := range inputPack.Queries {
keys[i] = k
i++
}
sort.Strings(keys)
for _, name := range keys {
query := inputPack.Queries[name]
// get the interval as uint from a variety of possible types
interval := uint(0)
switch i := query.Interval.(type) {
case string:
u64, err := strconv.ParseUint(i, 10, 32)
if err != nil {
return nil, fmt.Errorf("converting interval from string to uint: %w", err)
}
interval = uint(u64)
case uint:
interval = i
case float64:
interval = uint(i)
}
// handle nil query.Platform
var queryPlatforms string
if query.Platform != nil {
queryPlatforms = *query.Platform
}
convertedPlatforms, err := convertPlatforms(queryPlatforms)
if err != nil {
return nil, err
}
// handle nil query.Version
var minOsqueryVersion string
if query.Version != nil {
minOsqueryVersion = *query.Version
}
spec := &fleet.QuerySpec{
Name: name,
Description: query.Description,
Query: query.Query,
Interval: interval,
Platform: convertedPlatforms,
MinOsqueryVersion: minOsqueryVersion,
}
specs.Queries = append(specs.Queries, spec)
}
return specs, nil
}
func convertCommand() *cli.Command {
var (
flFilename string
outputFilename string
)
return &cli.Command{
Name: "convert",
Usage: "Convert osquery packs into Fleet reports",
UsageText: `fleetctl convert [options]`,
Flags: []cli.Flag{
configFlag(),
contextFlag(),
&cli.StringFlag{
Name: "f",
EnvVars: []string{"FILENAME"},
Value: "",
Destination: &flFilename,
Usage: "A file to apply",
},
&cli.StringFlag{
Name: "o",
EnvVars: []string{"OUTPUT_FILENAME"},
Value: "",
Destination: &outputFilename,
Usage: "The name of the file to output converted results",
},
},
Action: func(c *cli.Context) error {
if flFilename == "" {
return errors.New("-f must be specified")
}
b, err := os.ReadFile(flFilename)
if err != nil {
return err
}
// Remove any literal newlines (because they are not
// valid JSON but osquery accepts them) and replace
// with \n so that we get them in the YAML output where
// they are allowed.
re := regexp.MustCompile(`\s*\\\n`)
b = re.ReplaceAll(b, []byte(`\n`))
var specs *spec.Group
var pack fleet.PermissivePackContent
if err := json.Unmarshal(b, &pack); err != nil {
return err
}
base := filepath.Base(flFilename)
specs, err = specGroupFromPack(strings.TrimSuffix(base, filepath.Ext(base)), pack)
if err != nil {
return err
}
if specs == nil {
return errors.New("could not parse files")
}
var w io.Writer = os.Stdout
if outputFilename != "" {
file, err := os.Create(outputFilename)
if err != nil {
return err
}
defer file.Close()
w = file
}
for _, query := range specs.Queries {
specBytes, err := json.Marshal(query)
if err != nil {
return err
}
var raw any
if err := json.Unmarshal(specBytes, &raw); err != nil {
return err
}
replaceAliasKeys(raw, aliasRules, true)
specBytes, err = json.Marshal(raw)
if err != nil {
return err
}
meta := spec.Metadata{
Kind: fleet.ReportKind,
Version: fleet.ApiVersion,
Spec: specBytes,
}
out, err := yaml.Marshal(meta)
if err != nil {
return err
}
fmt.Fprintln(w, "---")
fmt.Fprint(w, string(out))
}
return nil
},
}
}