github.com/crowdsecurity/crowdsec@v1.6.1/pkg/cwhub/item.go (about)

     1  package cwhub
     2  
     3  import (
     4  	"encoding/json"
     5  	"fmt"
     6  	"path/filepath"
     7  	"slices"
     8  
     9  	"github.com/Masterminds/semver/v3"
    10  
    11  	"github.com/crowdsecurity/crowdsec/pkg/emoji"
    12  )
    13  
    14  const (
    15  	// managed item types.
    16  	COLLECTIONS    = "collections"
    17  	PARSERS        = "parsers"
    18  	POSTOVERFLOWS  = "postoverflows"
    19  	SCENARIOS      = "scenarios"
    20  	CONTEXTS       = "contexts"
    21  	APPSEC_CONFIGS = "appsec-configs"
    22  	APPSEC_RULES   = "appsec-rules"
    23  )
    24  
    25  const (
    26  	versionUpToDate        = iota // the latest version from index is installed
    27  	versionUpdateAvailable        // not installed, or lower than latest
    28  	versionUnknown                // local file with no version, or invalid version number
    29  	versionFuture                 // local version is higher latest, but is included in the index: should not happen
    30  )
    31  
    32  var (
    33  	// The order is important, as it is used to range over sub-items in collections.
    34  	ItemTypes = []string{PARSERS, POSTOVERFLOWS, SCENARIOS, CONTEXTS, APPSEC_CONFIGS, APPSEC_RULES, COLLECTIONS}
    35  )
    36  
    37  type HubItems map[string]map[string]*Item
    38  
    39  // ItemVersion is used to detect the version of a given item
    40  // by comparing the hash of each version to the local file.
    41  // If the item does not match any known version, it is considered tainted (modified).
    42  type ItemVersion struct {
    43  	Digest     string `json:"digest,omitempty" yaml:"digest,omitempty"`
    44  	Deprecated bool   `json:"deprecated,omitempty" yaml:"deprecated,omitempty"`
    45  }
    46  
    47  // ItemState is used to keep the local state (i.e. at runtime) of an item.
    48  // This data is not stored in the index, but is displayed with "cscli ... inspect".
    49  type ItemState struct {
    50  	LocalPath            string   `json:"local_path,omitempty" yaml:"local_path,omitempty"`
    51  	LocalVersion         string   `json:"local_version,omitempty" yaml:"local_version,omitempty"`
    52  	LocalHash            string   `json:"local_hash,omitempty" yaml:"local_hash,omitempty"`
    53  	Installed            bool     `json:"installed"`
    54  	Downloaded           bool     `json:"downloaded"`
    55  	UpToDate             bool     `json:"up_to_date"`
    56  	Tainted              bool     `json:"tainted"`
    57  	TaintedBy            []string `json:"tainted_by,omitempty" yaml:"tainted_by,omitempty"`
    58  	BelongsToCollections []string `json:"belongs_to_collections,omitempty" yaml:"belongs_to_collections,omitempty"`
    59  }
    60  
    61  // IsLocal returns true if the item has been create by a user (not downloaded from the hub).
    62  func (s *ItemState) IsLocal() bool {
    63  	return s.Installed && !s.Downloaded
    64  }
    65  
    66  // Text returns the status of the item as a string (eg. "enabled,update-available").
    67  func (s *ItemState) Text() string {
    68  	ret := "disabled"
    69  
    70  	if s.Installed {
    71  		ret = "enabled"
    72  	}
    73  
    74  	if s.IsLocal() {
    75  		ret += ",local"
    76  	}
    77  
    78  	if s.Tainted {
    79  		ret += ",tainted"
    80  	} else if !s.UpToDate && !s.IsLocal() {
    81  		ret += ",update-available"
    82  	}
    83  
    84  	return ret
    85  }
    86  
    87  // Emoji returns the status of the item as an emoji (eg. emoji.Warning).
    88  func (s *ItemState) Emoji() string {
    89  	switch {
    90  	case s.IsLocal():
    91  		return emoji.House
    92  	case !s.Installed:
    93  		return emoji.Prohibited
    94  	case s.Tainted || (!s.UpToDate && !s.IsLocal()):
    95  		return emoji.Warning
    96  	case s.Installed:
    97  		return emoji.CheckMark
    98  	default:
    99  		return emoji.QuestionMark
   100  	}
   101  }
   102  
   103  // Item is created from an index file and enriched with local info.
   104  type Item struct {
   105  	hub *Hub // back pointer to the hub, to retrieve other items and call install/remove methods
   106  
   107  	State ItemState `json:"-" yaml:"-"` // local state, not stored in the index
   108  
   109  	Type        string   `json:"type,omitempty" yaml:"type,omitempty"`           // one of the ItemTypes
   110  	Stage       string   `json:"stage,omitempty" yaml:"stage,omitempty"`         // Stage for parser|postoverflow: s00-raw/s01-...
   111  	Name        string   `json:"name,omitempty" yaml:"name,omitempty"`           // usually "author/name"
   112  	FileName    string   `json:"file_name,omitempty" yaml:"file_name,omitempty"` // eg. apache2-logs.yaml
   113  	Description string   `json:"description,omitempty" yaml:"description,omitempty"`
   114  	Author      string   `json:"author,omitempty" yaml:"author,omitempty"`
   115  	References  []string `json:"references,omitempty" yaml:"references,omitempty"`
   116  
   117  	RemotePath string                 `json:"path,omitempty" yaml:"path,omitempty"`       // path relative to the base URL eg. /parsers/stage/author/file.yaml
   118  	Version    string                 `json:"version,omitempty" yaml:"version,omitempty"` // the last available version
   119  	Versions   map[string]ItemVersion `json:"versions,omitempty"  yaml:"-"`               // all the known versions
   120  
   121  	// if it's a collection, it can have sub items
   122  	Parsers       []string `json:"parsers,omitempty" yaml:"parsers,omitempty"`
   123  	PostOverflows []string `json:"postoverflows,omitempty" yaml:"postoverflows,omitempty"`
   124  	Scenarios     []string `json:"scenarios,omitempty" yaml:"scenarios,omitempty"`
   125  	Collections   []string `json:"collections,omitempty" yaml:"collections,omitempty"`
   126  	Contexts      []string `json:"contexts,omitempty" yaml:"contexts,omitempty"`
   127  	AppsecConfigs []string `json:"appsec-configs,omitempty"   yaml:"appsec-configs,omitempty"`
   128  	AppsecRules   []string `json:"appsec-rules,omitempty"   yaml:"appsec-rules,omitempty"`
   129  }
   130  
   131  // installPath returns the location of the symlink to the item in the hub, or the path of the item itself if it's local
   132  // (eg. /etc/crowdsec/collections/xyz.yaml).
   133  // Raises an error if the path goes outside of the install dir.
   134  func (i *Item) installPath() (string, error) {
   135  	p := i.Type
   136  	if i.Stage != "" {
   137  		p = filepath.Join(p, i.Stage)
   138  	}
   139  
   140  	return safePath(i.hub.local.InstallDir, filepath.Join(p, i.FileName))
   141  }
   142  
   143  // downloadPath returns the location of the actual config file in the hub
   144  // (eg. /etc/crowdsec/hub/collections/author/xyz.yaml).
   145  // Raises an error if the path goes outside of the hub dir.
   146  func (i *Item) downloadPath() (string, error) {
   147  	ret, err := safePath(i.hub.local.HubDir, i.RemotePath)
   148  	if err != nil {
   149  		return "", err
   150  	}
   151  
   152  	return ret, nil
   153  }
   154  
   155  // HasSubItems returns true if items of this type can have sub-items. Currently only collections.
   156  func (i *Item) HasSubItems() bool {
   157  	return i.Type == COLLECTIONS
   158  }
   159  
   160  // MarshalJSON is used to prepare the output for "cscli ... inspect -o json".
   161  // It must not use a pointer receiver.
   162  func (i Item) MarshalJSON() ([]byte, error) {
   163  	type Alias Item
   164  
   165  	return json.Marshal(&struct {
   166  		Alias
   167  		// we have to repeat the fields here, json will have inline support in v2
   168  		LocalPath            string   `json:"local_path,omitempty"`
   169  		LocalVersion         string   `json:"local_version,omitempty"`
   170  		LocalHash            string   `json:"local_hash,omitempty"`
   171  		Installed            bool     `json:"installed"`
   172  		Downloaded           bool     `json:"downloaded"`
   173  		UpToDate             bool     `json:"up_to_date"`
   174  		Tainted              bool     `json:"tainted"`
   175  		Local                bool     `json:"local"`
   176  		BelongsToCollections []string `json:"belongs_to_collections,omitempty"`
   177  	}{
   178  		Alias:                Alias(i),
   179  		LocalPath:            i.State.LocalPath,
   180  		LocalVersion:         i.State.LocalVersion,
   181  		LocalHash:            i.State.LocalHash,
   182  		Installed:            i.State.Installed,
   183  		Downloaded:           i.State.Downloaded,
   184  		UpToDate:             i.State.UpToDate,
   185  		Tainted:              i.State.Tainted,
   186  		BelongsToCollections: i.State.BelongsToCollections,
   187  		Local:                i.State.IsLocal(),
   188  	})
   189  }
   190  
   191  // MarshalYAML is used to prepare the output for "cscli ... inspect -o raw".
   192  // It must not use a pointer receiver.
   193  func (i Item) MarshalYAML() (interface{}, error) {
   194  	type Alias Item
   195  
   196  	return &struct {
   197  		Alias `yaml:",inline"`
   198  		State ItemState `yaml:",inline"`
   199  		Local bool      `yaml:"local"`
   200  	}{
   201  		Alias: Alias(i),
   202  		State: i.State,
   203  		Local: i.State.IsLocal(),
   204  	}, nil
   205  }
   206  
   207  // SubItems returns a slice of sub-items, excluding the ones that were not found.
   208  func (i *Item) SubItems() []*Item {
   209  	sub := make([]*Item, 0)
   210  
   211  	for _, name := range i.Parsers {
   212  		s := i.hub.GetItem(PARSERS, name)
   213  		if s == nil {
   214  			continue
   215  		}
   216  
   217  		sub = append(sub, s)
   218  	}
   219  
   220  	for _, name := range i.PostOverflows {
   221  		s := i.hub.GetItem(POSTOVERFLOWS, name)
   222  		if s == nil {
   223  			continue
   224  		}
   225  
   226  		sub = append(sub, s)
   227  	}
   228  
   229  	for _, name := range i.Scenarios {
   230  		s := i.hub.GetItem(SCENARIOS, name)
   231  		if s == nil {
   232  			continue
   233  		}
   234  
   235  		sub = append(sub, s)
   236  	}
   237  
   238  	for _, name := range i.Contexts {
   239  		s := i.hub.GetItem(CONTEXTS, name)
   240  		if s == nil {
   241  			continue
   242  		}
   243  
   244  		sub = append(sub, s)
   245  	}
   246  
   247  	for _, name := range i.AppsecConfigs {
   248  		s := i.hub.GetItem(APPSEC_CONFIGS, name)
   249  		if s == nil {
   250  			continue
   251  		}
   252  
   253  		sub = append(sub, s)
   254  	}
   255  
   256  	for _, name := range i.AppsecRules {
   257  		s := i.hub.GetItem(APPSEC_RULES, name)
   258  		if s == nil {
   259  			continue
   260  		}
   261  
   262  		sub = append(sub, s)
   263  	}
   264  
   265  	for _, name := range i.Collections {
   266  		s := i.hub.GetItem(COLLECTIONS, name)
   267  		if s == nil {
   268  			continue
   269  		}
   270  
   271  		sub = append(sub, s)
   272  	}
   273  
   274  	return sub
   275  }
   276  
   277  func (i *Item) logMissingSubItems() {
   278  	if !i.HasSubItems() {
   279  		return
   280  	}
   281  
   282  	for _, subName := range i.Parsers {
   283  		if i.hub.GetItem(PARSERS, subName) == nil {
   284  			i.hub.logger.Errorf("can't find %s in %s, required by %s", subName, PARSERS, i.Name)
   285  		}
   286  	}
   287  
   288  	for _, subName := range i.Scenarios {
   289  		if i.hub.GetItem(SCENARIOS, subName) == nil {
   290  			i.hub.logger.Errorf("can't find %s in %s, required by %s", subName, SCENARIOS, i.Name)
   291  		}
   292  	}
   293  
   294  	for _, subName := range i.PostOverflows {
   295  		if i.hub.GetItem(POSTOVERFLOWS, subName) == nil {
   296  			i.hub.logger.Errorf("can't find %s in %s, required by %s", subName, POSTOVERFLOWS, i.Name)
   297  		}
   298  	}
   299  
   300  	for _, subName := range i.Contexts {
   301  		if i.hub.GetItem(CONTEXTS, subName) == nil {
   302  			i.hub.logger.Errorf("can't find %s in %s, required by %s", subName, CONTEXTS, i.Name)
   303  		}
   304  	}
   305  
   306  	for _, subName := range i.AppsecConfigs {
   307  		if i.hub.GetItem(APPSEC_CONFIGS, subName) == nil {
   308  			i.hub.logger.Errorf("can't find %s in %s, required by %s", subName, APPSEC_CONFIGS, i.Name)
   309  		}
   310  	}
   311  
   312  	for _, subName := range i.AppsecRules {
   313  		if i.hub.GetItem(APPSEC_RULES, subName) == nil {
   314  			i.hub.logger.Errorf("can't find %s in %s, required by %s", subName, APPSEC_RULES, i.Name)
   315  		}
   316  	}
   317  
   318  	for _, subName := range i.Collections {
   319  		if i.hub.GetItem(COLLECTIONS, subName) == nil {
   320  			i.hub.logger.Errorf("can't find %s in %s, required by %s", subName, COLLECTIONS, i.Name)
   321  		}
   322  	}
   323  }
   324  
   325  // Ancestors returns a slice of items (typically collections) that have this item as a direct or indirect dependency.
   326  func (i *Item) Ancestors() []*Item {
   327  	ret := make([]*Item, 0)
   328  
   329  	for _, parentName := range i.State.BelongsToCollections {
   330  		parent := i.hub.GetItem(COLLECTIONS, parentName)
   331  		if parent == nil {
   332  			continue
   333  		}
   334  
   335  		ret = append(ret, parent)
   336  	}
   337  
   338  	return ret
   339  }
   340  
   341  // descendants returns a list of all (direct or indirect) dependencies of the item.
   342  func (i *Item) descendants() ([]*Item, error) {
   343  	var collectSubItems func(item *Item, visited map[*Item]bool, result *[]*Item) error
   344  
   345  	collectSubItems = func(item *Item, visited map[*Item]bool, result *[]*Item) error {
   346  		if item == nil {
   347  			return nil
   348  		}
   349  
   350  		if visited[item] {
   351  			return nil
   352  		}
   353  
   354  		visited[item] = true
   355  
   356  		for _, subItem := range item.SubItems() {
   357  			if subItem == i {
   358  				return fmt.Errorf("circular dependency detected: %s depends on %s", item.Name, i.Name)
   359  			}
   360  
   361  			*result = append(*result, subItem)
   362  
   363  			err := collectSubItems(subItem, visited, result)
   364  			if err != nil {
   365  				return err
   366  			}
   367  		}
   368  
   369  		return nil
   370  	}
   371  
   372  	ret := []*Item{}
   373  	visited := map[*Item]bool{}
   374  
   375  	err := collectSubItems(i, visited, &ret)
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  
   380  	return ret, nil
   381  }
   382  
   383  // versionStatus returns the status of the item version compared to the hub version.
   384  // semver requires the 'v' prefix.
   385  func (i *Item) versionStatus() int {
   386  	local, err := semver.NewVersion(i.State.LocalVersion)
   387  	if err != nil {
   388  		return versionUnknown
   389  	}
   390  
   391  	// hub versions are already validated while syncing, ignore errors
   392  	latest, _ := semver.NewVersion(i.Version)
   393  
   394  	if local.LessThan(latest) {
   395  		return versionUpdateAvailable
   396  	}
   397  
   398  	if local.Equal(latest) {
   399  		return versionUpToDate
   400  	}
   401  
   402  	return versionFuture
   403  }
   404  
   405  // validPath returns true if the (relative) path is allowed for the item.
   406  // dirNname: the directory name (ie. crowdsecurity).
   407  // fileName: the filename (ie. apache2-logs.yaml).
   408  func (i *Item) validPath(dirName, fileName string) bool {
   409  	return (dirName+"/"+fileName == i.Name+".yaml") || (dirName+"/"+fileName == i.Name+".yml")
   410  }
   411  
   412  // FQName returns the fully qualified name of the item (ie. parsers:crowdsecurity/apache2-logs).
   413  func (i *Item) FQName() string {
   414  	return fmt.Sprintf("%s:%s", i.Type, i.Name)
   415  }
   416  
   417  // addTaint marks the item as tainted, and propagates the taint to the ancestors.
   418  // sub: the sub-item that caused the taint. May be the item itself!
   419  func (i *Item) addTaint(sub *Item) {
   420  	i.State.Tainted = true
   421  	taintedBy := sub.FQName()
   422  
   423  	idx, ok := slices.BinarySearch(i.State.TaintedBy, taintedBy)
   424  	if ok {
   425  		return
   426  	}
   427  
   428  	// insert the taintedBy in the slice
   429  
   430  	i.State.TaintedBy = append(i.State.TaintedBy, "")
   431  
   432  	copy(i.State.TaintedBy[idx+1:], i.State.TaintedBy[idx:])
   433  
   434  	i.State.TaintedBy[idx] = taintedBy
   435  
   436  	i.hub.logger.Debugf("%s is tainted by %s", i.Name, taintedBy)
   437  
   438  	// propagate the taint to the ancestors
   439  
   440  	for _, ancestor := range i.Ancestors() {
   441  		ancestor.addTaint(sub)
   442  	}
   443  }
   444  
   445  // latestHash() returns the hash of the latest version of the item.
   446  // if it's missing, the index file has been manually modified or got corrupted.
   447  func (i *Item) latestHash() string {
   448  	for k, v := range i.Versions {
   449  		if k == i.Version {
   450  			return v.Digest
   451  		}
   452  	}
   453  
   454  	return ""
   455  }