github.com/crowdsecurity/crowdsec@v1.6.1/pkg/cwhub/sync.go (about) 1 package cwhub 2 3 import ( 4 "crypto/sha256" 5 "encoding/hex" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "slices" 11 "sort" 12 "strings" 13 14 "github.com/Masterminds/semver/v3" 15 "github.com/sirupsen/logrus" 16 "gopkg.in/yaml.v3" 17 ) 18 19 func isYAMLFileName(path string) bool { 20 return strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml") 21 } 22 23 // linkTarget returns the target of a symlink, or empty string if it's dangling. 24 func linkTarget(path string, logger *logrus.Logger) (string, error) { 25 hubpath, err := os.Readlink(path) 26 if err != nil { 27 return "", fmt.Errorf("unable to read symlink: %s", path) 28 } 29 30 logger.Tracef("symlink %s -> %s", path, hubpath) 31 32 _, err = os.Lstat(hubpath) 33 if os.IsNotExist(err) { 34 logger.Warningf("link target does not exist: %s -> %s", path, hubpath) 35 return "", nil 36 } 37 38 return hubpath, nil 39 } 40 41 func getSHA256(filepath string) (string, error) { 42 f, err := os.Open(filepath) 43 if err != nil { 44 return "", fmt.Errorf("unable to open '%s': %w", filepath, err) 45 } 46 47 defer f.Close() 48 49 h := sha256.New() 50 if _, err := io.Copy(h, f); err != nil { 51 return "", fmt.Errorf("unable to calculate sha256 of '%s': %w", filepath, err) 52 } 53 54 return hex.EncodeToString(h.Sum(nil)), nil 55 } 56 57 // information used to create a new Item, from a file path. 58 type itemFileInfo struct { 59 inhub bool 60 fname string 61 stage string 62 ftype string 63 fauthor string 64 } 65 66 func (h *Hub) getItemFileInfo(path string, logger *logrus.Logger) (*itemFileInfo, error) { 67 var ret *itemFileInfo 68 69 hubDir := h.local.HubDir 70 installDir := h.local.InstallDir 71 72 subs := strings.Split(path, string(os.PathSeparator)) 73 74 logger.Tracef("path:%s, hubdir:%s, installdir:%s", path, hubDir, installDir) 75 logger.Tracef("subs:%v", subs) 76 // we're in hub (~/.hub/hub/) 77 if strings.HasPrefix(path, hubDir) { 78 logger.Tracef("in hub dir") 79 80 // .../hub/parsers/s00-raw/crowdsec/skip-pretag.yaml 81 // .../hub/scenarios/crowdsec/ssh_bf.yaml 82 // .../hub/profiles/crowdsec/linux.yaml 83 if len(subs) < 4 { 84 return nil, fmt.Errorf("path is too short: %s (%d)", path, len(subs)) 85 } 86 87 ret = &itemFileInfo{ 88 inhub: true, 89 fname: subs[len(subs)-1], 90 fauthor: subs[len(subs)-2], 91 stage: subs[len(subs)-3], 92 ftype: subs[len(subs)-4], 93 } 94 } else if strings.HasPrefix(path, installDir) { // we're in install /etc/crowdsec/<type>/... 95 logger.Tracef("in install dir") 96 97 if len(subs) < 3 { 98 return nil, fmt.Errorf("path is too short: %s (%d)", path, len(subs)) 99 } 100 // .../config/parser/stage/file.yaml 101 // .../config/postoverflow/stage/file.yaml 102 // .../config/scenarios/scenar.yaml 103 // .../config/collections/linux.yaml //file is empty 104 ret = &itemFileInfo{ 105 inhub: false, 106 fname: subs[len(subs)-1], 107 stage: subs[len(subs)-2], 108 ftype: subs[len(subs)-3], 109 fauthor: "", 110 } 111 } else { 112 return nil, fmt.Errorf("file '%s' is not from hub '%s' nor from the configuration directory '%s'", path, hubDir, installDir) 113 } 114 115 logger.Tracef("stage:%s ftype:%s", ret.stage, ret.ftype) 116 117 if ret.ftype != PARSERS && ret.ftype != POSTOVERFLOWS { 118 if !slices.Contains(ItemTypes, ret.stage) { 119 return nil, fmt.Errorf("unknown configuration type for file '%s'", path) 120 } 121 122 ret.ftype = ret.stage 123 ret.stage = "" 124 } 125 126 logger.Tracef("CORRECTED [%s] by [%s] in stage [%s] of type [%s]", ret.fname, ret.fauthor, ret.stage, ret.ftype) 127 128 return ret, nil 129 } 130 131 // sortedVersions returns the input data, sorted in reverse order (new, old) by semver. 132 func sortedVersions(raw []string) ([]string, error) { 133 vs := make([]*semver.Version, len(raw)) 134 135 for idx, r := range raw { 136 v, err := semver.NewVersion(r) 137 if err != nil { 138 return nil, fmt.Errorf("%s: %w", r, err) 139 } 140 141 vs[idx] = v 142 } 143 144 sort.Sort(sort.Reverse(semver.Collection(vs))) 145 146 ret := make([]string, len(vs)) 147 for idx, v := range vs { 148 ret[idx] = v.Original() 149 } 150 151 return ret, nil 152 } 153 154 func newLocalItem(h *Hub, path string, info *itemFileInfo) (*Item, error) { 155 type localItemName struct { 156 Name string `yaml:"name"` 157 } 158 159 _, fileName := filepath.Split(path) 160 161 item := &Item{ 162 hub: h, 163 Name: info.fname, 164 Stage: info.stage, 165 Type: info.ftype, 166 FileName: fileName, 167 State: ItemState{ 168 LocalPath: path, 169 Installed: true, 170 UpToDate: true, 171 }, 172 } 173 174 // try to read the name from the file 175 itemName := localItemName{} 176 177 itemContent, err := os.ReadFile(path) 178 if err != nil { 179 return nil, fmt.Errorf("failed to read %s: %w", path, err) 180 } 181 182 err = yaml.Unmarshal(itemContent, &itemName) 183 if err != nil { 184 return nil, fmt.Errorf("failed to unmarshal %s: %w", path, err) 185 } 186 187 if itemName.Name != "" { 188 item.Name = itemName.Name 189 } 190 191 return item, nil 192 } 193 194 func (h *Hub) itemVisit(path string, f os.DirEntry, err error) error { 195 hubpath := "" 196 197 if err != nil { 198 h.logger.Debugf("while syncing hub dir: %s", err) 199 // there is a path error, we ignore the file 200 return nil 201 } 202 203 // only happens if the current working directory was removed (!) 204 path, err = filepath.Abs(path) 205 if err != nil { 206 return err 207 } 208 209 // we only care about YAML files 210 if f == nil || f.IsDir() || !isYAMLFileName(f.Name()) { 211 return nil 212 } 213 214 info, err := h.getItemFileInfo(path, h.logger) 215 if err != nil { 216 return err 217 } 218 219 // non symlinks are local user files or hub files 220 if f.Type()&os.ModeSymlink == 0 { 221 h.logger.Tracef("%s is not a symlink", path) 222 223 if !info.inhub { 224 h.logger.Tracef("%s is a local file, skip", path) 225 226 item, err := newLocalItem(h, path, info) 227 if err != nil { 228 return err 229 } 230 231 h.addItem(item) 232 233 return nil 234 } 235 } else { 236 hubpath, err = linkTarget(path, h.logger) 237 if err != nil { 238 return err 239 } 240 241 if hubpath == "" { 242 // target does not exist, the user might have removed the file 243 // or switched to a hub branch without it 244 return nil 245 } 246 } 247 248 // try to find which configuration item it is 249 h.logger.Tracef("check [%s] of %s", info.fname, info.ftype) 250 251 for _, item := range h.GetItemMap(info.ftype) { 252 if info.fname != item.FileName { 253 continue 254 } 255 256 if item.Stage != info.stage { 257 continue 258 } 259 260 // if we are walking hub dir, just mark present files as downloaded 261 if info.inhub { 262 // wrong author 263 if info.fauthor != item.Author { 264 continue 265 } 266 267 // not the item we're looking for 268 if !item.validPath(info.fauthor, info.fname) { 269 continue 270 } 271 272 src, err := item.downloadPath() 273 if err != nil { 274 return err 275 } 276 277 if path == src { 278 h.logger.Tracef("marking %s as downloaded", item.Name) 279 item.State.Downloaded = true 280 } 281 } else if !hasPathSuffix(hubpath, item.RemotePath) { 282 // wrong file 283 // <type>/<stage>/<author>/<name>.yaml 284 continue 285 } 286 287 err := item.setVersionState(path, info.inhub) 288 if err != nil { 289 return err 290 } 291 292 return nil 293 } 294 295 h.logger.Infof("Ignoring file %s of type %s", path, info.ftype) 296 297 return nil 298 } 299 300 // checkSubItemVersions checks for the presence, taint and version state of sub-items. 301 func (i *Item) checkSubItemVersions() []string { 302 warn := make([]string, 0) 303 304 if !i.HasSubItems() { 305 return warn 306 } 307 308 if i.versionStatus() != versionUpToDate { 309 i.hub.logger.Debugf("%s dependencies not checked: not up-to-date", i.Name) 310 return warn 311 } 312 313 // ensure all the sub-items are installed, or tag the parent as tainted 314 i.hub.logger.Tracef("checking submembers of %s installed:%t", i.Name, i.State.Installed) 315 316 for _, sub := range i.SubItems() { 317 i.hub.logger.Tracef("check %s installed:%t", sub.Name, sub.State.Installed) 318 319 if !i.State.Installed { 320 continue 321 } 322 323 if w := sub.checkSubItemVersions(); len(w) > 0 { 324 if sub.State.Tainted { 325 i.addTaint(sub) 326 warn = append(warn, fmt.Sprintf("%s is tainted by %s", i.Name, sub.FQName())) 327 } 328 329 warn = append(warn, w...) 330 331 continue 332 } 333 334 if sub.State.Tainted { 335 i.addTaint(sub) 336 warn = append(warn, fmt.Sprintf("%s is tainted by %s", i.Name, sub.FQName())) 337 338 continue 339 } 340 341 if !sub.State.Installed && i.State.Installed { 342 i.addTaint(sub) 343 warn = append(warn, fmt.Sprintf("%s is tainted by missing %s", i.Name, sub.FQName())) 344 345 continue 346 } 347 348 if !sub.State.UpToDate { 349 i.State.UpToDate = false 350 warn = append(warn, fmt.Sprintf("%s is tainted by outdated %s", i.Name, sub.FQName())) 351 352 continue 353 } 354 355 i.hub.logger.Tracef("checking for %s - tainted:%t uptodate:%t", sub.Name, i.State.Tainted, i.State.UpToDate) 356 } 357 358 return warn 359 } 360 361 // syncDir scans a directory for items, and updates the Hub state accordingly. 362 func (h *Hub) syncDir(dir string) error { 363 // For each, scan PARSERS, POSTOVERFLOWS... and COLLECTIONS last 364 for _, scan := range ItemTypes { 365 // cpath: top-level item directory, either downloaded or installed items. 366 // i.e. /etc/crowdsec/parsers, /etc/crowdsec/hub/parsers, ... 367 cpath, err := filepath.Abs(fmt.Sprintf("%s/%s", dir, scan)) 368 if err != nil { 369 h.logger.Errorf("failed %s: %s", cpath, err) 370 continue 371 } 372 373 // explicit check for non existing directory, avoid spamming log.Debug 374 if _, err = os.Stat(cpath); os.IsNotExist(err) { 375 h.logger.Tracef("directory %s doesn't exist, skipping", cpath) 376 continue 377 } 378 379 if err = filepath.WalkDir(cpath, h.itemVisit); err != nil { 380 return err 381 } 382 } 383 384 return nil 385 } 386 387 // insert a string in a sorted slice, case insensitive, and return the new slice. 388 func insertInOrderNoCase(sl []string, value string) []string { 389 i := sort.Search(len(sl), func(i int) bool { 390 return strings.ToLower(sl[i]) >= strings.ToLower(value) 391 }) 392 393 return append(sl[:i], append([]string{value}, sl[i:]...)...) 394 } 395 396 func removeDuplicates(sl []string) []string { 397 seen := make(map[string]struct{}, len(sl)) 398 j := 0 399 400 for _, v := range sl { 401 if _, ok := seen[v]; ok { 402 continue 403 } 404 405 seen[v] = struct{}{} 406 sl[j] = v 407 j++ 408 } 409 410 return sl[:j] 411 } 412 413 // localSync updates the hub state with downloaded, installed and local items. 414 func (h *Hub) localSync() error { 415 err := h.syncDir(h.local.InstallDir) 416 if err != nil { 417 return fmt.Errorf("failed to scan %s: %w", h.local.InstallDir, err) 418 } 419 420 if err = h.syncDir(h.local.HubDir); err != nil { 421 return fmt.Errorf("failed to scan %s: %w", h.local.HubDir, err) 422 } 423 424 warnings := make([]string, 0) 425 426 for _, item := range h.GetItemMap(COLLECTIONS) { 427 // check for cyclic dependencies 428 subs, err := item.descendants() 429 if err != nil { 430 return err 431 } 432 433 // populate the sub- and sub-sub-items with the collections they belong to 434 for _, sub := range subs { 435 sub.State.BelongsToCollections = insertInOrderNoCase(sub.State.BelongsToCollections, item.Name) 436 } 437 438 if !item.State.Installed { 439 continue 440 } 441 442 vs := item.versionStatus() 443 switch vs { 444 case versionUpToDate: // latest 445 if w := item.checkSubItemVersions(); len(w) > 0 { 446 warnings = append(warnings, w...) 447 } 448 case versionUpdateAvailable: // not up-to-date 449 warnings = append(warnings, fmt.Sprintf("update for collection %s available (currently:%s, latest:%s)", item.Name, item.State.LocalVersion, item.Version)) 450 case versionFuture: 451 warnings = append(warnings, fmt.Sprintf("collection %s is in the future (currently:%s, latest:%s)", item.Name, item.State.LocalVersion, item.Version)) 452 case versionUnknown: 453 if !item.State.IsLocal() { 454 warnings = append(warnings, fmt.Sprintf("collection %s is tainted by local changes (latest:%s)", item.Name, item.Version)) 455 } 456 } 457 458 h.logger.Debugf("installed (%s) - status: %d | installed: %s | latest: %s | full: %+v", item.Name, vs, item.State.LocalVersion, item.Version, item.Versions) 459 } 460 461 h.Warnings = removeDuplicates(warnings) 462 463 return nil 464 } 465 466 func (i *Item) setVersionState(path string, inhub bool) error { 467 var err error 468 469 i.State.LocalHash, err = getSHA256(path) 470 if err != nil { 471 return fmt.Errorf("failed to get sha256 of %s: %w", path, err) 472 } 473 474 // let's reverse sort the versions to deal with hash collisions (#154) 475 versions := make([]string, 0, len(i.Versions)) 476 for k := range i.Versions { 477 versions = append(versions, k) 478 } 479 480 versions, err = sortedVersions(versions) 481 if err != nil { 482 return fmt.Errorf("while syncing %s %s: %w", i.Type, i.FileName, err) 483 } 484 485 i.State.LocalVersion = "?" 486 487 for _, version := range versions { 488 if i.Versions[version].Digest == i.State.LocalHash { 489 i.State.LocalVersion = version 490 break 491 } 492 } 493 494 if i.State.LocalVersion == "?" { 495 i.hub.logger.Tracef("got tainted match for %s: %s", i.Name, path) 496 497 if !inhub { 498 i.State.LocalPath = path 499 i.State.Installed = true 500 } 501 502 i.State.UpToDate = false 503 i.addTaint(i) 504 505 return nil 506 } 507 508 // we got an exact match, update struct 509 510 i.State.Downloaded = true 511 512 if !inhub { 513 i.hub.logger.Tracef("found exact match for %s, version is %s, latest is %s", i.Name, i.State.LocalVersion, i.Version) 514 i.State.LocalPath = path 515 i.State.Tainted = false 516 // if we're walking the hub, present file doesn't means installed file 517 i.State.Installed = true 518 } 519 520 if i.State.LocalVersion == i.Version { 521 i.hub.logger.Tracef("%s is up-to-date", i.Name) 522 i.State.UpToDate = true 523 } 524 525 return nil 526 }