소스 검색

bats: more cscli hub tests (#2541)

- updated logs and user messages
- added func tests for all the items: install, remove, upgrade, list
- rewritten taint tests for collections
- removed redundant csconfig.LoadPrometheus()
mmetc 1 년 전
부모
커밋
f496bd1692

+ 49 - 35
cmd/crowdsec-cli/collections.go

@@ -13,13 +13,13 @@ import (
 
 func NewCollectionsCmd() *cobra.Command {
 	cmdCollections := &cobra.Command{
-		Use:   "collections [action]",
-		Short: "Install/Remove/Upgrade/Inspect collections from the CrowdSec Hub.",
-		Example: `cscli collections install crowdsec/xxx crowdsec/xyz
-cscli collections inspect crowdsec/xxx crowdsec/xyz
-cscli collections upgrade crowdsec/xxx crowdsec/xyz
-cscli collections list
-cscli collections remove crowdsec/xxx crowdsec/xyz
+		Use:   "collections <action> [collection]...",
+		Short: "Manage hub collections",
+		Example: `cscli collections list -a
+cscli collections install crowdsecurity/http-cve crowdsecurity/iptables
+cscli collections inspect crowdsecurity/http-cve crowdsecurity/iptables
+cscli collections upgrade crowdsecurity/http-cve crowdsecurity/iptables
+cscli collections remove crowdsecurity/http-cve crowdsecurity/iptables
 `,
 		Args:              cobra.MinimumNArgs(1),
 		Aliases:           []string{"collection"},
@@ -88,10 +88,10 @@ func runCollectionsInstall(cmd *cobra.Command, args []string) error {
 
 func NewCollectionsInstallCmd() *cobra.Command {
 	cmdCollectionsInstall := &cobra.Command{
-		Use:               "install collection",
+		Use:               "install <collection>...",
 		Short:             "Install given collection(s)",
-		Long:              `Fetch and install given collection(s) from hub`,
-		Example:           `cscli collections install crowdsec/xxx crowdsec/xyz`,
+		Long:              `Fetch and install one or more collections from hub`,
+		Example:           `cscli collections install crowdsecurity/http-cve crowdsecurity/iptables`,
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -102,7 +102,7 @@ func NewCollectionsInstallCmd() *cobra.Command {
 
 	flags := cmdCollectionsInstall.Flags()
 	flags.BoolP("download-only", "d", false, "Only download packages, don't enable")
-	flags.Bool("force", false, "Force install : Overwrite tainted and outdated files")
+	flags.Bool("force", false, "Force install: overwrite tainted and outdated files")
 	flags.Bool("ignore", false, "Ignore errors when installing multiple collections")
 
 	return cmdCollectionsInstall
@@ -143,11 +143,12 @@ func runCollectionsRemove(cmd *cobra.Command, args []string) error {
 		if !force {
 			item := cwhub.GetItem(cwhub.COLLECTIONS, name)
 			if item == nil {
-				return fmt.Errorf("unable to retrieve: %s", name)
+				// XXX: this should be in GetItem?
+				return fmt.Errorf("can't find '%s' in %s", name, cwhub.COLLECTIONS)
 			}
 			if len(item.BelongsToCollections) > 0 {
-				log.Warningf("%s belongs to other collections :\n%s\n", name, item.BelongsToCollections)
-				log.Printf("Run 'sudo cscli collections remove %s --force' if you want to force remove this sub collection\n", name)
+				log.Warningf("%s belongs to other collections: %s", name, item.BelongsToCollections)
+				log.Warningf("Run 'sudo cscli collections remove %s --force' if you want to force remove this sub collection", name)
 				continue
 			}
 		}
@@ -163,10 +164,10 @@ func runCollectionsRemove(cmd *cobra.Command, args []string) error {
 
 func NewCollectionsRemoveCmd() *cobra.Command {
 	cmdCollectionsRemove := &cobra.Command{
-		Use:               "remove collection",
+		Use:               "remove <collection>...",
 		Short:             "Remove given collection(s)",
-		Long:              `Remove given collection(s) from hub`,
-		Example:           `cscli collections remove crowdsec/xxx crowdsec/xyz`,
+		Long:              `Remove one or more collections`,
+		Example:           `cscli collections remove crowdsecurity/http-cve crowdsecurity/iptables`,
 		Aliases:           []string{"delete"},
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -177,8 +178,8 @@ func NewCollectionsRemoveCmd() *cobra.Command {
 
 	flags := cmdCollectionsRemove.Flags()
 	flags.Bool("purge", false, "Delete source file too")
-	flags.Bool("force", false, "Force remove : Remove tainted and outdated files")
-	flags.Bool("all", false, "Delete all the collections")
+	flags.Bool("force", false, "Force remove: remove tainted and outdated files")
+	flags.Bool("all", false, "Remove all the collections")
 
 	return cmdCollectionsRemove
 }
@@ -197,7 +198,9 @@ func runCollectionsUpgrade(cmd *cobra.Command, args []string) error {
 	}
 
 	if all {
-		cwhub.UpgradeConfig(csConfig, cwhub.COLLECTIONS, "", force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.COLLECTIONS, "", force); err != nil {
+			return err
+		}
 		return nil
 	}
 
@@ -206,7 +209,9 @@ func runCollectionsUpgrade(cmd *cobra.Command, args []string) error {
 	}
 
 	for _, name := range args {
-		cwhub.UpgradeConfig(csConfig, cwhub.COLLECTIONS, name, force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.COLLECTIONS, name, force); err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -214,10 +219,10 @@ func runCollectionsUpgrade(cmd *cobra.Command, args []string) error {
 
 func NewCollectionsUpgradeCmd() *cobra.Command {
 	cmdCollectionsUpgrade := &cobra.Command{
-		Use:               "upgrade collection",
+		Use:               "upgrade <collection>...",
 		Short:             "Upgrade given collection(s)",
-		Long:              `Fetch and upgrade given collection(s) from hub`,
-		Example:           `cscli collections upgrade crowdsec/xxx crowdsec/xyz`,
+		Long:              `Fetch and upgrade one or more collections from the hub`,
+		Example:           `cscli collections upgrade crowdsecurity/http-cve crowdsecurity/iptables`,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compInstalledItems(cwhub.COLLECTIONS, args, toComplete)
@@ -227,7 +232,7 @@ func NewCollectionsUpgradeCmd() *cobra.Command {
 
 	flags := cmdCollectionsUpgrade.Flags()
 	flags.BoolP("all", "a", false, "Upgrade all the collections")
-	flags.Bool("force", false, "Force upgrade : Overwrite tainted and outdated files")
+	flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files")
 
 	return cmdCollectionsUpgrade
 }
@@ -244,8 +249,15 @@ func runCollectionsInspect(cmd *cobra.Command, args []string) error {
 		csConfig.Cscli.PrometheusUrl = url
 	}
 
+	noMetrics, err := flags.GetBool("no-metrics")
+	if err != nil {
+		return err
+	}
+
 	for _, name := range args {
-		InspectItem(name, cwhub.COLLECTIONS)
+		if err = InspectItem(name, cwhub.COLLECTIONS, noMetrics); err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -253,10 +265,10 @@ func runCollectionsInspect(cmd *cobra.Command, args []string) error {
 
 func NewCollectionsInspectCmd() *cobra.Command {
 	cmdCollectionsInspect := &cobra.Command{
-		Use:               "inspect collection",
-		Short:             "Inspect given collection",
-		Long:              `Inspect given collection`,
-		Example:           `cscli collections inspect crowdsec/xxx crowdsec/xyz`,
+		Use:               "inspect <collection>...",
+		Short:             "Inspect given collection(s)",
+		Long:              `Inspect one or more collections`,
+		Example:           `cscli collections inspect crowdsecurity/http-cve crowdsecurity/iptables`,
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -267,6 +279,7 @@ func NewCollectionsInspectCmd() *cobra.Command {
 
 	flags := cmdCollectionsInspect.Flags()
 	flags.StringP("url", "u", "", "Prometheus url")
+	flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)")
 
 	return cmdCollectionsInspect
 }
@@ -287,11 +300,12 @@ func runCollectionsList(cmd *cobra.Command, args []string) error {
 
 func NewCollectionsListCmd() *cobra.Command {
 	cmdCollectionsList := &cobra.Command{
-		Use:               "list collection [-a]",
-		Short:             "List all collections",
-		Long:              `List all collections`,
-		Example:           `cscli collections list`,
-		Args:              cobra.ExactArgs(0),
+		Use:   "list [collection... | -a]",
+		Short: "List collections",
+		Long:  `List of installed/available/specified collections`,
+		Example: `cscli collections list
+cscli collections list -a
+cscli collections list crowdsecurity/http-cve crowdsecurity/iptables`,
 		DisableAutoGenTag: true,
 		RunE:              runCollectionsList,
 	}

+ 25 - 17
cmd/crowdsec-cli/hub.go

@@ -15,17 +15,14 @@ import (
 func NewHubCmd() *cobra.Command {
 	var cmdHub = &cobra.Command{
 		Use:   "hub [action]",
-		Short: "Manage Hub",
-		Long: `
-Hub management
+		Short: "Manage hub index",
+		Long: `Hub management
 
 List/update parsers/scenarios/postoverflows/collections from [Crowdsec Hub](https://hub.crowdsec.net).
-The Hub is managed by cscli, to get the latest hub files from [Crowdsec Hub](https://hub.crowdsec.net), you need to update.
-		`,
-		Example: `
-cscli hub list   # List all installed configurations
-cscli hub update # Download list of available configurations from the hub
-		`,
+The Hub is managed by cscli, to get the latest hub files from [Crowdsec Hub](https://hub.crowdsec.net), you need to update.`,
+		Example: `cscli hub list
+cscli hub update
+cscli hub upgrade`,
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
@@ -75,7 +72,7 @@ func runHubList(cmd *cobra.Command, args []string) error {
 func NewHubListCmd() *cobra.Command {
 	var cmdHubList = &cobra.Command{
 		Use:               "list [-a]",
-		Short:             "List installed configs",
+		Short:             "List all installed configurations",
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		RunE: 	    runHubList,
@@ -115,7 +112,7 @@ func runHubUpdate(cmd *cobra.Command, args []string) error {
 func NewHubUpdateCmd() *cobra.Command {
 	var cmdHubUpdate = &cobra.Command{
 		Use:   "update",
-		Short: "Fetch available configs from hub",
+		Short: "Download the latest index (catalog of available configurations)",
 		Long: `
 Fetches the [.index.json](https://github.com/crowdsecurity/hub/blob/master/.index.json) file from hub, containing the list of available configs.
 `,
@@ -149,13 +146,24 @@ func runHubUpgrade(cmd *cobra.Command, args []string) error {
 	}
 
 	log.Infof("Upgrading collections")
-	cwhub.UpgradeConfig(csConfig, cwhub.COLLECTIONS, "", force)
+	if err := cwhub.UpgradeConfig(csConfig, cwhub.COLLECTIONS, "", force); err != nil {
+		return err
+	}
+
 	log.Infof("Upgrading parsers")
-	cwhub.UpgradeConfig(csConfig, cwhub.PARSERS, "", force)
+	if err := cwhub.UpgradeConfig(csConfig, cwhub.PARSERS, "", force); err != nil {
+		return err
+	}
+
 	log.Infof("Upgrading scenarios")
-	cwhub.UpgradeConfig(csConfig, cwhub.SCENARIOS, "", force)
+	if err := cwhub.UpgradeConfig(csConfig, cwhub.SCENARIOS, "", force); err != nil {
+		return err
+	}
+
 	log.Infof("Upgrading postoverflows")
-	cwhub.UpgradeConfig(csConfig, cwhub.PARSERS_OVFLW, "", force)
+	if err := cwhub.UpgradeConfig(csConfig, cwhub.PARSERS_OVFLW, "", force); err != nil {
+		return err
+	}
 
 	return nil
 }
@@ -163,7 +171,7 @@ func runHubUpgrade(cmd *cobra.Command, args []string) error {
 func NewHubUpgradeCmd() *cobra.Command {
 	var cmdHubUpgrade = &cobra.Command{
 		Use:   "upgrade",
-		Short: "Upgrade all configs installed from hub",
+		Short: "Upgrade all configurations to their latest version",
 		Long: `
 Upgrade all configs installed from Crowdsec Hub. Run 'sudo cscli hub update' if you want the latest versions available.
 `,
@@ -182,7 +190,7 @@ Upgrade all configs installed from Crowdsec Hub. Run 'sudo cscli hub update' if
 	}
 
 	flags := cmdHubUpgrade.Flags()
-	flags.Bool("force", false, "Force upgrade : Overwrite tainted and outdated files")
+	flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files")
 
 	return cmdHubUpgrade
 }

+ 0 - 4
cmd/crowdsec-cli/metrics.go

@@ -300,10 +300,6 @@ func runMetrics(cmd *cobra.Command, args []string) error {
 		return err
 	}
 
-	if err = csConfig.LoadPrometheus(); err != nil {
-		return fmt.Errorf("failed to load prometheus config: %w", err)
-	}
-
 	if csConfig.Prometheus == nil {
 		return fmt.Errorf("prometheus section missing, can't show metrics")
 	}

+ 47 - 32
cmd/crowdsec-cli/parsers.go

@@ -13,13 +13,13 @@ import (
 
 func NewParsersCmd() *cobra.Command {
 	cmdParsers := &cobra.Command{
-		Use:   "parsers [action] [config]",
-		Short: "Install/Remove/Upgrade/Inspect parser(s) from hub",
-		Example: `cscli parsers install crowdsecurity/sshd-logs
-cscli parsers inspect crowdsecurity/sshd-logs
-cscli parsers upgrade crowdsecurity/sshd-logs
-cscli parsers list
-cscli parsers remove crowdsecurity/sshd-logs
+		Use:   "parsers <action> [parser]...",
+		Short: "Manage hub parsers",
+		Example: `cscli parsers list -a
+cscli parsers install crowdsecurity/caddy-logs crowdsecurity/sshd-logs
+cscli parsers inspect crowdsecurity/caddy-logs crowdsecurity/sshd-logs
+cscli parsers upgrade crowdsecurity/caddy-logs crowdsecurity/sshd-logs
+cscli parsers remove crowdsecurity/caddy-logs crowdsecurity/sshd-logs
 `,
 		Args:              cobra.MinimumNArgs(1),
 		Aliases:           []string{"parser"},
@@ -88,10 +88,10 @@ func runParsersInstall(cmd *cobra.Command, args []string) error {
 
 func NewParsersInstallCmd() *cobra.Command {
 	cmdParsersInstall := &cobra.Command{
-		Use:               "install [config]",
+		Use:               "install <parser>...",
 		Short:             "Install given parser(s)",
-		Long:              `Fetch and install given parser(s) from hub`,
-		Example:           `cscli parsers install crowdsec/xxx crowdsec/xyz`,
+		Long:              `Fetch and install one or more parsers from the hub`,
+		Example:           `cscli parsers install crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -102,7 +102,7 @@ func NewParsersInstallCmd() *cobra.Command {
 
 	flags := cmdParsersInstall.Flags()
 	flags.BoolP("download-only", "d", false, "Only download packages, don't enable")
-	flags.Bool("force", false, "Force install: Overwrite tainted and outdated files")
+	flags.Bool("force", false, "Force install: overwrite tainted and outdated files")
 	flags.Bool("ignore", false, "Ignore errors when installing multiple parsers")
 
 	return cmdParsersInstall
@@ -151,10 +151,10 @@ func runParsersRemove(cmd *cobra.Command, args []string) error {
 
 func NewParsersRemoveCmd() *cobra.Command {
 	cmdParsersRemove := &cobra.Command{
-		Use:               "remove [config]",
+		Use:               "remove <parser>...",
 		Short:             "Remove given parser(s)",
-		Long:              `Remove given parse(s) from hub`,
-		Example:           `cscli parsers remove crowdsec/xxx crowdsec/xyz`,
+		Long:              `Remove one or more parsers`,
+		Example:           `cscli parsers remove crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
 		Aliases:           []string{"delete"},
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -165,8 +165,8 @@ func NewParsersRemoveCmd() *cobra.Command {
 
 	flags := cmdParsersRemove.Flags()
 	flags.Bool("purge", false, "Delete source file too")
-	flags.Bool("force", false, "Force remove: Remove tainted and outdated files")
-	flags.Bool("all", false, "Delete all the parsers")
+	flags.Bool("force", false, "Force remove: remove tainted and outdated files")
+	flags.Bool("all", false, "Remove all the parsers")
 
 	return cmdParsersRemove
 }
@@ -185,7 +185,9 @@ func runParsersUpgrade(cmd *cobra.Command, args []string) error {
 	}
 
 	if all {
-		cwhub.UpgradeConfig(csConfig, cwhub.PARSERS, "", force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.PARSERS, "", force); err != nil {
+			return err
+		}
 		return nil
 	}
 
@@ -194,7 +196,9 @@ func runParsersUpgrade(cmd *cobra.Command, args []string) error {
 	}
 
 	for _, name := range args {
-		cwhub.UpgradeConfig(csConfig, cwhub.PARSERS, name, force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.PARSERS, name, force); err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -202,10 +206,10 @@ func runParsersUpgrade(cmd *cobra.Command, args []string) error {
 
 func NewParsersUpgradeCmd() *cobra.Command {
 	cmdParsersUpgrade := &cobra.Command{
-		Use:               "upgrade [config]",
+		Use:               "upgrade <parser>...",
 		Short:             "Upgrade given parser(s)",
-		Long:              `Fetch and upgrade given parser(s) from hub`,
-		Example:           `cscli parsers upgrade crowdsec/xxx crowdsec/xyz`,
+		Long:              `Fetch and upgrade one or more parsers from the hub`,
+		Example:           `cscli parsers upgrade crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compInstalledItems(cwhub.PARSERS, args, toComplete)
@@ -214,8 +218,8 @@ func NewParsersUpgradeCmd() *cobra.Command {
 	}
 
 	flags := cmdParsersUpgrade.Flags()
-	flags.Bool("all", false, "Upgrade all the parsers")
-	flags.Bool("force", false, "Force upgrade : Overwrite tainted and outdated files")
+	flags.BoolP("all", "a", false, "Upgrade all the parsers")
+	flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files")
 
 	return cmdParsersUpgrade
 }
@@ -232,17 +236,26 @@ func runParsersInspect(cmd *cobra.Command, args []string) error {
 		csConfig.Cscli.PrometheusUrl = url
 	}
 
-	InspectItem(args[0], cwhub.PARSERS)
+	noMetrics, err := flags.GetBool("no-metrics")
+	if err != nil {
+		return err
+	}
+
+	for _, name := range args {
+		if err = InspectItem(name, cwhub.PARSERS, noMetrics); err != nil {
+			return err
+		}
+	}
 
 	return nil
 }
 
 func NewParsersInspectCmd() *cobra.Command {
 	cmdParsersInspect := &cobra.Command{
-		Use:               "inspect [name]",
-		Short:             "Inspect given parser",
-		Long:              `Inspect given parser`,
-		Example:           `cscli parsers inspect crowdsec/xxx`,
+		Use:               "inspect <parser>",
+		Short:             "Inspect a parser",
+		Long:              `Inspect a parser`,
+		Example:           `cscli parsers inspect crowdsecurity/httpd-logs crowdsecurity/sshd-logs`,
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -253,6 +266,7 @@ func NewParsersInspectCmd() *cobra.Command {
 
 	flags := cmdParsersInspect.Flags()
 	flags.StringP("url", "u", "", "Prometheus url")
+	flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)")
 
 	return cmdParsersInspect
 }
@@ -273,11 +287,12 @@ func runParsersList(cmd *cobra.Command, args []string) error {
 
 func NewParsersListCmd() *cobra.Command {
 	cmdParsersList := &cobra.Command{
-		Use:   "list [name]",
-		Short: "List all parsers or given one",
-		Long:  `List all parsers or given one`,
+		Use:   "list [parser... | -a]",
+		Short: "List parsers",
+		Long:  `List of installed/available/specified parsers`,
 		Example: `cscli parsers list
-cscli parser list crowdsecurity/xxx`,
+cscli parsers list -a
+cscli parsers list crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
 		DisableAutoGenTag: true,
 		RunE:              runParsersList,
 	}

+ 45 - 30
cmd/crowdsec-cli/postoverflows.go

@@ -13,13 +13,13 @@ import (
 
 func NewPostOverflowsCmd() *cobra.Command {
 	cmdPostOverflows := &cobra.Command{
-		Use:   "postoverflows [action] [config]",
-		Short: "Install/Remove/Upgrade/Inspect postoverflow(s) from hub",
-		Example: `cscli postoverflows install crowdsecurity/cdn-whitelist
-cscli postoverflows inspect crowdsecurity/cdn-whitelist
-cscli postoverflows upgrade crowdsecurity/cdn-whitelist
-cscli postoverflows list
-cscli postoverflows remove crowdsecurity/cdn-whitelist
+		Use:   "postoverflows <action> [postoverflow]...",
+		Short: "Manage hub postoverflows",
+		Example: `cscli postoverflows list -a
+cscli postoverflows install crowdsecurity/cdn-whitelist crowdsecurity/rdns
+cscli postoverflows inspect crowdsecurity/cdn-whitelist crowdsecurity/rdns
+cscli postoverflows upgrade crowdsecurity/cdn-whitelist crowdsecurity/rdns
+cscli postoverflows remove crowdsecurity/cdn-whitelist crowdsecurity/rdns
 `,
 		Args:              cobra.MinimumNArgs(1),
 		Aliases:           []string{"postoverflow"},
@@ -88,10 +88,10 @@ func runPostOverflowsInstall(cmd *cobra.Command, args []string) error {
 
 func NewPostOverflowsInstallCmd() *cobra.Command {
 	cmdPostOverflowsInstall := &cobra.Command{
-		Use:               "install [config]",
+		Use:               "install <postoverflow>...",
 		Short:             "Install given postoverflow(s)",
-		Long:              `Fetch and install given postoverflow(s) from hub`,
-		Example:           `cscli postoverflows install crowdsec/xxx crowdsec/xyz`,
+		Long:              `Fetch and install one or more postoverflows from the hub`,
+		Example:           `cscli postoverflows install crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -102,7 +102,7 @@ func NewPostOverflowsInstallCmd() *cobra.Command {
 
 	flags := cmdPostOverflowsInstall.Flags()
 	flags.BoolP("download-only", "d", false, "Only download packages, don't enable")
-	flags.Bool("force", false, "Force install : Overwrite tainted and outdated files")
+	flags.Bool("force", false, "Force install: overwrite tainted and outdated files")
 	flags.Bool("ignore", false, "Ignore errors when installing multiple postoverflows")
 
 	return cmdPostOverflowsInstall
@@ -151,10 +151,10 @@ func runPostOverflowsRemove(cmd *cobra.Command, args []string) error {
 
 func NewPostOverflowsRemoveCmd() *cobra.Command {
 	cmdPostOverflowsRemove := &cobra.Command{
-		Use:               "remove [config]",
+		Use:               "remove <postoverflow>...",
 		Short:             "Remove given postoverflow(s)",
-		Long:              `remove given postoverflow(s)`,
-		Example:           `cscli postoverflows remove crowdsec/xxx crowdsec/xyz`,
+		Long:              `remove one or more postoverflows from the hub`,
+		Example:           `cscli postoverflows remove crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
 		Aliases:           []string{"delete"},
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -165,7 +165,7 @@ func NewPostOverflowsRemoveCmd() *cobra.Command {
 
 	flags := cmdPostOverflowsRemove.Flags()
 	flags.Bool("purge", false, "Delete source file too")
-	flags.Bool("force", false, "Force remove : Remove tainted and outdated files")
+	flags.Bool("force", false, "Force remove: remove tainted and outdated files")
 	flags.Bool("all", false, "Delete all the postoverflows")
 
 	return cmdPostOverflowsRemove
@@ -185,7 +185,9 @@ func runPostOverflowUpgrade(cmd *cobra.Command, args []string) error {
 	}
 
 	if all {
-		cwhub.UpgradeConfig(csConfig, cwhub.PARSERS_OVFLW, "", force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.PARSERS_OVFLW, "", force); err != nil {
+			return err
+		}
 		return nil
 	}
 
@@ -194,7 +196,9 @@ func runPostOverflowUpgrade(cmd *cobra.Command, args []string) error {
 	}
 
 	for _, name := range args {
-		cwhub.UpgradeConfig(csConfig, cwhub.PARSERS_OVFLW, name, force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.PARSERS_OVFLW, name, force); err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -202,10 +206,10 @@ func runPostOverflowUpgrade(cmd *cobra.Command, args []string) error {
 
 func NewPostOverflowsUpgradeCmd() *cobra.Command {
 	cmdPostOverflowsUpgrade := &cobra.Command{
-		Use:               "upgrade [config]",
+		Use:               "upgrade <postoverflow>...",
 		Short:             "Upgrade given postoverflow(s)",
-		Long:              `Fetch and Upgrade given postoverflow(s) from hub`,
-		Example:           `cscli postoverflows upgrade crowdsec/xxx crowdsec/xyz`,
+		Long:              `Fetch and upgrade one or more postoverflows from the hub`,
+		Example:           `cscli postoverflows upgrade crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compInstalledItems(cwhub.PARSERS_OVFLW, args, toComplete)
@@ -215,7 +219,7 @@ func NewPostOverflowsUpgradeCmd() *cobra.Command {
 
 	flags := cmdPostOverflowsUpgrade.Flags()
 	flags.BoolP("all", "a", false, "Upgrade all the postoverflows")
-	flags.Bool("force", false, "Force upgrade : Overwrite tainted and outdated files")
+	flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files")
 
 	return cmdPostOverflowsUpgrade
 }
@@ -232,17 +236,26 @@ func runPostOverflowsInspect(cmd *cobra.Command, args []string) error {
 		csConfig.Cscli.PrometheusUrl = url
 	}
 
-	InspectItem(args[0], cwhub.PARSERS_OVFLW)
+	noMetrics, err := flags.GetBool("no-metrics")
+	if err != nil {
+		return err
+	}
+
+	for _, name := range args {
+		if err = InspectItem(name, cwhub.PARSERS_OVFLW, noMetrics); err != nil {
+			return err
+		}
+	}
 
 	return nil
 }
 
 func NewPostOverflowsInspectCmd() *cobra.Command {
 	cmdPostOverflowsInspect := &cobra.Command{
-		Use:               "inspect [config]",
-		Short:             "Inspect given postoverflow",
-		Long:              `Inspect given postoverflow`,
-		Example:           `cscli postoverflows inspect crowdsec/xxx crowdsec/xyz`,
+		Use:               "inspect <postoverflow>",
+		Short:             "Inspect a postoverflow",
+		Long:              `Inspect a postoverflow`,
+		Example:           `cscli postoverflows inspect crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -254,6 +267,7 @@ func NewPostOverflowsInspectCmd() *cobra.Command {
 	flags := cmdPostOverflowsInspect.Flags()
 	// XXX: is this needed for postoverflows?
 	flags.StringP("url", "u", "", "Prometheus url")
+	flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)")
 
 	return cmdPostOverflowsInspect
 }
@@ -274,11 +288,12 @@ func runPostOverflowsList(cmd *cobra.Command, args []string) error {
 
 func NewPostOverflowsListCmd() *cobra.Command {
 	cmdPostOverflowsList := &cobra.Command{
-		Use:   "list [config]",
-		Short: "List all postoverflows or given one",
-		Long:  `List all postoverflows or given one`,
+		Use:   "list [postoverflow]...",
+		Short: "List postoverflows",
+		Long:  `List of installed/available/specified postoverflows`,
 		Example: `cscli postoverflows list
-cscli postoverflows list crowdsecurity/xxx`,
+cscli postoverflows list -a
+cscli postoverflows list crowdsecurity/cdn-whitelist crowdsecurity/rdns`,
 		DisableAutoGenTag: true,
 		RunE:              runPostOverflowsList,
 	}

+ 46 - 31
cmd/crowdsec-cli/scenarios.go

@@ -13,13 +13,13 @@ import (
 
 func NewScenariosCmd() *cobra.Command {
 	cmdScenarios := &cobra.Command{
-		Use:   "scenarios [action] [config]",
-		Short: "Install/Remove/Upgrade/Inspect scenario(s) from hub",
-		Example: `cscli scenarios list [-a]
-cscli scenarios install crowdsecurity/ssh-bf
-cscli scenarios inspect crowdsecurity/ssh-bf
-cscli scenarios upgrade crowdsecurity/ssh-bf
-cscli scenarios remove crowdsecurity/ssh-bf
+		Use:   "scenarios <action> [scenario]...",
+		Short: "Manage hub scenarios",
+		Example: `cscli scenarios list -a
+cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/http-probing
+cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/http-probing
+cscli scenarios upgrade crowdsecurity/ssh-bf crowdsecurity/http-probing
+cscli scenarios remove crowdsecurity/ssh-bf crowdsecurity/http-probing
 `,
 		Args:              cobra.MinimumNArgs(1),
 		Aliases:           []string{"scenario"},
@@ -88,10 +88,10 @@ func runScenariosInstall(cmd *cobra.Command, args []string) error {
 
 func NewCmdScenariosInstall() *cobra.Command {
 	cmdScenariosInstall := &cobra.Command{
-		Use:               "install [config]",
+		Use:               "install <scenario>...",
 		Short:             "Install given scenario(s)",
-		Long:              `Fetch and install given scenario(s) from hub`,
-		Example:           `cscli scenarios install crowdsec/xxx crowdsec/xyz`,
+		Long:              `Fetch and install one or more scenarios from the hub`,
+		Example:           `cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/http-probing`,
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -102,7 +102,7 @@ func NewCmdScenariosInstall() *cobra.Command {
 
 	flags := cmdScenariosInstall.Flags()
 	flags.BoolP("download-only", "d", false, "Only download packages, don't enable")
-	flags.Bool("force", false, "Force install : Overwrite tainted and outdated files")
+	flags.Bool("force", false, "Force install: overwrite tainted and outdated files")
 	flags.Bool("ignore", false, "Ignore errors when installing multiple scenarios")
 
 	return cmdScenariosInstall
@@ -151,10 +151,10 @@ func runScenariosRemove(cmd *cobra.Command, args []string) error {
 
 func NewCmdScenariosRemove() *cobra.Command {
 	cmdScenariosRemove := &cobra.Command{
-		Use:               "remove [config]",
+		Use:               "remove <scenario>...",
 		Short:             "Remove given scenario(s)",
-		Long:              `remove given scenario(s)`,
-		Example:           `cscli scenarios remove crowdsec/xxx crowdsec/xyz`,
+		Long:              `remove one or more scenarios`,
+		Example:           `cscli scenarios remove crowdsecurity/ssh-bf crowdsecurity/http-probing`,
 		Aliases:           []string{"delete"},
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -165,8 +165,8 @@ func NewCmdScenariosRemove() *cobra.Command {
 
 	flags := cmdScenariosRemove.Flags()
 	flags.Bool("purge", false, "Delete source file too")
-	flags.Bool("force", false, "Force remove: Remove tainted and outdated files")
-	flags.Bool("all", false, "Delete all the scenarios")
+	flags.Bool("force", false, "Force remove: remove tainted and outdated files")
+	flags.Bool("all", false, "Remove all the scenarios")
 
 	return cmdScenariosRemove
 }
@@ -185,7 +185,9 @@ func runScenariosUpgrade(cmd *cobra.Command, args []string) error {
 	}
 
 	if all {
-		cwhub.UpgradeConfig(csConfig, cwhub.SCENARIOS, "", force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.SCENARIOS, "", force); err != nil {
+			return err
+		}
 		return nil
 	}
 
@@ -194,7 +196,9 @@ func runScenariosUpgrade(cmd *cobra.Command, args []string) error {
 	}
 
 	for _, name := range args {
-		cwhub.UpgradeConfig(csConfig, cwhub.SCENARIOS, name, force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.SCENARIOS, name, force); err != nil {
+			return err
+		}
 	}
 
 	return nil
@@ -202,10 +206,10 @@ func runScenariosUpgrade(cmd *cobra.Command, args []string) error {
 
 func NewCmdScenariosUpgrade() *cobra.Command {
 	cmdScenariosUpgrade := &cobra.Command{
-		Use:               "upgrade [config]",
+		Use:               "upgrade <scenario>...",
 		Short:             "Upgrade given scenario(s)",
-		Long:              `Fetch and Upgrade given scenario(s) from hub`,
-		Example:           `cscli scenarios upgrade crowdsec/xxx crowdsec/xyz`,
+		Long:              `Fetch and upgrade one or more scenarios from the hub`,
+		Example:           `cscli scenarios upgrade crowdsecurity/ssh-bf crowdsecurity/http-probing`,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compInstalledItems(cwhub.SCENARIOS, args, toComplete)
@@ -215,7 +219,7 @@ func NewCmdScenariosUpgrade() *cobra.Command {
 
 	flags := cmdScenariosUpgrade.Flags()
 	flags.BoolP("all", "a", false, "Upgrade all the scenarios")
-	flags.Bool("force", false, "Force upgrade : Overwrite tainted and outdated files")
+	flags.Bool("force", false, "Force upgrade: overwrite tainted and outdated files")
 
 	return cmdScenariosUpgrade
 }
@@ -232,17 +236,26 @@ func runScenariosInspect(cmd *cobra.Command, args []string) error {
 		csConfig.Cscli.PrometheusUrl = url
 	}
 
-	InspectItem(args[0], cwhub.SCENARIOS)
+	noMetrics, err := flags.GetBool("no-metrics")
+	if err != nil {
+		return err
+	}
+
+	for _, name := range args {
+		if err = InspectItem(name, cwhub.SCENARIOS, noMetrics); err != nil {
+			return err
+		}
+	}
 
 	return nil
 }
 
 func NewCmdScenariosInspect() *cobra.Command {
 	cmdScenariosInspect := &cobra.Command{
-		Use:               "inspect [config]",
-		Short:             "Inspect given scenario",
-		Long:              `Inspect given scenario`,
-		Example:           `cscli scenarios inspect crowdsec/xxx`,
+		Use:               "inspect <scenario>",
+		Short:             "Inspect a scenario",
+		Long:              `Inspect a scenario`,
+		Example:           `cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/http-probing`,
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -253,6 +266,7 @@ func NewCmdScenariosInspect() *cobra.Command {
 
 	flags := cmdScenariosInspect.Flags()
 	flags.StringP("url", "u", "", "Prometheus url")
+	flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)")
 
 	return cmdScenariosInspect
 }
@@ -273,11 +287,12 @@ func runScenariosList(cmd *cobra.Command, args []string) error {
 
 func NewCmdScenariosList() *cobra.Command {
 	cmdScenariosList := &cobra.Command{
-		Use:   "list [config]",
-		Short: "List all scenario(s) or given one",
-		Long:  `List all scenario(s) or given one`,
+		Use:   "list [scenario]...",
+		Short: "List scenarios",
+		Long:  `List of installed/available/specified scenarios`,
 		Example: `cscli scenarios list
-cscli scenarios list crowdsecurity/xxx`,
+cscli scenarios list -a
+cscli scenarios list crowdsecurity/ssh-bf crowdsecurity/http-probing`,
 		DisableAutoGenTag: true,
 		RunE:              runScenariosList,
 	}

+ 1 - 5
cmd/crowdsec-cli/support.go

@@ -58,10 +58,6 @@ func stripAnsiString(str string) string {
 
 func collectMetrics() ([]byte, []byte, error) {
 	log.Info("Collecting prometheus metrics")
-	err := csConfig.LoadPrometheus()
-	if err != nil {
-		return nil, nil, err
-	}
 
 	if csConfig.Cscli.PrometheusUrl == "" {
 		log.Warn("No Prometheus URL configured, metrics will not be collected")
@@ -69,7 +65,7 @@ func collectMetrics() ([]byte, []byte, error) {
 	}
 
 	humanMetrics := bytes.NewBuffer(nil)
-	err = FormatPrometheusMetrics(humanMetrics, csConfig.Cscli.PrometheusUrl, "human")
+	err := FormatPrometheusMetrics(humanMetrics, csConfig.Cscli.PrometheusUrl, "human")
 
 	if err != nil {
 		return nil, nil, fmt.Errorf("could not fetch promtheus metrics: %s", err)

+ 20 - 13
cmd/crowdsec-cli/utils.go

@@ -41,9 +41,9 @@ func printHelp(cmd *cobra.Command) {
 func Suggest(itemType string, baseItem string, suggestItem string, score int, ignoreErr bool) {
 	errMsg := ""
 	if score < MaxDistance {
-		errMsg = fmt.Sprintf("unable to find %s '%s', did you mean %s ?", itemType, baseItem, suggestItem)
+		errMsg = fmt.Sprintf("can't find '%s' in %s, did you mean %s?", baseItem, itemType, suggestItem)
 	} else {
-		errMsg = fmt.Sprintf("unable to find %s '%s'", itemType, baseItem)
+		errMsg = fmt.Sprintf("can't find '%s' in %s", baseItem, itemType)
 	}
 	if ignoreErr {
 		log.Error(errMsg)
@@ -185,33 +185,40 @@ func ListItems(out io.Writer, itemTypes []string, args []string, showType bool,
 	}
 }
 
-func InspectItem(name string, objecitemType string) {
-
-	hubItem := cwhub.GetItem(objecitemType, name)
+func InspectItem(name string, itemType string, noMetrics bool) error {
+	hubItem := cwhub.GetItem(itemType, name)
 	if hubItem == nil {
-		log.Fatalf("unable to retrieve item.")
+		return fmt.Errorf("can't find '%s' in %s", name, itemType)
 	}
-	var b []byte
-	var err error
+
+	var (
+		b []byte
+		err error
+	)
+
 	switch csConfig.Cscli.Output {
 	case "human", "raw":
 		b, err = yaml.Marshal(*hubItem)
 		if err != nil {
-			log.Fatalf("unable to marshal item : %s", err)
+			return fmt.Errorf("unable to marshal item: %s", err)
 		}
 	case "json":
 		b, err = json.MarshalIndent(*hubItem, "", " ")
 		if err != nil {
-			log.Fatalf("unable to marshal item : %s", err)
+			return fmt.Errorf("unable to marshal item: %s", err)
 		}
 	}
+
 	fmt.Printf("%s", string(b))
-	if csConfig.Cscli.Output == "json" || csConfig.Cscli.Output == "raw" {
-		return
+
+	if noMetrics || csConfig.Cscli.Output == "json" || csConfig.Cscli.Output == "raw" {
+		return nil
 	}
 
-	fmt.Printf("\nCurrent metrics : \n")
+	fmt.Printf("\nCurrent metrics: \n")
 	ShowMetrics(hubItem)
+
+	return nil
 }
 
 func manageCliDecisionAlerts(ip *string, ipRange *string, scope *string, value *string) error {

+ 8 - 0
pkg/csconfig/cscli.go

@@ -1,5 +1,9 @@
 package csconfig
 
+import (
+	"fmt"
+)
+
 /*cscli specific config, such as hub directory*/
 type CscliCfg struct {
 	Output             string            `yaml:"output,omitempty"`
@@ -27,5 +31,9 @@ func (c *Config) LoadCSCLI() error {
 	c.Cscli.HubDir = c.ConfigPaths.HubDir
 	c.Cscli.HubIndexFile = c.ConfigPaths.HubIndexFile
 
+	if c.Prometheus.ListenAddr != "" && c.Prometheus.ListenPort != 0 {
+		c.Cscli.PrometheusUrl = fmt.Sprintf("http://%s:%d/metrics", c.Prometheus.ListenAddr, c.Prometheus.ListenPort)
+	}
+
 	return nil
 }

+ 11 - 4
pkg/csconfig/cscli_test.go

@@ -38,12 +38,19 @@ func TestLoadCSCLI(t *testing.T) {
 					HubDir:       "./hub",
 					HubIndexFile: "./hub/.index.json",
 				},
+				Prometheus: &PrometheusCfg{
+					Enabled:    true,
+					Level:      "full",
+					ListenAddr: "127.0.0.1",
+					ListenPort: 6060,
+				},
 			},
 			expected: &CscliCfg{
-				ConfigDir:    configDirFullPath,
-				DataDir:      dataFullPath,
-				HubDir:       hubFullPath,
-				HubIndexFile: hubIndexFileFullPath,
+				ConfigDir:     configDirFullPath,
+				DataDir:       dataFullPath,
+				HubDir:        hubFullPath,
+				HubIndexFile:  hubIndexFileFullPath,
+				PrometheusUrl: "http://127.0.0.1:6060/metrics",
 			},
 		},
 		{

+ 0 - 11
pkg/csconfig/prometheus.go

@@ -1,19 +1,8 @@
 package csconfig
 
-import "fmt"
-
 type PrometheusCfg struct {
 	Enabled    bool   `yaml:"enabled"`
 	Level      string `yaml:"level"` //aggregated|full
 	ListenAddr string `yaml:"listen_addr"`
 	ListenPort int    `yaml:"listen_port"`
 }
-
-func (c *Config) LoadPrometheus() error {
-	if c.Cscli != nil && c.Cscli.PrometheusUrl == "" && c.Prometheus != nil {
-		if c.Prometheus.ListenAddr != "" && c.Prometheus.ListenPort != 0 {
-			c.Cscli.PrometheusUrl = fmt.Sprintf("http://%s:%d/metrics", c.Prometheus.ListenAddr, c.Prometheus.ListenPort)
-		}
-	}
-	return nil
-}

+ 0 - 42
pkg/csconfig/prometheus_test.go

@@ -1,42 +0,0 @@
-package csconfig
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/crowdsecurity/go-cs-lib/cstest"
-)
-
-func TestLoadPrometheus(t *testing.T) {
-	tests := []struct {
-		name        string
-		input       *Config
-		expectedURL string
-		expectedErr string
-	}{
-		{
-			name: "basic valid configuration",
-			input: &Config{
-				Prometheus: &PrometheusCfg{
-					Enabled:    true,
-					Level:      "full",
-					ListenAddr: "127.0.0.1",
-					ListenPort: 6060,
-				},
-				Cscli: &CscliCfg{},
-			},
-			expectedURL: "http://127.0.0.1:6060/metrics",
-		},
-	}
-
-	for _, tc := range tests {
-		tc := tc
-		t.Run(tc.name, func(t *testing.T) {
-			err := tc.input.LoadPrometheus()
-			cstest.RequireErrorContains(t, err, tc.expectedErr)
-
-			require.Equal(t, tc.expectedURL, tc.input.Cscli.PrometheusUrl)
-		})
-	}
-}

+ 10 - 8
pkg/cwhub/helpers.go

@@ -107,7 +107,7 @@ func RemoveMany(csConfig *csconfig.Config, itemType string, name string, all boo
 	if name != "" {
 		item := GetItem(itemType, name)
 		if item == nil {
-			return fmt.Errorf("unable to retrieve: %s", name)
+			return fmt.Errorf("can't find '%s' in %s", name, itemType)
 		}
 
 		err := DisableItem(csConfig.Hub, item, purge, forceAction)
@@ -151,7 +151,7 @@ func RemoveMany(csConfig *csconfig.Config, itemType string, name string, all boo
 	return nil
 }
 
-func UpgradeConfig(csConfig *csconfig.Config, itemType string, name string, force bool) {
+func UpgradeConfig(csConfig *csconfig.Config, itemType string, name string, force bool) error {
 	updated := 0
 	found := false
 
@@ -166,17 +166,17 @@ func UpgradeConfig(csConfig *csconfig.Config, itemType string, name string, forc
 		}
 
 		if !v.Downloaded {
-			log.Warningf("%s : not downloaded, please install.", v.Name)
+			log.Warningf("%s: not downloaded, please install.", v.Name)
 			continue
 		}
 
 		found = true
 
 		if v.UpToDate {
-			log.Infof("%s : up-to-date", v.Name)
+			log.Infof("%s: up-to-date", v.Name)
 
 			if err := DownloadDataIfNeeded(csConfig.Hub, v, force); err != nil {
-				log.Fatalf("%s : download failed : %v", v.Name, err)
+				return fmt.Errorf("%s: download failed: %w", v.Name, err)
 			}
 
 			if !force {
@@ -185,7 +185,7 @@ func UpgradeConfig(csConfig *csconfig.Config, itemType string, name string, forc
 		}
 
 		if err := DownloadLatest(csConfig.Hub, &v, force, true); err != nil {
-			log.Fatalf("%s : download failed : %v", v.Name, err)
+			return fmt.Errorf("%s: download failed: %w", v.Name, err)
 		}
 
 		if !v.UpToDate {
@@ -203,14 +203,14 @@ func UpgradeConfig(csConfig *csconfig.Config, itemType string, name string, forc
 		}
 
 		if err := AddItem(itemType, v); err != nil {
-			log.Fatalf("unable to add %s: %v", v.Name, err)
+			return fmt.Errorf("unable to add %s: %w", v.Name, err)
 		}
 	}
 
 	if !found && name == "" {
 		log.Infof("No %s installed, nothing to upgrade", itemType)
 	} else if !found {
-		log.Errorf("Item '%s' not found in hub", name)
+		log.Errorf("can't find '%s' in %s", name, itemType)
 	} else if updated == 0 && found {
 		if name == "" {
 			log.Infof("All %s are already up-to-date", itemType)
@@ -220,4 +220,6 @@ func UpgradeConfig(csConfig *csconfig.Config, itemType string, name string, forc
 	} else if updated != 0 {
 		log.Infof("Upgraded %d items", updated)
 	}
+
+	return nil
 }

+ 9 - 5
pkg/cwhub/helpers_test.go

@@ -45,7 +45,8 @@ func TestUpgradeConfigNewScenarioInCollection(t *testing.T) {
 	require.False(t, hubIdx[COLLECTIONS]["crowdsecurity/test_collection"].UpToDate)
 	require.False(t, hubIdx[COLLECTIONS]["crowdsecurity/test_collection"].Tainted)
 
-	UpgradeConfig(cfg, COLLECTIONS, "crowdsecurity/test_collection", false)
+	err := UpgradeConfig(cfg, COLLECTIONS, "crowdsecurity/test_collection", false)
+	require.NoError(t, err)
 	assertCollectionDepsInstalled(t, "crowdsecurity/test_collection")
 
 	require.True(t, hubIdx[SCENARIOS]["crowdsecurity/barfoo_scenario"].Downloaded)
@@ -85,11 +86,12 @@ func TestUpgradeConfigInDisabledScenarioShouldNotBeInstalled(t *testing.T) {
 	require.True(t, hubIdx[COLLECTIONS]["crowdsecurity/test_collection"].Installed)
 	require.True(t, hubIdx[COLLECTIONS]["crowdsecurity/test_collection"].UpToDate)
 
-	if err := UpdateHubIdx(cfg.Hub); err != nil {
+	if err = UpdateHubIdx(cfg.Hub); err != nil {
 		t.Fatalf("failed to download index : %s", err)
 	}
 
-	UpgradeConfig(cfg, COLLECTIONS, "crowdsecurity/test_collection", false)
+	err = UpgradeConfig(cfg, COLLECTIONS, "crowdsecurity/test_collection", false)
+	require.NoError(t, err)
 
 	getHubIdxOrFail(t)
 	require.False(t, hubIdx[SCENARIOS]["crowdsecurity/foobar_scenario"].Installed)
@@ -141,14 +143,16 @@ func TestUpgradeConfigNewScenarioIsInstalledWhenReferencedScenarioIsDisabled(t *
 	// we just removed. Nor should it install the newly added scenario
 	pushUpdateToCollectionInHub()
 
-	if err := UpdateHubIdx(cfg.Hub); err != nil {
+	if err = UpdateHubIdx(cfg.Hub); err != nil {
 		t.Fatalf("failed to download index : %s", err)
 	}
 
 	require.False(t, hubIdx[SCENARIOS]["crowdsecurity/foobar_scenario"].Installed)
 	getHubIdxOrFail(t)
 
-	UpgradeConfig(cfg, COLLECTIONS, "crowdsecurity/test_collection", false)
+	err = UpgradeConfig(cfg, COLLECTIONS, "crowdsecurity/test_collection", false)
+	require.NoError(t, err)
+
 	getHubIdxOrFail(t)
 	require.False(t, hubIdx[SCENARIOS]["crowdsecurity/foobar_scenario"].Installed)
 	require.True(t, hubIdx[SCENARIOS]["crowdsecurity/barfoo_scenario"].Installed)

+ 0 - 145
test/bats/20_collections.bats

@@ -1,145 +0,0 @@
-#!/usr/bin/env bats
-# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
-
-set -u
-
-setup_file() {
-    load "../lib/setup_file.sh"
-}
-
-teardown_file() {
-    load "../lib/teardown_file.sh"
-}
-
-setup() {
-    load "../lib/setup.sh"
-    ./instance-data load
-    ./instance-crowdsec start
-}
-
-teardown() {
-    ./instance-crowdsec stop
-}
-
-#----------
-
-@test "we can list collections" {
-    rune -0 cscli collections list
-}
-
-@test "there are 2 collections (linux and sshd)" {
-    rune -0 cscli collections list -o json
-    rune -0 jq '.collections | length' <(output)
-    assert_output 2
-}
-
-@test "can install a collection (as a regular user) and remove it" {
-    # collection is not installed
-    rune -0 cscli collections list -o json
-    rune -0 jq -r '.collections[].name' <(output)
-    refute_line "crowdsecurity/mysql"
-
-    # we install it
-    rune -0 cscli collections install crowdsecurity/mysql -o human
-    assert_stderr --partial "Enabled crowdsecurity/mysql"
-
-    # it has been installed
-    rune -0 cscli collections list -o json
-    rune -0 jq -r '.collections[].name' <(output)
-    assert_line "crowdsecurity/mysql"
-
-    # we install it
-    rune -0 cscli collections remove crowdsecurity/mysql -o human
-    assert_stderr --partial "Removed symlink [crowdsecurity/mysql]"
-
-    # it has been removed
-    rune -0 cscli collections list -o json
-    rune -0 jq -r '.collections[].name' <(output)
-    refute_line "crowdsecurity/mysql"
-}
-
-@test "must use --force to remove a collection that belongs to another, which becomes tainted" {
-    # we expect no error since we may have multiple collections, some removed and some not
-    rune -0 cscli collections remove crowdsecurity/sshd
-    assert_stderr --partial "crowdsecurity/sshd belongs to other collections"
-    assert_stderr --partial "[crowdsecurity/linux]"
-
-    rune -0 cscli collections remove crowdsecurity/sshd --force
-    assert_stderr --partial "Removed symlink [crowdsecurity/sshd]"
-    rune -0 cscli collections inspect crowdsecurity/linux -o json
-    rune -0 jq -r '.tainted' <(output)
-    assert_output "true"
-}
-
-@test "can remove a collection" {
-    rune -0 cscli collections remove crowdsecurity/linux
-    assert_stderr --partial "Removed"
-    assert_stderr --regexp   ".*for the new configuration to be effective."
-    rune -0 cscli collections inspect crowdsecurity/linux -o human
-    assert_line 'installed: false'
-}
-
-@test "collections delete is an alias for collections remove" {
-    rune -0 cscli collections delete crowdsecurity/linux
-    assert_stderr --partial "Removed"
-    assert_stderr --regexp   ".*for the new configuration to be effective."
-}
-
-@test "removing a collection that does not exist is noop" {
-    rune -0 cscli collections remove crowdsecurity/apache2
-    refute_stderr --partial "Removed"
-    assert_stderr --regexp   ".*for the new configuration to be effective."
-}
-
-@test "can remove a removed collection" {
-    rune -0 cscli collections install crowdsecurity/mysql
-    rune -0 cscli collections remove crowdsecurity/mysql
-    assert_stderr --partial "Removed"
-    rune -0 cscli collections remove crowdsecurity/mysql
-    refute_stderr --partial "Removed"
-}
-
-@test "can remove all collections" {
-    # we may have this too, from package installs
-    rune cscli parsers delete crowdsecurity/whitelists
-    rune -0 cscli collections remove --all
-    assert_stderr --partial "Removed symlink [crowdsecurity/sshd]"
-    assert_stderr --partial "Removed symlink [crowdsecurity/linux]"
-    rune -0 cscli hub list -o json
-    assert_json '{collections:[],parsers:[],postoverflows:[],scenarios:[]}'
-    rune -0 cscli collections remove --all
-    assert_stderr --partial 'Disabled 0 items'
-}
-
-@test "a taint bubbles up to the top collection" {
-    coll=crowdsecurity/nginx
-    subcoll=crowdsecurity/base-http-scenarios
-    scenario=crowdsecurity/http-crawl-non_statics
-
-    # install a collection with dependencies
-    rune -0 cscli collections install "$coll"
-
-    # the collection, subcollection and scenario are installed and not tainted
-    # we have to default to false because tainted is (as of 1.4.6) returned
-    # only when true
-    rune -0 cscli collections inspect "$coll" -o json
-    rune -0 jq -e '(.installed,.tainted|false)==(true,false)' <(output)
-    rune -0 cscli collections inspect "$subcoll" -o json
-    rune -0 jq -e '(.installed,.tainted|false)==(true,false)' <(output)
-    rune -0 cscli scenarios inspect "$scenario" -o json
-    rune -0 jq -e '(.installed,.tainted|false)==(true,false)' <(output)
-
-    # we taint the scenario
-    HUB_DIR=$(config_get '.config_paths.hub_dir')
-    yq e '.description="I am tainted"' -i "$HUB_DIR/scenarios/$scenario.yaml"
-
-    # the collection, subcollection and scenario are now tainted
-    rune -0 cscli scenarios inspect "$scenario" -o json
-    rune -0 jq -e '(.installed,.tainted)==(true,true)' <(output)
-    rune -0 cscli collections inspect "$subcoll" -o json
-    rune -0 jq -e '(.installed,.tainted)==(true,true)' <(output)
-    rune -0 cscli collections inspect "$coll" -o json
-    rune -0 jq -e '(.installed,.tainted)==(true,true)' <(output)
-}
-
-# TODO test download-only

+ 319 - 0
test/bats/20_hub_collections.bats

@@ -0,0 +1,319 @@
+#!/usr/bin/env bats
+# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
+
+set -u
+
+setup_file() {
+    load "../lib/setup_file.sh"
+    HUB_DIR=$(config_get '.config_paths.hub_dir')
+    export HUB_DIR
+    CONFIG_DIR=$(config_get '.config_paths.config_dir')
+    export CONFIG_DIR
+}
+
+teardown_file() {
+    load "../lib/teardown_file.sh"
+}
+
+setup() {
+    load "../lib/setup.sh"
+    load "../lib/bats-file/load.bash"
+    ./instance-data load
+    hub_uninstall_all
+    hub_min=$(jq <"$HUB_DIR/.index.json" 'del(..|.content?) | del(..|.long_description?) | del(..|.deprecated?) | del (..|.labels?)')
+    echo "$hub_min" >"$HUB_DIR/.index.json"
+}
+
+teardown() {
+    ./instance-crowdsec stop
+}
+
+#----------
+
+@test "cscli collections list" {
+    # no items
+    rune -0 cscli collections list
+    assert_output --partial "COLLECTIONS"
+    rune -0 cscli collections list -o json
+    assert_json '{collections:[]}'
+    rune -0 cscli collections list -o raw
+    assert_output 'name,status,version,description'
+
+    # some items
+    rune -0 cscli collections install crowdsecurity/sshd crowdsecurity/smb
+
+    rune -0 cscli collections list
+    assert_output --partial crowdsecurity/sshd
+    assert_output --partial crowdsecurity/smb
+    rune -0 grep -c enabled <(output)
+    assert_output "2"
+
+    rune -0 cscli collections list -o json
+    assert_output --partial crowdsecurity/sshd
+    assert_output --partial crowdsecurity/smb
+    rune -0 jq '.collections | length' <(output)
+    assert_output "2"
+
+    rune -0 cscli collections list -o raw
+    assert_output --partial crowdsecurity/sshd
+    assert_output --partial crowdsecurity/smb
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+}
+
+@test "cscli collections list -a" {
+    expected=$(jq <"$HUB_DIR/.index.json" -r '.collections | length')
+
+    rune -0 cscli collections list -a
+    rune -0 grep -c disabled <(output)
+    assert_output "$expected"
+
+    rune -0 cscli collections list -o json -a
+    rune -0 jq '.collections | length' <(output)
+    assert_output "$expected"
+
+    rune -0 cscli collections list -o raw -a
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "$expected"
+}
+
+
+@test "cscli collections list [collection]..." {
+    rune -0 cscli collections install crowdsecurity/sshd crowdsecurity/smb
+
+    # list one item
+    rune -0 cscli collections list crowdsecurity/sshd
+    assert_output --partial "crowdsecurity/sshd"
+    refute_output --partial "crowdsecurity/smb"
+
+    # list multiple items
+    rune -0 cscli collections list crowdsecurity/sshd crowdsecurity/smb
+    assert_output --partial "crowdsecurity/sshd"
+    assert_output --partial "crowdsecurity/smb"
+
+    rune -0 cscli collections list crowdsecurity/sshd -o json
+    rune -0 jq '.collections | length' <(output)
+    assert_output "1"
+    rune -0 cscli collections list crowdsecurity/sshd crowdsecurity/smb -o json
+    rune -0 jq '.collections | length' <(output)
+    assert_output "2"
+
+    rune -0 cscli collections list crowdsecurity/sshd -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "1"
+    rune -0 cscli collections list crowdsecurity/sshd crowdsecurity/smb -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+}
+
+@test "cscli collections list [collection]... (not installed / not existing)" {
+    skip "not implemented yet"
+    # not installed
+    rune -1 cscli collections list crowdsecurity/sshd
+    # not existing
+    rune -1 cscli collections list blahblah/blahblah
+}
+
+@test "cscli collections install [collection]..." {
+    rune -1 cscli collections install
+    assert_stderr --partial 'requires at least 1 arg(s), only received 0'
+
+    # not in hub
+    rune -1 cscli collections install crowdsecurity/blahblah
+    assert_stderr --partial "can't find 'crowdsecurity/blahblah' in collections"
+
+    # simple install
+    rune -0 cscli collections install crowdsecurity/sshd
+    rune -0 cscli collections inspect crowdsecurity/sshd --no-metrics
+    assert_output --partial 'crowdsecurity/sshd'
+    assert_output --partial 'installed: true'
+
+    # autocorrect
+    rune -1 cscli collections install crowdsecurity/ssshd
+    assert_stderr --partial "can't find 'crowdsecurity/ssshd' in collections, did you mean crowdsecurity/sshd?"
+
+    # install multiple
+    rune -0 cscli collections install crowdsecurity/sshd crowdsecurity/smb
+    rune -0 cscli collections inspect crowdsecurity/sshd --no-metrics
+    assert_output --partial 'crowdsecurity/sshd'
+    assert_output --partial 'installed: true'
+    rune -0 cscli collections inspect crowdsecurity/smb --no-metrics
+    assert_output --partial 'crowdsecurity/smb'
+    assert_output --partial 'installed: true'
+}
+
+@test "cscli collections install [collection]... (file location and download-only)" {
+    # simple install
+    rune -0 cscli collections install crowdsecurity/linux --download-only
+    rune -0 cscli collections inspect crowdsecurity/linux --no-metrics
+    assert_output --partial 'crowdsecurity/linux'
+    assert_output --partial 'installed: false'
+    assert_file_exists "$HUB_DIR/collections/crowdsecurity/linux.yaml"
+    assert_file_not_exists "$CONFIG_DIR/collections/linux.yaml"
+
+    rune -0 cscli collections install crowdsecurity/linux
+    assert_file_exists "$CONFIG_DIR/collections/linux.yaml"
+}
+
+
+@test "cscli collections inspect [collection]..." {
+    rune -1 cscli collections inspect
+    assert_stderr --partial 'requires at least 1 arg(s), only received 0'
+    ./instance-crowdsec start
+
+    rune -1 cscli collections inspect blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in collections"
+
+    # one item
+    rune -0 cscli collections inspect crowdsecurity/sshd --no-metrics
+    assert_line 'type: collections'
+    assert_line 'name: crowdsecurity/sshd'
+    assert_line 'author: crowdsecurity'
+    assert_line 'remote_path: collections/crowdsecurity/sshd.yaml'
+    assert_line 'installed: false'
+    refute_line --partial 'Current metrics:'
+
+    # one item, with metrics
+    rune -0 cscli collections inspect crowdsecurity/sshd
+    assert_line --partial 'Current metrics:'
+
+    # one item, json
+    rune -0 cscli collections inspect crowdsecurity/sshd -o json
+    rune -0 jq -c '[.type, .name, .author, .path, .installed]' <(output)
+    # XXX: .installed is missing -- not false
+    assert_json '["collections","crowdsecurity/sshd","crowdsecurity","collections/crowdsecurity/sshd.yaml",null]'
+
+    # one item, raw
+    rune -0 cscli collections inspect crowdsecurity/sshd -o raw
+    assert_line 'type: collections'
+    assert_line 'name: crowdsecurity/sshd'
+    assert_line 'author: crowdsecurity'
+    assert_line 'remote_path: collections/crowdsecurity/sshd.yaml'
+    assert_line 'installed: false'
+    refute_line --partial 'Current metrics:'
+
+    # multiple items
+    rune -0 cscli collections inspect crowdsecurity/sshd crowdsecurity/smb --no-metrics
+    assert_output --partial 'crowdsecurity/sshd'
+    assert_output --partial 'crowdsecurity/smb'
+    rune -1 grep -c 'Current metrics:' <(output)
+    assert_output "0"
+
+    # multiple items, with metrics
+    rune -0 cscli collections inspect crowdsecurity/sshd crowdsecurity/smb
+    rune -0 grep -c 'Current metrics:' <(output)
+    assert_output "2"
+
+    # multiple items, json
+    rune -0 cscli collections inspect crowdsecurity/sshd crowdsecurity/smb -o json
+    rune -0 jq -sc '[.[] | [.type, .name, .author, .path, .installed]]' <(output)
+    assert_json '[["collections","crowdsecurity/sshd","crowdsecurity","collections/crowdsecurity/sshd.yaml",null],["collections","crowdsecurity/smb","crowdsecurity","collections/crowdsecurity/smb.yaml",null]]'
+
+    # multiple items, raw
+    rune -0 cscli collections inspect crowdsecurity/sshd crowdsecurity/smb -o raw
+    assert_output --partial 'crowdsecurity/sshd'
+    assert_output --partial 'crowdsecurity/smb'
+    run -1 grep -c 'Current metrics:' <(output)
+    assert_output "0"
+}
+
+@test "cscli collections remove [collection]..." {
+    rune -1 cscli collections remove
+    assert_stderr --partial "specify at least one collection to remove or '--all'"
+
+    rune -1 cscli collections remove blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in collections"
+
+    # XXX: we can however remove a real item if it's not installed, or already removed
+    rune -0 cscli collections remove crowdsecurity/sshd
+
+    # install, then remove, check files
+    rune -0 cscli collections install crowdsecurity/sshd
+    assert_file_exists "$CONFIG_DIR/collections/sshd.yaml"
+    rune -0 cscli collections remove crowdsecurity/sshd
+    assert_file_not_exists "$CONFIG_DIR/collections/sshd.yaml"
+
+    # delete is an alias for remove
+    rune -0 cscli collections install crowdsecurity/sshd
+    assert_file_exists "$CONFIG_DIR/collections/sshd.yaml"
+    rune -0 cscli collections delete crowdsecurity/sshd
+    assert_file_not_exists "$CONFIG_DIR/collections/sshd.yaml"
+
+    # purge
+    assert_file_exists "$HUB_DIR/collections/crowdsecurity/sshd.yaml"
+    rune -0 cscli collections remove crowdsecurity/sshd --purge
+    assert_file_not_exists "$HUB_DIR/collections/crowdsecurity/sshd.yaml"
+
+    rune -0 cscli collections install crowdsecurity/sshd crowdsecurity/smb
+
+    # --all
+    rune -0 cscli collections list -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+
+    rune -0 cscli collections remove --all
+
+    rune -0 cscli collections list -o raw
+    rune -1 grep -vc 'name,status,version,description' <(output)
+    assert_output "0"
+}
+
+@test "cscli collections upgrade [collection]..." {
+    rune -1 cscli collections upgrade
+    assert_stderr --partial "specify at least one collection to upgrade or '--all'"
+
+    # XXX: should this return 1 instead of log.Error?
+    rune -0 cscli collections upgrade blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in collections"
+
+    # XXX: same message if the item exists but is not installed, this is confusing
+    rune -0 cscli collections upgrade crowdsecurity/sshd
+    assert_stderr --partial "can't find 'crowdsecurity/sshd' in collections"
+
+    # hash of an empty file
+    sha256_empty="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+
+    # add version 0.0 to the hub
+    new_hub=$(jq --arg DIGEST "$sha256_empty" <"$HUB_DIR/.index.json" '. * {collections:{"crowdsecurity/sshd":{"versions":{"0.0":{"digest":$DIGEST, "deprecated": false}}}}}')
+    echo "$new_hub" >"$HUB_DIR/.index.json"
+ 
+    rune -0 cscli collections install crowdsecurity/sshd
+
+    # bring the file to v0.0
+    truncate -s 0 "$CONFIG_DIR/collections/sshd.yaml"
+    rune -0 cscli collections inspect crowdsecurity/sshd -o json
+    rune -0 jq -e '.local_version=="0.0"' <(output)
+
+    # upgrade
+    rune -0 cscli collections upgrade crowdsecurity/sshd
+    rune -0 cscli collections inspect crowdsecurity/sshd -o json
+    rune -0 jq -e '.local_version==.version' <(output)
+
+    # taint
+    echo "dirty" >"$CONFIG_DIR/collections/sshd.yaml"
+    # XXX: should return error
+    rune -0 cscli collections upgrade crowdsecurity/sshd
+    assert_stderr --partial "crowdsecurity/sshd is tainted, --force to overwrite"
+    rune -0 cscli collections inspect crowdsecurity/sshd -o json
+    rune -0 jq -e '.local_version=="?"' <(output)
+
+    # force upgrade with taint
+    rune -0 cscli collections upgrade crowdsecurity/sshd --force
+    rune -0 cscli collections inspect crowdsecurity/sshd -o json
+    rune -0 jq -e '.local_version==.version' <(output)
+
+    # multiple items
+    rune -0 cscli collections install crowdsecurity/smb
+    echo "dirty" >"$CONFIG_DIR/collections/sshd.yaml"
+    echo "dirty" >"$CONFIG_DIR/collections/smb.yaml"
+    rune -0 cscli collections list -o json
+    rune -0 jq -e '[.collections[].local_version]==["?","?"]' <(output)
+    rune -0 cscli collections upgrade crowdsecurity/sshd crowdsecurity/smb
+    rune -0 jq -e '[.collections[].local_version]==[.collections[].version]' <(output)
+
+    # upgrade all
+    echo "dirty" >"$CONFIG_DIR/collections/sshd.yaml"
+    echo "dirty" >"$CONFIG_DIR/collections/smb.yaml"
+    rune -0 cscli collections upgrade --all
+    rune -0 jq -e '[.collections[].local_version]==[.collections[].version]' <(output)
+}

+ 86 - 0
test/bats/20_hub_collections_dep.bats

@@ -0,0 +1,86 @@
+#!/usr/bin/env bats
+# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
+
+set -u
+
+setup_file() {
+    load "../lib/setup_file.sh"
+    HUB_DIR=$(config_get '.config_paths.hub_dir')
+    export HUB_DIR
+    CONFIG_DIR=$(config_get '.config_paths.config_dir')
+    export CONFIG_DIR
+}
+
+teardown_file() {
+    load "../lib/teardown_file.sh"
+}
+
+setup() {
+    load "../lib/setup.sh"
+    load "../lib/bats-file/load.bash"
+    ./instance-data load
+    hub_uninstall_all
+    hub_min=$(jq <"$HUB_DIR/.index.json" 'del(..|.content?) | del(..|.long_description?) | del(..|.deprecated?) | del (..|.labels?)')
+    echo "$hub_min" >"$HUB_DIR/.index.json"
+}
+
+teardown() {
+    ./instance-crowdsec stop
+}
+
+#----------
+
+@test "cscli collections (dependencies)" {
+    # inject a dependency: smb requires sshd
+    hub_dep=$(jq <"$HUB_DIR/.index.json" '. * {collections:{"crowdsecurity/smb":{collections:["crowdsecurity/sshd"]}}}')
+    echo "$hub_dep" >"$HUB_DIR/.index.json"
+
+    # verify that installing smb brings sshd
+    rune -0 cscli collections install crowdsecurity/smb
+    rune -0 cscli collections list -o json
+    rune -0 jq -e '[.collections[].name]==["crowdsecurity/smb","crowdsecurity/sshd"]' <(output)
+
+    # verify that removing smb removes sshd too
+    rune -0 cscli collections remove crowdsecurity/smb
+    rune -0 cscli collections list -o json
+    rune -0 jq -e '.collections | length == 0' <(output)
+
+    # we can't remove sshd without --force
+    rune -0 cscli collections install crowdsecurity/smb
+    # XXX: should this be an error?
+    rune -0 cscli collections remove crowdsecurity/sshd
+    assert_stderr --partial "crowdsecurity/sshd belongs to other collections: [crowdsecurity/smb]"
+    assert_stderr --partial "Run 'sudo cscli collections remove crowdsecurity/sshd --force' if you want to force remove this sub collection"
+    rune -0 cscli collections list -o json
+    rune -0 jq -c '[.collections[].name]' <(output)
+    assert_json '["crowdsecurity/smb","crowdsecurity/sshd"]'
+
+    # use the --force
+    rune -0 cscli collections remove crowdsecurity/sshd --force
+    rune -0 cscli collections list -o json
+    rune -0 jq -c '[.collections[].name]' <(output)
+    assert_json '["crowdsecurity/smb"]'
+
+    # and now smb is tainted!
+    rune -0 cscli collections inspect crowdsecurity/smb -o json
+    rune -0 jq -e '.tainted//false==true' <(output)
+    rune -0 cscli collections remove crowdsecurity/smb --force
+
+    # empty
+    rune -0 cscli collections list -o json
+    rune -0 jq -e '.collections | length == 0' <(output)
+
+    # reinstall
+    rune -0 cscli collections install crowdsecurity/smb --force
+
+    # taint on sshd means smb is tainted as well
+    rune -0 cscli collections inspect crowdsecurity/smb -o json
+    jq -e '.tainted//false==false' <(output)
+    echo "dirty" >"$CONFIG_DIR/collections/sshd.yaml"
+    rune -0 cscli collections inspect crowdsecurity/smb -o json
+    jq -e '.tainted//false==true' <(output)
+
+    # now we can't remove smb without --force
+    rune -1 cscli collections remove crowdsecurity/smb
+    assert_stderr --partial "unable to disable crowdsecurity/smb: crowdsecurity/smb is tainted, use '--force' to overwrite"
+}

+ 416 - 0
test/bats/20_hub_parsers.bats

@@ -0,0 +1,416 @@
+#!/usr/bin/env bats
+# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
+
+set -u
+
+setup_file() {
+    load "../lib/setup_file.sh"
+    HUB_DIR=$(config_get '.config_paths.hub_dir')
+    export HUB_DIR
+    CONFIG_DIR=$(config_get '.config_paths.config_dir')
+    export CONFIG_DIR
+}
+
+teardown_file() {
+    load "../lib/teardown_file.sh"
+}
+
+setup() {
+    load "../lib/setup.sh"
+    load "../lib/bats-file/load.bash"
+    ./instance-data load
+    hub_uninstall_all
+    # XXX: remove all "content" fields from the index, to make sure
+    # XXX: we don't rely on it in any way
+    hub_min=$(jq <"$HUB_DIR/.index.json" 'del(..|.content?) | del(..|.long_description?) | del(..|.deprecated?) | del (..|.labels?)')
+    echo "$hub_min" >"$HUB_DIR/.index.json"
+}
+
+teardown() {
+    ./instance-crowdsec stop
+}
+
+#----------
+
+@test "cscli parsers list" {
+    # no items
+    rune -0 cscli parsers list
+    assert_output --partial "PARSERS"
+    rune -0 cscli parsers list -o json
+    assert_json '{parsers:[]}'
+    rune -0 cscli parsers list -o raw
+    assert_output 'name,status,version,description'
+
+    # some items
+    rune -0 cscli parsers install crowdsecurity/whitelists crowdsecurity/windows-auth
+
+    rune -0 cscli parsers list
+    assert_output --partial crowdsecurity/whitelists
+    assert_output --partial crowdsecurity/windows-auth
+    rune -0 grep -c enabled <(output)
+    assert_output "2"
+
+    rune -0 cscli parsers list -o json
+    assert_output --partial crowdsecurity/whitelists
+    assert_output --partial crowdsecurity/windows-auth
+    rune -0 jq '.parsers | length' <(output)
+    assert_output "2"
+
+    rune -0 cscli parsers list -o raw
+    assert_output --partial crowdsecurity/whitelists
+    assert_output --partial crowdsecurity/windows-auth
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+}
+
+@test "cscli parsers list -a" {
+    expected=$(jq <"$HUB_DIR/.index.json" -r '.parsers | length')
+
+    rune -0 cscli parsers list -a
+    rune -0 grep -c disabled <(output)
+    assert_output "$expected"
+
+    rune -0 cscli parsers list -o json -a
+    rune -0 jq '.parsers | length' <(output)
+    assert_output "$expected"
+
+    rune -0 cscli parsers list -o raw -a
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "$expected"
+}
+
+
+@test "cscli parsers list [parser]..." {
+    rune -0 cscli parsers install crowdsecurity/whitelists crowdsecurity/windows-auth
+
+    # list one item
+    rune -0 cscli parsers list crowdsecurity/whitelists
+    assert_output --partial "crowdsecurity/whitelists"
+    refute_output --partial "crowdsecurity/windows-auth"
+
+    # list multiple items
+    rune -0 cscli parsers list crowdsecurity/whitelists crowdsecurity/windows-auth
+    assert_output --partial "crowdsecurity/whitelists"
+    assert_output --partial "crowdsecurity/windows-auth"
+
+    rune -0 cscli parsers list crowdsecurity/whitelists -o json
+    rune -0 jq '.parsers | length' <(output)
+    assert_output "1"
+    rune -0 cscli parsers list crowdsecurity/whitelists crowdsecurity/windows-auth -o json
+    rune -0 jq '.parsers | length' <(output)
+    assert_output "2"
+
+    rune -0 cscli parsers list crowdsecurity/whitelists -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "1"
+    rune -0 cscli parsers list crowdsecurity/whitelists crowdsecurity/windows-auth -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+}
+
+@test "cscli parsers list [parser]... (not installed / not existing)" {
+    skip "not implemented yet"
+    # not installed
+    rune -1 cscli parsers list crowdsecurity/whitelists
+    # not existing
+    rune -1 cscli parsers list blahblah/blahblah
+}
+
+@test "cscli parsers install [parser]..." {
+    rune -1 cscli parsers install
+    assert_stderr --partial 'requires at least 1 arg(s), only received 0'
+
+    # not in hub
+    rune -1 cscli parsers install crowdsecurity/blahblah
+    assert_stderr --partial "can't find 'crowdsecurity/blahblah' in parsers"
+
+    # simple install
+    rune -0 cscli parsers install crowdsecurity/whitelists
+    rune -0 cscli parsers inspect crowdsecurity/whitelists --no-metrics
+    assert_output --partial 'crowdsecurity/whitelists'
+    assert_output --partial 'installed: true'
+
+    # autocorrect
+    rune -1 cscli parsers install crowdsecurity/sshd-logz
+    assert_stderr --partial "can't find 'crowdsecurity/sshd-logz' in parsers, did you mean crowdsecurity/sshd-logs?"
+
+    # install multiple
+    rune -0 cscli parsers install crowdsecurity/pgsql-logs crowdsecurity/postfix-logs
+    rune -0 cscli parsers inspect crowdsecurity/pgsql-logs --no-metrics
+    assert_output --partial 'crowdsecurity/pgsql-logs'
+    assert_output --partial 'installed: true'
+    rune -0 cscli parsers inspect crowdsecurity/postfix-logs --no-metrics
+    assert_output --partial 'crowdsecurity/postfix-logs'
+    assert_output --partial 'installed: true'
+}
+
+@test "cscli parsers install [parser]... (file location and download-only)" {
+    # simple install
+    rune -0 cscli parsers install crowdsecurity/whitelists --download-only
+    rune -0 cscli parsers inspect crowdsecurity/whitelists --no-metrics
+    assert_output --partial 'crowdsecurity/whitelists'
+    assert_output --partial 'installed: false'
+    assert_file_exists "$HUB_DIR/parsers/s02-enrich/crowdsecurity/whitelists.yaml"
+    assert_file_not_exists "$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+
+    rune -0 cscli parsers install crowdsecurity/whitelists
+    assert_file_exists "$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+}
+
+# XXX: test install with --force
+# XXX: test install with --ignore
+
+@test "cscli parsers inspect [parser]..." {
+    rune -1 cscli parsers inspect
+    assert_stderr --partial 'requires at least 1 arg(s), only received 0'
+    ./instance-crowdsec start
+
+    rune -1 cscli parsers inspect blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in parsers"
+
+    # one item
+    rune -0 cscli parsers inspect crowdsecurity/sshd-logs --no-metrics
+    assert_line 'type: parsers'
+    assert_line 'stage: s01-parse'
+    assert_line 'name: crowdsecurity/sshd-logs'
+    assert_line 'author: crowdsecurity'
+    assert_line 'remote_path: parsers/s01-parse/crowdsecurity/sshd-logs.yaml'
+    assert_line 'installed: false'
+    refute_line --partial 'Current metrics:'
+
+    # one item, with metrics
+    rune -0 cscli parsers inspect crowdsecurity/sshd-logs
+    assert_line --partial 'Current metrics:'
+
+    # one item, json
+    rune -0 cscli parsers inspect crowdsecurity/sshd-logs -o json
+    rune -0 jq -c '[.type, .stage, .name, .author, .path, .installed]' <(output)
+    # XXX: .installed is missing -- not false
+    assert_json '["parsers","s01-parse","crowdsecurity/sshd-logs","crowdsecurity","parsers/s01-parse/crowdsecurity/sshd-logs.yaml",null]'
+
+    # one item, raw
+    rune -0 cscli parsers inspect crowdsecurity/sshd-logs -o raw
+    assert_line 'type: parsers'
+    assert_line 'stage: s01-parse'
+    assert_line 'name: crowdsecurity/sshd-logs'
+    assert_line 'author: crowdsecurity'
+    assert_line 'remote_path: parsers/s01-parse/crowdsecurity/sshd-logs.yaml'
+    assert_line 'installed: false'
+    refute_line --partial 'Current metrics:'
+
+    # multiple items
+    rune -0 cscli parsers inspect crowdsecurity/sshd-logs crowdsecurity/whitelists --no-metrics
+    assert_output --partial 'crowdsecurity/sshd-logs'
+    assert_output --partial 'crowdsecurity/whitelists'
+    rune -1 grep -c 'Current metrics:' <(output)
+    assert_output "0"
+
+    # multiple items, with metrics
+    rune -0 cscli parsers inspect crowdsecurity/sshd-logs crowdsecurity/whitelists
+    rune -0 grep -c 'Current metrics:' <(output)
+    assert_output "2"
+
+    # multiple items, json
+    rune -0 cscli parsers inspect crowdsecurity/sshd-logs crowdsecurity/whitelists -o json
+    rune -0 jq -sc '[.[] | [.type, .stage, .name, .author, .path, .installed]]' <(output)
+    assert_json '[["parsers","s01-parse","crowdsecurity/sshd-logs","crowdsecurity","parsers/s01-parse/crowdsecurity/sshd-logs.yaml",null],["parsers","s02-enrich","crowdsecurity/whitelists","crowdsecurity","parsers/s02-enrich/crowdsecurity/whitelists.yaml",null]]'
+
+    # multiple items, raw
+    rune -0 cscli parsers inspect crowdsecurity/sshd-logs crowdsecurity/whitelists -o raw
+    assert_output --partial 'crowdsecurity/sshd-logs'
+    assert_output --partial 'crowdsecurity/whitelists'
+    run -1 grep -c 'Current metrics:' <(output)
+    assert_output "0"
+}
+
+@test "cscli parsers remove [parser]..." {
+    rune -1 cscli parsers remove
+    assert_stderr --partial "specify at least one parser to remove or '--all'"
+
+    rune -1 cscli parsers remove blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in parsers"
+
+    # XXX: we can however remove a real item if it's not installed, or already removed
+    rune -0 cscli parsers remove crowdsecurity/whitelists
+
+    # XXX: have the --force ignore uninstalled items
+    # XXX: maybe also with --purge
+
+    # install, then remove, check files
+    rune -0 cscli parsers install crowdsecurity/whitelists
+    assert_file_exists "$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+    rune -0 cscli parsers remove crowdsecurity/whitelists
+    assert_file_not_exists "$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+
+    # delete is an alias for remove
+    rune -0 cscli parsers install crowdsecurity/whitelists
+    assert_file_exists "$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+    rune -0 cscli parsers delete crowdsecurity/whitelists
+    assert_file_not_exists "$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+
+    # purge
+    assert_file_exists "$HUB_DIR/parsers/s02-enrich/crowdsecurity/whitelists.yaml"
+    rune -0 cscli parsers remove crowdsecurity/whitelists --purge
+    assert_file_not_exists "$HUB_DIR/parsers/s02-enrich/crowdsecurity/whitelists.yaml"
+
+    rune -0 cscli parsers install crowdsecurity/whitelists crowdsecurity/windows-auth
+
+    # --all
+    rune -0 cscli parsers list -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+
+    rune -0 cscli parsers remove --all
+
+    rune -0 cscli parsers list -o raw
+    rune -1 grep -vc 'name,status,version,description' <(output)
+    assert_output "0"
+}
+
+@test "cscli parsers upgrade [parser]..." {
+    rune -1 cscli parsers upgrade
+    assert_stderr --partial "specify at least one parser to upgrade or '--all'"
+
+    # XXX: should this return 1 instead of log.Error?
+    rune -0 cscli parsers upgrade blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in parsers"
+
+    # XXX: same message if the item exists but is not installed, this is confusing
+    rune -0 cscli parsers upgrade crowdsecurity/whitelists
+    assert_stderr --partial "can't find 'crowdsecurity/whitelists' in parsers"
+
+    # hash of an empty file
+    sha256_empty="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+
+    # add version 0.0 to the hub
+    new_hub=$(jq --arg DIGEST "$sha256_empty" <"$HUB_DIR/.index.json" '. * {parsers:{"crowdsecurity/whitelists":{"versions":{"0.0":{"digest":$DIGEST, "deprecated": false}}}}}')
+    echo "$new_hub" >"$HUB_DIR/.index.json"
+ 
+    rune -0 cscli parsers install crowdsecurity/whitelists
+
+    # bring the file to v0.0
+    truncate -s 0 "$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+    rune -0 cscli parsers inspect crowdsecurity/whitelists -o json
+    rune -0 jq -e '.local_version=="0.0"' <(output)
+
+    # upgrade
+    rune -0 cscli parsers upgrade crowdsecurity/whitelists
+    rune -0 cscli parsers inspect crowdsecurity/whitelists -o json
+    rune -0 jq -e '.local_version==.version' <(output)
+
+    # taint
+    echo "dirty" >"$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+    # XXX: should return error
+    rune -0 cscli parsers upgrade crowdsecurity/whitelists
+    assert_stderr --partial "crowdsecurity/whitelists is tainted, --force to overwrite"
+    rune -0 cscli parsers inspect crowdsecurity/whitelists -o json
+    rune -0 jq -e '.local_version=="?"' <(output)
+
+    # force upgrade with taint
+    rune -0 cscli parsers upgrade crowdsecurity/whitelists --force
+    rune -0 cscli parsers inspect crowdsecurity/whitelists -o json
+    rune -0 jq -e '.local_version==.version' <(output)
+
+    # multiple items
+    rune -0 cscli parsers install crowdsecurity/windows-auth
+    echo "dirty" >"$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+    echo "dirty" >"$CONFIG_DIR/parsers/s01-parse/windows-auth.yaml"
+    rune -0 cscli parsers list -o json
+    rune -0 jq -e '[.parsers[].local_version]==["?","?"]' <(output)
+    rune -0 cscli parsers upgrade crowdsecurity/whitelists crowdsecurity/windows-auth
+    rune -0 jq -e '[.parsers[].local_version]==[.parsers[].version]' <(output)
+
+    # upgrade all
+    echo "dirty" >"$CONFIG_DIR/parsers/s02-enrich/whitelists.yaml"
+    echo "dirty" >"$CONFIG_DIR/parsers/s01-parse/windows-auth.yaml"
+    rune -0 cscli parsers upgrade --all
+    rune -0 jq -e '[.parsers[].local_version]==[.parsers[].version]' <(output)
+}
+
+
+
+#@test "must use --force to remove a collection that belongs to another, which becomes tainted" {
+#    # we expect no error since we may have multiple collections, some removed and some not
+#    rune -0 cscli collections remove crowdsecurity/sshd
+#    assert_stderr --partial "crowdsecurity/sshd belongs to other collections"
+#    assert_stderr --partial "[crowdsecurity/linux]"
+#
+#    rune -0 cscli collections remove crowdsecurity/sshd --force
+#    assert_stderr --partial "Removed symlink [crowdsecurity/sshd]"
+#    rune -0 cscli collections inspect crowdsecurity/linux -o json
+#    rune -0 jq -r '.tainted' <(output)
+#    assert_output "true"
+#}
+#
+#@test "can remove a collection" {
+#    rune -0 cscli collections remove crowdsecurity/linux
+#    assert_stderr --partial "Removed"
+#    assert_stderr --regexp   ".*for the new configuration to be effective."
+#    rune -0 cscli collections inspect crowdsecurity/linux -o human --no-metrics
+#    assert_line 'installed: false'
+#}
+#
+#@test "collections delete is an alias for collections remove" {
+#    rune -0 cscli collections delete crowdsecurity/linux
+#    assert_stderr --partial "Removed"
+#    assert_stderr --regexp   ".*for the new configuration to be effective."
+#}
+#
+#@test "removing a collection that does not exist is noop" {
+#    rune -0 cscli collections remove crowdsecurity/apache2
+#    refute_stderr --partial "Removed"
+#    assert_stderr --regexp   ".*for the new configuration to be effective."
+#}
+#
+#@test "can remove a removed collection" {
+#    rune -0 cscli collections install crowdsecurity/mysql
+#    rune -0 cscli collections remove crowdsecurity/mysql
+#    assert_stderr --partial "Removed"
+#    rune -0 cscli collections remove crowdsecurity/mysql
+#    refute_stderr --partial "Removed"
+#}
+#
+#@test "can remove all collections" {
+#    # we may have this too, from package installs
+#    rune cscli parsers delete crowdsecurity/whitelists
+#    rune -0 cscli collections remove --all
+#    assert_stderr --partial "Removed symlink [crowdsecurity/sshd]"
+#    assert_stderr --partial "Removed symlink [crowdsecurity/linux]"
+#    rune -0 cscli hub list -o json
+#    assert_json '{collections:[],parsers:[],postoverflows:[],scenarios:[]}'
+#    rune -0 cscli collections remove --all
+#    assert_stderr --partial 'Disabled 0 items'
+#}
+#
+#@test "a taint bubbles up to the top collection" {
+#    coll=crowdsecurity/nginx
+#    subcoll=crowdsecurity/base-http-scenarios
+#    scenario=crowdsecurity/http-crawl-non_statics
+#
+#    # install a collection with dependencies
+#    rune -0 cscli collections install "$coll"
+#
+#    # the collection, subcollection and scenario are installed and not tainted
+#    # we have to default to false because tainted is (as of 1.4.6) returned
+#    # only when true
+#    rune -0 cscli collections inspect "$coll" -o json
+#    rune -0 jq -e '(.installed,.tainted|false)==(true,false)' <(output)
+#    rune -0 cscli collections inspect "$subcoll" -o json
+#    rune -0 jq -e '(.installed,.tainted|false)==(true,false)' <(output)
+#    rune -0 cscli scenarios inspect "$scenario" -o json
+#    rune -0 jq -e '(.installed,.tainted|false)==(true,false)' <(output)
+#
+#    # we taint the scenario
+#    HUB_DIR=$(config_get '.config_paths.hub_dir')
+#    yq e '.description="I am tainted"' -i "$HUB_DIR/scenarios/$scenario.yaml"
+#
+#    # the collection, subcollection and scenario are now tainted
+#    rune -0 cscli scenarios inspect "$scenario" -o json
+#    rune -0 jq -e '(.installed,.tainted)==(true,true)' <(output)
+#    rune -0 cscli collections inspect "$subcoll" -o json
+#    rune -0 jq -e '(.installed,.tainted)==(true,true)' <(output)
+#    rune -0 cscli collections inspect "$coll" -o json
+#    rune -0 jq -e '(.installed,.tainted)==(true,true)' <(output)
+#}
+#
+## TODO test download-only

+ 321 - 0
test/bats/20_hub_postoverflows.bats

@@ -0,0 +1,321 @@
+#!/usr/bin/env bats
+# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
+
+set -u
+
+setup_file() {
+    load "../lib/setup_file.sh"
+    HUB_DIR=$(config_get '.config_paths.hub_dir')
+    export HUB_DIR
+    CONFIG_DIR=$(config_get '.config_paths.config_dir')
+    export CONFIG_DIR
+}
+
+teardown_file() {
+    load "../lib/teardown_file.sh"
+}
+
+setup() {
+    load "../lib/setup.sh"
+    load "../lib/bats-file/load.bash"
+    ./instance-data load
+    hub_uninstall_all
+    hub_min=$(jq <"$HUB_DIR/.index.json" 'del(..|.content?) | del(..|.long_description?) | del(..|.deprecated?) | del (..|.labels?)')
+    echo "$hub_min" >"$HUB_DIR/.index.json"
+}
+
+teardown() {
+    ./instance-crowdsec stop
+}
+
+#----------
+
+@test "cscli postoverflows list" {
+    # no items
+    rune -0 cscli postoverflows list
+    assert_output --partial "POSTOVERFLOWS"
+    rune -0 cscli postoverflows list -o json
+    assert_json '{postoverflows:[]}'
+    rune -0 cscli postoverflows list -o raw
+    assert_output 'name,status,version,description'
+
+    # some items
+    rune -0 cscli postoverflows install crowdsecurity/rdns crowdsecurity/cdn-whitelist
+
+    rune -0 cscli postoverflows list
+    assert_output --partial crowdsecurity/rdns
+    assert_output --partial crowdsecurity/cdn-whitelist
+    rune -0 grep -c enabled <(output)
+    assert_output "2"
+
+    rune -0 cscli postoverflows list -o json
+    assert_output --partial crowdsecurity/rdns
+    assert_output --partial crowdsecurity/cdn-whitelist
+    rune -0 jq '.postoverflows | length' <(output)
+    assert_output "2"
+
+    rune -0 cscli postoverflows list -o raw
+    assert_output --partial crowdsecurity/rdns
+    assert_output --partial crowdsecurity/cdn-whitelist
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+}
+
+@test "cscli postoverflows list -a" {
+    expected=$(jq <"$HUB_DIR/.index.json" -r '.postoverflows | length')
+
+    rune -0 cscli postoverflows list -a
+    rune -0 grep -c disabled <(output)
+    assert_output "$expected"
+
+    rune -0 cscli postoverflows list -o json -a
+    rune -0 jq '.postoverflows | length' <(output)
+    assert_output "$expected"
+
+    rune -0 cscli postoverflows list -o raw -a
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "$expected"
+}
+
+
+@test "cscli postoverflows list [scenario]..." {
+    rune -0 cscli postoverflows install crowdsecurity/rdns crowdsecurity/cdn-whitelist
+
+    # list one item
+    rune -0 cscli postoverflows list crowdsecurity/rdns
+    assert_output --partial "crowdsecurity/rdns"
+    refute_output --partial "crowdsecurity/cdn-whitelist"
+
+    # list multiple items
+    rune -0 cscli postoverflows list crowdsecurity/rdns crowdsecurity/cdn-whitelist
+    assert_output --partial "crowdsecurity/rdns"
+    assert_output --partial "crowdsecurity/cdn-whitelist"
+
+    rune -0 cscli postoverflows list crowdsecurity/rdns -o json
+    rune -0 jq '.postoverflows | length' <(output)
+    assert_output "1"
+    rune -0 cscli postoverflows list crowdsecurity/rdns crowdsecurity/cdn-whitelist -o json
+    rune -0 jq '.postoverflows | length' <(output)
+    assert_output "2"
+
+    rune -0 cscli postoverflows list crowdsecurity/rdns -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "1"
+    rune -0 cscli postoverflows list crowdsecurity/rdns crowdsecurity/cdn-whitelist -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+}
+
+@test "cscli postoverflows list [scenario]... (not installed / not existing)" {
+    skip "not implemented yet"
+    # not installed
+    rune -1 cscli postoverflows list crowdsecurity/rdns
+    # not existing
+    rune -1 cscli postoverflows list blahblah/blahblah
+}
+
+@test "cscli postoverflows install [scenario]..." {
+    rune -1 cscli postoverflows install
+    assert_stderr --partial 'requires at least 1 arg(s), only received 0'
+
+    # not in hub
+    rune -1 cscli postoverflows install crowdsecurity/blahblah
+    assert_stderr --partial "can't find 'crowdsecurity/blahblah' in postoverflows"
+
+    # simple install
+    rune -0 cscli postoverflows install crowdsecurity/rdns
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns --no-metrics
+    assert_output --partial 'crowdsecurity/rdns'
+    assert_output --partial 'installed: true'
+
+    # autocorrect
+    rune -1 cscli postoverflows install crowdsecurity/rdnf
+    assert_stderr --partial "can't find 'crowdsecurity/rdnf' in postoverflows, did you mean crowdsecurity/rdns?"
+
+    # install multiple
+    rune -0 cscli postoverflows install crowdsecurity/rdns crowdsecurity/cdn-whitelist
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns --no-metrics
+    assert_output --partial 'crowdsecurity/rdns'
+    assert_output --partial 'installed: true'
+    rune -0 cscli postoverflows inspect crowdsecurity/cdn-whitelist --no-metrics
+    assert_output --partial 'crowdsecurity/cdn-whitelist'
+    assert_output --partial 'installed: true'
+}
+
+@test "cscli postoverflows install [postoverflow]... (file location and download-only)" {
+    # simple install
+    rune -0 cscli postoverflows install crowdsecurity/rdns --download-only
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns --no-metrics
+    assert_output --partial 'crowdsecurity/rdns'
+    assert_output --partial 'installed: false'
+    assert_file_exists "$HUB_DIR/postoverflows/s00-enrich/crowdsecurity/rdns.yaml"
+    assert_file_not_exists "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+
+    rune -0 cscli postoverflows install crowdsecurity/rdns
+    assert_file_exists "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+}
+
+
+@test "cscli postoverflows inspect [scenario]..." {
+    rune -1 cscli postoverflows inspect
+    assert_stderr --partial 'requires at least 1 arg(s), only received 0'
+    ./instance-crowdsec start
+
+    rune -1 cscli postoverflows inspect blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in postoverflows"
+
+    # one item
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns --no-metrics
+    assert_line 'type: postoverflows'
+    assert_line 'stage: s00-enrich'
+    assert_line 'name: crowdsecurity/rdns'
+    assert_line 'author: crowdsecurity'
+    assert_line 'remote_path: postoverflows/s00-enrich/crowdsecurity/rdns.yaml'
+    assert_line 'installed: false'
+    refute_line --partial 'Current metrics:'
+
+    # one item, with metrics
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns
+    assert_line --partial 'Current metrics:'
+
+    # one item, json
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns -o json
+    rune -0 jq -c '[.type, .stage, .name, .author, .path, .installed]' <(output)
+    # XXX: .installed is missing -- not false
+    assert_json '["postoverflows","s00-enrich","crowdsecurity/rdns","crowdsecurity","postoverflows/s00-enrich/crowdsecurity/rdns.yaml",null]'
+
+    # one item, raw
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns -o raw
+    assert_line 'type: postoverflows'
+    assert_line 'stage: s00-enrich'
+    assert_line 'name: crowdsecurity/rdns'
+    assert_line 'author: crowdsecurity'
+    assert_line 'remote_path: postoverflows/s00-enrich/crowdsecurity/rdns.yaml'
+    assert_line 'installed: false'
+    refute_line --partial 'Current metrics:'
+
+    # multiple items
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns crowdsecurity/cdn-whitelist --no-metrics
+    assert_output --partial 'crowdsecurity/rdns'
+    assert_output --partial 'crowdsecurity/cdn-whitelist'
+    rune -1 grep -c 'Current metrics:' <(output)
+    assert_output "0"
+
+    # multiple items, with metrics
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns crowdsecurity/cdn-whitelist
+    rune -0 grep -c 'Current metrics:' <(output)
+    assert_output "2"
+
+    # multiple items, json
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns crowdsecurity/cdn-whitelist -o json
+    rune -0 jq -sc '[.[] | [.type, .stage, .name, .author, .path, .installed]]' <(output)
+    assert_json '[["postoverflows","s00-enrich","crowdsecurity/rdns","crowdsecurity","postoverflows/s00-enrich/crowdsecurity/rdns.yaml",null],["postoverflows","s01-whitelist","crowdsecurity/cdn-whitelist","crowdsecurity","postoverflows/s01-whitelist/crowdsecurity/cdn-whitelist.yaml",null]]'
+
+    # multiple items, raw
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns crowdsecurity/cdn-whitelist -o raw
+    assert_output --partial 'crowdsecurity/rdns'
+    assert_output --partial 'crowdsecurity/cdn-whitelist'
+    run -1 grep -c 'Current metrics:' <(output)
+    assert_output "0"
+}
+
+@test "cscli postoverflows remove [postoverflow]..." {
+    rune -1 cscli postoverflows remove
+    assert_stderr --partial "specify at least one postoverflow to remove or '--all'"
+
+    rune -1 cscli postoverflows remove blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in postoverflows"
+
+    # XXX: we can however remove a real item if it's not installed, or already removed
+    rune -0 cscli postoverflows remove crowdsecurity/rdns
+
+    # install, then remove, check files
+    rune -0 cscli postoverflows install crowdsecurity/rdns
+    assert_file_exists "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+    rune -0 cscli postoverflows remove crowdsecurity/rdns
+    assert_file_not_exists "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+
+    # delete is an alias for remove
+    rune -0 cscli postoverflows install crowdsecurity/rdns
+    assert_file_exists "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+    rune -0 cscli postoverflows delete crowdsecurity/rdns
+    assert_file_not_exists "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+
+    # purge
+    assert_file_exists "$HUB_DIR/postoverflows/s00-enrich/crowdsecurity/rdns.yaml"
+    rune -0 cscli postoverflows remove crowdsecurity/rdns --purge
+    assert_file_not_exists "$HUB_DIR/postoverflows/s00-enrich/crowdsecurity/rdns.yaml"
+
+    rune -0 cscli postoverflows install crowdsecurity/rdns crowdsecurity/cdn-whitelist
+
+    # --all
+    rune -0 cscli postoverflows list -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+
+    rune -0 cscli postoverflows remove --all
+
+    rune -0 cscli postoverflows list -o raw
+    rune -1 grep -vc 'name,status,version,description' <(output)
+    assert_output "0"
+}
+
+@test "cscli postoverflows upgrade [postoverflow]..." {
+    rune -1 cscli postoverflows upgrade
+    assert_stderr --partial "specify at least one postoverflow to upgrade or '--all'"
+
+    # XXX: should this return 1 instead of log.Error?
+    rune -0 cscli postoverflows upgrade blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in postoverflows"
+
+    # XXX: same message if the item exists but is not installed, this is confusing
+    rune -0 cscli postoverflows upgrade crowdsecurity/rdns
+    assert_stderr --partial "can't find 'crowdsecurity/rdns' in postoverflows"
+
+    # hash of an empty file
+    sha256_empty="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+
+    # add version 0.0 to the hub
+    new_hub=$(jq --arg DIGEST "$sha256_empty" <"$HUB_DIR/.index.json" '. * {postoverflows:{"crowdsecurity/rdns":{"versions":{"0.0":{"digest":$DIGEST, "deprecated": false}}}}}')
+    echo "$new_hub" >"$HUB_DIR/.index.json"
+ 
+    rune -0 cscli postoverflows install crowdsecurity/rdns
+
+    # bring the file to v0.0
+    truncate -s 0 "$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns -o json
+    rune -0 jq -e '.local_version=="0.0"' <(output)
+
+    # upgrade
+    rune -0 cscli postoverflows upgrade crowdsecurity/rdns
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns -o json
+    rune -0 jq -e '.local_version==.version' <(output)
+
+    # taint
+    echo "dirty" >"$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+    # XXX: should return error
+    rune -0 cscli postoverflows upgrade crowdsecurity/rdns
+    assert_stderr --partial "crowdsecurity/rdns is tainted, --force to overwrite"
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns -o json
+    rune -0 jq -e '.local_version=="?"' <(output)
+
+    # force upgrade with taint
+    rune -0 cscli postoverflows upgrade crowdsecurity/rdns --force
+    rune -0 cscli postoverflows inspect crowdsecurity/rdns -o json
+    rune -0 jq -e '.local_version==.version' <(output)
+
+    # multiple items
+    rune -0 cscli postoverflows install crowdsecurity/cdn-whitelist
+    echo "dirty" >"$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+    echo "dirty" >"$CONFIG_DIR/postoverflows/s01-whitelist/cdn-whitelist.yaml"
+    rune -0 cscli postoverflows list -o json
+    rune -0 jq -e '[.postoverflows[].local_version]==["?","?"]' <(output)
+    rune -0 cscli postoverflows upgrade crowdsecurity/rdns crowdsecurity/cdn-whitelist
+    rune -0 jq -e '[.postoverflows[].local_version]==[.postoverflows[].version]' <(output)
+
+    # upgrade all
+    echo "dirty" >"$CONFIG_DIR/postoverflows/s00-enrich/rdns.yaml"
+    echo "dirty" >"$CONFIG_DIR/postoverflows/s01-whitelist/cdn-whitelist.yaml"
+    rune -0 cscli postoverflows upgrade --all
+    rune -0 jq -e '[.postoverflows[].local_version]==[.postoverflows[].version]' <(output)
+}

+ 320 - 0
test/bats/20_hub_scenarios.bats

@@ -0,0 +1,320 @@
+#!/usr/bin/env bats
+# vim: ft=bats:list:ts=8:sts=4:sw=4:et:ai:si:
+
+set -u
+
+setup_file() {
+    load "../lib/setup_file.sh"
+    HUB_DIR=$(config_get '.config_paths.hub_dir')
+    export HUB_DIR
+    CONFIG_DIR=$(config_get '.config_paths.config_dir')
+    export CONFIG_DIR
+}
+
+teardown_file() {
+    load "../lib/teardown_file.sh"
+}
+
+setup() {
+    load "../lib/setup.sh"
+    load "../lib/bats-file/load.bash"
+    ./instance-data load
+    hub_uninstall_all
+    hub_min=$(jq <"$HUB_DIR/.index.json" 'del(..|.content?) | del(..|.long_description?) | del(..|.deprecated?) | del (..|.labels?)')
+    echo "$hub_min" >"$HUB_DIR/.index.json"
+}
+
+teardown() {
+    ./instance-crowdsec stop
+}
+
+#----------
+
+@test "cscli scenarios list" {
+    # no items
+    rune -0 cscli scenarios list
+    assert_output --partial "SCENARIOS"
+    rune -0 cscli scenarios list -o json
+    assert_json '{scenarios:[]}'
+    rune -0 cscli scenarios list -o raw
+    assert_output 'name,status,version,description'
+
+    # some items
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/telnet-bf
+
+    rune -0 cscli scenarios list
+    assert_output --partial crowdsecurity/ssh-bf
+    assert_output --partial crowdsecurity/telnet-bf
+    rune -0 grep -c enabled <(output)
+    assert_output "2"
+
+    rune -0 cscli scenarios list -o json
+    assert_output --partial crowdsecurity/ssh-bf
+    assert_output --partial crowdsecurity/telnet-bf
+    rune -0 jq '.scenarios | length' <(output)
+    assert_output "2"
+
+    rune -0 cscli scenarios list -o raw
+    assert_output --partial crowdsecurity/ssh-bf
+    assert_output --partial crowdsecurity/telnet-bf
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+}
+
+@test "cscli scenarios list -a" {
+    expected=$(jq <"$HUB_DIR/.index.json" -r '.scenarios | length')
+
+    rune -0 cscli scenarios list -a
+    rune -0 grep -c disabled <(output)
+    assert_output "$expected"
+
+    rune -0 cscli scenarios list -o json -a
+    rune -0 jq '.scenarios | length' <(output)
+    assert_output "$expected"
+
+    rune -0 cscli scenarios list -o raw -a
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "$expected"
+}
+
+
+@test "cscli scenarios list [scenario]..." {
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/telnet-bf
+
+    # list one item
+    rune -0 cscli scenarios list crowdsecurity/ssh-bf
+    assert_output --partial "crowdsecurity/ssh-bf"
+    refute_output --partial "crowdsecurity/telnet-bf"
+
+    # list multiple items
+    rune -0 cscli scenarios list crowdsecurity/ssh-bf crowdsecurity/telnet-bf
+    assert_output --partial "crowdsecurity/ssh-bf"
+    assert_output --partial "crowdsecurity/telnet-bf"
+
+    rune -0 cscli scenarios list crowdsecurity/ssh-bf -o json
+    rune -0 jq '.scenarios | length' <(output)
+    assert_output "1"
+    rune -0 cscli scenarios list crowdsecurity/ssh-bf crowdsecurity/telnet-bf -o json
+    rune -0 jq '.scenarios | length' <(output)
+    assert_output "2"
+
+    rune -0 cscli scenarios list crowdsecurity/ssh-bf -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "1"
+    rune -0 cscli scenarios list crowdsecurity/ssh-bf crowdsecurity/telnet-bf -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+}
+
+@test "cscli scenarios list [scenario]... (not installed / not existing)" {
+    skip "not implemented yet"
+    # not installed
+    rune -1 cscli scenarios list crowdsecurity/ssh-bf
+    # not existing
+    rune -1 cscli scenarios list blahblah/blahblah
+}
+
+@test "cscli scenarios install [scenario]..." {
+    rune -1 cscli scenarios install
+    assert_stderr --partial 'requires at least 1 arg(s), only received 0'
+
+    # not in hub
+    rune -1 cscli scenarios install crowdsecurity/blahblah
+    assert_stderr --partial "can't find 'crowdsecurity/blahblah' in scenarios"
+
+    # simple install
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf --no-metrics
+    assert_output --partial 'crowdsecurity/ssh-bf'
+    assert_output --partial 'installed: true'
+
+    # autocorrect
+    rune -1 cscli scenarios install crowdsecurity/ssh-tf
+    assert_stderr --partial "can't find 'crowdsecurity/ssh-tf' in scenarios, did you mean crowdsecurity/ssh-bf?"
+
+    # install multiple
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/telnet-bf
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf --no-metrics
+    assert_output --partial 'crowdsecurity/ssh-bf'
+    assert_output --partial 'installed: true'
+    rune -0 cscli scenarios inspect crowdsecurity/telnet-bf --no-metrics
+    assert_output --partial 'crowdsecurity/telnet-bf'
+    assert_output --partial 'installed: true'
+}
+
+
+@test "cscli scenarios install [scenario]... (file location and download-only)" {
+    # simple install
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf --download-only
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf --no-metrics
+    assert_output --partial 'crowdsecurity/ssh-bf'
+    assert_output --partial 'installed: false'
+    assert_file_exists "$HUB_DIR/scenarios/crowdsecurity/ssh-bf.yaml"
+    assert_file_not_exists "$CONFIG_DIR/scenarios/ssh-bf.yaml"
+
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf
+    assert_file_exists "$CONFIG_DIR/scenarios/ssh-bf.yaml"
+}
+
+
+@test "cscli scenarios inspect [scenario]..." {
+    rune -1 cscli scenarios inspect
+    assert_stderr --partial 'requires at least 1 arg(s), only received 0'
+    ./instance-crowdsec start
+
+    rune -1 cscli scenarios inspect blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in scenarios"
+
+    # one item
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf --no-metrics
+    assert_line 'type: scenarios'
+    assert_line 'name: crowdsecurity/ssh-bf'
+    assert_line 'author: crowdsecurity'
+    assert_line 'remote_path: scenarios/crowdsecurity/ssh-bf.yaml'
+    assert_line 'installed: false'
+    refute_line --partial 'Current metrics:'
+
+    # one item, with metrics
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf
+    assert_line --partial 'Current metrics:'
+
+    # one item, json
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf -o json
+    rune -0 jq -c '[.type, .name, .author, .path, .installed]' <(output)
+    # XXX: .installed is missing -- not false
+    assert_json '["scenarios","crowdsecurity/ssh-bf","crowdsecurity","scenarios/crowdsecurity/ssh-bf.yaml",null]'
+
+    # one item, raw
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf -o raw
+    assert_line 'type: scenarios'
+    assert_line 'name: crowdsecurity/ssh-bf'
+    assert_line 'author: crowdsecurity'
+    assert_line 'remote_path: scenarios/crowdsecurity/ssh-bf.yaml'
+    assert_line 'installed: false'
+    refute_line --partial 'Current metrics:'
+
+    # multiple items
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/telnet-bf --no-metrics
+    assert_output --partial 'crowdsecurity/ssh-bf'
+    assert_output --partial 'crowdsecurity/telnet-bf'
+    rune -1 grep -c 'Current metrics:' <(output)
+    assert_output "0"
+
+    # multiple items, with metrics
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/telnet-bf
+    rune -0 grep -c 'Current metrics:' <(output)
+    assert_output "2"
+
+    # multiple items, json
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/telnet-bf -o json
+    rune -0 jq -sc '[.[] | [.type, .name, .author, .path, .installed]]' <(output)
+    assert_json '[["scenarios","crowdsecurity/ssh-bf","crowdsecurity","scenarios/crowdsecurity/ssh-bf.yaml",null],["scenarios","crowdsecurity/telnet-bf","crowdsecurity","scenarios/crowdsecurity/telnet-bf.yaml",null]]'
+
+    # multiple items, raw
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf crowdsecurity/telnet-bf -o raw
+    assert_output --partial 'crowdsecurity/ssh-bf'
+    assert_output --partial 'crowdsecurity/telnet-bf'
+    run -1 grep -c 'Current metrics:' <(output)
+    assert_output "0"
+}
+
+@test "cscli scenarios remove [scenario]..." {
+    rune -1 cscli scenarios remove
+    assert_stderr --partial "specify at least one scenario to remove or '--all'"
+
+    rune -1 cscli scenarios remove blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in scenarios"
+
+    # XXX: we can however remove a real item if it's not installed, or already removed
+    rune -0 cscli scenarios remove crowdsecurity/ssh-bf
+
+    # install, then remove, check files
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf
+    assert_file_exists "$CONFIG_DIR/scenarios/ssh-bf.yaml"
+    rune -0 cscli scenarios remove crowdsecurity/ssh-bf
+    assert_file_not_exists "$CONFIG_DIR/scenarios/ssh-bf.yaml"
+
+    # delete is an alias for remove
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf
+    assert_file_exists "$CONFIG_DIR/scenarios/ssh-bf.yaml"
+    rune -0 cscli scenarios delete crowdsecurity/ssh-bf
+    assert_file_not_exists "$CONFIG_DIR/scenarios/ssh-bf.yaml"
+
+    # purge
+    assert_file_exists "$HUB_DIR/scenarios/crowdsecurity/ssh-bf.yaml"
+    rune -0 cscli scenarios remove crowdsecurity/ssh-bf --purge
+    assert_file_not_exists "$HUB_DIR/scenarios/crowdsecurity/ssh-bf.yaml"
+
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf crowdsecurity/telnet-bf
+
+    # --all
+    rune -0 cscli scenarios list -o raw
+    rune -0 grep -vc 'name,status,version,description' <(output)
+    assert_output "2"
+
+    rune -0 cscli scenarios remove --all
+
+    rune -0 cscli scenarios list -o raw
+    rune -1 grep -vc 'name,status,version,description' <(output)
+    assert_output "0"
+}
+
+@test "cscli scenarios upgrade [scenario]..." {
+    rune -1 cscli scenarios upgrade
+    assert_stderr --partial "specify at least one scenario to upgrade or '--all'"
+
+    # XXX: should this return 1 instead of log.Error?
+    rune -0 cscli scenarios upgrade blahblah/blahblah
+    assert_stderr --partial "can't find 'blahblah/blahblah' in scenarios"
+
+    # XXX: same message if the item exists but is not installed, this is confusing
+    rune -0 cscli scenarios upgrade crowdsecurity/ssh-bf
+    assert_stderr --partial "can't find 'crowdsecurity/ssh-bf' in scenarios"
+
+    # hash of an empty file
+    sha256_empty="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
+
+    # add version 0.0 to the hub
+    new_hub=$(jq --arg DIGEST "$sha256_empty" <"$HUB_DIR/.index.json" '. * {scenarios:{"crowdsecurity/ssh-bf":{"versions":{"0.0":{"digest":$DIGEST, "deprecated": false}}}}}')
+    echo "$new_hub" >"$HUB_DIR/.index.json"
+ 
+    rune -0 cscli scenarios install crowdsecurity/ssh-bf
+
+    # bring the file to v0.0
+    truncate -s 0 "$CONFIG_DIR/scenarios/ssh-bf.yaml"
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf -o json
+    rune -0 jq -e '.local_version=="0.0"' <(output)
+
+    # upgrade
+    rune -0 cscli scenarios upgrade crowdsecurity/ssh-bf
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf -o json
+    rune -0 jq -e '.local_version==.version' <(output)
+
+    # taint
+    echo "dirty" >"$CONFIG_DIR/scenarios/ssh-bf.yaml"
+    # XXX: should return error
+    rune -0 cscli scenarios upgrade crowdsecurity/ssh-bf
+    assert_stderr --partial "crowdsecurity/ssh-bf is tainted, --force to overwrite"
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf -o json
+    rune -0 jq -e '.local_version=="?"' <(output)
+
+    # force upgrade with taint
+    rune -0 cscli scenarios upgrade crowdsecurity/ssh-bf --force
+    rune -0 cscli scenarios inspect crowdsecurity/ssh-bf -o json
+    rune -0 jq -e '.local_version==.version' <(output)
+
+    # multiple items
+    rune -0 cscli scenarios install crowdsecurity/telnet-bf
+    echo "dirty" >"$CONFIG_DIR/scenarios/ssh-bf.yaml"
+    echo "dirty" >"$CONFIG_DIR/scenarios/telnet-bf.yaml"
+    rune -0 cscli scenarios list -o json
+    rune -0 jq -e '[.scenarios[].local_version]==["?","?"]' <(output)
+    rune -0 cscli scenarios upgrade crowdsecurity/ssh-bf crowdsecurity/telnet-bf
+    rune -0 jq -e '[.scenarios[].local_version]==[.scenarios[].version]' <(output)
+
+    # upgrade all
+    echo "dirty" >"$CONFIG_DIR/scenarios/ssh-bf.yaml"
+    echo "dirty" >"$CONFIG_DIR/scenarios/telnet-bf.yaml"
+    rune -0 cscli scenarios upgrade --all
+    rune -0 jq -e '[.scenarios[].local_version]==[.scenarios[].version]' <(output)
+}

+ 6 - 0
test/lib/setup_file.sh

@@ -238,6 +238,12 @@ assert_stderr_line() {
 }
 export -f assert_stderr_line
 
+hub_uninstall_all() {
+    CONFIG_DIR=$(dirname "$CONFIG_YAML")
+    rm -rf "$CONFIG_DIR"/collections/* "$CONFIG_DIR"/parsers/*/* "$CONFIG_DIR"/scenarios/* "$CONFIG_DIR"/postoverflows/*
+}
+export -f hub_uninstall_all
+
 # remove color and style sequences from stdin
 plaintext() {
     sed -E 's/\x1B\[[0-9;]*[JKmsu]//g'