Files
George Karr a25ae3ddfc Adding initial tool - dibble the tapir for seeding data (#46122)
## Overview

`dibble` is a one-stop CLI for seeding a Fleet server with test data —
users, teams, policies, reports, labels, scripts, MDM profiles,
software, secrets, CAs, and vulns — replacing ~8 ad-hoc seeding tools
with a single binary.

It makes it easy to:
- **Spin up a populated dev/test server in one command** — `dibble all`
plants everything with sensible, idempotent defaults.
- **Skip the flag-memorization** — running `dibble` with no args
launches an interactive wizard that prompts for Fleet URL, API token,
theme, and which entities to seed, and offers to save the config to
`~/.dibble.yaml`.
- **Seed individual entity types** — `dibble users`, `dibble teams`,
`dibble policies`, etc., when you only need one slice.
- **Get themed, recognizable test data** — pick a theme (hitchhikers,
tng, lotr, ghibli, parksrec, …) so seeded names are easy to eyeball in
the UI.

Hosts are intentionally out of scope — `cmd/osquery-perf` still owns
that. `dibble hosts` is a thin convenience wrapper that picks a fleet,
fetches its enroll secret, and prints/runs the osquery-perf invocation
for you.

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

* **New Features**
* Dibble: a CLI tool to seed realistic test data, including an
interactive wizard and subcommands for
teams/users/software/policies/scripts/reports/profiles/labels/activities/enroll-secrets/hosts/vulns,
plus theme-driven “cas” and “ping”.
* Theme system: multiple curated themes to generate consistent seeded
identities, policies, software, labels, and scripts.
* **Chores**
* Ignored the built dibble binary and added a Makefile build target to
compile the dibble tool.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-06-17 11:07:05 -05:00

230 lines
7.5 KiB
Go

package command
import (
cryptorand "crypto/rand"
"encoding/hex"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/fleetdm/fleet/v4/tools/dibble/pkg/themes"
)
// Build-time variables (set via -ldflags).
var (
version = "dev"
commit = "none"
date = "unknown"
)
// Execute is the entry point used by cmd/dibble/main.go. It constructs the
// root cobra command, routes no-arg invocations through the wizard, and runs
// whichever path was chosen. Returns a non-nil error on failure.
func Execute(args []string) error {
rootCmd := newRootCmd()
if shouldRunWizard(args) {
// Parse persistent flags so values like
// dibble --fleet-url X --api-token Y
// are honored by the wizard (and bound into viper via
// BindPFlag in newRootCmd). Without this, the wizard would
// prompt for values the user already provided.
if err := rootCmd.ParseFlags(args); err != nil {
return err
}
return runWizard(rootCmd)
}
return rootCmd.Execute()
}
// shouldRunWizard reports whether `dibble` was invoked with no subcommand,
// in which case we drop into the interactive wizard. Flags-only invocations
// (e.g. `dibble --fleet-url X`) still trigger the wizard so it can fill in
// any missing config.
func shouldRunWizard(args []string) bool {
for _, a := range args {
switch a {
case "help", "--help", "-h", "completion", "--version":
return false
}
// First non-flag positional → a subcommand was given.
if len(a) > 0 && a[0] != '-' {
return false
}
if a == "--no-wizard" {
return false
}
}
return true
}
// Config keys (viper).
const (
keyFleetURL = "fleet_url"
keyAPIToken = "api_token"
keyTheme = "theme"
keyInsecure = "insecure"
keySuffix = "suffix"
)
// newRootCmd builds the root cobra command and wires viper.
//
// Configuration precedence (highest first):
// 1. Command-line flag
// 2. Environment variable (FLEET_URL, FLEET_API_TOKEN, DIBBLE_THEME)
// 3. ~/.dibble.yaml
// 4. Built-in defaults
func newRootCmd() *cobra.Command {
cobra.OnInitialize(initConfig)
root := &cobra.Command{
Use: "dibble",
Short: "Fleet's seed slinger — plant test data into a Fleet server",
Long: `dibble is the one-stop CLI for seeding test data into a Fleet server.
Run it with no arguments for an interactive wizard, or pass a subcommand to
script it.
Hosts are intentionally out of scope — use cmd/osquery-perf for that.`,
SilenceUsage: true,
// Errors are surfaced via reportErrors (warnf per error) or main.go
// (one "dibble: <err>" line for everything else). Cobra's own
// "Error: <err>" print would duplicate them.
SilenceErrors: true,
}
// Persistent flags (apply to every subcommand).
root.PersistentFlags().StringP("fleet-url", "u", "", "Fleet server URL (env FLEET_URL)")
root.PersistentFlags().StringP("api-token", "a", "", "Fleet API token (env FLEET_API_TOKEN)")
root.PersistentFlags().StringP("config", "c", "", "Config file (default $HOME/.dibble.yaml)")
root.PersistentFlags().StringP("theme", "t", "mix", "Easter-egg theme: mix, hitchhikers, goodplace, parksrec, tng, lotr, dbz, robin_williams, ghibli, cosmere, sailor_moon")
root.PersistentFlags().BoolP("dry-run", "d", false, "Print actions without calling the Fleet API")
root.PersistentFlags().BoolP("verbose", "v", false, "Verbose logging")
root.PersistentFlags().Bool("no-wizard", false, "Disable the interactive wizard (always error on missing config)")
root.PersistentFlags().BoolP("insecure", "k", false, "Skip TLS certificate verification (for self-signed dev certs)")
root.PersistentFlags().StringP("suffix", "s", "", `Append to every generated name to avoid collisions on re-run. Use "auto" for a random 4-char suffix per run.`)
// Bind to viper so env / config-file values flow through. The
// config/dry-run/verbose flags are read via viper.Get* in initConfig
// and the HTTP client; without explicit binding they'd silently
// default to zero values regardless of what the user passed.
_ = viper.BindPFlag(keyFleetURL, root.PersistentFlags().Lookup("fleet-url"))
_ = viper.BindPFlag(keyAPIToken, root.PersistentFlags().Lookup("api-token"))
_ = viper.BindPFlag(keyTheme, root.PersistentFlags().Lookup("theme"))
_ = viper.BindPFlag(keyInsecure, root.PersistentFlags().Lookup("insecure"))
_ = viper.BindPFlag(keySuffix, root.PersistentFlags().Lookup("suffix"))
_ = viper.BindPFlag("config", root.PersistentFlags().Lookup("config"))
_ = viper.BindPFlag("dry-run", root.PersistentFlags().Lookup("dry-run"))
_ = viper.BindPFlag("verbose", root.PersistentFlags().Lookup("verbose"))
// Env mapping. FLEET_URL / FLEET_API_TOKEN feel natural to people coming
// from fleetctl, even though the dibble prefix would be more consistent.
viper.SetEnvPrefix("DIBBLE")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.AutomaticEnv()
_ = viper.BindEnv(keyFleetURL, "FLEET_URL", "DIBBLE_FLEET_URL")
_ = viper.BindEnv(keyAPIToken, "FLEET_API_TOKEN", "DIBBLE_API_TOKEN")
// Subcommands.
root.AddCommand(newVersionCmd())
root.AddCommand(newPingCmd())
root.AddCommand(newAllCmd())
root.AddCommand(newUsersCmd())
root.AddCommand(newTeamsCmd())
root.AddCommand(newPoliciesCmd())
root.AddCommand(newReportsCmd())
root.AddCommand(newLabelsCmd())
root.AddCommand(newScriptsCmd())
root.AddCommand(newProfilesCmd())
root.AddCommand(newSoftwareCmd())
root.AddCommand(newEnrollSecretsCmd())
root.AddCommand(newCAsCmd())
root.AddCommand(newVulnsCmd())
root.AddCommand(newActivitiesCmd())
root.AddCommand(newHostsCmd())
root.AddCommand(newIDPCmd())
return root
}
func initConfig() {
if cfgFile := viper.GetString("config"); cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := os.UserHomeDir()
if err != nil {
return
}
viper.AddConfigPath(home)
viper.SetConfigName(".dibble")
viper.SetConfigType("yaml")
}
_ = viper.ReadInConfig() // missing config file is fine — wizard will help.
}
// configPath returns the path used to persist wizard answers.
func configPath() string {
if p := viper.ConfigFileUsed(); p != "" {
return p
}
home, err := os.UserHomeDir()
if err != nil {
return ".dibble.yaml"
}
return filepath.Join(home, ".dibble.yaml")
}
func requireConfig() error {
if viper.GetString(keyFleetURL) == "" {
return fmt.Errorf("missing --fleet-url (or FLEET_URL, or ~/.dibble.yaml). Run `dibble` with no args for the wizard")
}
if viper.GetString(keyAPIToken) == "" {
return fmt.Errorf("missing --api-token (or FLEET_API_TOKEN, or ~/.dibble.yaml). Run `dibble` with no args for the wizard")
}
return nil
}
// currentTheme returns the theme selected by viper with the suffix applied.
// "auto" expands to a 4-character random hex suffix that's stable for the
// life of this process so every entity in the run shares the same tag.
func currentTheme() (themes.Theme, error) {
t, err := themes.Get(viper.GetString(keyTheme))
if err != nil {
return themes.Theme{}, err
}
suffix := viper.GetString(keySuffix)
if suffix == "auto" {
suffix = autoSuffix()
}
t.Suffix = suffix
return t, nil
}
var cachedAutoSuffix string
func autoSuffix() string {
if cachedAutoSuffix != "" {
return cachedAutoSuffix
}
var b [2]byte
if _, err := cryptorand.Read(b[:]); err != nil {
cachedAutoSuffix = "x"
return cachedAutoSuffix
}
cachedAutoSuffix = hex.EncodeToString(b[:])
return cachedAutoSuffix
}
func newVersionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print dibble version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("dibble %s (commit %s, built %s)\n", version, commit, date)
fmt.Println("— Dibble the Tapir 🌱")
},
}
}