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 }