Ver código fonte

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 ano atrás
pai
commit
f496bd1692

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

@@ -13,13 +13,13 @@ import (
 
 
 func NewCollectionsCmd() *cobra.Command {
 func NewCollectionsCmd() *cobra.Command {
 	cmdCollections := &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),
 		Args:              cobra.MinimumNArgs(1),
 		Aliases:           []string{"collection"},
 		Aliases:           []string{"collection"},
@@ -88,10 +88,10 @@ func runCollectionsInstall(cmd *cobra.Command, args []string) error {
 
 
 func NewCollectionsInstallCmd() *cobra.Command {
 func NewCollectionsInstallCmd() *cobra.Command {
 	cmdCollectionsInstall := &cobra.Command{
 	cmdCollectionsInstall := &cobra.Command{
-		Use:               "install collection",
+		Use:               "install <collection>...",
 		Short:             "Install given collection(s)",
 		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),
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -102,7 +102,7 @@ func NewCollectionsInstallCmd() *cobra.Command {
 
 
 	flags := cmdCollectionsInstall.Flags()
 	flags := cmdCollectionsInstall.Flags()
 	flags.BoolP("download-only", "d", false, "Only download packages, don't enable")
 	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")
 	flags.Bool("ignore", false, "Ignore errors when installing multiple collections")
 
 
 	return cmdCollectionsInstall
 	return cmdCollectionsInstall
@@ -143,11 +143,12 @@ func runCollectionsRemove(cmd *cobra.Command, args []string) error {
 		if !force {
 		if !force {
 			item := cwhub.GetItem(cwhub.COLLECTIONS, name)
 			item := cwhub.GetItem(cwhub.COLLECTIONS, name)
 			if item == nil {
 			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 {
 			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
 				continue
 			}
 			}
 		}
 		}
@@ -163,10 +164,10 @@ func runCollectionsRemove(cmd *cobra.Command, args []string) error {
 
 
 func NewCollectionsRemoveCmd() *cobra.Command {
 func NewCollectionsRemoveCmd() *cobra.Command {
 	cmdCollectionsRemove := &cobra.Command{
 	cmdCollectionsRemove := &cobra.Command{
-		Use:               "remove collection",
+		Use:               "remove <collection>...",
 		Short:             "Remove given collection(s)",
 		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"},
 		Aliases:           []string{"delete"},
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -177,8 +178,8 @@ func NewCollectionsRemoveCmd() *cobra.Command {
 
 
 	flags := cmdCollectionsRemove.Flags()
 	flags := cmdCollectionsRemove.Flags()
 	flags.Bool("purge", false, "Delete source file too")
 	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
 	return cmdCollectionsRemove
 }
 }
@@ -197,7 +198,9 @@ func runCollectionsUpgrade(cmd *cobra.Command, args []string) error {
 	}
 	}
 
 
 	if all {
 	if all {
-		cwhub.UpgradeConfig(csConfig, cwhub.COLLECTIONS, "", force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.COLLECTIONS, "", force); err != nil {
+			return err
+		}
 		return nil
 		return nil
 	}
 	}
 
 
@@ -206,7 +209,9 @@ func runCollectionsUpgrade(cmd *cobra.Command, args []string) error {
 	}
 	}
 
 
 	for _, name := range args {
 	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
 	return nil
@@ -214,10 +219,10 @@ func runCollectionsUpgrade(cmd *cobra.Command, args []string) error {
 
 
 func NewCollectionsUpgradeCmd() *cobra.Command {
 func NewCollectionsUpgradeCmd() *cobra.Command {
 	cmdCollectionsUpgrade := &cobra.Command{
 	cmdCollectionsUpgrade := &cobra.Command{
-		Use:               "upgrade collection",
+		Use:               "upgrade <collection>...",
 		Short:             "Upgrade given collection(s)",
 		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,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compInstalledItems(cwhub.COLLECTIONS, args, toComplete)
 			return compInstalledItems(cwhub.COLLECTIONS, args, toComplete)
@@ -227,7 +232,7 @@ func NewCollectionsUpgradeCmd() *cobra.Command {
 
 
 	flags := cmdCollectionsUpgrade.Flags()
 	flags := cmdCollectionsUpgrade.Flags()
 	flags.BoolP("all", "a", false, "Upgrade all the collections")
 	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
 	return cmdCollectionsUpgrade
 }
 }
@@ -244,8 +249,15 @@ func runCollectionsInspect(cmd *cobra.Command, args []string) error {
 		csConfig.Cscli.PrometheusUrl = url
 		csConfig.Cscli.PrometheusUrl = url
 	}
 	}
 
 
+	noMetrics, err := flags.GetBool("no-metrics")
+	if err != nil {
+		return err
+	}
+
 	for _, name := range args {
 	for _, name := range args {
-		InspectItem(name, cwhub.COLLECTIONS)
+		if err = InspectItem(name, cwhub.COLLECTIONS, noMetrics); err != nil {
+			return err
+		}
 	}
 	}
 
 
 	return nil
 	return nil
@@ -253,10 +265,10 @@ func runCollectionsInspect(cmd *cobra.Command, args []string) error {
 
 
 func NewCollectionsInspectCmd() *cobra.Command {
 func NewCollectionsInspectCmd() *cobra.Command {
 	cmdCollectionsInspect := &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),
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -267,6 +279,7 @@ func NewCollectionsInspectCmd() *cobra.Command {
 
 
 	flags := cmdCollectionsInspect.Flags()
 	flags := cmdCollectionsInspect.Flags()
 	flags.StringP("url", "u", "", "Prometheus url")
 	flags.StringP("url", "u", "", "Prometheus url")
+	flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)")
 
 
 	return cmdCollectionsInspect
 	return cmdCollectionsInspect
 }
 }
@@ -287,11 +300,12 @@ func runCollectionsList(cmd *cobra.Command, args []string) error {
 
 
 func NewCollectionsListCmd() *cobra.Command {
 func NewCollectionsListCmd() *cobra.Command {
 	cmdCollectionsList := &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,
 		DisableAutoGenTag: true,
 		RunE:              runCollectionsList,
 		RunE:              runCollectionsList,
 	}
 	}

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

@@ -15,17 +15,14 @@ import (
 func NewHubCmd() *cobra.Command {
 func NewHubCmd() *cobra.Command {
 	var cmdHub = &cobra.Command{
 	var cmdHub = &cobra.Command{
 		Use:   "hub [action]",
 		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).
 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),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
 		PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
@@ -75,7 +72,7 @@ func runHubList(cmd *cobra.Command, args []string) error {
 func NewHubListCmd() *cobra.Command {
 func NewHubListCmd() *cobra.Command {
 	var cmdHubList = &cobra.Command{
 	var cmdHubList = &cobra.Command{
 		Use:               "list [-a]",
 		Use:               "list [-a]",
-		Short:             "List installed configs",
+		Short:             "List all installed configurations",
 		Args:              cobra.ExactArgs(0),
 		Args:              cobra.ExactArgs(0),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		RunE: 	    runHubList,
 		RunE: 	    runHubList,
@@ -115,7 +112,7 @@ func runHubUpdate(cmd *cobra.Command, args []string) error {
 func NewHubUpdateCmd() *cobra.Command {
 func NewHubUpdateCmd() *cobra.Command {
 	var cmdHubUpdate = &cobra.Command{
 	var cmdHubUpdate = &cobra.Command{
 		Use:   "update",
 		Use:   "update",
-		Short: "Fetch available configs from hub",
+		Short: "Download the latest index (catalog of available configurations)",
 		Long: `
 		Long: `
 Fetches the [.index.json](https://github.com/crowdsecurity/hub/blob/master/.index.json) file from hub, containing the list of available configs.
 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")
 	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")
 	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")
 	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")
 	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
 	return nil
 }
 }
@@ -163,7 +171,7 @@ func runHubUpgrade(cmd *cobra.Command, args []string) error {
 func NewHubUpgradeCmd() *cobra.Command {
 func NewHubUpgradeCmd() *cobra.Command {
 	var cmdHubUpgrade = &cobra.Command{
 	var cmdHubUpgrade = &cobra.Command{
 		Use:   "upgrade",
 		Use:   "upgrade",
-		Short: "Upgrade all configs installed from hub",
+		Short: "Upgrade all configurations to their latest version",
 		Long: `
 		Long: `
 Upgrade all configs installed from Crowdsec Hub. Run 'sudo cscli hub update' if you want the latest versions available.
 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 := 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
 	return cmdHubUpgrade
 }
 }

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

@@ -300,10 +300,6 @@ func runMetrics(cmd *cobra.Command, args []string) error {
 		return err
 		return err
 	}
 	}
 
 
-	if err = csConfig.LoadPrometheus(); err != nil {
-		return fmt.Errorf("failed to load prometheus config: %w", err)
-	}
-
 	if csConfig.Prometheus == nil {
 	if csConfig.Prometheus == nil {
 		return fmt.Errorf("prometheus section missing, can't show metrics")
 		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 {
 func NewParsersCmd() *cobra.Command {
 	cmdParsers := &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),
 		Args:              cobra.MinimumNArgs(1),
 		Aliases:           []string{"parser"},
 		Aliases:           []string{"parser"},
@@ -88,10 +88,10 @@ func runParsersInstall(cmd *cobra.Command, args []string) error {
 
 
 func NewParsersInstallCmd() *cobra.Command {
 func NewParsersInstallCmd() *cobra.Command {
 	cmdParsersInstall := &cobra.Command{
 	cmdParsersInstall := &cobra.Command{
-		Use:               "install [config]",
+		Use:               "install <parser>...",
 		Short:             "Install given parser(s)",
 		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),
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -102,7 +102,7 @@ func NewParsersInstallCmd() *cobra.Command {
 
 
 	flags := cmdParsersInstall.Flags()
 	flags := cmdParsersInstall.Flags()
 	flags.BoolP("download-only", "d", false, "Only download packages, don't enable")
 	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")
 	flags.Bool("ignore", false, "Ignore errors when installing multiple parsers")
 
 
 	return cmdParsersInstall
 	return cmdParsersInstall
@@ -151,10 +151,10 @@ func runParsersRemove(cmd *cobra.Command, args []string) error {
 
 
 func NewParsersRemoveCmd() *cobra.Command {
 func NewParsersRemoveCmd() *cobra.Command {
 	cmdParsersRemove := &cobra.Command{
 	cmdParsersRemove := &cobra.Command{
-		Use:               "remove [config]",
+		Use:               "remove <parser>...",
 		Short:             "Remove given parser(s)",
 		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"},
 		Aliases:           []string{"delete"},
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -165,8 +165,8 @@ func NewParsersRemoveCmd() *cobra.Command {
 
 
 	flags := cmdParsersRemove.Flags()
 	flags := cmdParsersRemove.Flags()
 	flags.Bool("purge", false, "Delete source file too")
 	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
 	return cmdParsersRemove
 }
 }
@@ -185,7 +185,9 @@ func runParsersUpgrade(cmd *cobra.Command, args []string) error {
 	}
 	}
 
 
 	if all {
 	if all {
-		cwhub.UpgradeConfig(csConfig, cwhub.PARSERS, "", force)
+		if err := cwhub.UpgradeConfig(csConfig, cwhub.PARSERS, "", force); err != nil {
+			return err
+		}
 		return nil
 		return nil
 	}
 	}
 
 
@@ -194,7 +196,9 @@ func runParsersUpgrade(cmd *cobra.Command, args []string) error {
 	}
 	}
 
 
 	for _, name := range args {
 	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
 	return nil
@@ -202,10 +206,10 @@ func runParsersUpgrade(cmd *cobra.Command, args []string) error {
 
 
 func NewParsersUpgradeCmd() *cobra.Command {
 func NewParsersUpgradeCmd() *cobra.Command {
 	cmdParsersUpgrade := &cobra.Command{
 	cmdParsersUpgrade := &cobra.Command{
-		Use:               "upgrade [config]",
+		Use:               "upgrade <parser>...",
 		Short:             "Upgrade given parser(s)",
 		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,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 			return compInstalledItems(cwhub.PARSERS, args, toComplete)
 			return compInstalledItems(cwhub.PARSERS, args, toComplete)
@@ -214,8 +218,8 @@ func NewParsersUpgradeCmd() *cobra.Command {
 	}
 	}
 
 
 	flags := cmdParsersUpgrade.Flags()
 	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
 	return cmdParsersUpgrade
 }
 }
@@ -232,17 +236,26 @@ func runParsersInspect(cmd *cobra.Command, args []string) error {
 		csConfig.Cscli.PrometheusUrl = url
 		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
 	return nil
 }
 }
 
 
 func NewParsersInspectCmd() *cobra.Command {
 func NewParsersInspectCmd() *cobra.Command {
 	cmdParsersInspect := &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),
 		Args:              cobra.MinimumNArgs(1),
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
 		ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
@@ -253,6 +266,7 @@ func NewParsersInspectCmd() *cobra.Command {
 
 
 	flags := cmdParsersInspect.Flags()
 	flags := cmdParsersInspect.Flags()
 	flags.StringP("url", "u", "", "Prometheus url")
 	flags.StringP("url", "u", "", "Prometheus url")
+	flags.Bool("no-metrics", false, "Don't show metrics (when cscli.output=human)")
 
 
 	return cmdParsersInspect
 	return cmdParsersInspect
 }
 }
@@ -273,11 +287,12 @@ func runParsersList(cmd *cobra.Command, args []string) error {
 
 
 func NewParsersListCmd() *cobra.Command {
 func NewParsersListCmd() *cobra.Command {
 	cmdParsersList := &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
 		Example: `cscli parsers list
-cscli parser list crowdsecurity/xxx`,
+cscli parsers list -a
+cscli parsers list crowdsecurity/caddy-logs crowdsecurity/sshd-logs`,
 		DisableAutoGenTag: true,
 		DisableAutoGenTag: true,
 		RunE:              runParsersList,
 		RunE:              runParsersList,
 	}
 	}

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

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

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

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

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

@@ -58,10 +58,6 @@ func stripAnsiString(str string) string {
 
 
 func collectMetrics() ([]byte, []byte, error) {
 func collectMetrics() ([]byte, []byte, error) {
 	log.Info("Collecting prometheus metrics")
 	log.Info("Collecting prometheus metrics")
-	err := csConfig.LoadPrometheus()
-	if err != nil {
-		return nil, nil, err
-	}
 
 
 	if csConfig.Cscli.PrometheusUrl == "" {
 	if csConfig.Cscli.PrometheusUrl == "" {
 		log.Warn("No Prometheus URL configured, metrics will not be collected")
 		log.Warn("No Prometheus URL configured, metrics will not be collected")
@@ -69,7 +65,7 @@ func collectMetrics() ([]byte, []byte, error) {
 	}
 	}
 
 
 	humanMetrics := bytes.NewBuffer(nil)
 	humanMetrics := bytes.NewBuffer(nil)
-	err = FormatPrometheusMetrics(humanMetrics, csConfig.Cscli.PrometheusUrl, "human")
+	err := FormatPrometheusMetrics(humanMetrics, csConfig.Cscli.PrometheusUrl, "human")
 
 
 	if err != nil {
 	if err != nil {
 		return nil, nil, fmt.Errorf("could not fetch promtheus metrics: %s", err)
 		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) {
 func Suggest(itemType string, baseItem string, suggestItem string, score int, ignoreErr bool) {
 	errMsg := ""
 	errMsg := ""
 	if score < MaxDistance {
 	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 {
 	} else {
-		errMsg = fmt.Sprintf("unable to find %s '%s'", itemType, baseItem)
+		errMsg = fmt.Sprintf("can't find '%s' in %s", baseItem, itemType)
 	}
 	}
 	if ignoreErr {
 	if ignoreErr {
 		log.Error(errMsg)
 		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 {
 	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 {
 	switch csConfig.Cscli.Output {
 	case "human", "raw":
 	case "human", "raw":
 		b, err = yaml.Marshal(*hubItem)
 		b, err = yaml.Marshal(*hubItem)
 		if err != nil {
 		if err != nil {
-			log.Fatalf("unable to marshal item : %s", err)
+			return fmt.Errorf("unable to marshal item: %s", err)
 		}
 		}
 	case "json":
 	case "json":
 		b, err = json.MarshalIndent(*hubItem, "", " ")
 		b, err = json.MarshalIndent(*hubItem, "", " ")
 		if err != nil {
 		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))
 	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)
 	ShowMetrics(hubItem)
+
+	return nil
 }
 }
 
 
 func manageCliDecisionAlerts(ip *string, ipRange *string, scope *string, value *string) error {
 func manageCliDecisionAlerts(ip *string, ipRange *string, scope *string, value *string) error {

+ 8 - 0
pkg/csconfig/cscli.go

@@ -1,5 +1,9 @@
 package csconfig
 package csconfig
 
 
+import (
+	"fmt"
+)
+
 /*cscli specific config, such as hub directory*/
 /*cscli specific config, such as hub directory*/
 type CscliCfg struct {
 type CscliCfg struct {
 	Output             string            `yaml:"output,omitempty"`
 	Output             string            `yaml:"output,omitempty"`
@@ -27,5 +31,9 @@ func (c *Config) LoadCSCLI() error {
 	c.Cscli.HubDir = c.ConfigPaths.HubDir
 	c.Cscli.HubDir = c.ConfigPaths.HubDir
 	c.Cscli.HubIndexFile = c.ConfigPaths.HubIndexFile
 	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
 	return nil
 }
 }

+ 11 - 4
pkg/csconfig/cscli_test.go

@@ -38,12 +38,19 @@ func TestLoadCSCLI(t *testing.T) {
 					HubDir:       "./hub",
 					HubDir:       "./hub",
 					HubIndexFile: "./hub/.index.json",
 					HubIndexFile: "./hub/.index.json",
 				},
 				},
+				Prometheus: &PrometheusCfg{
+					Enabled:    true,
+					Level:      "full",
+					ListenAddr: "127.0.0.1",
+					ListenPort: 6060,
+				},
 			},
 			},
 			expected: &CscliCfg{
 			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
 package csconfig
 
 
-import "fmt"
-
 type PrometheusCfg struct {
 type PrometheusCfg struct {
 	Enabled    bool   `yaml:"enabled"`
 	Enabled    bool   `yaml:"enabled"`
 	Level      string `yaml:"level"` //aggregated|full
 	Level      string `yaml:"level"` //aggregated|full
 	ListenAddr string `yaml:"listen_addr"`
 	ListenAddr string `yaml:"listen_addr"`
 	ListenPort int    `yaml:"listen_port"`
 	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 != "" {
 	if name != "" {
 		item := GetItem(itemType, name)
 		item := GetItem(itemType, name)
 		if item == nil {
 		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)
 		err := DisableItem(csConfig.Hub, item, purge, forceAction)
@@ -151,7 +151,7 @@ func RemoveMany(csConfig *csconfig.Config, itemType string, name string, all boo
 	return nil
 	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
 	updated := 0
 	found := false
 	found := false
 
 
@@ -166,17 +166,17 @@ func UpgradeConfig(csConfig *csconfig.Config, itemType string, name string, forc
 		}
 		}
 
 
 		if !v.Downloaded {
 		if !v.Downloaded {
-			log.Warningf("%s : not downloaded, please install.", v.Name)
+			log.Warningf("%s: not downloaded, please install.", v.Name)
 			continue
 			continue
 		}
 		}
 
 
 		found = true
 		found = true
 
 
 		if v.UpToDate {
 		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 {
 			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 {
 			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 {
 		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 {
 		if !v.UpToDate {
@@ -203,14 +203,14 @@ func UpgradeConfig(csConfig *csconfig.Config, itemType string, name string, forc
 		}
 		}
 
 
 		if err := AddItem(itemType, v); err != nil {
 		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 == "" {
 	if !found && name == "" {
 		log.Infof("No %s installed, nothing to upgrade", itemType)
 		log.Infof("No %s installed, nothing to upgrade", itemType)
 	} else if !found {
 	} 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 {
 	} else if updated == 0 && found {
 		if name == "" {
 		if name == "" {
 			log.Infof("All %s are already up-to-date", itemType)
 			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 {
 	} else if updated != 0 {
 		log.Infof("Upgraded %d items", updated)
 		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"].UpToDate)
 	require.False(t, hubIdx[COLLECTIONS]["crowdsecurity/test_collection"].Tainted)
 	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")
 	assertCollectionDepsInstalled(t, "crowdsecurity/test_collection")
 
 
 	require.True(t, hubIdx[SCENARIOS]["crowdsecurity/barfoo_scenario"].Downloaded)
 	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"].Installed)
 	require.True(t, hubIdx[COLLECTIONS]["crowdsecurity/test_collection"].UpToDate)
 	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)
 		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)
 	getHubIdxOrFail(t)
 	require.False(t, hubIdx[SCENARIOS]["crowdsecurity/foobar_scenario"].Installed)
 	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
 	// we just removed. Nor should it install the newly added scenario
 	pushUpdateToCollectionInHub()
 	pushUpdateToCollectionInHub()
 
 
-	if err := UpdateHubIdx(cfg.Hub); err != nil {
+	if err = UpdateHubIdx(cfg.Hub); err != nil {
 		t.Fatalf("failed to download index : %s", err)
 		t.Fatalf("failed to download index : %s", err)
 	}
 	}
 
 
 	require.False(t, hubIdx[SCENARIOS]["crowdsecurity/foobar_scenario"].Installed)
 	require.False(t, hubIdx[SCENARIOS]["crowdsecurity/foobar_scenario"].Installed)
 	getHubIdxOrFail(t)
 	getHubIdxOrFail(t)
 
 
-	UpgradeConfig(cfg, COLLECTIONS, "crowdsecurity/test_collection", false)
+	err = UpgradeConfig(cfg, COLLECTIONS, "crowdsecurity/test_collection", false)
+	require.NoError(t, err)
+
 	getHubIdxOrFail(t)
 	getHubIdxOrFail(t)
 	require.False(t, hubIdx[SCENARIOS]["crowdsecurity/foobar_scenario"].Installed)
 	require.False(t, hubIdx[SCENARIOS]["crowdsecurity/foobar_scenario"].Installed)
 	require.True(t, hubIdx[SCENARIOS]["crowdsecurity/barfoo_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
 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
 # remove color and style sequences from stdin
 plaintext() {
 plaintext() {
     sed -E 's/\x1B\[[0-9;]*[JKmsu]//g'
     sed -E 's/\x1B\[[0-9;]*[JKmsu]//g'