diff --git a/cmd/crowdsec-cli/config_backup.go b/cmd/crowdsec-cli/config_backup.go index 93772d611..c4d09e687 100644 --- a/cmd/crowdsec-cli/config_backup.go +++ b/cmd/crowdsec-cli/config_backup.go @@ -46,7 +46,7 @@ func backupHub(dirPath string) error { } //for the local/tainted ones, we back up the full file - if v.State.Tainted || v.IsLocal() || !v.State.UpToDate { + if v.State.Tainted || v.State.IsLocal() || !v.State.UpToDate { //we need to backup stages for parsers if itemType == cwhub.PARSERS || itemType == cwhub.POSTOVERFLOWS { fstagedir := fmt.Sprintf("%s%s", itemDirectory, v.Stage) @@ -54,7 +54,7 @@ func backupHub(dirPath string) error { return fmt.Errorf("error while creating stage dir %s : %s", fstagedir, err) } } - clog.Debugf("[%s]: backing up file (tainted:%t local:%t up-to-date:%t)", k, v.State.Tainted, v.IsLocal(), v.State.UpToDate) + clog.Debugf("[%s]: backing up file (tainted:%t local:%t up-to-date:%t)", k, v.State.Tainted, v.State.IsLocal(), v.State.UpToDate) tfile := fmt.Sprintf("%s%s/%s", itemDirectory, v.Stage, v.FileName) if err = CopyFile(v.State.LocalPath, tfile); err != nil { return fmt.Errorf("failed copy %s %s to %s : %s", itemType, v.State.LocalPath, tfile, err) diff --git a/cmd/crowdsec-cli/hub.go b/cmd/crowdsec-cli/hub.go index 2d139cd90..0a48e0231 100644 --- a/cmd/crowdsec-cli/hub.go +++ b/cmd/crowdsec-cli/hub.go @@ -66,7 +66,7 @@ func runHubList(cmd *cobra.Command, args []string) error { } } - err = listItems(color.Output, cwhub.ItemTypes, items) + err = listItems(color.Output, cwhub.ItemTypes, items, true) if err != nil { return err } diff --git a/cmd/crowdsec-cli/itemcommands.go b/cmd/crowdsec-cli/itemcommands.go index a95803def..5c699fde9 100644 --- a/cmd/crowdsec-cli/itemcommands.go +++ b/cmd/crowdsec-cli/itemcommands.go @@ -580,7 +580,7 @@ func itemsListRunner(it hubItemType) func(cmd *cobra.Command, args []string) err return err } - if err = listItems(color.Output, []string{it.name}, items); err != nil { + if err = listItems(color.Output, []string{it.name}, items, false); err != nil { return err } diff --git a/cmd/crowdsec-cli/items.go b/cmd/crowdsec-cli/items.go index c77ff3f88..6a91ab1f5 100644 --- a/cmd/crowdsec-cli/items.go +++ b/cmd/crowdsec-cli/items.go @@ -53,11 +53,19 @@ func selectItems(hub *cwhub.Hub, itemType string, args []string, installedOnly b return items, nil } -func listItems(out io.Writer, itemTypes []string, items map[string][]*cwhub.Item) error { +func listItems(out io.Writer, itemTypes []string, items map[string][]*cwhub.Item, omitIfEmpty bool) error { switch csConfig.Cscli.Output { case "human": + nothingToDisplay := true for _, itemType := range itemTypes { + if omitIfEmpty && len(items[itemType]) == 0 { + continue + } listHubItemTable(out, "\n"+strings.ToUpper(itemType), items[itemType]) + nothingToDisplay = false + } + if nothingToDisplay { + fmt.Println("No items to display") } case "json": type itemHubStatus struct { @@ -75,14 +83,15 @@ func listItems(out io.Writer, itemTypes []string, items map[string][]*cwhub.Item hubStatus[itemType] = make([]itemHubStatus, len(items[itemType])) for i, item := range items[itemType] { - status, emo := item.InstallStatus() + status := item.State.Text() + status_emo := item.State.Emoji() hubStatus[itemType][i] = itemHubStatus{ Name: item.Name, LocalVersion: item.State.LocalVersion, LocalPath: item.State.LocalPath, Description: item.Description, Status: status, - UTF8Status: fmt.Sprintf("%v %s", emo, status), + UTF8Status: fmt.Sprintf("%v %s", status_emo, status), } } } @@ -107,10 +116,9 @@ func listItems(out io.Writer, itemTypes []string, items map[string][]*cwhub.Item for _, itemType := range itemTypes { for _, item := range items[itemType] { - status, _ := item.InstallStatus() row := []string{ item.Name, - status, + item.State.Text(), item.State.LocalVersion, item.Description, } diff --git a/cmd/crowdsec-cli/support.go b/cmd/crowdsec-cli/support.go index 1470d37aa..6395ad6d7 100644 --- a/cmd/crowdsec-cli/support.go +++ b/cmd/crowdsec-cli/support.go @@ -140,7 +140,7 @@ func collectHubItems(hub *cwhub.Hub, itemType string) []byte { log.Warnf("could not collect %s list: %s", itemType, err) } - if err := listItems(out, []string{itemType}, items); err != nil { + if err := listItems(out, []string{itemType}, items, false); err != nil { log.Warnf("could not collect %s list: %s", itemType, err) } return out.Bytes() diff --git a/cmd/crowdsec-cli/utils_table.go b/cmd/crowdsec-cli/utils_table.go index 28b185a01..f3179e40f 100644 --- a/cmd/crowdsec-cli/utils_table.go +++ b/cmd/crowdsec-cli/utils_table.go @@ -18,8 +18,8 @@ func listHubItemTable(out io.Writer, title string, items []*cwhub.Item) { t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft) for _, item := range items { - status, emo := item.InstallStatus() - t.AddRow(item.Name, fmt.Sprintf("%v %s", emo, status), item.State.LocalVersion, item.State.LocalPath) + status := fmt.Sprintf("%v %s", item.State.Emoji(), item.State.Text()) + t.AddRow(item.Name, status, item.State.LocalVersion, item.State.LocalPath) } renderTableTitle(out, title) t.Render() diff --git a/cmd/crowdsec/main.go b/cmd/crowdsec/main.go index 8c7fb2991..362ed1869 100644 --- a/cmd/crowdsec/main.go +++ b/cmd/crowdsec/main.go @@ -262,10 +262,6 @@ func LoadConfig(configFile string, disableAgent bool, disableAPI bool, quiet boo return nil, errors.New("You must run at least the API Server or crowdsec") } - if flags.TestMode && !cConfig.DisableAgent { - cConfig.Crowdsec.LintOnly = true - } - if flags.OneShotDSN != "" && flags.SingleFileType == "" { return nil, errors.New("-dsn requires a -type argument") } diff --git a/pkg/csconfig/crowdsec_service.go b/pkg/csconfig/crowdsec_service.go index dc226cfd6..c0b8b5a38 100644 --- a/pkg/csconfig/crowdsec_service.go +++ b/pkg/csconfig/crowdsec_service.go @@ -23,7 +23,6 @@ type CrowdsecServiceCfg struct { BucketsRoutinesCount int `yaml:"buckets_routines"` OutputRoutinesCount int `yaml:"output_routines"` SimulationConfig *SimulationConfig `yaml:"-"` - LintOnly bool `yaml:"-"` // if set to true, exit after loading configs BucketStateFile string `yaml:"state_input_file,omitempty"` // if we need to unserialize buckets at start BucketStateDumpDir string `yaml:"state_output_dir,omitempty"` // if we need to unserialize buckets on shutdown BucketsGCEnabled bool `yaml:"-"` // we need to garbage collect buckets when in forensic mode diff --git a/pkg/cwhub/hub.go b/pkg/cwhub/hub.go index 18218bd87..bd623e1f9 100644 --- a/pkg/cwhub/hub.go +++ b/pkg/cwhub/hub.go @@ -109,7 +109,7 @@ func (h *Hub) ItemStats() []string { loaded += fmt.Sprintf("%d %s, ", len(h.GetItemMap(itemType)), itemType) for _, item := range h.GetItemMap(itemType) { - if item.IsLocal() { + if item.State.IsLocal() { local++ } diff --git a/pkg/cwhub/item.go b/pkg/cwhub/item.go index 9c3ec8cd2..9cf044396 100644 --- a/pkg/cwhub/item.go +++ b/pkg/cwhub/item.go @@ -53,6 +53,48 @@ type ItemState struct { BelongsToCollections []string `json:"belongs_to_collections,omitempty" yaml:"belongs_to_collections,omitempty"` } +// IsLocal returns true if the item has been create by a user (not downloaded from the hub). +func (s *ItemState) IsLocal() bool { + return s.Installed && !s.Downloaded +} + +// Text returns the status of the item as a string (eg. "enabled,update-available"). +func (s *ItemState) Text() string { + ret := "disabled" + + if s.Installed { + ret = "enabled" + } + + if s.IsLocal() { + ret += ",local" + } + + if s.Tainted { + ret += ",tainted" + } else if !s.UpToDate && !s.IsLocal() { + ret += ",update-available" + } + + return ret +} + +// Emoji returns the status of the item as an emoji (eg. emoji.Warning). +func (s *ItemState) Emoji() emoji.Emoji { + switch { + case s.IsLocal(): + return emoji.House + case !s.Installed: + return emoji.Prohibited + case s.Tainted || (!s.UpToDate && !s.IsLocal()): + return emoji.Warning + case s.Installed: + return emoji.CheckMark + default: + return emoji.QuestionMark + } +} + // Item is created from an index file and enriched with local info. type Item struct { hub *Hub // back pointer to the hub, to retrieve other items and call install/remove methods @@ -107,11 +149,6 @@ func (i *Item) HasSubItems() bool { return i.Type == COLLECTIONS } -// IsLocal returns true if the item has been create by a user (not downloaded from the hub). -func (i *Item) IsLocal() bool { - return i.State.Installed && !i.State.Downloaded -} - // MarshalJSON is used to prepare the output for "cscli ... inspect -o json". // It must not use a pointer receiver. func (i Item) MarshalJSON() ([]byte, error) { @@ -139,7 +176,7 @@ func (i Item) MarshalJSON() ([]byte, error) { UpToDate: i.State.UpToDate, Tainted: i.State.Tainted, BelongsToCollections: i.State.BelongsToCollections, - Local: i.IsLocal(), + Local: i.State.IsLocal(), }) } @@ -155,7 +192,7 @@ func (i Item) MarshalYAML() (interface{}, error) { }{ Alias: Alias(i), State: i.State, - Local: i.IsLocal(), + Local: i.State.IsLocal(), }, nil } @@ -290,48 +327,6 @@ func (i *Item) descendants() ([]*Item, error) { return ret, nil } -// InstallStatus returns the status of the item as a string and an emoji -// (eg. "enabled,update-available" and emoji.Warning). -func (i *Item) InstallStatus() (string, emoji.Emoji) { - status := "disabled" - ok := false - - if i.State.Installed { - ok = true - status = "enabled" - } - - managed := true - if i.IsLocal() { - managed = false - status += ",local" - } - - warning := false - if i.State.Tainted { - warning = true - status += ",tainted" - } else if !i.State.UpToDate && !i.IsLocal() { - warning = true - status += ",update-available" - } - - emo := emoji.QuestionMark - - switch { - case !managed: - emo = emoji.House - case !i.State.Installed: - emo = emoji.Prohibited - case warning: - emo = emoji.Warning - case ok: - emo = emoji.CheckMark - } - - return status, emo -} - // versionStatus returns the status of the item version compared to the hub version. // semver requires the 'v' prefix. func (i *Item) versionStatus() int { diff --git a/pkg/cwhub/item_test.go b/pkg/cwhub/item_test.go index 33a52e8e0..703bbb5cb 100644 --- a/pkg/cwhub/item_test.go +++ b/pkg/cwhub/item_test.go @@ -23,7 +23,7 @@ func TestItemStatus(t *testing.T) { item.State.Tainted = false item.State.Downloaded = true - txt, _ := item.InstallStatus() + txt := item.State.Text() require.Equal(t, "enabled,update-available", txt) item.State.Installed = true @@ -31,7 +31,7 @@ func TestItemStatus(t *testing.T) { item.State.Tainted = false item.State.Downloaded = false - txt, _ = item.InstallStatus() + txt = item.State.Text() require.Equal(t, "enabled,local", txt) } diff --git a/pkg/cwhub/iteminstall.go b/pkg/cwhub/iteminstall.go index 038e5471d..270d4c114 100644 --- a/pkg/cwhub/iteminstall.go +++ b/pkg/cwhub/iteminstall.go @@ -13,7 +13,7 @@ func (i *Item) enable() error { return fmt.Errorf("%s is tainted, won't enable unless --force", i.Name) } - if i.IsLocal() { + if i.State.IsLocal() { return fmt.Errorf("%s is local, won't enable", i.Name) } diff --git a/pkg/cwhub/itemremove.go b/pkg/cwhub/itemremove.go index cba5a5904..a58bd3fa8 100644 --- a/pkg/cwhub/itemremove.go +++ b/pkg/cwhub/itemremove.go @@ -66,7 +66,7 @@ func (i *Item) disable(purge bool, force bool) (bool, error) { // Remove disables the item, optionally removing the downloaded content. func (i *Item) Remove(purge bool, force bool) (bool, error) { - if i.IsLocal() { + if i.State.IsLocal() { log.Warningf("%s is a local item, please delete manually", i.Name) return false, nil } diff --git a/pkg/cwhub/itemupgrade.go b/pkg/cwhub/itemupgrade.go index 5e7c6f71a..07c83b3c4 100644 --- a/pkg/cwhub/itemupgrade.go +++ b/pkg/cwhub/itemupgrade.go @@ -20,7 +20,7 @@ import ( func (i *Item) Upgrade(force bool) (bool, error) { updated := false - if i.IsLocal() { + if i.State.IsLocal() { log.Infof("not upgrading %s: local item", i.Name) return false, nil } @@ -155,7 +155,7 @@ func (i *Item) fetch() ([]byte, error) { // download downloads the item from the hub and writes it to the hub directory. func (i *Item) download(overwrite bool) (string, error) { - if i.IsLocal() { + if i.State.IsLocal() { return "", fmt.Errorf("%s is local, can't download", i.Name) } // if user didn't --force, don't overwrite local, tainted, up-to-date files diff --git a/pkg/cwhub/sync.go b/pkg/cwhub/sync.go index bbd8b2724..ae62b9342 100644 --- a/pkg/cwhub/sync.go +++ b/pkg/cwhub/sync.go @@ -420,7 +420,7 @@ func (h *Hub) localSync() error { case versionFuture: warnings = append(warnings, fmt.Sprintf("collection %s is in the future (currently:%s, latest:%s)", item.Name, item.State.LocalVersion, item.Version)) case versionUnknown: - if !item.IsLocal() { + if !item.State.IsLocal() { warnings = append(warnings, fmt.Sprintf("collection %s is tainted (latest:%s)", item.Name, item.Version)) } } diff --git a/test/bats/20_hub.bats b/test/bats/20_hub.bats index 5189044b9..f64a64e4b 100644 --- a/test/bats/20_hub.bats +++ b/test/bats/20_hub.bats @@ -34,17 +34,19 @@ teardown() { # no items rune -0 cscli hub list - assert_output --regexp ".*PARSERS.*POSTOVERFLOWS.*SCENARIOS.*COLLECTIONS.*" + assert_output "No items to display" rune -0 cscli hub list -o json assert_json '{parsers:[],scenarios:[],collections:[],postoverflows:[]}' rune -0 cscli hub list -o raw assert_output 'name,status,version,description,type' - # some items + # some items: with output=human, show only non-empty tables rune -0 cscli parsers install crowdsecurity/whitelists rune -0 cscli scenarios install crowdsecurity/telnet-bf rune -0 cscli hub list - assert_output --regexp ".*PARSERS.*crowdsecurity/whitelists.*POSTOVERFLOWS.*SCENARIOS.*crowdsecurity/telnet-bf.*COLLECTIONS.*" + assert_output --regexp ".*PARSERS.*crowdsecurity/whitelists.*SCENARIOS.*crowdsecurity/telnet-bf.*" + refute_output --partial 'POSTOVERFLOWS' + refute_output --partial 'COLLECTIONS' rune -0 cscli hub list -o json rune -0 jq -e '(.parsers | length == 1) and (.scenarios | length == 1)' <(output) rune -0 cscli hub list -o raw