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 }