Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions cmd/crowdsec-cli/clihub/hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os"
"strings"

"github.com/fatih/color"
log "github.com/sirupsen/logrus"
Expand Down Expand Up @@ -51,14 +52,15 @@ cscli hub upgrade`,

cmd.AddCommand(cli.newBranchCmd())
cmd.AddCommand(cli.newListCmd())
cmd.AddCommand(cli.newSearchCmd())
cmd.AddCommand(cli.newUpdateCmd())
cmd.AddCommand(cli.newUpgradeCmd())
cmd.AddCommand(cli.newTypesCmd())

return cmd
}

func (cli *cliHub) List(out io.Writer, hub *cwhub.Hub, all bool) error {
func (cli *cliHub) List(out io.Writer, hub *cwhub.Hub, all bool, full bool, statuses []string) error {
cfg := cli.cfg()

for _, v := range hub.Warnings {
Expand All @@ -78,13 +80,39 @@ func (cli *cliHub) List(out io.Writer, hub *cwhub.Hub, all bool) error {
if err != nil {
return err
}

items[itemType] = filterItemsByStatus(items[itemType], statuses)
}

err = ListItems(out, cfg.Cscli.Color, cwhub.ItemTypes, items, true, cfg.Cscli.Output)
if err != nil {
return err
// json/raw keep the per-type structure for scripts, regardless of the human view
if cfg.Cscli.Output != "human" {
return ListItems(out, cfg.Cscli.Color, cwhub.ItemTypes, items, true, cfg.Cscli.Output)
}

// -a: flat table of every item type (installed and not)
if all {
merged := make([]*cwhub.Item, 0)
for _, itemType := range cwhub.ItemTypes {
merged = append(merged, items[itemType]...)
}

if len(merged) == 0 {
fmt.Fprintln(out, "No items to display")
return nil
}

listHubItemCompactTable(out, hub, cfg.Cscli.Color, merged, false)

return nil
}

// default: a tree of installed collections (sub-collections nested) + a group of
// standalone installed items. Status pruning happens inside the tree walk.
roots := installedRootCollections(hub)
standalone := filterItemsByStatus(installedStandalone(hub), statuses)

listHubOverviewTable(out, hub, cfg.Cscli.Color, roots, standalone, statuses, full)

return nil
}

Expand Down Expand Up @@ -115,25 +143,43 @@ func (cli *cliHub) newBranchCmd() *cobra.Command {
}

func (cli *cliHub) newListCmd() *cobra.Command {
var all bool
var (
all bool
full bool
statuses []string
)

cmd := &cobra.Command{
Use: "list [-a]",
Short: "List all installed configurations",
Use: "list [-a]",
Short: "List relevant installed items",
Long: `List installed relevant items (collections, standalone items) and shows their status.
Use --all to list all items, including those not installed.
Use --full to expand every installed item in the tree, not just collections.`,
Args: args.NoArgs,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, _ []string) error {
if err := validateStatuses(statuses); err != nil {
return err
}

hub, err := require.Hub(cli.cfg(), log.StandardLogger())
if err != nil {
return err
}

return cli.List(color.Output, hub, all)
return cli.List(color.Output, hub, all, full, statuses)
},
}

flags := cmd.Flags()
flags.BoolVarP(&all, "all", "a", false, "List all available items, including those not installed")
flags.BoolVar(&full, "full", false, "Show every installed item in the tree, not just collections")
flags.StringSliceVar(&statuses, "status", nil, "Filter by status ("+strings.Join(validItemStatuses, ", ")+")")
cmd.MarkFlagsMutuallyExclusive("all", "full")

_ = cmd.RegisterFlagCompletionFunc("status", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return validItemStatuses, cobra.ShellCompDirectiveNoFileComp
})

return cmd
}
Expand Down
95 changes: 95 additions & 0 deletions cmd/crowdsec-cli/clihub/items.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,101 @@ func SelectItems(hub *cwhub.Hub, itemType string, args []string, installedOnly b
return wantedItems, nil
}

// validItemStatuses are the accepted values for the --status filter of hub list/search.
var validItemStatuses = []string{"installed", cwhub.StatusNotInstalled, cwhub.StatusUpToDate, cwhub.StatusOutdated, cwhub.StatusTainted, cwhub.StatusLocal}

// validateStatuses returns an error if a token is not a recognized status filter.
func validateStatuses(statuses []string) error {
for _, s := range statuses {
if !slices.Contains(validItemStatuses, s) {
return fmt.Errorf("invalid status %q (valid values: %s)", s, strings.Join(validItemStatuses, ", "))
}
}

return nil
}

// itemMatchesStatus reports whether an item's local state matches any of the given status tokens.
// An empty list matches everything. All tokens except "installed" map to a cwhub.Status* word.
func itemMatchesStatus(item *cwhub.Item, statuses []string) bool {
if len(statuses) == 0 {
return true
}

for _, s := range statuses {
if s == "installed" {
if item.State.IsInstalled() {
return true
}

continue
}

if item.State.Status() == s {
return true
}
}

return false
}

// filterItemsByStatus returns the items whose state matches any of the given status tokens.
func filterItemsByStatus(items []*cwhub.Item, statuses []string) []*cwhub.Item {
if len(statuses) == 0 {
return items
}

ret := make([]*cwhub.Item, 0, len(items))

for _, item := range items {
if itemMatchesStatus(item, statuses) {
ret = append(ret, item)
}
}

return ret
}

// belongsToInstalledCollection reports whether the item is pulled in by at least one installed
// collection (as opposed to being installed on its own).
func belongsToInstalledCollection(item *cwhub.Item) bool {
return len(item.InstalledParents()) > 0
}

// installedRootCollections returns installed collections that are not contained in any other
// installed collection. They are the roots of the "cscli hub list" tree.
func installedRootCollections(hub *cwhub.Hub) []*cwhub.Item {
ret := make([]*cwhub.Item, 0)

for _, item := range hub.GetInstalledByType(cwhub.COLLECTIONS, true) {
if !belongsToInstalledCollection(item) {
ret = append(ret, item)
}
}

return ret
}

// installedStandalone returns installed non-collection items that are not part of any installed
// collection (eg. directly-installed or local items), sorted by type then name.
func installedStandalone(hub *cwhub.Hub) []*cwhub.Item {
ret := make([]*cwhub.Item, 0)

for _, itemType := range cwhub.ItemTypes {
if itemType == cwhub.COLLECTIONS {
continue
}

for _, item := range hub.GetInstalledByType(itemType, true) {
if !belongsToInstalledCollection(item) {
ret = append(ret, item)
}
}
}

return ret
}

func ListItems(out io.Writer, wantColor string, itemTypes []string, items map[string][]*cwhub.Item, omitIfEmpty bool, output string) error {
switch output {
case "human":
Expand Down
100 changes: 100 additions & 0 deletions cmd/crowdsec-cli/clihub/search.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package clihub

import (
"fmt"
"io"
"strings"

"github.com/fatih/color"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"

"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/core/require"
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
)

// itemMatchesTerms reports whether every term (case-insensitive) is a substring of the item name or description.
func itemMatchesTerms(item *cwhub.Item, terms []string) bool {
haystack := strings.ToLower(item.Name + " " + item.Description)

for _, term := range terms {
if !strings.Contains(haystack, strings.ToLower(term)) {
return false
}
}

return true
}

func (cli *cliHub) search(out io.Writer, hub *cwhub.Hub, terms []string, statuses []string) error {
cfg := cli.cfg()

items := make(map[string][]*cwhub.Item)

for _, itemType := range cwhub.ItemTypes {
matched := make([]*cwhub.Item, 0)

for _, item := range hub.GetItemsByType(itemType, true) {
if itemMatchesTerms(item, terms) && itemMatchesStatus(item, statuses) {
matched = append(matched, item)
}
}

items[itemType] = matched
}

// human output is a single compact table; json/raw keep the per-type structure for scripts
if cfg.Cscli.Output == "human" {
merged := make([]*cwhub.Item, 0)
for _, itemType := range cwhub.ItemTypes {
merged = append(merged, items[itemType]...)
}

if len(merged) == 0 {
fmt.Fprintln(out, "No matching items")
return nil
}

listHubItemCompactTable(out, hub, cfg.Cscli.Color, merged, true)

return nil
}

return ListItems(out, cfg.Cscli.Color, cwhub.ItemTypes, items, true, cfg.Cscli.Output)
}

func (cli *cliHub) newSearchCmd() *cobra.Command {
var statuses []string

cmd := &cobra.Command{
Use: "search [term]...",
Short: "Search the local hub index by name and description",
Long: `Search the local hub index.
An item matches when its name or description contains all the given terms.`,
Example: `cscli hub search nginx
cscli hub search http cve
cscli hub search ssh --status installed`,
DisableAutoGenTag: true,
RunE: func(_ *cobra.Command, terms []string) error {
if err := validateStatuses(statuses); err != nil {
return err
}

hub, err := require.Hub(cli.cfg(), log.StandardLogger())
if err != nil {
return err
}

return cli.search(color.Output, hub, terms, statuses)
},
}

flags := cmd.Flags()
flags.StringSliceVar(&statuses, "status", nil, "Filter by status ("+strings.Join(validItemStatuses, ", ")+")")

_ = cmd.RegisterFlagCompletionFunc("status", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return validItemStatuses, cobra.ShellCompDirectiveNoFileComp
})

return cmd
}
Loading
Loading