github.com/crowdsecurity/crowdsec@v1.6.1/cmd/crowdsec-cli/itemcli.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"os"
     6  	"strings"
     7  
     8  	"github.com/fatih/color"
     9  	"github.com/hexops/gotextdiff"
    10  	"github.com/hexops/gotextdiff/myers"
    11  	"github.com/hexops/gotextdiff/span"
    12  	log "github.com/sirupsen/logrus"
    13  	"github.com/spf13/cobra"
    14  
    15  	"github.com/crowdsecurity/go-cs-lib/coalesce"
    16  
    17  	"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
    18  	"github.com/crowdsecurity/crowdsec/pkg/cwhub"
    19  )
    20  
    21  type cliHelp struct {
    22  	// Example is required, the others have a default value
    23  	// generated from the item type
    24  	use     string
    25  	short   string
    26  	long    string
    27  	example string
    28  }
    29  
    30  type cliItem struct {
    31  	name          string // plural, as used in the hub index
    32  	singular      string
    33  	oneOrMore     string // parenthetical pluralizaion: "parser(s)"
    34  	help          cliHelp
    35  	installHelp   cliHelp
    36  	removeHelp    cliHelp
    37  	upgradeHelp   cliHelp
    38  	inspectHelp   cliHelp
    39  	inspectDetail func(item *cwhub.Item) error
    40  	listHelp      cliHelp
    41  }
    42  
    43  func (cli cliItem) NewCommand() *cobra.Command {
    44  	cmd := &cobra.Command{
    45  		Use:               coalesce.String(cli.help.use, fmt.Sprintf("%s <action> [item]...", cli.name)),
    46  		Short:             coalesce.String(cli.help.short, fmt.Sprintf("Manage hub %s", cli.name)),
    47  		Long:              cli.help.long,
    48  		Example:           cli.help.example,
    49  		Args:              cobra.MinimumNArgs(1),
    50  		Aliases:           []string{cli.singular},
    51  		DisableAutoGenTag: true,
    52  	}
    53  
    54  	cmd.AddCommand(cli.newInstallCmd())
    55  	cmd.AddCommand(cli.newRemoveCmd())
    56  	cmd.AddCommand(cli.newUpgradeCmd())
    57  	cmd.AddCommand(cli.newInspectCmd())
    58  	cmd.AddCommand(cli.newListCmd())
    59  
    60  	return cmd
    61  }
    62  
    63  func (cli cliItem) install(args []string, downloadOnly bool, force bool, ignoreError bool) error {
    64  	hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
    65  	if err != nil {
    66  		return err
    67  	}
    68  
    69  	for _, name := range args {
    70  		item := hub.GetItem(cli.name, name)
    71  		if item == nil {
    72  			msg := suggestNearestMessage(hub, cli.name, name)
    73  			if !ignoreError {
    74  				return fmt.Errorf(msg)
    75  			}
    76  
    77  			log.Errorf(msg)
    78  
    79  			continue
    80  		}
    81  
    82  		if err := item.Install(force, downloadOnly); err != nil {
    83  			if !ignoreError {
    84  				return fmt.Errorf("error while installing '%s': %w", item.Name, err)
    85  			}
    86  
    87  			log.Errorf("Error while installing '%s': %s", item.Name, err)
    88  		}
    89  	}
    90  
    91  	log.Infof(ReloadMessage())
    92  
    93  	return nil
    94  }
    95  
    96  func (cli cliItem) newInstallCmd() *cobra.Command {
    97  	var (
    98  		downloadOnly bool
    99  		force        bool
   100  		ignoreError  bool
   101  	)
   102  
   103  	cmd := &cobra.Command{
   104  		Use:               coalesce.String(cli.installHelp.use, "install [item]..."),
   105  		Short:             coalesce.String(cli.installHelp.short, fmt.Sprintf("Install given %s", cli.oneOrMore)),
   106  		Long:              coalesce.String(cli.installHelp.long, fmt.Sprintf("Fetch and install one or more %s from the hub", cli.name)),
   107  		Example:           cli.installHelp.example,
   108  		Args:              cobra.MinimumNArgs(1),
   109  		DisableAutoGenTag: true,
   110  		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   111  			return compAllItems(cli.name, args, toComplete)
   112  		},
   113  		RunE: func(cmd *cobra.Command, args []string) error {
   114  			return cli.install(args, downloadOnly, force, ignoreError)
   115  		},
   116  	}
   117  
   118  	flags := cmd.Flags()
   119  	flags.BoolVarP(&downloadOnly, "download-only", "d", false, "Only download packages, don't enable")
   120  	flags.BoolVar(&force, "force", false, "Force install: overwrite tainted and outdated files")
   121  	flags.BoolVar(&ignoreError, "ignore", false, fmt.Sprintf("Ignore errors when installing multiple %s", cli.name))
   122  
   123  	return cmd
   124  }
   125  
   126  // return the names of the installed parents of an item, used to check if we can remove it
   127  func istalledParentNames(item *cwhub.Item) []string {
   128  	ret := make([]string, 0)
   129  
   130  	for _, parent := range item.Ancestors() {
   131  		if parent.State.Installed {
   132  			ret = append(ret, parent.Name)
   133  		}
   134  	}
   135  
   136  	return ret
   137  }
   138  
   139  func (cli cliItem) remove(args []string, purge bool, force bool, all bool) error {
   140  	hub, err := require.Hub(csConfig, nil, log.StandardLogger())
   141  	if err != nil {
   142  		return err
   143  	}
   144  
   145  	if all {
   146  		getter := hub.GetInstalledItems
   147  		if purge {
   148  			getter = hub.GetAllItems
   149  		}
   150  
   151  		items, err := getter(cli.name)
   152  		if err != nil {
   153  			return err
   154  		}
   155  
   156  		removed := 0
   157  
   158  		for _, item := range items {
   159  			didRemove, err := item.Remove(purge, force)
   160  			if err != nil {
   161  				return err
   162  			}
   163  
   164  			if didRemove {
   165  				log.Infof("Removed %s", item.Name)
   166  				removed++
   167  			}
   168  		}
   169  
   170  		log.Infof("Removed %d %s", removed, cli.name)
   171  
   172  		if removed > 0 {
   173  			log.Infof(ReloadMessage())
   174  		}
   175  
   176  		return nil
   177  	}
   178  
   179  	if len(args) == 0 {
   180  		return fmt.Errorf("specify at least one %s to remove or '--all'", cli.singular)
   181  	}
   182  
   183  	removed := 0
   184  
   185  	for _, itemName := range args {
   186  		item := hub.GetItem(cli.name, itemName)
   187  		if item == nil {
   188  			return fmt.Errorf("can't find '%s' in %s", itemName, cli.name)
   189  		}
   190  
   191  		parents := istalledParentNames(item)
   192  
   193  		if !force && len(parents) > 0 {
   194  			log.Warningf("%s belongs to collections: %s", item.Name, parents)
   195  			log.Warningf("Run 'sudo cscli %s remove %s --force' if you want to force remove this %s", item.Type, item.Name, cli.singular)
   196  
   197  			continue
   198  		}
   199  
   200  		didRemove, err := item.Remove(purge, force)
   201  		if err != nil {
   202  			return err
   203  		}
   204  
   205  		if didRemove {
   206  			log.Infof("Removed %s", item.Name)
   207  			removed++
   208  		}
   209  	}
   210  
   211  	log.Infof("Removed %d %s", removed, cli.name)
   212  
   213  	if removed > 0 {
   214  		log.Infof(ReloadMessage())
   215  	}
   216  
   217  	return nil
   218  }
   219  
   220  func (cli cliItem) newRemoveCmd() *cobra.Command {
   221  	var (
   222  		purge bool
   223  		force bool
   224  		all   bool
   225  	)
   226  
   227  	cmd := &cobra.Command{
   228  		Use:               coalesce.String(cli.removeHelp.use, "remove [item]..."),
   229  		Short:             coalesce.String(cli.removeHelp.short, fmt.Sprintf("Remove given %s", cli.oneOrMore)),
   230  		Long:              coalesce.String(cli.removeHelp.long, fmt.Sprintf("Remove one or more %s", cli.name)),
   231  		Example:           cli.removeHelp.example,
   232  		Aliases:           []string{"delete"},
   233  		DisableAutoGenTag: true,
   234  		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   235  			return compInstalledItems(cli.name, args, toComplete)
   236  		},
   237  		RunE: func(cmd *cobra.Command, args []string) error {
   238  			return cli.remove(args, purge, force, all)
   239  		},
   240  	}
   241  
   242  	flags := cmd.Flags()
   243  	flags.BoolVar(&purge, "purge", false, "Delete source file too")
   244  	flags.BoolVar(&force, "force", false, "Force remove: remove tainted and outdated files")
   245  	flags.BoolVar(&all, "all", false, fmt.Sprintf("Remove all the %s", cli.name))
   246  
   247  	return cmd
   248  }
   249  
   250  func (cli cliItem) upgrade(args []string, force bool, all bool) error {
   251  	hub, err := require.Hub(csConfig, require.RemoteHub(csConfig), log.StandardLogger())
   252  	if err != nil {
   253  		return err
   254  	}
   255  
   256  	if all {
   257  		items, err := hub.GetInstalledItems(cli.name)
   258  		if err != nil {
   259  			return err
   260  		}
   261  
   262  		updated := 0
   263  
   264  		for _, item := range items {
   265  			didUpdate, err := item.Upgrade(force)
   266  			if err != nil {
   267  				return err
   268  			}
   269  
   270  			if didUpdate {
   271  				updated++
   272  			}
   273  		}
   274  
   275  		log.Infof("Updated %d %s", updated, cli.name)
   276  
   277  		if updated > 0 {
   278  			log.Infof(ReloadMessage())
   279  		}
   280  
   281  		return nil
   282  	}
   283  
   284  	if len(args) == 0 {
   285  		return fmt.Errorf("specify at least one %s to upgrade or '--all'", cli.singular)
   286  	}
   287  
   288  	updated := 0
   289  
   290  	for _, itemName := range args {
   291  		item := hub.GetItem(cli.name, itemName)
   292  		if item == nil {
   293  			return fmt.Errorf("can't find '%s' in %s", itemName, cli.name)
   294  		}
   295  
   296  		didUpdate, err := item.Upgrade(force)
   297  		if err != nil {
   298  			return err
   299  		}
   300  
   301  		if didUpdate {
   302  			log.Infof("Updated %s", item.Name)
   303  			updated++
   304  		}
   305  	}
   306  
   307  	if updated > 0 {
   308  		log.Infof(ReloadMessage())
   309  	}
   310  
   311  	return nil
   312  }
   313  
   314  func (cli cliItem) newUpgradeCmd() *cobra.Command {
   315  	var (
   316  		all   bool
   317  		force bool
   318  	)
   319  
   320  	cmd := &cobra.Command{
   321  		Use:               coalesce.String(cli.upgradeHelp.use, "upgrade [item]..."),
   322  		Short:             coalesce.String(cli.upgradeHelp.short, fmt.Sprintf("Upgrade given %s", cli.oneOrMore)),
   323  		Long:              coalesce.String(cli.upgradeHelp.long, fmt.Sprintf("Fetch and upgrade one or more %s from the hub", cli.name)),
   324  		Example:           cli.upgradeHelp.example,
   325  		DisableAutoGenTag: true,
   326  		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   327  			return compInstalledItems(cli.name, args, toComplete)
   328  		},
   329  		RunE: func(cmd *cobra.Command, args []string) error {
   330  			return cli.upgrade(args, force, all)
   331  		},
   332  	}
   333  
   334  	flags := cmd.Flags()
   335  	flags.BoolVarP(&all, "all", "a", false, fmt.Sprintf("Upgrade all the %s", cli.name))
   336  	flags.BoolVar(&force, "force", false, "Force upgrade: overwrite tainted and outdated files")
   337  
   338  	return cmd
   339  }
   340  
   341  func (cli cliItem) inspect(args []string, url string, diff bool, rev bool, noMetrics bool) error {
   342  	if rev && !diff {
   343  		return fmt.Errorf("--rev can only be used with --diff")
   344  	}
   345  
   346  	if url != "" {
   347  		csConfig.Cscli.PrometheusUrl = url
   348  	}
   349  
   350  	remote := (*cwhub.RemoteHubCfg)(nil)
   351  
   352  	if diff {
   353  		remote = require.RemoteHub(csConfig)
   354  	}
   355  
   356  	hub, err := require.Hub(csConfig, remote, log.StandardLogger())
   357  	if err != nil {
   358  		return err
   359  	}
   360  
   361  	for _, name := range args {
   362  		item := hub.GetItem(cli.name, name)
   363  		if item == nil {
   364  			return fmt.Errorf("can't find '%s' in %s", name, cli.name)
   365  		}
   366  
   367  		if diff {
   368  			fmt.Println(cli.whyTainted(hub, item, rev))
   369  
   370  			continue
   371  		}
   372  
   373  		if err = inspectItem(item, !noMetrics); err != nil {
   374  			return err
   375  		}
   376  
   377  		if cli.inspectDetail != nil {
   378  			if err = cli.inspectDetail(item); err != nil {
   379  				return err
   380  			}
   381  		}
   382  	}
   383  
   384  	return nil
   385  }
   386  
   387  func (cli cliItem) newInspectCmd() *cobra.Command {
   388  	var (
   389  		url       string
   390  		diff      bool
   391  		rev       bool
   392  		noMetrics bool
   393  	)
   394  
   395  	cmd := &cobra.Command{
   396  		Use:               coalesce.String(cli.inspectHelp.use, "inspect [item]..."),
   397  		Short:             coalesce.String(cli.inspectHelp.short, fmt.Sprintf("Inspect given %s", cli.oneOrMore)),
   398  		Long:              coalesce.String(cli.inspectHelp.long, fmt.Sprintf("Inspect the state of one or more %s", cli.name)),
   399  		Example:           cli.inspectHelp.example,
   400  		Args:              cobra.MinimumNArgs(1),
   401  		DisableAutoGenTag: true,
   402  		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   403  			return compInstalledItems(cli.name, args, toComplete)
   404  		},
   405  		RunE: func(cmd *cobra.Command, args []string) error {
   406  			return cli.inspect(args, url, diff, rev, noMetrics)
   407  		},
   408  	}
   409  
   410  	flags := cmd.Flags()
   411  	flags.StringVarP(&url, "url", "u", "", "Prometheus url")
   412  	flags.BoolVar(&diff, "diff", false, "Show diff with latest version (for tainted items)")
   413  	flags.BoolVar(&rev, "rev", false, "Reverse diff output")
   414  	flags.BoolVar(&noMetrics, "no-metrics", false, "Don't show metrics (when cscli.output=human)")
   415  
   416  	return cmd
   417  }
   418  
   419  func (cli cliItem) list(args []string, all bool) error {
   420  	hub, err := require.Hub(csConfig, nil, log.StandardLogger())
   421  	if err != nil {
   422  		return err
   423  	}
   424  
   425  	items := make(map[string][]*cwhub.Item)
   426  
   427  	items[cli.name], err = selectItems(hub, cli.name, args, !all)
   428  	if err != nil {
   429  		return err
   430  	}
   431  
   432  	if err = listItems(color.Output, []string{cli.name}, items, false); err != nil {
   433  		return err
   434  	}
   435  
   436  	return nil
   437  }
   438  
   439  func (cli cliItem) newListCmd() *cobra.Command {
   440  	var all bool
   441  
   442  	cmd := &cobra.Command{
   443  		Use:               coalesce.String(cli.listHelp.use, "list [item... | -a]"),
   444  		Short:             coalesce.String(cli.listHelp.short, fmt.Sprintf("List %s", cli.oneOrMore)),
   445  		Long:              coalesce.String(cli.listHelp.long, fmt.Sprintf("List of installed/available/specified %s", cli.name)),
   446  		Example:           cli.listHelp.example,
   447  		DisableAutoGenTag: true,
   448  		RunE: func(_ *cobra.Command, args []string) error {
   449  			return cli.list(args, all)
   450  		},
   451  	}
   452  
   453  	flags := cmd.Flags()
   454  	flags.BoolVarP(&all, "all", "a", false, "List disabled items as well")
   455  
   456  	return cmd
   457  }
   458  
   459  // return the diff between the installed version and the latest version
   460  func (cli cliItem) itemDiff(item *cwhub.Item, reverse bool) (string, error) {
   461  	if !item.State.Installed {
   462  		return "", fmt.Errorf("'%s' is not installed", item.FQName())
   463  	}
   464  
   465  	latestContent, remoteURL, err := item.FetchLatest()
   466  	if err != nil {
   467  		return "", err
   468  	}
   469  
   470  	localContent, err := os.ReadFile(item.State.LocalPath)
   471  	if err != nil {
   472  		return "", fmt.Errorf("while reading %s: %w", item.State.LocalPath, err)
   473  	}
   474  
   475  	file1 := item.State.LocalPath
   476  	file2 := remoteURL
   477  	content1 := string(localContent)
   478  	content2 := string(latestContent)
   479  
   480  	if reverse {
   481  		file1, file2 = file2, file1
   482  		content1, content2 = content2, content1
   483  	}
   484  
   485  	edits := myers.ComputeEdits(span.URIFromPath(file1), content1, content2)
   486  	diff := gotextdiff.ToUnified(file1, file2, content1, edits)
   487  
   488  	return fmt.Sprintf("%s", diff), nil
   489  }
   490  
   491  func (cli cliItem) whyTainted(hub *cwhub.Hub, item *cwhub.Item, reverse bool) string {
   492  	if !item.State.Installed {
   493  		return fmt.Sprintf("# %s is not installed", item.FQName())
   494  	}
   495  
   496  	if !item.State.Tainted {
   497  		return fmt.Sprintf("# %s is not tainted", item.FQName())
   498  	}
   499  
   500  	if len(item.State.TaintedBy) == 0 {
   501  		return fmt.Sprintf("# %s is tainted but we don't know why. please report this as a bug", item.FQName())
   502  	}
   503  
   504  	ret := []string{
   505  		fmt.Sprintf("# Let's see why %s is tainted.", item.FQName()),
   506  	}
   507  
   508  	for _, fqsub := range item.State.TaintedBy {
   509  		ret = append(ret, fmt.Sprintf("\n-> %s\n", fqsub))
   510  
   511  		sub, err := hub.GetItemFQ(fqsub)
   512  		if err != nil {
   513  			ret = append(ret, err.Error())
   514  		}
   515  
   516  		diff, err := cli.itemDiff(sub, reverse)
   517  		if err != nil {
   518  			ret = append(ret, err.Error())
   519  		}
   520  
   521  		if diff != "" {
   522  			ret = append(ret, diff)
   523  		} else if len(sub.State.TaintedBy) > 0 {
   524  			taintList := strings.Join(sub.State.TaintedBy, ", ")
   525  			if sub.FQName() == taintList {
   526  				// hack: avoid message "item is tainted by itself"
   527  				continue
   528  			}
   529  			ret = append(ret, fmt.Sprintf("# %s is tainted by %s", sub.FQName(), taintList))
   530  		}
   531  	}
   532  
   533  	return strings.Join(ret, "\n")
   534  }